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