@rosenfan/matrix 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.claude/skills/matrix/SKILL.md +59 -62
  2. package/.claude/skills/matrix/scripts/matrix_claude.py +11 -11
  3. package/.claude/skills/matrix/scripts/matrix_state.py +53 -53
  4. package/.claude/skills/matrix-archive/SKILL.md +36 -33
  5. package/.claude/skills/matrix-build/SKILL.md +28 -23
  6. package/.claude/skills/matrix-claude/SKILL.md +13 -11
  7. package/.claude/skills/matrix-design/SKILL.md +32 -26
  8. package/.claude/skills/matrix-hotfix/SKILL.md +1 -1
  9. package/.claude/skills/matrix-open/SKILL.md +27 -15
  10. package/.claude/skills/matrix-status/SKILL.md +5 -5
  11. package/.claude/skills/matrix-tweak/SKILL.md +1 -1
  12. package/.claude/skills/matrix-verify/SKILL.md +35 -17
  13. package/README.md +131 -96
  14. package/README.zh-CN.md +127 -92
  15. package/assets/skills-zh-CN/matrix/SKILL.md +75 -72
  16. package/assets/skills-zh-CN/matrix-archive/SKILL.md +36 -33
  17. package/assets/skills-zh-CN/matrix-build/SKILL.md +41 -37
  18. package/assets/skills-zh-CN/matrix-claude/SKILL.md +33 -31
  19. package/assets/skills-zh-CN/matrix-design/SKILL.md +13 -7
  20. package/assets/skills-zh-CN/matrix-hotfix/SKILL.md +1 -1
  21. package/assets/skills-zh-CN/matrix-open/SKILL.md +18 -6
  22. package/assets/skills-zh-CN/matrix-status/SKILL.md +5 -5
  23. package/assets/skills-zh-CN/matrix-tweak/SKILL.md +1 -1
  24. package/assets/skills-zh-CN/matrix-verify/SKILL.md +27 -9
  25. package/package.json +2 -2
  26. package/src/catalog.js +29 -18
  27. package/src/cli.js +150 -39
  28. package/src/distribution.js +268 -114
  29. package/src/installer.js +10 -10
  30. package/src/matt-catalog.mjs +67 -0
  31. package/src/matt.js +115 -33
  32. package/src/messages.js +18 -14
  33. package/src/runtime-resolver.js +76 -0
  34. package/src/self-update.js +81 -0
  35. package/src/ui.js +1 -1
  36. package/src/workflow-transaction.js +622 -618
  37. package/src/workflow.js +949 -464
@@ -2,14 +2,16 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- import {
5
+ import {
6
6
  MATRIX_SKILLS, MATT_SKILLS, UNMANAGED_MATT_SKILLS, PLATFORM_IDS, PLATFORMS, hashDirectory,
7
- installationRoot, manifestPath, releaseCatalog, scopeBase, sourceSkillsRoot, workflowRuntimeSource, workflowTransactionSource
8
- } from "./catalog.js";
9
- import { createMattAdapter } from "./matt.js";
10
- import { text } from "./messages.js";
7
+ installationRoot, manifestPath, mattCatalogSource, releaseCatalog, scopeBase, sourceSkillsRoot, workflowRuntimeSource, workflowTransactionSource
8
+ } from "./catalog.js";
9
+ import { createMattAdapter, mattReceiptPath } from "./matt.js";
10
+ import { MATT_CATALOG_DIGEST, MATT_COMPATIBILITY, MATT_CONTENT_HASHES } from "./matt-catalog.mjs";
11
+ import { text } from "./messages.js";
11
12
 
12
- const VERSION = 2;
13
+ const VERSION = 3;
14
+ const MATT_OPERATIONS = new Set(["none", "missing", "readonly"]);
13
15
  const digest = (value) => crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
14
16
  const exists = (value) => fs.existsSync(value);
15
17
  const pathExists = (value) => { try { fs.lstatSync(value); return true; } catch { return false; } };
