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.
- package/.agents/macca-lock.json +2 -1
- package/.agents/macca-managed-skills.txt +1 -0
- package/.agents/skills/_shared/references/additional-skills.md +17 -1
- package/.agents/skills/_shared/references/brainstorm-session.md +61 -21
- package/.agents/skills/_shared/references/config-mutation.md +58 -18
- package/.agents/skills/_shared/references/fix-mode.md +21 -7
- package/.agents/skills/_shared/references/human-loop.md +2 -0
- package/.agents/skills/_shared/references/implementation-principles.md +4 -0
- package/.agents/skills/_shared/references/interaction-contract.md +33 -0
- package/.agents/skills/_shared/references/invocation-policy.md +14 -3
- package/.agents/skills/_shared/references/language-config.md +24 -4
- package/.agents/skills/_shared/references/output-ownership.md +12 -5
- package/.agents/skills/_shared/references/planning-principles.md +35 -0
- package/.agents/skills/_shared/references/scope-delta.md +21 -0
- package/.agents/skills/_shared/references/scope-rules.md +1 -1
- package/.agents/skills/_shared/references/skill-catalog.md +30 -13
- package/.agents/skills/_shared/references/workspace-safety.md +11 -0
- package/.agents/skills/_shared/scripts/config-file.js +132 -0
- package/.agents/skills/_shared/scripts/config-validator.js +164 -0
- package/.agents/skills/_shared/scripts/read-preferences.js +128 -0
- package/.agents/skills/add-feature/SKILL.md +24 -18
- package/.agents/skills/brainstorm-api/SKILL.md +24 -23
- package/.agents/skills/brainstorm-api/assets/api.template.md +11 -23
- package/.agents/skills/brainstorm-architecture/SKILL.md +41 -48
- package/.agents/skills/brainstorm-architecture/assets/architecture.template.md +24 -16
- package/.agents/skills/brainstorm-prd/SKILL.md +26 -28
- package/.agents/skills/brainstorm-prd/assets/PRD.template.md +10 -9
- package/.agents/skills/brainstorm-rules/SKILL.md +42 -46
- package/.agents/skills/brainstorm-rules/assets/rules.template.md +8 -8
- package/.agents/skills/brainstorm-schema/SKILL.md +17 -17
- package/.agents/skills/brainstorm-schema/assets/schema.template.md +9 -8
- package/.agents/skills/brainstorm-styleguide/SKILL.md +27 -26
- package/.agents/skills/brainstorm-styleguide/assets/StyleGuide.template.md +27 -51
- package/.agents/skills/brainstorm-task/SKILL.md +53 -37
- package/.agents/skills/brainstorm-task/assets/Task.template.md +11 -12
- package/.agents/skills/bug-fix/SKILL.md +78 -175
- package/.agents/skills/bug-fix/assets/bug-log.template.md +61 -0
- package/.agents/skills/code-review/SKILL.md +14 -6
- package/.agents/skills/code-review/references/review-checklist.md +24 -44
- package/.agents/skills/developer/SKILL.md +23 -25
- package/.agents/skills/developer/references/close-phase.md +11 -6
- package/.agents/skills/developer/references/execute-task.md +15 -8
- package/.agents/skills/developer/references/onboarding.md +16 -19
- package/.agents/skills/help/SKILL.md +49 -49
- package/.agents/skills/meet/SKILL.md +37 -85
- package/.agents/skills/quick-dev/SKILL.md +40 -75
- package/.agents/skills/release-readiness/SKILL.md +1 -1
- package/.agents/skills/setup-macca-method/SKILL.md +79 -0
- package/.agents/skills/spec-audit/SKILL.md +21 -17
- package/.agents/skills/spec-compliance/SKILL.md +36 -18
- package/.agents/skills/spec-init/SKILL.md +53 -22
- package/CHANGELOG.md +31 -0
- package/README.md +92 -1195
- package/bin/macca-method.js +833 -286
- package/docs/configuration.md +174 -0
- package/docs/troubleshooting.md +180 -0
- package/docs/workflows.md +175 -0
- package/package.json +48 -4
- package/scripts/lib/npm-command.js +76 -0
- package/scripts/test-cli-setup.js +869 -0
- package/scripts/test-config.js +320 -0
- package/scripts/test-install.js +63 -28
- package/scripts/test-installer-safety.js +545 -0
- package/scripts/test-npm-command.js +143 -0
- package/scripts/test-package-safety.js +252 -0
- package/scripts/test-preferences.js +475 -0
- package/scripts/test-upgrade-legacy.js +11 -21
- package/scripts/validate-docs.js +166 -0
- package/scripts/validate-package.js +285 -0
- package/scripts/validate-skill-behavior.js +914 -12
- package/.agents/skills/antislop-copywriting/SKILL.md +0 -372
- package/.agents/skills/skill-creator/LICENSE.txt +0 -202
- package/.agents/skills/skill-creator/SKILL.md +0 -485
- package/.agents/skills/skill-creator/agents/analyzer.md +0 -274
- package/.agents/skills/skill-creator/agents/comparator.md +0 -202
- package/.agents/skills/skill-creator/agents/grader.md +0 -223
- package/.agents/skills/skill-creator/assets/eval_review.html +0 -146
- package/.agents/skills/skill-creator/eval-viewer/generate_review.py +0 -471
- package/.agents/skills/skill-creator/eval-viewer/viewer.html +0 -1325
- package/.agents/skills/skill-creator/references/schemas.md +0 -441
- package/.agents/skills/skill-creator/scripts/__init__.py +0 -0
- package/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +0 -401
- package/.agents/skills/skill-creator/scripts/generate_report.py +0 -326
- package/.agents/skills/skill-creator/scripts/improve_description.py +0 -247
- package/.agents/skills/skill-creator/scripts/package_skill.py +0 -136
- package/.agents/skills/skill-creator/scripts/quick_validate.py +0 -103
- package/.agents/skills/skill-creator/scripts/run_eval.py +0 -310
- package/.agents/skills/skill-creator/scripts/run_loop.py +0 -328
- package/.agents/skills/skill-creator/scripts/utils.py +0 -47
|
@@ -0,0 +1,475 @@
|
|
|
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 readerPath = path.resolve(__dirname, "../.agents/skills/_shared/scripts/read-preferences.js");
|
|
11
|
+
const validator = require("../.agents/skills/_shared/scripts/config-validator.js");
|
|
12
|
+
const { readPreferences, summarize } = require(readerPath);
|
|
13
|
+
const source = fs.readFileSync(readerPath, "utf8");
|
|
14
|
+
const loaderSource = fs.readFileSync(path.join(path.dirname(readerPath), "config-file.js"), "utf8");
|
|
15
|
+
const validatorSource = fs.readFileSync(path.join(path.dirname(readerPath), "config-validator.js"), "utf8");
|
|
16
|
+
const LIMIT = 1024 * 1024;
|
|
17
|
+
const SECRET = "SYNTHETIC_SECRET_DO_NOT_OUTPUT_42";
|
|
18
|
+
let passed = 0;
|
|
19
|
+
|
|
20
|
+
function test(name, action) {
|
|
21
|
+
action();
|
|
22
|
+
passed += 1;
|
|
23
|
+
process.stdout.write(`OK: ${name}\n`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ioError(code) {
|
|
27
|
+
return Object.assign(new Error(SECRET), { code });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Entire fixture filesystem stays in memory. Any write/chmod/mkdir or other
|
|
31
|
+
// unapproved fs API fails immediately; tests never load live user preferences.
|
|
32
|
+
function runCli(content = "{}", options = {}) {
|
|
33
|
+
const output = { stdout: "", stderr: "", exitCode: 0, calls: [] };
|
|
34
|
+
const bytes = Buffer.from(content);
|
|
35
|
+
const platform = options.platform || "linux";
|
|
36
|
+
const pathApi = platform === "win32" ? path.win32 : path.posix;
|
|
37
|
+
const args = options.args || [platform === "win32" ? `C:\\workspace\\.agents\\${SECRET}.json` : `/workspace/.agents/${SECRET}.json`];
|
|
38
|
+
const target = pathApi.resolve(args[0] || "/unused");
|
|
39
|
+
let offset = 0;
|
|
40
|
+
let stats = 0;
|
|
41
|
+
const stat = (extra = {}) => ({
|
|
42
|
+
dev: 1, ino: 2, size: bytes.length, mtimeMs: 10, ctimeMs: 10,
|
|
43
|
+
isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false, ...extra,
|
|
44
|
+
});
|
|
45
|
+
const constants = { O_RDONLY: 0, O_NOFOLLOW: 131072, O_NONBLOCK: 2048 };
|
|
46
|
+
const mockFs = Object.freeze({
|
|
47
|
+
constants: options.zeroSafeFlags ? { O_RDONLY: 0, O_NOFOLLOW: 0, O_NONBLOCK: 0 }
|
|
48
|
+
: options.noSafeFlags ? { O_RDONLY: 0 } : constants,
|
|
49
|
+
lstatSync(file) {
|
|
50
|
+
if (options.chainCalls) options.chainCalls.push(file);
|
|
51
|
+
if (file !== target) {
|
|
52
|
+
if (file === options.badAncestor) {
|
|
53
|
+
if (options.ancestorMissing) throw ioError("ENOENT");
|
|
54
|
+
return stat({ isFile: () => false, isDirectory: () => true,
|
|
55
|
+
isSymbolicLink: () => !options.ancestorIdentityOnly && (!options.ancestorSwap || stats > 0),
|
|
56
|
+
ino: options.ancestorSwap && stats > 0 ? 9 : 2 });
|
|
57
|
+
}
|
|
58
|
+
return stat({ isFile: () => false, isDirectory: () => true });
|
|
59
|
+
}
|
|
60
|
+
output.calls.push("lstat");
|
|
61
|
+
if (options.lstatError) throw options.lstatError;
|
|
62
|
+
return stat(options.before);
|
|
63
|
+
},
|
|
64
|
+
openSync(file, flags) {
|
|
65
|
+
output.calls.push("open");
|
|
66
|
+
assert.equal(file, target);
|
|
67
|
+
assert.equal(flags, platform === "win32" ? constants.O_RDONLY
|
|
68
|
+
: constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
69
|
+
if (options.openError) throw options.openError;
|
|
70
|
+
return 123;
|
|
71
|
+
},
|
|
72
|
+
fstatSync(fd) {
|
|
73
|
+
output.calls.push("fstat");
|
|
74
|
+
assert.equal(fd, 123);
|
|
75
|
+
stats += 1;
|
|
76
|
+
if (options.statError) throw options.statError;
|
|
77
|
+
return stat(stats === 1 ? options.opened : options.after);
|
|
78
|
+
},
|
|
79
|
+
readSync(fd, buffer, start, length, position) {
|
|
80
|
+
output.calls.push("read");
|
|
81
|
+
assert.equal(fd, 123);
|
|
82
|
+
assert.equal(position, null);
|
|
83
|
+
assert.equal(buffer.length, LIMIT + 1);
|
|
84
|
+
assert(start + length <= LIMIT + 1);
|
|
85
|
+
if (options.readError) throw options.readError;
|
|
86
|
+
const count = Math.min(length, bytes.length - offset, options.chunkSize || Infinity);
|
|
87
|
+
bytes.copy(buffer, start, offset, offset + count);
|
|
88
|
+
offset += count;
|
|
89
|
+
return count;
|
|
90
|
+
},
|
|
91
|
+
closeSync(fd) {
|
|
92
|
+
output.calls.push("close");
|
|
93
|
+
assert.equal(fd, 123);
|
|
94
|
+
if (options.closeError) throw options.closeError;
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
const loaderModule = { exports: {} };
|
|
98
|
+
vm.runInNewContext(loaderSource, {
|
|
99
|
+
Buffer, module: loaderModule, process: { platform },
|
|
100
|
+
require(name) {
|
|
101
|
+
if (name === "node:fs") return mockFs;
|
|
102
|
+
assert.equal(name, "node:path");
|
|
103
|
+
return pathApi;
|
|
104
|
+
},
|
|
105
|
+
}, { timeout: 5000 });
|
|
106
|
+
const module = { exports: {} };
|
|
107
|
+
const requireStub = (name) => {
|
|
108
|
+
if (name === "./config-file.js") {
|
|
109
|
+
if (options.noLoader) throw ioError("MODULE_NOT_FOUND");
|
|
110
|
+
return loaderModule.exports;
|
|
111
|
+
}
|
|
112
|
+
assert.equal(name, "./config-validator.js");
|
|
113
|
+
if (options.noValidator) throw ioError("MODULE_NOT_FOUND");
|
|
114
|
+
return validator;
|
|
115
|
+
};
|
|
116
|
+
requireStub.main = options.imported ? {} : module;
|
|
117
|
+
vm.runInNewContext(options.validatorCli ? validatorSource : source, {
|
|
118
|
+
Buffer, module, require: requireStub,
|
|
119
|
+
process: {
|
|
120
|
+
argv: ["node", readerPath, ...args],
|
|
121
|
+
stdout: { write(text) { output.stdout += text; } },
|
|
122
|
+
stderr: { write(text) { output.stderr += text; } },
|
|
123
|
+
get exitCode() { return output.exitCode; },
|
|
124
|
+
set exitCode(value) { output.exitCode = value; },
|
|
125
|
+
},
|
|
126
|
+
}, { filename: readerPath, timeout: 5000 });
|
|
127
|
+
assert(!`${output.stdout}${output.stderr}`.includes(SECRET), "secret leaked to CLI output");
|
|
128
|
+
return output;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function summary(content, options) {
|
|
132
|
+
const result = runCli(content, options);
|
|
133
|
+
assert.equal(result.exitCode, 0, result.stderr);
|
|
134
|
+
assert.equal(result.stderr, "");
|
|
135
|
+
return JSON.parse(result.stdout);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function rejected(content, options) {
|
|
139
|
+
const result = runCli(content, options);
|
|
140
|
+
assert.equal(result.exitCode, 1);
|
|
141
|
+
assert.equal(result.stdout, "");
|
|
142
|
+
assert(result.stderr.length > 0);
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
test("module exports readPreferences/summarize without filesystem or CLI side effects", () => {
|
|
147
|
+
assert.equal(typeof readPreferences, "function");
|
|
148
|
+
assert.deepEqual(Object.keys(require(readerPath)).sort(), ["readPreferences", "summarize"]);
|
|
149
|
+
assert.deepEqual(runCli(SECRET, { imported: true }), {
|
|
150
|
+
stdout: "", stderr: "", exitCode: 0, calls: [],
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("summary has an exact closed schema, without changing the validator API or accepted extensions", () => {
|
|
155
|
+
assert.deepEqual(Object.keys(validator).sort(), ["assertValidConfig", "validateConfig"]);
|
|
156
|
+
const config = {
|
|
157
|
+
name: SECRET, project: SECRET, [SECRET]: SECRET,
|
|
158
|
+
languagePreferences: {
|
|
159
|
+
communication: { normalized: "EN", raw: SECRET, [SECRET]: SECRET },
|
|
160
|
+
documents: { raw: "id", [SECRET]: SECRET },
|
|
161
|
+
},
|
|
162
|
+
developerPreferences: { workMode: "direct", scope: "fullstack", [SECRET]: SECRET },
|
|
163
|
+
brainstormPreferences: {
|
|
164
|
+
discussionMode: "all-at-once", recommendations: false, discoveryDepth: "quick", [SECRET]: SECRET,
|
|
165
|
+
},
|
|
166
|
+
codeReviewPreferences: { fixMode: "report-first", [SECRET]: SECRET },
|
|
167
|
+
additionalSkills: [{ name: SECRET, purpose: SECRET, paths: { [SECRET]: SECRET }, [SECRET]: SECRET }],
|
|
168
|
+
availableMCPs: [SECRET],
|
|
169
|
+
};
|
|
170
|
+
assert.deepEqual(validator.validateConfig(config), []);
|
|
171
|
+
assert.deepEqual(summary(JSON.stringify(config)), {
|
|
172
|
+
absent: false,
|
|
173
|
+
identity: { nameSet: true, projectSet: true },
|
|
174
|
+
languagePreferences: {
|
|
175
|
+
communication: { configured: true, effective: "english", source: "normalized" },
|
|
176
|
+
documents: { configured: true, effective: "indonesian", source: "raw" },
|
|
177
|
+
},
|
|
178
|
+
developerPreferences: {
|
|
179
|
+
workMode: { configured: true, value: "direct" },
|
|
180
|
+
scope: { configured: true, value: "fullstack" },
|
|
181
|
+
},
|
|
182
|
+
brainstormPreferences: {
|
|
183
|
+
discussionMode: { configured: true, value: "all-at-once" },
|
|
184
|
+
recommendations: { configured: true, value: false },
|
|
185
|
+
discoveryDepth: { configured: true, value: "quick" },
|
|
186
|
+
},
|
|
187
|
+
codeReviewPreferences: { fixMode: { configured: true, value: "report-first" } },
|
|
188
|
+
additionalSkills: { configured: true, count: 1 },
|
|
189
|
+
availableMCPs: { configured: true, count: 1, denied: false },
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("missing config is absent with independent unsaved defaults and no writes", () => {
|
|
194
|
+
const result = runCli("", { lstatError: ioError("ENOENT") });
|
|
195
|
+
const absent = JSON.parse(result.stdout);
|
|
196
|
+
assert.equal(result.exitCode, 0);
|
|
197
|
+
assert.equal(result.stderr, "");
|
|
198
|
+
assert.deepEqual(result.calls, ["lstat"]);
|
|
199
|
+
assert.deepEqual(absent, { ...summarize({}), absent: true });
|
|
200
|
+
assert.deepEqual(absent.languagePreferences, {
|
|
201
|
+
communication: { configured: false, effective: "indonesian", source: "default" },
|
|
202
|
+
documents: { configured: false, effective: "indonesian", source: "default" },
|
|
203
|
+
});
|
|
204
|
+
assert.deepEqual(absent.developerPreferences.scope, { configured: false, value: null });
|
|
205
|
+
assert.deepEqual(absent.availableMCPs, { configured: false, count: 0, denied: false });
|
|
206
|
+
assert.equal(summary("{}").absent, false);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("known and unknown free text never leaves the process, including names/paths/toolName", () => {
|
|
210
|
+
const skill = { name: SECRET, purpose: SECRET, toolName: SECRET, paths: { [SECRET]: SECRET } };
|
|
211
|
+
for (const key of ["path", "githubPath", "opencodePath", "claudePath", "cursorPath",
|
|
212
|
+
"windsurfPath", "geminiPath", "kiloPath", "kimiPath", "codexPath"]) skill[key] = SECRET;
|
|
213
|
+
const config = {
|
|
214
|
+
name: SECRET, project: SECRET, toolName: SECRET, path: SECRET, purpose: SECRET,
|
|
215
|
+
[SECRET]: { secret: SECRET },
|
|
216
|
+
languagePreferences: {
|
|
217
|
+
communication: { raw: SECRET, normalized: SECRET, [SECRET]: SECRET },
|
|
218
|
+
documents: { raw: "English", normalized: SECRET, [SECRET]: SECRET },
|
|
219
|
+
[SECRET]: SECRET,
|
|
220
|
+
},
|
|
221
|
+
developerPreferences: { scope: "backend", workMode: "plan-first", [SECRET]: SECRET },
|
|
222
|
+
brainstormPreferences: {
|
|
223
|
+
discussionMode: "three-at-a-time", recommendations: false, discoveryDepth: "critical", [SECRET]: SECRET,
|
|
224
|
+
},
|
|
225
|
+
codeReviewPreferences: { fixMode: "fix-then-report", [SECRET]: SECRET },
|
|
226
|
+
additionalSkills: [skill], availableMCPs: [SECRET],
|
|
227
|
+
};
|
|
228
|
+
const before = JSON.stringify(config);
|
|
229
|
+
const safe = summary(before);
|
|
230
|
+
assert.deepEqual(safe, summarize(config));
|
|
231
|
+
assert.equal(JSON.stringify(config), before);
|
|
232
|
+
assert(!JSON.stringify(safe).includes(SECRET));
|
|
233
|
+
assert.deepEqual(safe.identity, { nameSet: true, projectSet: true });
|
|
234
|
+
assert.deepEqual(safe.additionalSkills, { configured: true, count: 1 });
|
|
235
|
+
assert.deepEqual(safe.availableMCPs, { configured: true, count: 1, denied: false });
|
|
236
|
+
assert.deepEqual(safe.brainstormPreferences.recommendations, { configured: true, value: false });
|
|
237
|
+
assert.deepEqual(safe.languagePreferences.communication, {
|
|
238
|
+
configured: true, effective: "indonesian", source: "default",
|
|
239
|
+
});
|
|
240
|
+
assert.deepEqual(safe.languagePreferences.documents, { configured: true, effective: "english", source: "raw" });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("every finite preference is preserved, without inventing missing choices", () => {
|
|
244
|
+
const choices = {
|
|
245
|
+
developerPreferences: { scope: ["frontend", "backend", "fullstack"], workMode: ["direct", "plan-first"] },
|
|
246
|
+
brainstormPreferences: {
|
|
247
|
+
discussionMode: ["one-by-one", "three-at-a-time", "all-at-once"],
|
|
248
|
+
discoveryDepth: ["quick", "standard", "critical"], recommendations: [false, true],
|
|
249
|
+
},
|
|
250
|
+
codeReviewPreferences: { fixMode: ["report-first", "fix-then-report"] },
|
|
251
|
+
};
|
|
252
|
+
for (const [section, fields] of Object.entries(choices)) {
|
|
253
|
+
for (const [key, values] of Object.entries(fields)) {
|
|
254
|
+
assert.deepEqual(summarize({})[section][key], { configured: false, value: null });
|
|
255
|
+
for (const value of values) {
|
|
256
|
+
assert.deepEqual(summarize({ [section]: { [key]: value } })[section][key], { configured: true, value });
|
|
257
|
+
}
|
|
258
|
+
rejected(JSON.stringify({ [section]: { [key]: SECRET } }));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("exact language aliases normalize independently with normalized/raw/default precedence", () => {
|
|
264
|
+
const aliases = {
|
|
265
|
+
indonesian: ["indonesian", "id", "indo", "indonesia", "bahasa indonesia"],
|
|
266
|
+
english: ["english", "en", "eng", "inggris", "bahasa inggris"],
|
|
267
|
+
};
|
|
268
|
+
for (const [effective, values] of Object.entries(aliases)) {
|
|
269
|
+
for (const value of values) {
|
|
270
|
+
for (const source of ["normalized", "raw"]) {
|
|
271
|
+
const channel = { raw: SECRET, normalized: SECRET, [source]: ` ${value.toUpperCase()} ` };
|
|
272
|
+
const safe = summarize({ languagePreferences: { communication: channel, documents: {} } });
|
|
273
|
+
assert.deepEqual(safe.languagePreferences.communication, { configured: true, effective, source });
|
|
274
|
+
assert.equal(safe.languagePreferences.documents.effective, "indonesian");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const safe = summary(JSON.stringify({ languagePreferences: {
|
|
279
|
+
communication: { normalized: "EN", raw: "id" }, documents: { normalized: "", raw: "en" },
|
|
280
|
+
} }));
|
|
281
|
+
assert.equal(safe.languagePreferences.communication.source, "normalized");
|
|
282
|
+
assert.equal(safe.languagePreferences.communication.effective, "english");
|
|
283
|
+
assert.equal(safe.languagePreferences.documents.source, "raw");
|
|
284
|
+
for (const raw of ["", "en-US", "English please", SECRET]) {
|
|
285
|
+
assert.equal(summarize({ languagePreferences: { documents: { raw } } }).languagePreferences.documents.source, "default");
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("empty/none restrictions and false remain distinct from missing", () => {
|
|
290
|
+
for (const availableMCPs of [[], "none"]) {
|
|
291
|
+
const safe = summarize({ availableMCPs, additionalSkills: [], brainstormPreferences: { recommendations: false } });
|
|
292
|
+
assert.deepEqual(safe.availableMCPs, { configured: true, count: 0, denied: true });
|
|
293
|
+
assert.deepEqual(safe.additionalSkills, { configured: true, count: 0 });
|
|
294
|
+
assert.deepEqual(safe.brainstormPreferences.recommendations, { configured: true, value: false });
|
|
295
|
+
}
|
|
296
|
+
assert.deepEqual(summarize({ availableMCPs: ["none"] }).availableMCPs, { configured: true, count: 1, denied: false });
|
|
297
|
+
assert.deepEqual(summarize({ name: "", project: " " }).identity, { nameSet: false, projectSet: false });
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("extensions/prototype keys are accepted and accessors/inherited preferences never run", () => {
|
|
301
|
+
const config = JSON.parse(`{"__proto__":{"name":"${SECRET}"},"constructor":"${SECRET}","tests":{"secret":"${SECRET}"}}`);
|
|
302
|
+
Object.defineProperty(config, "unknown", { get() { throw new Error(SECRET); } });
|
|
303
|
+
assert.deepEqual(summarize(config), summarize({}));
|
|
304
|
+
const inherited = Object.create({ developerPreferences: { scope: "backend" }, name: SECRET });
|
|
305
|
+
assert.deepEqual(summarize(inherited), summarize({}));
|
|
306
|
+
assert.throws(() => summarize({ get name() { throw new Error(SECRET); } }), /name: must be a data property/);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("malformed/schema errors use validator diagnostics without parser or value excerpts", () => {
|
|
310
|
+
for (const content of [SECRET, `{"name":"${SECRET}", broken}`, "null", "[]",
|
|
311
|
+
JSON.stringify({ name: { [SECRET]: SECRET } }),
|
|
312
|
+
JSON.stringify({ additionalSkills: [{ name: SECRET, paths: { [SECRET]: false } }] }),
|
|
313
|
+
JSON.stringify({ availableMCPs: [null, SECRET] }),
|
|
314
|
+
]) {
|
|
315
|
+
const result = rejected(content);
|
|
316
|
+
assert.equal(result.calls.at(-1), "close");
|
|
317
|
+
assert.match(result.stderr, /Invalid config/);
|
|
318
|
+
}
|
|
319
|
+
assert.match(rejected(SECRET).stderr, /\$: must contain valid JSON/);
|
|
320
|
+
assert.match(rejected(JSON.stringify({ additionalSkills: [{ name: SECRET, paths: { [SECRET]: false } }] })).stderr,
|
|
321
|
+
/additionalSkills\[0\]\.paths\[0\]: must be a string/);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("symlinks, directories, FIFOs and devices are rejected before open/read", () => {
|
|
325
|
+
for (const before of [
|
|
326
|
+
{ isFile: () => false, isSymbolicLink: () => true },
|
|
327
|
+
{ isFile: () => true, isSymbolicLink: () => true },
|
|
328
|
+
...["directory", "fifo", "device"].map(() => ({ isFile: () => false })),
|
|
329
|
+
]) {
|
|
330
|
+
const result = rejected(SECRET, { before });
|
|
331
|
+
assert.deepEqual(result.calls, ["lstat"]);
|
|
332
|
+
assert.match(result.stderr, /regular, non-symlink|symlinks or junctions/);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("raced symlinks/FIFOs and changed identity are rejected without reading", () => {
|
|
337
|
+
assert.deepEqual(rejected(SECRET, { openError: ioError("ELOOP") }).calls, ["lstat", "open"]);
|
|
338
|
+
for (const opened of [{ isFile: () => false }, { ino: 3 }, { dev: 2 }]) {
|
|
339
|
+
assert.deepEqual(rejected(SECRET, { opened }).calls, ["lstat", "open", "fstat", "close"]);
|
|
340
|
+
}
|
|
341
|
+
assert.deepEqual(rejected(SECRET, { noSafeFlags: true }).calls, ["lstat"]);
|
|
342
|
+
assert.deepEqual(rejected(SECRET, { zeroSafeFlags: true }).calls, ["lstat"]);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("byte cap covers oversized files, growth and short reads; exact 1 MiB is accepted", () => {
|
|
346
|
+
assert.deepEqual(rejected("{}", { before: { size: LIMIT + 1 } }).calls, ["lstat"]);
|
|
347
|
+
assert.deepEqual(rejected("{}", { opened: { size: LIMIT + 1 } }).calls, ["lstat", "open", "fstat", "close"]);
|
|
348
|
+
assert.equal(summary("{}" + " ".repeat(LIMIT - 2), { chunkSize: 65536 }).absent, false);
|
|
349
|
+
const grown = rejected("{}" + " ".repeat(LIMIT), { before: { size: 2 }, opened: { size: 2 } });
|
|
350
|
+
assert.match(grown.stderr, /1 MiB/);
|
|
351
|
+
assert.equal(grown.calls.at(-1), "close");
|
|
352
|
+
assert.equal(summary('{"name":"private"}', { chunkSize: 1 }).identity.nameSet, true);
|
|
353
|
+
for (const after of [{ size: 3 }, { mtimeMs: 11 }, { ctimeMs: 11 }]) {
|
|
354
|
+
assert.match(rejected("{}", { after }).stderr, /changed during reading/);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("I/O, usage and missing validator errors never echo raw paths or exceptions", () => {
|
|
359
|
+
for (const options of [
|
|
360
|
+
{ lstatError: ioError("EACCES") }, { openError: ioError("ENOENT") },
|
|
361
|
+
{ statError: ioError("EIO") }, { readError: ioError("EIO") }, { closeError: ioError("EIO") },
|
|
362
|
+
]) rejected(SECRET, options);
|
|
363
|
+
assert.match(rejected("{}", { noValidator: true }).stderr, /validation unavailable/);
|
|
364
|
+
rejected("", { noValidator: true, lstatError: ioError("ENOENT") });
|
|
365
|
+
for (const args of [[], [SECRET, SECRET]]) {
|
|
366
|
+
const result = rejected("", { args });
|
|
367
|
+
assert.deepEqual(result.calls, []);
|
|
368
|
+
assert.match(result.stderr, /^Usage:/);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("real CLI is read-only, preserves file mode and leaves missing parent absent", () => {
|
|
373
|
+
const packagePath = fs.realpathSync(path.resolve(__dirname, "../package.json"));
|
|
374
|
+
const before = fs.readFileSync(packagePath);
|
|
375
|
+
const mode = fs.statSync(packagePath).mode;
|
|
376
|
+
const result = spawnSync(process.execPath, [readerPath, packagePath], { encoding: "utf8", timeout: 10000 });
|
|
377
|
+
assert.ifError(result.error);
|
|
378
|
+
assert.equal(result.status, 0, result.stderr);
|
|
379
|
+
assert.equal(result.stderr, "");
|
|
380
|
+
assert.equal(JSON.parse(result.stdout).identity.nameSet, true);
|
|
381
|
+
assert(!result.stdout.includes("macca-method"));
|
|
382
|
+
assert.deepEqual(readPreferences(packagePath), JSON.parse(result.stdout));
|
|
383
|
+
assert.deepEqual(fs.readFileSync(packagePath), before);
|
|
384
|
+
assert.equal(fs.statSync(packagePath).mode, mode);
|
|
385
|
+
const missingParent = path.join(fs.realpathSync(__dirname), "__macca_preferences_missing_fixture__");
|
|
386
|
+
assert.equal(fs.existsSync(missingParent), false);
|
|
387
|
+
const missing = spawnSync(process.execPath, [readerPath, path.join(missingParent, "config.json")], {
|
|
388
|
+
encoding: "utf8", timeout: 10000,
|
|
389
|
+
});
|
|
390
|
+
assert.ifError(missing.error);
|
|
391
|
+
assert.equal(missing.status, 0);
|
|
392
|
+
assert.equal(JSON.parse(missing.stdout).absent, true);
|
|
393
|
+
assert.equal(fs.existsSync(missingParent), false);
|
|
394
|
+
const usage = spawnSync(process.execPath, [readerPath], { encoding: "utf8", timeout: 10000 });
|
|
395
|
+
assert.ifError(usage.error);
|
|
396
|
+
assert.equal(usage.status, 1);
|
|
397
|
+
assert.match(usage.stderr, /^Usage:/);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("both CLIs reject ancestor links/junctions, including dangling links, before opening", () => {
|
|
401
|
+
for (const validatorCli of [false, true]) {
|
|
402
|
+
for (const platform of ["linux", "win32"]) {
|
|
403
|
+
const badAncestor = platform === "win32" ? "C:\\workspace\\.agents" : "/workspace/.agents";
|
|
404
|
+
const chainCalls = [];
|
|
405
|
+
// A dangling ancestor is still a symlink at lstat; its missing target
|
|
406
|
+
// must never be reached (or misreported as absent preferences).
|
|
407
|
+
for (const lstatError of [undefined, ioError("ENOENT")]) {
|
|
408
|
+
const result = rejected(SECRET, { validatorCli, platform, badAncestor, lstatError, chainCalls });
|
|
409
|
+
assert.deepEqual(result.calls, []);
|
|
410
|
+
assert.equal(chainCalls.at(-1), badAncestor);
|
|
411
|
+
assert.match(result.stderr, /symlinks or junctions/);
|
|
412
|
+
}
|
|
413
|
+
const raced = rejected(SECRET, { validatorCli, platform, badAncestor, ancestorSwap: true });
|
|
414
|
+
assert(!raced.calls.includes("read"));
|
|
415
|
+
assert.equal(raced.calls.at(-1), "close");
|
|
416
|
+
const replaced = rejected(SECRET, {
|
|
417
|
+
validatorCli, platform, badAncestor, ancestorSwap: true, ancestorIdentityOnly: true,
|
|
418
|
+
});
|
|
419
|
+
assert(!replaced.calls.includes("read"));
|
|
420
|
+
assert.match(replaced.stderr, /path changed/);
|
|
421
|
+
const missing = runCli("", { validatorCli, platform, badAncestor, ancestorMissing: true });
|
|
422
|
+
assert.equal(missing.exitCode, validatorCli ? 1 : 0);
|
|
423
|
+
if (!validatorCli) assert.equal(JSON.parse(missing.stdout).absent, true);
|
|
424
|
+
assert.deepEqual(missing.calls, []);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("Windows local paths work without POSIX flags; namespaces/devices/ADS are denied before I/O", () => {
|
|
430
|
+
for (const validatorCli of [false, true]) {
|
|
431
|
+
for (const flags of [{ noSafeFlags: true }, { zeroSafeFlags: true }]) {
|
|
432
|
+
const result = runCli("{}", { validatorCli, platform: "win32", ...flags });
|
|
433
|
+
assert.equal(result.exitCode, 0, result.stderr);
|
|
434
|
+
assert.equal(result.calls.at(-1), "close");
|
|
435
|
+
}
|
|
436
|
+
for (const file of ["\\\\server\\share\\config.json", "\\\\?\\C:\\config.json", "\\\\.\\pipe\\config",
|
|
437
|
+
"C:config.json", "C:\\NUL", "C:\\workspace\\CON.txt", "C:\\workspace\\config.json:stream",
|
|
438
|
+
"C:\\workspace.\\config.json", "C:\\workspace\\COM¹.txt"]) {
|
|
439
|
+
const chainCalls = [];
|
|
440
|
+
const result = rejected(SECRET, { validatorCli, platform: "win32", args: [file], chainCalls });
|
|
441
|
+
assert.deepEqual(result.calls, []);
|
|
442
|
+
assert.deepEqual(chainCalls, []);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("validator CLI shares bounded descriptor reads and rejects FIFO/oversize/unsafe flags", () => {
|
|
448
|
+
for (const options of [
|
|
449
|
+
{ before: { isFile: () => false } }, { before: { size: LIMIT + 1 } },
|
|
450
|
+
{ noSafeFlags: true }, { zeroSafeFlags: true },
|
|
451
|
+
]) {
|
|
452
|
+
const result = rejected(SECRET, { validatorCli: true, ...options });
|
|
453
|
+
assert(!result.calls.includes("open"));
|
|
454
|
+
assert(!result.calls.includes("read"));
|
|
455
|
+
}
|
|
456
|
+
for (const options of [{ opened: { isFile: () => false } }, { opened: { ino: 4 } }]) {
|
|
457
|
+
const result = rejected(SECRET, { validatorCli: true, ...options });
|
|
458
|
+
assert(!result.calls.includes("read"));
|
|
459
|
+
assert.equal(result.calls.at(-1), "close");
|
|
460
|
+
}
|
|
461
|
+
assert.match(rejected("{}" + " ".repeat(LIMIT), {
|
|
462
|
+
validatorCli: true, before: { size: 2 }, opened: { size: 2 },
|
|
463
|
+
}).stderr, /1 MiB/);
|
|
464
|
+
const exact = runCli("{}" + " ".repeat(LIMIT - 2), { validatorCli: true, chunkSize: 65536 });
|
|
465
|
+
assert.equal(exact.exitCode, 0);
|
|
466
|
+
assert.equal(exact.stdout, "Valid config.\n");
|
|
467
|
+
for (const content of [SECRET, JSON.stringify({ developerPreferences: { scope: SECRET } })]) {
|
|
468
|
+
rejected(content, { validatorCli: true });
|
|
469
|
+
}
|
|
470
|
+
for (const validatorCli of [false, true]) {
|
|
471
|
+
assert.match(rejected("{}", { validatorCli, noLoader: true }).stderr, /loader unavailable/);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
process.stdout.write(`\n${passed} preference reader tests passed.\n`);
|
|
@@ -6,6 +6,7 @@ const fs = require("node:fs");
|
|
|
6
6
|
const os = require("node:os");
|
|
7
7
|
const path = require("node:path");
|
|
8
8
|
const { execFileSync } = require("node:child_process");
|
|
9
|
+
const { resolveNpmCommand } = require("./lib/npm-command.js");
|
|
9
10
|
|
|
10
11
|
const rootDir = path.resolve(__dirname, "..");
|
|
11
12
|
const temporaryRoot = fs.mkdtempSync(
|
|
@@ -14,14 +15,6 @@ const temporaryRoot = fs.mkdtempSync(
|
|
|
14
15
|
const projectDir = path.join(temporaryRoot, "project");
|
|
15
16
|
const obsoleteMeetingName = Buffer.from("cmFwYXQ=", "base64").toString("utf8");
|
|
16
17
|
|
|
17
|
-
function commandName(base) {
|
|
18
|
-
if (process.platform !== "win32") {
|
|
19
|
-
return base;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return base === "npm" || base === "npx" ? `${base}.cmd` : base;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
18
|
function resolveTempRoot() {
|
|
26
19
|
const tempRoot = os.tmpdir();
|
|
27
20
|
try {
|
|
@@ -32,22 +25,13 @@ function resolveTempRoot() {
|
|
|
32
25
|
}
|
|
33
26
|
|
|
34
27
|
function captureNpm(args, options = {}) {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
return capture(process.execPath, [npmExecPath, ...args], options);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return capture(commandName("npm"), args, options);
|
|
28
|
+
const npm = resolveNpmCommand(args);
|
|
29
|
+
return capture(npm.command, npm.args, options);
|
|
41
30
|
}
|
|
42
31
|
|
|
43
32
|
function runNpm(args, options = {}) {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
run(process.execPath, [npmExecPath, ...args], options);
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
run(commandName("npm"), args, options);
|
|
33
|
+
const npm = resolveNpmCommand(args);
|
|
34
|
+
run(npm.command, npm.args, options);
|
|
51
35
|
}
|
|
52
36
|
|
|
53
37
|
function run(command, args, options = {}) {
|
|
@@ -55,6 +39,7 @@ function run(command, args, options = {}) {
|
|
|
55
39
|
cwd: rootDir,
|
|
56
40
|
stdio: "inherit",
|
|
57
41
|
...options,
|
|
42
|
+
shell: false,
|
|
58
43
|
});
|
|
59
44
|
}
|
|
60
45
|
|
|
@@ -63,6 +48,7 @@ function capture(command, args, options = {}) {
|
|
|
63
48
|
cwd: rootDir,
|
|
64
49
|
encoding: "utf8",
|
|
65
50
|
...options,
|
|
51
|
+
shell: false,
|
|
66
52
|
});
|
|
67
53
|
}
|
|
68
54
|
|
|
@@ -83,6 +69,7 @@ try {
|
|
|
83
69
|
"pack",
|
|
84
70
|
"macca-method@1.1.0",
|
|
85
71
|
"--json",
|
|
72
|
+
"--ignore-scripts",
|
|
86
73
|
"--pack-destination",
|
|
87
74
|
temporaryRoot,
|
|
88
75
|
]);
|
|
@@ -121,6 +108,7 @@ try {
|
|
|
121
108
|
const packOutput = captureNpm([
|
|
122
109
|
"pack",
|
|
123
110
|
"--json",
|
|
111
|
+
"--ignore-scripts",
|
|
124
112
|
"--pack-destination",
|
|
125
113
|
temporaryRoot,
|
|
126
114
|
]);
|
|
@@ -142,6 +130,8 @@ try {
|
|
|
142
130
|
const currentRoot = path.join(projectDir, ".opencode", "skills");
|
|
143
131
|
assertExists(path.join(currentRoot, "meet", "SKILL.md"));
|
|
144
132
|
assertExists(path.join(currentRoot, "release-readiness", "SKILL.md"));
|
|
133
|
+
assertExists(path.join(currentRoot, "setup-macca-method", "SKILL.md"));
|
|
134
|
+
assertExists(path.join(currentRoot, "_shared", "scripts", "config-validator.js"));
|
|
145
135
|
assertMissing(path.join(legacyRoot, obsoleteMeetingName));
|
|
146
136
|
|
|
147
137
|
const config = JSON.parse(
|