@rosenfan/matrix 0.1.3 → 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 (35) hide show
  1. package/.claude/skills/matrix/SKILL.md +29 -34
  2. package/.claude/skills/matrix/scripts/matrix_state.py +11 -11
  3. package/.claude/skills/matrix-archive/SKILL.md +12 -12
  4. package/.claude/skills/matrix-build/SKILL.md +16 -13
  5. package/.claude/skills/matrix-claude/SKILL.md +10 -10
  6. package/.claude/skills/matrix-design/SKILL.md +19 -17
  7. package/.claude/skills/matrix-hotfix/SKILL.md +1 -1
  8. package/.claude/skills/matrix-open/SKILL.md +23 -21
  9. package/.claude/skills/matrix-status/SKILL.md +3 -3
  10. package/.claude/skills/matrix-tweak/SKILL.md +1 -1
  11. package/.claude/skills/matrix-verify/SKILL.md +39 -23
  12. package/README.md +78 -69
  13. package/README.zh-CN.md +77 -68
  14. package/assets/skills-zh-CN/matrix/SKILL.md +33 -33
  15. package/assets/skills-zh-CN/matrix-archive/SKILL.md +10 -10
  16. package/assets/skills-zh-CN/matrix-build/SKILL.md +13 -11
  17. package/assets/skills-zh-CN/matrix-claude/SKILL.md +10 -10
  18. package/assets/skills-zh-CN/matrix-design/SKILL.md +13 -11
  19. package/assets/skills-zh-CN/matrix-hotfix/SKILL.md +1 -1
  20. package/assets/skills-zh-CN/matrix-open/SKILL.md +19 -17
  21. package/assets/skills-zh-CN/matrix-status/SKILL.md +3 -3
  22. package/assets/skills-zh-CN/matrix-tweak/SKILL.md +1 -1
  23. package/assets/skills-zh-CN/matrix-verify/SKILL.md +27 -11
  24. package/package.json +1 -1
  25. package/src/catalog.js +31 -20
  26. package/src/cli.js +149 -91
  27. package/src/distribution.js +259 -136
  28. package/src/installer.js +10 -10
  29. package/src/matt-catalog.mjs +67 -0
  30. package/src/matt.js +115 -33
  31. package/src/messages.js +14 -10
  32. package/src/runtime-resolver.js +76 -0
  33. package/src/self-update.js +81 -81
  34. package/src/workflow-transaction.js +13 -9
  35. package/src/workflow.js +641 -388
@@ -3,19 +3,21 @@ import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import {
6
- MATRIX_SKILLS, MATT_SKILLS, UNMANAGED_MATT_SKILLS, PLATFORM_IDS, PLATFORMS, hashDirectory,
7
- installationRoot, manifestPath, releaseCatalog, scopeBase, sourceSkillsRoot, workflowRuntimeSource, workflowTransactionSource
6
+ MATRIX_SKILLS, MATT_SKILLS, UNMANAGED_MATT_SKILLS, PLATFORM_IDS, PLATFORMS, hashDirectory,
7
+ installationRoot, manifestPath, mattCatalogSource, releaseCatalog, scopeBase, sourceSkillsRoot, workflowRuntimeSource, workflowTransactionSource
8
8
  } from "./catalog.js";
9
- import { createMattAdapter } from "./matt.js";
10
- import { text } from "./messages.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; } };
16
18
  const nowStamp = () => new Date().toISOString().replace(/[:.]/g, "-");
17
- const transactionPath = (intent) => path.join(installationRoot(intent.scope, intent.projectRoot, intent.home), "transaction.json");
18
- const configPath = (intent) => path.join(intent.projectRoot, ".matrix", "config.yaml");
19
+ const transactionPath = (intent) => path.join(installationRoot(intent.scope, intent.projectRoot, intent.home), "transaction.json");
20
+ const configPath = (intent) => path.join(intent.projectRoot, ".matrix", "config.yaml");
19
21
  const writeJson = (file, value) => {
20
22
  fs.mkdirSync(path.dirname(file), { recursive: true });
21
23
  const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
@@ -31,57 +33,76 @@ const readConfig = (file) => {
31
33
  }));
