@rosenfan/matrix 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/matrix/SKILL.md +29 -34
- package/.claude/skills/matrix/scripts/matrix_state.py +11 -11
- package/.claude/skills/matrix-archive/SKILL.md +12 -12
- package/.claude/skills/matrix-build/SKILL.md +16 -13
- package/.claude/skills/matrix-claude/SKILL.md +10 -10
- package/.claude/skills/matrix-design/SKILL.md +19 -17
- package/.claude/skills/matrix-hotfix/SKILL.md +1 -1
- package/.claude/skills/matrix-open/SKILL.md +23 -21
- package/.claude/skills/matrix-status/SKILL.md +3 -3
- package/.claude/skills/matrix-tweak/SKILL.md +1 -1
- package/.claude/skills/matrix-verify/SKILL.md +39 -23
- package/README.md +78 -69
- package/README.zh-CN.md +77 -68
- package/assets/skills-zh-CN/matrix/SKILL.md +33 -33
- package/assets/skills-zh-CN/matrix-archive/SKILL.md +10 -10
- package/assets/skills-zh-CN/matrix-build/SKILL.md +13 -11
- package/assets/skills-zh-CN/matrix-claude/SKILL.md +10 -10
- package/assets/skills-zh-CN/matrix-design/SKILL.md +13 -11
- package/assets/skills-zh-CN/matrix-hotfix/SKILL.md +1 -1
- package/assets/skills-zh-CN/matrix-open/SKILL.md +19 -17
- package/assets/skills-zh-CN/matrix-status/SKILL.md +3 -3
- package/assets/skills-zh-CN/matrix-tweak/SKILL.md +1 -1
- package/assets/skills-zh-CN/matrix-verify/SKILL.md +27 -11
- package/package.json +1 -1
- package/src/catalog.js +31 -20
- package/src/cli.js +149 -91
- package/src/distribution.js +259 -136
- package/src/installer.js +10 -10
- package/src/matt-catalog.mjs +67 -0
- package/src/matt.js +115 -33
- package/src/messages.js +14 -10
- package/src/runtime-resolver.js +76 -0
- package/src/self-update.js +81 -81
- package/src/workflow-transaction.js +13 -9
- package/src/workflow.js +641 -388
package/src/workflow.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
2
|
+
import path from "node:path";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { MATT_AUTOMATIC_SKILLS, MATT_CATALOG_DIGEST, MATT_COMPATIBILITY, MATT_CONTENT_HASHES } from "./matt-catalog.mjs";
|
|
6
8
|
import {
|
|
7
9
|
captureWorkflowEntry,
|
|
8
10
|
runWorkflowTransaction,
|
|
@@ -14,62 +16,68 @@ import {
|
|
|
14
16
|
workflowWriteOperation
|
|
15
17
|
} from "./workflow-transaction.js";
|
|
16
18
|
|
|
17
|
-
const PHASES = ["open", "design", "build", "verify", "archive"];
|
|
18
|
-
const FULL_PROFILE = {
|
|
19
|
-
id: "full",
|
|
20
|
-
next: { open: "design", design: "build", build: "verify", verify: "archive" },
|
|
21
|
-
contractFiles: ["proposal.md", "design.md", "plan.md"]
|
|
22
|
-
};
|
|
23
|
-
const LIGHTWEIGHT_PROFILE = {
|
|
24
|
-
id: "lightweight",
|
|
25
|
-
next: { open: "build", build: "verify", verify: "archive" },
|
|
26
|
-
contractFiles: ["proposal.md"]
|
|
27
|
-
};
|
|
28
|
-
const FULL_RETURNS = [
|
|
29
|
-
{ from: "build", to: "design", reason: "design-gap" },
|
|
30
|
-
{ from: "verify", to: "build", reason: "verification-failed" },
|
|
31
|
-
{ from: "verify", to: "design", reason: "acceptance-or-design-gap" },
|
|
32
|
-
{ from: "archive", to: "design", reason: "acceptance-or-design-gap" }
|
|
33
|
-
];
|
|
34
|
-
const LIGHTWEIGHT_RETURNS = [
|
|
35
|
-
{ from: "build", to: "design", reason: "design-gap", upgrade: true },
|
|
36
|
-
{ from: "verify", to: "build", reason: "verification-failed" },
|
|
37
|
-
{ from: "verify", to: "design", reason: "acceptance-or-design-gap", upgrade: true },
|
|
38
|
-
{ from: "archive", to: "design", reason: "acceptance-or-design-gap", upgrade: true }
|
|
39
|
-
];
|
|
40
|
-
const RETURN_REASONS = new Set([...FULL_RETURNS, ...LIGHTWEIGHT_RETURNS].map(({ reason }) => reason));
|
|
19
|
+
const PHASES = ["open", "design", "build", "verify", "archive"];
|
|
20
|
+
const FULL_PROFILE = {
|
|
21
|
+
id: "full",
|
|
22
|
+
next: { open: "design", design: "build", build: "verify", verify: "archive" },
|
|
23
|
+
contractFiles: ["proposal.md", "design.md", "plan.md"]
|
|
24
|
+
};
|
|
25
|
+
const LIGHTWEIGHT_PROFILE = {
|
|
26
|
+
id: "lightweight",
|
|
27
|
+
next: { open: "build", build: "verify", verify: "archive" },
|
|
28
|
+
contractFiles: ["proposal.md"]
|
|
29
|
+
};
|
|
30
|
+
const FULL_RETURNS = [
|
|
31
|
+
{ from: "build", to: "design", reason: "design-gap" },
|
|
32
|
+
{ from: "verify", to: "build", reason: "verification-failed" },
|
|
33
|
+
{ from: "verify", to: "design", reason: "acceptance-or-design-gap" },
|
|
34
|
+
{ from: "archive", to: "design", reason: "acceptance-or-design-gap" }
|
|
35
|
+
];
|
|
36
|
+
const LIGHTWEIGHT_RETURNS = [
|
|
37
|
+
{ from: "build", to: "design", reason: "design-gap", upgrade: true },
|
|
38
|
+
{ from: "verify", to: "build", reason: "verification-failed" },
|
|
39
|
+
{ from: "verify", to: "design", reason: "acceptance-or-design-gap", upgrade: true },
|
|
40
|
+
{ from: "archive", to: "design", reason: "acceptance-or-design-gap", upgrade: true }
|
|
41
|
+
];
|
|
42
|
+
const RETURN_REASONS = new Set([...FULL_RETURNS, ...LIGHTWEIGHT_RETURNS].map(({ reason }) => reason));
|
|
41
43
|
const INVALIDATED_EVIDENCE = ["build", "test", "review", "archive"];
|
|
42
44
|
const ABORT_REASONS = new Set(["requirement-cancelled", "superseded", "no-longer-valuable", "blocked", "other"]);
|
|
43
|
-
const ORCHESTRATIONS = ["prim", "arch"];
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
45
|
+
const ORCHESTRATIONS = ["prim", "arch"];
|
|
46
|
+
const quoteCommandPath = (value) => process.platform === "win32" ? `"${value}"` : `'${value.replaceAll("'", "'\\''")}'`;
|
|
47
|
+
const workflowCommand = (argumentsText) => `node ${quoteCommandPath(fileURLToPath(import.meta.url))} ${argumentsText}`;
|
|
48
|
+
const LEGACY_ARCH_SKILLS = [
|
|
49
|
+
"grilling", "domain-modeling", "research", "wayfinder", "prototype",
|
|
50
|
+
"codebase-design", "tdd", "diagnosing-bugs", "resolving-merge-conflicts", "code-review"
|
|
51
|
+
];
|
|
52
|
+
const ARTIFACT_POLICY = {
|
|
53
|
+
proposal: ["## Goal", "## Scope", "## Non-goals", "## Acceptance", "## Risks"],
|
|
54
|
+
design: ["## Decisions", "## Boundaries", "## Test seams", "## Risks"],
|
|
55
|
+
plan: ["## Steps", "## Validation", "## Stop conditions"],
|
|
56
|
+
build: ["## Build evidence"],
|
|
57
|
+
verification: ["## Test evidence", "## Review evidence"]
|
|
58
|
+
};
|
|
59
|
+
const LIGHTWEIGHT_COMMON_PROPOSAL = ["## Goal", "## Scope", "## Non-goals", "## Approach", "## Acceptance", "## Validation", "## Risks", "## Upgrade conditions"];
|
|
60
|
+
const LIGHTWEIGHT_POLICY = {
|
|
61
|
+
hotfix: {
|
|
62
|
+
proposal: [...LIGHTWEIGHT_COMMON_PROPOSAL, "## Expected behavior", "## Actual behavior", "## Reproduction"],
|
|
63
|
+
build: ["## Build evidence", "## Reproduction evidence", "## Root cause"],
|
|
64
|
+
verification: ["## Test evidence", "## Regression evidence", "## Review evidence"]
|
|
65
|
+
},
|
|
66
|
+
tweak: {
|
|
67
|
+
proposal: [...LIGHTWEIGHT_COMMON_PROPOSAL, "## Current behavior", "## Preserved behavior", "## Diff boundary"],
|
|
68
|
+
build: ["## Build evidence", "## Scope evidence"],
|
|
69
|
+
verification: ["## Test evidence", "## Scope review evidence", "## Review evidence"]
|
|
70
|
+
}
|
|
71
|
+
};
|
|
68
72
|
const WORKSPACE_BASELINE_FILE = "workspace-baseline.json";
|
|
69
73
|
const WORKSPACE_EXCLUDES = new Set([".git", ".matrix", ".comet", "node_modules"]);
|
|
70
74
|
const WORKSPACE_MAX_FILES = 50000;
|
|
71
75
|
const WORKSPACE_MAX_BYTES = 512 * 1024 * 1024;
|
|
72
76
|
const WORKSPACE_MAX_DURATION_MS = 60000;
|
|
77
|
+
const REVIEW_RECEIPT_SCHEMA = "matrix/review-receipt/v1";
|
|
78
|
+
const REVIEW_SOURCES = new Set(["code-review", "matrix-fallback"]);
|
|
79
|
+
const REVIEW_OUTCOMES = new Set(["passed", "failed"]);
|
|
80
|
+
const REVIEW_FALLBACK_REASONS = new Set(["git-baseline-unavailable", "empty-head-diff", "uncommitted-worktree", "capability-execution-failed"]);
|
|
73
81
|
const skill = (phase) => `$matrix-${phase}`;
|
|
74
82
|
const stamp = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
75
83
|
const rootFor = (cwd) => path.join(cwd, ".matrix");
|
|
@@ -78,114 +86,114 @@ const read = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
|
|
|
78
86
|
|
|
79
87
|
function fail(code, message) { return { ok: false, code, message }; }
|
|
80
88
|
function option(args, name) { const index = args.indexOf(name); return index >= 0 ? args[index + 1] : null; }
|
|
81
|
-
function flatYaml(file) {
|
|
89
|
+
function flatYaml(file) {
|
|
82
90
|
try {
|
|
83
91
|
return Object.fromEntries(fs.readFileSync(file, "utf8").split(/\r?\n/).filter((line) => line.includes(":") && !line.startsWith(" ")).map((line) => {
|
|
84
92
|
const [key, ...value] = line.split(":");
|
|
85
93
|
return [key.trim(), value.join(":").trim().replace(/^"|"$/g, "")];
|
|
86
94
|
}));
|
|
87
|
-
} catch { return {}; }
|
|
88
|
-
}
|
|
89
|
-
function excludedWorkspacePath(relative) {
|
|
90
|
-
const first = relative.replaceAll("\\", "/").split("/")[0];
|
|
91
|
-
return WORKSPACE_EXCLUDES.has(first);
|
|
92
|
-
}
|
|
93
|
-
function workspaceFiles(cwd) {
|
|
94
|
-
const git = spawnSync("git", ["-C", cwd, "ls-files", "-co", "--exclude-standard", "-z"], {
|
|
95
|
-
encoding: "buffer",
|
|
96
|
-
windowsHide: true,
|
|
97
|
-
maxBuffer: 64 * 1024 * 1024
|
|
98
|
-
});
|
|
99
|
-
if (!git.error && git.status === 0) {
|
|
100
|
-
return {
|
|
101
|
-
provider: "git",
|
|
102
|
-
files: [...new Set(git.stdout.toString("utf8").split("\0").filter(Boolean))]
|
|
103
|
-
.map((relative) => relative.replaceAll("\\", "/"))
|
|
104
|
-
.filter((relative) => !excludedWorkspacePath(relative))
|
|
105
|
-
.sort()
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
const files = [];
|
|
109
|
-
const visit = (directory, prefix = "") => {
|
|
110
|
-
for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
111
|
-
const relative = path.join(prefix, entry.name).replaceAll("\\", "/");
|
|
112
|
-
if (!prefix && WORKSPACE_EXCLUDES.has(entry.name)) continue;
|
|
113
|
-
if (entry.isDirectory()) visit(path.join(directory, entry.name), relative);
|
|
114
|
-
else files.push(relative);
|
|
115
|
-
if (files.length > WORKSPACE_MAX_FILES) throw new Error(`Workspace baseline exceeds ${WORKSPACE_MAX_FILES} files.`);
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
visit(cwd);
|
|
119
|
-
return { provider: "physical", files };
|
|
120
|
-
}
|
|
121
|
-
function captureWorkspaceBaseline(cwd) {
|
|
122
|
-
const startedAt = Date.now();
|
|
123
|
-
const selection = workspaceFiles(cwd);
|
|
124
|
-
const hash = crypto.createHash("sha256");
|
|
125
|
-
hash.update("matrix/workspace-baseline/v1\0", "utf8");
|
|
126
|
-
let totalBytes = 0;
|
|
127
|
-
let fileCount = 0;
|
|
128
|
-
for (const relative of selection.files) {
|
|
129
|
-
if (Date.now() - startedAt > WORKSPACE_MAX_DURATION_MS) throw new Error(`Workspace baseline exceeds ${WORKSPACE_MAX_DURATION_MS} milliseconds.`);
|
|
130
|
-
if (fileCount >= WORKSPACE_MAX_FILES) throw new Error(`Workspace baseline exceeds ${WORKSPACE_MAX_FILES} files.`);
|
|
131
|
-
const absolute = path.join(cwd, relative);
|
|
132
|
-
const pathBytes = Buffer.from(relative, "utf8");
|
|
133
|
-
const pathLength = Buffer.alloc(4); pathLength.writeUInt32BE(pathBytes.length);
|
|
134
|
-
hash.update(pathLength); hash.update(pathBytes);
|
|
135
|
-
let stat;
|
|
136
|
-
try { stat = fs.lstatSync(absolute); }
|
|
137
|
-
catch (error) {
|
|
138
|
-
if (error.code !== "ENOENT") throw error;
|
|
139
|
-
hash.update("missing\0", "utf8");
|
|
140
|
-
fileCount += 1;
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
if (stat.isSymbolicLink()) {
|
|
144
|
-
const target = Buffer.from(fs.readlinkSync(absolute), "utf8");
|
|
145
|
-
hash.update("symlink\0", "utf8"); hash.update(target);
|
|
146
|
-
totalBytes += target.length;
|
|
147
|
-
} else if (stat.isFile()) {
|
|
148
|
-
const bytes = fs.readFileSync(absolute);
|
|
149
|
-
totalBytes += bytes.length;
|
|
150
|
-
if (totalBytes > WORKSPACE_MAX_BYTES) throw new Error(`Workspace baseline exceeds ${WORKSPACE_MAX_BYTES} bytes.`);
|
|
151
|
-
hash.update("file\0", "utf8"); hash.update(bytes);
|
|
152
|
-
} else {
|
|
153
|
-
hash.update("special\0", "utf8");
|
|
154
|
-
}
|
|
155
|
-
fileCount += 1;
|
|
156
|
-
}
|
|
157
|
-
return {
|
|
158
|
-
schema: "matrix/workspace-baseline/v1",
|
|
159
|
-
provider: selection.provider,
|
|
160
|
-
hash: `sha256:${hash.digest("hex")}`,
|
|
161
|
-
file_count: fileCount,
|
|
162
|
-
content_bytes: totalBytes
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
function workspaceOrderGuard(cwd, p) {
|
|
166
|
-
let baseline;
|
|
167
|
-
try { baseline = read(p.workspaceBaseline); }
|
|
168
|
-
catch (error) {
|
|
169
|
-
return fail("WORKSPACE_BASELINE_MISSING", `Lightweight workflow baseline is unavailable: ${error.message}`);
|
|
170
|
-
}
|
|
171
|
-
let current;
|
|
172
|
-
try { current = captureWorkspaceBaseline(cwd); }
|
|
173
|
-
catch (error) {
|
|
174
|
-
return fail("WORKSPACE_INSPECTION_FAILED", `Could not inspect the lightweight workflow boundary: ${error.message}`);
|
|
175
|
-
}
|
|
176
|
-
if (baseline.schema !== "matrix/workspace-baseline/v1" || !/^sha256:[0-9a-f]{64}$/.test(baseline.hash ?? "")) {
|
|
177
|
-
return fail("WORKSPACE_BASELINE_INVALID", "Lightweight workflow baseline is invalid.");
|
|
178
|
-
}
|
|
179
|
-
if (current.hash !== baseline.hash) {
|
|
180
|
-
return {
|
|
181
|
-
...fail("PREMATURE_IMPLEMENTATION", "Project files changed after lightweight initialization and before Build approval."),
|
|
182
|
-
baseline_hash: baseline.hash,
|
|
183
|
-
current_hash: current.hash
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
return { ok: true, code: "OK", baseline_hash: baseline.hash, current_hash: current.hash };
|
|
187
|
-
}
|
|
188
|
-
function hashDirectory(directory) {
|
|
95
|
+
} catch { return {}; }
|
|
96
|
+
}
|
|
97
|
+
function excludedWorkspacePath(relative) {
|
|
98
|
+
const first = relative.replaceAll("\\", "/").split("/")[0];
|
|
99
|
+
return WORKSPACE_EXCLUDES.has(first);
|
|
100
|
+
}
|
|
101
|
+
function workspaceFiles(cwd) {
|
|
102
|
+
const git = spawnSync("git", ["-C", cwd, "ls-files", "-co", "--exclude-standard", "-z"], {
|
|
103
|
+
encoding: "buffer",
|
|
104
|
+
windowsHide: true,
|
|
105
|
+
maxBuffer: 64 * 1024 * 1024
|
|
106
|
+
});
|
|
107
|
+
if (!git.error && git.status === 0) {
|
|
108
|
+
return {
|
|
109
|
+
provider: "git",
|
|
110
|
+
files: [...new Set(git.stdout.toString("utf8").split("\0").filter(Boolean))]
|
|
111
|
+
.map((relative) => relative.replaceAll("\\", "/"))
|
|
112
|
+
.filter((relative) => !excludedWorkspacePath(relative))
|
|
113
|
+
.sort()
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const files = [];
|
|
117
|
+
const visit = (directory, prefix = "") => {
|
|
118
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
119
|
+
const relative = path.join(prefix, entry.name).replaceAll("\\", "/");
|
|
120
|
+
if (!prefix && WORKSPACE_EXCLUDES.has(entry.name)) continue;
|
|
121
|
+
if (entry.isDirectory()) visit(path.join(directory, entry.name), relative);
|
|
122
|
+
else files.push(relative);
|
|
123
|
+
if (files.length > WORKSPACE_MAX_FILES) throw new Error(`Workspace baseline exceeds ${WORKSPACE_MAX_FILES} files.`);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
visit(cwd);
|
|
127
|
+
return { provider: "physical", files };
|
|
128
|
+
}
|
|
129
|
+
function captureWorkspaceBaseline(cwd) {
|
|
130
|
+
const startedAt = Date.now();
|
|
131
|
+
const selection = workspaceFiles(cwd);
|
|
132
|
+
const hash = crypto.createHash("sha256");
|
|
133
|
+
hash.update("matrix/workspace-baseline/v1\0", "utf8");
|
|
134
|
+
let totalBytes = 0;
|
|
135
|
+
let fileCount = 0;
|
|
136
|
+
for (const relative of selection.files) {
|
|
137
|
+
if (Date.now() - startedAt > WORKSPACE_MAX_DURATION_MS) throw new Error(`Workspace baseline exceeds ${WORKSPACE_MAX_DURATION_MS} milliseconds.`);
|
|
138
|
+
if (fileCount >= WORKSPACE_MAX_FILES) throw new Error(`Workspace baseline exceeds ${WORKSPACE_MAX_FILES} files.`);
|
|
139
|
+
const absolute = path.join(cwd, relative);
|
|
140
|
+
const pathBytes = Buffer.from(relative, "utf8");
|
|
141
|
+
const pathLength = Buffer.alloc(4); pathLength.writeUInt32BE(pathBytes.length);
|
|
142
|
+
hash.update(pathLength); hash.update(pathBytes);
|
|
143
|
+
let stat;
|
|
144
|
+
try { stat = fs.lstatSync(absolute); }
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (error.code !== "ENOENT") throw error;
|
|
147
|
+
hash.update("missing\0", "utf8");
|
|
148
|
+
fileCount += 1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (stat.isSymbolicLink()) {
|
|
152
|
+
const target = Buffer.from(fs.readlinkSync(absolute), "utf8");
|
|
153
|
+
hash.update("symlink\0", "utf8"); hash.update(target);
|
|
154
|
+
totalBytes += target.length;
|
|
155
|
+
} else if (stat.isFile()) {
|
|
156
|
+
const bytes = fs.readFileSync(absolute);
|
|
157
|
+
totalBytes += bytes.length;
|
|
158
|
+
if (totalBytes > WORKSPACE_MAX_BYTES) throw new Error(`Workspace baseline exceeds ${WORKSPACE_MAX_BYTES} bytes.`);
|
|
159
|
+
hash.update("file\0", "utf8"); hash.update(bytes);
|
|
160
|
+
} else {
|
|
161
|
+
hash.update("special\0", "utf8");
|
|
162
|
+
}
|
|
163
|
+
fileCount += 1;
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
schema: "matrix/workspace-baseline/v1",
|
|
167
|
+
provider: selection.provider,
|
|
168
|
+
hash: `sha256:${hash.digest("hex")}`,
|
|
169
|
+
file_count: fileCount,
|
|
170
|
+
content_bytes: totalBytes
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function workspaceOrderGuard(cwd, p) {
|
|
174
|
+
let baseline;
|
|
175
|
+
try { baseline = read(p.workspaceBaseline); }
|
|
176
|
+
catch (error) {
|
|
177
|
+
return fail("WORKSPACE_BASELINE_MISSING", `Lightweight workflow baseline is unavailable: ${error.message}`);
|
|
178
|
+
}
|
|
179
|
+
let current;
|
|
180
|
+
try { current = captureWorkspaceBaseline(cwd); }
|
|
181
|
+
catch (error) {
|
|
182
|
+
return fail("WORKSPACE_INSPECTION_FAILED", `Could not inspect the lightweight workflow boundary: ${error.message}`);
|
|
183
|
+
}
|
|
184
|
+
if (baseline.schema !== "matrix/workspace-baseline/v1" || !/^sha256:[0-9a-f]{64}$/.test(baseline.hash ?? "")) {
|
|
185
|
+
return fail("WORKSPACE_BASELINE_INVALID", "Lightweight workflow baseline is invalid.");
|
|
186
|
+
}
|
|
187
|
+
if (current.hash !== baseline.hash) {
|
|
188
|
+
return {
|
|
189
|
+
...fail("PREMATURE_IMPLEMENTATION", "Project files changed after lightweight initialization and before Build approval."),
|
|
190
|
+
baseline_hash: baseline.hash,
|
|
191
|
+
current_hash: current.hash
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return { ok: true, code: "OK", baseline_hash: baseline.hash, current_hash: current.hash };
|
|
195
|
+
}
|
|
196
|
+
function hashDirectory(directory) {
|
|
189
197
|
const hash = crypto.createHash("sha256");
|
|
190
198
|
const files = [];
|
|
191
199
|
const visit = (current, relative = "") => {
|
|
@@ -207,38 +215,57 @@ function orchestrationConfig(cwd) {
|
|
|
207
215
|
const config = flatYaml(path.join(rootFor(cwd), "config.yaml"));
|
|
208
216
|
const scope = config.installation_scope === "global" ? "global" : "project";
|
|
209
217
|
const manifestFile = scope === "global" ? path.join(os.homedir(), ".matrix", "installation.json") : path.join(rootFor(cwd), "installation.json");
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
218
|
+
const mattReceiptFile = path.join(rootFor(cwd), "matt-installation.json");
|
|
219
|
+
let manifest = null;
|
|
220
|
+
let mattReceipt = null;
|
|
221
|
+
try { manifest = JSON.parse(fs.readFileSync(manifestFile, "utf8")); } catch {}
|
|
222
|
+
try { mattReceipt = JSON.parse(fs.readFileSync(mattReceiptFile, "utf8")); } catch {}
|
|
223
|
+
const defaultOrchestration = ORCHESTRATIONS.includes(config.default_orchestration) ? config.default_orchestration
|
|
224
|
+
: ORCHESTRATIONS.includes(manifest?.orchestration?.default) ? manifest.orchestration.default : "prim";
|
|
225
|
+
const artifactLanguage = ["en", "zh-CN"].includes(config.language) ? config.language
|
|
226
|
+
: ["en", "zh-CN"].includes(manifest?.language) ? manifest.language : "en";
|
|
227
|
+
return { config, scope, manifestFile, manifest, mattReceiptFile, mattReceipt, defaultOrchestration, artifactLanguage };
|
|
228
|
+
}
|
|
229
|
+
function archIntegrity(cwd, flow = null, contentHashes = MATT_CONTENT_HASHES) {
|
|
230
|
+
const { manifest, manifestFile, mattReceiptFile, mattReceipt } = orchestrationConfig(cwd);
|
|
231
|
+
const platforms = Object.entries(manifest?.platforms ?? {});
|
|
232
|
+
if (!manifest?.orchestration?.available?.includes("arch") || !platforms.length) {
|
|
233
|
+
return { ok: false, code: "ARCH_INSTALLATION_INCOMPLETE", message: "Arch requires a verified managed Skill cohort on every configured platform.", recovery_command: "matrix init --with-mattpocock" };
|
|
234
|
+
}
|
|
235
|
+
const legacy = Boolean(flow && !flow.arch_catalog_digest);
|
|
236
|
+
if (!legacy && (mattReceipt?.compatibility?.release !== MATT_COMPATIBILITY.release
|
|
237
|
+
|| mattReceipt?.compatibility?.commit !== MATT_COMPATIBILITY.commit
|
|
238
|
+
|| mattReceipt?.catalogDigest !== MATT_CATALOG_DIGEST)) {
|
|
239
|
+
return { ok: false, code: "ARCH_INSTALLATION_INCOMPLETE", message: "Arch requires the project-local Matt compatibility receipt for this Matrix release.", manifest: mattReceiptFile, recovery_command: "matrix init --with-mattpocock" };
|
|
240
|
+
}
|
|
241
|
+
if (!legacy && flow && (flow.arch_catalog_digest !== MATT_CATALOG_DIGEST || flow.matt_release !== MATT_COMPATIBILITY.release)) {
|
|
242
|
+
return { ok: false, code: "ARCH_INSTALLATION_INCOMPLETE", message: "The active Arch catalog identity does not match this Runtime.", manifest: mattReceiptFile, recovery_command: "matrix update" };
|
|
243
|
+
}
|
|
244
|
+
const findings = [];
|
|
245
|
+
for (const [platform, record] of platforms) {
|
|
246
|
+
for (const skill of legacy ? LEGACY_ARCH_SKILLS : MATT_AUTOMATIC_SKILLS) {
|
|
247
|
+
const installed = legacy ? record?.matt?.skills?.[skill] : mattReceipt?.platforms?.[platform]?.skills?.[skill];
|
|
248
|
+
const expectedRoot = path.join(cwd, platform === "codex" ? ".agents" : ".claude", "skills", skill);
|
|
249
|
+
const target = legacy ? installed?.root : expectedRoot;
|
|
250
|
+
if (!legacy && installed?.root && path.resolve(installed.root) !== path.resolve(expectedRoot)) {
|
|
251
|
+
findings.push({ platform, skill, reason: "non-project" });
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (!target || !installed?.hash || !fs.existsSync(path.join(target, "SKILL.md"))) {
|
|
255
|
+
findings.push({ platform, skill, reason: "missing" });
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
const actual = hashDirectory(target);
|
|
260
|
+
if (legacy ? actual !== installed.hash : actual !== installed.hash || actual !== contentHashes[skill]) findings.push({ platform, skill, reason: "modified" });
|
|
234
261
|
} catch { findings.push({ platform, skill, reason: "unreadable" }); }
|
|
235
262
|
}
|
|
236
263
|
}
|
|
237
|
-
return findings.length ? {
|
|
238
|
-
ok: false, code: "ARCH_INSTALLATION_INCOMPLETE", message: "Arch installation integrity failed. Repair the managed cohort before continuing.",
|
|
239
|
-
manifest: manifestFile, findings, recovery_command: "matrix init --with-mattpocock"
|
|
240
|
-
} : { ok: true, code: "OK", manifest: manifestFile, platforms: platforms.map(([platform]) => platform) };
|
|
241
|
-
}
|
|
264
|
+
return findings.length ? {
|
|
265
|
+
ok: false, code: "ARCH_INSTALLATION_INCOMPLETE", message: "Arch installation integrity failed. Repair the managed cohort before continuing.",
|
|
266
|
+
manifest: legacy ? manifestFile : mattReceiptFile, findings, recovery_command: "matrix init --with-mattpocock"
|
|
267
|
+
} : { 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 };
|
|
268
|
+
}
|
|
242
269
|
function taskContent(change, taskId, proposal, design, plan, resultPath, contractHash, approvalRevision) {
|
|
243
270
|
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`;
|
|
244
271
|
}
|
|
@@ -246,14 +273,15 @@ function activeBoard(taskId, taskPath, resultPath, change) {
|
|
|
246
273
|
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`;
|
|
247
274
|
}
|
|
248
275
|
function paths(cwd, id, archived = false) {
|
|
249
|
-
const root = rootFor(cwd);
|
|
250
|
-
const change = path.join(root, archived ? "archive" : "changes", id);
|
|
251
|
-
return {
|
|
252
|
-
root,
|
|
253
|
-
active: path.join(root, "active.json"),
|
|
276
|
+
const root = rootFor(cwd);
|
|
277
|
+
const change = path.join(root, archived ? "archive" : "changes", id);
|
|
278
|
+
return {
|
|
279
|
+
root,
|
|
280
|
+
active: path.join(root, "active.json"),
|
|
254
281
|
change,
|
|
255
282
|
flow: path.join(change, "matrix.yaml"),
|
|
256
283
|
artifacts: path.join(change, "artifacts"),
|
|
284
|
+
reviewReceipt: path.join(change, "artifacts", "review-receipt.json"),
|
|
257
285
|
workspaceBaseline: path.join(change, WORKSPACE_BASELINE_FILE)
|
|
258
286
|
};
|
|
259
287
|
}
|
|
@@ -261,91 +289,218 @@ function normalizeFlow(flow) {
|
|
|
261
289
|
if (!flow) return null;
|
|
262
290
|
if (flow.schema !== "matrix/change/v2") return flow.schema ? { unsupported_schema: flow.schema } : null;
|
|
263
291
|
const revision = Number(flow.revision);
|
|
264
|
-
const approved = flow.approved_contract_hash || null;
|
|
292
|
+
const approved = flow.approved_contract_hash || null;
|
|
265
293
|
const approvalRevision = flow.contract_approved_revision === "" || flow.contract_approved_revision == null ? null : Number(flow.contract_approved_revision);
|
|
266
294
|
const workflowProfile = flow.workflow_profile || null;
|
|
267
|
-
|
|
295
|
+
const archCatalogDigest = flow.arch_catalog_digest || null;
|
|
296
|
+
const mattRelease = flow.matt_release || null;
|
|
297
|
+
if (
|
|
268
298
|
typeof flow.id !== "string" || !flow.id ||
|
|
269
299
|
!["full", "hotfix", "tweak"].includes(flow.workflow) ||
|
|
270
300
|
!ORCHESTRATIONS.includes(flow.orchestration) ||
|
|
271
301
|
!["active", "archived", "aborted"].includes(flow.status) ||
|
|
272
302
|
!PHASES.includes(flow.phase) ||
|
|
273
|
-
!Number.isInteger(revision) || revision < 1 ||
|
|
274
|
-
(workflowProfile !== null && workflowProfile !== "lightweight") ||
|
|
275
|
-
(workflowProfile === "lightweight" && !["hotfix", "tweak"].includes(flow.workflow)) ||
|
|
303
|
+
!Number.isInteger(revision) || revision < 1 ||
|
|
304
|
+
(workflowProfile !== null && workflowProfile !== "lightweight") ||
|
|
305
|
+
(workflowProfile === "lightweight" && !["hotfix", "tweak"].includes(flow.workflow)) ||
|
|
276
306
|
(flow.artifact_language != null && !["en", "zh-CN"].includes(flow.artifact_language)) ||
|
|
307
|
+
Boolean(archCatalogDigest) !== Boolean(mattRelease) ||
|
|
308
|
+
(archCatalogDigest && !/^[0-9a-f]{64}$/.test(archCatalogDigest)) ||
|
|
277
309
|
Boolean(approved) !== Boolean(approvalRevision) ||
|
|
278
310
|
(approved && !/^sha256:[0-9a-f]{64}$/.test(approved)) ||
|
|
279
311
|
(approvalRevision != null && (!Number.isInteger(approvalRevision) || approvalRevision < 1 || approvalRevision > revision))
|
|
280
312
|
) return null;
|
|
281
|
-
return { ...flow, revision, workflow_profile: workflowProfile, artifact_language: flow.artifact_language ?? null, approved_contract_hash: approved, contract_approved_revision: approvalRevision };
|
|
313
|
+
return { ...flow, revision, workflow_profile: workflowProfile, artifact_language: flow.artifact_language ?? null, arch_catalog_digest: archCatalogDigest, matt_release: mattRelease, approved_contract_hash: approved, contract_approved_revision: approvalRevision };
|
|
282
314
|
}
|
|
283
315
|
function readFlow(p) {
|
|
284
316
|
if (!fs.existsSync(p.flow)) return null;
|
|
285
317
|
const parsed = Object.fromEntries(fs.readFileSync(p.flow, "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, "")]; }));
|
|
286
318
|
return normalizeFlow(parsed);
|
|
287
319
|
}
|
|
288
|
-
function flowContent(flow) {
|
|
289
|
-
const order = ["schema", "id", "workflow", "workflow_profile", "orchestration", "artifact_language", "status", "phase", "revision", "approved_contract_hash", "contract_approved_revision", "title", "created_at", "updated_at", "acceptance", "scope"];
|
|
290
|
-
return `${order.filter((key) => (
|
|
291
|
-
}
|
|
292
|
-
function active(cwd) { const activeFile = path.join(rootFor(cwd), "active.json"); if (!fs.existsSync(activeFile)) return null; return read(activeFile).change_id; }
|
|
320
|
+
function flowContent(flow) {
|
|
321
|
+
const order = ["schema", "id", "workflow", "workflow_profile", "orchestration", "artifact_language", "arch_catalog_digest", "matt_release", "status", "phase", "revision", "approved_contract_hash", "contract_approved_revision", "title", "created_at", "updated_at", "acceptance", "scope"];
|
|
322
|
+
return `${order.filter((key) => (!["workflow_profile", "artifact_language", "arch_catalog_digest", "matt_release"].includes(key) || flow[key])).map((key) => `${key}: ${flow[key] ?? ""}`).join("\n")}\n`;
|
|
323
|
+
}
|
|
324
|
+
function active(cwd) { const activeFile = path.join(rootFor(cwd), "active.json"); if (!fs.existsSync(activeFile)) return null; return read(activeFile).change_id; }
|
|
293
325
|
function meaningful(file, headings) {
|
|
294
|
-
if (!fs.existsSync(file)) return false;
|
|
295
|
-
const value = fs.readFileSync(file, "utf8").trim();
|
|
296
|
-
if (value.length < 40) return false;
|
|
297
|
-
return headings.every((heading) => {
|
|
298
|
-
const start = value.indexOf(heading);
|
|
299
|
-
if (start < 0) return false;
|
|
300
|
-
const bodyStart = start + heading.length;
|
|
301
|
-
const next = value.indexOf("\n## ", bodyStart);
|
|
302
|
-
return value.slice(bodyStart, next < 0 ? value.length : next).trim().length >= 8;
|
|
326
|
+
if (!fs.existsSync(file)) return false;
|
|
327
|
+
const value = fs.readFileSync(file, "utf8").trim();
|
|
328
|
+
if (value.length < 40) return false;
|
|
329
|
+
return headings.every((heading) => {
|
|
330
|
+
const start = value.indexOf(heading);
|
|
331
|
+
if (start < 0) return false;
|
|
332
|
+
const bodyStart = start + heading.length;
|
|
333
|
+
const next = value.indexOf("\n## ", bodyStart);
|
|
334
|
+
return value.slice(bodyStart, next < 0 ? value.length : next).trim().length >= 8;
|
|
303
335
|
});
|
|
304
336
|
}
|
|
305
|
-
function
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
337
|
+
function digestText(domain, value) {
|
|
338
|
+
return `sha256:${crypto.createHash("sha256").update(`${domain}\0`, "utf8").update(value).digest("hex")}`;
|
|
339
|
+
}
|
|
340
|
+
function reviewCandidateHash(diffHash, workspaceHash) {
|
|
341
|
+
return digestText("matrix/review-candidate/v1", `${diffHash}\0${workspaceHash}`);
|
|
342
|
+
}
|
|
343
|
+
function markdownSection(value, heading) {
|
|
344
|
+
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
345
|
+
const match = new RegExp(`^${escaped}\\s*$`, "m").exec(value);
|
|
346
|
+
if (!match) return null;
|
|
347
|
+
const level = heading.match(/^#+/)?.[0].length ?? 1;
|
|
348
|
+
const tail = value.slice(match.index + match[0].length);
|
|
349
|
+
const next = new RegExp(`^#{1,${level}}\\s+`, "m").exec(tail);
|
|
350
|
+
return tail.slice(0, next?.index ?? tail.length).trim();
|
|
351
|
+
}
|
|
352
|
+
function reviewEvidence(p) {
|
|
353
|
+
const file = path.join(p.artifacts, "verification.md");
|
|
354
|
+
let value;
|
|
355
|
+
try { value = fs.readFileSync(file, "utf8"); }
|
|
356
|
+
catch (error) { return fail("REVIEW_EVIDENCE_INCOMPLETE", `Review evidence is unavailable: ${error.message}`); }
|
|
357
|
+
const review = markdownSection(value, "## Review evidence");
|
|
358
|
+
const standards = review == null ? null : markdownSection(review, "### Standards");
|
|
359
|
+
const spec = review == null ? null : markdownSection(review, "### Spec");
|
|
360
|
+
if (!review || !standards || standards.length < 8 || !spec || spec.length < 8) {
|
|
361
|
+
return fail("REVIEW_EVIDENCE_INCOMPLETE", "Review evidence requires substantive ### Standards and ### Spec sections.");
|
|
320
362
|
}
|
|
321
|
-
return { hash:
|
|
363
|
+
return { ok: true, code: "OK", hash: digestText("matrix/review-evidence/v1", review), standards, spec };
|
|
322
364
|
}
|
|
323
|
-
function
|
|
324
|
-
|
|
325
|
-
return
|
|
365
|
+
function gitText(cwd, args) {
|
|
366
|
+
const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8", windowsHide: true });
|
|
367
|
+
return !result.error && result.status === 0 ? result.stdout.trim() : null;
|
|
326
368
|
}
|
|
327
|
-
function
|
|
328
|
-
|
|
369
|
+
function gitPaths(cwd, args) {
|
|
370
|
+
const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "buffer", windowsHide: true, maxBuffer: 64 * 1024 * 1024 });
|
|
371
|
+
if (result.error || result.status !== 0) return null;
|
|
372
|
+
return result.stdout.toString("utf8").split("\0").filter(Boolean).map((relative) => relative.replaceAll("\\", "/"));
|
|
329
373
|
}
|
|
330
|
-
function
|
|
331
|
-
const
|
|
332
|
-
const
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
374
|
+
function managedReviewPrefixes(cwd) {
|
|
375
|
+
const { manifest, mattReceipt } = orchestrationConfig(cwd);
|
|
376
|
+
const prefixes = [];
|
|
377
|
+
for (const [platform, record] of Object.entries(manifest?.platforms ?? {})) {
|
|
378
|
+
const root = platform === "codex" ? ".agents/skills" : platform === "claude-code" ? ".claude/skills" : null;
|
|
379
|
+
if (!root) continue;
|
|
380
|
+
for (const skill of Object.keys(record?.skills ?? {})) prefixes.push(`${root}/${skill}`);
|
|
381
|
+
for (const skill of Object.keys(mattReceipt?.platforms?.[platform]?.skills ?? {})) prefixes.push(`${root}/${skill}`);
|
|
382
|
+
}
|
|
383
|
+
return prefixes;
|
|
384
|
+
}
|
|
385
|
+
function reviewWorktreeFacts(cwd) {
|
|
386
|
+
const tracked = gitPaths(cwd, ["diff", "--name-only", "-z", "HEAD", "--"]);
|
|
387
|
+
const untracked = gitPaths(cwd, ["ls-files", "--others", "--exclude-standard", "-z"]);
|
|
388
|
+
if (!tracked || !untracked) return fail("REVIEW_WORKTREE_UNAVAILABLE", "The candidate worktree could not be inspected.");
|
|
389
|
+
const managed = managedReviewPrefixes(cwd);
|
|
390
|
+
const files = [...new Set([...tracked, ...untracked])].filter((relative) => !excludedWorkspacePath(relative)
|
|
391
|
+
&& !managed.some((prefix) => relative === prefix || relative.startsWith(`${prefix}/`))).sort();
|
|
392
|
+
return { ok: true, code: "OK", dirty: files.length > 0, files };
|
|
343
393
|
}
|
|
344
|
-
function
|
|
345
|
-
|
|
394
|
+
function codeReviewFacts(cwd, fixedPoint, { allowEmpty = false, requireClean = true } = {}) {
|
|
395
|
+
if (!fixedPoint || fixedPoint.startsWith("-")) return fail("REVIEW_FIXED_POINT_INVALID", "code-review requires a resolvable Git fixed point.");
|
|
396
|
+
const resolved = gitText(cwd, ["rev-parse", "--verify", `${fixedPoint}^{commit}`]);
|
|
397
|
+
const head = gitText(cwd, ["rev-parse", "--verify", "HEAD^{commit}"]);
|
|
398
|
+
if (!resolved || !head) return fail("REVIEW_FIXED_POINT_INVALID", "code-review requires a resolvable fixed point and Git HEAD.");
|
|
399
|
+
const mergeBase = gitText(cwd, ["merge-base", resolved, head]);
|
|
400
|
+
if (!mergeBase) return fail("REVIEW_FIXED_POINT_INVALID", "The fixed point and HEAD have no merge base.");
|
|
401
|
+
const diff = spawnSync("git", ["-C", cwd, "diff", "--binary", `${resolved}...${head}`, "--"], { encoding: "buffer", windowsHide: true, maxBuffer: 64 * 1024 * 1024 });
|
|
402
|
+
if (diff.error || diff.status !== 0) return fail("REVIEW_DIFF_UNAVAILABLE", "The code-review diff could not be captured.");
|
|
403
|
+
if (!diff.stdout.length && !allowEmpty) return fail("REVIEW_DIFF_EMPTY", "The code-review fixed point has an empty diff to HEAD. Use the bounded Matrix fallback for an uncommitted candidate.");
|
|
404
|
+
const worktree = reviewWorktreeFacts(cwd);
|
|
405
|
+
if (!worktree.ok) return worktree;
|
|
406
|
+
if (requireClean && worktree.dirty) return { ...fail("REVIEW_WORKTREE_DIRTY", "code-review covers fixed point to HEAD only. Use the bounded uncommitted-worktree fallback for staged, unstaged, or untracked candidate files."), files: worktree.files };
|
|
407
|
+
return { ok: true, code: "OK", fixed_point: resolved, head, merge_base: mergeBase, diff_hash: digestText("matrix/code-review-diff/v1", diff.stdout), diff_empty: !diff.stdout.length };
|
|
346
408
|
}
|
|
347
|
-
function
|
|
348
|
-
|
|
409
|
+
function expectedReviewRevision(flow) { return flow.phase === "archive" ? flow.revision - 1 : flow.revision; }
|
|
410
|
+
function reviewReceiptGuard(p, flow) {
|
|
411
|
+
if (flow.orchestration !== "arch" || !["verify", "archive"].includes(flow.phase)) return { ok: true, code: "OK" };
|
|
412
|
+
if (!fs.existsSync(p.reviewReceipt)) return fail("REVIEW_RECEIPT_MISSING", "Arch Verify requires a Runtime-generated review receipt for the current revision.");
|
|
413
|
+
let receipt;
|
|
414
|
+
try { receipt = read(p.reviewReceipt); }
|
|
415
|
+
catch (error) { return fail("REVIEW_RECEIPT_INVALID", `The Arch review receipt is unreadable: ${error.message}`); }
|
|
416
|
+
const expectedRevision = expectedReviewRevision(flow);
|
|
417
|
+
if (receipt.schema !== REVIEW_RECEIPT_SCHEMA || receipt.change_id !== flow.id || receipt.revision !== expectedRevision
|
|
418
|
+
|| receipt.orchestration !== "arch" || !REVIEW_SOURCES.has(receipt.source)
|
|
419
|
+
|| !REVIEW_OUTCOMES.has(receipt.standards) || !REVIEW_OUTCOMES.has(receipt.specification)
|
|
420
|
+
|| receipt.contract_hash !== flow.approved_contract_hash) {
|
|
421
|
+
return fail("REVIEW_RECEIPT_INVALID", "The Arch review receipt does not match the current workflow identity.");
|
|
422
|
+
}
|
|
423
|
+
if (receipt.standards !== "passed" || receipt.specification !== "passed") {
|
|
424
|
+
return fail("REVIEW_FAILED", "Standards and Spec review axes must both pass before Archive.");
|
|
425
|
+
}
|
|
426
|
+
const evidence = reviewEvidence(p);
|
|
427
|
+
if (!evidence.ok) return evidence;
|
|
428
|
+
if (receipt.evidence_hash !== evidence.hash) return fail("REVIEW_RECEIPT_STALE", "Review evidence changed after the receipt was recorded.");
|
|
429
|
+
let workspace;
|
|
430
|
+
try { workspace = captureWorkspaceBaseline(path.dirname(p.root)); }
|
|
431
|
+
catch (error) { return fail("REVIEW_RECEIPT_STALE", `The reviewed workspace can no longer be inspected: ${error.message}`); }
|
|
432
|
+
if (receipt.workspace?.provider !== workspace.provider || receipt.workspace?.hash !== workspace.hash) {
|
|
433
|
+
return fail("REVIEW_RECEIPT_STALE", "Project files changed after the review receipt was recorded.");
|
|
434
|
+
}
|
|
435
|
+
if (receipt.source === "code-review") {
|
|
436
|
+
const facts = codeReviewFacts(path.dirname(p.root), receipt.fixed_point);
|
|
437
|
+
if (!facts.ok || facts.head !== receipt.head || facts.merge_base !== receipt.merge_base || facts.diff_hash !== receipt.diff_hash) {
|
|
438
|
+
return fail("REVIEW_RECEIPT_STALE", "The code-review fixed point, HEAD, or diff changed after receipt recording.");
|
|
439
|
+
}
|
|
440
|
+
} else if (!REVIEW_FALLBACK_REASONS.has(receipt.reason)) {
|
|
441
|
+
return fail("REVIEW_RECEIPT_INVALID", "Matrix fallback receipts require an allowed companion failure reason.");
|
|
442
|
+
} else if (receipt.reason === "git-baseline-unavailable" && gitText(path.dirname(p.root), ["rev-parse", "--verify", "HEAD^{commit}"])) {
|
|
443
|
+
return fail("REVIEW_RECEIPT_STALE", "The fallback claimed no Git baseline, but the project now has a resolvable HEAD.");
|
|
444
|
+
} else if (receipt.reason === "empty-head-diff") {
|
|
445
|
+
const facts = codeReviewFacts(path.dirname(p.root), receipt.fixed_point, { allowEmpty: true, requireClean: false });
|
|
446
|
+
if (!facts.ok || !facts.diff_empty || facts.head !== receipt.head || facts.merge_base !== receipt.merge_base || facts.diff_hash !== receipt.diff_hash) {
|
|
447
|
+
return fail("REVIEW_RECEIPT_STALE", "The empty HEAD diff fallback no longer matches the recorded Git facts.");
|
|
448
|
+
}
|
|
449
|
+
} else if (receipt.reason === "uncommitted-worktree") {
|
|
450
|
+
const facts = codeReviewFacts(path.dirname(p.root), receipt.fixed_point, { allowEmpty: true, requireClean: false });
|
|
451
|
+
const worktree = reviewWorktreeFacts(path.dirname(p.root));
|
|
452
|
+
const candidateHash = facts.ok ? reviewCandidateHash(facts.diff_hash, workspace.hash) : null;
|
|
453
|
+
if (!facts.ok || !worktree.ok || !worktree.dirty || facts.head !== receipt.head || facts.merge_base !== receipt.merge_base
|
|
454
|
+
|| facts.diff_hash !== receipt.diff_hash || candidateHash !== receipt.candidate_hash) {
|
|
455
|
+
return fail("REVIEW_RECEIPT_STALE", "The uncommitted worktree fallback no longer matches the reviewed candidate.");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return { ok: true, code: "OK", receipt };
|
|
459
|
+
}
|
|
460
|
+
function shortcut(flow) { return flow.workflow === "hotfix" || flow.workflow === "tweak"; }
|
|
461
|
+
function contractSnapshotForFiles(p, files) {
|
|
462
|
+
const hash = crypto.createHash("sha256");
|
|
463
|
+
hash.update("matrix/workflow-contract/v1\\0", "utf8");
|
|
464
|
+
const contents = {};
|
|
465
|
+
for (const name of files) {
|
|
466
|
+
const logical = `artifacts/${name}`;
|
|
467
|
+
let bytes;
|
|
468
|
+
try { bytes = fs.readFileSync(path.join(p.artifacts, name)); }
|
|
469
|
+
catch (error) { if (error.code === "ENOENT") return { missing: true, contents }; return { error, contents }; }
|
|
470
|
+
const pathBytes = Buffer.from(logical, "utf8");
|
|
471
|
+
const pathLength = Buffer.alloc(4); pathLength.writeUInt32BE(pathBytes.length);
|
|
472
|
+
const contentLength = Buffer.alloc(8); contentLength.writeBigUInt64BE(BigInt(bytes.length));
|
|
473
|
+
hash.update(pathLength); hash.update(pathBytes); hash.update(contentLength); hash.update(bytes);
|
|
474
|
+
contents[name] = bytes.toString("utf8");
|
|
475
|
+
}
|
|
476
|
+
return { hash: `sha256:${hash.digest("hex")}`, contents };
|
|
477
|
+
}
|
|
478
|
+
function workflowProfile(p, flow) {
|
|
479
|
+
if (!shortcut(flow)) return FULL_PROFILE;
|
|
480
|
+
return flow.workflow_profile === "lightweight" ? LIGHTWEIGHT_PROFILE : { ...FULL_PROFILE, id: "legacy-full" };
|
|
481
|
+
}
|
|
482
|
+
function evidencePolicy(flow, profile) {
|
|
483
|
+
return profile.id === "lightweight" ? flow.workflow : "full";
|
|
484
|
+
}
|
|
485
|
+
function structuralGuard(p, flow, phase) {
|
|
486
|
+
const base = p.artifacts;
|
|
487
|
+
const profile = workflowProfile(p, flow);
|
|
488
|
+
const policy = evidencePolicy(flow, profile);
|
|
489
|
+
const shortcutPolicy = LIGHTWEIGHT_POLICY[policy];
|
|
490
|
+
const checks = phase === "open" ? [["proposal", meaningful(path.join(base, "proposal.md"), shortcutPolicy?.proposal ?? ARTIFACT_POLICY.proposal)]]
|
|
491
|
+
: phase === "design" ? [["proposal", meaningful(path.join(base, "proposal.md"), ARTIFACT_POLICY.proposal)], ["design", meaningful(path.join(base, "design.md"), ARTIFACT_POLICY.design)], ["plan", meaningful(path.join(base, "plan.md"), ARTIFACT_POLICY.plan)]]
|
|
492
|
+
: phase === "build" ? [
|
|
493
|
+
...(profile.id === "lightweight" ? [] : [["plan", meaningful(path.join(base, "plan.md"), ARTIFACT_POLICY.plan)]]),
|
|
494
|
+
["build evidence", meaningful(path.join(base, "verification.md"), shortcutPolicy?.build ?? ARTIFACT_POLICY.build)]
|
|
495
|
+
]
|
|
496
|
+
: [["verification", meaningful(path.join(base, "verification.md"), shortcutPolicy?.verification ?? ARTIFACT_POLICY.verification)]];
|
|
497
|
+
return { pass: checks.every(([, result]) => result), checks };
|
|
498
|
+
}
|
|
499
|
+
function contractSnapshot(p, flow) {
|
|
500
|
+
return contractSnapshotForFiles(p, workflowProfile(p, flow).contractFiles);
|
|
501
|
+
}
|
|
502
|
+
function contractState(p, flow) {
|
|
503
|
+
const snapshot = contractSnapshot(p, flow);
|
|
349
504
|
if (snapshot.error) return { status: "unreadable", snapshot };
|
|
350
505
|
if (!flow.approved_contract_hash) return { status: "unapproved", snapshot };
|
|
351
506
|
if (snapshot.missing || snapshot.hash !== flow.approved_contract_hash) return { status: "changed", snapshot };
|
|
@@ -476,24 +631,36 @@ function archivePreflightChanged(expected, current) {
|
|
|
476
631
|
...fail("ARCHIVE_PREFLIGHT_CHANGED", "Archive inputs changed after dry-run. Run dry-run again and review the new preflight."),
|
|
477
632
|
expected_preflight_hash: expected,
|
|
478
633
|
current_preflight_hash: current,
|
|
479
|
-
recovery_command: "
|
|
634
|
+
recovery_command: workflowCommand("archive --dry-run")
|
|
480
635
|
};
|
|
481
636
|
}
|
|
482
637
|
function guard(p, flow, phase) {
|
|
483
|
-
if (!["build", "verify", "archive"].includes(phase)) {
|
|
484
|
-
const structural = structuralGuard(p, flow, phase);
|
|
485
|
-
return { ok: structural.pass, code: structural.pass ? "OK" : "GUARD_FAILED", ...structural };
|
|
486
|
-
}
|
|
638
|
+
if (!["build", "verify", "archive"].includes(phase)) {
|
|
639
|
+
const structural = structuralGuard(p, flow, phase);
|
|
640
|
+
return { ok: structural.pass, code: structural.pass ? "OK" : "GUARD_FAILED", ...structural };
|
|
641
|
+
}
|
|
487
642
|
const contract = contractState(p, flow); const failure = contractFailure(flow, contract);
|
|
488
643
|
if (failure) return { ok: false, ...failure, checks: [["contract", false]] };
|
|
489
644
|
const structural = structuralGuard(p, flow, phase);
|
|
490
|
-
|
|
491
|
-
}
|
|
645
|
+
const checks = [["contract", true], ...structural.checks];
|
|
646
|
+
if (!structural.pass) return { ok: false, code: "GUARD_FAILED", ...structural, ...contractFields(flow, contract), checks };
|
|
647
|
+
if (["verify", "archive"].includes(phase)) {
|
|
648
|
+
const evidence = reviewEvidence(p);
|
|
649
|
+
if (!evidence.ok) return { ...evidence, pass: false, ...contractFields(flow, contract), checks: [...checks, ["review axes", false]] };
|
|
650
|
+
checks.push(["review axes", true]);
|
|
651
|
+
}
|
|
652
|
+
if (flow.orchestration === "arch" && ["verify", "archive"].includes(phase)) {
|
|
653
|
+
const receipt = reviewReceiptGuard(p, flow);
|
|
654
|
+
if (!receipt.ok) return { ...receipt, pass: false, ...contractFields(flow, contract), checks: [...checks, ["arch review receipt", false]] };
|
|
655
|
+
checks.push(["arch review receipt", true]);
|
|
656
|
+
}
|
|
657
|
+
return { ok: true, code: "OK", pass: true, ...contractFields(flow, contract), checks };
|
|
658
|
+
}
|
|
492
659
|
function eventContent(file, name, before, after, revision, details = {}) {
|
|
493
660
|
const current = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
494
661
|
return `${current}${JSON.stringify({ schema: "matrix/event/v1", at: stamp(), event: name, revision, from: before, to: after, ...details })}\n`;
|
|
495
662
|
}
|
|
496
|
-
export function invoke(argv, { cwd = process.cwd(), failAfterOperation = null } = {}) {
|
|
663
|
+
export function invoke(argv, { cwd = process.cwd(), failAfterOperation = null, mattContentHashes = MATT_CONTENT_HASHES } = {}) {
|
|
497
664
|
const [command, ...args] = argv;
|
|
498
665
|
if (command === "init") {
|
|
499
666
|
const id = (args.shift() ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
@@ -505,26 +672,26 @@ export function invoke(argv, { cwd = process.cwd(), failAfterOperation = null }
|
|
|
505
672
|
const orchestration = requestedOrchestration ?? configured.defaultOrchestration;
|
|
506
673
|
if (!id || !title || !["full", "hotfix", "tweak"].includes(workflow)) return fail("INVALID_INTENT", "init requires a valid change id and --title.");
|
|
507
674
|
if (orchestration === "arch") {
|
|
508
|
-
const integrity = archIntegrity(cwd);
|
|
675
|
+
const integrity = archIntegrity(cwd, null, mattContentHashes);
|
|
509
676
|
if (!integrity.ok) return { ...integrity, orchestration };
|
|
510
677
|
}
|
|
511
|
-
if (active(cwd)) return fail("ACTIVE_CHANGE_EXISTS", `Active Matrix change already exists: ${active(cwd)}.`);
|
|
512
|
-
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}.`);
|
|
513
|
-
let workspaceBaseline = null;
|
|
514
|
-
if (workflow === "hotfix" || workflow === "tweak") {
|
|
515
|
-
try { workspaceBaseline = captureWorkspaceBaseline(cwd); }
|
|
516
|
-
catch (error) { return fail("WORKSPACE_BASELINE_FAILED", `Could not initialize the lightweight workflow boundary: ${error.message}`); }
|
|
517
|
-
}
|
|
518
|
-
const flow = { schema: "matrix/change/v2", id, workflow, ...(workflow === "full" ? {} : { workflow_profile: "lightweight" }), orchestration, artifact_language: configured.artifactLanguage, status: "active", phase: "open", revision: 1, title, created_at: stamp(), updated_at: stamp(), acceptance: "pending", scope: "pending" };
|
|
519
|
-
const operations = [
|
|
678
|
+
if (active(cwd)) return fail("ACTIVE_CHANGE_EXISTS", `Active Matrix change already exists: ${active(cwd)}.`);
|
|
679
|
+
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}.`);
|
|
680
|
+
let workspaceBaseline = null;
|
|
681
|
+
if (workflow === "hotfix" || workflow === "tweak") {
|
|
682
|
+
try { workspaceBaseline = captureWorkspaceBaseline(cwd); }
|
|
683
|
+
catch (error) { return fail("WORKSPACE_BASELINE_FAILED", `Could not initialize the lightweight workflow boundary: ${error.message}`); }
|
|
684
|
+
}
|
|
685
|
+
const flow = { schema: "matrix/change/v2", id, workflow, ...(workflow === "full" ? {} : { workflow_profile: "lightweight" }), orchestration, artifact_language: configured.artifactLanguage, ...(orchestration === "arch" ? { arch_catalog_digest: MATT_CATALOG_DIGEST, matt_release: MATT_COMPATIBILITY.release } : {}), status: "active", phase: "open", revision: 1, title, created_at: stamp(), updated_at: stamp(), acceptance: "pending", scope: "pending" };
|
|
686
|
+
const operations = [
|
|
520
687
|
...(!fs.existsSync(path.join(p.root, "archive")) ? [workflowMkdirOperation(cwd, path.join(p.root, "archive"))] : []),
|
|
521
688
|
workflowMkdirOperation(cwd, p.artifacts),
|
|
522
|
-
...(!fs.existsSync(path.join(p.root, "config.yaml")) ? [workflowWriteOperation(cwd, path.join(p.root, "config.yaml"), `schema: matrix/config/v1\nauto_transition: true\ndefault_orchestration: ${configured.defaultOrchestration}\ninstallation_scope: project\nlanguage: ${configured.artifactLanguage}\n`)] : []),
|
|
523
|
-
...(!fs.existsSync(path.join(p.root, ".gitignore")) ? [workflowWriteOperation(cwd, path.join(p.root, ".gitignore"), "active.json\ninstallation.json\nchanges/*/events.jsonl\nchanges/*/artifacts/handoff.md\n")] : []),
|
|
524
|
-
workflowWriteOperation(cwd, p.flow, flowContent(flow)),
|
|
525
|
-
...(workspaceBaseline ? [workflowWriteOperation(cwd, p.workspaceBaseline, `${JSON.stringify(workspaceBaseline, null, 2)}\n`)] : []),
|
|
526
|
-
workflowWriteOperation(cwd, path.join(p.change, "events.jsonl"), eventContent(path.join(p.change, "events.jsonl"), "initialized", null, "open", flow.revision, { workflow, orchestration, ...(shortcut(flow) ? { profile: "lightweight" } : {}) })),
|
|
527
|
-
workflowWriteOperation(cwd, p.active, `${JSON.stringify({ change_id: id }, null, 2)}\n`)
|
|
689
|
+
...(!fs.existsSync(path.join(p.root, "config.yaml")) ? [workflowWriteOperation(cwd, path.join(p.root, "config.yaml"), `schema: matrix/config/v1\nauto_transition: true\ndefault_orchestration: ${configured.defaultOrchestration}\ninstallation_scope: project\nlanguage: ${configured.artifactLanguage}\n`)] : []),
|
|
690
|
+
...(!fs.existsSync(path.join(p.root, ".gitignore")) ? [workflowWriteOperation(cwd, path.join(p.root, ".gitignore"), "active.json\ninstallation.json\nmatt-installation.json\nchanges/*/events.jsonl\nchanges/*/artifacts/handoff.md\n")] : []),
|
|
691
|
+
workflowWriteOperation(cwd, p.flow, flowContent(flow)),
|
|
692
|
+
...(workspaceBaseline ? [workflowWriteOperation(cwd, p.workspaceBaseline, `${JSON.stringify(workspaceBaseline, null, 2)}\n`)] : []),
|
|
693
|
+
workflowWriteOperation(cwd, path.join(p.change, "events.jsonl"), eventContent(path.join(p.change, "events.jsonl"), "initialized", null, "open", flow.revision, { workflow, orchestration, ...(orchestration === "arch" ? { arch_catalog_digest: MATT_CATALOG_DIGEST, matt_release: MATT_COMPATIBILITY.release } : {}), ...(shortcut(flow) ? { profile: "lightweight" } : {}) })),
|
|
694
|
+
workflowWriteOperation(cwd, p.active, `${JSON.stringify({ change_id: id }, null, 2)}\n`)
|
|
528
695
|
];
|
|
529
696
|
return runWorkflowTransaction({
|
|
530
697
|
cwd,
|
|
@@ -532,7 +699,7 @@ export function invoke(argv, { cwd = process.cwd(), failAfterOperation = null }
|
|
|
532
699
|
changeId: id,
|
|
533
700
|
operations,
|
|
534
701
|
failAfterOperation,
|
|
535
|
-
result: { ok: true, code: "OK", change_id: id, workflow, profile: shortcut(flow) ? "lightweight" : "full", evidence_policy: workflow === "full" ? "full" : workflow, orchestration, artifact_language: flow.artifact_language, status: flow.status, phase: flow.phase, revision: flow.revision, next_skill: skill("open") }
|
|
702
|
+
result: { ok: true, code: "OK", change_id: id, workflow, profile: shortcut(flow) ? "lightweight" : "full", evidence_policy: workflow === "full" ? "full" : workflow, orchestration, artifact_language: flow.artifact_language, arch_catalog_digest: flow.arch_catalog_digest ?? null, matt_release: flow.matt_release ?? null, status: flow.status, phase: flow.phase, revision: flow.revision, next_skill: skill("open") }
|
|
536
703
|
});
|
|
537
704
|
}
|
|
538
705
|
if (command === "doctor") {
|
|
@@ -558,70 +725,149 @@ export function invoke(argv, { cwd = process.cwd(), failAfterOperation = null }
|
|
|
558
725
|
if (!id) return fail("NO_ACTIVE_CHANGE", "No active Matrix change. Start with $matrix <request>.");
|
|
559
726
|
let p = paths(cwd, id);
|
|
560
727
|
if (command === "inspect" && !fs.existsSync(p.change)) p = paths(cwd, id, true);
|
|
561
|
-
const flow = readFlow(p);
|
|
728
|
+
const flow = readFlow(p);
|
|
562
729
|
if (flow?.unsupported_schema) return fail("STATE_VERSION_UNSUPPORTED", `Unsupported Matrix state schema: ${flow.unsupported_schema}.`);
|
|
563
|
-
if (!flow) return fail("STATE_INVALID", "active.json does not reference a valid Matrix change.");
|
|
564
|
-
const artifactLanguage = flow.artifact_language ?? orchestrationConfig(cwd).artifactLanguage;
|
|
565
|
-
const integrity = flow.status === "active" && flow.orchestration === "arch" ? archIntegrity(cwd) : { ok: true, code: "OK" };
|
|
730
|
+
if (!flow) return fail("STATE_INVALID", "active.json does not reference a valid Matrix change.");
|
|
731
|
+
const artifactLanguage = flow.artifact_language ?? orchestrationConfig(cwd).artifactLanguage;
|
|
732
|
+
const integrity = flow.status === "active" && flow.orchestration === "arch" ? archIntegrity(cwd, flow, mattContentHashes) : { ok: true, code: "OK" };
|
|
566
733
|
if (["transition", "return", "abort"].includes(command) || (command === "archive" && !args.includes("--dry-run"))) {
|
|
567
734
|
const blocked = workflowMutationGate(cwd);
|
|
568
735
|
if (blocked) return blocked;
|
|
569
736
|
}
|
|
570
|
-
if (command === "inspect") {
|
|
571
|
-
const profile = workflowProfile(p, flow);
|
|
572
|
-
const contract = contractState(p, flow);
|
|
573
|
-
const result = guard(p, flow, flow.phase);
|
|
574
|
-
const failure = contract.status === "unreadable" ? contractFailure(flow, contract) : null;
|
|
575
|
-
return {
|
|
576
|
-
...(failure ?? (!integrity.ok ? integrity : { ok: true, code: "OK" })),
|
|
577
|
-
change_id: id,
|
|
578
|
-
workflow: flow.workflow,
|
|
579
|
-
profile: profile.id,
|
|
580
|
-
evidence_policy: evidencePolicy(flow, profile),
|
|
737
|
+
if (command === "inspect") {
|
|
738
|
+
const profile = workflowProfile(p, flow);
|
|
739
|
+
const contract = contractState(p, flow);
|
|
740
|
+
const result = guard(p, flow, flow.phase);
|
|
741
|
+
const failure = contract.status === "unreadable" ? contractFailure(flow, contract) : null;
|
|
742
|
+
return {
|
|
743
|
+
...(failure ?? (!integrity.ok ? integrity : { ok: true, code: "OK" })),
|
|
744
|
+
change_id: id,
|
|
745
|
+
workflow: flow.workflow,
|
|
746
|
+
profile: profile.id,
|
|
747
|
+
evidence_policy: evidencePolicy(flow, profile),
|
|
581
748
|
orchestration: flow.orchestration,
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
749
|
+
arch_catalog_digest: flow.arch_catalog_digest ?? null,
|
|
750
|
+
matt_release: flow.matt_release ?? null,
|
|
751
|
+
artifact_language: artifactLanguage,
|
|
752
|
+
status: flow.status,
|
|
753
|
+
phase: flow.phase,
|
|
754
|
+
revision: flow.revision,
|
|
755
|
+
next_skill: flow.status === "active" ? skill(flow.phase) : null,
|
|
756
|
+
guard_pass: result.ok,
|
|
757
|
+
...contractFields(flow, contract)
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
if (!integrity.ok && command !== "abort") {
|
|
761
|
+
const profile = workflowProfile(p, flow);
|
|
762
|
+
return { ...integrity, change_id: id, workflow: flow.workflow, profile: profile.id, evidence_policy: evidencePolicy(flow, profile), orchestration: flow.orchestration, artifact_language: artifactLanguage, status: flow.status, phase: flow.phase, revision: flow.revision, next_skill: skill(flow.phase) };
|
|
763
|
+
}
|
|
595
764
|
if (command === "guard") {
|
|
596
|
-
const phase = args[0] ?? flow.phase;
|
|
597
|
-
if (!PHASES.includes(phase)) return fail("INVALID_INTENT", "Unknown phase.");
|
|
598
|
-
const profile = workflowProfile(p, flow);
|
|
765
|
+
const phase = args[0] ?? flow.phase;
|
|
766
|
+
if (!PHASES.includes(phase)) return fail("INVALID_INTENT", "Unknown phase.");
|
|
767
|
+
const profile = workflowProfile(p, flow);
|
|
599
768
|
if (profile.id === "lightweight" && phase === "design") return fail("PHASE_UNAVAILABLE", "Lightweight workflows do not have a Design phase.");
|
|
600
769
|
return { phase, profile: profile.id, evidence_policy: evidencePolicy(flow, profile), artifact_language: artifactLanguage, ...guard(p, flow, phase) };
|
|
601
770
|
}
|
|
602
|
-
if (command === "
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
if (
|
|
606
|
-
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
771
|
+
if (command === "review") {
|
|
772
|
+
if (flow.status !== "active" || flow.phase !== "verify") return fail("ILLEGAL_PHASE", "Review receipts can only be recorded during Verify.");
|
|
773
|
+
if (flow.orchestration !== "arch") return fail("REVIEW_RECEIPT_UNAVAILABLE", "Prim performs Matrix-owned review without an Arch companion receipt.");
|
|
774
|
+
if (fs.existsSync(p.reviewReceipt)) return fail("REVIEW_RECEIPT_EXISTS", "This revision already has a review receipt. Use a controlled Return before recording another review.");
|
|
775
|
+
const source = option(args, "--source");
|
|
776
|
+
const standards = option(args, "--standards");
|
|
777
|
+
const specification = option(args, "--spec");
|
|
778
|
+
const fixedPoint = option(args, "--fixed-point");
|
|
779
|
+
const reason = option(args, "--reason");
|
|
780
|
+
if (!REVIEW_SOURCES.has(source)) return fail("REVIEW_SOURCE_INVALID", "Review source must be code-review or matrix-fallback.");
|
|
781
|
+
if (!REVIEW_OUTCOMES.has(standards) || !REVIEW_OUTCOMES.has(specification)) return fail("REVIEW_OUTCOME_INVALID", "Standards and Spec outcomes must each be passed or failed.");
|
|
782
|
+
const contract = contractState(p, flow); const contractError = contractFailure(flow, contract);
|
|
783
|
+
if (contractError) return contractError;
|
|
784
|
+
const evidence = reviewEvidence(p);
|
|
785
|
+
if (!evidence.ok) return evidence;
|
|
786
|
+
let sourceFacts = {};
|
|
787
|
+
if (source === "code-review") {
|
|
788
|
+
if (reason) return fail("REVIEW_SOURCE_INVALID", "A successful code-review receipt cannot include a fallback reason.");
|
|
789
|
+
const facts = codeReviewFacts(cwd, fixedPoint);
|
|
790
|
+
if (!facts.ok) return facts;
|
|
791
|
+
sourceFacts = { fixed_point: facts.fixed_point, head: facts.head, merge_base: facts.merge_base, diff_hash: facts.diff_hash };
|
|
792
|
+
} else {
|
|
793
|
+
if (!REVIEW_FALLBACK_REASONS.has(reason)) return fail("REVIEW_FALLBACK_REASON_INVALID", "Matrix fallback requires git-baseline-unavailable, empty-head-diff, uncommitted-worktree, or capability-execution-failed.");
|
|
794
|
+
if (reason === "git-baseline-unavailable") {
|
|
795
|
+
if (fixedPoint) return fail("REVIEW_SOURCE_INVALID", "A missing Git baseline fallback cannot include a fixed point.");
|
|
796
|
+
if (gitText(cwd, ["rev-parse", "--verify", "HEAD^{commit}"])) return fail("REVIEW_FALLBACK_REASON_MISMATCH", "The project has a resolvable Git HEAD; git-baseline-unavailable is not valid.");
|
|
797
|
+
sourceFacts = { reason };
|
|
798
|
+
} else if (reason === "empty-head-diff") {
|
|
799
|
+
const facts = codeReviewFacts(cwd, fixedPoint, { allowEmpty: true, requireClean: false });
|
|
800
|
+
if (!facts.ok) return facts;
|
|
801
|
+
if (!facts.diff_empty) return fail("REVIEW_FALLBACK_REASON_MISMATCH", "The fixed point has a non-empty HEAD diff; use code-review instead of fallback.");
|
|
802
|
+
const worktree = reviewWorktreeFacts(cwd);
|
|
803
|
+
if (!worktree.ok) return worktree;
|
|
804
|
+
if (worktree.dirty) return fail("REVIEW_FALLBACK_REASON_MISMATCH", "The candidate worktree is not clean; use the uncommitted-worktree fallback so Matrix can bind its content.");
|
|
805
|
+
sourceFacts = { reason, fixed_point: facts.fixed_point, head: facts.head, merge_base: facts.merge_base, diff_hash: facts.diff_hash };
|
|
806
|
+
} else if (reason === "uncommitted-worktree") {
|
|
807
|
+
const facts = codeReviewFacts(cwd, fixedPoint, { allowEmpty: true, requireClean: false });
|
|
808
|
+
if (!facts.ok) return facts;
|
|
809
|
+
const worktree = reviewWorktreeFacts(cwd);
|
|
810
|
+
if (!worktree.ok) return worktree;
|
|
811
|
+
if (!worktree.dirty) return fail("REVIEW_FALLBACK_REASON_MISMATCH", "The candidate worktree is clean; use code-review instead of fallback.");
|
|
812
|
+
sourceFacts = { reason, fixed_point: facts.fixed_point, head: facts.head, merge_base: facts.merge_base, diff_hash: facts.diff_hash };
|
|
813
|
+
} else {
|
|
814
|
+
if (fixedPoint) return fail("REVIEW_SOURCE_INVALID", "A capability execution failure does not claim a successful code-review fixed point.");
|
|
815
|
+
sourceFacts = { reason };
|
|
816
|
+
}
|
|
614
817
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
if (
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
:
|
|
624
|
-
|
|
818
|
+
let workspace;
|
|
819
|
+
try { workspace = captureWorkspaceBaseline(cwd); }
|
|
820
|
+
catch (error) { return fail("WORKSPACE_INSPECTION_FAILED", `Could not bind the reviewed workspace: ${error.message}`); }
|
|
821
|
+
if (sourceFacts.reason === "uncommitted-worktree") sourceFacts.candidate_hash = reviewCandidateHash(sourceFacts.diff_hash, workspace.hash);
|
|
822
|
+
const receipt = {
|
|
823
|
+
schema: REVIEW_RECEIPT_SCHEMA,
|
|
824
|
+
change_id: id,
|
|
825
|
+
revision: flow.revision,
|
|
826
|
+
orchestration: flow.orchestration,
|
|
827
|
+
source,
|
|
828
|
+
standards,
|
|
829
|
+
specification,
|
|
830
|
+
contract_hash: flow.approved_contract_hash,
|
|
831
|
+
evidence_hash: evidence.hash,
|
|
832
|
+
workspace: { provider: workspace.provider, hash: workspace.hash, file_count: workspace.file_count, content_bytes: workspace.content_bytes },
|
|
833
|
+
...sourceFacts,
|
|
834
|
+
recorded_at: stamp()
|
|
835
|
+
};
|
|
836
|
+
return runWorkflowTransaction({
|
|
837
|
+
cwd,
|
|
838
|
+
kind: "review",
|
|
839
|
+
changeId: id,
|
|
840
|
+
failAfterOperation,
|
|
841
|
+
operations: [
|
|
842
|
+
workflowWriteOperation(cwd, p.reviewReceipt, `${JSON.stringify(receipt, null, 2)}\n`),
|
|
843
|
+
workflowWriteOperation(cwd, path.join(p.change, "events.jsonl"), eventContent(path.join(p.change, "events.jsonl"), "review-recorded", "verify", "verify", flow.revision, { source, standards, specification, ...sourceFacts }))
|
|
844
|
+
],
|
|
845
|
+
result: { ok: true, code: "OK", change_id: id, revision: flow.revision, source, standards, specification, receipt: p.reviewReceipt, ...sourceFacts }
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
if (command === "transition") {
|
|
849
|
+
const to = args[0];
|
|
850
|
+
const confirmed = args.includes("--confirmed");
|
|
851
|
+
if (args.slice(1).some((arg) => arg !== "--confirmed")) return fail("INVALID_INTENT", "Transition accepts only a target phase and optional --confirmed.");
|
|
852
|
+
if (flow.status !== "active") return fail("CHANGE_NOT_ACTIVE", `Matrix change is ${flow.status}.`);
|
|
853
|
+
const profile = workflowProfile(p, flow);
|
|
854
|
+
if (profile.next[flow.phase] !== to) return fail("ILLEGAL_TRANSITION", `Illegal ${profile.id} transition ${flow.phase} -> ${to}.`);
|
|
855
|
+
const result = guard(p, flow, flow.phase); if (!result.ok) return { ...result, message: result.message ?? "Current phase guard failed." };
|
|
856
|
+
if (profile.id === "lightweight" && flow.phase === "open" && to === "build") {
|
|
857
|
+
if (!confirmed) return fail("CONFIRMATION_REQUIRED", "Lightweight Open requires explicit user confirmation before Build.");
|
|
858
|
+
const order = workspaceOrderGuard(cwd, p);
|
|
859
|
+
if (!order.ok) return order;
|
|
860
|
+
}
|
|
861
|
+
const before = flow.phase;
|
|
862
|
+
const approval = to === "build" ? contractState(p, flow) : null;
|
|
863
|
+
if (approval && approval.status !== "approved-and-matching" && approval.status !== "unapproved") return { ...contractFailure(flow, approval), change_id: id };
|
|
864
|
+
if (to === "build" && approval.snapshot.missing) return { ...fail("GUARD_FAILED", "Workflow contract artifacts are incomplete."), change_id: id };
|
|
865
|
+
const next = { ...flow, schema: "matrix/change/v2", phase: to, revision: flow.revision + 1, updated_at: stamp(), ...(to === "build" ? { approved_contract_hash: approval.snapshot.hash, contract_approved_revision: flow.revision + 1 } : {}) };
|
|
866
|
+
const nextProfile = workflowProfile(p, next);
|
|
867
|
+
const details = to === "build"
|
|
868
|
+
? { approved_contract_hash: next.approved_contract_hash, contract_approved_revision: next.contract_approved_revision, ...(shortcut(flow) ? { confirmed, profile: nextProfile.id } : {}) }
|
|
869
|
+
: shortcut(flow) ? { profile: nextProfile.id } : {};
|
|
870
|
+
const resultFields = { ok: true, code: "OK", change_id: id, workflow: next.workflow, profile: nextProfile.id, evidence_policy: evidencePolicy(next, nextProfile), orchestration: next.orchestration, artifact_language: next.artifact_language ?? artifactLanguage, status: next.status, phase: to, revision: next.revision, next_skill: skill(to), ...contractFields(next, contractState(p, next)) };
|
|
625
871
|
return runWorkflowTransaction({
|
|
626
872
|
cwd,
|
|
627
873
|
kind: "transition",
|
|
@@ -634,48 +880,55 @@ export function invoke(argv, { cwd = process.cwd(), failAfterOperation = null }
|
|
|
634
880
|
result: resultFields
|
|
635
881
|
});
|
|
636
882
|
}
|
|
637
|
-
if (command === "return") {
|
|
638
|
-
const to = args[0];
|
|
639
|
-
const reason = option(args, "--reason");
|
|
640
|
-
if (!reason) return fail("RETURN_REASON_REQUIRED", "Return requires --reason.");
|
|
641
|
-
if (flow.status !== "active") return fail("CHANGE_NOT_ACTIVE", `Matrix change is ${flow.status}.`);
|
|
642
|
-
if (!RETURN_REASONS.has(reason)) return fail("RETURN_REASON_UNSUPPORTED", `Unsupported Return reason: ${reason}.`);
|
|
643
|
-
const profile = workflowProfile(p, flow);
|
|
644
|
-
const returns = profile.id === "lightweight" ? LIGHTWEIGHT_RETURNS : FULL_RETURNS;
|
|
645
|
-
const returnRule = returns.find((entry) => entry.from === flow.phase && entry.to === to && entry.reason === reason);
|
|
646
|
-
if (!returnRule) return fail("ILLEGAL_RETURN", `Illegal ${profile.id} Return ${flow.phase} -> ${to} for reason ${reason}.`);
|
|
883
|
+
if (command === "return") {
|
|
884
|
+
const to = args[0];
|
|
885
|
+
const reason = option(args, "--reason");
|
|
886
|
+
if (!reason) return fail("RETURN_REASON_REQUIRED", "Return requires --reason.");
|
|
887
|
+
if (flow.status !== "active") return fail("CHANGE_NOT_ACTIVE", `Matrix change is ${flow.status}.`);
|
|
888
|
+
if (!RETURN_REASONS.has(reason)) return fail("RETURN_REASON_UNSUPPORTED", `Unsupported Return reason: ${reason}.`);
|
|
889
|
+
const profile = workflowProfile(p, flow);
|
|
890
|
+
const returns = profile.id === "lightweight" ? LIGHTWEIGHT_RETURNS : FULL_RETURNS;
|
|
891
|
+
const returnRule = returns.find((entry) => entry.from === flow.phase && entry.to === to && entry.reason === reason);
|
|
892
|
+
if (!returnRule) return fail("ILLEGAL_RETURN", `Illegal ${profile.id} Return ${flow.phase} -> ${to} for reason ${reason}.`);
|
|
647
893
|
if (flow.phase === "verify" && to === "build") { const contract = contractState(p, flow); const failure = contractFailure(flow, contract); if (failure) return failure; }
|
|
648
|
-
const verification = path.join(p.artifacts, "verification.md");
|
|
649
|
-
let
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
894
|
+
const verification = path.join(p.artifacts, "verification.md");
|
|
895
|
+
let verificationDestination = null;
|
|
896
|
+
let receiptDestination = null;
|
|
897
|
+
if (fs.existsSync(verification)) {
|
|
898
|
+
const history = path.join(p.artifacts, "evidence-history");
|
|
899
|
+
verificationDestination = path.join(history, `revision-${flow.revision}-verification.md`);
|
|
900
|
+
if (fs.existsSync(verificationDestination)) return fail("EVIDENCE_INVALIDATION_FAILED", `Evidence history already exists for revision ${flow.revision}.`);
|
|
901
|
+
}
|
|
902
|
+
if (fs.existsSync(p.reviewReceipt)) {
|
|
903
|
+
const history = path.join(p.artifacts, "evidence-history");
|
|
904
|
+
receiptDestination = path.join(history, `revision-${flow.revision}-review-receipt.json`);
|
|
905
|
+
if (fs.existsSync(receiptDestination)) return fail("EVIDENCE_INVALIDATION_FAILED", `Review receipt history already exists for revision ${flow.revision}.`);
|
|
906
|
+
}
|
|
907
|
+
const next = {
|
|
908
|
+
...flow,
|
|
909
|
+
schema: "matrix/change/v2",
|
|
910
|
+
workflow: returnRule.upgrade ? "full" : flow.workflow,
|
|
911
|
+
workflow_profile: returnRule.upgrade ? null : flow.workflow_profile,
|
|
912
|
+
phase: to,
|
|
913
|
+
revision: flow.revision + 1,
|
|
914
|
+
updated_at: stamp(),
|
|
915
|
+
...(to === "design" ? { approved_contract_hash: null, contract_approved_revision: null } : {})
|
|
916
|
+
};
|
|
917
|
+
const nextProfile = workflowProfile(p, next);
|
|
666
918
|
return runWorkflowTransaction({
|
|
667
919
|
cwd,
|
|
668
920
|
kind: "return",
|
|
669
921
|
changeId: id,
|
|
670
|
-
failAfterOperation,
|
|
671
|
-
operations: [
|
|
672
|
-
...(
|
|
673
|
-
|
|
674
|
-
workflowWriteOperation(cwd,
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
922
|
+
failAfterOperation,
|
|
923
|
+
operations: [
|
|
924
|
+
...(verificationDestination ? [workflowMoveOperation(cwd, verification, verificationDestination)] : []),
|
|
925
|
+
...(receiptDestination ? [workflowMoveOperation(cwd, p.reviewReceipt, receiptDestination)] : []),
|
|
926
|
+
workflowWriteOperation(cwd, p.flow, flowContent(next)),
|
|
927
|
+
workflowWriteOperation(cwd, path.join(p.change, "events.jsonl"), eventContent(path.join(p.change, "events.jsonl"), "returned", flow.phase, to, next.revision, { reason, invalidated_evidence: INVALIDATED_EVIDENCE, ...(returnRule.upgrade ? { workflow_from: flow.workflow, workflow_to: "full" } : {}) }))
|
|
928
|
+
],
|
|
929
|
+
result: { ok: true, code: "OK", change_id: id, workflow: next.workflow, profile: nextProfile.id, evidence_policy: evidencePolicy(next, nextProfile), orchestration: next.orchestration, artifact_language: next.artifact_language ?? artifactLanguage, status: next.status, phase: to, revision: next.revision, next_skill: skill(to), event: "returned", invalidated_evidence: INVALIDATED_EVIDENCE, ...contractFields(next, contractState(p, next)) }
|
|
930
|
+
});
|
|
931
|
+
}
|
|
679
932
|
if (command === "abort") {
|
|
680
933
|
const reason = option(args, "--reason");
|
|
681
934
|
const note = option(args, "--note");
|
|
@@ -699,36 +952,36 @@ export function invoke(argv, { cwd = process.cwd(), failAfterOperation = null }
|
|
|
699
952
|
workflowWriteOperation(cwd, targetEvents, eventContent(sourceEvents, "aborted", flow.phase, flow.phase, next.revision, { reason, ...(note ? { note } : {}) }), { before: captureWorkflowEntry(sourceEvents) }),
|
|
700
953
|
workflowRemoveOperation(cwd, p.active)
|
|
701
954
|
],
|
|
702
|
-
result: { ok: true, code: "OK", change_id: id, workflow: next.workflow, orchestration: next.orchestration, artifact_language: next.artifact_language ?? artifactLanguage, status: next.status, phase: next.phase, revision: next.revision, next_skill: null, event: "aborted" }
|
|
955
|
+
result: { ok: true, code: "OK", change_id: id, workflow: next.workflow, orchestration: next.orchestration, artifact_language: next.artifact_language ?? artifactLanguage, status: next.status, phase: next.phase, revision: next.revision, next_skill: null, event: "aborted" }
|
|
703
956
|
});
|
|
704
957
|
}
|
|
705
|
-
if (command === "archive") {
|
|
706
|
-
if (flow.status !== "active") return fail("CHANGE_NOT_ACTIVE", `Matrix change is ${flow.status}.`);
|
|
707
|
-
if (flow.phase !== "archive") return fail("ILLEGAL_TRANSITION", "Only an archive-phase change can be archived.");
|
|
708
|
-
const profile = workflowProfile(p, flow);
|
|
709
|
-
const dryRun = args.includes("--dry-run");
|
|
710
|
-
const expected = option(args, "--expect-preflight");
|
|
711
|
-
const confirmedByUser = args.includes("--confirmed");
|
|
712
|
-
if ((dryRun && (expected || confirmedByUser)) || args.some((arg) => !["--dry-run", "--expect-preflight", "--confirmed", expected].includes(arg))) {
|
|
713
|
-
return { ...fail("ARCHIVE_PREFLIGHT_INVALID", "Archive accepts either --dry-run or --expect-preflight <sha256>, not both or additional arguments."), recovery_command: "
|
|
714
|
-
}
|
|
715
|
-
if (!dryRun && !expected) return { ...fail("ARCHIVE_PREFLIGHT_REQUIRED", "Archive requires an explicit dry-run followed by --expect-preflight <sha256>."), recovery_command: "
|
|
716
|
-
if (expected && !/^sha256:[0-9a-f]{64}$/.test(expected)) return { ...fail("ARCHIVE_PREFLIGHT_INVALID", "Expected preflight must be sha256 followed by 64 lowercase hexadecimal characters."), recovery_command: "
|
|
717
|
-
if (expected && shortcut(flow) && !confirmedByUser) return { ...fail("CONFIRMATION_REQUIRED", "Shortcut workflow Archive requires explicit user confirmation."), recovery_command: `
|
|
718
|
-
if (dryRun) {
|
|
958
|
+
if (command === "archive") {
|
|
959
|
+
if (flow.status !== "active") return fail("CHANGE_NOT_ACTIVE", `Matrix change is ${flow.status}.`);
|
|
960
|
+
if (flow.phase !== "archive") return fail("ILLEGAL_TRANSITION", "Only an archive-phase change can be archived.");
|
|
961
|
+
const profile = workflowProfile(p, flow);
|
|
962
|
+
const dryRun = args.includes("--dry-run");
|
|
963
|
+
const expected = option(args, "--expect-preflight");
|
|
964
|
+
const confirmedByUser = args.includes("--confirmed");
|
|
965
|
+
if ((dryRun && (expected || confirmedByUser)) || args.some((arg) => !["--dry-run", "--expect-preflight", "--confirmed", expected].includes(arg))) {
|
|
966
|
+
return { ...fail("ARCHIVE_PREFLIGHT_INVALID", "Archive accepts either --dry-run or --expect-preflight <sha256>, not both or additional arguments."), recovery_command: workflowCommand("archive --dry-run") };
|
|
967
|
+
}
|
|
968
|
+
if (!dryRun && !expected) return { ...fail("ARCHIVE_PREFLIGHT_REQUIRED", "Archive requires an explicit dry-run followed by --expect-preflight <sha256>."), recovery_command: workflowCommand("archive --dry-run") };
|
|
969
|
+
if (expected && !/^sha256:[0-9a-f]{64}$/.test(expected)) return { ...fail("ARCHIVE_PREFLIGHT_INVALID", "Expected preflight must be sha256 followed by 64 lowercase hexadecimal characters."), recovery_command: workflowCommand("archive --dry-run") };
|
|
970
|
+
if (expected && shortcut(flow) && !confirmedByUser) return { ...fail("CONFIRMATION_REQUIRED", "Shortcut workflow Archive requires explicit user confirmation."), recovery_command: workflowCommand(`archive --expect-preflight ${expected} --confirmed`) };
|
|
971
|
+
if (dryRun) {
|
|
719
972
|
const result = guard(p, flow, "archive"); if (!result.ok) return { ...result, message: result.message ?? "Archive guard failed." };
|
|
720
973
|
const captured = captureArchivePreflight(p, id); if (captured.failure) return captured.failure;
|
|
721
974
|
const currentFlow = readFlow(p); if (!currentFlow) return fail("ARCHIVE_PREFLIGHT_UNSTABLE", "Canonical workflow state changed during dry-run.");
|
|
722
975
|
const finalGuard = guard(p, currentFlow, "archive"); if (!finalGuard.ok) return { ...finalGuard, message: finalGuard.message ?? "Archive guard failed." };
|
|
723
976
|
const confirmed = captureArchivePreflight(p, id); if (confirmed.failure) return confirmed.failure;
|
|
724
977
|
if (captured.hash !== confirmed.hash) return fail("ARCHIVE_PREFLIGHT_UNSTABLE", "Archive inputs changed during dry-run. Retry dry-run.");
|
|
725
|
-
return {
|
|
726
|
-
ok: true, code: "OK", change_id: id, workflow: currentFlow.workflow, profile: profile.id, evidence_policy: evidencePolicy(currentFlow, profile), orchestration: currentFlow.orchestration, artifact_language: currentFlow.artifact_language ?? artifactLanguage, status: currentFlow.status, phase: currentFlow.phase, revision: currentFlow.revision,
|
|
727
|
-
preflight_hash: confirmed.hash,
|
|
728
|
-
effect_summary: { target: `.matrix/archive/${id}`, files: confirmed.fileCount, directories: confirmed.directoryCount, content_bytes: confirmed.totalBytes },
|
|
729
|
-
commit_command: `
|
|
730
|
-
...contractFields(currentFlow, contractState(p, currentFlow))
|
|
731
|
-
};
|
|
978
|
+
return {
|
|
979
|
+
ok: true, code: "OK", change_id: id, workflow: currentFlow.workflow, profile: profile.id, evidence_policy: evidencePolicy(currentFlow, profile), orchestration: currentFlow.orchestration, artifact_language: currentFlow.artifact_language ?? artifactLanguage, status: currentFlow.status, phase: currentFlow.phase, revision: currentFlow.revision,
|
|
980
|
+
preflight_hash: confirmed.hash,
|
|
981
|
+
effect_summary: { target: `.matrix/archive/${id}`, files: confirmed.fileCount, directories: confirmed.directoryCount, content_bytes: confirmed.totalBytes },
|
|
982
|
+
commit_command: workflowCommand(`archive --expect-preflight ${confirmed.hash}${shortcut(currentFlow) ? " --confirmed" : ""}`),
|
|
983
|
+
...contractFields(currentFlow, contractState(p, currentFlow))
|
|
984
|
+
};
|
|
732
985
|
}
|
|
733
986
|
const captured = captureArchivePreflight(p, id);
|
|
734
987
|
if (captured.failure) return captured.failure;
|
|
@@ -754,11 +1007,11 @@ export function invoke(argv, { cwd = process.cwd(), failAfterOperation = null }
|
|
|
754
1007
|
operations: [
|
|
755
1008
|
workflowMoveOperation(cwd, p.change, target),
|
|
756
1009
|
workflowWriteOperation(cwd, targetFlow, flowContent(next), { before: captureWorkflowEntry(p.flow) }),
|
|
757
|
-
workflowWriteOperation(cwd, targetEvents, eventContent(sourceEvents, "archived", "archive", "archive", next.revision, { preflight_hash: expected, confirmed: confirmedByUser }), { before: captureWorkflowEntry(sourceEvents) }),
|
|
1010
|
+
workflowWriteOperation(cwd, targetEvents, eventContent(sourceEvents, "archived", "archive", "archive", next.revision, { preflight_hash: expected, confirmed: confirmedByUser }), { before: captureWorkflowEntry(sourceEvents) }),
|
|
758
1011
|
workflowRemoveOperation(cwd, p.active)
|
|
759
1012
|
],
|
|
760
1013
|
result: {
|
|
761
|
-
ok: true, code: "OK", archived: id, change_id: id, workflow: next.workflow, profile: profile.id, evidence_policy: evidencePolicy(next, profile), orchestration: next.orchestration, artifact_language: next.artifact_language ?? artifactLanguage, status: next.status, phase: next.phase, revision: next.revision, next_skill: null, event: "archived",
|
|
1014
|
+
ok: true, code: "OK", archived: id, change_id: id, workflow: next.workflow, profile: profile.id, evidence_policy: evidencePolicy(next, profile), orchestration: next.orchestration, artifact_language: next.artifact_language ?? artifactLanguage, status: next.status, phase: next.phase, revision: next.revision, next_skill: null, event: "archived",
|
|
762
1015
|
preflight_hash: expected,
|
|
763
1016
|
effect_summary: { target: `.matrix/archive/${id}`, files: confirmed.fileCount, directories: confirmed.directoryCount, content_bytes: confirmed.totalBytes },
|
|
764
1017
|
...contractFields(next, contract)
|
|
@@ -766,10 +1019,10 @@ export function invoke(argv, { cwd = process.cwd(), failAfterOperation = null }
|
|
|
766
1019
|
});
|
|
767
1020
|
}
|
|
768
1021
|
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(p, flow, flow.phase).ok}\n`); return { ok: true, code: "OK", path: destination }; }
|
|
769
|
-
if (command === "export") {
|
|
770
|
-
if (flow.phase !== "build") return fail("ILLEGAL_PHASE", "Export requires the build phase.");
|
|
771
|
-
if (workflowProfile(p, flow).id === "lightweight") return fail("EXPORT_UNSUPPORTED_WORKFLOW", "Claude export requires the full three-artifact workflow contract.");
|
|
772
|
-
const contract = contractState(p, flow); const failure = contractFailure(flow, contract); if (failure) return failure;
|
|
1022
|
+
if (command === "export") {
|
|
1023
|
+
if (flow.phase !== "build") return fail("ILLEGAL_PHASE", "Export requires the build phase.");
|
|
1024
|
+
if (workflowProfile(p, flow).id === "lightweight") return fail("EXPORT_UNSUPPORTED_WORKFLOW", "Claude export requires the full three-artifact workflow contract.");
|
|
1025
|
+
const contract = contractState(p, flow); const failure = contractFailure(flow, contract); if (failure) return failure;
|
|
773
1026
|
const taskId = args.includes("--task-id") ? args[args.indexOf("--task-id") + 1] : args[0];
|
|
774
1027
|
const target = args.includes("--target") ? args[args.indexOf("--target") + 1] : "generic";
|
|
775
1028
|
const applyBoard = args.includes("--apply-fnsec-board");
|