@rosenfan/matrix 0.1.4 → 0.1.5

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 (48) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +29 -29
  3. package/README.zh-CN.md +27 -27
  4. package/{.claude → assets}/skills/matrix/SKILL.md +78 -78
  5. package/{.claude → assets}/skills/matrix/agents/openai.yaml +4 -4
  6. package/{.claude → assets}/skills/matrix/scripts/matrix_claude.py +11 -11
  7. package/{.claude → assets}/skills/matrix/scripts/matrix_state.py +49 -49
  8. package/assets/skills/matrix/scripts/test_matrix_claude.py +69 -0
  9. package/assets/skills/matrix/scripts/test_matrix_state.py +69 -0
  10. package/{.claude → assets}/skills/matrix-archive/SKILL.md +36 -36
  11. package/{.claude/skills/matrix-hotfix → assets/skills/matrix-archive}/agents/openai.yaml +4 -4
  12. package/{.claude → assets}/skills/matrix-build/SKILL.md +41 -41
  13. package/{.claude/skills/matrix-archive → assets/skills/matrix-build}/agents/openai.yaml +4 -4
  14. package/{.claude → assets}/skills/matrix-claude/SKILL.md +98 -98
  15. package/{.claude → assets}/skills/matrix-claude/agents/openai.yaml +4 -4
  16. package/{.claude → assets}/skills/matrix-design/SKILL.md +65 -65
  17. package/{.claude → assets}/skills/matrix-design/agents/openai.yaml +4 -4
  18. package/{.claude → assets}/skills/matrix-hotfix/SKILL.md +8 -8
  19. package/{.claude/skills/matrix-build → assets/skills/matrix-hotfix}/agents/openai.yaml +4 -4
  20. package/{.claude → assets}/skills/matrix-open/SKILL.md +39 -39
  21. package/assets/skills/matrix-open/agents/openai.yaml +4 -0
  22. package/{.claude → assets}/skills/matrix-status/SKILL.md +15 -15
  23. package/{.claude → assets}/skills/matrix-status/agents/openai.yaml +4 -4
  24. package/{.claude → assets}/skills/matrix-tweak/SKILL.md +8 -8
  25. package/assets/skills/matrix-tweak/agents/openai.yaml +4 -0
  26. package/{.claude → assets}/skills/matrix-verify/SKILL.md +62 -62
  27. package/assets/skills/matrix-verify/agents/openai.yaml +4 -0
  28. package/assets/skills-zh-CN/matrix/SKILL.md +14 -14
  29. package/assets/skills-zh-CN/matrix-build/SKILL.md +4 -4
  30. package/assets/skills-zh-CN/matrix-design/SKILL.md +4 -4
  31. package/assets/skills-zh-CN/matrix-open/SKILL.md +3 -3
  32. package/assets/skills-zh-CN/matrix-verify/SKILL.md +19 -19
  33. package/bin/matrix.js +15 -15
  34. package/package.json +4 -13
  35. package/src/catalog.js +79 -72
  36. package/src/cli.js +100 -100
  37. package/src/distribution.js +181 -178
  38. package/src/installer.js +10 -10
  39. package/src/matt-catalog.mjs +1 -1
  40. package/src/matt.js +115 -115
  41. package/src/messages.js +14 -14
  42. package/src/runtime-resolver.js +76 -76
  43. package/src/self-update.js +81 -81
  44. package/src/workflow-transaction.js +13 -13
  45. package/src/workflow.js +711 -705
  46. package/.claude/skills/matrix-open/agents/openai.yaml +0 -4
  47. package/.claude/skills/matrix-tweak/agents/openai.yaml +0 -4
  48. package/.claude/skills/matrix-verify/agents/openai.yaml +0 -4
@@ -1,63 +1,66 @@
1
- import crypto from "node:crypto";
2
- import fs from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import {
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import {
6
6
  MATRIX_SKILLS, MATT_SKILLS, UNMANAGED_MATT_SKILLS, PLATFORM_IDS, PLATFORMS, hashDirectory,
7
7
  installationRoot, manifestPath, mattCatalogSource, releaseCatalog, scopeBase, sourceSkillsRoot, workflowRuntimeSource, workflowTransactionSource
8
- } from "./catalog.js";
8
+ } from "./catalog.js";
9
9
  import { createMattAdapter, mattReceiptPath } from "./matt.js";