32
34
  } catch { return {}; }
33
35
  };
34
- const configContent = (existing, defaultOrchestration, scope, language) => {
35
- const values = {
36
- ...existing,
37
- schema: "matrix/config/v1",
38
- auto_transition: existing.auto_transition === "false" ? "false" : "true",
39
- default_orchestration: defaultOrchestration,
40
- installation_scope: scope,
41
- language
42
- };
43
- const preferred = ["schema", "auto_transition", "default_orchestration", "installation_scope", "language"];
44
- const keys = [...preferred, ...Object.keys(values).filter((key) => !preferred.includes(key)).sort()];
45
- return `${keys.map((key) => `${key}: ${values[key]}`).join("\n")}\n`;
46
- };
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
+ };
47
49
  const removeEntry = (value) => { if (pathExists(value)) fs.rmSync(value, { recursive: true, force: true }); };
48
50
  const removeStaging = (stagingRoot) => {
49
51
  removeEntry(stagingRoot);
50
52
  const parent = path.dirname(stagingRoot);
51
53
  if (exists(parent) && fs.readdirSync(parent).length === 0) fs.rmdirSync(parent);
52
54
  };
53
- const compareVersions = (left, right) => {
55
+ const compareVersions = (left, right) => {
54
56
  const parse = (value) => String(value ?? "0.0.0").split("-")[0].split(".").map((part) => Number(part) || 0);
55
57
  const a = parse(left); const b = parse(right);
56
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;
57
59
  return 0;
58
60
  };
59
- const skillPresent = (root, skill) => exists(path.join(root, skill, "SKILL.md"));
60
- 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);
81
+
82
+ function readManifest(intent) { return readJson(manifestPath(intent.scope, intent.projectRoot, intent.home)); }
61
83
 
62
- function readManifest(intent) { return readJson(manifestPath(intent.scope, intent.projectRoot, intent.home)); }
63
-
64
84
  export function installedIntent({ projectRoot = process.cwd(), home = os.homedir() } = {}) {
65
85
  const resolvedProjectRoot = path.resolve(projectRoot);
66
86
  const config = readConfig(path.join(resolvedProjectRoot, ".matrix", "config.yaml"));
67
- const scope = config.installation_scope === "global" ? "global" : "project";
68
- const manifest = readJson(manifestPath(scope, resolvedProjectRoot, home));
69
- const platforms = Object.keys(manifest?.platforms ?? {}).filter((id) => PLATFORM_IDS.includes(id));
70
- if (!manifest || !platforms.length) return { ok: false, code: "MANIFEST_MISSING", scope, config };
71
- const language = ["en", "zh-CN"].includes(manifest.language) ? manifest.language : (["en", "zh-CN"].includes(config.language) ? config.language : "en");
72
- const defaultOrchestration = ["prim", "arch"].includes(manifest.orchestration?.default) ? manifest.orchestration.default : "prim";
73
- const matt = manifest.orchestration?.available?.includes("arch") ? "missing" : "none";
74
- return {
75
- ok: true,
76
- intent: {
77
- projectRoot: resolvedProjectRoot, home, scope, platforms, language,
78
- mode: ["copy", "symlink"].includes(manifest.mode) ? manifest.mode : "copy",
79
- matt, defaultOrchestration, nonInteractive: true, matrixPolicy: "safe", dryRun: false
80
- },
81
- manifest,
82
- config
83
- };
84
- }
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
+ }
85
106
 
