@massa-ai/cursor-plugin 1.26.0 → 1.29.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.
Files changed (156) hide show
  1. package/.cursor-plugin/plugin.json +1 -1
  2. package/agent-profiles/balanced/massa-ai-judge.md +4 -8
  3. package/agent-profiles/balanced/massa-ai-meta-judge.md +2 -5
  4. package/agent-profiles/balanced/massa-ai-navigator.md +1 -1
  5. package/agent-profiles/cheap/massa-ai-judge.md +4 -8
  6. package/agent-profiles/cheap/massa-ai-meta-judge.md +2 -5
  7. package/agent-profiles/cheap/massa-ai-navigator.md +1 -1
  8. package/agent-profiles/heavy/massa-ai-judge.md +4 -8
  9. package/agent-profiles/heavy/massa-ai-meta-judge.md +2 -5
  10. package/agent-profiles/heavy/massa-ai-navigator.md +1 -1
  11. package/agent-profiles/home/massa-ai-judge.md +4 -8
  12. package/agent-profiles/home/massa-ai-meta-judge.md +2 -5
  13. package/agent-profiles/home/massa-ai-navigator.md +1 -1
  14. package/agent-profiles/work/massa-ai-judge.md +4 -8
  15. package/agent-profiles/work/massa-ai-meta-judge.md +2 -5
  16. package/agent-profiles/work/massa-ai-navigator.md +1 -1
  17. package/agents/massa-ai-judge.md +4 -8
  18. package/agents/massa-ai-meta-judge.md +2 -5
  19. package/agents/massa-ai-navigator.md +1 -1
  20. package/install.sh +35 -0
  21. package/package.json +1 -1
  22. package/skills/agents/judge/SKILL.md +6 -9
  23. package/skills/agents/meta-judge/SKILL.md +3 -6
  24. package/skills/agents/navigator/SKILL.md +2 -2
  25. package/skills/massa-ai/SKILL.md +6 -17
  26. package/skills/massa-ai/references/adr-authoring.md +3 -3
  27. package/skills/massa-ai/references/agent-orchestration.md +18 -3
  28. package/skills/massa-ai/references/architecture-coupling-lens.md +1 -1
  29. package/skills/massa-ai/references/architecture-deepening-lens.md +1 -1
  30. package/skills/massa-ai/references/architecture-domain-lens.md +1 -1
  31. package/skills/massa-ai/references/architecture-lenses.md +1 -1
  32. package/skills/massa-ai/references/audit-report-io.md +32 -2
  33. package/skills/massa-ai/references/audit-scope.md +22 -1
  34. package/skills/massa-ai/references/code-annotation.md +5 -5
  35. package/skills/massa-ai/references/codebase-investigation.md +1 -1
  36. package/skills/massa-ai/references/coding-guidelines.md +67 -0
  37. package/skills/massa-ai/references/context-firewall.md +2 -1
  38. package/skills/massa-ai/references/conversation-feedback.md +1 -1
  39. package/skills/massa-ai/references/debug-diagnosis-loop.md +1 -1
  40. package/skills/massa-ai/references/decision-engine.md +1 -1
  41. package/skills/massa-ai/references/evidence-gate.md +1 -1
  42. package/skills/massa-ai/references/figma-pre-analysis.md +3 -3
  43. package/skills/massa-ai/references/furps/analyst-role.md +1 -1
  44. package/skills/massa-ai/references/furps/checklist.md +1 -1
  45. package/skills/massa-ai/references/furps/intake.md +1 -1
  46. package/skills/massa-ai/references/furps/report-contract.md +1 -1
  47. package/skills/massa-ai/references/graceful-degradation.md +22 -0
  48. package/skills/massa-ai/references/hook-enforcement.md +3 -3
  49. package/skills/massa-ai/references/implementation-delivery.md +4 -4
  50. package/skills/massa-ai/references/installation.md +1 -1
  51. package/skills/massa-ai/references/lessons.md +2 -2
  52. package/skills/massa-ai/references/maestro/artifacts-reports.md +1 -1
  53. package/skills/massa-ai/references/maestro/cli-device.md +1 -1
  54. package/skills/massa-ai/references/maestro/cloud.md +1 -1
  55. package/skills/massa-ai/references/maestro/config-env-output.md +1 -1
  56. package/skills/massa-ai/references/maestro/fact-ledger.md +1 -1
  57. package/skills/massa-ai/references/maestro/js-scripting.md +1 -1
  58. package/skills/massa-ai/references/maestro/mcp.md +1 -1
  59. package/skills/massa-ai/references/maestro/patterns.md +1 -1
  60. package/skills/massa-ai/references/maestro/selectors.md +1 -1
  61. package/skills/massa-ai/references/maestro/workspace-execution.md +1 -1
  62. package/skills/massa-ai/references/maestro/yaml-commands.md +1 -1
  63. package/skills/massa-ai/references/maestro.md +1 -1
  64. package/skills/massa-ai/references/mcp-tools.md +2 -2
  65. package/skills/massa-ai/references/memory-policy.md +2 -2
  66. package/skills/massa-ai/references/mobile-context.md +9 -5
  67. package/skills/massa-ai/references/mobile-diagnosis.md +2 -2
  68. package/skills/massa-ai/references/mobile-figma-matcher/android-compose.md +1 -1
  69. package/skills/massa-ai/references/mobile-figma-matcher/android-views.md +1 -1
  70. package/skills/massa-ai/references/mobile-figma-matcher/core.md +1 -1
  71. package/skills/massa-ai/references/mobile-figma-matcher/ios-swiftui.md +1 -1
  72. package/skills/massa-ai/references/mobile-figma-matcher/ios-uikit.md +1 -1
  73. package/skills/massa-ai/references/mobile-figma-matcher/kmp-compose-multiplatform.md +1 -1
  74. package/skills/massa-ai/references/mobile-figma-matcher/repository-detection.md +1 -1
  75. package/skills/massa-ai/references/naming-standards.md +1 -1
  76. package/skills/massa-ai/references/pr-task-fix.md +2 -2
  77. package/skills/massa-ai/references/project-context.md +2 -2
  78. package/skills/massa-ai/references/repo-rules-discovery.md +4 -4
  79. package/skills/massa-ai/references/rfc/discovery-and-sizing.md +1 -1
  80. package/skills/massa-ai/references/rfc/document-contract.md +1 -1
  81. package/skills/massa-ai/references/rfc/quality-and-lifecycle.md +1 -1
  82. package/skills/massa-ai/references/root-cause-scripts.md +2 -2
  83. package/skills/massa-ai/references/skill-architect/examples.md +256 -0
  84. package/skills/massa-ai/references/skill-architect/patterns.md +317 -0
  85. package/skills/massa-ai/references/skill-architect/quality-checklist.md +70 -0
  86. package/skills/massa-ai/references/sonarqube-mcp.md +73 -0
  87. package/skills/massa-ai/references/spec-driven/artifact-store.md +1 -1
  88. package/skills/massa-ai/references/spec-driven/brownfield-mapping.md +16 -0
  89. package/skills/massa-ai/references/spec-driven/code-analysis.md +1 -1
  90. package/skills/massa-ai/references/spec-driven/coding-principles.md +1 -1
  91. package/skills/massa-ai/references/spec-driven/context-limits.md +1 -1
  92. package/skills/massa-ai/references/spec-driven/design.md +22 -1
  93. package/skills/massa-ai/references/spec-driven/discuss.md +1 -1
  94. package/skills/massa-ai/references/spec-driven/execute.md +3 -1
  95. package/skills/massa-ai/references/spec-driven/memory.md +1 -1
  96. package/skills/massa-ai/references/spec-driven/specify.md +3 -3
  97. package/skills/massa-ai/references/spec-driven/sub-agents.md +1 -1
  98. package/skills/massa-ai/references/spec-driven/tasks.md +1 -1
  99. package/skills/massa-ai/references/spec-driven/validate.md +1 -1
  100. package/skills/massa-ai/references/subagent-design.md +4 -4
  101. package/skills/massa-ai/references/synapse-policy.md +1 -1
  102. package/skills/massa-ai/references/tdd/calibrated-examples.md +1 -1
  103. package/skills/massa-ai/references/tdd/discovery-and-sizing.md +1 -1
  104. package/skills/massa-ai/references/tdd/document-contract.md +1 -1
  105. package/skills/massa-ai/references/tdd/quality-and-lifecycle.md +1 -1
  106. package/skills/massa-ai/references/the-fool/cognitive-bias-inventory.md +1 -1
  107. package/skills/massa-ai/references/the-fool/dialectic-synthesis.md +1 -1
  108. package/skills/massa-ai/references/the-fool/evidence-audit.md +1 -1
  109. package/skills/massa-ai/references/the-fool/pre-mortem-analysis.md +1 -1
  110. package/skills/massa-ai/references/the-fool/red-team-adversarial.md +1 -1
  111. package/skills/massa-ai/references/the-fool/socratic-questioning.md +1 -1
  112. package/skills/massa-ai/references/ticket/atlassian-fix.md +1 -1
  113. package/skills/massa-ai/references/ticket/intake-and-sources.md +1 -1
  114. package/skills/massa-ai/references/ticket/templates-and-quality.md +1 -1
  115. package/skills/massa-ai/references/verification-ladder.md +1 -1
  116. package/skills/massa-ai/scripts/validate_audit_report.ts +382 -0
  117. package/skills/massa-ai/scripts/validate_design.ts +264 -0
  118. package/skills/massa-ai/scripts/validate_skill.ts +364 -0
  119. package/skills/massa-ai/workflows/adr.md +16 -8
  120. package/skills/massa-ai/workflows/architecture/architecture-audit.md +23 -40
  121. package/skills/massa-ai/workflows/architecture/architecture-fix.md +14 -6
  122. package/skills/massa-ai/workflows/bugs/bugs-audit.md +19 -35
  123. package/skills/massa-ai/workflows/bugs/bugs-fix.md +13 -5
  124. package/skills/massa-ai/workflows/code-quality/code-quality-audit.md +25 -41
  125. package/skills/massa-ai/workflows/code-quality/code-quality-fix.md +13 -5
  126. package/skills/massa-ai/workflows/commit.md +13 -5
  127. package/skills/massa-ai/workflows/debug.md +11 -3
  128. package/skills/massa-ai/workflows/design.md +15 -7
  129. package/skills/massa-ai/workflows/exploration.md +12 -4
  130. package/skills/massa-ai/workflows/feature.md +14 -13
  131. package/skills/massa-ai/workflows/general.md +13 -8
  132. package/skills/massa-ai/workflows/implementation/implementation-audit.md +15 -15
  133. package/skills/massa-ai/workflows/implementation/implementation-fix.md +13 -5
  134. package/skills/massa-ai/workflows/judge-with-debate.md +12 -4
  135. package/skills/massa-ai/workflows/long-session.md +10 -2
  136. package/skills/massa-ai/workflows/maestro/maestro-audit.md +11 -3
  137. package/skills/massa-ai/workflows/maestro/maestro-fix.md +12 -4
  138. package/skills/massa-ai/workflows/maestro/maestro.md +12 -4
  139. package/skills/massa-ai/workflows/mobile-figma/mobile-figma-audit.md +11 -3
  140. package/skills/massa-ai/workflows/mobile-figma/mobile-figma-fix.md +12 -4
  141. package/skills/massa-ai/workflows/onboarding.md +10 -2
  142. package/skills/massa-ai/workflows/refactor.md +12 -4
  143. package/skills/massa-ai/workflows/refinement/furps-refinement.md +12 -4
  144. package/skills/massa-ai/workflows/requirements/requirements-audit.md +19 -36
  145. package/skills/massa-ai/workflows/requirements/requirements-fix.md +13 -5
  146. package/skills/massa-ai/workflows/rfc.md +10 -2
  147. package/skills/massa-ai/workflows/security/security-audit.md +19 -35
  148. package/skills/massa-ai/workflows/security/security-fix.md +13 -5
  149. package/skills/massa-ai/workflows/skill-architect.md +393 -0
  150. package/skills/massa-ai/workflows/spec-driven.md +20 -23
  151. package/skills/massa-ai/workflows/tdd.md +10 -2
  152. package/skills/massa-ai/workflows/tests/tests-audit.md +19 -35
  153. package/skills/massa-ai/workflows/tests/tests-fix.md +13 -5
  154. package/skills/massa-ai/workflows/the-fool.md +11 -3
  155. package/skills/massa-ai/workflows/ticket.md +10 -2
  156. package/skills/massa-ai/workflows/to-prd.md +81 -0
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * validate_design.ts - deterministic closure-gate checks for a feature
4
+ * design.md (STO-9 top validator pack).
5
+ *
6
+ * Turns the spec-driven Design phase's "required sections + every flagged
7
+ * concern has a mitigation" rule into a checkable pass/fail run, instead of
8
+ * trusting the model to remember it. Same heuristic-markdown-inspection
9
+ * style as validate_spec.ts: Bun builtins only, zero dependencies, operates
10
+ * only on the design.md artifact.
11
+ *
12
+ * What it checks:
13
+ * ERROR - a required section is missing (Design Summary, Risks & Concerns,
14
+ * Tech Decisions - heading-prefix match, case-insensitive)
15
+ * ERROR - a Risks & Concerns table row has an empty/placeholder Mitigation
16
+ * cell (a bare "> None found" section with zero rows is valid -
17
+ * it is a real, empty risk register, not an unfilled one)
18
+ * WARN - Risks & Concerns has no table rows and no "None found" marker
19
+ * (ambiguous: unfilled template vs. genuinely empty)
20
+ *
21
+ * Usage:
22
+ * bun skills/massa-ai/scripts/validate_design.ts [target] [--root DIR] [--strict]
23
+ *
24
+ * target Path to a design.md, a feature directory, or a project root.
25
+ * Omitted -> auto-detect the single feature under <root>/.specs/features/.
26
+ * --root Project root that contains .specs/ (default: current dir).
27
+ * --strict Treat warnings as errors.
28
+ *
29
+ * Exit codes: 0 pass, 1 errors found (or warnings under --strict), 2 usage error.
30
+ */
31
+
32
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
33
+
34
+ const REQUIRED_SECTIONS = ["Design Summary", "Risks & Concerns", "Tech Decisions"];
35
+ const PLACEHOLDER_RE = /^\s*\[.+\]\s*$|^\s*<.+>\s*$|^\s*-?\s*$/;
36
+
37
+ /** Mirrors os.path.join()'s "./" preservation — see validate_spec.ts for why. */
38
+ function pyJoin(...parts: string[]): string {
39
+ let result = parts[0] ?? "";
40
+ for (let i = 1; i < parts.length; i++) {
41
+ const part = parts[i]!;
42
+ if (part.startsWith("/")) {
43
+ result = part;
44
+ } else if (result === "" || result.endsWith("/")) {
45
+ result += part;
46
+ } else {
47
+ result += `/${part}`;
48
+ }
49
+ }
50
+ return result;
51
+ }
52
+
53
+ function isFile(p: string): boolean {
54
+ return existsSync(p) && statSync(p).isFile();
55
+ }
56
+
57
+ function isDir(p: string): boolean {
58
+ return existsSync(p) && statSync(p).isDirectory();
59
+ }
60
+
61
+ function splitLines(text: string): string[] {
62
+ if (text === "") return [];
63
+ const result: string[] = [];
64
+ const lineBreakRe = /\r\n|\r|\n/g;
65
+ let start = 0;
66
+ let match: RegExpExecArray | null;
67
+ while ((match = lineBreakRe.exec(text)) !== null) {
68
+ result.push(text.slice(start, match.index));
69
+ start = match.index + match[0].length;
70
+ }
71
+ if (start < text.length) result.push(text.slice(start));
72
+ return result;
73
+ }
74
+
75
+ function escapeRegExp(s: string): string {
76
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
77
+ }
78
+
79
+ function autodetect(root: string): string | null {
80
+ const base = pyJoin(root, ".specs", "features");
81
+ if (!isDir(base)) return null;
82
+ const features = readdirSync(base)
83
+ .filter((d) => isFile(pyJoin(base, d, "design.md")))
84
+ .sort();
85
+ if (features.length === 1) return pyJoin(base, features[0]!, "design.md");
86
+ if (features.length === 0) return null;
87
+ const joined = features.map((f) => pyJoin(base, f, "design.md")).join("\n ");
88
+ console.error(`validate_design: multiple features found; pass one explicitly:\n ${joined}`);
89
+ process.exit(1);
90
+ }
91
+
92
+ function resolveDesign(target: string | null, root: string): string | null {
93
+ if (target) {
94
+ if (isFile(target)) return target;
95
+ if (isDir(target)) {
96
+ const cand = pyJoin(target, "design.md");
97
+ if (isFile(cand)) return cand;
98
+ return autodetect(target);
99
+ }
100
+ const cand = pyJoin(root, ".specs", "features", target, "design.md");
101
+ if (isFile(cand)) return cand;
102
+ return null;
103
+ }
104
+ return autodetect(root);
105
+ }
106
+
107
+ function splitRow(line: string): string[] {
108
+ const stripped = line.trim().replace(/^\|+/, "").replace(/\|+$/, "");
109
+ return stripped.split("|").map((c) => c.trim());
110
+ }
111
+
112
+ function isSeparator(line: string): boolean {
113
+ return /^\s*\|?[\s:|-]+\|?\s*$/.test(line) && line.includes("-");
114
+ }
115
+
116
+ /** Return [start, end) for the FIRST `#{1,3} <namePrefix>...` heading (prefix match, case-insensitive). */
117
+ function sectionBoundsPrefix(lines: string[], namePrefix: string): [number, number] | null {
118
+ const headingRe = new RegExp(`^#{1,3}\\s+${escapeRegExp(namePrefix)}\\b`, "i");
119
+ let start: number | null = null;
120
+ for (let i = 0; i < lines.length; i++) {
121
+ if (headingRe.test(lines[i]!.trim())) {
122
+ start = i + 1;
123
+ break;
124
+ }
125
+ }
126
+ if (start === null) return null;
127
+ let end = lines.length;
128
+ const nextHeadingRe = /^#{1,3}\s+\S/;
129
+ for (let j = start; j < lines.length; j++) {
130
+ if (nextHeadingRe.test(lines[j]!)) {
131
+ end = j;
132
+ break;
133
+ }
134
+ }
135
+ return [start, end];
136
+ }
137
+
138
+ function check(designPath: string): { errors: string[]; warnings: string[] } {
139
+ const text = readFileSync(designPath, "utf-8");
140
+ const lines = splitLines(text);
141
+ const errors: string[] = [];
142
+ const warnings: string[] = [];
143
+
144
+ // 1. Required sections.
145
+ const bounds = new Map<string, [number, number] | null>();
146
+ for (const name of REQUIRED_SECTIONS) {
147
+ const b = sectionBoundsPrefix(lines, name);
148
+ bounds.set(name, b);
149
+ if (b === null) {
150
+ errors.push(`missing required section: ## ${name}`);
151
+ }
152
+ }
153
+
154
+ // 2. Risks & Concerns: every table row needs a non-empty, non-placeholder
155
+ // Mitigation cell. A bare "None found" marker with zero rows is valid.
156
+ const risksBounds = bounds.get("Risks & Concerns");
157
+ if (risksBounds) {
158
+ const [bs, be] = risksBounds;
159
+ const rows: string[] = [];
160
+ for (let i = bs; i < be; i++) {
161
+ if (lines[i]!.trim().startsWith("|")) rows.push(lines[i]!);
162
+ }
163
+ const bodyText = lines.slice(bs, be).join("\n").toLowerCase();
164
+ let data = rows.filter((r) => !isSeparator(r));
165
+ if (data.length) {
166
+ const headerCells = splitRow(data[0]!).map((c) => c.toLowerCase());
167
+ const mitigationIdx = headerCells.findIndex((c) => c.includes("mitigation"));
168
+ data = data.slice(1); // drop header row
169
+ for (const r of data) {
170
+ const cells = splitRow(r);
171
+ const idx = mitigationIdx >= 0 ? mitigationIdx : cells.length - 1;
172
+ const mitigation = cells[idx] ?? "";
173
+ const concern = cells[0] ?? "(unknown concern)";
174
+ if (!mitigation || PLACEHOLDER_RE.test(mitigation)) {
175
+ errors.push(`Risks & Concerns row '${concern.slice(0, 50)}' has an empty or unfilled Mitigation cell`);
176
+ }
177
+ }
178
+ } else if (!bodyText.includes("none found")) {
179
+ warnings.push("Risks & Concerns has no table rows and no 'None found' marker (ambiguous — confirm intentional)");
180
+ }
181
+ }
182
+
183
+ return { errors, warnings };
184
+ }
185
+
186
+ const USAGE = "usage: validate_design.ts [-h] [--root ROOT] [--strict] [target]";
187
+ const HELP = `${USAGE}
188
+
189
+ Closure-gate checks for a feature design.md.
190
+
191
+ positional arguments:
192
+ target Path to a design.md, a feature directory, or a project root
193
+
194
+ options:
195
+ -h, --help show this help message and exit
196
+ --root ROOT
197
+ --strict`;
198
+
199
+ interface Args {
200
+ target: string | null;
201
+ root: string;
202
+ strict: boolean;
203
+ }
204
+
205
+ function printUsageError(msg: string): void {
206
+ process.stderr.write(`${USAGE}\nvalidate_design.ts: error: ${msg}\n`);
207
+ }
208
+
209
+ function parseArgs(argv: string[]): Args | null {
210
+ let root = ".";
211
+ let strict = false;
212
+ const positionals: string[] = [];
213
+ for (let i = 0; i < argv.length; i++) {
214
+ const a = argv[i]!;
215
+ if (a === "--root") {
216
+ if (i + 1 >= argv.length) {
217
+ printUsageError("argument --root: expected one argument");
218
+ return null;
219
+ }
220
+ root = argv[++i]!;
221
+ } else if (a.startsWith("--root=")) {
222
+ root = a.slice("--root=".length);
223
+ } else if (a === "--strict") {
224
+ strict = true;
225
+ } else if (a === "-h" || a === "--help") {
226
+ console.log(HELP);
227
+ process.exit(0);
228
+ } else if (a.startsWith("-") && a !== "-") {
229
+ printUsageError(`unrecognized arguments: ${a}`);
230
+ return null;
231
+ } else {
232
+ positionals.push(a);
233
+ }
234
+ }
235
+ if (positionals.length > 1) {
236
+ printUsageError(`unrecognized arguments: ${positionals.slice(1).join(" ")}`);
237
+ return null;
238
+ }
239
+ return { target: positionals[0] ?? null, root, strict };
240
+ }
241
+
242
+ function main(argv: string[]): number {
243
+ const args = parseArgs(argv);
244
+ if (args === null) return 2;
245
+
246
+ const design = resolveDesign(args.target, args.root);
247
+ if (!design) {
248
+ console.error("validate_design: could not locate a design.md. Pass a path or run from the project root.");
249
+ return 2;
250
+ }
251
+
252
+ const { errors, warnings } = check(design);
253
+ for (const w of warnings) console.log(` WARN ${w}`);
254
+ for (const e of errors) console.log(` ERROR ${e}`);
255
+ const fail = errors.length > 0 || (warnings.length > 0 && args.strict);
256
+ console.log(`\nvalidate_design: ${errors.length} error(s), ${warnings.length} warning(s) in ${design}`);
257
+ return fail ? 1 : 0;
258
+ }
259
+
260
+ if (import.meta.main) {
261
+ process.exit(main(process.argv.slice(2)));
262
+ }
263
+
264
+ export { check };
@@ -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
+ }
@@ -1,8 +1,16 @@
1
+ ---
2
+ name: adr
3
+ description: "Use this workflow to record a finalized or being-finalized architecture decision; route open options to rfc and implementation planning to tdd."
4
+ license: MIT
5
+ metadata:
6
+ version: "1.1.0"
7
+ ---
8
+
1
9
  ### 📜 ADR (Architecture Decision Record)