10
10
  import { MATT_CATALOG_DIGEST, MATT_COMPATIBILITY, MATT_CONTENT_HASHES } from "./matt-catalog.mjs";
11
11
  import { text } from "./messages.js";
12
-
13
- const VERSION = 3;
12
+
13
+ // Manifest v4 records content digests computed over EOL-normalized bytes and the
14
+ // assets/skills source layout; v3 manifests (raw-byte hashes, .claude/skills era)
15
+ // stay readable and are refreshed to v4 by the next update.
16
+ const VERSION = 4;
14
17
  const MATT_OPERATIONS = new Set(["none", "missing", "readonly"]);
15
- const digest = (value) => crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
16
- const exists = (value) => fs.existsSync(value);
17
- const pathExists = (value) => { try { fs.lstatSync(value); return true; } catch { return false; } };
18
- const nowStamp = () => new Date().toISOString().replace(/[:.]/g, "-");
18
+ const digest = (value) => crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
19
+ const exists = (value) => fs.existsSync(value);
20
+ const pathExists = (value) => { try { fs.lstatSync(value); return true; } catch { return false; } };
21
+ const nowStamp = () => new Date().toISOString().replace(/[:.]/g, "-");
19
22
  const transactionPath = (intent) => path.join(installationRoot(intent.scope, intent.projectRoot, intent.home), "transaction.json");
20
23
  const configPath = (intent) => path.join(intent.projectRoot, ".matrix", "config.yaml");
21
- const writeJson = (file, value) => {
22
- fs.mkdirSync(path.dirname(file), { recursive: true });
23
- const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
24
- fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`);
25
- fs.renameSync(temporary, file);
26
- };
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
- };
49
- const removeEntry = (value) => { if (pathExists(value)) fs.rmSync(value, { recursive: true, force: true }); };
50
- const removeStaging = (stagingRoot) => {
51
- removeEntry(stagingRoot);
52
- const parent = path.dirname(stagingRoot);
53
- if (exists(parent) && fs.readdirSync(parent).length === 0) fs.rmdirSync(parent);
54
- };
24
+ const writeJson = (file, value) => {
25
+ fs.mkdirSync(path.dirname(file), { recursive: true });
26
+ const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
27
+ fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`);
28
+ fs.renameSync(temporary, file);
29
+ };
30
+ const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; } };
31
+ const readConfig = (file) => {
32
+ try {
33
+ return Object.fromEntries(fs.readFileSync(file, "utf8").split(/\r?\n/).filter((line) => line.includes(":") && !line.startsWith(" ")).map((line) => {
34
+ const [key, ...value] = line.split(":");
35
+ return [key.trim(), value.join(":").trim()];
36
+ }));
37
+ } catch { return {}; }
38
+ };
39
+ const configContent = (existing, defaultOrchestration, scope, language) => {
40
+ const values = {
41
+ ...existing,
42
+ schema: "matrix/config/v1",
43
+ auto_transition: existing.auto_transition === "false" ? "false" : "true",
44
+ default_orchestration: defaultOrchestration,
45
+ installation_scope: scope,
46
+ language
47
+ };
48
+ const preferred = ["schema", "auto_transition", "default_orchestration", "installation_scope", "language"];
49
+ const keys = [...preferred, ...Object.keys(values).filter((key) => !preferred.includes(key)).sort()];
50
+ return `${keys.map((key) => `${key}: ${values[key]}`).join("\n")}\n`;
51
+ };
52
+ const removeEntry = (value) => { if (pathExists(value)) fs.rmSync(value, { recursive: true, force: true }); };
53
+ const removeStaging = (stagingRoot) => {
54
+ removeEntry(stagingRoot);
55
+ const parent = path.dirname(stagingRoot);
56
+ if (exists(parent) && fs.readdirSync(parent).length === 0) fs.rmdirSync(parent);
57
+ };
55
58
  const compareVersions = (left, right) => {
56
- const parse = (value) => String(value ?? "0.0.0").split("-")[0].split(".").map((part) => Number(part) || 0);
57
- const a = parse(left); const b = parse(right);
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;
59
- return 0;
60
- };
59
+ const parse = (value) => String(value ?? "0.0.0").split("-")[0].split(".").map((part) => Number(part) || 0);
60
+ const a = parse(left); const b = parse(right);
61
+ 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;
62
+ return 0;
63
+ };
61
64
  const skillPresent = (root, skill) => exists(path.join(root, skill, "SKILL.md"));
