macca-method 2.1.2 → 3.0.0-rc.1

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 (89) hide show
  1. package/.agents/macca-lock.json +2 -1
  2. package/.agents/macca-managed-skills.txt +1 -0
  3. package/.agents/skills/_shared/references/additional-skills.md +17 -1
  4. package/.agents/skills/_shared/references/brainstorm-session.md +61 -21
  5. package/.agents/skills/_shared/references/config-mutation.md +58 -18
  6. package/.agents/skills/_shared/references/fix-mode.md +21 -7
  7. package/.agents/skills/_shared/references/human-loop.md +2 -0
  8. package/.agents/skills/_shared/references/implementation-principles.md +4 -0
  9. package/.agents/skills/_shared/references/interaction-contract.md +33 -0
  10. package/.agents/skills/_shared/references/invocation-policy.md +14 -3
  11. package/.agents/skills/_shared/references/language-config.md +24 -4
  12. package/.agents/skills/_shared/references/output-ownership.md +12 -5
  13. package/.agents/skills/_shared/references/planning-principles.md +35 -0
  14. package/.agents/skills/_shared/references/scope-delta.md +21 -0
  15. package/.agents/skills/_shared/references/scope-rules.md +1 -1
  16. package/.agents/skills/_shared/references/skill-catalog.md +30 -13
  17. package/.agents/skills/_shared/references/workspace-safety.md +11 -0
  18. package/.agents/skills/_shared/scripts/config-file.js +132 -0
  19. package/.agents/skills/_shared/scripts/config-validator.js +164 -0
  20. package/.agents/skills/_shared/scripts/read-preferences.js +128 -0
  21. package/.agents/skills/add-feature/SKILL.md +24 -18
  22. package/.agents/skills/brainstorm-api/SKILL.md +24 -23
  23. package/.agents/skills/brainstorm-api/assets/api.template.md +11 -23
  24. package/.agents/skills/brainstorm-architecture/SKILL.md +41 -48
  25. package/.agents/skills/brainstorm-architecture/assets/architecture.template.md +24 -16
  26. package/.agents/skills/brainstorm-prd/SKILL.md +26 -28
  27. package/.agents/skills/brainstorm-prd/assets/PRD.template.md +10 -9
  28. package/.agents/skills/brainstorm-rules/SKILL.md +42 -46
  29. package/.agents/skills/brainstorm-rules/assets/rules.template.md +8 -8
  30. package/.agents/skills/brainstorm-schema/SKILL.md +17 -17
  31. package/.agents/skills/brainstorm-schema/assets/schema.template.md +9 -8
  32. package/.agents/skills/brainstorm-styleguide/SKILL.md +27 -26
  33. package/.agents/skills/brainstorm-styleguide/assets/StyleGuide.template.md +27 -51
  34. package/.agents/skills/brainstorm-task/SKILL.md +53 -37
  35. package/.agents/skills/brainstorm-task/assets/Task.template.md +11 -12
  36. package/.agents/skills/bug-fix/SKILL.md +78 -175
  37. package/.agents/skills/bug-fix/assets/bug-log.template.md +61 -0
  38. package/.agents/skills/code-review/SKILL.md +14 -6
  39. package/.agents/skills/code-review/references/review-checklist.md +24 -44
  40. package/.agents/skills/developer/SKILL.md +23 -25
  41. package/.agents/skills/developer/references/close-phase.md +11 -6
  42. package/.agents/skills/developer/references/execute-task.md +15 -8
  43. package/.agents/skills/developer/references/onboarding.md +16 -19
  44. package/.agents/skills/help/SKILL.md +49 -49
  45. package/.agents/skills/meet/SKILL.md +37 -85
  46. package/.agents/skills/quick-dev/SKILL.md +40 -75
  47. package/.agents/skills/release-readiness/SKILL.md +1 -1
  48. package/.agents/skills/setup-macca-method/SKILL.md +79 -0
  49. package/.agents/skills/spec-audit/SKILL.md +21 -17
  50. package/.agents/skills/spec-compliance/SKILL.md +36 -18
  51. package/.agents/skills/spec-init/SKILL.md +53 -22
  52. package/CHANGELOG.md +31 -0
  53. package/README.md +92 -1195
  54. package/bin/macca-method.js +833 -286
  55. package/docs/configuration.md +174 -0
  56. package/docs/troubleshooting.md +180 -0
  57. package/docs/workflows.md +175 -0
  58. package/package.json +48 -4
  59. package/scripts/lib/npm-command.js +76 -0
  60. package/scripts/test-cli-setup.js +869 -0
  61. package/scripts/test-config.js +320 -0
  62. package/scripts/test-install.js +63 -28
  63. package/scripts/test-installer-safety.js +545 -0
  64. package/scripts/test-npm-command.js +143 -0
  65. package/scripts/test-package-safety.js +252 -0
  66. package/scripts/test-preferences.js +475 -0
  67. package/scripts/test-upgrade-legacy.js +11 -21
  68. package/scripts/validate-docs.js +166 -0
  69. package/scripts/validate-package.js +285 -0
  70. package/scripts/validate-skill-behavior.js +914 -12
  71. package/.agents/skills/antislop-copywriting/SKILL.md +0 -372
  72. package/.agents/skills/skill-creator/LICENSE.txt +0 -202
  73. package/.agents/skills/skill-creator/SKILL.md +0 -485
  74. package/.agents/skills/skill-creator/agents/analyzer.md +0 -274
  75. package/.agents/skills/skill-creator/agents/comparator.md +0 -202
  76. package/.agents/skills/skill-creator/agents/grader.md +0 -223
  77. package/.agents/skills/skill-creator/assets/eval_review.html +0 -146
  78. package/.agents/skills/skill-creator/eval-viewer/generate_review.py +0 -471
  79. package/.agents/skills/skill-creator/eval-viewer/viewer.html +0 -1325
  80. package/.agents/skills/skill-creator/references/schemas.md +0 -441
  81. package/.agents/skills/skill-creator/scripts/__init__.py +0 -0
  82. package/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +0 -401
  83. package/.agents/skills/skill-creator/scripts/generate_report.py +0 -326
  84. package/.agents/skills/skill-creator/scripts/improve_description.py +0 -247
  85. package/.agents/skills/skill-creator/scripts/package_skill.py +0 -136
  86. package/.agents/skills/skill-creator/scripts/quick_validate.py +0 -103
  87. package/.agents/skills/skill-creator/scripts/run_eval.py +0 -310
  88. package/.agents/skills/skill-creator/scripts/run_loop.py +0 -328
  89. package/.agents/skills/skill-creator/scripts/utils.py +0 -47