@@ -22,36 +24,86 @@ const writeJson = (file, value) => {
22
24
  fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`);
23
25
  fs.renameSync(temporary, file);
24
26
  };
25
- const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; } };
26
- const readConfig = (file) => {
27
- try {
28
- return Object.fromEntries(fs.readFileSync(file, "utf8").split(/\r?\n/).filter((line) => line.includes(":") && !line.startsWith(" ")).map((line) => {
29
- const [key, ...value] = line.split(":");
30
- return [key.trim(), value.join(":").trim()];
31
- }));
32
- } catch { return {}; }
33
- };
34
- const configContent = (existing, defaultOrchestration, scope) => {
35
- const autoTransition = existing.auto_transition === "false" ? "false" : "true";
36
- return `schema: matrix/config/v1\nauto_transition: ${autoTransition}\ndefault_orchestration: ${defaultOrchestration}\ninstallation_scope: ${scope}\n`;
37
- };
27
+ const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; } };
28
+ const readConfig = (file) => {
29
+ try {
30
+ return Object.fromEntries(fs.readFileSync(file, "utf8").split(/\r?\n/).filter((line) => line.includes(":") && !line.startsWith(" ")).map((line) => {
31
+ const [key, ...value] = line.split(":");
32
+ return [key.trim(), value.join(":").trim()];
33
+ }));
34
+ } catch { return {}; }
35
+ };
36
+ const configContent = (existing, defaultOrchestration, scope, language) => {
37
+ const values = {
38
+ ...existing,
39
+ schema: "matrix/config/v1",
40
+ auto_transition: existing.auto_transition === "false" ? "false" : "true",
41
+ default_orchestration: defaultOrchestration,
42
+ installation_scope: scope,
43
+ language
44
+ };
45
+ const preferred = ["schema", "auto_transition", "default_orchestration", "installation_scope", "language"];
46
+ const keys = [...preferred, ...Object.keys(values).filter((key) => !preferred.includes(key)).sort()];
47
+ return `${keys.map((key) => `${key}: ${values[key]}`).join("\n")}\n`;
48
+ };
38
49
  const removeEntry = (value) => { if (pathExists(value)) fs.rmSync(value, { recursive: true, force: true }); };
39
50
  const removeStaging = (stagingRoot) => {
40
51
  removeEntry(stagingRoot);
41
52
  const parent = path.dirname(stagingRoot);
42
53
  if (exists(parent) && fs.readdirSync(parent).length === 0) fs.rmdirSync(parent);
43
54
  };
44
- const compareVersions = (left, right) => {
55
+ const compareVersions = (left, right) => {
45
56
  const parse = (value) => String(value ?? "0.0.0").split("-")[0].split(".").map((part) => Number(part) || 0);
46
57
  const a = parse(left); const b = parse(right);
47
58
  for (let index = 0; index < Math.max(a.length, b.length); index += 1) if ((a[index] ?? 0) !== (b[index] ?? 0)) return (a[index] ?? 0) > (b[index] ?? 0) ? 1 : -1;
48
59
  return 0;
49
60
  };
50
- const skillPresent = (root, skill) => exists(path.join(root, skill, "SKILL.md"));
51
- const isBrokenLink = (value) => { try { return fs.lstatSync(value).isSymbolicLink() && !exists(value); } catch { return false; } };
61
+ const skillPresent = (root, skill) => exists(path.join(root, skill, "SKILL.md"));
62
+ const isBrokenLink = (value) => { try { return fs.lstatSync(value).isSymbolicLink() && !exists(value); } catch { return false; } };
63
+ const mattObservationMatchesRelease = (observation, contentHashes) => MATT_SKILLS.every((skill) => observation.matt.skills?.[skill]?.hash === contentHashes[skill]);
64
+ const mattObservationMatchesReceipt = (observation) => MATT_SKILLS.every((skill) => observation.matt.classifications?.[skill]?.state === "matching");
65
+ const verifyMattCandidate = (candidate, actions, contentHashes) => {
66
+ for (const action of actions) for (const skill of action.matt.skills) {
67
+ const source = path.join(candidate.platforms[action.platform], skill);
68
+ if (!exists(path.join(source, "SKILL.md")) || hashDirectory(source) !== contentHashes[skill]) {
69
+ throw new Error(`Matt candidate content did not match the reviewed release: ${action.platform}/${skill}`);
70
+ }
71
+ }
72
+ };
73
+ const localizedState = (language, state) => text(language, ({
74
+ missing: "stateMissing", matching: "stateMatching", complete: "stateComplete", "user-modified": "stateUserModified",
75
+ partial: "statePartial", damaged: "statePartial", newer: "stateMatching", "outdated-unchanged": "stateOutdated",
76
+ "safe-update": "stateSafeUpdate", adoptable: "stateAdoptable", unreadable: "stateUnreadable"
77
+ })[state] ?? state);
78
+ const localizedAction = (language, action) => text(language, ({
79
+ install: "actionInstall", keep: "actionKeep", "backup-replace": "actionBackupReplace", repair: "actionRepair", blocked: "actionBlocked"
80
+ })[action] ?? action);
52
81
 
53
82
  function readManifest(intent) { return readJson(manifestPath(intent.scope, intent.projectRoot, intent.home)); }
54
83
 
84
+ export function installedIntent({ projectRoot = process.cwd(), home = os.homedir() } = {}) {
85
+ const resolvedProjectRoot = path.resolve(projectRoot);
86
+ const config = readConfig(path.join(resolvedProjectRoot, ".matrix", "config.yaml"));
87
+ const projectManifest = readJson(manifestPath("project", resolvedProjectRoot, home));
88
+ const scope = projectManifest ? "project" : config.installation_scope === "global" ? "global" : "project";
89
+ const manifest = projectManifest ?? readJson(manifestPath(scope, resolvedProjectRoot, home));
90
+ const platforms = Object.keys(manifest?.platforms ?? {}).filter((id) => PLATFORM_IDS.includes(id));
91
+ if (!manifest || !platforms.length) return { ok: false, code: "MANIFEST_MISSING", scope, config };
92
+ const language = ["en", "zh-CN"].includes(manifest.language) ? manifest.language : (["en", "zh-CN"].includes(config.language) ? config.language : "en");
93
+ const defaultOrchestration = ["prim", "arch"].includes(manifest.orchestration?.default) ? manifest.orchestration.default : "prim";
94
+ const matt = "readonly";
95
+ return {
96
+ ok: true,
97
+ intent: {
98
+ projectRoot: resolvedProjectRoot, home, scope, platforms, language,
99
+ mode: ["copy", "symlink"].includes(manifest.mode) ? manifest.mode : "copy",
100
+ matt, defaultOrchestration, nonInteractive: true, matrixPolicy: "safe", dryRun: false
101
+ },
102
+ manifest,
103
+ config
104
+ };
105
+ }
106
+
55
107
  function detectPlatforms(projectRoot, scope, home) {
56
108
  const bases = [scopeBase(scope, projectRoot, home)];
57
109
  if (scope === "project") bases.push(home);
@@ -63,16 +115,33 @@ function normalizeIntent(raw) {
63
115
  const platforms = [...new Set(raw.platforms ?? [])];
64
116
  if (!["project", "global"].includes(scope)) return { error: "INVALID_INTENT", detail: "scope" };
65
117
  if (!["copy", "symlink"].includes(raw.mode ?? "copy")) return { error: "INVALID_INTENT", detail: "mode" };
66
- if (!["en", "zh-CN"].includes(raw.language ?? "en")) return { error: "INVALID_INTENT", detail: "language" };
118
+ if (!["en", "zh-CN"].includes(raw.language ?? "en")) return { error: "INVALID_INTENT", detail: "language" };
67
119
  if (raw.defaultOrchestration != null && !["prim", "arch"].includes(raw.defaultOrchestration)) return { error: "INVALID_INTENT", detail: "default-orchestration" };
120
+ if (!MATT_OPERATIONS.has(raw.matt ?? "none")) return { error: "INVALID_INTENT", detail: "matt" };
121
+ if (raw.mattPolicy != null && !["preserve", "replace"].includes(raw.mattPolicy)) return { error: "INVALID_INTENT", detail: "matt-policy" };
68
122
  if (platforms.some((id) => !PLATFORM_IDS.includes(id))) return { error: "UNVERIFIED_PLATFORM" };
69
123
  return { projectRoot: path.resolve(raw.projectRoot ?? process.cwd()), home: raw.home ?? os.homedir(), scope, platforms,
70
124
  language: raw.language ?? "en", mode: raw.mode ?? "copy", matrixPolicy: raw.matrixPolicy ?? "safe",
71
- matt: raw.matt ?? "none", defaultOrchestration: raw.defaultOrchestration ?? null,
72
- nonInteractive: Boolean(raw.nonInteractive), dryRun: Boolean(raw.dryRun) };
125
+ matt: raw.matt ?? "none", mattPolicy: raw.mattPolicy ?? "preserve", defaultOrchestration: raw.defaultOrchestration ?? null,
126
+ nonInteractive: Boolean(raw.nonInteractive), dryRun: Boolean(raw.dryRun) };
73
127
  }
74
-
75
- function observePlatform(intent, catalog, manifest, platformId) {
128
+
129
+ export function classifyMattTarget({ present, readable = true, actual, expected, recorded, receiptTrusted = false }) {
130
+ if (!present) return "missing";
131
+ if (!readable || !actual) return "unreadable";
132
+ if (expected && actual === expected) return receiptTrusted && recorded === actual ? "matching" : "adoptable";
133
+ if (recorded && actual === recorded) return "safe-update";
134
+ return "user-modified";
135
+ }
136
+
137
+ function trustedMattReceipt(receipt) {
138
+ return Boolean(receipt
139
+ && receipt.compatibility?.release === MATT_COMPATIBILITY.release
140
+ && receipt.compatibility?.commit === MATT_COMPATIBILITY.commit
141
+ && receipt.catalogDigest === MATT_CATALOG_DIGEST);
142
+ }
143
+
144
+ function observePlatform(intent, catalog, manifest, mattReceipt, platformId, contentHashes) {
76
145
  const platform = PLATFORMS[platformId]; const base = scopeBase(intent.scope, intent.projectRoot, intent.home);
77
146
  const root = platform.skillRoot(base); const records = manifest?.platforms?.[platformId]?.skills ?? {};
78
147
  const skills = MATRIX_SKILLS.map((skill) => {
@@ -85,22 +154,35 @@ function observePlatform(intent, catalog, manifest, platformId) {
85
154
  if (actual === recorded) return { skill, target, state: compareVersions(manifest?.catalogVersion, catalog.version) > 0 ? "newer" : "outdated-unchanged", actual };
86
155
  return { skill, target, state: "user-modified", actual };
87
156
  });
88
- const states = new Set(skills.map((item) => item.state));
89
- const matrix = states.size === 1 && states.has("matching") ? "matching" : states.has("user-modified") ? "user-modified"
90
- : states.has("missing") && states.size === 1 ? "missing" : states.has("newer") ? "newer"
91
- : states.has("damaged") ? "damaged" : states.has("outdated-unchanged") ? "outdated-unchanged" : "partial";
92
- const globalRoot = platform.skillRoot(intent.home);
93
- const mattPresent = MATT_SKILLS.filter((skill) => skillPresent(root, skill));
94
- const globalMatt = intent.scope === "project" ? MATT_SKILLS.filter((skill) => skillPresent(globalRoot, skill)) : [];
95
- const availableMatt = new Set([...mattPresent, ...globalMatt]); const missing = MATT_SKILLS.filter((skill) => !availableMatt.has(skill));
96
- const inherited = globalMatt.filter((skill) => !mattPresent.includes(skill)).length;
97
- const managedSkills = Object.fromEntries([...availableMatt].map((skill) => {
98
- const target = mattPresent.includes(skill) ? path.join(root, skill) : path.join(globalRoot, skill);
99
- return [skill, { root: target, hash: hashDirectory(target) }];
157
+ const states = new Set(skills.map((item) => item.state));
158
+ const matrix = states.size === 1 && states.has("matching") ? "matching" : states.has("user-modified") ? "user-modified"
159
+ : states.has("missing") && states.size === 1 ? "missing" : states.has("newer") ? "newer"
160
+ : states.has("damaged") ? "damaged" : states.has("outdated-unchanged") ? "outdated-unchanged" : "partial";
161
+ const mattRoot = platform.skillRoot(intent.projectRoot);
162
+ const globalRoot = platform.skillRoot(intent.home);
163
+ const mattPresent = MATT_SKILLS.filter((skill) => skillPresent(mattRoot, skill));
164
+ const globalMatt = MATT_SKILLS.filter((skill) => skillPresent(globalRoot, skill));
165
+ const missing = MATT_SKILLS.filter((skill) => !mattPresent.includes(skill));
166
+ const ignoredGlobal = globalMatt.filter((skill) => !mattPresent.includes(skill)).length;
167
+ const receiptRecords = mattReceipt?.platforms?.[platformId]?.skills ?? {};
168
+ const legacyRecords = manifest?.platforms?.[platformId]?.matt?.skills ?? {};
169
+ const receiptIsTrusted = trustedMattReceipt(mattReceipt);
170
+ const classifications = Object.fromEntries(MATT_SKILLS.map((skill) => {
171
+ const target = path.join(mattRoot, skill);
172
+ const present = pathExists(target);
173
+ let actual = null; let readable = true;
174
+ if (present) {
175
+ try { actual = hashDirectory(target); } catch { readable = false; }
176
+ }
177
+ const recorded = receiptRecords[skill]?.hash ?? legacyRecords[skill]?.hash ?? null;
178
+ const state = classifyMattTarget({ present, readable, actual, expected: contentHashes[skill], recorded, receiptTrusted: receiptIsTrusted });
179
+ return [skill, { state, actual, expected: contentHashes[skill], recorded }];
100
180
  }));
101
- const unmanaged = UNMANAGED_MATT_SKILLS.filter((skill) => skillPresent(root, skill) || (intent.scope === "project" && skillPresent(globalRoot, skill)));
102
- return { id: platformId, name: platform.name, root, skills, matrix, matt: { present: mattPresent.length, missing, total: MATT_SKILLS.length, inherited,
103
- state: availableMatt.size === MATT_SKILLS.length ? "complete" : availableMatt.size ? "partial" : "missing", skills: managedSkills, unmanaged } };
181
+ const managedSkills = Object.fromEntries(Object.entries(classifications).filter(([, item]) => item.actual).map(([skill, item]) => [skill, { root: path.join(mattRoot, skill), hash: item.actual }]));
182
+ const classificationCounts = Object.values(classifications).reduce((counts, item) => ({ ...counts, [item.state]: (counts[item.state] ?? 0) + 1 }), {});
183
+ const unmanaged = UNMANAGED_MATT_SKILLS.filter((skill) => skillPresent(mattRoot, skill));
184
+ return { id: platformId, name: platform.name, root, skills, matrix, matt: { root: mattRoot, present: mattPresent.length, missing, total: MATT_SKILLS.length, inherited: 0, ignoredGlobal,
185
+ state: mattPresent.length === MATT_SKILLS.length ? "complete" : mattPresent.length ? "partial" : "missing", skills: managedSkills, classifications, classificationCounts, unmanaged } };
104
186
  }
105
187
 
106
188
  function actionFor(observation, policy) {
@@ -111,23 +193,26 @@ function actionFor(observation, policy) {
111
193
  return "repair";
112
194
  }
113
195
 
114
- export function recoverPendingTransaction(intent) {
196
+ export function recoverPendingTransaction(intent, { allowMattWrites = true } = {}) {
115
197
  const file = transactionPath(intent); const journal = readJson(file);
116
198
  if (!journal) return { ok: true, recovered: false };
117
- const restoreMetadata = (entry) => {
118
- if (!entry) return;
119
- if (entry.backup && pathExists(entry.backup)) {
120
- removeEntry(entry.path);
121
- fs.mkdirSync(path.dirname(entry.path), { recursive: true });
122
- fs.renameSync(entry.backup, entry.path);
123
- } else if (entry.written && !entry.existed) removeEntry(entry.path);
124
- };
125
- try {
199
+ if (!allowMattWrites && ((journal.operations ?? []).some((operation) => operation.kind === "matt") || journal.mattReceipt?.written || journal.mattReceipt?.backup)) {
200
+ return { ok: false, code: "RECOVERY_REQUIRED", recovery: `A pending Matt init transaction must be recovered by matrix init --with-mattpocock before Matrix update can continue. Keep ${file}.` };
201
+ }
202
+ const restoreMetadata = (entry) => {
203
+ if (!entry) return;
204
+ if (entry.backup && pathExists(entry.backup)) {
205
+ removeEntry(entry.path);
206
+ fs.mkdirSync(path.dirname(entry.path), { recursive: true });
207
+ fs.renameSync(entry.backup, entry.path);
208
+ } else if (entry.written && !entry.existed) removeEntry(entry.path);
209
+ };
210
+ try {
126
211
  if (journal.state === "committed") {
127
212
  removeStaging(journal.stagingRoot);
128
213
  for (const operation of journal.operations ?? []) removeEntry(operation.backup);
129
- removeEntry(journal.manifest?.backup); removeEntry(journal.config?.backup); removeEntry(file);
130
- return { ok: true, recovered: true, receipt: `Finalized completed Matrix transaction ${journal.id}.` };
214
+ removeEntry(journal.manifest?.backup); removeEntry(journal.config?.backup); removeEntry(journal.mattReceipt?.backup); removeEntry(file);
215
+ return { ok: true, recovered: true, receipt: `Finalized completed Matrix transaction ${journal.id}.` };
131
216
  }
132
217
  for (const operation of [...(journal.operations ?? [])].reverse()) {
133
218
  if (operation.status !== "pending") removeEntry(operation.target);
@@ -135,36 +220,44 @@ export function recoverPendingTransaction(intent) {
135
220
  }
136
221
  restoreMetadata(journal.config);
137
222
  restoreMetadata(journal.manifest);
223
+ restoreMetadata(journal.mattReceipt);
138
224
  removeStaging(journal.stagingRoot); removeEntry(file);
139
225
  return { ok: true, recovered: true, receipt: `Recovered interrupted Matrix transaction ${journal.id}.` };
140
226
  } catch (error) { return { ok: false, code: "RECOVERY_REQUIRED", recovery: `Keep ${file} and restore the paths recorded there before retrying.`, error: error.message }; }
141
227
  }
142
228
 
143
- export function createDistribution({ failAt, mattAdapter = createMattAdapter() } = {}) {
144
- const evaluate = (rawIntent) => {
145
- const intent = normalizeIntent(rawIntent); const language = intent.language ?? rawIntent.language ?? "en";
146
- if (intent.error) return { ok: false, code: intent.error, diagnostics: [{ code: intent.error, message: text(language, "noPlatform") }] };
147
- if (!intent.platforms.length) { const detected = detectPlatforms(intent.projectRoot, intent.scope, intent.home); if (!detected.length) return { ok: false, code: "INPUT_REQUIRED", diagnostics: [{ code: "INPUT_REQUIRED", message: text(language, "noPlatform") }] }; intent.platforms = detected; }
148
- let catalog; try { catalog = releaseCatalog(intent.language); } catch (error) { return { ok: false, code: "PACKAGE_INCOMPLETE", intent, diagnostics: [{ code: "PACKAGE_INCOMPLETE", message: error.message }] }; }
229
+ export function createDistribution({ failAt, mattAdapter = createMattAdapter(), mattCandidateVerifier = null, mattContentHashes = MATT_CONTENT_HASHES } = {}) {
230
+ const candidateVerifier = mattCandidateVerifier ?? ((candidate, actions) => verifyMattCandidate(candidate, actions, mattContentHashes));
231
+ const evaluate = (rawIntent) => {
232
+ const intent = normalizeIntent(rawIntent); const language = intent.language ?? rawIntent.language ?? "en";
233
+ if (intent.error) return { ok: false, code: intent.error, diagnostics: [{ code: intent.error, message: text(language, "noPlatform") }] };
234
+ if (!intent.platforms.length) { const detected = detectPlatforms(intent.projectRoot, intent.scope, intent.home); if (!detected.length) return { ok: false, code: "INPUT_REQUIRED", diagnostics: [{ code: "INPUT_REQUIRED", message: text(language, "noPlatform") }] }; intent.platforms = detected; }
235
+ let catalog; try { catalog = releaseCatalog(intent.language); } catch (error) { return { ok: false, code: "PACKAGE_INCOMPLETE", intent, diagnostics: [{ code: "PACKAGE_INCOMPLETE", message: error.message }] }; }
149
236
  const manifest = readManifest(intent);
237
+ const mattReceipt = readJson(mattReceiptPath(intent.projectRoot));
150
238
  intent.platforms = [...new Set([...Object.keys(manifest?.platforms ?? {}), ...intent.platforms])];
151
- const observations = intent.platforms.map((id) => observePlatform(intent, catalog, manifest, id));
152
- const existingConfig = readConfig(configPath(intent));
153
- const existingDefault = ["prim", "arch"].includes(existingConfig.default_orchestration) ? existingConfig.default_orchestration
154
- : ["prim", "arch"].includes(manifest?.orchestration?.default) ? manifest.orchestration.default : null;
155
- intent.defaultOrchestration = intent.defaultOrchestration ?? existingDefault ?? (intent.matt === "missing" && intent.nonInteractive ? "arch" : "prim");
156
- const actions = observations.map((item) => ({ platform: item.id, matrix: actionFor(item, intent.matrixPolicy), matt: item.matt.state === "complete" || intent.matt === "none" ? { kind: "keep", skills: [] } : { kind: "install-missing", skills: item.matt.missing } }));
239
+ const observations = intent.platforms.map((id) => observePlatform(intent, catalog, manifest, mattReceipt, id, mattContentHashes));
240
+ const existingConfig = readConfig(configPath(intent));
241
+ const existingDefault = ["prim", "arch"].includes(existingConfig.default_orchestration) ? existingConfig.default_orchestration
242
+ : ["prim", "arch"].includes(manifest?.orchestration?.default) ? manifest.orchestration.default : null;
243
+ intent.defaultOrchestration = intent.defaultOrchestration ?? existingDefault ?? (intent.matt === "missing" && intent.nonInteractive ? "arch" : "prim");
244
+ const actions = observations.map((item) => ({ platform: item.id, matrix: actionFor(item, intent.matrixPolicy), matt: intent.matt === "missing" ? { kind: "reconcile", skills: [...MATT_SKILLS] } : { kind: "keep", skills: [] } }));
157
245
  const blocked = actions.filter((item) => item.matrix === "blocked");
246
+ const blockedMatt = intent.matt === "missing" && intent.mattPolicy !== "replace"
247
+ ? observations.filter((item) => (item.matt.classificationCounts["user-modified"] ?? 0) > 0 || (item.matt.classificationCounts.unreadable ?? 0) > 0)
248
+ : [];
158
249
  const archMustRemainAvailable = manifest?.orchestration?.available?.includes("arch") || intent.defaultOrchestration === "arch";
159
- const incompleteWithoutInstall = archMustRemainAvailable && actions.some((item, index) => observations[index].matt.state !== "complete" && item.matt.kind !== "install-missing");
160
- const plan = { version: VERSION, intent, catalogDigest: catalog.digest, manifestPath: manifestPath(intent.scope, intent.projectRoot, intent.home), configPath: configPath(intent), actions, observations, preconditionDigest: digest(observations) };
161
- plan.seal = digest({ intent, catalogDigest: plan.catalogDigest, actions, preconditionDigest: plan.preconditionDigest });
162
- const code = blocked.length ? "USER_MODIFIED_BLOCKED" : incompleteWithoutInstall ? "ARCH_INSTALLATION_INCOMPLETE" : "OK";
163
- return { ok: code === "OK", code, intent, observations, actions, plan, diagnostics: [
164
- ...blocked.map((item) => ({ code: "USER_MODIFIED_BLOCKED", platform: item.platform, message: `${item.platform}: existing Matrix assets are not proven managed.` })),
165
- ...(incompleteWithoutInstall ? [{ code: "ARCH_INSTALLATION_INCOMPLETE", message: "Arch requires the complete managed cohort on every configured platform. Retry with --with-mattpocock." }] : [])
166
- ], summary: `${text(intent.language, "preview")}: ${actions.map((item) => `${item.platform}: ${item.matrix}`).join(", ")}` };
167
- };
250
+ const mattReady = trustedMattReceipt(mattReceipt) && observations.every(mattObservationMatchesReceipt);
251
+ const incompleteWithoutInstall = intent.matt !== "readonly" && intent.matt !== "missing" && archMustRemainAvailable && !mattReady;
252
+ const plan = { version: VERSION, intent, catalogDigest: catalog.digest, manifestPath: manifestPath(intent.scope, intent.projectRoot, intent.home), configPath: configPath(intent), actions, observations, preconditionDigest: digest(observations) };
253
+ plan.seal = digest({ intent, catalogDigest: plan.catalogDigest, actions, preconditionDigest: plan.preconditionDigest });
254
+ const code = blocked.length ? "USER_MODIFIED_BLOCKED" : blockedMatt.length ? "MATT_USER_MODIFIED" : incompleteWithoutInstall ? "ARCH_INSTALLATION_INCOMPLETE" : "OK";
255
+ return { ok: code === "OK", code, intent, observations, actions, plan, diagnostics: [
256
+ ...blocked.map((item) => ({ code: "USER_MODIFIED_BLOCKED", platform: item.platform, message: text(intent.language, "matrixModifiedDiagnostic").replace("{platform}", item.platform) })),
257
+ ...blockedMatt.map((item) => ({ code: "MATT_USER_MODIFIED", platform: item.id, states: item.matt.classificationCounts, message: text(intent.language, "mattModifiedDiagnostic").replace("{platform}", item.name) })),
258
+ ...(incompleteWithoutInstall ? [{ code: "ARCH_INSTALLATION_INCOMPLETE", message: text(intent.language, "archIncompleteDiagnostic") }] : [])
259
+ ], summary: `${text(intent.language, "preview")}: ${actions.map((item) => `${item.platform}: ${localizedAction(intent.language, item.matrix)}`).join(", ")}` };
260
+ };
168
261
 
169
262
  return { evaluate,
170
263
  diagnose(rawIntent) {
@@ -173,42 +266,63 @@ export function createDistribution({ failAt, mattAdapter = createMattAdapter() }
173
266
  if (pending) diagnostics.push({ code: "RECOVERY_REQUIRED", message: text(evaluation.intent.language, "transactionPending") });
174
267
  if (!manifest) diagnostics.push({ code: "MANIFEST_MISSING", message: text(evaluation.intent.language, "manifestMissing") });
175
268
  for (const observation of evaluation.observations ?? []) {
176
- diagnostics.push({ code: `MATRIX_${observation.matrix.toUpperCase().replaceAll("-", "_")}`, platform: observation.id, message: `${observation.name}: Matrix is ${observation.matrix}.` });
269
+ diagnostics.push({ code: `MATRIX_${observation.matrix.toUpperCase().replaceAll("-", "_")}`, platform: observation.id, message: text(evaluation.intent.language, "matrixStateDiagnostic").replace("{platform}", observation.name).replace("{state}", localizedState(evaluation.intent.language, observation.matrix)) });
177
270
  const matrixRoot = path.join(observation.root, "matrix");
178
- if (pathExists(matrixRoot) && (!exists(matrixRoot) || !exists(path.join(matrixRoot, "scripts", "matrix-runtime.mjs")) || !exists(path.join(matrixRoot, "scripts", "workflow-transaction.js")))) diagnostics.push({ code: "RUNTIME_MISSING", platform: observation.id, message: `${observation.name}: ${text(evaluation.intent.language, "runtimeMissing")}` });
271
+ if (pathExists(matrixRoot) && (!exists(matrixRoot) || !exists(path.join(matrixRoot, "scripts", "matrix-runtime.mjs")) || !exists(path.join(matrixRoot, "scripts", "workflow-transaction.js")))) diagnostics.push({ code: "RUNTIME_MISSING", platform: observation.id, message: `${observation.name}: ${text(evaluation.intent.language, "runtimeMissing")}` });
179
272
  for (const skill of observation.skills.filter((entry) => isBrokenLink(entry.target))) diagnostics.push({ code: "BROKEN_LINK", platform: observation.id, path: skill.target, message: `${observation.name}: ${text(evaluation.intent.language, "brokenLink")} ${skill.target}` });
180
- if (observation.id === "codex" && PLATFORMS.codex.legacyRoots(scopeBase(evaluation.intent.scope, evaluation.intent.projectRoot, evaluation.intent.home)).some(exists)) diagnostics.push({ code: "CODEX_LEGACY_DETECTED", platform: observation.id, message: "Codex legacy .codex/skills was detected; Matrix will not write there." });
181
- diagnostics.push({ code: `MATT_${observation.matt.state.toUpperCase()}`, platform: observation.id, message: `${observation.name}: Matt Pocock skills are ${observation.matt.state}.` });
182
- if (observation.matt.inherited) diagnostics.push({ code: "MATT_INHERITED", platform: observation.id, message: `${observation.name}: ${observation.matt.inherited} ${text(evaluation.intent.language, "mattInherited")}` });
183
- if (observation.matt.unmanaged.length) diagnostics.push({ code: "ARCH_UNMANAGED_EXTRA", platform: observation.id, skills: observation.matt.unmanaged, message: `${observation.name}: unmanaged extra Skills are preserved and will not be invoked: ${observation.matt.unmanaged.join(", ")}.` });
184
- }
273
+ if (observation.id === "codex" && PLATFORMS.codex.legacyRoots(scopeBase(evaluation.intent.scope, evaluation.intent.projectRoot, evaluation.intent.home)).some(exists)) diagnostics.push({ code: "CODEX_LEGACY_DETECTED", platform: observation.id, message: text(evaluation.intent.language, "codexLegacyDiagnostic") });
274
+ diagnostics.push({ code: `MATT_${observation.matt.state.toUpperCase()}`, platform: observation.id, message: text(evaluation.intent.language, "mattStateDiagnostic").replace("{platform}", observation.name).replace("{state}", localizedState(evaluation.intent.language, observation.matt.state)) });
275
+ if (observation.matt.ignoredGlobal) diagnostics.push({ code: "MATT_GLOBAL_IGNORED", platform: observation.id, count: observation.matt.ignoredGlobal, message: `${observation.name}: ${observation.matt.ignoredGlobal} ${text(evaluation.intent.language, "mattGlobalIgnored")}` });
276
+ if (observation.matt.unmanaged.length) diagnostics.push({ code: "ARCH_UNMANAGED_EXTRA", platform: observation.id, skills: observation.matt.unmanaged, message: text(evaluation.intent.language, "unmanagedExtraDiagnostic").replace("{platform}", observation.name).replace("{skills}", observation.matt.unmanaged.join(", ")) });
277
+ }
185
278
  return { ...evaluation, diagnosis: diagnostics };
186
279
  },
187
280
  commit(plan) {
188
281
  if (!plan?.seal || plan.seal !== digest({ intent: plan.intent, catalogDigest: plan.catalogDigest, actions: plan.actions, preconditionDigest: plan.preconditionDigest })) return { ok: false, code: "INVALID_INTENT" };
189
- const recovery = recoverPendingTransaction(plan.intent); if (!recovery.ok) return recovery;
190
- const refreshed = evaluate({ ...plan.intent, platforms: plan.intent.platforms });
282
+ const recovery = recoverPendingTransaction(plan.intent, { allowMattWrites: plan.intent.matt !== "readonly" }); if (!recovery.ok) return recovery;
283
+ const refreshed = evaluate({ ...plan.intent, platforms: plan.intent.platforms });
191
284
  if (!refreshed.plan || refreshed.plan.preconditionDigest !== plan.preconditionDigest || refreshed.plan.catalogDigest !== plan.catalogDigest) return { ok: false, code: "PLAN_STALE" };
192
285
  if (!refreshed.ok) return { ok: false, code: refreshed.code, diagnostics: refreshed.diagnostics };
193
286
  if (plan.intent.dryRun) return { ok: true, code: "DRY_RUN", actions: plan.actions };
287
+ const previousManifest = readManifest(plan.intent);
288
+ const previousMattReceipt = readJson(mattReceiptPath(plan.intent.projectRoot));
194
289
  const root = installationRoot(plan.intent.scope, plan.intent.projectRoot, plan.intent.home); const id = nowStamp(); const stagingRoot = path.join(root, "staging", id); const journalFile = transactionPath(plan.intent);
290
+ const mattActions = plan.actions.filter((item) => item.matt.kind === "reconcile");
195
291
  const journal = {
196
292
  id, state: "in-progress", stagingRoot,
197
293
  manifest: { path: plan.manifestPath, existed: pathExists(plan.manifestPath), backup: null, written: false },
198
294
  config: { path: plan.configPath, existed: pathExists(plan.configPath), backup: null, written: false },
295
+ mattReceipt: { path: mattReceiptPath(plan.intent.projectRoot), existed: pathExists(mattReceiptPath(plan.intent.projectRoot)), backup: null, written: false },
199
296
  operations: []
200
297
  };
201
- for (const item of refreshed.observations) if (plan.actions.find((entry) => entry.platform === item.id).matrix !== "keep") for (const skill of MATRIX_SKILLS) journal.operations.push({ target: path.join(item.root, skill), backup: null, status: "pending" });
202
- const persist = () => writeJson(journalFile, journal);
203
- try {
204
- persist(); const catalog = releaseCatalog(plan.intent.language); const stagedPlatforms = new Map();
298
+ for (const item of refreshed.observations) if (plan.actions.find((entry) => entry.platform === item.id).matrix !== "keep") for (const skill of MATRIX_SKILLS) journal.operations.push({ target: path.join(item.root, skill), backup: null, status: "pending" });
299
+ for (const action of mattActions) {
300
+ const observation = refreshed.observations.find((item) => item.id === action.platform);
301
+ for (const skill of action.matt.skills) journal.operations.push({ kind: "matt", platform: action.platform, skill, target: path.join(observation.matt.root, skill), backup: null, status: "pending" });
302
+ }
303
+ const persist = () => writeJson(journalFile, journal);
304
+ let mattCandidate = null;
305
+ try {
306
+ persist(); const catalog = releaseCatalog(plan.intent.language); const stagedPlatforms = new Map();
205
307
  for (const item of refreshed.observations) {
206
308
  if (plan.actions.find((entry) => entry.platform === item.id).matrix === "keep") continue;
207
309
  const platformStage = path.join(stagingRoot, item.id);
208
- for (const skill of MATRIX_SKILLS) { const staged = path.join(platformStage, skill); fs.mkdirSync(path.dirname(staged), { recursive: true }); fs.cpSync(path.join(catalog.root, skill), staged, { recursive: true }); fs.mkdirSync(path.join(staged, "scripts"), { recursive: true }); fs.copyFileSync(workflowRuntimeSource(), path.join(staged, "scripts", "matrix-runtime.mjs")); fs.copyFileSync(workflowTransactionSource(), path.join(staged, "scripts", "workflow-transaction.js")); if (skill === "matrix") for (const filename of ["matrix_state.py", "matrix_claude.py"]) fs.copyFileSync(path.join(sourceSkillsRoot("en"), "matrix", "scripts", filename), path.join(staged, "scripts", filename)); if (!exists(path.join(staged, "SKILL.md"))) throw new Error(`Stage validation failed: ${item.id}/${skill}`); }
209
- stagedPlatforms.set(item.id, platformStage);
210
- }
211
- failAt?.("after-stage"); let operationIndex = 0;
310
+ for (const skill of MATRIX_SKILLS) { const staged = path.join(platformStage, skill); fs.mkdirSync(path.dirname(staged), { recursive: true }); fs.cpSync(path.join(catalog.root, skill), staged, { recursive: true }); fs.mkdirSync(path.join(staged, "scripts"), { recursive: true }); fs.writeFileSync(path.join(staged, "scripts", "package.json"), '{"type":"module"}\n'); fs.copyFileSync(workflowRuntimeSource(), path.join(staged, "scripts", "matrix-runtime.mjs")); fs.copyFileSync(workflowTransactionSource(), path.join(staged, "scripts", "workflow-transaction.js")); fs.copyFileSync(mattCatalogSource(), path.join(staged, "scripts", "matt-catalog.mjs")); if (skill === "matrix") for (const filename of ["matrix_state.py", "matrix_claude.py"]) fs.copyFileSync(path.join(sourceSkillsRoot("en"), "matrix", "scripts", filename), path.join(staged, "scripts", filename)); if (!exists(path.join(staged, "SKILL.md"))) throw new Error(`Stage validation failed: ${item.id}/${skill}`); }
311
+ stagedPlatforms.set(item.id, platformStage);
312
+ }
313
+ if (mattActions.length) {
314
+ if (typeof mattAdapter.prepareCandidate !== "function") {
315
+ const restored = recoverPendingTransaction(plan.intent);
316
+ return { ok: true, code: "PARTIAL", committed: [], backups: [], recovery: "Matt adapter cannot provide an isolated candidate. Run matrix init --with-mattpocock with the supported Matrix CLI.", matt: [{ ok: false, code: "MATT_DIRECT_INSTALL_UNSUPPORTED" }], recoveryReceipt: restored.receipt ?? recovery.receipt };
317
+ }
318
+ mattCandidate = mattAdapter.prepareCandidate({ platforms: mattActions.map((item) => item.platform), skills: MATT_SKILLS });
319
+ if (!mattCandidate.ok) {
320
+ const restored = recoverPendingTransaction(plan.intent);
321
+ return { ok: true, code: "PARTIAL", committed: [], backups: [], recovery: mattCandidate.recovery, matt: [mattCandidate], recoveryReceipt: restored.receipt ?? recovery.receipt };
322
+ }
323
+ candidateVerifier(mattCandidate, mattActions);
324
+ }
325
+ failAt?.("after-stage"); let operationIndex = 0;
212
326
  for (const item of refreshed.observations) {
213
327
  if (plan.actions.find((entry) => entry.platform === item.id).matrix === "keep") continue;
214
328
  fs.mkdirSync(item.root, { recursive: true });
@@ -219,38 +333,78 @@ export function createDistribution({ failAt, mattAdapter = createMattAdapter() }
219
333
  operation.status = "backed-up"; persist(); failAt?.("after-backup", { platform: item.id, skill });
220
334
  const staged = path.join(stagedPlatforms.get(item.id), skill);
221
335
  if (plan.intent.mode === "symlink") { const store = path.join(root, "skills", catalog.digest, skill); if (!pathExists(store)) { fs.mkdirSync(path.dirname(store), { recursive: true }); fs.renameSync(staged, store); } fs.symlinkSync(store, target, process.platform === "win32" ? "junction" : "dir"); } else fs.renameSync(staged, target);
222
- operation.status = "committed"; persist(); failAt?.("after-commit", { platform: item.id, skill });
223
- }
224
- }
225
- const mattActions = plan.actions.filter((item) => item.matt.kind === "install-missing"); const mattResults = [];
226
- for (const action of mattActions) { const result = mattAdapter.installMissing({ projectRoot: plan.intent.projectRoot, platforms: [action.platform], skills: action.matt.skills, global: plan.intent.scope === "global" }); mattResults.push({ platform: action.platform, ...result }); }
227
- const postObservations = plan.intent.platforms.map((platform) => observePlatform(plan.intent, catalog, readManifest(plan.intent), platform));
228
- const archRequired = plan.intent.defaultOrchestration === "arch" || readManifest(plan.intent)?.orchestration?.available?.includes("arch");
229
- const incomplete = archRequired || mattActions.length ? postObservations.filter((item) => item.matt.state !== "complete") : [];
230
- const backups = journal.operations.filter((item) => item.backup).map((item) => ({ target: item.target, backup: item.backup }));
336
+ operation.status = "committed"; persist(); failAt?.("after-commit", { platform: item.id, skill });
337
+ }
338
+ }
339
+ const mattResults = [];
340
+ if (mattCandidate) {
341
+ for (const operation of journal.operations.filter((item) => item.kind === "matt")) {
342
+ const source = path.join(mattCandidate.platforms[operation.platform], operation.skill);
343
+ if (pathExists(operation.target)) {
344
+ const actual = hashDirectory(operation.target); const candidateHash = hashDirectory(source);
345
+ if (actual === candidateHash) continue;
346
+ const recorded = previousMattReceipt?.platforms?.[operation.platform]?.skills?.[operation.skill]?.hash
347
+ ?? previousManifest?.platforms?.[operation.platform]?.matt?.skills?.[operation.skill]?.hash;
348
+ if ((!recorded || actual !== recorded) && plan.intent.mattPolicy !== "replace") {
349
+ const error = new Error(`Matt target is user-modified: ${operation.target}`);
350
+ error.code = "MATT_USER_MODIFIED";
351
+ throw error;
352
+ }
353
+ operation.status = "backing-up";
354
+ operation.backup = path.join(root, "backups", id, "matt", operation.platform, operation.skill); persist();
355
+ fs.mkdirSync(path.dirname(operation.backup), { recursive: true });
356
+ fs.renameSync(operation.target, operation.backup);
357
+ operation.status = "backed-up"; persist();
358
+ }
359
+ fs.mkdirSync(path.dirname(operation.target), { recursive: true });
360
+ fs.cpSync(source, operation.target, { recursive: true });
361
+ operation.status = "committed"; persist(); failAt?.("after-matt-commit", { platform: operation.platform, skill: operation.skill });
362
+ }
363
+ for (const action of mattActions) mattResults.push({ platform: action.platform, ok: true, code: "OK", attempted: [action.platform] });
364
+ }
365
+ const postObservations = plan.intent.platforms.map((platform) => observePlatform(plan.intent, catalog, readManifest(plan.intent), previousMattReceipt, platform, mattContentHashes));
366
+ const incomplete = mattActions.length ? postObservations.filter((item) => !mattObservationMatchesRelease(item, mattContentHashes)) : [];
367
+ const backups = journal.operations.filter((item) => item.backup).map((item) => ({ target: item.target, backup: item.backup }));
231
368
  if (mattResults.some((item) => !item.ok) || incomplete.length) {
369
+ if (mattCandidate) mattAdapter.discardCandidate?.(mattCandidate);
232
370
  const restored = recoverPendingTransaction(plan.intent);
233
- return { ok: true, code: "PARTIAL", committed: [], backups: [], recovery: "The platform was not added. Repair the Arch Skill installation and rerun matrix init --with-mattpocock.", matt: mattResults, recoveryReceipt: restored.receipt ?? recovery.receipt };
371
+ return { ok: true, code: "PARTIAL", committed: [], backups: [], recovery: "The platform was not added. Repair the Arch Skill installation and rerun matrix init --with-mattpocock.", matt: mattResults, recoveryReceipt: restored.receipt ?? recovery.receipt };
234
372
  }
235
373
  if (journal.manifest.existed) { journal.manifest.backup = path.join(root, "backups", id, "installation.json"); persist(); fs.mkdirSync(path.dirname(journal.manifest.backup), { recursive: true }); fs.renameSync(plan.manifestPath, journal.manifest.backup); }
236
374
  if (journal.config.existed) { journal.config.backup = path.join(root, "backups", id, "config.yaml"); persist(); fs.mkdirSync(path.dirname(journal.config.backup), { recursive: true }); fs.renameSync(plan.configPath, journal.config.backup); }
237
- const records = Object.fromEntries(postObservations.map((item) => [item.id, {
238
- root: item.root,
239
- skills: Object.fromEntries(MATRIX_SKILLS.map((skill) => [skill, { hash: hashDirectory(path.join(item.root, skill)) }])),
240
- matt: { state: item.matt.state, skills: item.matt.skills }
241
- }]));
242
- const available = postObservations.every((item) => item.matt.state === "complete") ? ["prim", "arch"] : ["prim"];
375
+ if (mattActions.length && journal.mattReceipt.existed) { journal.mattReceipt.backup = path.join(root, "backups", id, "matt-installation.json"); persist(); fs.mkdirSync(path.dirname(journal.mattReceipt.backup), { recursive: true }); fs.renameSync(journal.mattReceipt.path, journal.mattReceipt.backup); }
376
+ const records = Object.fromEntries(postObservations.map((item) => {
377
+ const legacyMatt = previousManifest?.platforms?.[item.id]?.matt ?? null;
378
+ return [item.id, {
379
+ root: item.root,
380
+ skills: Object.fromEntries(MATRIX_SKILLS.map((skill) => [skill, { hash: hashDirectory(path.join(item.root, skill)) }])),
381
+ ...(legacyMatt ? { matt: legacyMatt } : {})
382
+ }];
383
+ }));
384
+ const available = plan.intent.matt === "readonly" ? (previousManifest?.orchestration?.available ?? ["prim"])
385
+ : mattActions.length ? (postObservations.every((observation) => mattObservationMatchesRelease(observation, mattContentHashes)) ? ["prim", "arch"] : ["prim"])
386
+ : (previousManifest?.orchestration?.available ?? ["prim"]);
243
387
  const previousConfig = readConfig(journal.config.backup ?? plan.configPath);
244
388
  fs.mkdirSync(path.dirname(plan.configPath), { recursive: true });
245
- fs.writeFileSync(plan.configPath, configContent(previousConfig, plan.intent.defaultOrchestration, plan.intent.scope)); journal.config.written = true; persist();
389
+ fs.writeFileSync(plan.configPath, configContent(previousConfig, plan.intent.defaultOrchestration, plan.intent.scope, plan.intent.language)); journal.config.written = true; persist();
390
+ if (mattActions.length && available.includes("arch")) {
391
+ writeJson(journal.mattReceipt.path, {
392
+ version: 1,
393
+ compatibility: MATT_COMPATIBILITY,
394
+ catalogDigest: MATT_CATALOG_DIGEST,
395
+ platforms: Object.fromEntries(postObservations.map((item) => [item.id, { root: item.matt.root, skills: item.matt.skills }]))
396
+ });
397
+ journal.mattReceipt.written = true; persist();
398
+ }
246
399
  writeJson(plan.manifestPath, {
247
- version: VERSION, catalogVersion: catalog.version, language: plan.intent.language, mode: plan.intent.mode, scope: plan.intent.scope,
248
- catalogDigest: catalog.digest, orchestration: { default: plan.intent.defaultOrchestration, available }, platforms: records
249
- });
400
+ version: VERSION, catalogVersion: catalog.version, language: plan.intent.language, mode: plan.intent.mode, scope: plan.intent.scope,
401
+ catalogDigest: catalog.digest, orchestration: { default: plan.intent.defaultOrchestration, available }, platforms: records
402
+ });
250
403
  journal.manifest.written = true; persist(); failAt?.("after-manifest"); journal.state = "committed"; persist();
251
- removeStaging(stagingRoot); removeEntry(journal.manifest.backup); removeEntry(journal.config.backup); removeEntry(journalFile);
404
+ if (mattCandidate) mattAdapter.discardCandidate?.(mattCandidate);
405
+ removeStaging(stagingRoot); removeEntry(journal.manifest.backup); removeEntry(journal.config.backup); removeEntry(journal.mattReceipt.backup); removeEntry(journalFile);
252
406
  return { ok: true, code: "OK", committed: journal.operations.map((item) => item.target), backups, matt: mattResults, recoveryReceipt: recovery.receipt };
253
- } catch (error) { const restored = recoverPendingTransaction(plan.intent); return { ok: false, code: "MATRIX_COMMIT_FAILED", error: error.message, recovery: restored.ok ? "Matrix targets and installation manifest were restored." : restored.recovery }; }
407
+ } catch (error) { if (mattCandidate) mattAdapter.discardCandidate?.(mattCandidate); const restored = recoverPendingTransaction(plan.intent); return { ok: false, code: error.code === "MATT_USER_MODIFIED" ? error.code : "MATRIX_COMMIT_FAILED", error: error.message, recovery: restored.ok ? "Matrix targets and installation manifest were restored." : restored.recovery }; }
254
408
  }
255
409
  };
256
410
  }
package/src/installer.js CHANGED
@@ -1,8 +1,7 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { createDistribution } from "./distribution.js";
4
- import { MATRIX_SKILLS, MATT_SKILLS, PLATFORMS, sourceSkillsRoot } from "./catalog.js";
5
- import { createMattAdapter } from "./matt.js";
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createDistribution } from "./distribution.js";
4
+ import { MATRIX_SKILLS, MATT_SKILLS, PLATFORMS, sourceSkillsRoot } from "./catalog.js";
6
5
 
7
6
  export { MATRIX_SKILLS, MATT_SKILLS, sourceSkillsRoot };
8
7
 
@@ -36,10 +35,11 @@ export function installMatrix({ destination, mode = "copy", conflict = "error",
36
35
  return { installed: result.committed ?? MATRIX_SKILLS, skipped: [], existing: [], backupRoot: result.backups?.[0] ? result.backups[0].backup : null };
37
36
  }
38
37
 
39
- export function installMattSkills({ projectRoot, platforms = ["claude-code"], skills = MATT_SKILLS, global = false, timeout = 300_000 }) {
40
- const result = createMattAdapter().installMissing({ projectRoot, platforms, skills, global, timeout });
41
- if (!result.ok) throw new Error(`${result.code}: ${result.message}`);
42
- return result;
43
- }
38
+ export function installMattSkills({ projectRoot, platforms = ["claude-code"], skills = MATT_SKILLS, global = false, timeout = 300_000 }) {
39
+ void projectRoot; void platforms; void skills; void global; void timeout;
40
+ const error = new Error("Direct Matt installation was retired; run matrix init --with-mattpocock so Distribution owns the complete Matrix/Matt transaction.");
41
+ error.code = "MATT_DIRECT_INSTALL_UNSUPPORTED";
42
+ throw error;
43
+ }
44
44
 
45
45
  export function findPython() { return null; }