@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.
- package/.claude/skills/matrix/SKILL.md +29 -34
- package/.claude/skills/matrix/scripts/matrix_state.py +11 -11
- package/.claude/skills/matrix-archive/SKILL.md +12 -12
- package/.claude/skills/matrix-build/SKILL.md +16 -13
- package/.claude/skills/matrix-claude/SKILL.md +10 -10
- package/.claude/skills/matrix-design/SKILL.md +19 -17
- package/.claude/skills/matrix-hotfix/SKILL.md +1 -1
- package/.claude/skills/matrix-open/SKILL.md +23 -21
- package/.claude/skills/matrix-status/SKILL.md +3 -3
- package/.claude/skills/matrix-tweak/SKILL.md +1 -1
- package/.claude/skills/matrix-verify/SKILL.md +39 -23
- package/README.md +78 -69
- package/README.zh-CN.md +77 -68
- package/assets/skills-zh-CN/matrix/SKILL.md +33 -33
- package/assets/skills-zh-CN/matrix-archive/SKILL.md +10 -10
- package/assets/skills-zh-CN/matrix-build/SKILL.md +13 -11
- package/assets/skills-zh-CN/matrix-claude/SKILL.md +10 -10
- package/assets/skills-zh-CN/matrix-design/SKILL.md +13 -11
- package/assets/skills-zh-CN/matrix-hotfix/SKILL.md +1 -1
- package/assets/skills-zh-CN/matrix-open/SKILL.md +19 -17
- package/assets/skills-zh-CN/matrix-status/SKILL.md +3 -3
- package/assets/skills-zh-CN/matrix-tweak/SKILL.md +1 -1
- package/assets/skills-zh-CN/matrix-verify/SKILL.md +27 -11
- package/package.json +1 -1
- package/src/catalog.js +31 -20
- package/src/cli.js +149 -91
- package/src/distribution.js +259 -136
- package/src/installer.js +10 -10
- package/src/matt-catalog.mjs +67 -0
- package/src/matt.js +115 -33
- package/src/messages.js +14 -10
- package/src/runtime-resolver.js +76 -0
- package/src/self-update.js +81 -81
- package/src/workflow-transaction.js +13 -9
- package/src/workflow.js +641 -388
package/src/distribution.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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:
|
|
188
|
-
const blocked = actions.filter((item) => item.matrix === "blocked");
|
|
189
|
-
const
|
|
190
|
-
|
|
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:
|
|
196
|
-
...(
|
|
197
|
-
|
|
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:
|
|
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:
|
|
212
|
-
diagnostics.push({ code: `MATT_${observation.matt.state.toUpperCase()}`, platform: observation.id, message:
|
|
213
|
-
if (observation.matt.
|
|
214
|
-
if (observation.matt.unmanaged.length) diagnostics.push({ code: "ARCH_UNMANAGED_EXTRA", platform: observation.id, skills: observation.matt.unmanaged, message:
|
|
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
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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; }
|