@@ -0,0 +1,320 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const assert = require("node:assert/strict");
6
+ const fs = require("node:fs");
7
+ const path = require("node:path");
8
+ const vm = require("node:vm");
9
+ const { spawnSync } = require("node:child_process");
10
+ const validatorPath = path.resolve(
11
+ __dirname, "../.agents/skills/_shared/scripts/config-validator.js",
12
+ );
13
+ const { validateConfig, assertValidConfig } = require(validatorPath);
14
+ const source = fs.readFileSync(validatorPath, "utf8");
15
+ const loaderSource = fs.readFileSync(path.join(path.dirname(validatorPath), "config-file.js"), "utf8");
16
+ let passed = 0;
17
+
18
+ function test(name, action) {
19
+ action();
20
+ passed += 1;
21
+ process.stdout.write(`OK: ${name}\n`);
22
+ }
23
+
24
+ function fields(value) {
25
+ return validateConfig(value).map((error) => error.field);
26
+ }
27
+
28
+ // CLI fixtures stay in memory: even the test suite does not write config files.
29
+ // A read-only fs stub also fails if the CLI attempts any other filesystem API.
30
+ function runCli(content, { args = ["SECRET-config.json"], readError, imported = false } = {}) {
31
+ const output = { stdout: "", stderr: "", exitCode: 0, reads: 0 };
32
+ const file = path.resolve(args[0] || "unused");
33
+ const bytes = Buffer.from(content);
34
+ let offset = 0;
35
+ const stat = (directory = false) => ({
36
+ dev: 1, ino: 2, size: bytes.length, mtimeMs: 10, ctimeMs: 10,
37
+ isFile: () => !directory, isDirectory: () => directory, isSymbolicLink: () => false,
38
+ });
39
+ const loader = { exports: {} };
40
+ vm.runInNewContext(loaderSource, {
41
+ module: loader, Buffer, process: { platform: process.platform },
42
+ require(name) {
43
+ if (name === "node:path") return path;
44
+ assert.equal(name, "node:fs");
45
+ return Object.freeze({
46
+ constants: fs.constants,
47
+ lstatSync(target) {
48
+ if (target === file && readError) throw readError;
49
+ return stat(target !== file);
50
+ },
51
+ openSync(target) { assert.equal(target, file); return 123; },
52
+ fstatSync(fd) { assert.equal(fd, 123); return stat(); },
53
+ readSync(fd, buffer, start, length) {
54
+ assert.equal(fd, 123);
55
+ output.reads += 1;
56
+ const count = Math.min(length, bytes.length - offset);
57
+ bytes.copy(buffer, start, offset, offset + count);
58
+ offset += count;
59
+ return count;
60
+ },
61
+ closeSync(fd) { assert.equal(fd, 123); },
62
+ });
63
+ },
64
+ }, { timeout: 5000 });
65
+ const module = { exports: {} };
66
+ const requireStub = (name) => {
67
+ assert.equal(name, "./config-file.js");
68
+ return loader.exports;
69
+ };
70
+ requireStub.main = imported ? {} : module;
71
+ const processStub = {
72
+ argv: ["node", validatorPath, ...args],
73
+ stdout: { write: (text) => { output.stdout += text; } },
74
+ stderr: { write: (text) => { output.stderr += text; } },
75
+ get exitCode() { return output.exitCode; },
76
+ set exitCode(value) { output.exitCode = value; },
77
+ };
78
+ vm.runInNewContext(source, {
79
+ module, require: requireStub, process: processStub,
80
+ }, { filename: validatorPath, timeout: 5000 });
81
+ return output;
82
+ }
83
+
84
+ test("optional fields and complete current config", () => {
85
+ assert.deepEqual(validateConfig({}), []);
86
+ assert.deepEqual(validateConfig({
87
+ name: "", project: "",
88
+ languagePreferences: {
89
+ communication: { raw: "English", normalized: "english" },
90
+ documents: { raw: "Bahasa Indonesia", normalized: "indonesian" },
91
+ },
92
+ developerPreferences: { workMode: "direct", scope: "fullstack" },
93
+ brainstormPreferences: {
94
+ discussionMode: "one-by-one", recommendations: true, discoveryDepth: "standard",
95
+ },
96
+ codeReviewPreferences: { fixMode: "report-first" },
97
+ additionalSkills: [{
98
+ name: "react", purpose: "UI", paths: { copilot: "a", opencode: "b", codex: "c" },
99
+ }],
100
+ availableMCPs: ["context7", "supabase"],
101
+ }), []);
102
+ assert.equal(assertValidConfig({}, "valid.json"), undefined);
103
+ });
104
+
105
+ test("all documented enums remain accepted", () => {
106
+ const enums = {
107
+ developerPreferences: { workMode: ["direct", "plan-first"], scope: ["frontend", "backend", "fullstack"] },
108
+ brainstormPreferences: {
109
+ discussionMode: ["one-by-one", "three-at-a-time", "all-at-once"],
110
+ discoveryDepth: ["quick", "standard", "critical"], recommendations: [true, false],
111
+ },
112
+ codeReviewPreferences: { fixMode: ["report-first", "fix-then-report"] },
113
+ };
114
+ for (const [section, choices] of Object.entries(enums)) {
115
+ for (const [field, values] of Object.entries(choices)) {
116
+ for (const value of values) assert.deepEqual(validateConfig({ [section]: { [field]: value } }), []);
117
+ }
118
+ }
119
+ });
120
+
121
+ test("language aliases and fallback strings are preserved verbatim", () => {
122
+ for (const language of [
123
+ "indonesian", "id", "indo", "indonesia", "bahasa indonesia",
124
+ "english", "en", "eng", "inggris", "bahasa inggris", " EN ", "", "custom language",
125
+ ]) {
126
+ const config = { languagePreferences: {
127
+ communication: { raw: language, normalized: language }, documents: {},
128
+ } };
129
+ const before = JSON.stringify(config);
130
+ assert.deepEqual(validateConfig(config), []);
131
+ assert.equal(JSON.stringify(config), before);
132
+ }
133
+ });
134
+
135
+ test("legacy paths and explicit MCP denial are supported", () => {
136
+ const skill = { name: "legacy", purpose: "", paths: { futureHost: "" } };
137
+ for (const key of ["path", "githubPath", "opencodePath", "claudePath", "cursorPath",
138
+ "windsurfPath", "geminiPath", "kiloPath", "kimiPath", "codexPath"]) {
139
+ skill[key] = "legacy/SKILL.md";
140
+ assert.deepEqual(fields({ additionalSkills: [{ name: "legacy", [key]: 12 }] }), [
141
+ `additionalSkills[0].${key}`,
142
+ ]);
143
+ }
144
+ assert.deepEqual(validateConfig({ additionalSkills: [skill], availableMCPs: "none" }), []);
145
+ assert.deepEqual(validateConfig({ additionalSkills: [], availableMCPs: [] }), []);
146
+ assert.deepEqual(validateConfig({ availableMCPs: ["none"] }), []);
147
+ assert.deepEqual(fields({ additionalSkills: ["legacy"] }), ["additionalSkills[0]"]);
148
+ assert.deepEqual(fields({ availableMCPs: "context7" }), ["availableMCPs"]);
149
+ });
150
+
151
+ test("root and nested objects reject null, arrays, and primitives", () => {
152
+ for (const bad of [null, [], "x", 7, true, undefined]) {
153
+ assert.deepEqual(fields(bad), ["$"]);
154
+ for (const key of ["languagePreferences", "developerPreferences", "brainstormPreferences", "codeReviewPreferences"]) {
155
+ assert.deepEqual(fields({ [key]: bad }), [key]);
156
+ }
157
+ for (const key of ["communication", "documents"]) {
158
+ assert.deepEqual(fields({ languagePreferences: { [key]: bad } }), [`languagePreferences.${key}`]);
159
+ }
160
+ }
161
+ });
162
+
163
+ test("nested errors accumulate with exact field and array positions", () => {
164
+ assert.deepEqual(fields({
165
+ name: null, project: [],
166
+ languagePreferences: { communication: { raw: [], normalized: false }, documents: null },
167
+ developerPreferences: { workMode: "unsafe", scope: null },
168
+ brainstormPreferences: { discussionMode: [], recommendations: "true", discoveryDepth: "unsafe" },
169
+ codeReviewPreferences: { fixMode: "unsafe" },
170
+ additionalSkills: [null, {}, { name: " ", purpose: 7, paths: null }, { name: "x", paths: { codex: [] } }],
171
+ availableMCPs: ["context7", null, " ", 4],
172
+ }), [
173
+ "name", "project", "languagePreferences.communication.raw",
174
+ "languagePreferences.communication.normalized", "languagePreferences.documents",
175
+ "developerPreferences.workMode", "developerPreferences.scope",
176
+ "brainstormPreferences.discussionMode", "brainstormPreferences.recommendations",
177
+ "brainstormPreferences.discoveryDepth", "codeReviewPreferences.fixMode",
178
+ "additionalSkills[0]", "additionalSkills[1].name", "additionalSkills[2].name",
179
+ "additionalSkills[2].purpose", "additionalSkills[2].paths", "additionalSkills[3].paths[0]",
180
+ "availableMCPs[1]", "availableMCPs[2]", "availableMCPs[3]",
181
+ ]);
182
+ for (const bad of [null, {}, "", false]) {
183
+ assert.deepEqual(fields({ additionalSkills: bad }), ["additionalSkills"]);
184
+ assert.deepEqual(fields({ availableMCPs: bad }), ["availableMCPs"]);
185
+ }
186
+ assert.deepEqual(fields({ availableMCPs: new Array(1) }), ["availableMCPs[0]"]);
187
+ });
188
+
189
+ test("unknown fields at every schema level are untouched and never evaluated", () => {
190
+ const extension = { get secret() { throw new Error("extension evaluated"); } };
191
+ const language = { raw: "custom", normalized: "custom" };
192
+ const skill = { name: "x", paths: { newHost: "x" } };
193
+ const config = {
194
+ languagePreferences: { communication: language, documents: {} },
195
+ developerPreferences: {}, brainstormPreferences: {}, codeReviewPreferences: {},
196
+ additionalSkills: [skill], availableMCPs: [],
197
+ };
198
+ const levels = [config, config.languagePreferences, language, config.languagePreferences.documents,
199
+ config.developerPreferences, config.brainstormPreferences, config.codeReviewPreferences, skill];
200
+ for (const level of levels) {
201
+ level.extension = extension;
202
+ Object.defineProperty(level, "unknownAccessor", {
203
+ enumerable: true, get() { throw new Error("unknown accessor evaluated"); },
204
+ });
205
+ Object.freeze(level);
206
+ }
207
+ assert.deepEqual(validateConfig(config), []);
208
+ assertValidConfig(config);
209
+ for (const level of levels) assert.equal(level.extension, extension);
210
+ const special = JSON.parse('{"__proto__":{"polluted":true},"constructor":null,"prototype":[]}');
211
+ const before = JSON.stringify(special);
212
+ assert.deepEqual(validateConfig(special), []);
213
+ assert.equal(JSON.stringify(special), before);
214
+ assert.equal(Object.prototype.polluted, undefined);
215
+ assert.deepEqual(fields(Object.assign(Object.create({ name: 42 }), { project: "" })), []);
216
+ });
217
+
218
+ test("known accessors are rejected without execution", () => {
219
+ const config = { get name() { throw new Error("must not execute"); } };
220
+ assert.deepEqual(fields(config), ["name"]);
221
+ });
222
+
223
+ test("assertion errors expose code and fields but never hostile values or map keys", () => {
224
+ const secret = "SECRET-credential-123\n\u001b[31m";
225
+ const config = {
226
+ name: { secret }, developerPreferences: { workMode: secret },
227
+ additionalSkills: [{ name: "x", paths: { [secret]: { secret } } }],
228
+ };
229
+ const before = JSON.stringify(config);
230
+ const diagnostics = validateConfig(config);
231
+ assert.equal(JSON.stringify(config), before);
232
+ for (const error of diagnostics) assert.deepEqual(Object.keys(error), ["field", "message"]);
233
+ assert(!JSON.stringify(diagnostics).includes("SECRET"));
234
+ assert.throws(() => assertValidConfig(config, "project/.agents/developer-config.json"), (error) => {
235
+ assert.equal(error.code, "MACCA_CONFIG_INVALID");
236
+ assert.equal(error.field, "name");
237
+ assert.deepEqual(error.fields, ["name", "developerPreferences.workMode", "additionalSkills[0].paths[0]"]);
238
+ assert.match(error.message, /project\/\.agents\/developer-config\.json/);
239
+ assert(!`${error.stack}${JSON.stringify(error)}`.includes("SECRET"));
240
+ return true;
241
+ });
242
+ assert.throws(() => assertValidConfig(null, "config\n\u001b.json"), (error) => {
243
+ assert(error.message.includes('"config\\n\\u001b.json"'));
244
+ return true;
245
+ });
246
+ });
247
+
248
+ test("requiring the module has no CLI side effects and exports only the agreed API", () => {
249
+ assert.deepEqual(Object.keys(require(validatorPath)).sort(), ["assertValidConfig", "validateConfig"]);
250
+ assert.deepEqual(runCli("not JSON", { imported: true }), { stdout: "", stderr: "", exitCode: 0, reads: 0 });
251
+ });
252
+
253
+ test("CLI success is human-readable and read-only", () => {
254
+ assert.deepEqual(runCli('{"name":"SECRET","extension":{"token":"PRIVATE"}}'), {
255
+ stdout: 'Valid config.\n', stderr: "", exitCode: 0, reads: 2,
256
+ });
257
+ });
258
+
259
+ test("CLI schema and JSON diagnostics redact configuration and parser excerpts", () => {
260
+ for (const content of [
261
+ '{"developerPreferences":{"workMode":"SECRET"}}',
262
+ '{"additionalSkills":[{"name":"x","paths":{"SECRET":false}}]}',
263
+ '{"name":"SECRET", broken JSON}',
264
+ "SECRET invalid JSON", "null", "[]",
265
+ ]) {
266
+ const result = runCli(content);
267
+ assert.equal(result.exitCode, 1);
268
+ assert.equal(result.stdout, "");
269
+ assert.match(result.stderr, /Invalid config:/);
270
+ assert(!result.stderr.includes("SECRET"));
271
+ }
272
+ assert.match(runCli('{"availableMCPs":[false]}').stderr, /availableMCPs\[0\]/);
273
+ assert.match(runCli("malformed").stderr, /\$: must contain valid JSON/);
274
+ });
275
+
276
+ test("CLI missing/unreadable files and usage return exit 1 with no raw exceptions", () => {
277
+ for (const code of ["ENOENT", "EACCES", "EISDIR"]) {
278
+ const readError = Object.assign(new Error("SECRET filesystem excerpt"), { code });
279
+ const result = runCli("", { readError });
280
+ assert.equal(result.exitCode, 1);
281
+ assert.equal(result.stdout, "");
282
+ assert.match(result.stderr, code === "ENOENT" ? /Missing config file/ : /Cannot inspect config file/);
283
+ assert(!result.stderr.includes("SECRET"));
284
+ }
285
+ for (const args of [[], ["one.json", "SECRET"]]) {
286
+ const result = runCli("", { args });
287
+ assert.equal(result.exitCode, 1);
288
+ assert.equal(result.reads, 0);
289
+ assert.match(result.stderr, /^Usage:/);
290
+ assert(!result.stderr.includes("SECRET"));
291
+ }
292
+ });
293
+
294
+ test("real Node CLI exits correctly without creating fixture files", () => {
295
+ const usage = spawnSync(process.execPath, [validatorPath], { encoding: "utf8", timeout: 10000 });
296
+ assert.ifError(usage.error);
297
+ assert.equal(usage.status, 1);
298
+ assert.match(usage.stderr, /^Usage:/);
299
+ // package.json is also a valid extension-rich config object (name is a string).
300
+ const packagePath = fs.realpathSync(path.resolve(__dirname, "../package.json"));
301
+ for (const [file, status] of [[packagePath, 0], [fs.realpathSync(validatorPath), 1]]) {
302
+ const before = fs.readFileSync(file);
303
+ const result = spawnSync(process.execPath, [validatorPath, file], {
304
+ encoding: "utf8", timeout: 10000,
305
+ });
306
+ assert.ifError(result.error);
307
+ assert.equal(result.status, status, result.stderr);
308
+ assert.deepEqual(fs.readFileSync(file), before);
309
+ assert.match(status === 0 ? result.stdout : result.stderr, status === 0 ? /^Valid config/ : /must contain valid JSON/);
310
+ }
311
+ const missing = path.join(fs.realpathSync(__dirname), "__macca_config_missing_fixture__.json");
312
+ assert.equal(fs.existsSync(missing), false);
313
+ const result = spawnSync(process.execPath, [validatorPath, missing], { encoding: "utf8", timeout: 10000 });
314
+ assert.ifError(result.error);
315
+ assert.equal(result.status, 1);
316
+ assert.match(result.stderr, /^Missing config file/);
317
+ assert.equal(fs.existsSync(missing), false);
318
+ });
319
+
320
+ process.stdout.write(`\n${passed} config validator tests passed.\n`);
@@ -3,10 +3,12 @@
3
3
  "use strict";
