macca-method 2.1.1 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.agents/macca-lock.json +1 -1
  2. package/.agents/skills/_shared/references/brainstorm-session.md +5 -5
  3. package/.agents/skills/_shared/references/invocation-policy.md +20 -20
  4. package/.agents/skills/_shared/references/output-ownership.md +11 -11
  5. package/.agents/skills/_shared/references/scope-rules.md +1 -1
  6. package/.agents/skills/_shared/references/skill-catalog.md +20 -20
  7. package/.agents/skills/_shared/scripts/validate-skills.py +37 -15
  8. package/.agents/skills/add-feature/SKILL.md +9 -3
  9. package/.agents/skills/antislop-copywriting/SKILL.md +372 -0
  10. package/.agents/skills/brainstorm-api/SKILL.md +28 -16
  11. package/.agents/skills/brainstorm-api/assets/api.template.md +35 -15
  12. package/.agents/skills/brainstorm-architecture/SKILL.md +35 -15
  13. package/.agents/skills/brainstorm-architecture/assets/architecture.template.md +44 -25
  14. package/.agents/skills/brainstorm-prd/SKILL.md +47 -17
  15. package/.agents/skills/brainstorm-prd/assets/PRD.template.md +47 -23
  16. package/.agents/skills/brainstorm-rules/SKILL.md +36 -19
  17. package/.agents/skills/brainstorm-rules/assets/rules.template.md +32 -18
  18. package/.agents/skills/brainstorm-schema/SKILL.md +18 -8
  19. package/.agents/skills/brainstorm-schema/assets/schema.template.md +25 -10
  20. package/.agents/skills/brainstorm-styleguide/SKILL.md +37 -19
  21. package/.agents/skills/brainstorm-styleguide/assets/StyleGuide.template.md +78 -60
  22. package/.agents/skills/brainstorm-task/SKILL.md +27 -14
  23. package/.agents/skills/brainstorm-task/assets/Task.template.md +29 -18
  24. package/.agents/skills/bug-fix/SKILL.md +25 -1
  25. package/.agents/skills/code-review/SKILL.md +7 -7
  26. package/.agents/skills/code-review/references/review-checklist.md +21 -10
  27. package/.agents/skills/developer/SKILL.md +8 -0
  28. package/.agents/skills/developer/references/execute-task.md +13 -7
  29. package/.agents/skills/help/SKILL.md +32 -20
  30. package/.agents/skills/meet/SKILL.md +9 -4
  31. package/.agents/skills/quick-dev/SKILL.md +27 -22
  32. package/.agents/skills/release-readiness/SKILL.md +17 -13
  33. package/.agents/skills/skill-creator/LICENSE.txt +202 -0
  34. package/.agents/skills/skill-creator/SKILL.md +485 -0
  35. package/.agents/skills/skill-creator/agents/analyzer.md +274 -0
  36. package/.agents/skills/skill-creator/agents/comparator.md +202 -0
  37. package/.agents/skills/skill-creator/agents/grader.md +223 -0
  38. package/.agents/skills/skill-creator/assets/eval_review.html +146 -0
  39. package/.agents/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  40. package/.agents/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  41. package/.agents/skills/skill-creator/references/schemas.md +441 -0
  42. package/.agents/skills/skill-creator/scripts/__init__.py +0 -0
  43. package/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  44. package/.agents/skills/skill-creator/scripts/generate_report.py +326 -0
  45. package/.agents/skills/skill-creator/scripts/improve_description.py +247 -0
  46. package/.agents/skills/skill-creator/scripts/package_skill.py +136 -0
  47. package/.agents/skills/skill-creator/scripts/quick_validate.py +103 -0
  48. package/.agents/skills/skill-creator/scripts/run_eval.py +310 -0
  49. package/.agents/skills/skill-creator/scripts/run_loop.py +328 -0
  50. package/.agents/skills/skill-creator/scripts/utils.py +47 -0
  51. package/.agents/skills/spec-audit/SKILL.md +28 -1
  52. package/.agents/skills/spec-compliance/SKILL.md +31 -18
  53. package/.agents/skills/spec-init/SKILL.md +29 -17
  54. package/README.md +158 -122
  55. package/bin/macca-method.js +1378 -1077
  56. package/package.json +40 -40
  57. package/scripts/run-skill-validator.js +27 -9
  58. package/scripts/test-install.js +599 -357
  59. package/scripts/test-upgrade-legacy.js +119 -100
  60. package/scripts/validate-skill-behavior.js +175 -64
@@ -10,7 +10,9 @@ const { execFileSync } = require("node:child_process");
10
10
 
11
11
  const rootDir = path.resolve(__dirname, "..");
12
12
  const mode = process.argv.includes("--published") ? "published" : "local";
13
- const tmpDir = fs.mkdtempSync(path.join(resolveTempRoot(), "macca-test-install-"));
13
+ const tmpDir = fs.mkdtempSync(
14
+ path.join(resolveTempRoot(), "macca-test-install-"),
15
+ );
14
16
  const projectDir = path.join(tmpDir, "project");
15
17
  const collisionDir = path.join(tmpDir, "collision-project");
16
18
  const symlinkDir = path.join(tmpDir, "symlink-project");
@@ -18,413 +20,653 @@ const driftDir = path.join(tmpDir, "drift-project");
18
20
  let packageSpec = "macca-method";
19
21
 
20
22
  function commandName(base) {
21
- if (process.platform !== "win32") {
22
- return base;
23
- }
23
+ if (process.platform !== "win32") {
24
+ return base;
25
+ }
24
26
 
25
- return base === "npm" || base === "npx" ? `${base}.cmd` : base;
27
+ return base === "npm" || base === "npx" ? `${base}.cmd` : base;
26
28
  }
27
29
 