86
107
  function detectPlatforms(projectRoot, scope, home) {
87
108
  const bases = [scopeBase(scope, projectRoot, home)];
@@ -89,21 +110,38 @@ function detectPlatforms(projectRoot, scope, home) {
89
110
  return PLATFORM_IDS.filter((id) => bases.some((base) => exists(PLATFORMS[id].skillRoot(base))));
90
111
  }
91
112
 
92
- function normalizeIntent(raw) {
113
+ function normalizeIntent(raw) {
93
114
  const scope = raw.scope ?? "project";
94
115
  const platforms = [...new Set(raw.platforms ?? [])];
95
116
  if (!["project", "global"].includes(scope)) return { error: "INVALID_INTENT", detail: "scope" };
96
117
  if (!["copy", "symlink"].includes(raw.mode ?? "copy")) return { error: "INVALID_INTENT", detail: "mode" };
97
118
  if (!["en", "zh-CN"].includes(raw.language ?? "en")) return { error: "INVALID_INTENT", detail: "language" };
98
- if (raw.defaultOrchestration != null && !["prim", "arch"].includes(raw.defaultOrchestration)) return { error: "INVALID_INTENT", detail: "default-orchestration" };
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" };
99
122
  if (platforms.some((id) => !PLATFORM_IDS.includes(id))) return { error: "UNVERIFIED_PLATFORM" };
100
123
  return { projectRoot: path.resolve(raw.projectRoot ?? process.cwd()), home: raw.home ?? os.homedir(), scope, platforms,
101
- language: raw.language ?? "en", mode: raw.mode ?? "copy", matrixPolicy: raw.matrixPolicy ?? "safe",
102
- matt: raw.matt ?? "none", defaultOrchestration: raw.defaultOrchestration ?? null,
124
+ language: raw.language ?? "en", mode: raw.mode ?? "copy", matrixPolicy: raw.matrixPolicy ?? "safe",
125
+ matt: raw.matt ?? "none", mattPolicy: raw.mattPolicy ?? "preserve", defaultOrchestration: raw.defaultOrchestration ?? null,
103
126
  nonInteractive: Boolean(raw.nonInteractive), dryRun: Boolean(raw.dryRun) };
104
- }
105
-
106
- function observePlatform(intent, catalog, manifest, platformId) {
127
+ }
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) {
107
145
  const platform = PLATFORMS[platformId]; const base = scopeBase(intent.scope, intent.projectRoot, intent.home);
108
146
  const root = platform.skillRoot(base); const records = manifest?.platforms?.[platformId]?.skills ?? {};
109
147
  const skills = MATRIX_SKILLS.map((skill) => {
@@ -116,23 +154,36 @@ function observePlatform(intent, catalog, manifest, platformId) {
116
154
  if (actual === recorded) return { skill, target, state: compareVersions(manifest?.catalogVersion, catalog.version) > 0 ? "newer" : "outdated-unchanged", actual };
117
155
  return { skill, target, state: "user-modified", actual };
118
156
  });
119
- const states = new Set(skills.map((item) => item.state));
120
- const matrix = states.size === 1 && states.has("matching") ? "matching" : states.has("user-modified") ? "user-modified"
121
- : states.has("missing") && states.size === 1 ? "missing" : states.has("newer") ? "newer"
122
- : states.has("damaged") ? "damaged" : states.has("outdated-unchanged") ? "outdated-unchanged" : "partial";
123
- const globalRoot = platform.skillRoot(intent.home);
124
- const mattPresent = MATT_SKILLS.filter((skill) => skillPresent(root, skill));
125
- const globalMatt = intent.scope === "project" ? MATT_SKILLS.filter((skill) => skillPresent(globalRoot, skill)) : [];
126
- const availableMatt = new Set([...mattPresent, ...globalMatt]); const missing = MATT_SKILLS.filter((skill) => !availableMatt.has(skill));
127
- const inherited = globalMatt.filter((skill) => !mattPresent.includes(skill)).length;
128
- const managedSkills = Object.fromEntries([...availableMatt].map((skill) => {
129
- const target = mattPresent.includes(skill) ? path.join(root, skill) : path.join(globalRoot, skill);
130
- return [skill, { root: target, hash: hashDirectory(target) }];
131
- }));
132
- const unmanaged = UNMANAGED_MATT_SKILLS.filter((skill) => skillPresent(root, skill) || (intent.scope === "project" && skillPresent(globalRoot, skill)));
133
- return { id: platformId, name: platform.name, root, skills, matrix, matt: { present: mattPresent.length, missing, total: MATT_SKILLS.length, inherited,
134
- state: availableMatt.size === MATT_SKILLS.length ? "complete" : availableMatt.size ? "partial" : "missing", skills: managedSkills, unmanaged } };
135
- }
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 }];
180
+ }));
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 } };
186
+ }
136
187
 
