@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.
@@ -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
+ }
@@ -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 ["codex", "claude-code"]) {
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
+ }
@@ -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
- const result = spawnSync(command, args, { encoding: "utf8", ...options });
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
- readline.emitKeypressEvents(stdin); const wasRaw = stdin.isRaw; stdin.setRawMode(true); stdin.resume(); stdout.write(`? ${question}\n`);
67
- const render = (clear = false) => { if (clear) stdout.write(`\u001b[${choices.length}A`); choices.forEach((choice, index) => { stdout.write("\u001b[2K"); stdout.write(`${index === selected ? paint("blue", ">") : " "} ${index === selected ? paint("blue", choice.label) : choice.label}\n`); }); };
68
- render();
69
- return new Promise((resolve, reject) => {
70
- const cleanup = () => { stdin.off("keypress", onKeypress); stdin.setRawMode(Boolean(wasRaw)); stdin.pause(); };
71
- 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); } };
72
- stdin.on("keypress", onKeypress);
73
- });
74
- },
75
- async selectMany(question, choices) {
76
- if (!stdin.isTTY || !stdout.isTTY) return choices.map((choice) => choice.value);
77
- let selected = 0; const checked = new Set(choices.map((choice) => choice.value));
78
- readline.emitKeypressEvents(stdin); const wasRaw = stdin.isRaw; stdin.setRawMode(true); stdin.resume(); stdout.write(`? ${question}\n`);
79
- 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`); }); };
80
- render();
81
- return new Promise((resolve, reject) => {
82
- const cleanup = () => { stdin.off("keypress", onKeypress); stdin.setRawMode(Boolean(wasRaw)); stdin.pause(); };
83
- 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)); } };
84
- stdin.on("keypress", onKeypress);
85
- });
86
- },
87
- 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 }]); }
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, platform === "codex" ? ".agents" : ".claude", "skills", skill);
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
- return `# Claude Code task: ${taskId}\n\n## Metadata\n\n| Field | Value |\n| --- | --- |\n| Status | CLAUDE_QUEUED |\n| Matrix change | ${change} |\n| Decision owner | Codex |\n| Implementation actor | Claude Code |\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. Codex reviews the result and the actual diff.\n`;
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 === "codex" ? ".agents/skills" : platform === "claude-code" ? ".claude/skills" : null;
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
@@ -1,4 +0,0 @@
1
- interface:
2
- display_name: "Matrix Claude Handoff"
3
- short_description: "Export frozen Matrix design as a Claude Code task"
4
- default_prompt: "Use $matrix-claude to prepare a Claude Code task package."