@rosenfan/matrix 0.1.5 → 0.1.7
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/README.md +22 -19
- package/README.zh-CN.md +21 -18
- package/assets/skills/matrix/SKILL.md +6 -5
- package/assets/skills/matrix-archive/SKILL.md +2 -0
- package/assets/skills/matrix-build/SKILL.md +1 -1
- package/assets/skills/matrix-design/SKILL.md +12 -6
- package/assets/skills/matrix-handoff/SKILL.md +89 -0
- package/assets/skills/matrix-handoff/agents/openai.yaml +4 -0
- package/assets/skills/matrix-open/SKILL.md +13 -2
- package/assets/skills/matrix-verify/SKILL.md +4 -4
- package/assets/skills-zh-CN/matrix/SKILL.md +84 -83
- package/assets/skills-zh-CN/matrix-archive/SKILL.md +2 -0
- package/assets/skills-zh-CN/matrix-build/SKILL.md +41 -41
- package/assets/skills-zh-CN/matrix-design/SKILL.md +41 -24
- package/assets/skills-zh-CN/matrix-handoff/SKILL.md +79 -0
- package/assets/skills-zh-CN/matrix-open/SKILL.md +40 -30
- package/package.json +1 -1
- package/src/catalog.js +10 -2
- package/src/cli.js +303 -233
- package/src/distribution.js +1 -1
- package/src/installer.js +45 -45
- package/src/matt-catalog.mjs +35 -33
- package/src/matt.js +122 -127
- package/src/messages.js +46 -37
- package/src/npm-invocation.js +23 -0
- package/src/project-index.js +43 -0
- package/src/runtime-resolver.js +76 -76
- package/src/self-update.js +8 -1
- package/src/ui.js +97 -89
- package/src/workflow.js +36 -4
- package/assets/skills/matrix-claude/SKILL.md +0 -98
- package/assets/skills/matrix-claude/agents/openai.yaml +0 -4
- package/assets/skills-zh-CN/matrix-claude/SKILL.md +0 -33
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export const projectsIndexPath = (home = os.homedir()) => path.join(home, ".matrix", "projects.json");
|
|
6
|
+
|
|
7
|
+
function writeAtomically(file, value) {
|
|
8
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
9
|
+
const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
10
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`);
|
|
11
|
+
fs.renameSync(temporary, file);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function readProjectsIndex({ home = os.homedir() } = {}) {
|
|
15
|
+
try {
|
|
16
|
+
const parsed = JSON.parse(fs.readFileSync(projectsIndexPath(home), "utf8"));
|
|
17
|
+
if (!Array.isArray(parsed)) return { ok: false, code: "PROJECTS_INDEX_CORRUPT", projects: [] };
|
|
18
|
+
const projects = parsed.filter((entry) => entry && typeof entry.path === "string");
|
|
19
|
+
return { ok: true, projects };
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error.code === "ENOENT") return { ok: true, projects: [] };
|
|
22
|
+
return { ok: false, code: "PROJECTS_INDEX_CORRUPT", projects: [] };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function registerProject({ projectRoot, scope, platforms, language, home = os.homedir() } = {}) {
|
|
27
|
+
const resolved = path.resolve(projectRoot);
|
|
28
|
+
const { ok, projects } = readProjectsIndex({ home });
|
|
29
|
+
const next = (ok ? projects : []).filter((entry) => entry.path !== resolved);
|
|
30
|
+
next.unshift({ path: resolved, scope, platforms, language, updatedAt: new Date().toISOString().replace(/\.\d{3}Z$/, "Z") });
|
|
31
|
+
writeAtomically(projectsIndexPath(home), next);
|
|
32
|
+
return { ok: true, projects: next };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function pruneProject({ projectRoot, home = os.homedir() } = {}) {
|
|
36
|
+
const resolved = path.resolve(projectRoot);
|
|
37
|
+
const { ok, projects } = readProjectsIndex({ home });
|
|
38
|
+
if (!ok) return { ok: false, removed: null };
|
|
39
|
+
const removed = projects.find((entry) => entry.path === resolved) ?? null;
|
|
40
|
+
if (!removed) return { ok: true, removed: null };
|
|
41
|
+
writeAtomically(projectsIndexPath(home), projects.filter((entry) => entry.path !== resolved));
|
|
42
|
+
return { ok: true, removed };
|
|
43
|
+
}
|
package/src/runtime-resolver.js
CHANGED
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { MATRIX_VERSION, PLATFORMS, hashDirectory, releaseCatalog } from "./catalog.js";
|
|
4
|
-
|
|
5
|
-
function readJson(file) {
|
|
6
|
-
try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; }
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function installationScope(projectRoot) {
|
|
10
|
-
try {
|
|
11
|
-
const line = fs.readFileSync(path.join(projectRoot, ".matrix", "config.yaml"), "utf8").split(/\r?\n/).find((value) => value.startsWith("installation_scope:"));
|
|
12
|
-
return line?.split(":").slice(1).join(":").trim() === "global" ? "global" : "project";
|
|
13
|
-
} catch { return "project"; }
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function runtimeCommand(runtime) {
|
|
17
|
-
const quoted = process.platform === "win32" ? `"${runtime}"` : `'${runtime.replaceAll("'", "'\\''")}'`;
|
|
18
|
-
return `node ${quoted}`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function resolveProjectRuntime({ projectRoot = process.cwd() } = {}) {
|
|
22
|
-
const root = path.resolve(projectRoot);
|
|
23
|
-
const manifest = readJson(path.join(root, ".matrix", "installation.json"));
|
|
24
|
-
if (!manifest) return installationScope(root) === "global"
|
|
25
|
-
? { ok: true, code: "GLOBAL_INSTALLATION", runtime: null }
|
|
26
|
-
: { ok: true, code: "PROJECT_INSTALLATION_ABSENT", runtime: null };
|
|
27
|
-
const candidates = [];
|
|
28
|
-
let currentCatalog = null;
|
|
29
|
-
let catalogUnavailable = false;
|
|
30
|
-
try { currentCatalog = releaseCatalog(manifest.language ?? "en"); } catch { catalogUnavailable = true; }
|
|
31
|
-
for (const platform of
|
|
32
|
-
const expected = manifest.platforms?.[platform]?.skills?.matrix?.hash;
|
|
33
|
-
if (!expected) continue;
|
|
34
|
-
const matrixRoot = path.join(PLATFORMS[platform].skillRoot(root), "matrix");
|
|
35
|
-
const runtime = path.join(matrixRoot, "scripts", "matrix-runtime.mjs");
|
|
36
|
-
if (!fs.existsSync(runtime)) { candidates.push({ platform, matrixRoot, runtime, reason: "missing" }); continue; }
|
|
37
|
-
try {
|
|
38
|
-
const actual = hashDirectory(matrixRoot);
|
|
39
|
-
if (actual === expected) {
|
|
40
|
-
if (manifest.catalogVersion !== MATRIX_VERSION) {
|
|
41
|
-
const catalogVersion = manifest.catalogVersion ?? null;
|
|
42
|
-
return {
|
|
43
|
-
ok: false,
|
|
44
|
-
code: catalogVersion ? "PROJECT_RUNTIME_VERSION_MISMATCH" : "PROJECT_RUNTIME_VERSION_UNKNOWN",
|
|
45
|
-
message: catalogVersion
|
|
46
|
-
? `The project Matrix Runtime is ${catalogVersion}, while this launcher is ${MATRIX_VERSION}. The project remains authoritative, but a different-release Runtime requires explicit invocation.`
|
|
47
|
-
: `The project Matrix Runtime has no release identity. The project remains authoritative, but an unidentified Runtime requires explicit invocation.`,
|
|
48
|
-
platform,
|
|
49
|
-
runtime,
|
|
50
|
-
catalogVersion,
|
|
51
|
-
launcherVersion: MATRIX_VERSION,
|
|
52
|
-
recovery_command: runtimeCommand(runtime)
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
if (catalogUnavailable
|
|
56
|
-
|| manifest.catalogDigest !== currentCatalog.digest || expected !== currentCatalog.skills.matrix) {
|
|
57
|
-
return {
|
|
58
|
-
ok: false,
|
|
59
|
-
code: "PROJECT_RUNTIME_RELEASE_MISMATCH",
|
|
60
|
-
message: `The project Matrix Runtime claims ${MATRIX_VERSION} but does not match that release catalog. Run matrix update.`,
|
|
61
|
-
platform,
|
|
62
|
-
runtime,
|
|
63
|
-
catalogVersion: manifest.catalogVersion,
|
|
64
|
-
expected: currentCatalog?.skills.matrix ?? null,
|
|
65
|
-
actual
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
return { ok: true, code: "PROJECT_RUNTIME", platform, runtime, catalogVersion: manifest.catalogVersion };
|
|
69
|
-
}
|
|
70
|
-
candidates.push({ platform, matrixRoot, runtime, reason: "modified" });
|
|
71
|
-
} catch { candidates.push({ platform, matrixRoot, runtime, reason: "unreadable" }); }
|
|
72
|
-
}
|
|
73
|
-
return candidates.length
|
|
74
|
-
? { ok: false, code: "PROJECT_RUNTIME_UNTRUSTED", message: "The project Matrix Runtime does not match its installation manifest.", findings: candidates }
|
|
75
|
-
: { ok: true, code: "PROJECT_RUNTIME_ABSENT", runtime: null };
|
|
76
|
-
}
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { MATRIX_VERSION, PLATFORMS, PLATFORM_IDS, hashDirectory, releaseCatalog } from "./catalog.js";
|
|
4
|
+
|
|
5
|
+
function readJson(file) {
|
|
6
|
+
try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function installationScope(projectRoot) {
|
|
10
|
+
try {
|
|
11
|
+
const line = fs.readFileSync(path.join(projectRoot, ".matrix", "config.yaml"), "utf8").split(/\r?\n/).find((value) => value.startsWith("installation_scope:"));
|
|
12
|
+
return line?.split(":").slice(1).join(":").trim() === "global" ? "global" : "project";
|
|
13
|
+
} catch { return "project"; }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function runtimeCommand(runtime) {
|
|
17
|
+
const quoted = process.platform === "win32" ? `"${runtime}"` : `'${runtime.replaceAll("'", "'\\''")}'`;
|
|
18
|
+
return `node ${quoted}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resolveProjectRuntime({ projectRoot = process.cwd() } = {}) {
|
|
22
|
+
const root = path.resolve(projectRoot);
|
|
23
|
+
const manifest = readJson(path.join(root, ".matrix", "installation.json"));
|
|
24
|
+
if (!manifest) return installationScope(root) === "global"
|
|
25
|
+
? { ok: true, code: "GLOBAL_INSTALLATION", runtime: null }
|
|
26
|
+
: { ok: true, code: "PROJECT_INSTALLATION_ABSENT", runtime: null };
|
|
27
|
+
const candidates = [];
|
|
28
|
+
let currentCatalog = null;
|
|
29
|
+
let catalogUnavailable = false;
|
|
30
|
+
try { currentCatalog = releaseCatalog(manifest.language ?? "en"); } catch { catalogUnavailable = true; }
|
|
31
|
+
for (const platform of PLATFORM_IDS) {
|
|
32
|
+
const expected = manifest.platforms?.[platform]?.skills?.matrix?.hash;
|
|
33
|
+
if (!expected) continue;
|
|
34
|
+
const matrixRoot = path.join(PLATFORMS[platform].skillRoot(root), "matrix");
|
|
35
|
+
const runtime = path.join(matrixRoot, "scripts", "matrix-runtime.mjs");
|
|
36
|
+
if (!fs.existsSync(runtime)) { candidates.push({ platform, matrixRoot, runtime, reason: "missing" }); continue; }
|
|
37
|
+
try {
|
|
38
|
+
const actual = hashDirectory(matrixRoot);
|
|
39
|
+
if (actual === expected) {
|
|
40
|
+
if (manifest.catalogVersion !== MATRIX_VERSION) {
|
|
41
|
+
const catalogVersion = manifest.catalogVersion ?? null;
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
code: catalogVersion ? "PROJECT_RUNTIME_VERSION_MISMATCH" : "PROJECT_RUNTIME_VERSION_UNKNOWN",
|
|
45
|
+
message: catalogVersion
|
|
46
|
+
? `The project Matrix Runtime is ${catalogVersion}, while this launcher is ${MATRIX_VERSION}. The project remains authoritative, but a different-release Runtime requires explicit invocation.`
|
|
47
|
+
: `The project Matrix Runtime has no release identity. The project remains authoritative, but an unidentified Runtime requires explicit invocation.`,
|
|
48
|
+
platform,
|
|
49
|
+
runtime,
|
|
50
|
+
catalogVersion,
|
|
51
|
+
launcherVersion: MATRIX_VERSION,
|
|
52
|
+
recovery_command: runtimeCommand(runtime)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (catalogUnavailable
|
|
56
|
+
|| manifest.catalogDigest !== currentCatalog.digest || expected !== currentCatalog.skills.matrix) {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
code: "PROJECT_RUNTIME_RELEASE_MISMATCH",
|
|
60
|
+
message: `The project Matrix Runtime claims ${MATRIX_VERSION} but does not match that release catalog. Run matrix update.`,
|
|
61
|
+
platform,
|
|
62
|
+
runtime,
|
|
63
|
+
catalogVersion: manifest.catalogVersion,
|
|
64
|
+
expected: currentCatalog?.skills.matrix ?? null,
|
|
65
|
+
actual
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return { ok: true, code: "PROJECT_RUNTIME", platform, runtime, catalogVersion: manifest.catalogVersion };
|
|
69
|
+
}
|
|
70
|
+
candidates.push({ platform, matrixRoot, runtime, reason: "modified" });
|
|
71
|
+
} catch { candidates.push({ platform, matrixRoot, runtime, reason: "unreadable" }); }
|
|
72
|
+
}
|
|
73
|
+
return candidates.length
|
|
74
|
+
? { ok: false, code: "PROJECT_RUNTIME_UNTRUSTED", message: "The project Matrix Runtime does not match its installation manifest.", findings: candidates }
|
|
75
|
+
: { ok: true, code: "PROJECT_RUNTIME_ABSENT", runtime: null };
|
|
76
|
+
}
|
package/src/self-update.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { buildNpmProcessInvocation } from "./npm-invocation.js";
|
|
5
6
|
|
|
6
7
|
export const PACKAGE_NAME = "@rosenfan/matrix";
|
|
7
8
|
const REGISTRY = "https://registry.npmjs.org/@rosenfan%2fmatrix/latest";
|
|
@@ -30,7 +31,13 @@ export async function latestVersion(fetchImpl = fetch) {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
function run(command, args, options = {}) {
|
|
33
|
-
|
|
34
|
+
let invocation;
|
|
35
|
+
try {
|
|
36
|
+
invocation = buildNpmProcessInvocation({ command, args });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return { ok: false, status: null, error: error.message, stdout: "", stderr: "" };
|
|
39
|
+
}
|
|
40
|
+
const result = spawnSync(invocation.executable, invocation.args, { encoding: "utf8", ...options });
|
|
34
41
|
return { ok: !result.error && result.status === 0, status: result.status, error: result.error?.message, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
35
42
|
}
|
|
36
43
|
|
package/src/ui.js
CHANGED
|
@@ -1,89 +1,97 @@
|
|
|
1
|
-
import readline from "node:readline";
|
|
2
|
-
import { stdin, stdout } from "node:process";
|
|
3
|
-
|
|
4
|
-
const codes = { cyan: "\u001b[96m", blue: "\u001b[94m", green: "\u001b[92m", yellow: "\u001b[93m", magenta: "\u001b[95m", dim: "\u001b[2m", reset: "\u001b[0m" };
|
|
5
|
-
const pause = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
6
|
-
const logo = String.raw`███╗ ███╗ █████╗ ████████╗██████╗ ██╗██╗ ██╗
|
|
7
|
-
████╗ ████║██╔══██╗╚══██╔══╝██╔══██╗██║╚██╗██╔╝
|
|
8
|
-
██╔████╔██║███████║ ██║ ██████╔╝██║ ╚███╔╝
|
|
9
|
-
██║╚██╔╝██║██╔══██║ ██║ ██╔══██╗██║ ██╔██╗
|
|
10
|
-
██║ ╚═╝ ██║██║ ██║ ██║ ██║ ██║██║██╔╝ ██╗
|
|
11
|
-
╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝`;
|
|
12
|
-
const bannerLines = 9;
|
|
13
|
-
|
|
14
|
-
function cancelled() {
|
|
15
|
-
const error = new Error("Cancelled.");
|
|
16
|
-
error.code = "CANCELLED";
|
|
17
|
-
return error;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function createUi({ color = stdout.isTTY, animate = true } = {}) {
|
|
21
|
-
const paint = (name, value) => color ? `${codes[name]}${value}${codes.reset}` : value;
|
|
22
|
-
const shouldAnimate = animate && color && stdout.isTTY;
|
|
23
|
-
const writeFrame = (stage, status, clearPrevious = false) => {
|
|
24
|
-
if (clearPrevious) stdout.write(`\u001b[${bannerLines}A`);
|
|
25
|
-
stdout.write("\u001b[2K" + paint("green", `[ MATRIX BOOT // ${status} ]`) + "\n");
|
|
26
|
-
for (const line of logo.split("\n")) {
|
|
27
|
-
const reveal = Math.floor(line.length * stage);
|
|
28
|
-
const stable = line.slice(0, reveal);
|
|
29
|
-
const interference = stage === 1 ? "" : "#@%/\\|~".repeat(Math.ceil((line.length - reveal) / 7)).slice(0, line.length - reveal);
|
|
30
|
-
stdout.write("\u001b[2K" + paint(stage < 0.7 ? "green" : "cyan", stable + interference) + "\n");
|
|
31
|
-
}
|
|
32
|
-
stdout.write("\u001b[2K" + paint("yellow", ` ${Math.round(stage * 100)}% ${status}`) + "\n\n");
|
|
33
|
-
};
|
|
34
|
-
return {
|
|
35
|
-
async banner() {
|
|
36
|
-
if (shouldAnimate) {
|
|
37
|
-
const sequence = [
|
|
38
|
-
[0.04, "SIGNAL LOST", 260],
|
|
39
|
-
[0.28, "MAGNETIC INTERFERENCE", 320],
|
|
40
|
-
[0.62, "NEURAL LINK FORMING", 360],
|
|
41
|
-
[1, "SIGNAL LOCKED", 420]
|
|
42
|
-
];
|
|
43
|
-
stdout.write("\u001b[?25l");
|
|
44
|
-
try {
|
|
45
|
-
for (let index = 0; index < sequence.length; index += 1) {
|
|
46
|
-
const [stage, status, duration] = sequence[index];
|
|
47
|
-
writeFrame(stage, status, index > 0);
|
|
48
|
-
await pause(duration);
|
|
49
|
-
}
|
|
50
|
-
} finally {
|
|
51
|
-
stdout.write("\u001b[?25h");
|
|
52
|
-
}
|
|
53
|
-
stdout.write(`\u001b[${bannerLines}A`);
|
|
54
|
-
}
|
|
55
|
-
console.log(paint("cyan", logo));
|
|
56
|
-
console.log(paint("yellow", "Evidence-driven workflow with Prim and Arch orchestration"));
|
|
57
|
-
console.log();
|
|
58
|
-
},
|
|
59
|
-
info(message) { console.log(`${paint("blue", "> ")} ${message}`); },
|
|
60
|
-
success(message) { console.log(`${paint("green", "OK")} ${message}`); },
|
|
61
|
-
warn(message) { console.log(`${paint("yellow", "!")} ${message}`); },
|
|
62
|
-
muted(message) { console.log(paint("dim", message)); },
|
|
63
|
-
async select(question, choices, initial = 0) {
|
|
64
|
-
if (!stdin.isTTY || !stdout.isTTY) return choices[initial].value;
|
|
65
|
-
let selected = initial;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
render()
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
1
|
+
import readline from "node:readline";
|
|
2
|
+
import { stdin, stdout } from "node:process";
|
|
3
|
+
|
|
4
|
+
const codes = { cyan: "\u001b[96m", blue: "\u001b[94m", green: "\u001b[92m", yellow: "\u001b[93m", magenta: "\u001b[95m", dim: "\u001b[2m", reset: "\u001b[0m" };
|
|
5
|
+
const pause = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
6
|
+
const logo = String.raw`███╗ ███╗ █████╗ ████████╗██████╗ ██╗██╗ ██╗
|
|
7
|
+
████╗ ████║██╔══██╗╚══██╔══╝██╔══██╗██║╚██╗██╔╝
|
|
8
|
+
██╔████╔██║███████║ ██║ ██████╔╝██║ ╚███╔╝
|
|
9
|
+
██║╚██╔╝██║██╔══██║ ██║ ██╔══██╗██║ ██╔██╗
|
|
10
|
+
██║ ╚═╝ ██║██║ ██║ ██║ ██║ ██║██║██╔╝ ██╗
|
|
11
|
+
╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝`;
|
|
12
|
+
const bannerLines = 9;
|
|
13
|
+
|
|
14
|
+
function cancelled() {
|
|
15
|
+
const error = new Error("Cancelled.");
|
|
16
|
+
error.code = "CANCELLED";
|
|
17
|
+
return error;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createUi({ color = stdout.isTTY, animate = true } = {}) {
|
|
21
|
+
const paint = (name, value) => color ? `${codes[name]}${value}${codes.reset}` : value;
|
|
22
|
+
const shouldAnimate = animate && color && stdout.isTTY;
|
|
23
|
+
const writeFrame = (stage, status, clearPrevious = false) => {
|
|
24
|
+
if (clearPrevious) stdout.write(`\u001b[${bannerLines}A`);
|
|
25
|
+
stdout.write("\u001b[2K" + paint("green", `[ MATRIX BOOT // ${status} ]`) + "\n");
|
|
26
|
+
for (const line of logo.split("\n")) {
|
|
27
|
+
const reveal = Math.floor(line.length * stage);
|
|
28
|
+
const stable = line.slice(0, reveal);
|
|
29
|
+
const interference = stage === 1 ? "" : "#@%/\\|~".repeat(Math.ceil((line.length - reveal) / 7)).slice(0, line.length - reveal);
|
|
30
|
+
stdout.write("\u001b[2K" + paint(stage < 0.7 ? "green" : "cyan", stable + interference) + "\n");
|
|
31
|
+
}
|
|
32
|
+
stdout.write("\u001b[2K" + paint("yellow", ` ${Math.round(stage * 100)}% ${status}`) + "\n\n");
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
async banner() {
|
|
36
|
+
if (shouldAnimate) {
|
|
37
|
+
const sequence = [
|
|
38
|
+
[0.04, "SIGNAL LOST", 260],
|
|
39
|
+
[0.28, "MAGNETIC INTERFERENCE", 320],
|
|
40
|
+
[0.62, "NEURAL LINK FORMING", 360],
|
|
41
|
+
[1, "SIGNAL LOCKED", 420]
|
|
42
|
+
];
|
|
43
|
+
stdout.write("\u001b[?25l");
|
|
44
|
+
try {
|
|
45
|
+
for (let index = 0; index < sequence.length; index += 1) {
|
|
46
|
+
const [stage, status, duration] = sequence[index];
|
|
47
|
+
writeFrame(stage, status, index > 0);
|
|
48
|
+
await pause(duration);
|
|
49
|
+
}
|
|
50
|
+
} finally {
|
|
51
|
+
stdout.write("\u001b[?25h");
|
|
52
|
+
}
|
|
53
|
+
stdout.write(`\u001b[${bannerLines}A`);
|
|
54
|
+
}
|
|
55
|
+
console.log(paint("cyan", logo));
|
|
56
|
+
console.log(paint("yellow", "Evidence-driven workflow with Prim and Arch orchestration"));
|
|
57
|
+
console.log();
|
|
58
|
+
},
|
|
59
|
+
info(message) { console.log(`${paint("blue", "> ")} ${message}`); },
|
|
60
|
+
success(message) { console.log(`${paint("green", "OK")} ${message}`); },
|
|
61
|
+
warn(message) { console.log(`${paint("yellow", "!")} ${message}`); },
|
|
62
|
+
muted(message) { console.log(paint("dim", message)); },
|
|
63
|
+
async select(question, choices, initial = 0) {
|
|
64
|
+
if (!stdin.isTTY || !stdout.isTTY) return choices[initial].value;
|
|
65
|
+
let selected = initial;
|
|
66
|
+
let renderedLines = 0;
|
|
67
|
+
readline.emitKeypressEvents(stdin); const wasRaw = stdin.isRaw; stdin.setRawMode(true); stdin.resume(); stdout.write(`? ${question}\n`);
|
|
68
|
+
const render = (clear = false) => {
|
|
69
|
+
if (clear) stdout.write(`\u001b[${renderedLines}A`);
|
|
70
|
+
renderedLines = 0;
|
|
71
|
+
choices.forEach((choice, index) => {
|
|
72
|
+
stdout.write("\u001b[2K"); stdout.write(`${index === selected ? paint("blue", ">") : " "} ${index === selected ? paint("blue", choice.label) : choice.label}\n`); renderedLines += 1;
|
|
73
|
+
if (choice.detail) { stdout.write("\u001b[2K"); stdout.write(` ${paint("dim", choice.detail)}\n`); renderedLines += 1; }
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
render();
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const cleanup = () => { stdin.off("keypress", onKeypress); stdin.setRawMode(Boolean(wasRaw)); stdin.pause(); };
|
|
79
|
+
const onKeypress = (_value, key = {}) => { if (key.ctrl && key.name === "c") { cleanup(); reject(cancelled()); } else if (["up", "k"].includes(key.name)) { selected = (selected - 1 + choices.length) % choices.length; render(true); } else if (["down", "j"].includes(key.name)) { selected = (selected + 1) % choices.length; render(true); } else if (key.name === "return") { cleanup(); resolve(choices[selected].value); } };
|
|
80
|
+
stdin.on("keypress", onKeypress);
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
async selectMany(question, choices) {
|
|
84
|
+
if (!stdin.isTTY || !stdout.isTTY) return choices.map((choice) => choice.value);
|
|
85
|
+
let selected = 0; const checked = new Set(choices.map((choice) => choice.value));
|
|
86
|
+
readline.emitKeypressEvents(stdin); const wasRaw = stdin.isRaw; stdin.setRawMode(true); stdin.resume(); stdout.write(`? ${question}\n`);
|
|
87
|
+
const render = (clear = false) => { if (clear) stdout.write(`\u001b[${choices.length}A`); choices.forEach((choice, index) => { stdout.write("\u001b[2K"); const marker = checked.has(choice.value) ? "[x]" : "[ ]"; stdout.write(`${index === selected ? paint("blue", ">") : " "} ${marker} ${index === selected ? paint("blue", choice.label) : choice.label}\n`); }); };
|
|
88
|
+
render();
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const cleanup = () => { stdin.off("keypress", onKeypress); stdin.setRawMode(Boolean(wasRaw)); stdin.pause(); };
|
|
91
|
+
const onKeypress = (_value, key = {}) => { if (key.ctrl && key.name === "c") { cleanup(); reject(cancelled()); } else if (["up", "k"].includes(key.name)) { selected = (selected - 1 + choices.length) % choices.length; render(true); } else if (["down", "j"].includes(key.name)) { selected = (selected + 1) % choices.length; render(true); } else if (key.name === "space") { const value = choices[selected].value; checked.has(value) ? checked.delete(value) : checked.add(value); render(true); } else if (key.name === "return") { cleanup(); resolve(choices.filter((choice) => checked.has(choice.value)).map((choice) => choice.value)); } };
|
|
92
|
+
stdin.on("keypress", onKeypress);
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
async confirm(question, defaultValue = true, labels = { yes: "Yes", no: "No" }) { return this.select(question, [{ label: defaultValue ? labels.yes : labels.no, value: defaultValue }, { label: defaultValue ? labels.no : labels.yes, value: !defaultValue }]); }
|
|
96
|
+
};
|
|
97
|
+
}
|
package/src/workflow.js
CHANGED
|
@@ -49,6 +49,13 @@ const LEGACY_ARCH_SKILLS = [
|
|
|
49
49
|
"grilling", "domain-modeling", "research", "wayfinder", "prototype",
|
|
50
50
|
"codebase-design", "tdd", "diagnosing-bugs", "resolving-merge-conflicts", "code-review"
|
|
51
51
|
];
|
|
52
|
+
// The bundled runtime is injected without catalog.js, so this platform→skill-root
|
|
53
|
+
// mapping mirrors catalog.js PLATFORMS and is pinned by a parity test.
|
|
54
|
+
export const PLATFORM_SKILL_ROOTS = Object.freeze({
|
|
55
|
+
"claude-code": ".claude/skills",
|
|
56
|
+
codex: ".agents/skills",
|
|
57
|
+
zcode: ".zcode/skills"
|
|
58
|
+
});
|
|
52
59
|
const ARTIFACT_POLICY = {
|
|
53
60
|
proposal: ["## Goal", "## Scope", "## Non-goals", "## Acceptance", "## Risks"],
|
|
54
61
|
design: ["## Decisions", "## Boundaries", "## Test seams", "## Risks"],
|
|
@@ -249,9 +256,11 @@ function archIntegrity(cwd, flow = null, contentHashes = MATT_CONTENT_HASHES) {
|
|
|
249
256
|
}
|
|
250
257
|
const findings = [];
|
|
251
258
|
for (const [platform, record] of platforms) {
|
|
259
|
+
const relativeRoot = PLATFORM_SKILL_ROOTS[platform];
|
|
252
260
|
for (const skill of legacy ? LEGACY_ARCH_SKILLS : MATT_AUTOMATIC_SKILLS) {
|
|
261
|
+
if (!relativeRoot) { findings.push({ platform, skill, reason: "unsupported" }); continue; }
|
|
253
262
|
const installed = legacy ? record?.matt?.skills?.[skill] : mattReceipt?.platforms?.[platform]?.skills?.[skill];
|
|
254
|
-
const expectedRoot = path.join(cwd,
|
|
263
|
+
const expectedRoot = path.join(cwd, relativeRoot, skill);
|
|
255
264
|
const target = legacy ? installed?.root : expectedRoot;
|
|
256
265
|
if (!legacy && installed?.root && path.resolve(installed.root) !== path.resolve(expectedRoot)) {
|
|
257
266
|
findings.push({ platform, skill, reason: "non-project" });
|
|
@@ -272,8 +281,12 @@ function archIntegrity(cwd, flow = null, contentHashes = MATT_CONTENT_HASHES) {
|
|
|
272
281
|
manifest: legacy ? manifestFile : mattReceiptFile, findings, recovery_command: "matrix init --with-mattpocock"
|
|
273
282
|
} : { ok: true, code: "OK", manifest: legacy ? manifestFile : mattReceiptFile, platforms: platforms.map(([platform]) => platform), cohort: legacy ? "legacy-v0.1.3" : MATT_CATALOG_DIGEST, matt_release: legacy ? null : MATT_COMPATIBILITY.release };
|
|
274
283
|
}
|
|
275
|
-
function taskContent(change, taskId, proposal, design, plan, resultPath, contractHash, approvalRevision) {
|
|
276
|
-
|
|
284
|
+
function taskContent(change, taskId, proposal, design, plan, resultPath, contractHash, approvalRevision, actor = {}) {
|
|
285
|
+
const agent = actor.agent ?? "Claude Code";
|
|
286
|
+
const owner = actor.owner ?? "Codex";
|
|
287
|
+
const statusToken = actor.statusToken ?? "CLAUDE_QUEUED";
|
|
288
|
+
const reviewer = actor.reviewer ?? "Codex";
|
|
289
|
+
return `# ${agent} task: ${taskId}\n\n## Metadata\n\n| Field | Value |\n| --- | --- |\n| Status | ${statusToken} |\n| Matrix change | ${change} |\n| Decision owner | ${owner} |\n| Implementation actor | ${agent} |\n| Approved contract | ${contractHash} |\n| Approval revision | ${approvalRevision} |\n| Result | ${resultPath} |\n\n## Objective\n\nThe frozen Matrix artifacts below are authoritative. Do not expand their scope.\n\n## Proposal\n\n${proposal}\n\n## Frozen design\n\n${design}\n\n## Ordered plan\n\n${plan}\n\n## Allowed work\n\n- Read repository instructions and existing tests before implementing.\n- Change only files required by the frozen design and plan.\n\n## Prohibited work\n\n- Do not edit Matrix state, transition its phase, archive it, or alter the frozen design.\n- Do not add unrelated features, public APIs, dependencies, or external side effects.\n- Do not claim completion without recording verifiable evidence.\n\n## Stop and report\n\nStop with \`NEEDS_DECISION\` if the scope must expand, the design conflicts with the repository, validation cannot pass within scope, or an authorization/safety issue arises.\n\n## Required result\n\nCreate \`${resultPath}\` with metadata, changed files and reasons, commands and outcomes, test results, boundary confirmation, residual risks, and any \`NEEDS_DECISION\` items. ${reviewer} reviews the result and the actual diff.\n`;
|
|
277
290
|
}
|
|
278
291
|
function activeBoard(taskId, taskPath, resultPath, change) {
|
|
279
292
|
return `# Active task board\n\n## Current objective\n\n| Field | Value |\n| --- | --- |\n| ID | ${taskId} |\n| Status | CLAUDE_QUEUED |\n| Matrix change | ${change} |\n| Task | ${taskPath} |\n| Result | ${resultPath} |\n`;
|
|
@@ -381,7 +394,7 @@ function managedReviewPrefixes(cwd) {
|
|
|
381
394
|
const { manifest, mattReceipt } = orchestrationConfig(cwd);
|
|
382
395
|
const prefixes = [];
|
|
383
396
|
for (const [platform, record] of Object.entries(manifest?.platforms ?? {})) {
|
|
384
|
-
const root = platform
|
|
397
|
+
const root = PLATFORM_SKILL_ROOTS[platform];
|
|
385
398
|
if (!root) continue;
|
|
386
399
|
for (const skill of Object.keys(record?.skills ?? {})) prefixes.push(`${root}/${skill}`);
|
|
387
400
|
for (const skill of Object.keys(mattReceipt?.platforms?.[platform]?.skills ?? {})) prefixes.push(`${root}/${skill}`);
|
|
@@ -1030,10 +1043,29 @@ export function invoke(argv, { cwd = process.cwd(), failAfterOperation = null, m
|
|
|
1030
1043
|
if (workflowProfile(p, flow).id === "lightweight") return fail("EXPORT_UNSUPPORTED_WORKFLOW", "Claude export requires the full three-artifact workflow contract.");
|
|
1031
1044
|
const contract = contractState(p, flow); const failure = contractFailure(flow, contract); if (failure) return failure;
|
|
1032
1045
|
const taskId = args.includes("--task-id") ? args[args.indexOf("--task-id") + 1] : args[0];
|
|
1046
|
+
const agent = args.includes("--agent") ? args[args.indexOf("--agent") + 1] : null;
|
|
1047
|
+
const from = args.includes("--from") ? args[args.indexOf("--from") + 1] : null;
|
|
1048
|
+
if (agent && args.includes("--target")) return fail("INVALID_INTENT", "export --agent and --target are mutually exclusive.");
|
|
1049
|
+
if (agent && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(agent)) return fail("INVALID_INTENT", "export --agent requires a kebab-case agent identifier.");
|
|
1050
|
+
if (from && !agent) return fail("INVALID_INTENT", "export --from requires --agent.");
|
|
1051
|
+
if (from && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(from)) return fail("INVALID_INTENT", "export --from requires a kebab-case agent identifier.");
|
|
1052
|
+
if (agent && (!args.includes("--task-id") || !/^[A-Za-z0-9][A-Za-z0-9-]*$/.test(taskId ?? ""))) return fail("INVALID_INTENT", "export --agent requires --task-id.");
|
|
1033
1053
|
const target = args.includes("--target") ? args[args.indexOf("--target") + 1] : "generic";
|
|
1034
1054
|
const applyBoard = args.includes("--apply-fnsec-board");
|
|
1035
1055
|
if (!/^[A-Za-z0-9][A-Za-z0-9-]*$/.test(taskId ?? "") || !["generic", "fnsec"].includes(target)) return fail("INVALID_INTENT", "export requires a task id and target generic or fnsec.");
|
|
1036
1056
|
const { proposal, design, plan } = contract.snapshot.contents;
|
|
1057
|
+
if (agent) {
|
|
1058
|
+
const destination = path.join(p.artifacts, `handoff-task-${taskId}.md`);
|
|
1059
|
+
const result = path.join(p.artifacts, `handoff-result-${taskId}.md`);
|
|
1060
|
+
const content = taskContent(id, taskId, proposal, design, plan, path.relative(cwd, result).replaceAll("\\", "/"), flow.approved_contract_hash, flow.contract_approved_revision, {
|
|
1061
|
+
agent,
|
|
1062
|
+
owner: from ?? "Matrix change owner",
|
|
1063
|
+
statusToken: `${agent.replaceAll("-", "_").toUpperCase()}_QUEUED`,
|
|
1064
|
+
reviewer: from ?? "Matrix change owner"
|
|
1065
|
+
});
|
|
1066
|
+
fs.writeFileSync(destination, content);
|
|
1067
|
+
return { ok: true, code: "OK", path: destination };
|
|
1068
|
+
}
|
|
1037
1069
|
if (target === "generic") {
|
|
1038
1070
|
const destination = path.join(p.artifacts, "claude-task.md"); const result = path.join(p.artifacts, "claude-result.md");
|
|
1039
1071
|
fs.writeFileSync(destination, taskContent(id, taskId, proposal, design, plan, path.relative(cwd, result).replaceAll("\\", "/"), flow.approved_contract_hash, flow.contract_approved_revision));
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: matrix-claude
|
|
3
|
-
description: Export a frozen Matrix design as a bounded Claude Code task package without changing Matrix workflow state. Use when Codex should design and Claude Code should implement.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Matrix Claude Handoff
|
|
7
|
-
|
|
8
|
-
This is an optional Matrix sidecar, not a Matrix phase. It must not call `transition`, `archive`, or write `matrix.yaml` or `events.jsonl`. It supports only a matching full or legacy-full three-artifact Contract; new lightweight hotfix/tweak changes are intentionally unsupported.
|
|
9
|
-
|
|
10
|
-
Use `artifact_language` from inspect for human-readable handoff text while retaining the stable machine-readable headings and metadata keys.
|
|
11
|
-
|
|
12
|
-
## When to Use
|
|
13
|
-
|
|
14
|
-
Use `$matrix-claude` when:
|
|
15
|
-
- The active change is in Build and the Design -> Build transition has approved the contract
|
|
16
|
-
- You want Claude Code (instead of Codex) to implement the design
|
|
17
|
-
- You need a bounded task package with all implementation details
|
|
18
|
-
|
|
19
|
-
## Preconditions
|
|
20
|
-
|
|
21
|
-
The exporter performs this read-only check before writing anything:
|
|
22
|
-
|
|
23
|
-
```powershell
|
|
24
|
-
node <matrix-skill-directory>/scripts/matrix-runtime.mjs guard build
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
If `matrix` is genuinely absent from PATH, use the bundled runtime from the installed root Matrix Skill. Do not fall back when the primary command starts and returns an error.
|
|
28
|
-
|
|
29
|
-
If the build guard reports `CONTRACT_CHANGED` or `CONTRACT_UNAPPROVED`, return to `$matrix-design` and approve a new contract through Build. The exporter reads the approved bytes of `proposal.md`, `design.md`, and `plan.md`; it does not invent missing facts.
|
|
30
|
-
|
|
31
|
-
## Export
|
|
32
|
-
|
|
33
|
-
Treat explicit invocation of `$matrix-claude` as the user's choice of Claude Code as the implementation actor. Generate the package with the deterministic exporter; it refuses a non-matching approved contract and does not alter Matrix state.
|
|
34
|
-
|
|
35
|
-
```powershell
|
|
36
|
-
# Generic project: only exports a Matrix artifact.
|
|
37
|
-
node <matrix-skill-directory>/scripts/matrix-runtime.mjs export --task-id <ID> --target generic
|
|
38
|
-
|
|
39
|
-
# FnSec: exports its formal task package. Add --apply-fnsec-board only when
|
|
40
|
-
# active.md is NO_ACTIVE_OBJECTIVE; an existing task is never overwritten.
|
|
41
|
-
node <matrix-skill-directory>/scripts/matrix-runtime.mjs export --task-id <ID> --target fnsec --apply-fnsec-board
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## Task Package Contents
|
|
45
|
-
|
|
46
|
-
The exported package (`artifacts/claude-task.md`) contains everything Claude Code needs to implement and produce evidence for `$matrix-verify`:
|
|
47
|
-
|
|
48
|
-
1. **Task ID, execution actor, and approved contract hash**: Claude Code and the exact approved snapshot
|
|
49
|
-
2. **Objective and acceptance criteria** — copied from `proposal.md`
|
|
50
|
-
3. **In-scope and out-of-scope boundaries** — from `proposal.md`
|
|
51
|
-
4. **Frozen design decisions** — from `design.md` (Decisions, Boundaries, Test seams)
|
|
52
|
-
5. **Ordered implementation plan** — from `plan.md` (Steps, Validation)
|
|
53
|
-
6. **Allowed and prohibited files/actions** — to prevent scope creep
|
|
54
|
-
7. **Required commands** — test, lint, typecheck, build commands to run
|
|
55
|
-
8. **Stop conditions** — ambiguity, scope expansion, failing acceptance, design conflict
|
|
56
|
-
9. **Required evidence for verify**:
|
|
57
|
-
- Changed files list
|
|
58
|
-
- Commands and their outputs
|
|
59
|
-
- Test results (pass/fail)
|
|
60
|
-
- Build evidence
|
|
61
|
-
- Residual risks
|
|
62
|
-
- `NEEDS_DECISION` items (if any)
|
|
63
|
-
|
|
64
|
-
## After Claude Code Completes
|
|
65
|
-
|
|
66
|
-
When Claude Code returns results, verify that the following evidence is present before proceeding to `$matrix-verify`:
|
|
67
|
-
|
|
68
|
-
- [ ] `artifacts/verification.md` exists with `## Build evidence` section
|
|
69
|
-
- [ ] Build evidence includes concrete commands and outcomes
|
|
70
|
-
- [ ] All tests in the plan's validation section have been run
|
|
71
|
-
- [ ] Any `NEEDS_DECISION` items have been resolved or documented
|
|
72
|
-
|
|
73
|
-
If evidence is missing, ask Claude Code to补充 before proceeding.
|
|
74
|
-
|
|
75
|
-
## Resuming Matrix Workflow
|
|
76
|
-
|
|
77
|
-
After Claude Code completes and evidence is present:
|
|
78
|
-
|
|
79
|
-
1. Run build guard to verify evidence:
|
|
80
|
-
```powershell
|
|
81
|
-
node <matrix-skill-directory>/scripts/matrix-runtime.mjs guard build
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
2. If guard passes, transition to verify:
|
|
85
|
-
```powershell
|
|
86
|
-
node <matrix-skill-directory>/scripts/matrix-runtime.mjs transition verify
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
3. Enter `$matrix-verify` to继续验证流程
|
|
90
|
-
|
|
91
|
-
If the build guard fails, return to Claude Code to补充 missing evidence.
|
|
92
|
-
|
|
93
|
-
## Important Notes
|
|
94
|
-
|
|
95
|
-
- This sidecar does NOT change Matrix phase, workflow, or guard result
|
|
96
|
-
- The Matrix state remains in `build` phase; export itself does not advance it
|
|
97
|
-
- Claude Code must produce evidence that satisfies the build guard conditions
|
|
98
|
-
- After successful transition to verify, the normal verify流程 continues
|