62
65
  const isBrokenLink = (value) => { try { return fs.lstatSync(value).isSymbolicLink() && !exists(value); } catch { return false; } };
63
66
  const mattObservationMatchesRelease = (observation, contentHashes) => MATT_SKILLS.every((skill) => observation.matt.skills?.[skill]?.hash === contentHashes[skill]);
@@ -78,52 +81,52 @@ const localizedState = (language, state) => text(language, ({
78
81
  const localizedAction = (language, action) => text(language, ({
79
82
  install: "actionInstall", keep: "actionKeep", "backup-replace": "actionBackupReplace", repair: "actionRepair", blocked: "actionBlocked"
80
83
  })[action] ?? action);
81
-
82
- function readManifest(intent) { return readJson(manifestPath(intent.scope, intent.projectRoot, intent.home)); }
83
-
84
+
85
+ function readManifest(intent) { return readJson(manifestPath(intent.scope, intent.projectRoot, intent.home)); }
86
+
84
87
  export function installedIntent({ projectRoot = process.cwd(), home = os.homedir() } = {}) {
85
88
  const resolvedProjectRoot = path.resolve(projectRoot);
86
89
  const config = readConfig(path.join(resolvedProjectRoot, ".matrix", "config.yaml"));
87
90
  const projectManifest = readJson(manifestPath("project", resolvedProjectRoot, home));
88
91
  const scope = projectManifest ? "project" : config.installation_scope === "global" ? "global" : "project";
89
92
  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";
93
+ const platforms = Object.keys(manifest?.platforms ?? {}).filter((id) => PLATFORM_IDS.includes(id));
94
+ if (!manifest || !platforms.length) return { ok: false, code: "MANIFEST_MISSING", scope, config };
95
+ const language = ["en", "zh-CN"].includes(manifest.language) ? manifest.language : (["en", "zh-CN"].includes(config.language) ? config.language : "en");
96
+ const defaultOrchestration = ["prim", "arch"].includes(manifest.orchestration?.default) ? manifest.orchestration.default : "prim";
94
97
  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
-
107
- function detectPlatforms(projectRoot, scope, home) {
108
- const bases = [scopeBase(scope, projectRoot, home)];
109
- if (scope === "project") bases.push(home);
110
- return PLATFORM_IDS.filter((id) => bases.some((base) => exists(PLATFORMS[id].skillRoot(base))));
111
- }
112
-
98
+ return {
99
+ ok: true,
100
+ intent: {
101
+ projectRoot: resolvedProjectRoot, home, scope, platforms, language,
102
+ mode: ["copy", "symlink"].includes(manifest.mode) ? manifest.mode : "copy",
103
+ matt, defaultOrchestration, nonInteractive: true, matrixPolicy: "safe", dryRun: false
104
+ },
105
+ manifest,
106
+ config
107
+ };
108
+ }
109
+
110
+ function detectPlatforms(projectRoot, scope, home) {
111
+ const bases = [scopeBase(scope, projectRoot, home)];
112
+ if (scope === "project") bases.push(home);
113
+ return PLATFORM_IDS.filter((id) => bases.some((base) => exists(PLATFORMS[id].skillRoot(base))));
114
+ }
115
+
113
116
  function normalizeIntent(raw) {
114
- const scope = raw.scope ?? "project";
115
- const platforms = [...new Set(raw.platforms ?? [])];
116
- if (!["project", "global"].includes(scope)) return { error: "INVALID_INTENT", detail: "scope" };
117
- if (!["copy", "symlink"].includes(raw.mode ?? "copy")) return { error: "INVALID_INTENT", detail: "mode" };
118
- if (!["en", "zh-CN"].includes(raw.language ?? "en")) return { error: "INVALID_INTENT", detail: "language" };
117
+ const scope = raw.scope ?? "project";
118
+ const platforms = [...new Set(raw.platforms ?? [])];
119
+ if (!["project", "global"].includes(scope)) return { error: "INVALID_INTENT", detail: "scope" };
120
+ if (!["copy", "symlink"].includes(raw.mode ?? "copy")) return { error: "INVALID_INTENT", detail: "mode" };
121
+ if (!["en", "zh-CN"].includes(raw.language ?? "en")) return { error: "INVALID_INTENT", detail: "language" };
119
122
  if (raw.defaultOrchestration != null && !["prim", "arch"].includes(raw.defaultOrchestration)) return { error: "INVALID_INTENT", detail: "default-orchestration" };
120
123
  if (!MATT_OPERATIONS.has(raw.matt ?? "none")) return { error: "INVALID_INTENT", detail: "matt" };
121
124
  if (raw.mattPolicy != null && !["preserve", "replace"].includes(raw.mattPolicy)) return { error: "INVALID_INTENT", detail: "matt-policy" };
122
- if (platforms.some((id) => !PLATFORM_IDS.includes(id))) return { error: "UNVERIFIED_PLATFORM" };
123
- return { projectRoot: path.resolve(raw.projectRoot ?? process.cwd()), home: raw.home ?? os.homedir(), scope, platforms,
125
+ if (platforms.some((id) => !PLATFORM_IDS.includes(id))) return { error: "UNVERIFIED_PLATFORM" };
126
+ return { projectRoot: path.resolve(raw.projectRoot ?? process.cwd()), home: raw.home ?? os.homedir(), scope, platforms,
124
127
  language: raw.language ?? "en", mode: raw.mode ?? "copy", matrixPolicy: raw.matrixPolicy ?? "safe",
125
128
  matt: raw.matt ?? "none", mattPolicy: raw.mattPolicy ?? "preserve", defaultOrchestration: raw.defaultOrchestration ?? null,
126
- nonInteractive: Boolean(raw.nonInteractive), dryRun: Boolean(raw.dryRun) };
129
+ nonInteractive: Boolean(raw.nonInteractive), dryRun: Boolean(raw.dryRun) };
127
130
  }
128
131
 
129
132
  export function classifyMattTarget({ present, readable = true, actual, expected, recorded, receiptTrusted = false }) {
@@ -142,18 +145,18 @@ function trustedMattReceipt(receipt) {
142
145
  }
143
146
 
144
147
  function observePlatform(intent, catalog, manifest, mattReceipt, platformId, contentHashes) {
145
- const platform = PLATFORMS[platformId]; const base = scopeBase(intent.scope, intent.projectRoot, intent.home);
146
- const root = platform.skillRoot(base); const records = manifest?.platforms?.[platformId]?.skills ?? {};
147
- const skills = MATRIX_SKILLS.map((skill) => {
148
- const target = path.join(root, skill);
149
- if (!pathExists(target)) return { skill, target, state: "missing" };
150
- if (isBrokenLink(target) || !exists(path.join(target, "SKILL.md"))) return { skill, target, state: "damaged" };
151
- const actual = hashDirectory(target); const recorded = records[skill]?.hash; const expected = catalog.skills[skill];
152
- if (!recorded) return { skill, target, state: "user-modified", actual };
153
- if (actual === expected) return { skill, target, state: "matching", actual };
154
- if (actual === recorded) return { skill, target, state: compareVersions(manifest?.catalogVersion, catalog.version) > 0 ? "newer" : "outdated-unchanged", actual };
155
- return { skill, target, state: "user-modified", actual };
156
- });
148
+ const platform = PLATFORMS[platformId]; const base = scopeBase(intent.scope, intent.projectRoot, intent.home);
149
+ const root = platform.skillRoot(base); const records = manifest?.platforms?.[platformId]?.skills ?? {};
150
+ const skills = MATRIX_SKILLS.map((skill) => {
151
+ const target = path.join(root, skill);
152
+ if (!pathExists(target)) return { skill, target, state: "missing" };
153
+ if (isBrokenLink(target) || !exists(path.join(target, "SKILL.md"))) return { skill, target, state: "damaged" };
154
+ const actual = hashDirectory(target); const recorded = records[skill]?.hash; const expected = catalog.skills[skill];
155
+ if (!recorded) return { skill, target, state: "user-modified", actual };
156
+ if (actual === expected) return { skill, target, state: "matching", actual };
157
+ if (actual === recorded) return { skill, target, state: compareVersions(manifest?.catalogVersion, catalog.version) > 0 ? "newer" : "outdated-unchanged", actual };
158
+ return { skill, target, state: "user-modified", actual };
159
+ });
157
160
  const states = new Set(skills.map((item) => item.state));
158
161
  const matrix = states.size === 1 && states.has("matching") ? "matching" : states.has("user-modified") ? "user-modified"
159
162
  : states.has("missing") && states.size === 1 ? "missing" : states.has("newer") ? "newer"
@@ -184,63 +187,63 @@ function observePlatform(intent, catalog, manifest, mattReceipt, platformId, con
184
187
  return { id: platformId, name: platform.name, root, skills, matrix, matt: { root: mattRoot, present: mattPresent.length, missing, total: MATT_SKILLS.length, inherited: 0, ignoredGlobal,
185
188
  state: mattPresent.length === MATT_SKILLS.length ? "complete" : mattPresent.length ? "partial" : "missing", skills: managedSkills, classifications, classificationCounts, unmanaged } };
186
189
  }
187
-
188
- function actionFor(observation, policy) {
189
- if (observation.matrix === "matching" || observation.matrix === "newer") return "keep";
190
- if (observation.matrix === "missing") return "install";
191
- if (observation.matrix === "outdated-unchanged") return "backup-replace";
192
- if (["damaged", "user-modified"].includes(observation.matrix)) return policy === "replace" ? "backup-replace" : "blocked";
193
- return "repair";
194
- }
195
-
190
+
191
+ function actionFor(observation, policy) {
192
+ if (observation.matrix === "matching" || observation.matrix === "newer") return "keep";
193
+ if (observation.matrix === "missing") return "install";
194
+ if (observation.matrix === "outdated-unchanged") return "backup-replace";
195
+ if (["damaged", "user-modified"].includes(observation.matrix)) return policy === "replace" ? "backup-replace" : "blocked";
196
+ return "repair";
197
+ }
198
+
196
199
  export function recoverPendingTransaction(intent, { allowMattWrites = true } = {}) {
197
200
  const file = transactionPath(intent); const journal = readJson(file);
198
201
  if (!journal) return { ok: true, recovered: false };
199
202
  if (!allowMattWrites && ((journal.operations ?? []).some((operation) => operation.kind === "matt") || journal.mattReceipt?.written || journal.mattReceipt?.backup)) {
200
203
  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
204
  }
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 {
205
+ const restoreMetadata = (entry) => {
206
+ if (!entry) return;
207
+ if (entry.backup && pathExists(entry.backup)) {
208
+ removeEntry(entry.path);
209
+ fs.mkdirSync(path.dirname(entry.path), { recursive: true });
210
+ fs.renameSync(entry.backup, entry.path);
211
+ } else if (entry.written && !entry.existed) removeEntry(entry.path);
212
+ };
213
+ try {
211
214
  if (journal.state === "committed") {
212
215
  removeStaging(journal.stagingRoot);
213
216
  for (const operation of journal.operations ?? []) removeEntry(operation.backup);
214
217
  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}.` };
216
- }
217
- for (const operation of [...(journal.operations ?? [])].reverse()) {
218
- if (operation.status !== "pending") removeEntry(operation.target);
219
- if (operation.backup && pathExists(operation.backup)) { fs.mkdirSync(path.dirname(operation.target), { recursive: true }); fs.renameSync(operation.backup, operation.target); }
220
- }
218
+ return { ok: true, recovered: true, receipt: `Finalized completed Matrix transaction ${journal.id}.` };
219
+ }
220
+ for (const operation of [...(journal.operations ?? [])].reverse()) {
221
+ if (operation.status !== "pending") removeEntry(operation.target);
222
+ if (operation.backup && pathExists(operation.backup)) { fs.mkdirSync(path.dirname(operation.target), { recursive: true }); fs.renameSync(operation.backup, operation.target); }
223
+ }
221
224
  restoreMetadata(journal.config);
222
225
  restoreMetadata(journal.manifest);
223
226
  restoreMetadata(journal.mattReceipt);
224
- removeStaging(journal.stagingRoot); removeEntry(file);
225
- return { ok: true, recovered: true, receipt: `Recovered interrupted Matrix transaction ${journal.id}.` };
226
- } catch (error) { return { ok: false, code: "RECOVERY_REQUIRED", recovery: `Keep ${file} and restore the paths recorded there before retrying.`, error: error.message }; }
227
- }
228
-
227
+ removeStaging(journal.stagingRoot); removeEntry(file);
228
+ return { ok: true, recovered: true, receipt: `Recovered interrupted Matrix transaction ${journal.id}.` };
229
+ } catch (error) { return { ok: false, code: "RECOVERY_REQUIRED", recovery: `Keep ${file} and restore the paths recorded there before retrying.`, error: error.message }; }
230
+ }
231
+
229
232
  export function createDistribution({ failAt, mattAdapter = createMattAdapter(), mattCandidateVerifier = null, mattContentHashes = MATT_CONTENT_HASHES } = {}) {
230
233
  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 }] }; }
234
+ const evaluate = (rawIntent) => {
235
+ const intent = normalizeIntent(rawIntent); const language = intent.language ?? rawIntent.language ?? "en";
236
+ if (intent.error) return { ok: false, code: intent.error, diagnostics: [{ code: intent.error, message: text(language, "noPlatform") }] };
237
+ 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; }
238
+ let catalog; try { catalog = releaseCatalog(intent.language); } catch (error) { return { ok: false, code: "PACKAGE_INCOMPLETE", intent, diagnostics: [{ code: "PACKAGE_INCOMPLETE", message: error.message }] }; }
236
239
  const manifest = readManifest(intent);
237
240
  const mattReceipt = readJson(mattReceiptPath(intent.projectRoot));
238
241
  intent.platforms = [...new Set([...Object.keys(manifest?.platforms ?? {}), ...intent.platforms])];
239
242
  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");
243
+ const existingConfig = readConfig(configPath(intent));
244
+ const existingDefault = ["prim", "arch"].includes(existingConfig.default_orchestration) ? existingConfig.default_orchestration
245
+ : ["prim", "arch"].includes(manifest?.orchestration?.default) ? manifest.orchestration.default : null;
246
+ intent.defaultOrchestration = intent.defaultOrchestration ?? existingDefault ?? (intent.matt === "missing" && intent.nonInteractive ? "arch" : "prim");
244
247
  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
248
  const blocked = actions.filter((item) => item.matrix === "blocked");
246
249
  const blockedMatt = intent.matt === "missing" && intent.mattPolicy !== "replace"
@@ -249,41 +252,41 @@ export function createDistribution({ failAt, mattAdapter = createMattAdapter(),
249
252
  const archMustRemainAvailable = manifest?.orchestration?.available?.includes("arch") || intent.defaultOrchestration === "arch";
250
253
  const mattReady = trustedMattReceipt(mattReceipt) && observations.every(mattObservationMatchesReceipt);
251
254
  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 });
255
+ const plan = { version: VERSION, intent, catalogDigest: catalog.digest, manifestPath: manifestPath(intent.scope, intent.projectRoot, intent.home), configPath: configPath(intent), actions, observations, preconditionDigest: digest(observations) };
256
+ plan.seal = digest({ intent, catalogDigest: plan.catalogDigest, actions, preconditionDigest: plan.preconditionDigest });
254
257
  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: [
258
+ return { ok: code === "OK", code, intent, observations, actions, plan, diagnostics: [
256
259
  ...blocked.map((item) => ({ code: "USER_MODIFIED_BLOCKED", platform: item.platform, message: text(intent.language, "matrixModifiedDiagnostic").replace("{platform}", item.platform) })),
257
260
  ...blockedMatt.map((item) => ({ code: "MATT_USER_MODIFIED", platform: item.id, states: item.matt.classificationCounts, message: text(intent.language, "mattModifiedDiagnostic").replace("{platform}", item.name) })),
258
261
  ...(incompleteWithoutInstall ? [{ code: "ARCH_INSTALLATION_INCOMPLETE", message: text(intent.language, "archIncompleteDiagnostic") }] : [])
259
262
  ], summary: `${text(intent.language, "preview")}: ${actions.map((item) => `${item.platform}: ${localizedAction(intent.language, item.matrix)}`).join(", ")}` };
260
- };
261
-
262
- return { evaluate,
263
- diagnose(rawIntent) {
264
- const evaluation = evaluate(rawIntent); if (!evaluation.intent) return evaluation;
265
- const diagnostics = []; const pending = readJson(transactionPath(evaluation.intent)); const manifest = readManifest(evaluation.intent);
266
- if (pending) diagnostics.push({ code: "RECOVERY_REQUIRED", message: text(evaluation.intent.language, "transactionPending") });
267
- if (!manifest) diagnostics.push({ code: "MANIFEST_MISSING", message: text(evaluation.intent.language, "manifestMissing") });
268
- for (const observation of evaluation.observations ?? []) {
263
+ };
264
+
265
+ return { evaluate,
266
+ diagnose(rawIntent) {
267
+ const evaluation = evaluate(rawIntent); if (!evaluation.intent) return evaluation;
268
+ const diagnostics = []; const pending = readJson(transactionPath(evaluation.intent)); const manifest = readManifest(evaluation.intent);
269
+ if (pending) diagnostics.push({ code: "RECOVERY_REQUIRED", message: text(evaluation.intent.language, "transactionPending") });
270
+ if (!manifest) diagnostics.push({ code: "MANIFEST_MISSING", message: text(evaluation.intent.language, "manifestMissing") });
271
+ for (const observation of evaluation.observations ?? []) {
269
272
  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)) });
270
- const matrixRoot = path.join(observation.root, "matrix");
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")}` });
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}` });
273
+ const matrixRoot = path.join(observation.root, "matrix");
274
+ 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")}` });
275
+ 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}` });
273
276
  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
277
  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
278
  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
279
  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
- }
278
- return { ...evaluation, diagnosis: diagnostics };
279
- },
280
- commit(plan) {
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" };
280
+ }
281
+ return { ...evaluation, diagnosis: diagnostics };
282
+ },
283
+ commit(plan) {
284
+ if (!plan?.seal || plan.seal !== digest({ intent: plan.intent, catalogDigest: plan.catalogDigest, actions: plan.actions, preconditionDigest: plan.preconditionDigest })) return { ok: false, code: "INVALID_INTENT" };
282
285
  const recovery = recoverPendingTransaction(plan.intent, { allowMattWrites: plan.intent.matt !== "readonly" }); if (!recovery.ok) return recovery;
283
286
  const refreshed = evaluate({ ...plan.intent, platforms: plan.intent.platforms });
284
- if (!refreshed.plan || refreshed.plan.preconditionDigest !== plan.preconditionDigest || refreshed.plan.catalogDigest !== plan.catalogDigest) return { ok: false, code: "PLAN_STALE" };
285
- if (!refreshed.ok) return { ok: false, code: refreshed.code, diagnostics: refreshed.diagnostics };
286
- if (plan.intent.dryRun) return { ok: true, code: "DRY_RUN", actions: plan.actions };
287
+ if (!refreshed.plan || refreshed.plan.preconditionDigest !== plan.preconditionDigest || refreshed.plan.catalogDigest !== plan.catalogDigest) return { ok: false, code: "PLAN_STALE" };
288
+ if (!refreshed.ok) return { ok: false, code: refreshed.code, diagnostics: refreshed.diagnostics };
289
+ if (plan.intent.dryRun) return { ok: true, code: "DRY_RUN", actions: plan.actions };
287
290
  const previousManifest = readManifest(plan.intent);
288
291
  const previousMattReceipt = readJson(mattReceiptPath(plan.intent.projectRoot));
289
292
  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);
@@ -304,9 +307,9 @@ export function createDistribution({ failAt, mattAdapter = createMattAdapter(),
304
307
  let mattCandidate = null;
305
308
  try {
306
309
  persist(); const catalog = releaseCatalog(plan.intent.language); const stagedPlatforms = new Map();
307
- for (const item of refreshed.observations) {
308
- if (plan.actions.find((entry) => entry.platform === item.id).matrix === "keep") continue;
309
- const platformStage = path.join(stagingRoot, item.id);
310
+ for (const item of refreshed.observations) {
311
+ if (plan.actions.find((entry) => entry.platform === item.id).matrix === "keep") continue;
312
+ const platformStage = path.join(stagingRoot, item.id);
310
313
  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
314
  stagedPlatforms.set(item.id, platformStage);
312
315
  }
@@ -323,16 +326,16 @@ export function createDistribution({ failAt, mattAdapter = createMattAdapter(),
323
326
  candidateVerifier(mattCandidate, mattActions);
324
327
  }
325
328
  failAt?.("after-stage"); let operationIndex = 0;
326
- for (const item of refreshed.observations) {
327
- if (plan.actions.find((entry) => entry.platform === item.id).matrix === "keep") continue;
328
- fs.mkdirSync(item.root, { recursive: true });
329
- for (const skill of MATRIX_SKILLS) {
330
- const operation = journal.operations[operationIndex++]; const target = operation.target; operation.status = "backing-up";
331
- if (pathExists(target)) operation.backup = path.join(root, "backups", id, item.id, skill); persist();
332
- if (operation.backup) { fs.mkdirSync(path.dirname(operation.backup), { recursive: true }); fs.renameSync(target, operation.backup); }
333
- operation.status = "backed-up"; persist(); failAt?.("after-backup", { platform: item.id, skill });
334
- const staged = path.join(stagedPlatforms.get(item.id), skill);
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);
329
+ for (const item of refreshed.observations) {
330
+ if (plan.actions.find((entry) => entry.platform === item.id).matrix === "keep") continue;
331
+ fs.mkdirSync(item.root, { recursive: true });
332
+ for (const skill of MATRIX_SKILLS) {
333
+ const operation = journal.operations[operationIndex++]; const target = operation.target; operation.status = "backing-up";
334
+ if (pathExists(target)) operation.backup = path.join(root, "backups", id, item.id, skill); persist();
335
+ if (operation.backup) { fs.mkdirSync(path.dirname(operation.backup), { recursive: true }); fs.renameSync(target, operation.backup); }
336
+ operation.status = "backed-up"; persist(); failAt?.("after-backup", { platform: item.id, skill });
337
+ const staged = path.join(stagedPlatforms.get(item.id), skill);
338
+ 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);
336
339
  operation.status = "committed"; persist(); failAt?.("after-commit", { platform: item.id, skill });
337
340
  }
338
341
  }
@@ -364,11 +367,11 @@ export function createDistribution({ failAt, mattAdapter = createMattAdapter(),
364
367
  }
365
368
  const postObservations = plan.intent.platforms.map((platform) => observePlatform(plan.intent, catalog, readManifest(plan.intent), previousMattReceipt, platform, mattContentHashes));
366
369
  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 }));
370
+ const backups = journal.operations.filter((item) => item.backup).map((item) => ({ target: item.target, backup: item.backup }));
368
371
  if (mattResults.some((item) => !item.ok) || incomplete.length) {
369
372
  if (mattCandidate) mattAdapter.discardCandidate?.(mattCandidate);
370
373
  const restored = recoverPendingTransaction(plan.intent);
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 };
374
+ 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 };
372
375
  }
373
376
  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
377
  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); }
@@ -397,14 +400,14 @@ export function createDistribution({ failAt, mattAdapter = createMattAdapter(),
397
400
  journal.mattReceipt.written = true; persist();
398
401
  }
399
402
  writeJson(plan.manifestPath, {
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
- });
403
+ version: VERSION, catalogVersion: catalog.version, language: plan.intent.language, mode: plan.intent.mode, scope: plan.intent.scope,
404
+ catalogDigest: catalog.digest, orchestration: { default: plan.intent.defaultOrchestration, available }, platforms: records
405
+ });
403
406
  journal.manifest.written = true; persist(); failAt?.("after-manifest"); journal.state = "committed"; persist();
404
407
  if (mattCandidate) mattAdapter.discardCandidate?.(mattCandidate);
405
408
  removeStaging(stagingRoot); removeEntry(journal.manifest.backup); removeEntry(journal.config.backup); removeEntry(journal.mattReceipt.backup); removeEntry(journalFile);
406
409
  return { ok: true, code: "OK", committed: journal.operations.map((item) => item.target), backups, matt: mattResults, recoveryReceipt: recovery.receipt };
407
410
  } 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 }; }
408
- }
409
- };
410
- }
411
+ }
412
+ };
413
+ }
package/src/installer.js CHANGED
@@ -1,7 +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";
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
5
 
6
6
  export { MATRIX_SKILLS, MATT_SKILLS, sourceSkillsRoot };
7
7
 
@@ -35,11 +35,11 @@ export function installMatrix({ destination, mode = "copy", conflict = "error",
35
35
  return { installed: result.committed ?? MATRIX_SKILLS, skipped: [], existing: [], backupRoot: result.backups?.[0] ? result.backups[0].backup : null };
36
36
  }
37
37
 
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
- }
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; }
@@ -1,7 +1,7 @@
1
1
  import crypto from "node:crypto";
2
2
 
3
3
  export const MATT_COMPATIBILITY = Object.freeze({
4
- matrixVersion: "0.1.4",
4
+ matrixVersion: "0.1.5",
5
5
  release: "v1.2.3",
6
6
  commit: "6acc160e4e0cd062dbbbd7a1b26ae92855edf07e",
7
7
  archive: "https://github.com/mattpocock/skills/archive/6acc160e4e0cd062dbbbd7a1b26ae92855edf07e.tar.gz",