@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.
- package/.claude/skills/matrix/SKILL.md +49 -0
- package/.claude/skills/matrix/agents/openai.yaml +4 -0
- package/.claude/skills/matrix/scripts/matrix_claude.py +166 -0
- package/.claude/skills/matrix/scripts/matrix_state.py +114 -0
- package/.claude/skills/matrix-archive/SKILL.md +26 -0
- package/.claude/skills/matrix-archive/agents/openai.yaml +4 -0
- package/.claude/skills/matrix-build/SKILL.md +55 -0
- package/.claude/skills/matrix-build/agents/openai.yaml +4 -0
- package/.claude/skills/matrix-claude/SKILL.md +96 -0
- package/.claude/skills/matrix-claude/agents/openai.yaml +4 -0
- package/.claude/skills/matrix-design/SKILL.md +68 -0
- package/.claude/skills/matrix-design/agents/openai.yaml +4 -0
- package/.claude/skills/matrix-hotfix/SKILL.md +8 -0
- package/.claude/skills/matrix-hotfix/agents/openai.yaml +4 -0
- package/.claude/skills/matrix-open/SKILL.md +33 -0
- package/.claude/skills/matrix-open/agents/openai.yaml +4 -0
- package/.claude/skills/matrix-status/SKILL.md +14 -0
- package/.claude/skills/matrix-status/agents/openai.yaml +4 -0
- package/.claude/skills/matrix-tweak/SKILL.md +8 -0
- package/.claude/skills/matrix-tweak/agents/openai.yaml +4 -0
- package/.claude/skills/matrix-verify/SKILL.md +82 -0
- package/.claude/skills/matrix-verify/agents/openai.yaml +4 -0
- package/LICENSE +21 -0
- package/README.md +273 -0
- package/README.zh-CN.md +271 -0
- package/assets/skills-zh-CN/matrix/SKILL.md +28 -0
- package/assets/skills-zh-CN/matrix-archive/SKILL.md +12 -0
- package/assets/skills-zh-CN/matrix-build/SKILL.md +13 -0
- package/assets/skills-zh-CN/matrix-claude/SKILL.md +30 -0
- package/assets/skills-zh-CN/matrix-design/SKILL.md +13 -0
- package/assets/skills-zh-CN/matrix-hotfix/SKILL.md +8 -0
- package/assets/skills-zh-CN/matrix-open/SKILL.md +13 -0
- package/assets/skills-zh-CN/matrix-status/SKILL.md +14 -0
- package/assets/skills-zh-CN/matrix-tweak/SKILL.md +8 -0
- package/assets/skills-zh-CN/matrix-verify/SKILL.md +13 -0
- package/bin/matrix.js +15 -0
- package/package.json +56 -0
- package/src/catalog.js +87 -0
- package/src/cli.js +117 -0
- package/src/distribution.js +192 -0
- package/src/installer.js +45 -0
- package/src/matt.js +34 -0
- package/src/messages.js +33 -0
- package/src/ui.js +89 -0
- package/src/workflow.js +95 -0
package/src/installer.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createDistribution } from "./distribution.js";
|
|
4
|
+
import { MATRIX_SKILLS, MATT_SKILLS, PLATFORMS, sourceSkillsRoot } from "./catalog.js";
|
|
5
|
+
import { createMattAdapter } from "./matt.js";
|
|
6
|
+
|
|
7
|
+
export { MATRIX_SKILLS, MATT_SKILLS, sourceSkillsRoot };
|
|
8
|
+
|
|
9
|
+
export function destinationFor({ scope, projectRoot, platform = "claude-code", home }) {
|
|
10
|
+
const base = scope === "global" ? (home ?? process.env.USERPROFILE ?? process.env.HOME) : projectRoot;
|
|
11
|
+
return PLATFORMS[platform].skillRoot(base);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function inspectInstallation(destination) {
|
|
15
|
+
const inspect = (names) => names.filter((skill) => {
|
|
16
|
+
try { return Boolean(destination) && requireExists(destination, skill); } catch { return false; }
|
|
17
|
+
});
|
|
18
|
+
const installed = inspect(MATRIX_SKILLS);
|
|
19
|
+
const mattInstalled = inspect(MATT_SKILLS);
|
|
20
|
+
return { installed, missing: MATRIX_SKILLS.filter((skill) => !installed.includes(skill)), mattInstalled, mattMissing: MATT_SKILLS.filter((skill) => !mattInstalled.includes(skill)) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function requireExists(destination, skill) {
|
|
24
|
+
// Avoid exposing a second installation policy through this legacy helper.
|
|
25
|
+
return fs.existsSync(path.join(destination, skill, "SKILL.md"));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function installMatrix({ destination, mode = "copy", conflict = "error", dryRun = false }) {
|
|
29
|
+
const platform = destination?.includes(".agents") ? "codex" : "claude-code";
|
|
30
|
+
const projectRoot = path.resolve(destination, "..", "..");
|
|
31
|
+
const distribution = createDistribution();
|
|
32
|
+
const evaluation = distribution.evaluate({ projectRoot, scope: "project", platforms: [platform], mode, matrixPolicy: conflict === "replace" ? "replace" : "safe", dryRun });
|
|
33
|
+
if (!evaluation.ok) throw new Error(evaluation.diagnostics?.[0]?.message ?? evaluation.code);
|
|
34
|
+
const result = distribution.commit(evaluation.plan);
|
|
35
|
+
if (!result.ok) throw new Error(result.error ?? result.code);
|
|
36
|
+
return { installed: result.committed ?? MATRIX_SKILLS, skipped: [], existing: [], backupRoot: result.backups?.[0] ? result.backups[0].backup : null };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function installMattSkills({ projectRoot, platforms = ["claude-code"], skills = MATT_SKILLS, global = false, timeout = 300_000 }) {
|
|
40
|
+
const result = createMattAdapter().installMissing({ projectRoot, platforms, skills, global, timeout });
|
|
41
|
+
if (!result.ok) throw new Error(`${result.code}: ${result.message}`);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function findPython() { return null; }
|
package/src/matt.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { PLATFORMS, MATT_SKILLS } from "./catalog.js";
|
|
3
|
+
|
|
4
|
+
export function defaultProcessRunner({ command, args, cwd, timeout }) {
|
|
5
|
+
const executable = process.platform === "win32" && command === "npx" ? "npx.cmd" : command;
|
|
6
|
+
// The only shell-enabled case is the Windows npm shim; all inputs are catalog values.
|
|
7
|
+
return execFileSync(executable, args, { cwd, stdio: "inherit", timeout, shell: process.platform === "win32" && command === "npx" });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createMattAdapter({ run = defaultProcessRunner } = {}) {
|
|
11
|
+
return {
|
|
12
|
+
installMissing({ projectRoot, platforms, skills = MATT_SKILLS, global = false, timeout = 300_000 }) {
|
|
13
|
+
const attempted = [];
|
|
14
|
+
try {
|
|
15
|
+
for (const platform of platforms) {
|
|
16
|
+
const args = ["--yes", "skills@latest", "add", "mattpocock/skills", "--yes", "--agent", PLATFORMS[platform].skillsCliAgent];
|
|
17
|
+
for (const skill of skills) args.push("--skill", skill);
|
|
18
|
+
if (global) args.push("--global");
|
|
19
|
+
run({ command: "npx", args, cwd: projectRoot, timeout });
|
|
20
|
+
attempted.push(platform);
|
|
21
|
+
}
|
|
22
|
+
return { ok: true, code: "OK", attempted };
|
|
23
|
+
} catch (error) {
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
code: "MATT_INSTALL_FAILED",
|
|
27
|
+
attempted,
|
|
28
|
+
message: error.message,
|
|
29
|
+
recovery: "Run matrix init --with-mattpocock after resolving the skills CLI error."
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
package/src/messages.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const messages = {
|
|
2
|
+
en: {
|
|
3
|
+
title: "Evidence-driven workflow for Matt Pocock's agent skills",
|
|
4
|
+
recommended: "Recommended setup", apply: "Apply", customize: "Customize", cancel: "Cancel",
|
|
5
|
+
project: "Project (current directory)", global: "Global (all projects)",
|
|
6
|
+
copy: "Copy (recommended)", symlink: "Symlink (advanced)",
|
|
7
|
+
matrix: "Matrix", matt: "Matt Pocock skills", installed: "installed", ready: "Matrix is ready.",
|
|
8
|
+
noPlatform: "No verified platform was detected. Select Claude Code, Codex, or both.",
|
|
9
|
+
preview: "Planned changes", confirm: "Apply these changes?", doctor: "Installation diagnosis",
|
|
10
|
+
languageQuestion: "Language", scopeQuestion: "Installation scope", platformsQuestion: "Platforms",
|
|
11
|
+
mattQuestion: "Install missing Matt Pocock skills?", replaceQuestion: "Existing Matrix files are not proven managed. Back up and replace?",
|
|
12
|
+
yes: "Yes", no: "No", stateMissing: "missing", stateMatching: "up to date", stateComplete: "complete", stateUserModified: "user-modified", statePartial: "partial", stateOutdated: "outdated",
|
|
13
|
+
actionInstall: "install", actionKeep: "keep", actionBackupReplace: "back up and replace", actionRepair: "repair", actionBlocked: "requires approval",
|
|
14
|
+
dryRunComplete: "Dry run complete.", manifestMissing: "No Matrix installation manifest was found for this scope.", transactionPending: "An interrupted Matrix transaction is awaiting recovery. Run matrix init again before applying a new plan.", runtimeMissing: "Bundled Matrix runtime is missing.", brokenLink: "Broken Matrix link detected.", mattInherited: "Matt skills are satisfied by the global installation."
|
|
15
|
+
},
|
|
16
|
+
"zh-CN": {
|
|
17
|
+
manifestMissing: "当前安装范围没有 Matrix 安装清单。", transactionPending: "发现中断的 Matrix 安装事务。请再次运行 matrix init 后再应用新计划。", runtimeMissing: "缺少随附的 Matrix runtime。", brokenLink: "检测到断开的 Matrix 链接。", mattInherited: "Matt skills 由全局安装提供。",
|
|
18
|
+
languageQuestion: "语言", scopeQuestion: "安装范围", platformsQuestion: "要配置的平台",
|
|
19
|
+
mattQuestion: "是否安装缺失的 Matt Pocock skills?", replaceQuestion: "现有 Matrix 文件无法确认由 Matrix 管理。是否备份并替换?",
|
|
20
|
+
yes: "是", no: "否", stateMissing: "未安装", stateMatching: "已是最新", stateComplete: "完整", stateUserModified: "已被用户修改", statePartial: "不完整", stateOutdated: "可更新",
|
|
21
|
+
actionInstall: "安装", actionKeep: "保留", actionBackupReplace: "备份并替换", actionRepair: "修复", actionBlocked: "需要确认",
|
|
22
|
+
dryRunComplete: "预览完成。",
|
|
23
|
+
title: "面向 Matt Pocock Agent Skills 的证据驱动工作流",
|
|
24
|
+
recommended: "推荐配置", apply: "开始安装", customize: "自定义", cancel: "取消",
|
|
25
|
+
project: "项目(当前目录)", global: "全局(所有项目)",
|
|
26
|
+
copy: "复制(推荐)", symlink: "符号链接(高级)",
|
|
27
|
+
matrix: "Matrix", matt: "Matt Pocock skills", installed: "已安装", ready: "Matrix 已就绪。",
|
|
28
|
+
noPlatform: "未检测到已验证的平台。请选择 Claude Code、Codex,或两者。",
|
|
29
|
+
preview: "计划变更", confirm: "确认应用这些变更?", doctor: "安装诊断"
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function text(language, key) { return messages[language]?.[key] ?? messages.en[key] ?? key; }
|
package/src/ui.js
ADDED
|
@@ -0,0 +1,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 for Matt Pocock's agent skills"));
|
|
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
|
+
}
|
package/src/workflow.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const PHASES = ["open", "design", "build", "verify", "archive"];
|
|
5
|
+
const NEXT = { open: "design", design: "build", build: "verify", verify: "archive" };
|
|
6
|
+
const skill = (phase) => `$matrix-${phase}`;
|
|
7
|
+
const stamp = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
8
|
+
const rootFor = (cwd) => path.join(cwd, ".matrix");
|
|
9
|
+
const exists = (file) => fs.existsSync(file);
|
|
10
|
+
const read = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
|
|
11
|
+
const write = (file, value) => fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
12
|
+
|
|
13
|
+
function fail(code, message) { return { ok: false, code, message }; }
|
|
14
|
+
function taskContent(change, taskId, proposal, design, plan, resultPath) {
|
|
15
|
+
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| 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- Run every validation command specified by the 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`;
|
|
16
|
+
}
|
|
17
|
+
function activeBoard(taskId, taskPath, resultPath, change) {
|
|
18
|
+
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`;
|
|
19
|
+
}
|
|
20
|
+
function paths(cwd, id) { const root = rootFor(cwd); return { root, active: path.join(root, "active.json"), change: path.join(root, "changes", id), flow: path.join(root, "changes", id, "matrix.json"), legacyFlow: path.join(root, "changes", id, "matrix.yaml"), artifacts: path.join(root, "changes", id, "artifacts") }; }
|
|
21
|
+
function readFlow(p) { if (fs.existsSync(p.flow)) return read(p.flow); if (!fs.existsSync(p.legacyFlow)) return null; return Object.fromEntries(fs.readFileSync(p.legacyFlow, "utf8").split(/\r?\n/).filter((line) => line.includes(":") && !line.startsWith(" ")).map((line) => { const [key, ...value] = line.split(":"); return [key.trim(), value.join(":").trim().replace(/^"|"$/g, "")]; })); }
|
|
22
|
+
function writeFlow(p, flow) { if (fs.existsSync(p.legacyFlow)) { const order = ["schema", "id", "workflow", "status", "phase", "title", "created_at", "updated_at", "acceptance", "scope"]; fs.writeFileSync(p.legacyFlow, `${order.map((key) => `${key}: ${flow[key] ?? ""}`).join("\n")}\n`); } else write(p.flow, flow); }
|
|
23
|
+
function active(cwd) { const activeFile = path.join(rootFor(cwd), "active.json"); if (!fs.existsSync(activeFile)) return null; return read(activeFile).change_id; }
|
|
24
|
+
function meaningful(file, headings) { if (!fs.existsSync(file)) return false; const value = fs.readFileSync(file, "utf8").trim(); return value.length >= 40 && headings.every((heading) => value.includes(heading)); }
|
|
25
|
+
function guard(cwd, id, phase) {
|
|
26
|
+
const base = paths(cwd, id).artifacts;
|
|
27
|
+
const checks = phase === "open" ? [["proposal", meaningful(path.join(base, "proposal.md"), ["## Goal", "## Scope", "## Acceptance"])]]
|
|
28
|
+
: phase === "design" ? [["design", meaningful(path.join(base, "design.md"), ["## Decisions", "## Test seams"])], ["plan", meaningful(path.join(base, "plan.md"), ["## Steps", "## Validation"])]]
|
|
29
|
+
: phase === "build" ? [["plan", meaningful(path.join(base, "plan.md"), ["## Steps"])], ["build evidence", meaningful(path.join(base, "verification.md"), ["## Build evidence"])]]
|
|
30
|
+
: [["verification", meaningful(path.join(base, "verification.md"), ["## Test evidence", "## Review evidence"])]];
|
|
31
|
+
return { pass: checks.every(([, result]) => result), checks };
|
|
32
|
+
}
|
|
33
|
+
function event(p, name, before, after) { fs.appendFileSync(path.join(p.change, "events.jsonl"), `${JSON.stringify({ at: stamp(), event: name, from: before, to: after })}\n`); }
|
|
34
|
+
|
|
35
|
+
export function invoke(argv, { cwd = process.cwd() } = {}) {
|
|
36
|
+
const [command, ...args] = argv;
|
|
37
|
+
if (command === "init") {
|
|
38
|
+
const id = (args.shift() ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
39
|
+
const workflow = args.includes("--workflow") ? args[args.indexOf("--workflow") + 1] : "full";
|
|
40
|
+
const title = args.includes("--title") ? args[args.indexOf("--title") + 1] : null;
|
|
41
|
+
if (!id || !title || !["full", "hotfix", "tweak"].includes(workflow)) return fail("INVALID_INTENT", "init requires a valid change id and --title.");
|
|
42
|
+
if (active(cwd)) return fail("ACTIVE_CHANGE_EXISTS", `Active Matrix change already exists: ${active(cwd)}.`);
|
|
43
|
+
const p = paths(cwd, id); if (fs.existsSync(p.change) || fs.existsSync(path.join(p.root, "archive", id))) return fail("CHANGE_EXISTS", `Matrix change already exists: ${id}.`);
|
|
44
|
+
fs.mkdirSync(p.artifacts, { recursive: true }); fs.mkdirSync(path.join(p.root, "archive"), { recursive: true });
|
|
45
|
+
if (!fs.existsSync(path.join(p.root, "config.yaml"))) fs.writeFileSync(path.join(p.root, "config.yaml"), "schema: matrix/config/v1\nauto_transition: true\n");
|
|
46
|
+
if (!fs.existsSync(path.join(p.root, ".gitignore"))) fs.writeFileSync(path.join(p.root, ".gitignore"), "active.json\ninstallation.json\nchanges/*/run-state.json\nchanges/*/events.jsonl\nchanges/*/artifacts/handoff.md\n");
|
|
47
|
+
const flow = { schema: "matrix/change/v1", id, workflow, status: "active", phase: "open", title, created_at: stamp(), updated_at: stamp(), acceptance: "pending", scope: "pending" };
|
|
48
|
+
fs.writeFileSync(p.legacyFlow, ""); writeFlow(p, flow); write(path.join(p.change, "run-state.json"), { schema: "matrix/run/v1", current_step: "open", iteration: 0, pending_gate: null, updated_at: stamp() }); write(p.active, { change_id: id }); event(p, "initialized", null, "open"); return { ok: true, code: "OK", change_id: id, next_skill: skill("open") };
|
|
49
|
+
}
|
|
50
|
+
const id = active(cwd); if (!id) return fail("NO_ACTIVE_CHANGE", "No active Matrix change. Start with $matrix <request>.");
|
|
51
|
+
const p = paths(cwd, id); const flow = readFlow(p); if (!flow) return fail("STATE_INVALID", "active.json does not reference a valid Matrix change.");
|
|
52
|
+
if (command === "inspect") { const result = guard(cwd, id, flow.phase); return { ok: true, code: "OK", change_id: id, workflow: flow.workflow, phase: flow.phase, next_skill: skill(flow.phase), guard_pass: result.pass }; }
|
|
53
|
+
if (command === "guard") { const phase = args[0] ?? flow.phase; if (!PHASES.includes(phase)) return fail("INVALID_INTENT", "Unknown phase."); const result = guard(cwd, id, phase); return { ok: result.pass, code: result.pass ? "OK" : "GUARD_FAILED", phase, ...result }; }
|
|
54
|
+
if (command === "transition") { const to = args[0]; if (NEXT[flow.phase] !== to) return fail("ILLEGAL_TRANSITION", `Illegal transition ${flow.phase} -> ${to}.`); const result = guard(cwd, id, flow.phase); if (!result.pass) return fail("GUARD_FAILED", "Current phase guard failed."); const before = flow.phase; flow.phase = to; flow.updated_at = stamp(); writeFlow(p, flow); event(p, "transition", before, to); return { ok: true, code: "OK", change_id: id, phase: to, next_skill: skill(to) }; }
|
|
55
|
+
if (command === "archive") {
|
|
56
|
+
if (flow.phase !== "archive") return fail("ILLEGAL_TRANSITION", "Only an archive-phase change can be archived.");
|
|
57
|
+
const result = guard(cwd, id, "archive"); if (!result.pass) return fail("GUARD_FAILED", "Archive guard failed.");
|
|
58
|
+
flow.status = "archived"; flow.updated_at = stamp(); writeFlow(p, flow); event(p, "archived", "archive", "archived");
|
|
59
|
+
const target = path.join(p.root, "archive", id); fs.mkdirSync(path.dirname(target), { recursive: true }); fs.renameSync(p.change, target); fs.rmSync(p.active);
|
|
60
|
+
return { ok: true, code: "OK", archived: id };
|
|
61
|
+
}
|
|
62
|
+
if (command === "handoff") { const destination = path.join(p.artifacts, "handoff.md"); fs.writeFileSync(destination, `# Matrix handoff: ${id}\n\n- Workflow: ${flow.workflow}\n- Current phase: ${flow.phase}\n- Resume with: \`${skill(flow.phase)}\`\n- Guard passes: ${guard(cwd, id, flow.phase).pass}\n`); return { ok: true, code: "OK", path: destination }; }
|
|
63
|
+
if (command === "export") {
|
|
64
|
+
if (flow.phase !== "design") return fail("ILLEGAL_PHASE", "Export requires the design phase.");
|
|
65
|
+
if (!guard(cwd, id, "design").pass) return fail("GUARD_FAILED", "Design guard failed.");
|
|
66
|
+
const taskId = args.includes("--task-id") ? args[args.indexOf("--task-id") + 1] : args[0];
|
|
67
|
+
const target = args.includes("--target") ? args[args.indexOf("--target") + 1] : "generic";
|
|
68
|
+
const applyBoard = args.includes("--apply-fnsec-board");
|
|
69
|
+
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.");
|
|
70
|
+
const artifacts = ["proposal.md", "design.md", "plan.md"].map((name) => path.join(p.artifacts, name));
|
|
71
|
+
if (artifacts.some((file) => !fs.existsSync(file))) return fail("ARTIFACT_MISSING", "Export requires proposal.md, design.md, and plan.md.");
|
|
72
|
+
const [proposal, design, plan] = artifacts.map((file) => fs.readFileSync(file, "utf8"));
|
|
73
|
+
if (target === "generic") {
|
|
74
|
+
const destination = path.join(p.artifacts, "claude-task.md"); const result = path.join(p.artifacts, "claude-result.md");
|
|
75
|
+
fs.writeFileSync(destination, taskContent(id, taskId, proposal, design, plan, path.relative(cwd, result).replaceAll("\\", "/")));
|
|
76
|
+
return { ok: true, code: "OK", path: destination };
|
|
77
|
+
}
|
|
78
|
+
const tasks = path.join(cwd, "docs", "tasks"); const board = path.join(tasks, "active.md");
|
|
79
|
+
if (!exists(tasks) || !exists(board)) return fail("FNSEC_TARGET_UNAVAILABLE", "FnSec target requires docs/tasks/active.md in the current project.");
|
|
80
|
+
const destination = path.join(tasks, `claude-task-${taskId}.md`); const result = path.join(tasks, `claude-result-${taskId}.md`);
|
|
81
|
+
if (exists(destination) || exists(result)) return fail("FNSEC_TASK_EXISTS", `FnSec task or result path already exists for ${taskId}.`);
|
|
82
|
+
const currentBoard = fs.readFileSync(board, "utf8");
|
|
83
|
+
if (applyBoard && !currentBoard.includes("NO_ACTIVE_OBJECTIVE")) return fail("FNSEC_BOARD_OCCUPIED", "FnSec active.md already has an objective; refused to overwrite it.");
|
|
84
|
+
fs.writeFileSync(destination, taskContent(id, taskId, proposal, design, plan, path.relative(cwd, result).replaceAll("\\", "/")));
|
|
85
|
+
if (applyBoard) fs.writeFileSync(board, activeBoard(taskId, path.relative(cwd, destination).replaceAll("\\", "/"), path.relative(cwd, result).replaceAll("\\", "/"), id));
|
|
86
|
+
return { ok: true, code: "OK", path: destination, board: applyBoard ? board : null };
|
|
87
|
+
}
|
|
88
|
+
return fail("INVALID_INTENT", `Unknown workflow command: ${command}.`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1].replaceAll("\\", "/")}`).href) {
|
|
92
|
+
const result = invoke(process.argv.slice(2));
|
|
93
|
+
console.log(JSON.stringify(result));
|
|
94
|
+
if (!result.ok) process.exitCode = 2;
|
|
95
|
+
}
|