28
30
  function resolveTempRoot() {
29
- const tempRoot = os.tmpdir();
30
- try {
31
- return fs.realpathSync(tempRoot);
32
- } catch {
33
- return tempRoot;
34
- }
31
+ const tempRoot = os.tmpdir();
32
+ try {
33
+ return fs.realpathSync(tempRoot);
34
+ } catch {
35
+ return tempRoot;
36
+ }
35
37
  }
36
38
 
37
39
  function runNpm(args, options = {}) {
38
- const npmExecPath = process.env.npm_execpath;
39
- if (npmExecPath && /\.c?js$/i.test(npmExecPath)) {
40
- return capture(process.execPath, [npmExecPath, ...args], options);
41
- }
40
+ const npmExecPath = process.env.npm_execpath;
41
+ if (npmExecPath && /\.c?js$/i.test(npmExecPath)) {
42
+ return capture(process.execPath, [npmExecPath, ...args], options);
43
+ }
42
44
 
43
- return capture(commandName("npm"), args, options);
45
+ return capture(commandName("npm"), args, options);
44
46
  }
45
47
 
46
48
  function runNpmWithInheritedStdio(args, options = {}) {
47
- const npmExecPath = process.env.npm_execpath;
48
- if (npmExecPath && /\.c?js$/i.test(npmExecPath)) {
49
- run(process.execPath, [npmExecPath, ...args], options);
50
- return;
51
- }
49
+ const npmExecPath = process.env.npm_execpath;
50
+ if (npmExecPath && /\.c?js$/i.test(npmExecPath)) {
51
+ run(process.execPath, [npmExecPath, ...args], options);
52
+ return;
53
+ }
52
54
 
53
- run(commandName("npm"), args, options);
55
+ run(commandName("npm"), args, options);
54
56
  }
55
57
 
56
58
  function run(command, args, options = {}) {
57
- execFileSync(command, args, {
58
- cwd: rootDir,
59
- stdio: "inherit",
60
- ...options
61
- });
59
+ execFileSync(command, args, {
60
+ cwd: rootDir,
61
+ stdio: "inherit",
62
+ ...options,
63
+ });
62
64
  }
63
65
 
64
66
  function capture(command, args, options = {}) {
65
- return execFileSync(command, args, {
66
- cwd: rootDir,
67
- encoding: "utf8",
68
- ...options
69
- });
67
+ return execFileSync(command, args, {
68
+ cwd: rootDir,
69
+ encoding: "utf8",
70
+ ...options,
71
+ });
70
72
  }
71
73
 
72
74
  function runCli(args) {
73
- if (mode === "published") {
74
- runNpmWithInheritedStdio(["exec", "--yes", "--package", "macca-method", "--", "macca-method", ...args]);
75
- return;
76
- }
77
-
78
- runNpmWithInheritedStdio(["exec", "--yes", "--package", packageSpec, "--", "macca-method", ...args]);
75
+ if (mode === "published") {
76
+ runNpmWithInheritedStdio([
77
+ "exec",
78
+ "--yes",
79
+ "--package",
80
+ "macca-method",
81
+ "--",
82
+ "macca-method",
83
+ ...args,
84
+ ]);
85
+ return;
86
+ }
87
+
88
+ runNpmWithInheritedStdio([
89
+ "exec",
90
+ "--yes",
91
+ "--package",
92
+ packageSpec,
93
+ "--",
94
+ "macca-method",
95
+ ...args,
96
+ ]);
79
97
  }
80
98
 