2
10
 
3
- Use this workflow to record a finalized or being-finalized architecture decision. Do not use it to decide among open options; route undecided proposals to `workflows/rfc.md`. Do not use it for implementation planning; route that to `workflows/tdd.md`.
11
+ Record a finalized or being-finalized architecture decision. Not for deciding among open options route undecided proposals to `workflows/rfc.md`. Not for implementation planning route that to `workflows/tdd.md`.
4
12
 
5
- Before the first substantive read, load `references/project-context.md` and run the project-context intake sweep for this repository.
13
+ Load `references/project-context.md` (intake sweep) before the first substantive read.
6
14
 
7
15
  1. Resolve/reuse `projectId` and `workflowSessionId`: `adr-[entity]`.
8
16
  2. Load shared references:
@@ -13,21 +21,21 @@ Before the first substantive read, load `references/project-context.md` and run
13
21
  4. Gather source context using `references/adr-authoring.md`:
14
22
  - Use explicitly provided Markdown files and prompt context first.
15
23
  - Use ADR templates, PRDs, and RFCs from Markdown or Atlassian MCP when provided.
16
- - Use optional complementary-stack ADRs from Markdown or Atlassian MCP as cross-stack context only when they affect contracts, constraints, dependencies, risks, or links.
17
- - Use optional same-stack example ADRs from Markdown or Atlassian MCP as format/style references only; do not treat their decision context as facts for the new ADR.
18
- - Use Figma MCP for UI/UX context only when provided and relevant; otherwise use PRD/RFC/NotebookLM context, or ignore UI/UX when absent.
19
- - When supplied Figma links, nodes, desktop selections, or screenshots materially affect the decision, use `workflows/design.md` as optional child context for mobile UI implications only; the ADR still owns the decision record. Screenshots are context-only unless paired with structured Figma evidence.
24
+ - Use optional complementary-stack ADRs (Markdown or Atlassian MCP) as cross-stack context only when they affect contracts, constraints, dependencies, risks, or links.
25
+ - Use optional same-stack example ADRs (Markdown or Atlassian MCP) as format/style references only not as facts for the new ADR.
26
+ - Use Figma MCP for UI/UX context only when provided and relevant; otherwise fall back to PRD/RFC/NotebookLM context, or skip UI/UX when absent.
27
+ - When Figma links, nodes, desktop selections, or screenshots materially affect the decision, use `workflows/design.md` as optional child context for mobile UI implications only; the ADR still owns the decision record. Screenshots are context-only unless paired with structured Figma evidence.
20
28
  - Use NotebookLM only when the user provides one or more notebook IDs; query each relevant notebook separately, preserve attribution, and do not assume a default notebook.