4
4
 
5
5
  const fs = require("node:fs");
6
+ const assert = require("node:assert/strict");
6
7
  const crypto = require("node:crypto");
7
8
  const os = require("node:os");
8
9
  const path = require("node:path");
9
10
  const { execFileSync } = require("node:child_process");
11
+ const { resolveNpmCommand } = require("./lib/npm-command.js");
10
12
 
11
13
  const rootDir = path.resolve(__dirname, "..");
12
14
  const mode = process.argv.includes("--published") ? "published" : "local";
@@ -19,12 +21,11 @@ const symlinkDir = path.join(tmpDir, "symlink-project");
19
21
  const driftDir = path.join(tmpDir, "drift-project");
20
22
  let packageSpec = "macca-method";
21
23
 
22
- function commandName(base) {
23
- if (process.platform !== "win32") {
24
- return base;
25
- }
26
-
27
- return base === "npm" || base === "npx" ? `${base}.cmd` : base;
24
+ function publishedPackageSpec() {
25
+ const version = String(process.env.MACCA_PUBLISHED_VERSION || "")
26
+ .trim()
27
+ .replace(/^v/, "");
28
+ return version ? `macca-method@${version}` : "macca-method@latest";
28
29
  }
29
30
 
30
31
  function resolveTempRoot() {
@@ -37,22 +38,13 @@ function resolveTempRoot() {
37
38
  }
38
39
 
39
40
  function runNpm(args, options = {}) {
40
- const npmExecPath = process.env.npm_execpath;
41
- if (npmExecPath && /\.c?js$/i.test(npmExecPath)) {
42
- return capture(process.execPath, [npmExecPath, ...args], options);
43
- }
44
-
45
- return capture(commandName("npm"), args, options);
41
+ const npm = resolveNpmCommand(args);
42
+ return capture(npm.command, npm.args, options);
46
43
  }
47
44
 
48
45
  function runNpmWithInheritedStdio(args, options = {}) {
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
- }
54
-
55
- run(commandName("npm"), args, options);
46
+ const npm = resolveNpmCommand(args);
47
+ run(npm.command, npm.args, options);
56
48
  }
57
49
 
58
50
  function run(command, args, options = {}) {
@@ -60,6 +52,7 @@ function run(command, args, options = {}) {
60
52
  cwd: rootDir,
61
53
  stdio: "inherit",
62
54
  ...options,
55
+ shell: false,
63
56
  });
64
57
  }