137
188
  function actionFor(observation, policy) {
138
189
  if (observation.matrix === "matching" || observation.matrix === "newer") return "keep";
@@ -142,9 +193,12 @@ function actionFor(observation, policy) {
142
193
  return "repair";
143
194
  }
144
195
 
145
- export function recoverPendingTransaction(intent) {
146
- const file = transactionPath(intent); const journal = readJson(file);
147
- if (!journal) return { ok: true, recovered: false };
196
+ export function recoverPendingTransaction(intent, { allowMattWrites = true } = {}) {
197
+ const file = transactionPath(intent); const journal = readJson(file);
198
+ if (!journal) return { ok: true, recovered: false };
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
+ }
148
202
  const restoreMetadata = (entry) => {
149
203
  if (!entry) return;
150
204
  if (entry.backup && pathExists(entry.backup)) {
@@ -154,47 +208,55 @@ export function recoverPendingTransaction(intent) {
154
208
  } else if (entry.written && !entry.existed) removeEntry(entry.path);
155
209
  };
156
210
  try {
157
- if (journal.state === "committed") {
158
- removeStaging(journal.stagingRoot);
159
- for (const operation of journal.operations ?? []) removeEntry(operation.backup);
160
- removeEntry(journal.manifest?.backup); removeEntry(journal.config?.backup); removeEntry(file);
211
+ if (journal.state === "committed") {
212
+ removeStaging(journal.stagingRoot);
213
+ for (const operation of journal.operations ?? []) removeEntry(operation.backup);
214
+ removeEntry(journal.manifest?.backup); removeEntry(journal.config?.backup); removeEntry(journal.mattReceipt?.backup); removeEntry(file);
161
215
  return { ok: true, recovered: true, receipt: `Finalized completed Matrix transaction ${journal.id}.` };
162
216
  }
163
217
  for (const operation of [...(journal.operations ?? [])].reverse()) {
164
218
  if (operation.status !== "pending") removeEntry(operation.target);
165
219
  if (operation.backup && pathExists(operation.backup)) { fs.mkdirSync(path.dirname(operation.target), { recursive: true }); fs.renameSync(operation.backup, operation.target); }
166
220
  }
167
- restoreMetadata(journal.config);
168
- restoreMetadata(journal.manifest);
221
+ restoreMetadata(journal.config);
222
+ restoreMetadata(journal.manifest);
223
+ restoreMetadata(journal.mattReceipt);
169
224
  removeStaging(journal.stagingRoot); removeEntry(file);
170
225
  return { ok: true, recovered: true, receipt: `Recovered interrupted Matrix transaction ${journal.id}.` };
171
226
  } catch (error) { return { ok: false, code: "RECOVERY_REQUIRED", recovery: `Keep ${file} and restore the paths recorded there before retrying.`, error: error.message }; }
172
227
  }
173
228
 
174
- export function createDistribution({ failAt, mattAdapter = createMattAdapter() } = {}) {
229
+ export function createDistribution({ failAt, mattAdapter = createMattAdapter(), mattCandidateVerifier = null, mattContentHashes = MATT_CONTENT_HASHES } = {}) {
230
+ const candidateVerifier = mattCandidateVerifier ?? ((candidate, actions) => verifyMattCandidate(candidate, actions, mattContentHashes));
175
231
  const evaluate = (rawIntent) => {
176
232
  const intent = normalizeIntent(rawIntent); const language = intent.language ?? rawIntent.language ?? "en";
177
233
  if (intent.error) return { ok: false, code: intent.error, diagnostics: [{ code: intent.error, message: text(language, "noPlatform") }] };
178
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; }
179
235
  let catalog; try { catalog = releaseCatalog(intent.language); } catch (error) { return { ok: false, code: "PACKAGE_INCOMPLETE", intent, diagnostics: [{ code: "PACKAGE_INCOMPLETE", message: error.message }] }; }
180
- const manifest = readManifest(intent);
181
- intent.platforms = [...new Set([...Object.keys(manifest?.platforms ?? {}), ...intent.platforms])];
182
- const observations = intent.platforms.map((id) => observePlatform(intent, catalog, manifest, id));
236
+ const manifest = readManifest(intent);
237
+ const mattReceipt = readJson(mattReceiptPath(intent.projectRoot));
238
+ intent.platforms = [...new Set([...Object.keys(manifest?.platforms ?? {}), ...intent.platforms])];
239
+ const observations = intent.platforms.map((id) => observePlatform(intent, catalog, manifest, mattReceipt, id, mattContentHashes));
183
240
  const existingConfig = readConfig(configPath(intent));
184
241
  const existingDefault = ["prim", "arch"].includes(existingConfig.default_orchestration) ? existingConfig.default_orchestration
185
242
  : ["prim", "arch"].includes(manifest?.orchestration?.default) ? manifest.orchestration.default : null;
186
243
  intent.defaultOrchestration = intent.defaultOrchestration ?? existingDefault ?? (intent.matt === "missing" && intent.nonInteractive ? "arch" : "prim");
187
- 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 } }));
188
- const blocked = actions.filter((item) => item.matrix === "blocked");
189
- const archMustRemainAvailable = manifest?.orchestration?.available?.includes("arch") || intent.defaultOrchestration === "arch";
190
- const incompleteWithoutInstall = archMustRemainAvailable && actions.some((item, index) => observations[index].matt.state !== "complete" && item.matt.kind !== "install-missing");
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: [] } }));
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
+ : [];
249
+ const archMustRemainAvailable = manifest?.orchestration?.available?.includes("arch") || intent.defaultOrchestration === "arch";
250
+ const mattReady = trustedMattReceipt(mattReceipt) && observations.every(mattObservationMatchesReceipt);
251
+ const incompleteWithoutInstall = intent.matt !== "readonly" && intent.matt !== "missing" && archMustRemainAvailable && !mattReady;
191
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) };
192
253
  plan.seal = digest({ intent, catalogDigest: plan.catalogDigest, actions, preconditionDigest: plan.preconditionDigest });