81
99
  function expectCliFailure(args, expectedText) {
82
- try {
83
- if (mode === "published") {
84
- runNpm(["exec", "--yes", "--package", "macca-method", "--", "macca-method", ...args], { cwd: rootDir, stdio: "pipe" });
85
- } else {
86
- runNpm(["exec", "--yes", "--package", packageSpec, "--", "macca-method", ...args], { cwd: rootDir, stdio: "pipe" });
87
- }
88
- } catch (error) {
89
- const output = `${error.stdout || ""}${error.stderr || ""}${error.message || ""}`;
90
- if (!output.includes(expectedText)) {
91
- throw new Error(`Expected failed command to include ${expectedText}, got: ${output}`);
92
- }
93
- return;
100
+ try {
101
+ if (mode === "published") {
102
+ runNpm(
103
+ [
104
+ "exec",
105
+ "--yes",
106
+ "--package",
107
+ "macca-method",
108
+ "--",
109
+ "macca-method",
110
+ ...args,
111
+ ],
112
+ { cwd: rootDir, stdio: "pipe" },
113
+ );
114
+ } else {
115
+ runNpm(
116
+ [
117
+ "exec",
118
+ "--yes",
119
+ "--package",
120
+ packageSpec,
121
+ "--",
122
+ "macca-method",
123
+ ...args,
124
+ ],
125
+ { cwd: rootDir, stdio: "pipe" },
126
+ );
94
127
  }
95
- throw new Error(`Expected command to fail: ${args.join(" ")}`);
128
+ } catch (error) {
129
+ const output = `${error.stdout || ""}${error.stderr || ""}${error.message || ""}`;
130
+ if (!output.includes(expectedText)) {
131
+ throw new Error(
132
+ `Expected failed command to include ${expectedText}, got: ${output}`,
133
+ );
134
+ }
135
+ return;
136
+ }
137
+ throw new Error(`Expected command to fail: ${args.join(" ")}`);
96
138
  }
97
139
 
98
140
  function assertPathExists(targetPath) {
99
- if (!fs.existsSync(targetPath)) {
100
- throw new Error(`Missing expected path: ${targetPath}`);
101
- }
141
+ if (!fs.existsSync(targetPath)) {
142
+ throw new Error(`Missing expected path: ${targetPath}`);
143
+ }
102
144
  }
103
145
 
104
146
  function readNonEmptyLines(filePath) {
105
- return fs
106
- .readFileSync(filePath, "utf8")
107
- .split(/\r?\n/)
108
- .map((line) => line.trim())
109
- .filter(Boolean);
147
+ return fs
148
+ .readFileSync(filePath, "utf8")
149
+ .split(/\r?\n/)
150
+ .map((line) => line.trim())
151
+ .filter(Boolean);
110
152
  }
111
153
 
112
154
  function listFiles(directoryPath) {
113
- const files = [];
114
-
115
- function walk(currentPath) {
116
- const entries = fs.readdirSync(currentPath, { withFileTypes: true });
117
- for (const entry of entries) {
118
- const entryPath = path.join(currentPath, entry.name);
119
- if (entry.isDirectory()) {
120
- walk(entryPath);
121
- continue;
122
- }
123
-
124
- files.push(entryPath);
125
- }
155
+ const files = [];
156
+
157
+ function walk(currentPath) {
158
+ const entries = fs.readdirSync(currentPath, { withFileTypes: true });
159
+ for (const entry of entries) {
160
+ const entryPath = path.join(currentPath, entry.name);
161
+ if (entry.isDirectory()) {
162
+ walk(entryPath);
163
+ continue;
164
+ }
165
+
166
+ files.push(entryPath);
126
167
  }
168
+ }
127
169
 
128
- walk(directoryPath);
129
- return files.sort();
170
+ walk(directoryPath);
171
+ return files.sort();
130
172
  }
131
173
 
132
174
  function ensureExpectedTools(filePath) {
133
- const tools = readNonEmptyLines(filePath);
134
- if (!tools.includes("codex")) {
135
- throw new Error("Expected codex in .agents/macca-tools.txt");
136
- }
137
-
138
- if (!tools.includes("copilot")) {
139
- throw new Error("Expected copilot in .agents/macca-tools.txt");
140
- }
175
+ const tools = readNonEmptyLines(filePath);
176
+ if (!tools.includes("codex")) {
177
+ throw new Error("Expected codex in .agents/macca-tools.txt");
178
+ }
179
+
180
+ if (!tools.includes("copilot")) {
181
+ throw new Error("Expected copilot in .agents/macca-tools.txt");
182
+ }
141
183
  }
142
184
 
143
185
  function resolveLocalPackage() {
144
- const packOutput = runNpm(["pack", "--json", "--pack-destination", tmpDir]);
145
- const packageList = JSON.parse(packOutput);
186
+ const packOutput = runNpm(["pack", "--json", "--pack-destination", tmpDir]);
187
+ const packageList = JSON.parse(packOutput);
188
+
189
+ if (
190
+ !Array.isArray(packageList) ||
191
+ packageList.length === 0 ||
192
+ !packageList[0].filename
193
+ ) {
194
+ throw new Error("npm pack did not return a tarball filename");
195
+ }
196
+
197
+ packageSpec = path.join(tmpDir, packageList[0].filename);
198
+ }
146
199
 
147
- if (!Array.isArray(packageList) || packageList.length === 0 || !packageList[0].filename) {
148
- throw new Error("npm pack did not return a tarball filename");
200
+ function main() {
201
+ process.stdout.write("\n");
202
+ process.stdout.write(" MACCA test-install\n");
203
+ process.stdout.write(` Mode: ${mode}\n`);
204
+
205
+ try {
206
+ if (mode === "local") {
207
+ resolveLocalPackage();
149
208
  }
150
209
 
151
- packageSpec = path.join(tmpDir, packageList[0].filename);
152
- }
210
+ fs.mkdirSync(projectDir, { recursive: true });
211
+ const collisionSkill = path.join(
212
+ collisionDir,
213
+ ".agents",
214
+ "skills",
215
+ "developer",
216
+ "SKILL.md",
217
+ );
218
+ fs.mkdirSync(path.dirname(collisionSkill), { recursive: true });
219
+ fs.writeFileSync(
220
+ collisionSkill,
221
+ "---\nname: developer\ndescription: User-owned collision fixture.\n---\n",
222
+ "utf8",
223
+ );
224
+ expectCliFailure(
225
+ ["install", "--yes", "--tool", "codex", "--directory", collisionDir],
226
+ "Refusing to overwrite or remove unowned skill directory",
227
+ );
228
+ if (
229
+ !fs
230
+ .readFileSync(collisionSkill, "utf8")
231
+ .includes("User-owned collision fixture")
232
+ ) {
233
+ throw new Error("Collision protection modified the user-owned skill");
234
+ }
153
235
 
154
- function main() {
155
- process.stdout.write("\n");
156
- process.stdout.write(" MACCA test-install\n");
157
- process.stdout.write(` Mode: ${mode}\n`);
158
-
159
- try {
160
- if (mode === "local") {
161
- resolveLocalPackage();
162
- }
163
-
164
- fs.mkdirSync(projectDir, { recursive: true });
165
- const collisionSkill = path.join(collisionDir, ".agents", "skills", "developer", "SKILL.md");
166
- fs.mkdirSync(path.dirname(collisionSkill), { recursive: true });
167
- fs.writeFileSync(collisionSkill, "---\nname: developer\ndescription: User-owned collision fixture.\n---\n", "utf8");
236
+ if (process.platform !== "win32") {
237
+ const outsideSkills = path.join(tmpDir, "outside-skills");
238
+ fs.mkdirSync(path.join(symlinkDir, ".agents"), { recursive: true });
239
+ fs.mkdirSync(outsideSkills, { recursive: true });
240
+ fs.symlinkSync(
241
+ outsideSkills,
242
+ path.join(symlinkDir, ".agents", "skills"),
243
+ "dir",
244
+ );
245
+ expectCliFailure(
246
+ ["install", "--yes", "--tool", "codex", "--directory", symlinkDir],
247
+ "Refusing symlinked skill destination component",
248
+ );
249
+ if (fs.readdirSync(outsideSkills).length !== 0) {
250
+ throw new Error(
251
+ "Symlink containment test wrote outside the target project",
252
+ );
253
+ }
254
+
255
+ const outerReal = path.join(tmpDir, "outer-real-project");
256
+ const outerLink = path.join(tmpDir, "outer-link-project");
257
+ fs.mkdirSync(outerReal, { recursive: true });
258
+ fs.symlinkSync(outerReal, outerLink, "dir");
259
+ expectCliFailure(
260
+ ["install", "--yes", "--tool", "codex", "--directory", outerLink],
261
+ "Refusing symlinked target project ancestor",
262
+ );
263
+ if (fs.readdirSync(outerReal).length !== 0) {
264
+ throw new Error(
265
+ "Target directory ancestry symlink test wrote into the real directory",
266
+ );
267
+ }
268
+ } else {
269
+ const outerReal = path.join(tmpDir, "outer-real-project");
270
+ const outerLink = path.join(tmpDir, "outer-link-project");
271
+ fs.mkdirSync(outerReal, { recursive: true });
272
+ try {
273
+ run(commandName("cmd"), ["/c", "mklink", "/J", outerLink, outerReal]);
168
274
  expectCliFailure(
169
- ["install", "--yes", "--tool", "codex", "--directory", collisionDir],
170
- "Refusing to overwrite or remove unowned skill directory"
275
+ ["install", "--yes", "--tool", "codex", "--directory", outerLink],
276
+ "Refusing symlinked target project ancestor",
171
277
  );
172
- if (!fs.readFileSync(collisionSkill, "utf8").includes("User-owned collision fixture")) {
173
- throw new Error("Collision protection modified the user-owned skill");
174
- }
175
-
176
- if (process.platform !== "win32") {
177
- const outsideSkills = path.join(tmpDir, "outside-skills");
178
- fs.mkdirSync(path.join(symlinkDir, ".agents"), { recursive: true });
179
- fs.mkdirSync(outsideSkills, { recursive: true });
180
- fs.symlinkSync(outsideSkills, path.join(symlinkDir, ".agents", "skills"), "dir");
181
- expectCliFailure(
182
- ["install", "--yes", "--tool", "codex", "--directory", symlinkDir],
183
- "Refusing symlinked skill destination component"
184
- );
185
- if (fs.readdirSync(outsideSkills).length !== 0) {
186
- throw new Error("Symlink containment test wrote outside the target project");
187
- }
188
-
189
- const outerReal = path.join(tmpDir, "outer-real-project");
190
- const outerLink = path.join(tmpDir, "outer-link-project");
191
- fs.mkdirSync(outerReal, { recursive: true });
192
- fs.symlinkSync(outerReal, outerLink, "dir");
193
- expectCliFailure(
194
- ["install", "--yes", "--tool", "codex", "--directory", outerLink],
195
- "Refusing symlinked target project ancestor"
196
- );
197
- if (fs.readdirSync(outerReal).length !== 0) {
198
- throw new Error("Target directory ancestry symlink test wrote into the real directory");
199
- }
200
- } else {
201
- const outerReal = path.join(tmpDir, "outer-real-project");
202
- const outerLink = path.join(tmpDir, "outer-link-project");
203
- fs.mkdirSync(outerReal, { recursive: true });
204
- try {
205
- run(commandName("cmd"), ["/c", "mklink", "/J", outerLink, outerReal]);
206
- expectCliFailure(
207
- ["install", "--yes", "--tool", "codex", "--directory", outerLink],
208
- "Refusing symlinked target project ancestor"
209
- );
210
- } catch {
211
- process.stdout.write(" Skipping Windows junction ancestry test (mklink unavailable or not permitted)\n");
212
- }
213
- }
214
-
215
- const customSkill = path.join(projectDir, ".agents", "skills", "custom-skill", "SKILL.md");
216
- fs.mkdirSync(path.dirname(customSkill), { recursive: true });
217
- fs.writeFileSync(customSkill, "---\nname: custom-skill\ndescription: User-owned test skill.\n---\n", "utf8");
218
-
219
- process.stdout.write("\n");
220
- process.stdout.write(` Installing into: ${projectDir}\n`);
221
-
222
- runCli(["install", "--yes", "--tool", "codex", "--tool", "github-copilot", "--directory", projectDir]);
223
-
224
- const configPath = path.join(projectDir, ".agents", "developer-config.json");
225
- const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
226
- config.name = "Preserved Name";
227
- config.project = "Preserved Project";
228
- config.languagePreferences.communication = { raw: "English", normalized: "english" };
229
- config.languagePreferences.documents = { raw: "English", normalized: "english" };
230
- config.customField = { preserved: true };
231
- fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
232
- runCli(["install", "--yes", "--tool", "opencode", "--directory", projectDir]);
233
-
234
- run(commandName("node"), [
235
- path.join(rootDir, "scripts", "run-skill-validator.js"),
236
- path.join(projectDir, ".opencode", "skills", "_shared", "scripts", "validate-skills.py")
237
- ]);
238
-
239
- const managedPath = path.join(projectDir, ".agents", "macca-managed-skills.txt");
240
- const originalManaged = fs.readFileSync(managedPath, "utf8");
241
- fs.writeFileSync(managedPath, `${originalManaged}../../outside\n`, "utf8");
242
- expectCliFailure(["upgrade", "--directory", projectDir], "locally modified MACCA metadata");
243
- fs.writeFileSync(managedPath, originalManaged, "utf8");
244
-
245
- const maliciousSource = path.join(projectDir, "src");
246
- const maliciousTransactionId = "1-1-deadbeef";
247
- const maliciousBackup = path.join(projectDir, `.src.macca-backup-${maliciousTransactionId}`);
248
- const interruptedJournal = path.join(projectDir, ".agents", "macca-transaction.json");
249
- fs.mkdirSync(maliciousSource, { recursive: true });
250
- fs.writeFileSync(path.join(maliciousSource, "sentinel.txt"), "keep", "utf8");
251
- fs.writeFileSync(interruptedJournal, `${JSON.stringify({
252
- version: 1,
253
- phase: "committed",
254
- transactionId: maliciousTransactionId,
255
- entries: [{
256
- targetPath: maliciousSource,
257
- stagingPath: null,
258
- backupPath: maliciousBackup,
259
- kind: "directory",
260
- hadTarget: true
261
- }]
262
- }, null, 2)}\n`, "utf8");
263
- expectCliFailure(["upgrade", "--directory", projectDir], "not a managed MACCA path");
264
- assertPathExists(path.join(maliciousSource, "sentinel.txt"));
265
- fs.rmSync(interruptedJournal, { force: true });
266
-
267
- const forgedTransactionId = "3-3-feedface";
268
- const customSkillDirectory = path.dirname(customSkill);
269
- const forgedBackup = path.join(
270
- path.dirname(customSkillDirectory),
271
- `.${path.basename(customSkillDirectory)}.macca-backup-${forgedTransactionId}`
278
+ } catch {
279
+ process.stdout.write(
280
+ " Skipping Windows junction ancestry test (mklink unavailable or not permitted)\n",
272
281
  );
273
- fs.writeFileSync(interruptedJournal, `${JSON.stringify({
274
- version: 1,
275
- phase: "committing",
276
- transactionId: forgedTransactionId,
277
- entries: [{
278
- targetPath: customSkillDirectory,
279
- stagingPath: null,
280
- backupPath: forgedBackup,
281
- kind: "directory",
282
- hadTarget: false,
283
- stagedHash: null
284
- }]
285
- }, null, 2)}\n`, "utf8");
286
- expectCliFailure(["upgrade", "--directory", projectDir], "matching transaction evidence");
287
- assertPathExists(customSkill);
288
- fs.rmSync(interruptedJournal, { force: true });
289
-
290
- const interruptedConfig = fs.readFileSync(configPath, "utf8");
291
- const interruptedTransactionId = "2-2-cafebabe";
292
- const interruptedBackup = path.join(
293
- projectDir,
294
- ".agents",
295
- `.developer-config.json.macca-backup-${interruptedTransactionId}`
282
+ }
283
+ }
284
+
285
+ const customSkill = path.join(
286
+ projectDir,
287
+ ".agents",
288
+ "skills",
289
+ "custom-skill",
290
+ "SKILL.md",
291
+ );
292
+ fs.mkdirSync(path.dirname(customSkill), { recursive: true });
293
+ fs.writeFileSync(
294
+ customSkill,
295
+ "---\nname: custom-skill\ndescription: User-owned test skill.\n---\n",
296
+ "utf8",
297
+ );
298
+
299
+ process.stdout.write("\n");
300
+ process.stdout.write(` Installing into: ${projectDir}\n`);
301
+
302
+ runCli([
303
+ "install",
304
+ "--yes",
305
+ "--tool",
306
+ "codex",
307
+ "--tool",
308
+ "github-copilot",
309
+ "--directory",
310
+ projectDir,
311
+ ]);
312
+
313
+ const configPath = path.join(
314
+ projectDir,
315
+ ".agents",
316
+ "developer-config.json",
317
+ );
318
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
319
+ config.name = "Preserved Name";
320
+ config.project = "Preserved Project";
321
+ config.languagePreferences.communication = {
322
+ raw: "English",
323
+ normalized: "english",
324
+ };
325
+ config.languagePreferences.documents = {
326
+ raw: "English",
327
+ normalized: "english",
328
+ };
329
+ config.customField = { preserved: true };
330
+ fs.writeFileSync(
331
+ configPath,
332
+ `${JSON.stringify(config, null, 2)}\n`,
333
+ "utf8",
334
+ );
335
+ runCli([
336
+ "install",
337
+ "--yes",
338
+ "--tool",
339
+ "opencode",
340
+ "--directory",
341
+ projectDir,
342
+ ]);
343
+
344
+ run(commandName("node"), [
345
+ path.join(rootDir, "scripts", "run-skill-validator.js"),
346
+ path.join(
347
+ projectDir,
348
+ ".opencode",
349
+ "skills",
350
+ "_shared",
351
+ "scripts",
352
+ "validate-skills.py",
353
+ ),
354
+ ]);
355
+
356
+ const managedPath = path.join(
357
+ projectDir,
358
+ ".agents",
359
+ "macca-managed-skills.txt",
360
+ );
361
+ const originalManaged = fs.readFileSync(managedPath, "utf8");
362
+ fs.writeFileSync(managedPath, `${originalManaged}../../outside\n`, "utf8");
363
+ expectCliFailure(
364
+ ["upgrade", "--directory", projectDir],
365
+ "locally modified MACCA metadata",
366
+ );
367
+ fs.writeFileSync(managedPath, originalManaged, "utf8");
368
+
369
+ const maliciousSource = path.join(projectDir, "src");
370
+ const maliciousTransactionId = "1-1-deadbeef";
371
+ const maliciousBackup = path.join(
372
+ projectDir,
373
+ `.src.macca-backup-${maliciousTransactionId}`,
374
+ );
375
+ const interruptedJournal = path.join(
376
+ projectDir,
377
+ ".agents",
378
+ "macca-transaction.json",
379
+ );
380
+ fs.mkdirSync(maliciousSource, { recursive: true });
381
+ fs.writeFileSync(
382
+ path.join(maliciousSource, "sentinel.txt"),
383
+ "keep",
384
+ "utf8",
385
+ );
386
+ fs.writeFileSync(
387
+ interruptedJournal,
388
+ `${JSON.stringify(
389
+ {
390
+ version: 1,
391
+ phase: "committed",
392
+ transactionId: maliciousTransactionId,
393
+ entries: [
394
+ {
395
+ targetPath: maliciousSource,
396
+ stagingPath: null,
397
+ backupPath: maliciousBackup,
398
+ kind: "directory",
399
+ hadTarget: true,
400
+ },
401
+ ],
402
+ },
403
+ null,
404
+ 2,
405
+ )}\n`,
406
+ "utf8",
407
+ );
408
+ expectCliFailure(
409
+ ["upgrade", "--directory", projectDir],
410
+ "not a managed MACCA path",
411
+ );
412
+ assertPathExists(path.join(maliciousSource, "sentinel.txt"));
413
+ fs.rmSync(interruptedJournal, { force: true });
414
+
415
+ const forgedTransactionId = "3-3-feedface";
416
+ const customSkillDirectory = path.dirname(customSkill);
417
+ const forgedBackup = path.join(
418
+ path.dirname(customSkillDirectory),
419
+ `.${path.basename(customSkillDirectory)}.macca-backup-${forgedTransactionId}`,
420
+ );
421
+ fs.writeFileSync(
422
+ interruptedJournal,
423
+ `${JSON.stringify(
424
+ {
425
+ version: 1,
426
+ phase: "committing",
427
+ transactionId: forgedTransactionId,
428
+ entries: [
429
+ {
430
+ targetPath: customSkillDirectory,
431
+ stagingPath: null,
432
+ backupPath: forgedBackup,
433
+ kind: "directory",
434
+ hadTarget: false,
435
+ stagedHash: null,
436
+ },
437
+ ],
438
+ },
439
+ null,
440
+ 2,
441
+ )}\n`,
442
+ "utf8",
443
+ );
444
+ expectCliFailure(
445
+ ["upgrade", "--directory", projectDir],
446
+ "matching transaction evidence",
447
+ );
448
+ assertPathExists(customSkill);
449
+ fs.rmSync(interruptedJournal, { force: true });
450
+
451
+ const interruptedConfig = fs.readFileSync(configPath, "utf8");
452
+ const interruptedTransactionId = "2-2-cafebabe";
453
+ const interruptedBackup = path.join(
454
+ projectDir,
455
+ ".agents",
456
+ `.developer-config.json.macca-backup-${interruptedTransactionId}`,
457
+ );
458
+ const interruptedTarget = `${JSON.stringify({ interrupted: true }, null, 2)}\n`;
459
+ fs.renameSync(configPath, interruptedBackup);
460
+ fs.writeFileSync(configPath, interruptedTarget, "utf8");
461
+ fs.writeFileSync(
462
+ interruptedJournal,
463
+ `${JSON.stringify(
464
+ {
465
+ version: 1,
466
+ phase: "committing",
467
+ transactionId: interruptedTransactionId,
468
+ entries: [
469
+ {
470
+ targetPath: configPath,
471
+ stagingPath: null,
472
+ backupPath: interruptedBackup,
473
+ kind: "file",
474
+ hadTarget: true,
475
+ stagedHash: crypto
476
+ .createHash("sha256")
477
+ .update(interruptedTarget)
478
+ .digest("hex"),
479
+ },
480
+ ],
481
+ },
482
+ null,
483
+ 2,
484
+ )}\n`,
485
+ "utf8",
486
+ );
487
+ runCli(["upgrade", "--directory", projectDir]);
488
+ if (fs.existsSync(interruptedJournal) || fs.existsSync(interruptedBackup)) {
489
+ throw new Error(
490
+ "Interrupted transaction recovery left journal or backup artifacts",
491
+ );
492
+ }
493
+ if (fs.readFileSync(configPath, "utf8") !== interruptedConfig) {
494
+ throw new Error(
495
+ "Interrupted transaction recovery did not restore developer-config.json",
496
+ );
497
+ }
498
+
499
+ runCli(["install", "--yes", "--tool", "opencode", "--directory", driftDir]);
500
+ const driftSkill = path.join(
501
+ driftDir,
502
+ ".opencode",
503
+ "skills",
504
+ "developer",
505
+ "SKILL.md",
506
+ );
507
+ fs.appendFileSync(driftSkill, "\nlocal edit\n", "utf8");
508
+ expectCliFailure(
509
+ ["upgrade", "--directory", driftDir],
510
+ "locally modified managed skill",
511
+ );
512
+
513
+ assertPathExists(path.join(projectDir, ".agents", "developer-config.json"));
514
+ assertPathExists(
515
+ path.join(projectDir, ".agents", "macca-managed-skills.txt"),
516
+ );
517
+ assertPathExists(path.join(projectDir, ".agents", "macca-tools.txt"));
518
+ assertPathExists(
519
+ path.join(projectDir, ".agents", "skills", "brainstorm-prd", "SKILL.md"),
520
+ );
521
+ assertPathExists(
522
+ path.join(projectDir, ".github", "skills", "brainstorm-prd", "SKILL.md"),
523
+ );
524
+ assertPathExists(
525
+ path.join(
526
+ projectDir,
527
+ ".opencode",
528
+ "skills",
529
+ "brainstorm-prd",
530
+ "SKILL.md",
531
+ ),
532
+ );
533
+ for (const skillsRoot of [
534
+ path.join(projectDir, ".agents", "skills"),
535
+ path.join(projectDir, ".github", "skills"),
536
+ path.join(projectDir, ".opencode", "skills"),
537
+ ]) {
538
+ assertPathExists(path.join(skillsRoot, "meet", "SKILL.md"));
539
+ }
540
+ assertPathExists(customSkill);
541
+ assertPathExists(path.join(projectDir, ".agents", "macca-lock.json"));
542
+
543
+ const allToolsDir = path.join(tmpDir, "all-tools-project");
544
+ runCli([
545
+ "install",
546
+ "--yes",
547
+ "--tool",
548
+ "codex",
549
+ "--tool",
550
+ "github-copilot",
551
+ "--tool",
552
+ "cursor",
553
+ "--tool",
554
+ "claude-code",
555
+ "--tool",
556
+ "windsurf",
557
+ "--tool",
558
+ "gemini-cli",
559
+ "--tool",
560
+ "opencode",
561
+ "--tool",
562
+ "kilo-code",
563
+ "--tool",
564
+ "kimi-cli",
565
+ "--directory",
566
+ allToolsDir,
567
+ ]);
568
+ for (const expectedPath of [
569
+ path.join(allToolsDir, ".agents", "skills", "meet", "SKILL.md"),
570
+ path.join(allToolsDir, ".github", "skills", "meet", "SKILL.md"),
571
+ path.join(allToolsDir, ".cursor", "skills", "meet", "SKILL.md"),
572
+ path.join(allToolsDir, ".claude", "skills", "meet", "SKILL.md"),
573
+ path.join(allToolsDir, ".windsurf", "skills", "meet", "SKILL.md"),
574
+ path.join(allToolsDir, ".gemini", "skills", "meet", "SKILL.md"),
575
+ path.join(allToolsDir, ".opencode", "skills", "meet", "SKILL.md"),
576
+ path.join(allToolsDir, ".kilo", "skills", "meet", "SKILL.md"),
577
+ ]) {
578
+ assertPathExists(expectedPath);
579
+ }
580
+
581
+ if (mode === "local") {
582
+ const downgradeDir = path.join(tmpDir, "downgrade-project");
583
+ runCli([
584
+ "install",
585
+ "--yes",
586
+ "--tool",
587
+ "github-copilot",
588
+ "--tool",
589
+ "opencode",
590
+ "--directory",
591
+ downgradeDir,
592
+ "--name",
593
+ "Downgrade User",
594
+ "--project",
595
+ "Downgrade Project",
596
+ "--communication-language",
597
+ "English",
598
+ "--document-language",
599
+ "English",
600
+ ]);
601
+ const downgradeLockPath = path.join(
602
+ downgradeDir,
603
+ ".agents",
604
+ "macca-lock.json",
605
+ );
606
+ const downgradeLock = JSON.parse(
607
+ fs.readFileSync(downgradeLockPath, "utf8"),
608
+ );
609
+ downgradeLock.version = "99.0.0";
610
+ fs.writeFileSync(
611
+ downgradeLockPath,
612
+ `${JSON.stringify(downgradeLock, null, 2)}\n`,
613
+ "utf8",
614
+ );
615
+ expectCliFailure(
616
+ ["upgrade", "--directory", downgradeDir],
617
+ "Refusing to downgrade MACCA",
618
+ );
619
+ assertPathExists(
620
+ path.join(downgradeDir, ".github", "skills", "meet", "SKILL.md"),
621
+ );
622
+ assertPathExists(
623
+ path.join(downgradeDir, ".opencode", "skills", "meet", "SKILL.md"),
624
+ );
625
+ }
626
+
627
+ const preservedConfig = JSON.parse(fs.readFileSync(configPath, "utf8"));
628
+ if (
629
+ !preservedConfig.customField ||
630
+ preservedConfig.customField.preserved !== true
631
+ ) {
632
+ throw new Error(
633
+ "Reinstall did not preserve unknown developer-config.json fields",
634
+ );
635
+ }
636
+ if (
637
+ preservedConfig.name !== "Preserved Name" ||
638
+ preservedConfig.project !== "Preserved Project" ||
639
+ preservedConfig.languagePreferences.communication.normalized !==
640
+ "english" ||
641
+ preservedConfig.languagePreferences.documents.normalized !== "english"
642
+ ) {
643
+ throw new Error("Reinstall reset known developer-config.json values");
644
+ }
645
+
646
+ const tools = readNonEmptyLines(
647
+ path.join(projectDir, ".agents", "macca-tools.txt"),
648
+ );
649
+ for (const expectedTool of ["codex", "copilot", "opencode"]) {
650
+ if (!tools.includes(expectedTool)) {
651
+ throw new Error(
652
+ `Reinstall orphaned previously selected tool: ${expectedTool}`,
296
653
  );
297
- const interruptedTarget = `${JSON.stringify({ interrupted: true }, null, 2)}\n`;
298
- fs.renameSync(configPath, interruptedBackup);
299
- fs.writeFileSync(configPath, interruptedTarget, "utf8");
300
- fs.writeFileSync(interruptedJournal, `${JSON.stringify({
301
- version: 1,
302
- phase: "committing",
303
- transactionId: interruptedTransactionId,
304
- entries: [{
305
- targetPath: configPath,
306
- stagingPath: null,
307
- backupPath: interruptedBackup,
308
- kind: "file",
309
- hadTarget: true,
310
- stagedHash: crypto.createHash("sha256").update(interruptedTarget).digest("hex")
311
- }]
312
- }, null, 2)}\n`, "utf8");
313
- runCli(["upgrade", "--directory", projectDir]);
314
- if (fs.existsSync(interruptedJournal) || fs.existsSync(interruptedBackup)) {
315
- throw new Error("Interrupted transaction recovery left journal or backup artifacts");
316
- }
317
- if (fs.readFileSync(configPath, "utf8") !== interruptedConfig) {
318
- throw new Error("Interrupted transaction recovery did not restore developer-config.json");
319
- }
320
-
321
- runCli(["install", "--yes", "--tool", "opencode", "--directory", driftDir]);
322
- const driftSkill = path.join(driftDir, ".opencode", "skills", "developer", "SKILL.md");
323
- fs.appendFileSync(driftSkill, "\nlocal edit\n", "utf8");
324
- expectCliFailure(["upgrade", "--directory", driftDir], "locally modified managed skill");
325
-
326
- assertPathExists(path.join(projectDir, ".agents", "developer-config.json"));
327
- assertPathExists(path.join(projectDir, ".agents", "macca-managed-skills.txt"));
328
- assertPathExists(path.join(projectDir, ".agents", "macca-tools.txt"));
329
- assertPathExists(path.join(projectDir, ".agents", "skills", "brainstorm-prd", "SKILL.md"));
330
- assertPathExists(path.join(projectDir, ".github", "skills", "brainstorm-prd", "SKILL.md"));
331
- assertPathExists(path.join(projectDir, ".opencode", "skills", "brainstorm-prd", "SKILL.md"));
332
- for (const skillsRoot of [
333
- path.join(projectDir, ".agents", "skills"),
334
- path.join(projectDir, ".github", "skills"),
335
- path.join(projectDir, ".opencode", "skills")
336
- ]) {
337
- assertPathExists(path.join(skillsRoot, "meet", "SKILL.md"));
338
- }
339
- assertPathExists(customSkill);
340
- assertPathExists(path.join(projectDir, ".agents", "macca-lock.json"));
341
-
342
- const allToolsDir = path.join(tmpDir, "all-tools-project");
343
- runCli([
344
- "install",
345
- "--yes",
346
- "--tool", "codex",
347
- "--tool", "github-copilot",
348
- "--tool", "cursor",
349
- "--tool", "claude-code",
350
- "--tool", "windsurf",
351
- "--tool", "gemini-cli",
352
- "--tool", "opencode",
353
- "--tool", "kilo-code",
354
- "--tool", "kimi-cli",
355
- "--directory", allToolsDir
356
- ]);
357
- for (const expectedPath of [
358
- path.join(allToolsDir, ".agents", "skills", "meet", "SKILL.md"),
359
- path.join(allToolsDir, ".github", "skills", "meet", "SKILL.md"),
360
- path.join(allToolsDir, ".cursor", "skills", "meet", "SKILL.md"),
361
- path.join(allToolsDir, ".claude", "skills", "meet", "SKILL.md"),
362
- path.join(allToolsDir, ".windsurf", "skills", "meet", "SKILL.md"),
363
- path.join(allToolsDir, ".gemini", "skills", "meet", "SKILL.md"),
364
- path.join(allToolsDir, ".opencode", "skills", "meet", "SKILL.md"),
365
- path.join(allToolsDir, ".kilo", "skills", "meet", "SKILL.md")
366
- ]) {
367
- assertPathExists(expectedPath);
368
- }
369
-
370
- if (mode === "local") {
371
- const downgradeDir = path.join(tmpDir, "downgrade-project");
372
- runCli([
373
- "install",
374
- "--yes",
375
- "--tool", "github-copilot",
376
- "--tool", "opencode",
377
- "--directory", downgradeDir,
378
- "--name", "Downgrade User",
379
- "--project", "Downgrade Project",
380
- "--communication-language", "English",
381
- "--document-language", "English"
382
- ]);
383
- const downgradeLockPath = path.join(downgradeDir, ".agents", "macca-lock.json");
384
- const downgradeLock = JSON.parse(fs.readFileSync(downgradeLockPath, "utf8"));
385
- downgradeLock.version = "99.0.0";
386
- fs.writeFileSync(downgradeLockPath, `${JSON.stringify(downgradeLock, null, 2)}\n`, "utf8");
387
- expectCliFailure([
388
- "upgrade",
389
- "--directory", downgradeDir
390
- ], "Refusing to downgrade MACCA");
391
- assertPathExists(path.join(downgradeDir, ".github", "skills", "meet", "SKILL.md"));
392
- assertPathExists(path.join(downgradeDir, ".opencode", "skills", "meet", "SKILL.md"));
393
- }
394
-
395
- const preservedConfig = JSON.parse(fs.readFileSync(configPath, "utf8"));
396
- if (!preservedConfig.customField || preservedConfig.customField.preserved !== true) {
397
- throw new Error("Reinstall did not preserve unknown developer-config.json fields");
398
- }
399
- if (
400
- preservedConfig.name !== "Preserved Name"
401
- || preservedConfig.project !== "Preserved Project"
402
- || preservedConfig.languagePreferences.communication.normalized !== "english"
403
- || preservedConfig.languagePreferences.documents.normalized !== "english"
404
- ) {
405
- throw new Error("Reinstall reset known developer-config.json values");
406
- }
407
-
408
- const tools = readNonEmptyLines(path.join(projectDir, ".agents", "macca-tools.txt"));
409
- for (const expectedTool of ["codex", "copilot", "opencode"]) {
410
- if (!tools.includes(expectedTool)) {
411
- throw new Error(`Reinstall orphaned previously selected tool: ${expectedTool}`);
412
- }
413
- }
414
-
415
- ensureExpectedTools(path.join(projectDir, ".agents", "macca-tools.txt"));
416
-
417
- process.stdout.write("\n");
418
- process.stdout.write(" Verified files:\n");
419
- for (const filePath of listFiles(projectDir)) {
420
- process.stdout.write(`${filePath}\n`);
421
- }
422
-
423
- process.stdout.write("\n");
424
- process.stdout.write(" ✓ test-install passed\n");
425
- } finally {
426
- fs.rmSync(tmpDir, { recursive: true, force: true });
654
+ }
427
655
  }
656
+
657
+ ensureExpectedTools(path.join(projectDir, ".agents", "macca-tools.txt"));
658
+
659
+ process.stdout.write("\n");
660
+ process.stdout.write(" Verified files:\n");
661
+ for (const filePath of listFiles(projectDir)) {
662
+ process.stdout.write(`${filePath}\n`);
663
+ }
664
+
665
+ process.stdout.write("\n");
666
+ process.stdout.write(" ✓ test-install passed\n");
667
+ } finally {
668
+ fs.rmSync(tmpDir, { recursive: true, force: true });
669
+ }
428
670
  }
429
671
 
430
672
  main();