65
58
 
@@ -68,6 +61,7 @@ function capture(command, args, options = {}) {
68
61
  cwd: rootDir,
69
62
  encoding: "utf8",
70
63
  ...options,
64
+ shell: false,
71
65
  });
72
66
  }
73
67
 
@@ -77,7 +71,7 @@ function runCli(args) {
77
71
  "exec",
78
72
  "--yes",
79
73
  "--package",
80
- "macca-method",
74
+ publishedPackageSpec(),
81
75
  "--",
82
76
  "macca-method",
83
77
  ...args,
@@ -104,7 +98,7 @@ function expectCliFailure(args, expectedText) {
104
98
  "exec",
105
99
  "--yes",
106
100
  "--package",
107
- "macca-method",
101
+ publishedPackageSpec(),
108
102
  "--",
109
103
  "macca-method",
110
104
  ...args,
@@ -143,6 +137,14 @@ function assertPathExists(targetPath) {
143
137
  }
144
138
  }
145
139
 
140
+ function assertInstalledSkills(skillsRoot) {
141
+ const expected = JSON.parse(fs.readFileSync(path.join(rootDir, ".agents", "macca-lock.json"), "utf8")).skills;
142
+ for (const skill of expected.filter((name) => name !== "_shared")) {
143
+ assertPathExists(path.join(skillsRoot, skill, "SKILL.md"));
144
+ }
145
+ assertPathExists(path.join(skillsRoot, "_shared", "scripts", "config-validator.js"));
146
+ }
147
+
146
148
  function readNonEmptyLines(filePath) {
147
149
  return fs
148
150
  .readFileSync(filePath, "utf8")
@@ -183,7 +185,7 @@ function ensureExpectedTools(filePath) {
183
185
  }
184
186
 
185
187
  function resolveLocalPackage() {
186
- const packOutput = runNpm(["pack", "--json", "--pack-destination", tmpDir]);
188
+ const packOutput = runNpm(["pack", "--json", "--ignore-scripts", "--pack-destination", tmpDir]);
187
189
  const packageList = JSON.parse(packOutput);
188
190
 
189
191
  if (
@@ -195,6 +197,7 @@ function resolveLocalPackage() {
195
197
  }
196
198
 
197
199
  packageSpec = path.join(tmpDir, packageList[0].filename);
200
+ run(process.execPath, [path.join(rootDir, "scripts", "validate-package.js"), packageSpec]);
198
201
  }
199
202
 
200
203
  function main() {
@@ -269,16 +272,26 @@ function main() {
269
272
  const outerReal = path.join(tmpDir, "outer-real-project");
270
273
  const outerLink = path.join(tmpDir, "outer-link-project");
271
274
  fs.mkdirSync(outerReal, { recursive: true });
275
+ let junctionCreated = false;
272
276
  try {
273
- run(commandName("cmd"), ["/c", "mklink", "/J", outerLink, outerReal]);
277
+ fs.symlinkSync(outerReal, outerLink, "junction");
278
+ junctionCreated = true;
279
+ } catch (error) {
280
+ if (!["EPERM", "EACCES"].includes(error.code)) throw error;
281
+ process.stdout.write(
282
+ " Skipping Windows junction ancestry test (junction creation not permitted)\n",
283
+ );
284
+ }
285
+ if (junctionCreated) {
274
286
  expectCliFailure(
275
287
  ["install", "--yes", "--tool", "codex", "--directory", outerLink],
276
288
  "Refusing symlinked target project ancestor",
277
289
  );
278
- } catch {
279
- process.stdout.write(
280
- " Skipping Windows junction ancestry test (mklink unavailable or not permitted)\n",
281
- );
290
+ if (fs.readdirSync(outerReal).length !== 0) {
291
+ throw new Error(
292
+ "Target directory junction test wrote into the real directory",
293
+ );
294
+ }
282
295
  }
283
296
  }
284
297
 
@@ -308,6 +321,14 @@ function main() {
308
321
  "github-copilot",
309
322
  "--directory",
310
323
  projectDir,
324
+ "--name",
325
+ "Initial User",
326
+ "--project",
327
+ "Initial Project",
328
+ "--communication-language",
329
+ "Unsupported Language",
330
+ "--document-language",
331
+ "Unsupported Language",
311
332
  ]);
312
333
 
313
334
  const configPath = path.join(
@@ -316,6 +337,12 @@ function main() {
316
337
  "developer-config.json",
317
338
  );
318
339
  const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
340
+ if (
341
+ config.languagePreferences.communication.normalized !== "indonesian" ||
342
+ config.languagePreferences.documents.normalized !== "indonesian"
343
+ ) {
344
+ throw new Error("Unknown language did not use the Indonesian fallback");
345
+ }
319
346
  config.name = "Preserved Name";
320
347
  config.project = "Preserved Project";
321
348
  config.languagePreferences.communication = {
@@ -341,7 +368,7 @@ function main() {
341
368
  projectDir,
342
369
  ]);
343
370
 
344
- run(commandName("node"), [
371
+ run(process.execPath, [
345
372
  path.join(rootDir, "scripts", "run-skill-validator.js"),
346
373
  path.join(
347
374
  projectDir,
@@ -359,6 +386,11 @@ function main() {
359
386
  "macca-managed-skills.txt",
360
387
  );
361
388
  const originalManaged = fs.readFileSync(managedPath, "utf8");
389
+ assert.deepEqual(
390
+ readNonEmptyLines(managedPath).sort(),
391
+ readNonEmptyLines(path.join(rootDir, ".agents", "macca-managed-skills.txt")).sort(),
392
+ "Installed manifest must include every official managed skill",
393
+ );
362
394
  fs.writeFileSync(managedPath, `${originalManaged}../../outside\n`, "utf8");
363
395
  expectCliFailure(
364
396
  ["upgrade", "--directory", projectDir],
@@ -509,6 +541,7 @@ function main() {
509
541
  ["upgrade", "--directory", driftDir],
510
542
  "locally modified managed skill",
511
543
  );
544
+ runCli(["upgrade", "--directory", driftDir, "--force"]);
512
545
 
513
546
  assertPathExists(path.join(projectDir, ".agents", "developer-config.json"));
514
547
  assertPathExists(
@@ -536,6 +569,7 @@ function main() {
536
569
  path.join(projectDir, ".opencode", "skills"),
537
570
  ]) {
538
571
  assertPathExists(path.join(skillsRoot, "meet", "SKILL.md"));
572
+ assertInstalledSkills(skillsRoot);
539
573
  }
540
574
  assertPathExists(customSkill);
541
575
  assertPathExists(path.join(projectDir, ".agents", "macca-lock.json"));
@@ -576,6 +610,7 @@ function main() {
576
610
  path.join(allToolsDir, ".kilo", "skills", "meet", "SKILL.md"),
577
611
  ]) {
578
612
  assertPathExists(expectedPath);
613
+ assertInstalledSkills(path.dirname(path.dirname(expectedPath)));
579
614
  }
580
615
 
581
616
  if (mode === "local") {