193
- const code = blocked.length ? "USER_MODIFIED_BLOCKED" : incompleteWithoutInstall ? "ARCH_INSTALLATION_INCOMPLETE" : "OK";
254
+ const code = blocked.length ? "USER_MODIFIED_BLOCKED" : blockedMatt.length ? "MATT_USER_MODIFIED" : incompleteWithoutInstall ? "ARCH_INSTALLATION_INCOMPLETE" : "OK";
194
255
  return { ok: code === "OK", code, intent, observations, actions, plan, diagnostics: [
195
- ...blocked.map((item) => ({ code: "USER_MODIFIED_BLOCKED", platform: item.platform, message: `${item.platform}: existing Matrix assets are not proven managed.` })),
196
- ...(incompleteWithoutInstall ? [{ code: "ARCH_INSTALLATION_INCOMPLETE", message: "Arch requires the complete managed cohort on every configured platform. Retry with --with-mattpocock." }] : [])
197
- ], summary: `${text(intent.language, "preview")}: ${actions.map((item) => `${item.platform}: ${item.matrix}`).join(", ")}` };
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(", ")}` };
198
260
  };
199
261
 
200
262
  return { evaluate,
@@ -204,42 +266,63 @@ export function createDistribution({ failAt, mattAdapter = createMattAdapter() }
204
266
  if (pending) diagnostics.push({ code: "RECOVERY_REQUIRED", message: text(evaluation.intent.language, "transactionPending") });
205
267
  if (!manifest) diagnostics.push({ code: "MANIFEST_MISSING", message: text(evaluation.intent.language, "manifestMissing") });
206
268
  for (const observation of evaluation.observations ?? []) {
207
- 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)) });
208
270
  const matrixRoot = path.join(observation.root, "matrix");
209
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")}` });
210
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}` });
211
- 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." });
212
- diagnostics.push({ code: `MATT_${observation.matt.state.toUpperCase()}`, platform: observation.id, message: `${observation.name}: Matt Pocock skills are ${observation.matt.state}.` });
213
- if (observation.matt.inherited) diagnostics.push({ code: "MATT_INHERITED", platform: observation.id, message: `${observation.name}: ${observation.matt.inherited} ${text(evaluation.intent.language, "mattInherited")}` });
214
- 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(", ")}.` });
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(", ")) });
215
277
  }
