@massa-ai/codex-plugin 1.27.0 → 1.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/agent-profiles/balanced/massa-ai-judge.toml +2 -2
- package/agent-profiles/cheap/massa-ai-judge.toml +2 -2
- package/agent-profiles/heavy/massa-ai-judge.toml +2 -2
- package/agent-profiles/home/massa-ai-judge.toml +2 -2
- package/agent-profiles/work/massa-ai-judge.toml +2 -2
- package/agents/massa-ai-judge.toml +2 -2
- package/install.sh +32 -0
- package/package.json +1 -1
- package/skills/agents/judge/SKILL.md +3 -3
- package/skills/massa-ai/SKILL.md +8 -3
- package/skills/massa-ai/references/agent-orchestration.md +1 -1
- package/skills/massa-ai/references/coding-guidelines.md +67 -0
- package/skills/massa-ai/references/installation.md +3 -3
- package/skills/massa-ai/references/mcp-tools.md +3 -1
- package/skills/massa-ai/references/skill-architect/examples.md +256 -0
- package/skills/massa-ai/references/skill-architect/patterns.md +317 -0
- package/skills/massa-ai/references/skill-architect/quality-checklist.md +70 -0
- package/skills/massa-ai/scripts/validate_skill.ts +364 -0
- package/skills/massa-ai/workflows/pr-review.md +246 -0
- package/skills/massa-ai/workflows/skill-architect.md +393 -0
- package/skills/massa-ai/workflows/to-prd.md +81 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Validate a skill folder against Skill Architect requirements.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bun scripts/validate_skill.ts <path-to-skill-folder>
|
|
7
|
+
* bun scripts/validate_skill.ts <path-to-skill-folder> --format json
|
|
8
|
+
* bun scripts/validate_skill.ts <path-to-skill-folder> --json-out /tmp/skill-report.json
|
|
9
|
+
*
|
|
10
|
+
* Exit codes:
|
|
11
|
+
* 0 = pass (warnings allowed)
|
|
12
|
+
* 1 = fail (at least one error)
|
|
13
|
+
*
|
|
14
|
+
* Token-efficient workflow: run once with --json-out, then reuse the saved
|
|
15
|
+
* JSON for feedback/review without re-running validation.
|
|
16
|
+
*
|
|
17
|
+
* TypeScript port of the former validate_skill.py (Skill Architect,
|
|
18
|
+
* Useful-Agent-Skills). Frontmatter is parsed with Bun's built-in real YAML
|
|
19
|
+
* parser (`Bun.YAML.parse`) — same precedent as
|
|
20
|
+
* scripts/__tests__/workflow-bun-cache.test.ts — so the Python version's
|
|
21
|
+
* PyYAML/stdlib fallback split is gone.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
25
|
+
import path from "path";
|
|
26
|
+
|
|
27
|
+
type Severity = "error" | "warning";
|
|
28
|
+
|
|
29
|
+
interface Check {
|
|
30
|
+
name: string;
|
|
31
|
+
passed: boolean;
|
|
32
|
+
message: string;
|
|
33
|
+
severity: Severity;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface Results {
|
|
37
|
+
path: string;
|
|
38
|
+
checks: Check[];
|
|
39
|
+
passed: number;
|
|
40
|
+
failed: number;
|
|
41
|
+
warnings: number;
|
|
42
|
+
parser_mode: string;
|
|
43
|
+
next_steps: string[];
|
|
44
|
+
summary?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const KEBAB_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
48
|
+
const FRONTMATTER_RE = /^---\s*\n([\s\S]*?)\n---\s*\n/;
|
|
49
|
+
|
|
50
|
+
export function validateSkill(skillPath: string): Results {
|
|
51
|
+
const results: Results = {
|
|
52
|
+
path: skillPath,
|
|
53
|
+
checks: [],
|
|
54
|
+
passed: 0,
|
|
55
|
+
failed: 0,
|
|
56
|
+
warnings: 0,
|
|
57
|
+
parser_mode: "unknown",
|
|
58
|
+
next_steps: [],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const addCheck = (name: string, passed: boolean, message: string, severity: Severity = "error") => {
|
|
62
|
+
results.checks.push({ name, passed, message, severity });
|
|
63
|
+
if (passed) results.passed += 1;
|
|
64
|
+
else if (severity === "warning") results.warnings += 1;
|
|
65
|
+
else results.failed += 1;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// --- Check 1: Folder exists ---
|
|
69
|
+
if (!existsSync(skillPath) || !statSync(skillPath).isDirectory()) {
|
|
70
|
+
addCheck("folder_exists", false, `Path is not a directory: ${skillPath}`);
|
|
71
|
+
results.summary = "FAIL — folder not found";
|
|
72
|
+
return results;
|
|
73
|
+
}
|
|
74
|
+
addCheck("folder_exists", true, "Skill folder exists");
|
|
75
|
+
|
|
76
|
+
// --- Check 2: Folder name is kebab-case ---
|
|
77
|
+
const folderName = path.basename(path.normalize(skillPath));
|
|
78
|
+
const isKebab = KEBAB_RE.test(folderName);
|
|
79
|
+
addCheck("folder_kebab_case", isKebab, `Folder name '${folderName}' ${isKebab ? "is" : "is NOT"} kebab-case`);
|
|
80
|
+
|
|
81
|
+
// --- Check 3: SKILL.md exists (exact casing) ---
|
|
82
|
+
const entries = readdirSync(skillPath);
|
|
83
|
+
const hasSkillMd = entries.includes("SKILL.md");
|
|
84
|
+
addCheck("skill_md_exists", hasSkillMd, hasSkillMd ? "SKILL.md exists" : "SKILL.md not found (case-sensitive)");
|
|
85
|
+
|
|
86
|
+
const wrongCasings = entries.filter((e) => e.toLowerCase() === "skill.md" && e !== "SKILL.md");
|
|
87
|
+
if (wrongCasings.length > 0) {
|
|
88
|
+
addCheck("skill_md_casing", false, `Found wrong casing: ${wrongCasings[0]} (must be exactly SKILL.md)`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!hasSkillMd) {
|
|
92
|
+
results.summary = "FAIL — SKILL.md not found";
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- Check 4: No README.md ---
|
|
97
|
+
const hasReadme = entries.some((e) => e.toLowerCase() === "readme.md");
|
|
98
|
+
addCheck(
|
|
99
|
+
"no_readme",
|
|
100
|
+
!hasReadme,
|
|
101
|
+
hasReadme ? "README.md found — remove it (skills are for agents, not humans)" : "No README.md in skill folder",
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// --- Check 5: Parse frontmatter ---
|
|
105
|
+
const content = readFileSync(path.join(skillPath, "SKILL.md"), "utf8");
|
|
106
|
+
const fmMatch = FRONTMATTER_RE.exec(content);
|
|
107
|
+
if (!fmMatch) {
|
|
108
|
+
addCheck("frontmatter_delimiters", false, "Missing or malformed --- delimiters in frontmatter");
|
|
109
|
+
results.summary = "FAIL — frontmatter parse error";
|
|
110
|
+
return results;
|
|
111
|
+
}
|
|
112
|
+
addCheck("frontmatter_delimiters", true, "YAML frontmatter delimiters present");
|
|
113
|
+
|
|
114
|
+
let fm: Record<string, unknown>;
|
|
115
|
+
try {
|
|
116
|
+
const parsed = Bun.YAML.parse(fmMatch[1]);
|
|
117
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
118
|
+
throw new Error("Frontmatter is not a YAML mapping");
|
|
119
|
+
}
|
|
120
|
+
fm = parsed as Record<string, unknown>;
|
|
121
|
+
results.parser_mode = "bun-yaml";
|
|
122
|
+
addCheck("frontmatter_valid_yaml", true, "Frontmatter is valid YAML (parsed with Bun.YAML)");
|
|
123
|
+
} catch (e) {
|
|
124
|
+
addCheck("frontmatter_valid_yaml", false, `YAML parse error: ${e instanceof Error ? e.message : String(e)}`);
|
|
125
|
+
results.summary = "FAIL — YAML parse error";
|
|
126
|
+
return results;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// --- Check 6: name field ---
|
|
130
|
+
const name = fm.name;
|
|
131
|
+
if (!name) {
|
|
132
|
+
addCheck("name_present", false, "Missing 'name' field in frontmatter");
|
|
133
|
+
} else {
|
|
134
|
+
addCheck("name_present", true, `name: ${name}`);
|
|
135
|
+
const isNameKebab = KEBAB_RE.test(String(name));
|
|
136
|
+
addCheck("name_kebab_case", isNameKebab, `name '${name}' ${isNameKebab ? "is" : "is NOT"} kebab-case`);
|
|
137
|
+
|
|
138
|
+
const nameLower = String(name).toLowerCase();
|
|
139
|
+
const hasReserved = nameLower.includes("claude") || nameLower.includes("anthropic");
|
|
140
|
+
addCheck(
|
|
141
|
+
"name_not_reserved",
|
|
142
|
+
!hasReserved,
|
|
143
|
+
hasReserved ? "Name contains 'claude' or 'anthropic' (reserved)" : "Name does not use reserved terms",
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const namesMatch = String(name) === folderName;
|
|
147
|
+
addCheck(
|
|
148
|
+
"name_matches_folder",
|
|
149
|
+
namesMatch,
|
|
150
|
+
namesMatch
|
|
151
|
+
? `name '${name}' matches folder '${folderName}'`
|
|
152
|
+
: `name '${name}' does NOT match folder '${folderName}'`,
|
|
153
|
+
"warning",
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// --- Check 7: description field ---
|
|
158
|
+
const desc = fm.description;
|
|
159
|
+
if (!desc) {
|
|
160
|
+
addCheck("description_present", false, "Missing 'description' field in frontmatter");
|
|
161
|
+
} else {
|
|
162
|
+
const descStr = String(desc).trim();
|
|
163
|
+
addCheck("description_present", true, `description present (${descStr.length} chars)`);
|
|
164
|
+
|
|
165
|
+
addCheck("description_length", descStr.length <= 1024, `Description length: ${descStr.length}/1024 chars`);
|
|
166
|
+
|
|
167
|
+
const hasXml = descStr.includes("<") || descStr.includes(">");
|
|
168
|
+
addCheck(
|
|
169
|
+
"description_no_xml",
|
|
170
|
+
!hasXml,
|
|
171
|
+
hasXml ? "XML angle brackets found in description (forbidden)" : "No XML brackets in description",
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const triggerKeywords = ["use when", "use for", "use this", "trigger", "ask for", "asks to", "says", "mentions"];
|
|
175
|
+
const descLower = descStr.toLowerCase();
|
|
176
|
+
const hasTriggers = triggerKeywords.some((kw) => descLower.includes(kw));
|
|
177
|
+
addCheck(
|
|
178
|
+
"description_has_triggers",
|
|
179
|
+
hasTriggers,
|
|
180
|
+
hasTriggers
|
|
181
|
+
? "Description includes trigger guidance"
|
|
182
|
+
: "Missing trigger phrases — add 'Use when...' guidance (mandatory per CONTRIBUTING.md)",
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const negativeKeywords = ["do not use", "don't use", "not for", "not intended for"];
|
|
186
|
+
const hasNegativeScope = negativeKeywords.some((kw) => descLower.includes(kw));
|
|
187
|
+
addCheck(
|
|
188
|
+
"description_has_negative_scope",
|
|
189
|
+
hasNegativeScope,
|
|
190
|
+
hasNegativeScope
|
|
191
|
+
? "Description includes negative scope"
|
|
192
|
+
: "Missing negative scope — add 'Do NOT use for...' guidance (mandatory per CONTRIBUTING.md)",
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// --- Check 7b: metadata field ---
|
|
197
|
+
const metadata = fm.metadata;
|
|
198
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
199
|
+
addCheck(
|
|
200
|
+
"metadata_present",
|
|
201
|
+
false,
|
|
202
|
+
"Missing 'metadata' field in frontmatter (expected metadata.version and metadata.author)",
|
|
203
|
+
"warning",
|
|
204
|
+
);
|
|
205
|
+
} else {
|
|
206
|
+
addCheck("metadata_present", true, "metadata field present");
|
|
207
|
+
const meta = metadata as Record<string, unknown>;
|
|
208
|
+
|
|
209
|
+
const metaVersion = meta.version;
|
|
210
|
+
addCheck(
|
|
211
|
+
"metadata_version",
|
|
212
|
+
Boolean(metaVersion),
|
|
213
|
+
metaVersion ? `metadata.version: ${metaVersion}` : "Missing metadata.version",
|
|
214
|
+
"warning",
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const metaAuthor = meta.author;
|
|
218
|
+
addCheck(
|
|
219
|
+
"metadata_author",
|
|
220
|
+
Boolean(metaAuthor),
|
|
221
|
+
metaAuthor ? `metadata.author: ${metaAuthor}` : "Missing metadata.author",
|
|
222
|
+
"warning",
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// --- Check 8: Body content ---
|
|
227
|
+
const body = content.slice(fmMatch[0].length);
|
|
228
|
+
const lineCount = body.trim().split("\n").length;
|
|
229
|
+
addCheck(
|
|
230
|
+
"body_line_count",
|
|
231
|
+
lineCount <= 500,
|
|
232
|
+
`SKILL.md body: ${lineCount} lines ${lineCount <= 500 ? "(good)" : "(consider moving content to references/)"}`,
|
|
233
|
+
lineCount > 500 ? "warning" : "error",
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const hasExamples = /(example|user says|result:)/i.test(body);
|
|
237
|
+
addCheck(
|
|
238
|
+
"body_has_examples",
|
|
239
|
+
hasExamples,
|
|
240
|
+
hasExamples ? "Instructions include examples" : "Consider adding usage examples",
|
|
241
|
+
"warning",
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const hasErrorHandling = /(error|fail|troubleshoot|issue|problem|if.*fails)/i.test(body);
|
|
245
|
+
addCheck(
|
|
246
|
+
"body_has_error_handling",
|
|
247
|
+
hasErrorHandling,
|
|
248
|
+
hasErrorHandling ? "Instructions include error handling" : "Consider adding error handling guidance",
|
|
249
|
+
"warning",
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// --- Check 9: Optional files ---
|
|
253
|
+
const refsDir = path.join(skillPath, "references");
|
|
254
|
+
if (entries.includes("references") && existsSync(refsDir) && statSync(refsDir).isDirectory()) {
|
|
255
|
+
for (const ref of readdirSync(refsDir)) {
|
|
256
|
+
const refMentioned = body.includes(ref) || body.includes(`references/${ref}`);
|
|
257
|
+
addCheck(
|
|
258
|
+
`ref_linked_${ref}`,
|
|
259
|
+
refMentioned,
|
|
260
|
+
refMentioned
|
|
261
|
+
? `references/${ref} is referenced in SKILL.md`
|
|
262
|
+
: `references/${ref} exists but is not referenced in SKILL.md`,
|
|
263
|
+
"warning",
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// --- Summary ---
|
|
269
|
+
if (results.failed === 0) {
|
|
270
|
+
results.summary =
|
|
271
|
+
`PASS — ${results.passed} checks passed` + (results.warnings > 0 ? `, ${results.warnings} warnings` : "");
|
|
272
|
+
} else {
|
|
273
|
+
results.summary = `FAIL — ${results.failed} errors, ${results.warnings} warnings`;
|
|
274
|
+
results.next_steps = results.checks
|
|
275
|
+
.filter((c) => !c.passed && c.severity === "error")
|
|
276
|
+
.map((c) => `Fix check '${c.name}': ${c.message}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return results;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function printReport(results: Results, verbose: boolean): void {
|
|
283
|
+
const bar = "=".repeat(60);
|
|
284
|
+
const line = "─".repeat(60);
|
|
285
|
+
console.log(`\n${bar}`);
|
|
286
|
+
console.log(" Skill Validation Report");
|
|
287
|
+
console.log(` Path: ${results.path}`);
|
|
288
|
+
console.log(` Parser: ${results.parser_mode}`);
|
|
289
|
+
console.log(`${bar}\n`);
|
|
290
|
+
|
|
291
|
+
for (const check of results.checks) {
|
|
292
|
+
if (check.passed && !verbose) continue;
|
|
293
|
+
const icon = check.passed ? "✅" : check.severity === "warning" ? "⚠️" : "❌";
|
|
294
|
+
console.log(` ${icon} ${check.name}: ${check.message}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
console.log(`\n${line}`);
|
|
298
|
+
console.log(` ${results.summary}`);
|
|
299
|
+
console.log(` Passed: ${results.passed} | Failed: ${results.failed} | Warnings: ${results.warnings}`);
|
|
300
|
+
console.log(`${line}\n`);
|
|
301
|
+
|
|
302
|
+
if (results.next_steps.length > 0) {
|
|
303
|
+
console.log(" Next steps:");
|
|
304
|
+
results.next_steps.forEach((step, i) => console.log(` ${i + 1}. ${step}`));
|
|
305
|
+
console.log("");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (import.meta.main) {
|
|
310
|
+
const args = process.argv.slice(2);
|
|
311
|
+
let skillPath: string | undefined;
|
|
312
|
+
let format: "human" | "json" | "both" = "human";
|
|
313
|
+
let verbose = false;
|
|
314
|
+
let prettyJson = false;
|
|
315
|
+
let jsonOut: string | undefined;
|
|
316
|
+
|
|
317
|
+
for (let i = 0; i < args.length; i++) {
|
|
318
|
+
const a = args[i]!;
|
|
319
|
+
if (a === "--format") {
|
|
320
|
+
const v = args[++i];
|
|
321
|
+
if (v !== "human" && v !== "json" && v !== "both") {
|
|
322
|
+
console.error(`Invalid --format: ${v} (choose human|json|both)`);
|
|
323
|
+
process.exit(2);
|
|
324
|
+
}
|
|
325
|
+
format = v;
|
|
326
|
+
} else if (a === "--verbose") verbose = true;
|
|
327
|
+
else if (a === "--pretty-json") prettyJson = true;
|
|
328
|
+
else if (a === "--json-out") jsonOut = args[++i];
|
|
329
|
+
else if (a === "-h" || a === "--help") {
|
|
330
|
+
console.log(
|
|
331
|
+
"Usage: bun scripts/validate_skill.ts <path> [--format human|json|both] [--verbose] [--pretty-json] [--json-out FILE]\n" +
|
|
332
|
+
"Tip: use --json-out FILE to save full results and avoid re-running for later feedback.",
|
|
333
|
+
);
|
|
334
|
+
process.exit(0);
|
|
335
|
+
} else if (!a.startsWith("-") && skillPath === undefined) skillPath = a;
|
|
336
|
+
else {
|
|
337
|
+
console.error(`Unknown argument: ${a}`);
|
|
338
|
+
process.exit(2);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (!skillPath) {
|
|
343
|
+
console.error("Missing required argument: path to the skill folder");
|
|
344
|
+
process.exit(2);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const results = validateSkill(skillPath);
|
|
348
|
+
const reportJson = JSON.stringify(results, null, prettyJson ? 2 : undefined);
|
|
349
|
+
|
|
350
|
+
if (format === "human" || format === "both") {
|
|
351
|
+
printReport(results, verbose);
|
|
352
|
+
if (!jsonOut) console.log(" Tip: add --json-out FILE to reuse this report without re-running.\n");
|
|
353
|
+
}
|
|
354
|
+
if (format === "json" || format === "both") {
|
|
355
|
+
if (format === "both") console.log("--- JSON Report ---");
|
|
356
|
+
console.log(reportJson);
|
|
357
|
+
}
|
|
358
|
+
if (jsonOut) {
|
|
359
|
+
writeFileSync(jsonOut, reportJson);
|
|
360
|
+
if (format === "human" || format === "both") console.log(` JSON report saved to: ${jsonOut}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
process.exit(results.failed === 0 ? 0 : 1);
|
|
364
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pr-review
|
|
3
|
+
description: "Explicit-route workflow to review a hosted GitHub Pull Request or GitLab Merge Request across six dimensions — security, requirements, test coverage, architecture, regression, performance — using massa-ai roster subagents, then post inline comments plus one consolidated summary through the host CLI (gh or glab). Use when the user says review PR 128, review this MR, or code review this pull request. Do NOT use for local working-diff review (audit workflows), creating PRs, replying to review comments, or fixing CI."
|
|
4
|
+
license: CC-BY-4.0
|
|
5
|
+
metadata:
|
|
6
|
+
version: "1.0.0"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Attribution: adapted from the `pr-review` skill by github.com/augusto-dmh
|
|
10
|
+
(TLC skills catalog), licensed CC-BY-4.0. Host abstraction (GitLab support),
|
|
11
|
+
massa-ai roster dispatches, memory/index/`.specs/` integration, and channel
|
|
12
|
+
discipline are this repository's additions; repository contracts win on any
|
|
13
|
+
conflict with the base.
|
|
14
|
+
|
|
15
|
+
### PR Review
|
|
16
|
+
|
|
17
|
+
Use when the user explicitly asks to review a hosted PR (Pull Request, GitHub) or
|
|
18
|
+
MR (Merge Request, GitLab) — "review PR 128", "review this MR", "check pull request
|
|
19
|
+
42". Explicit route only: never auto-trigger during coding. Local working-tree diff
|
|
20
|
+
review stays with the audit workflows and `massa-ai-reviewer`; this workflow exists
|
|
21
|
+
to **post findings back to the host**.
|
|
22
|
+
|
|
23
|
+
Load `references/project-context.md` (intake sweep) before the first substantive
|
|
24
|
+
read. Resolve `projectId` and `workflowSessionId` = `pr-review-<number>` per the
|
|
25
|
+
Core Contract, and run a budgeted `recall` (limit ≤ 3, minImportance ≥ 0.7) for
|
|
26
|
+
prior review conventions and known regression patterns.
|
|
27
|
+
|
|
28
|
+
## Execution Contract (non-negotiable)
|
|
29
|
+
|
|
30
|
+
1. **Orchestration-only.** The main agent never authors a review finding. It
|
|
31
|
+
gathers context, dispatches the review subagents, dedupes their returned
|
|
32
|
+
findings, and posts. Doing the review inline — even for a small diff — is a
|
|
33
|
+
failure of this workflow.
|
|
34
|
+
2. **Comment-only, never destructive.** Forbidden in every circumstance:
|
|
35
|
+
`gh pr review --approve`, `gh pr review --request-changes`, `gh pr merge`,
|
|
36
|
+
`glab mr approve`, `glab mr revoke`, `glab mr merge`, and the raw
|
|
37
|
+
`POST …/approve` / `POST …/unapprove` endpoints. Posting notes or discussions
|
|
38
|
+
never approves — keep it that way. Never modify repository files.
|
|
39
|
+
3. **Subagents never touch the host.** Review subagents are read-only and
|
|
40
|
+
host-agnostic: they receive the diff and context in their packet and return
|
|
41
|
+
findings in their reply block. Only the orchestrator executes `gh`/`glab`.
|
|
42
|
+
4. **File-body posting.** Every multiline body is written to a temp file and
|
|
43
|
+
posted with the host's file-body mechanism (`--body-file` / `-F body=@file`).
|
|
44
|
+
Inlining a multiline `--body` string is the protocol's most common failure.
|
|
45
|
+
5. **Ask, never guess.** No PR/MR reference in the request → ask for it. Host CLI
|
|
46
|
+
cannot resolve the reference → stop and surface the CLI error output.
|
|
47
|
+
|
|
48
|
+
## Step 1 — Initialize
|
|
49
|
+
|
|
50
|
+
### 1a. Resolve the host
|
|
51
|
+
|
|
52
|
+
Order: explicit user statement > CLI probe > git remote host. Probe with
|
|
53
|
+
`gh repo view` / `glab repo view` **exit status** (glab's no-remote error text is
|
|
54
|
+
not a documented stable string — never match on the message). Both probes fail →
|
|
55
|
+
stop and report which CLI is missing or unauthenticated (`gh auth status` /
|
|
56
|
+
`glab auth status`). Both succeed (mirrored repo) → ask the user which host to
|
|
57
|
+
review on; a posted comment is outward-facing. Record `HOST ∈ {github, gitlab}`.
|
|
58
|
+
|
|
59
|
+
### 1b. PR/MR context (via the command map below)
|
|
60
|
+
|
|
61
|
+
Resolve repository identity, then fetch: title + body/description + source
|
|
62
|
+
branch, the head anchor (`{SHA}` on GitHub; the full `diff_refs`
|
|
63
|
+
`{base_sha, head_sha, start_sha}` triple on GitLab), the full diff, and the
|
|
64
|
+
changed-file list. Then load the existing inline-comment inventory as
|
|
65
|
+
`{id, path, line, body}` records — **page to completion** (GitLab discussions
|
|
66
|
+
default to 20 per page; pin `per_page=100` and loop) — used for dedupe,
|
|
67
|
+
`[RESOLVED]` replies, and threading.
|
|
68
|
+
|
|
69
|
+
### 1c. Project discovery (the adaptive spine)
|
|
70
|
+
|
|
71
|
+
Probe the repository once and record a DISCOVERY MAP passed verbatim to every
|
|
72
|
+
subagent. Prefer evidence the project states over guesses; mark absences `none`.
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
TEST: <command CI actually runs> | globs: <...> | unit vs e2e: <split | none>
|
|
76
|
+
REQS: tracker=<GH #42 | Jira KEY-123 | GitLab #42 | none> ; specs=<paths | none>
|
|
77
|
+
CONVENTIONS: <doc/skill paths that state rules | none-found>
|
|
78
|
+
REVIEW_SKILLS: <project-local review skill paths | none>
|
|
79
|
+
INDEX: <massa-ai retrieval state: fresh | stale | unavailable — CLI fallback>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- **TEST**: the CI workflow config is authoritative; manifests are fallback.
|
|
83
|
+
- **REQS Track A (tracker)**: ticket key from branch name or PR/MR body —
|
|
84
|
+
`gh issue view {N} --json title,body` / `glab issue view {N} --output json`;
|
|
85
|
+
Jira only through an already-configured Atlassian MCP (never invent a host).
|
|
86
|
+
- **REQS Track B (in-repo)**: `.specs/project/FEATURES.json` and
|
|
87
|
+
`.specs/features/<slug>/{spec,tasks}.md` acceptance criteria matched by branch,
|
|
88
|
+
ticket, or feature stem; then `docs/`, ADR/RFC directories, `*-spec.md`.
|
|
89
|
+
- **CONVENTIONS/REVIEW_SKILLS**: `CONTRIBUTING*`, `ARCHITECTURE*`, `AGENTS.md`,
|
|
90
|
+
`CLAUDE.md`, `docs/**` convention files, `.claude/skills/`, `.cursor/skills/`.
|
|
91
|
+
- **INDEX**: `list_projects` freshness first; when fresh, `project_map` or
|
|
92
|
+
`get_architecture` for orientation and `impact_analysis` over the PR/MR diff
|
|
93
|
+
for centrality-ranked hotspots; `search` under `references/synapse-policy.md`
|
|
94
|
+
when two or more related searches are planned. Index results are leads until
|
|
95
|
+
confirmed against the diff — never evidence on their own. Server or index
|
|
96
|
+
unavailable → record it and continue per `references/graceful-degradation.md`.
|
|
97
|
+
|
|
98
|
+
## Host Command Map
|
|
99
|
+
|
|
100
|
+
The orchestrator reads every host operation from this table. `{REPO}`/`{PR}` are
|
|
101
|
+
GitHub coordinates; `{MR}` is the GitLab IID; `:id` is glab's project placeholder
|
|
102
|
+
(resolved from the current repo's remote — 8 placeholders are documented:
|
|
103
|
+
`:branch :fullpath :group :id :namespace :repo :user :username`).
|
|
104
|
+
|
|
105
|
+
| Operation | GitHub (`gh`) | GitLab (`glab`) |
|
|
106
|
+
| --- | --- | --- |
|
|
107
|
+
| Identity | `gh repo view --json nameWithOwner -q .nameWithOwner` → `{REPO}` | `glab repo view --output json --jq .path_with_namespace` (project id: `--jq .id`) |
|
|
108
|
+
| Metadata | `gh pr view {PR} --json title,body,headRefName,headRefOid` → `{SHA}` | `glab mr view {MR} --output json` → `title`, `description`, `source_branch`, `sha`, `diff_refs.{base_sha,head_sha,start_sha}` |
|
|
109
|
+
| Full diff | `gh pr diff {PR}` | `glab mr diff {MR} --raw` |
|
|
110
|
+
| Changed files | `gh pr diff {PR} --name-only` | `glab api "projects/:id/merge_requests/{MR}/diffs?per_page=100&page={N}"` → `new_path`/`old_path`, page to completion |
|
|
111
|
+
| Existing comments | `gh api repos/{REPO}/pulls/{PR}/comments` | `glab api "projects/:id/merge_requests/{MR}/discussions?per_page=100&page={N}"` → note `id`, `position.new_path`, `position.new_line`, `body` |
|
|
112
|
+
| Inline comment | `gh api repos/{REPO}/pulls/{PR}/comments -F body=@body.md -f commit_id={SHA} -f path={path} -F line={N} -f side=RIGHT` | `glab api --method POST "projects/:id/merge_requests/{MR}/discussions" -F body=@body.md -f "position[position_type]=text" -f "position[base_sha]={base}" -f "position[head_sha]={head}" -f "position[start_sha]={start}" -f "position[new_path]={path}" -f "position[old_path]={old}" -F "position[new_line]={N}"` |
|
|
113
|
+
| Thread reply | `gh api repos/{REPO}/pulls/{PR}/comments/{COMMENT_ID}/replies -F body=@body.md` | `glab api --method POST "projects/:id/merge_requests/{MR}/discussions/{DISCUSSION_ID}/notes" -F body=@body.md` |
|
|
114
|
+
| Summary | `gh pr review {PR} --comment --body-file summary.md` | `glab api --method POST "projects/:id/merge_requests/{MR}/notes" -F body=@summary.md` |
|
|
115
|
+
|
|
116
|
+
Anchoring and flag semantics (load-bearing, verified against official docs):
|
|
117
|
+
|
|
118
|
+
- **GitHub `line={N}`** is the 1-based line number in the **head file** on side
|
|
119
|
+
`RIGHT` — count from the hunk header across added and context lines. A
|
|
120
|
+
diff-relative offset returns 422 or lands on the wrong line.
|
|
121
|
+
- **GitLab added line** ⇒ send `position[new_line]` and **omit** `old_line`
|
|
122
|
+
(removed line: the reverse; context line: both). `new_path` **and** `old_path`
|
|
123
|
+
are both required for `position_type=text` — take `old_path` from the `/diffs`
|
|
124
|
+
inventory, never assume it equals `new_path` (renames break that).
|
|
125
|
+
- **`-F`/`--field` expands `@file` and infers types on both CLIs; `-f`/
|
|
126
|
+
`--raw-field` does neither** — `-f body=@body.md` posts the literal string
|
|
127
|
+
`@body.md`. Use `-F` for bodies and line numbers, `-f` for plain strings.
|
|
128
|
+
glab's `-F` also switches the default method to POST.
|
|
129
|
+
- `glab mr note create` has experimental inline flags (`--file`, `--line`,
|
|
130
|
+
`--reply`) — GitLab marks them "might be unstable or removed at any time"; the
|
|
131
|
+
stable `glab api` paths above are the contract. A plain summary may also use
|
|
132
|
+
`glab mr note create {MR} < summary.md` (body from stdin).
|
|
133
|
+
|
|
134
|
+
## Step 2 — Dispatch the review (two waves)
|
|
135
|
+
|
|
136
|
+
Six dimensions run as read-only roster dispatches under
|
|
137
|
+
`references/agent-orchestration.md` (wave cap 4 → wave 1 = rows 1–4, wave 2 =
|
|
138
|
+
rows 5–6). Each packet carries: the dimension row below, the DISCOVERY MAP, the
|
|
139
|
+
PR/MR intent (title/body/branch), the existing-comment inventory, the diff
|
|
140
|
+
trimmed to hunks relevant to the dimension per `references/context-firewall.md`,
|
|
141
|
+
the severity labels, and the reply contract.
|
|
142
|
+
|
|
143
|
+
| # | Dimension | Agent | Packet delta (lens / scope) | Marker `{type}` |
|
|
144
|
+
| --- | --- | --- | --- | --- |
|
|
145
|
+
| 1 | Security | `massa-ai-audit-specialist` | `lens: security` — secrets, authn/authz on new endpoints, injection, unsafe deserialization, PII in logs, permissive CORS, leaking payload fields | `security` |
|
|
146
|
+
| 2 | Requirements & DoD (Definition of Done) | `massa-ai-audit-specialist` | `lens: requirements` — score merged Track A + Track B criteria against the diff, evidence-or-zero: ✅ implemented (`path:line`) / 🟡 partial / ❌ missing; no source ⇒ report "requirements verification skipped" | `requirements` |
|
|
147
|
+
| 3 | Architecture & conventions | `massa-ai-audit-specialist` | `lens: architecture` — extract every explicit rule from the profile's CONVENTIONS/REVIEW_SKILLS docs into a numbered matrix, grade each changed file PASS/VIOLATION/N/A; no docs ⇒ minimal generic boundary sweep, stated | `architecture` |
|
|
148
|
+
| 4 | Performance | `massa-ai-audit-specialist` | `lens: performance` — only issues clearly visible in the diff: N+1 queries, unbounded fetches, per-row lazy I/O, sequential awaits of independent calls, loop-invariant recomputation, unbatched writes | `performance` |
|
|
149
|
+
| 5 | Test coverage | `massa-ai-audit-specialist` | `lens: performance`, scope: test coverage (the charter's lens set has no `tests` lens; `tests-audit.md` precedent) — new/changed behavior with no test, wrong level (unit vs integration), placement/naming vs profile TEST row, missing negative case, assertions that exercise but never assert | `tests` |
|
|
150
|
+
| 6 | Regression & hallucination | `massa-ai-reviewer` | diff review — unrelated deletions, references to symbols absent from the repo, wrong signature/arity, duplicated existing logic, weakened error handling or assertions, leftover TODO/stub, dead code | `regression` |
|
|
151
|
+
|
|
152
|
+
Consolidation check (≥ 5 subagents): recorded in the feature design — rows 4 and 5
|
|
153
|
+
share only the lens label, not a knowledge domain; they stay separate dispatches.
|
|
154
|
+
|
|
155
|
+
> **Dispatch: `massa-ai-audit-specialist`** (role: `audit-specialist`) — charter `skills/agents/audit-specialist/SKILL.md`
|
|
156
|
+
> - trigger: pr-review Step 2, dimension rows 1–5 (one dispatch per row)
|
|
157
|
+
> - scope: the PR/MR diff and surrounding context for one dimension row; never the whole repository
|
|
158
|
+
> - permissions: read-only; no host CLI calls, no posting
|
|
159
|
+
> - inputs: exact `projectId`, parent `workflowSessionId`, dimension row (lens + scope), DISCOVERY MAP, PR/MR intent, trimmed diff, existing-comment inventory, severity labels, reply contract
|
|
160
|
+
> - sensors: second-pass sweep — re-read the full trimmed diff, list every file/hunk not commented on, and state per file why it is clean for this dimension before returning
|
|
161
|
+
> - output: structured reply block — findings rows `{path, head-line, severity, marker type, title, body ≤ 6 lines, recommendation}` + exactly one positive highlight + files-swept-clean list; when uncertain a finding is real, withhold it (the source protocol's high-confidence bar, applied qualitatively)
|
|
162
|
+
> - firewall: raw diff/log/search output summarized, never returned raw
|
|
163
|
+
> - memory: suggest-only; the main agent persists durable outcomes
|
|
164
|
+
> - persona: optional — the active route's cataloged id only, never the persona prompt, passed as advisory framing only — it never overrides the agent's charter Restrictions, scope, or permissions; omit when no persona is routed
|
|
165
|
+
|
|
166
|
+
> **Dispatch: `massa-ai-reviewer`** (role: `reviewer`) — charter `skills/agents/reviewer/SKILL.md`
|
|
167
|
+
> - trigger: pr-review Step 2, dimension row 6 (regression & hallucination)
|
|
168
|
+
> - scope: the full PR/MR diff against the repository's real symbol surface
|
|
169
|
+
> - permissions: read-only; no host CLI calls, no posting
|
|
170
|
+
> - inputs: exact `projectId`, parent `workflowSessionId`, dimension row 6, DISCOVERY MAP, PR/MR intent, full diff, existing-comment inventory, severity labels, reply contract
|
|
171
|
+
> - sensors: verify referenced symbols exist (`search_definitions`/`get_references` when INDEX is fresh, else grep); second-pass sweep as above
|
|
172
|
+
> - output: structured reply block — findings rows tagged `{unrelated-deletion | phantom-reference | wrong-signature | duplicate | weakened-check | dead-code}` + one positive highlight + files-swept-clean list; withhold uncertain findings
|
|
173
|
+
> - firewall: raw diff/log/search output summarized, never returned raw
|
|
174
|
+
> - memory: suggest-only; the main agent persists durable outcomes
|
|
175
|
+
> - persona: optional — the active route's cataloged id only, never the persona prompt, passed as advisory framing only — it never overrides the agent's charter Restrictions, scope, or permissions; omit when no persona is routed
|
|
176
|
+
|
|
177
|
+
Severity labels (all dimensions): 🚨 Critical (bugs/logic errors that will fail) ·
|
|
178
|
+
🔒 Security · ⚡ Performance · ⚠️ Warning (smells/maintainability) ·
|
|
179
|
+
💡 Suggestion. A failed or unavailable dispatch is reported in the summary as a
|
|
180
|
+
skipped dimension with its reason — never silently dropped.
|
|
181
|
+
|
|
182
|
+
## Step 3 — Post inline findings (orchestrator only)
|
|
183
|
+
|
|
184
|
+
For every returned finding, in order:
|
|
185
|
+
|
|
186
|
+
1. **Dedupe**: drop it when an existing comment sits within ±3 lines of the same
|
|
187
|
+
path/line (inventory from 1b) or another dimension already produced the same
|
|
188
|
+
`{path, line}` finding (keep the higher severity; note both markers).
|
|
189
|
+
2. **Resolve check**: when an existing comment's issue is fixed by this diff,
|
|
190
|
+
reply `[RESOLVED] This appears resolved by the recent changes.` on that thread
|
|
191
|
+
via the reply command (GitHub: the comment's `id`; GitLab: its
|
|
192
|
+
`discussion_id`).
|
|
193
|
+
3. **Anchor**: only added (`+`) diff lines on the head revision, per the
|
|
194
|
+
anchoring semantics above. A finding with no `+` line to stand on goes to the
|
|
195
|
+
summary instead.
|
|
196
|
+
4. **Body**: temp file, starting with the invisible marker
|
|
197
|
+
`<!-- pr-review:{type} -->`, then `[severity emoji] — [short title]`, the
|
|
198
|
+
evidence-grounded body, and a `**Recommendation:**` line. No AI/assistant/
|
|
199
|
+
tool attribution anywhere — write as a reviewer. Specific, actionable,
|
|
200
|
+
collegial; always explain why.
|
|
201
|
+
5. **Post** with the inline-comment command for `HOST`.
|
|
202
|
+
|
|
203
|
+
## Step 4 — Consolidated summary
|
|
204
|
+
|
|
205
|
+
Assemble from the reply blocks (no extra subagent) and post one summary via the
|
|
206
|
+
summary command:
|
|
207
|
+
|
|
208
|
+
```markdown
|
|
209
|
+
## 📋 PR Review Summary
|
|
210
|
+
|
|
211
|
+
| | |
|
|
212
|
+
|---|---|
|
|
213
|
+
| **Host / target** | {github PR #N | gitlab MR !N} @ {head sha} |
|
|
214
|
+
| **Dimensions** | 6 (Security · Requirements & DoD · Tests · Architecture · Regression · Performance) |
|
|
215
|
+
| **Detected runner** | {TEST row | none found} |
|
|
216
|
+
| **Requirements source** | {tracker / spec paths / none} |
|
|
217
|
+
| **Project refs loaded** | {CONVENTIONS + REVIEW_SKILLS rows} |
|
|
218
|
+
| **Findings** | {N} across {M} files |
|
|
219
|
+
|
|
220
|
+
### 🔒 Security ({N}) / 🚨 Critical ({N}) / ⚡ Performance ({N}) / ⚠️ Warnings ({N}) / 💡 Suggestions ({N})
|
|
221
|
+
- [`path/file:L42`] Finding title — one line each, grouped by severity
|
|
222
|
+
|
|
223
|
+
### 📋 Requirements
|
|
224
|
+
{✅/🟡/❌ rows from dimension 2, with `path:line` evidence}
|
|
225
|
+
|
|
226
|
+
### 🔍 Files with no findings
|
|
227
|
+
- `path` — swept clean by {dimensions} (omit section when every logic file got a comment; config/lock/declaration files excluded)
|
|
228
|
+
|
|
229
|
+
### ✅ Highlights
|
|
230
|
+
- one per dimension
|
|
231
|
+
|
|
232
|
+
> See inline comments for details. {Skipped dimensions/sensors with reasons, if any.}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Zero findings overall → post "✅ No issues found across all review dimensions."
|
|
236
|
+
with the metadata table intact.
|
|
237
|
+
|
|
238
|
+
## Completion
|
|
239
|
+
|
|
240
|
+
- Emit Conversation Feedback status updates at wave boundaries when that policy
|
|
241
|
+
is active; expand every abbreviation on first use in user-facing output.
|
|
242
|
+
- Persist durable outcomes only (recurring review pattern, confirmed project
|
|
243
|
+
convention) with the required memory tags; do not fabricate memories.
|
|
244
|
+
- Close with `references/evidence-gate.md`: counts posted vs deduped vs withheld,
|
|
245
|
+
skipped dimensions/sensors with reasons, and the summary URL/reference.
|
|
246
|
+
<!-- validator anchors: comment-only | added (+) diff lines | page to completion | two waves -->
|