21
29
  - Corroborate with massa-ai search, current repo docs, existing ADRs, and code when the decision depends on current project reality.
22
30
  5. Run the ADR readiness gate from `references/adr-authoring.md`:
23
31
  - If a needed PRD, RFC, template, decision detail, or source fact is absent, ask for the missing context instead of inventing it.
24
32
  - If an RFC is needed but absent and the user does not know the missing context, assume the decision is not made and route to `workflows/rfc.md`.
25
33
  - Require a source-backed or user-confirmed title, date, status, context, decision, consequences, links, and supersession status before drafting.
26
- 6. Draft the ADR using the project's provided template when available; otherwise use the format selected through the `references/adr-authoring.md` fallback questions. Keep claims tied to source confidence: confirmed, user-provided, recalled, inferred, or unresolved.
34
+ 6. Draft the ADR using the project's template when available; otherwise use the format selected via `references/adr-authoring.md`'s fallback questions. Tie claims to source confidence: confirmed, user-provided, recalled, inferred, or unresolved.
27
35
  7. Save the generated ADR using the selected output target:
28
36
  - Default: write to the project's standard ADR directory in Default mode. In Plan Mode, propose the path and content without writing.
29
37
  - Confluence: when requested and a parent page link is provided, write a child page through Atlassian MCP and report the resulting page link.
30
38
  - Fallback: if Confluence was requested without a parent link or Atlassian MCP is unavailable, ask for the parent link or permission to write local Markdown under `.adr/`.
31
39
  - Use sequential numbering from the selected local ADR directory when writing Markdown.
32
- 8. At completion, persist the decision via `remember` as a scored `decision` memory with `memory:semantic`, explicitly linking the ADR file path and source context used.
40
+ 8. At completion, persist the decision via `remember` as a scored `decision` memory (`memory:semantic`), linking the ADR file path and source context used.
33
41
  9. Complete the Evidence Gate from `references/evidence-gate.md`.