@rosenfan/matrix 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.claude/skills/matrix/SKILL.md +49 -0
  2. package/.claude/skills/matrix/agents/openai.yaml +4 -0
  3. package/.claude/skills/matrix/scripts/matrix_claude.py +166 -0
  4. package/.claude/skills/matrix/scripts/matrix_state.py +114 -0
  5. package/.claude/skills/matrix-archive/SKILL.md +26 -0
  6. package/.claude/skills/matrix-archive/agents/openai.yaml +4 -0
  7. package/.claude/skills/matrix-build/SKILL.md +55 -0
  8. package/.claude/skills/matrix-build/agents/openai.yaml +4 -0
  9. package/.claude/skills/matrix-claude/SKILL.md +96 -0
  10. package/.claude/skills/matrix-claude/agents/openai.yaml +4 -0
  11. package/.claude/skills/matrix-design/SKILL.md +68 -0
  12. package/.claude/skills/matrix-design/agents/openai.yaml +4 -0
  13. package/.claude/skills/matrix-hotfix/SKILL.md +8 -0
  14. package/.claude/skills/matrix-hotfix/agents/openai.yaml +4 -0
  15. package/.claude/skills/matrix-open/SKILL.md +33 -0
  16. package/.claude/skills/matrix-open/agents/openai.yaml +4 -0
  17. package/.claude/skills/matrix-status/SKILL.md +14 -0
  18. package/.claude/skills/matrix-status/agents/openai.yaml +4 -0
  19. package/.claude/skills/matrix-tweak/SKILL.md +8 -0
  20. package/.claude/skills/matrix-tweak/agents/openai.yaml +4 -0
  21. package/.claude/skills/matrix-verify/SKILL.md +82 -0
  22. package/.claude/skills/matrix-verify/agents/openai.yaml +4 -0
  23. package/LICENSE +21 -0
  24. package/README.md +273 -0
  25. package/README.zh-CN.md +271 -0
  26. package/assets/skills-zh-CN/matrix/SKILL.md +28 -0
  27. package/assets/skills-zh-CN/matrix-archive/SKILL.md +12 -0
  28. package/assets/skills-zh-CN/matrix-build/SKILL.md +13 -0
  29. package/assets/skills-zh-CN/matrix-claude/SKILL.md +30 -0
  30. package/assets/skills-zh-CN/matrix-design/SKILL.md +13 -0
  31. package/assets/skills-zh-CN/matrix-hotfix/SKILL.md +8 -0
  32. package/assets/skills-zh-CN/matrix-open/SKILL.md +13 -0
  33. package/assets/skills-zh-CN/matrix-status/SKILL.md +14 -0
  34. package/assets/skills-zh-CN/matrix-tweak/SKILL.md +8 -0
  35. package/assets/skills-zh-CN/matrix-verify/SKILL.md +13 -0
  36. package/bin/matrix.js +15 -0
  37. package/package.json +56 -0
  38. package/src/catalog.js +87 -0
  39. package/src/cli.js +117 -0
  40. package/src/distribution.js +192 -0
  41. package/src/installer.js +45 -0
  42. package/src/matt.js +34 -0
  43. package/src/messages.js +33 -0
  44. package/src/ui.js +89 -0
  45. package/src/workflow.js +95 -0
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: matrix
3
+ description: 启动、恢复并治理持久化的 Matrix 开发工作流。
4
+ ---
5
+
6
+ # Matrix
7
+
8
+ Matrix 的事实来源是当前项目的 `.matrix/`,不得从聊天记录推断阶段。
9
+
10
+ 先执行:
11
+
12
+ ```powershell
13
+ matrix workflow inspect
14
+ ```
15
+
16
+ 只有当系统明确报告 `matrix` 命令不存在时,才解析当前 Matrix Skill 目录并执行:
17
+
18
+ ```powershell
19
+ node <matrix-skill-directory>/scripts/matrix-runtime.mjs inspect
20
+ ```
21
+
22
+ 若没有活动变更,为开发请求创建 kebab-case 变更 ID:
23
+
24
+ ```powershell
25
+ matrix workflow init <change-id> --workflow <full|hotfix|tweak> --title "<title>"
26
+ ```
27
+
28
+ 阶段只能由 `matrix workflow transition` 推进;架构、范围或归档前必须征求用户确认。
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: matrix-archive
3
+ description: 关闭并归档已经验证的 Matrix 变更。
4
+ ---
5
+
6
+ # Matrix Archive
7
+
8
+ 仅在 verify 证据完整、用户确认归档时执行:
9
+
10
+ ```powershell
11
+ matrix workflow archive
12
+ ```
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: matrix-build
3
+ description: 用测试优先的纵向切片实现冻结的 Matrix 计划。
4
+ ---
5
+
6
+ # Matrix Build
7
+
8
+ 只实现冻结计划中的范围。使用 `implement` 与预先确认的 `tdd` 测试边界;持续记录 `artifacts/verification.md` 的 `## Build evidence`。如发现范围或设计变化,返回设计阶段。
9
+
10
+ ```powershell
11
+ matrix workflow guard build
12
+ matrix workflow transition verify
13
+ ```
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: matrix-claude
3
+ description: 将冻结的 Matrix 设计导出为受边界约束的 Claude Code 任务包,不改变 Matrix 工作流状态。
4
+ ---
5
+
6
+ # Matrix Claude Handoff
7
+
8
+ 这是可选的旁路操作,而不是 Matrix phase。它不得执行 `transition`、`archive`,也不得写入 `matrix.yaml`、`run-state.json` 或 `events.jsonl`。
9
+
10
+ 仅当设计阶段守卫已通过,并且用户明确选择由 Claude Code 实现时使用。先检查:
11
+
12
+ ```powershell
13
+ matrix workflow guard design
14
+ ```
15
+
16
+ 如果主命令确实不在 PATH 中,才使用已安装根 Matrix Skill 中的 bundled runtime;主命令已启动但返回业务错误时不得回退。
17
+
18
+ 导出通用任务包:
19
+
20
+ ```powershell
21
+ matrix workflow export --task-id <ID> --target generic
22
+ ```
23
+
24
+ 导出 FnSec 任务包时,只有在 `docs/tasks/active.md` 包含 `NO_ACTIVE_OBJECTIVE` 时才可使用 `--apply-fnsec-board`;现有任务绝不覆盖:
25
+
26
+ ```powershell
27
+ matrix workflow export --task-id <ID> --target fnsec --apply-fnsec-board
28
+ ```
29
+
30
+ 任务包会包含冻结的 proposal、design、plan,以及 Claude Code 回执所需的改动、验证、边界确认和 `NEEDS_DECISION` 项目。
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: matrix-design
3
+ description: 在实现前设计并冻结活动 Matrix 变更。
4
+ ---
5
+
6
+ # Matrix Design
7
+
8
+ 阅读 proposal,写入 `design.md`(`## Decisions`、`## Boundaries`、`## Test seams`、`## Risks`)与 `plan.md`(`## Steps`、`## Validation`、`## Stop conditions`)。重大范围或架构决策须先由用户确认。
9
+
10
+ ```powershell
11
+ matrix workflow guard design
12
+ matrix workflow transition build
13
+ ```
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: matrix-hotfix
3
+ description: 为可复现的小型缺陷运行受约束的 Matrix 工作流。
4
+ ---
5
+
6
+ # Matrix Hotfix
7
+
8
+ 从 `$matrix` 以 `hotfix` 工作流启动。使用 `diagnosing-bugs` 建立可复现的红色反馈循环;如果问题影响架构、公开 API、数据模式或多模块协调,要求升级为完整工作流。
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: matrix-open
3
+ description: 将开发请求澄清为有证据的 Matrix 提案。
4
+ ---
5
+
6
+ # Matrix Open
7
+
8
+ 读取 `matrix workflow inspect` 的活动变更。使用 `grill-with-docs` 澄清完整工作流请求;写入 `artifacts/proposal.md`,必须包含 `## Goal`、`## Scope`、`## Non-goals`、`## Acceptance`、`## Risks`。
9
+
10
+ ```powershell
11
+ matrix workflow guard open
12
+ matrix workflow transition design
13
+ ```
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: matrix-status
3
+ description: 检查活动 Matrix 工作流的磁盘状态、守卫与恢复命令。
4
+ ---
5
+
6
+ # Matrix Status
7
+
8
+ 执行:
9
+
10
+ ```powershell
11
+ matrix workflow inspect
12
+ ```
13
+
14
+ 报告变更 ID、工作流、阶段、守卫结果和 `next_skill`;不得修改状态。
@@ -0,0 +1,8 @@
1
+ ---
2
+ name: matrix-tweak
3
+ description: 为边界清晰的小改动运行轻量 Matrix 工作流。
4
+ ---
5
+
6
+ # Matrix Tweak
7
+
8
+ 从 `$matrix` 以 `tweak` 工作流启动。保持提案和设计简洁,但仍定义验收条件与测试边界。若涉及 API、模式、架构或跨模块协调,升级为完整工作流。
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: matrix-verify
3
+ description: 依据计划验证 Matrix 变更并审查差异。
4
+ ---
5
+
6
+ # Matrix Verify
7
+
8
+ 运行计划的验证命令,在 `verification.md` 写入 `## Test evidence`。使用 `code-review` 完成标准与规格双轴审查,并在 `## Review evidence` 记录结论。失败时回到产生问题的阶段。
9
+
10
+ ```powershell
11
+ matrix workflow guard verify
12
+ matrix workflow transition archive
13
+ ```
package/bin/matrix.js ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from "../src/cli.js";
4
+
5
+ main(process.argv.slice(2)).then((exitCode) => {
6
+ if (Number.isInteger(exitCode) && exitCode !== 0) process.exitCode = exitCode;
7
+ }).catch((error) => {
8
+ if (error.code === "CANCELLED") {
9
+ console.log("\nCancelled.");
10
+ process.exitCode = 0;
11
+ } else {
12
+ console.error(`\nMatrix error: ${error.message}`);
13
+ process.exitCode = 1;
14
+ }
15
+ });
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@rosenfan/matrix",
3
+ "version": "0.1.0",
4
+ "description": "Install and manage the Matrix workflow for Matt Pocock's agent skills.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "matrix": "bin/matrix.js"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "src/",
13
+ "assets/skills-zh-CN",
14
+ ".claude/skills/matrix",
15
+ ".claude/skills/matrix-open",
16
+ ".claude/skills/matrix-design",
17
+ ".claude/skills/matrix-build",
18
+ ".claude/skills/matrix-verify",
19
+ ".claude/skills/matrix-archive",
20
+ ".claude/skills/matrix-hotfix",
21
+ ".claude/skills/matrix-tweak",
22
+ ".claude/skills/matrix-status",
23
+ ".claude/skills/matrix-claude",
24
+ "!.claude/skills/**/__pycache__",
25
+ "!.claude/skills/**/scripts/test_*.py",
26
+ "README.md",
27
+ "README.zh-CN.md",
28
+ "LICENSE"
29
+ ],
30
+ "scripts": {
31
+ "test": "node --test",
32
+ "pack:check": "npm pack --dry-run",
33
+ "prepublishOnly": "npm run test && npm run pack:check"
34
+ },
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/Rosenfan/Matrix.git"
41
+ },
42
+ "bugs": {
43
+ "url": "https://github.com/Rosenfan/Matrix/issues"
44
+ },
45
+ "homepage": "https://github.com/Rosenfan/Matrix#readme",
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "keywords": [
50
+ "claude-code",
51
+ "agent-skills",
52
+ "workflow",
53
+ "tdd",
54
+ "ai"
55
+ ]
56
+ }
package/src/catalog.js ADDED
@@ -0,0 +1,87 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ export const LANGUAGE_IDS = ["en", "zh-CN"];
8
+ export const PLATFORM_IDS = ["claude-code", "codex"];
9
+ export const MATRIX_SKILLS = [
10
+ "matrix", "matrix-open", "matrix-design", "matrix-build", "matrix-verify",
11
+ "matrix-archive", "matrix-hotfix", "matrix-tweak", "matrix-status", "matrix-claude"
12
+ ];
13
+ export const MATT_SKILLS = [
14
+ "grill-with-docs", "domain-modeling", "research", "wayfinder", "prototype",
15
+ "codebase-design", "implement", "tdd", "code-review", "diagnosing-bugs",
16
+ "resolving-merge-conflicts", "improve-codebase-architecture"
17
+ ];
18
+
19
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
20
+ const packageManifest = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
21
+
22
+ export const PLATFORMS = {
23
+ "claude-code": {
24
+ id: "claude-code", name: "Claude Code", skillsCliAgent: "claude-code",
25
+ skillRoot(base) { return path.join(base, ".claude", "skills"); },
26
+ legacyRoots() { return []; }
27
+ },
28
+ codex: {
29
+ id: "codex", name: "Codex", skillsCliAgent: "codex",
30
+ skillRoot(base) { return path.join(base, ".agents", "skills"); },
31
+ legacyRoots(base) { return [path.join(base, ".codex", "skills")]; }
32
+ }
33
+ };
34
+
35
+ export function sourceSkillsRoot(language = "en") {
36
+ const localized = language === "zh-CN"
37
+ ? path.join(packageRoot, "assets", "skills-zh-CN")
38
+ : path.join(packageRoot, ".claude", "skills");
39
+ if (!fs.existsSync(localized)) throw new Error(`Matrix package is incomplete: ${language} assets are missing.`);
40
+ return localized;
41
+ }
42
+
43
+ export function scopeBase(scope, projectRoot, home = os.homedir()) {
44
+ return scope === "global" ? home : projectRoot;
45
+ }
46
+
47
+ export function installationRoot(scope, projectRoot, home) {
48
+ return path.join(scopeBase(scope, projectRoot, home), ".matrix");
49
+ }
50
+
51
+ export function manifestPath(scope, projectRoot, home) {
52
+ return path.join(installationRoot(scope, projectRoot, home), "installation.json");
53
+ }
54
+
55
+ export function hashDirectory(directory, extraFiles = []) {
56
+ const hash = crypto.createHash("sha256");
57
+ const files = new Map();
58
+ const visit = (current, relative = "") => {
59
+ for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
60
+ const child = path.join(current, entry.name);
61
+ const childRelative = path.join(relative, entry.name).replaceAll("\\", "/");
62
+ if (entry.isDirectory()) visit(child, childRelative);
63
+ else if (entry.isFile()) {
64
+ files.set(childRelative, fs.readFileSync(child));
65
+ }
66
+ }
67
+ };
68
+ visit(directory);
69
+ for (const extra of extraFiles) files.set(extra.path, Buffer.from(extra.contents));
70
+ for (const [relative, contents] of [...files.entries()].sort(([left], [right]) => left.localeCompare(right))) {
71
+ hash.update(`${relative}\0`);
72
+ hash.update(contents);
73
+ }
74
+ return hash.digest("hex");
75
+ }
76
+
77
+ export function workflowRuntimeSource() { return path.join(packageRoot, "src", "workflow.js"); }
78
+
79
+ export function releaseCatalog(language = "en") {
80
+ if (!LANGUAGE_IDS.includes(language)) throw new Error(`Unsupported language: ${language}`);
81
+ const root = sourceSkillsRoot(language);
82
+ const missing = MATRIX_SKILLS.filter((skill) => !fs.existsSync(path.join(root, skill, "SKILL.md")));
83
+ if (missing.length) throw new Error(`Matrix package is incomplete. Missing: ${missing.join(", ")}`);
84
+ const runtime = fs.readFileSync(workflowRuntimeSource());
85
+ const skills = Object.fromEntries(MATRIX_SKILLS.map((skill) => [skill, hashDirectory(path.join(root, skill), [{ path: "scripts/matrix-runtime.mjs", contents: runtime }])]));
86
+ return { language, root, version: packageManifest.version, skills, digest: crypto.createHash("sha256").update(JSON.stringify(skills)).digest("hex") };
87
+ }
package/src/cli.js ADDED
@@ -0,0 +1,117 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { PLATFORMS } from "./catalog.js";
5
+ import { createDistribution } from "./distribution.js";
6
+ import { text } from "./messages.js";
7
+ import { createUi } from "./ui.js";
8
+ import { invoke } from "./workflow.js";
9
+
10
+ const packagePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
11
+ const version = JSON.parse(fs.readFileSync(packagePath, "utf8")).version;
12
+ const HELP = `Matrix ${version}\n\nUsage: matrix init [directory] [options]\n matrix doctor [directory] [options]\n\nOptions:\n --language <en|zh-CN>\n --scope <project|global>\n --platform <claude-code|codex> (repeatable)\n --mode <copy|symlink>\n --with-mattpocock | --without-mattpocock\n --force --yes, -y --dry-run --json --no-color`;
13
+
14
+ function parse(argv) {
15
+ const result = { command: null, directory: null, scope: null, language: null, platforms: [], mode: "copy", matt: null, matrixPolicy: "safe", yes: false, dryRun: false, json: false, color: true };
16
+ const values = [...argv];
17
+ if (values[0] && !values[0].startsWith("-")) result.command = values.shift();
18
+ while (values.length) {
19
+ const token = values.shift();
20
+ if (!token.startsWith("-") && !result.directory) result.directory = token;
21
+ else if (token === "--scope") result.scope = values.shift();
22
+ else if (token === "--language") result.language = values.shift();
23
+ else if (token === "--platform") result.platforms.push(values.shift());
24
+ else if (token === "--mode") result.mode = values.shift();
25
+ else if (token === "--with-mattpocock") result.matt = "missing";
26
+ else if (token === "--without-mattpocock") result.matt = "none";
27
+ else if (token === "--force") result.matrixPolicy = "replace";
28
+ else if (["--yes", "-y"].includes(token)) result.yes = true;
29
+ else if (token === "--dry-run") result.dryRun = true;
30
+ else if (token === "--json") result.json = true;
31
+ else if (token === "--no-color") result.color = false;
32
+ else if (["--help", "-h"].includes(token)) result.help = true;
33
+ else if (["--version", "-v"].includes(token)) result.version = true;
34
+ else throw new Error(`Unknown option: ${token}`);
35
+ }
36
+ return result;
37
+ }
38
+
39
+ function projectRoot(directory) {
40
+ const value = path.resolve(directory ?? process.cwd());
41
+ if (!fs.existsSync(value) || !fs.statSync(value).isDirectory()) throw new Error(`Not a directory: ${value}`);
42
+ return value;
43
+ }
44
+
45
+ function labelsFor(language) { return { yes: text(language, "yes"), no: text(language, "no") }; }
46
+
47
+ function localizedState(language, state) {
48
+ return text(language, ({ missing: "stateMissing", matching: "stateMatching", complete: "stateComplete", "user-modified": "stateUserModified", partial: "statePartial", damaged: "statePartial", newer: "stateMatching", "outdated-unchanged": "stateOutdated" })[state] ?? state);
49
+ }
50
+
51
+ function localizedAction(language, action) {
52
+ return text(language, ({ install: "actionInstall", keep: "actionKeep", "backup-replace": "actionBackupReplace", repair: "actionRepair", blocked: "actionBlocked" })[action] ?? action);
53
+ }
54
+
55
+ async function intentFrom(options, ui) {
56
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && !options.json);
57
+ const language = options.language ?? (options.yes ? "en" : await ui.select("Language / 语言", [{ label: "English", value: "en" }, { label: "中文", value: "zh-CN" }]));
58
+ const scope = options.scope ?? (options.yes ? "project" : await ui.select(text(language, "scopeQuestion"), [{ label: text(language, "project"), value: "project" }, { label: text(language, "global"), value: "global" }]));
59
+ let platforms = options.platforms;
60
+ if (!platforms.length && interactive && !options.yes) platforms = await ui.selectMany(text(language, "platformsQuestion"), [{ label: "Claude Code", value: "claude-code" }, { label: "Codex", value: "codex" }]);
61
+ const matt = options.matt ?? (interactive && !options.yes ? (await ui.confirm(text(language, "mattQuestion"), true, labelsFor(language)) ? "missing" : "none") : "missing");
62
+ return { projectRoot: projectRoot(options.directory), language, scope, platforms, mode: options.mode, matt, matrixPolicy: options.matrixPolicy, dryRun: options.dryRun };
63
+ }
64
+
65
+ function render(ui, evaluation) {
66
+ const language = evaluation.intent?.language ?? "en";
67
+ const actions = (evaluation.actions ?? []).map((item) => `${item.platform}: ${localizedAction(language, item.matrix)}`).join(", ");
68
+ ui.info(actions ? `${text(language, "preview")}: ${actions}` : evaluation.code);
69
+ for (const item of evaluation.observations ?? []) ui.muted(`${item.name}: ${text(language, "matrix")} ${localizedState(language, item.matrix)};${text(language, "matt")} ${localizedState(language, item.matt.state)}${item.matt.inherited ? ` (${text(language, "global")})` : ""}`);
70
+ for (const item of evaluation.diagnostics ?? []) ui.warn(item.code === "USER_MODIFIED_BLOCKED" ? text(language, "replaceQuestion") : item.message ?? item.code);
71
+ for (const item of evaluation.diagnosis ?? []) (item.code === "MATT_INHERITED" ? ui.muted : ui.warn)(item.message ?? item.code);
72
+ }
73
+
74
+ async function init(options) {
75
+ const ui = createUi({ color: options.color && !options.json });
76
+ if (!options.json) await ui.banner();
77
+ const intent = await intentFrom(options, ui);
78
+ const distribution = createDistribution();
79
+ let evaluation = distribution.evaluate(intent);
80
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY && !options.json);
81
+ if (!evaluation.ok && evaluation.code === "USER_MODIFIED_BLOCKED" && interactive && !options.yes && await ui.confirm(text(intent.language, "replaceQuestion"), false, labelsFor(intent.language))) evaluation = distribution.evaluate({ ...intent, matrixPolicy: "replace" });
82
+ if (options.json) { console.log(JSON.stringify(evaluation)); return evaluation.ok ? 0 : 2; }
83
+ render(ui, evaluation);
84
+ if (!evaluation.ok) return 2;
85
+ if (!options.yes && interactive && !options.dryRun && !await ui.confirm(text(intent.language, "confirm"), true, labelsFor(intent.language))) return 0;
86
+ const result = distribution.commit(evaluation.plan);
87
+ if (!result.ok) { ui.warn(result.error ?? result.code); return 2; }
88
+ if (result.code === "PARTIAL") { ui.warn(result.recovery); return 1; }
89
+ ui.success(options.dryRun ? text(intent.language, "dryRunComplete") : text(intent.language, "ready"));
90
+ ui.muted(evaluation.intent.platforms.map((id) => `${PLATFORMS[id].name}: $matrix <request>`).join("\n"));
91
+ return 0;
92
+ }
93
+
94
+ async function doctor(options) {
95
+ const ui = createUi({ color: options.color && !options.json });
96
+ const intent = await intentFrom({ ...options, yes: true }, ui);
97
+ const evaluation = createDistribution().diagnose(intent);
98
+ if (options.json) console.log(JSON.stringify(evaluation));
99
+ else { await ui.banner(); ui.info(text(intent.language, "doctor")); render(ui, evaluation); }
100
+ return evaluation.ok ? 0 : 2;
101
+ }
102
+
103
+ export async function main(argv) {
104
+ if (argv[0] === "workflow") {
105
+ const json = argv.includes("--json");
106
+ const result = invoke(argv.slice(1).filter((value) => value !== "--json"));
107
+ if (json) console.log(JSON.stringify(result));
108
+ else console.log(result.ok ? JSON.stringify(result, null, 2) : `MATRIX ERROR [${result.code}]: ${result.message}`);
109
+ return result.ok ? 0 : 2;
110
+ }
111
+ const options = parse(argv);
112
+ if (options.version) { console.log(version); return 0; }
113
+ if (options.help || !options.command) { console.log(HELP); return 0; }
114
+ if (options.command === "init") return init(options);
115
+ if (options.command === "doctor") return doctor(options);
116
+ throw new Error(`Unknown command: ${options.command}. Run matrix --help.`);
117
+ }
@@ -0,0 +1,192 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import {
6
+ MATRIX_SKILLS, MATT_SKILLS, PLATFORM_IDS, PLATFORMS, hashDirectory,
7
+ installationRoot, manifestPath, releaseCatalog, scopeBase, sourceSkillsRoot, workflowRuntimeSource
8
+ } from "./catalog.js";
9
+ import { createMattAdapter } from "./matt.js";
10
+ import { text } from "./messages.js";
11
+
12
+ const VERSION = 1;
13
+ const digest = (value) => crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex");
14
+ const exists = (value) => fs.existsSync(value);
15
+ const pathExists = (value) => { try { fs.lstatSync(value); return true; } catch { return false; } };
16
+ const nowStamp = () => new Date().toISOString().replace(/[:.]/g, "-");
17
+ const transactionPath = (intent) => path.join(installationRoot(intent.scope, intent.projectRoot, intent.home), "transaction.json");
18
+ const writeJson = (file, value) => {
19
+ fs.mkdirSync(path.dirname(file), { recursive: true });
20
+ const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
21
+ fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`);
22
+ fs.renameSync(temporary, file);
23
+ };
24
+ const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; } };
25
+ const removeEntry = (value) => { if (pathExists(value)) fs.rmSync(value, { recursive: true, force: true }); };
26
+ const removeStaging = (stagingRoot) => {
27
+ removeEntry(stagingRoot);
28
+ const parent = path.dirname(stagingRoot);
29
+ if (exists(parent) && fs.readdirSync(parent).length === 0) fs.rmdirSync(parent);
30
+ };
31
+ const compareVersions = (left, right) => {
32
+ const parse = (value) => String(value ?? "0.0.0").split("-")[0].split(".").map((part) => Number(part) || 0);
33
+ const a = parse(left); const b = parse(right);
34
+ 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;
35
+ return 0;
36
+ };
37
+ const skillPresent = (root, skill) => exists(path.join(root, skill, "SKILL.md"));
38
+ const isBrokenLink = (value) => { try { return fs.lstatSync(value).isSymbolicLink() && !exists(value); } catch { return false; } };
39
+
40
+ function readManifest(intent) { return readJson(manifestPath(intent.scope, intent.projectRoot, intent.home)); }
41
+
42
+ function detectPlatforms(projectRoot, scope, home) {
43
+ const bases = [scopeBase(scope, projectRoot, home)];
44
+ if (scope === "project") bases.push(home);
45
+ return PLATFORM_IDS.filter((id) => bases.some((base) => exists(PLATFORMS[id].skillRoot(base))));
46
+ }
47
+
48
+ function normalizeIntent(raw) {
49
+ const scope = raw.scope ?? "project";
50
+ const platforms = [...new Set(raw.platforms ?? [])];
51
+ if (!["project", "global"].includes(scope)) return { error: "INVALID_INTENT", detail: "scope" };
52
+ if (!["copy", "symlink"].includes(raw.mode ?? "copy")) return { error: "INVALID_INTENT", detail: "mode" };
53
+ if (!["en", "zh-CN"].includes(raw.language ?? "en")) return { error: "INVALID_INTENT", detail: "language" };
54
+ if (platforms.some((id) => !PLATFORM_IDS.includes(id))) return { error: "UNVERIFIED_PLATFORM" };
55
+ return { projectRoot: path.resolve(raw.projectRoot ?? process.cwd()), home: raw.home ?? os.homedir(), scope, platforms,
56
+ language: raw.language ?? "en", mode: raw.mode ?? "copy", matrixPolicy: raw.matrixPolicy ?? "safe",
57
+ matt: raw.matt ?? "none", dryRun: Boolean(raw.dryRun) };
58
+ }
59
+
60
+ function observePlatform(intent, catalog, manifest, platformId) {
61
+ const platform = PLATFORMS[platformId]; const base = scopeBase(intent.scope, intent.projectRoot, intent.home);
62
+ const root = platform.skillRoot(base); const records = manifest?.platforms?.[platformId]?.skills ?? {};
63
+ const skills = MATRIX_SKILLS.map((skill) => {
64
+ const target = path.join(root, skill);
65
+ if (!pathExists(target)) return { skill, target, state: "missing" };
66
+ if (isBrokenLink(target) || !exists(path.join(target, "SKILL.md"))) return { skill, target, state: "damaged" };
67
+ const actual = hashDirectory(target); const recorded = records[skill]?.hash; const expected = catalog.skills[skill];
68
+ if (!recorded) return { skill, target, state: "user-modified", actual };
69
+ if (actual === expected) return { skill, target, state: "matching", actual };
70
+ if (actual === recorded) return { skill, target, state: compareVersions(manifest?.catalogVersion, catalog.version) > 0 ? "newer" : "outdated-unchanged", actual };
71
+ return { skill, target, state: "user-modified", actual };
72
+ });
73
+ const states = new Set(skills.map((item) => item.state));
74
+ const matrix = states.size === 1 && states.has("matching") ? "matching" : states.has("user-modified") ? "user-modified"
75
+ : states.has("missing") && states.size === 1 ? "missing" : states.has("newer") ? "newer"
76
+ : states.has("damaged") ? "damaged" : states.has("outdated-unchanged") ? "outdated-unchanged" : "partial";
77
+ const globalRoot = platform.skillRoot(intent.home);
78
+ const mattPresent = MATT_SKILLS.filter((skill) => skillPresent(root, skill));
79
+ const globalMatt = intent.scope === "project" ? MATT_SKILLS.filter((skill) => skillPresent(globalRoot, skill)) : [];
80
+ const availableMatt = new Set([...mattPresent, ...globalMatt]); const missing = MATT_SKILLS.filter((skill) => !availableMatt.has(skill));
81
+ const inherited = globalMatt.filter((skill) => !mattPresent.includes(skill)).length;
82
+ return { id: platformId, name: platform.name, root, skills, matrix, matt: { present: mattPresent.length, missing, total: MATT_SKILLS.length, inherited,
83
+ state: availableMatt.size === MATT_SKILLS.length ? "complete" : availableMatt.size ? "partial" : "missing" } };
84
+ }
85
+
86
+ function actionFor(observation, policy) {
87
+ if (observation.matrix === "matching" || observation.matrix === "newer") return "keep";
88
+ if (observation.matrix === "missing") return "install";
89
+ if (observation.matrix === "outdated-unchanged") return "backup-replace";
90
+ if (["damaged", "user-modified"].includes(observation.matrix)) return policy === "replace" ? "backup-replace" : "blocked";
91
+ return "repair";
92
+ }
93
+
94
+ export function recoverPendingTransaction(intent) {
95
+ const file = transactionPath(intent); const journal = readJson(file);
96
+ if (!journal) return { ok: true, recovered: false };
97
+ try {
98
+ if (journal.state === "committed") {
99
+ removeStaging(journal.stagingRoot);
100
+ for (const operation of journal.operations ?? []) removeEntry(operation.backup);
101
+ removeEntry(journal.manifest?.backup); removeEntry(file);
102
+ return { ok: true, recovered: true, receipt: `Finalized completed Matrix transaction ${journal.id}.` };
103
+ }
104
+ for (const operation of [...(journal.operations ?? [])].reverse()) {
105
+ if (operation.status !== "pending") removeEntry(operation.target);
106
+ if (operation.backup && pathExists(operation.backup)) { fs.mkdirSync(path.dirname(operation.target), { recursive: true }); fs.renameSync(operation.backup, operation.target); }
107
+ }
108
+ if (journal.manifest?.backup && pathExists(journal.manifest.backup)) {
109
+ removeEntry(journal.manifest.path); fs.mkdirSync(path.dirname(journal.manifest.path), { recursive: true }); fs.renameSync(journal.manifest.backup, journal.manifest.path);
110
+ } else if (journal.manifest?.written && !journal.manifest?.existed) removeEntry(journal.manifest.path);
111
+ removeStaging(journal.stagingRoot); removeEntry(file);
112
+ return { ok: true, recovered: true, receipt: `Recovered interrupted Matrix transaction ${journal.id}.` };
113
+ } catch (error) { return { ok: false, code: "RECOVERY_REQUIRED", recovery: `Keep ${file} and restore the paths recorded there before retrying.`, error: error.message }; }
114
+ }
115
+
116
+ export function createDistribution({ failAt, mattAdapter = createMattAdapter() } = {}) {
117
+ const evaluate = (rawIntent) => {
118
+ const intent = normalizeIntent(rawIntent); const language = intent.language ?? rawIntent.language ?? "en";
119
+ if (intent.error) return { ok: false, code: intent.error, diagnostics: [{ code: intent.error, message: text(language, "noPlatform") }] };
120
+ 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; }
121
+ let catalog; try { catalog = releaseCatalog(intent.language); } catch (error) { return { ok: false, code: "PACKAGE_INCOMPLETE", intent, diagnostics: [{ code: "PACKAGE_INCOMPLETE", message: error.message }] }; }
122
+ const manifest = readManifest(intent); const observations = intent.platforms.map((id) => observePlatform(intent, catalog, manifest, id));
123
+ const actions = observations.map((item) => ({ platform: item.id, matrix: actionFor(item, intent.matrixPolicy), matt: item.matt.state === "complete" || intent.matt === "none" ? { kind: "keep", skills: [] } : { kind: "install-missing", skills: item.matt.missing } }));
124
+ const blocked = actions.filter((item) => item.matrix === "blocked"); const plan = { version: VERSION, intent, catalogDigest: catalog.digest, manifestPath: manifestPath(intent.scope, intent.projectRoot, intent.home), actions, observations, preconditionDigest: digest(observations) };
125
+ plan.seal = digest({ intent, catalogDigest: plan.catalogDigest, actions, preconditionDigest: plan.preconditionDigest });
126
+ return { ok: blocked.length === 0, code: blocked.length ? "USER_MODIFIED_BLOCKED" : "OK", intent, observations, actions, plan, diagnostics: blocked.map((item) => ({ code: "USER_MODIFIED_BLOCKED", platform: item.platform, message: `${item.platform}: existing Matrix assets are not proven managed.` })), summary: `${text(intent.language, "preview")}: ${actions.map((item) => `${item.platform}: ${item.matrix}`).join(", ")}` };
127
+ };
128
+
129
+ return { evaluate,
130
+ diagnose(rawIntent) {
131
+ const evaluation = evaluate(rawIntent); if (!evaluation.intent) return evaluation;
132
+ const diagnostics = []; const pending = readJson(transactionPath(evaluation.intent)); const manifest = readManifest(evaluation.intent);
133
+ if (pending) diagnostics.push({ code: "RECOVERY_REQUIRED", message: text(evaluation.intent.language, "transactionPending") });
134
+ if (!manifest) diagnostics.push({ code: "MANIFEST_MISSING", message: text(evaluation.intent.language, "manifestMissing") });
135
+ for (const observation of evaluation.observations ?? []) {
136
+ diagnostics.push({ code: `MATRIX_${observation.matrix.toUpperCase().replaceAll("-", "_")}`, platform: observation.id, message: `${observation.name}: Matrix is ${observation.matrix}.` });
137
+ const matrixRoot = path.join(observation.root, "matrix");
138
+ if (pathExists(matrixRoot) && (!exists(matrixRoot) || !exists(path.join(matrixRoot, "scripts", "matrix-runtime.mjs")))) diagnostics.push({ code: "RUNTIME_MISSING", platform: observation.id, message: `${observation.name}: ${text(evaluation.intent.language, "runtimeMissing")}` });
139
+ 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}` });
140
+ if (observation.id === "codex" && PLATFORMS.codex.legacyRoots(scopeBase(evaluation.intent.scope, evaluation.intent.projectRoot, evaluation.intent.home)).some(exists)) diagnostics.push({ code: "CODEX_LEGACY_DETECTED", platform: observation.id, message: "Codex legacy .codex/skills was detected; Matrix will not write there." });
141
+ diagnostics.push({ code: `MATT_${observation.matt.state.toUpperCase()}`, platform: observation.id, message: `${observation.name}: Matt Pocock skills are ${observation.matt.state}.` });
142
+ if (observation.matt.inherited) diagnostics.push({ code: "MATT_INHERITED", platform: observation.id, message: `${observation.name}: ${observation.matt.inherited} ${text(evaluation.intent.language, "mattInherited")}` });
143
+ }
144
+ return { ...evaluation, diagnosis: diagnostics };
145
+ },
146
+ commit(plan) {
147
+ if (!plan?.seal || plan.seal !== digest({ intent: plan.intent, catalogDigest: plan.catalogDigest, actions: plan.actions, preconditionDigest: plan.preconditionDigest })) return { ok: false, code: "INVALID_INTENT" };
148
+ const recovery = recoverPendingTransaction(plan.intent); if (!recovery.ok) return recovery;
149
+ const refreshed = evaluate({ ...plan.intent, platforms: plan.intent.platforms });
150
+ if (!refreshed.plan || refreshed.plan.preconditionDigest !== plan.preconditionDigest || refreshed.plan.catalogDigest !== plan.catalogDigest) return { ok: false, code: "PLAN_STALE" };
151
+ if (!refreshed.ok) return { ok: false, code: refreshed.code, diagnostics: refreshed.diagnostics };
152
+ if (plan.intent.dryRun) return { ok: true, code: "DRY_RUN", actions: plan.actions };
153
+ 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);
154
+ const journal = { id, state: "in-progress", stagingRoot, manifest: { path: plan.manifestPath, existed: pathExists(plan.manifestPath), backup: null, written: false }, operations: [] };
155
+ 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" });
156
+ const persist = () => writeJson(journalFile, journal);
157
+ try {
158
+ persist(); const catalog = releaseCatalog(plan.intent.language); const stagedPlatforms = new Map();
159
+ for (const item of refreshed.observations) {
160
+ if (plan.actions.find((entry) => entry.platform === item.id).matrix === "keep") continue;
161
+ const platformStage = path.join(stagingRoot, item.id);
162
+ 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")); 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}`); }
163
+ stagedPlatforms.set(item.id, platformStage);
164
+ }
165
+ failAt?.("after-stage"); let operationIndex = 0;
166
+ for (const item of refreshed.observations) {
167
+ if (plan.actions.find((entry) => entry.platform === item.id).matrix === "keep") continue;
168
+ fs.mkdirSync(item.root, { recursive: true });
169
+ for (const skill of MATRIX_SKILLS) {
170
+ const operation = journal.operations[operationIndex++]; const target = operation.target; operation.status = "backing-up";
171
+ if (pathExists(target)) operation.backup = path.join(root, "backups", id, item.id, skill); persist();
172
+ if (operation.backup) { fs.mkdirSync(path.dirname(operation.backup), { recursive: true }); fs.renameSync(target, operation.backup); }
173
+ operation.status = "backed-up"; persist(); failAt?.("after-backup", { platform: item.id, skill });
174
+ const staged = path.join(stagedPlatforms.get(item.id), skill);
175
+ 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);
176
+ operation.status = "committed"; persist(); failAt?.("after-commit", { platform: item.id, skill });
177
+ }
178
+ }
179
+ 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); }
180
+ const records = Object.fromEntries(refreshed.observations.map((item) => [item.id, { root: item.root, skills: Object.fromEntries(MATRIX_SKILLS.map((skill) => [skill, { hash: catalog.skills[skill] }])) }]));
181
+ writeJson(plan.manifestPath, { version: VERSION, catalogVersion: catalog.version, language: plan.intent.language, mode: plan.intent.mode, catalogDigest: catalog.digest, platforms: records }); journal.manifest.written = true; persist(); failAt?.("after-manifest"); journal.state = "committed"; persist();
182
+ removeStaging(stagingRoot); removeEntry(journal.manifest.backup); removeEntry(journalFile);
183
+ const mattActions = plan.actions.filter((item) => item.matt.kind === "install-missing"); const mattResults = [];
184
+ for (const action of mattActions) { const result = mattAdapter.installMissing({ projectRoot: plan.intent.projectRoot, platforms: [action.platform], skills: action.matt.skills, global: plan.intent.scope === "global" }); mattResults.push({ platform: action.platform, ...result }); }
185
+ const postcondition = evaluate({ ...plan.intent, platforms: plan.intent.platforms }); const incomplete = mattActions.length ? postcondition.observations.filter((item) => item.matt.state !== "complete") : [];
186
+ const backups = journal.operations.filter((item) => item.backup).map((item) => ({ target: item.target, backup: item.backup }));
187
+ if (mattResults.some((item) => !item.ok) || incomplete.length) return { ok: true, code: "PARTIAL", committed: journal.operations.map((item) => item.target), backups, recovery: "Matrix is installed. Run matrix init --with-mattpocock after resolving the Matt skills installation.", matt: mattResults, recoveryReceipt: recovery.receipt };
188
+ return { ok: true, code: "OK", committed: journal.operations.map((item) => item.target), backups, matt: mattResults, recoveryReceipt: recovery.receipt };
189
+ } 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 }; }
190
+ }
191
+ };
192
+ }