216
278
  return { ...evaluation, diagnosis: diagnostics };
217
279
  },
218
280
  commit(plan) {
219
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" };
220
- const recovery = recoverPendingTransaction(plan.intent); if (!recovery.ok) return recovery;
221
- 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 });
222
284
  if (!refreshed.plan || refreshed.plan.preconditionDigest !== plan.preconditionDigest || refreshed.plan.catalogDigest !== plan.catalogDigest) return { ok: false, code: "PLAN_STALE" };
223
285
  if (!refreshed.ok) return { ok: false, code: refreshed.code, diagnostics: refreshed.diagnostics };
224
286
  if (plan.intent.dryRun) return { ok: true, code: "DRY_RUN", actions: plan.actions };
225
- 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);
226
- const journal = {
227
- id, state: "in-progress", stagingRoot,
228
- manifest: { path: plan.manifestPath, existed: pathExists(plan.manifestPath), backup: null, written: false },
229
- config: { path: plan.configPath, existed: pathExists(plan.configPath), backup: null, written: false },
230
- operations: []
231
- };
232
- 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" });
233
- const persist = () => writeJson(journalFile, journal);
234
- try {
235
- persist(); const catalog = releaseCatalog(plan.intent.language); const stagedPlatforms = new Map();
287
+ const previousManifest = readManifest(plan.intent);
288
+ const previousMattReceipt = readJson(mattReceiptPath(plan.intent.projectRoot));
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");
291
+ const journal = {
292
+ id, state: "in-progress", stagingRoot,
293
+ manifest: { path: plan.manifestPath, existed: pathExists(plan.manifestPath), backup: null, written: false },
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 },
296
+ operations: []
297
+ };
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();
236
307
  for (const item of refreshed.observations) {
237
308
  if (plan.actions.find((entry) => entry.platform === item.id).matrix === "keep") continue;
238
309
  const platformStage = path.join(stagingRoot, item.id);
239
- 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}`); }
240
- stagedPlatforms.set(item.id, platformStage);
241
- }
242
- 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;
243
326
  for (const item of refreshed.observations) {
244
327
  if (plan.actions.find((entry) => entry.platform === item.id).matrix === "keep") continue;
245
328
  fs.mkdirSync(item.root, { recursive: true });
@@ -250,38 +333,78 @@ export function createDistribution({ failAt, mattAdapter = createMattAdapter() }
250
333
  operation.status = "backed-up"; persist(); failAt?.("after-backup", { platform: item.id, skill });
251
334
  const staged = path.join(stagedPlatforms.get(item.id), skill);
252
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);
253
- operation.status = "committed"; persist(); failAt?.("after-commit", { platform: item.id, skill });
254
- }
255
- }
256
- const mattActions = plan.actions.filter((item) => item.matt.kind === "install-missing"); const mattResults = [];
257
- 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 }); }
258
- const postObservations = plan.intent.platforms.map((platform) => observePlatform(plan.intent, catalog, readManifest(plan.intent), platform));
259
- const archRequired = plan.intent.defaultOrchestration === "arch" || readManifest(plan.intent)?.orchestration?.available?.includes("arch");
260
- const incomplete = archRequired || mattActions.length ? postObservations.filter((item) => item.matt.state !== "complete") : [];
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)) : [];
261
367
  const backups = journal.operations.filter((item) => item.backup).map((item) => ({ target: item.target, backup: item.backup }));
262
- if (mattResults.some((item) => !item.ok) || incomplete.length) {
263
- const restored = recoverPendingTransaction(plan.intent);
368
+ if (mattResults.some((item) => !item.ok) || incomplete.length) {
369
+ if (mattCandidate) mattAdapter.discardCandidate?.(mattCandidate);
370
+ const restored = recoverPendingTransaction(plan.intent);
264
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 };
265
- }
266
- 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); }
267
- 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); }
268
- const records = Object.fromEntries(postObservations.map((item) => [item.id, {
269
- root: item.root,
270
- skills: Object.fromEntries(MATRIX_SKILLS.map((skill) => [skill, { hash: hashDirectory(path.join(item.root, skill)) }])),
271
- matt: { state: item.matt.state, skills: item.matt.skills }
272
- }]));
273
- const available = postObservations.every((item) => item.matt.state === "complete") ? ["prim", "arch"] : ["prim"];
274
- const previousConfig = readConfig(journal.config.backup ?? plan.configPath);
275
- fs.mkdirSync(path.dirname(plan.configPath), { recursive: true });
372
+ }
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); }
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); }
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"]);
387
+ const previousConfig = readConfig(journal.config.backup ?? plan.configPath);
388
+ fs.mkdirSync(path.dirname(plan.configPath), { recursive: true });
276
389
  fs.writeFileSync(plan.configPath, configContent(previousConfig, plan.intent.defaultOrchestration, plan.intent.scope, plan.intent.language)); journal.config.written = true; persist();
277
- writeJson(plan.manifestPath, {
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
+ }
399
+ writeJson(plan.manifestPath, {
278
400
  version: VERSION, catalogVersion: catalog.version, language: plan.intent.language, mode: plan.intent.mode, scope: plan.intent.scope,
279
401
  catalogDigest: catalog.digest, orchestration: { default: plan.intent.defaultOrchestration, available }, platforms: records
280
402
  });
281
- journal.manifest.written = true; persist(); failAt?.("after-manifest"); journal.state = "committed"; persist();
282
- removeStaging(stagingRoot); removeEntry(journal.manifest.backup); removeEntry(journal.config.backup); removeEntry(journalFile);
283
- return { ok: true, code: "OK", committed: journal.operations.map((item) => item.target), backups, matt: mattResults, recoveryReceipt: recovery.receipt };
284
- } 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 }; }
403
+ journal.manifest.written = true; persist(); failAt?.("after-manifest"); journal.state = "committed"; persist();
404
+ if (mattCandidate) mattAdapter.discardCandidate?.(mattCandidate);
405
+ removeStaging(stagingRoot); removeEntry(journal.manifest.backup); removeEntry(journal.config.backup); removeEntry(journal.mattReceipt.backup); removeEntry(journalFile);
406
+ return { ok: true, code: "OK", committed: journal.operations.map((item) => item.target), backups, matt: mattResults, recoveryReceipt: recovery.receipt };
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 }; }
285
408
  }
286
409
  };
287
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; }