@massa-ai/codex-plugin 1.26.0 → 1.27.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 (148) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/agent-profiles/balanced/massa-ai-judge.toml +3 -6
  3. package/agent-profiles/balanced/massa-ai-meta-judge.toml +2 -5
  4. package/agent-profiles/balanced/massa-ai-navigator.toml +1 -1
  5. package/agent-profiles/cheap/massa-ai-judge.toml +3 -6
  6. package/agent-profiles/cheap/massa-ai-meta-judge.toml +2 -5
  7. package/agent-profiles/cheap/massa-ai-navigator.toml +1 -1
  8. package/agent-profiles/heavy/massa-ai-judge.toml +3 -6
  9. package/agent-profiles/heavy/massa-ai-meta-judge.toml +2 -5
  10. package/agent-profiles/heavy/massa-ai-navigator.toml +1 -1
  11. package/agent-profiles/home/massa-ai-judge.toml +3 -6
  12. package/agent-profiles/home/massa-ai-meta-judge.toml +2 -5
  13. package/agent-profiles/home/massa-ai-navigator.toml +1 -1
  14. package/agent-profiles/work/massa-ai-judge.toml +3 -6
  15. package/agent-profiles/work/massa-ai-meta-judge.toml +2 -5
  16. package/agent-profiles/work/massa-ai-navigator.toml +1 -1
  17. package/agents/massa-ai-judge.toml +3 -6
  18. package/agents/massa-ai-meta-judge.toml +2 -5
  19. package/agents/massa-ai-navigator.toml +1 -1
  20. package/package.json +1 -1
  21. package/skills/agents/judge/SKILL.md +4 -7
  22. package/skills/agents/meta-judge/SKILL.md +3 -6
  23. package/skills/agents/navigator/SKILL.md +2 -2
  24. package/skills/massa-ai/SKILL.md +1 -16
  25. package/skills/massa-ai/references/adr-authoring.md +3 -3
  26. package/skills/massa-ai/references/agent-orchestration.md +17 -2
  27. package/skills/massa-ai/references/architecture-coupling-lens.md +1 -1
  28. package/skills/massa-ai/references/architecture-deepening-lens.md +1 -1
  29. package/skills/massa-ai/references/architecture-domain-lens.md +1 -1
  30. package/skills/massa-ai/references/architecture-lenses.md +1 -1
  31. package/skills/massa-ai/references/audit-report-io.md +32 -2
  32. package/skills/massa-ai/references/audit-scope.md +22 -1
  33. package/skills/massa-ai/references/code-annotation.md +5 -5
  34. package/skills/massa-ai/references/codebase-investigation.md +1 -1
  35. package/skills/massa-ai/references/context-firewall.md +2 -1
  36. package/skills/massa-ai/references/conversation-feedback.md +1 -1
  37. package/skills/massa-ai/references/debug-diagnosis-loop.md +1 -1
  38. package/skills/massa-ai/references/decision-engine.md +1 -1
  39. package/skills/massa-ai/references/evidence-gate.md +1 -1
  40. package/skills/massa-ai/references/figma-pre-analysis.md +3 -3
  41. package/skills/massa-ai/references/furps/analyst-role.md +1 -1
  42. package/skills/massa-ai/references/furps/checklist.md +1 -1
  43. package/skills/massa-ai/references/furps/intake.md +1 -1
  44. package/skills/massa-ai/references/furps/report-contract.md +1 -1
  45. package/skills/massa-ai/references/graceful-degradation.md +22 -0
  46. package/skills/massa-ai/references/hook-enforcement.md +3 -3
  47. package/skills/massa-ai/references/implementation-delivery.md +4 -4
  48. package/skills/massa-ai/references/installation.md +1 -1
  49. package/skills/massa-ai/references/lessons.md +2 -2
  50. package/skills/massa-ai/references/maestro/artifacts-reports.md +1 -1
  51. package/skills/massa-ai/references/maestro/cli-device.md +1 -1
  52. package/skills/massa-ai/references/maestro/cloud.md +1 -1
  53. package/skills/massa-ai/references/maestro/config-env-output.md +1 -1
  54. package/skills/massa-ai/references/maestro/fact-ledger.md +1 -1
  55. package/skills/massa-ai/references/maestro/js-scripting.md +1 -1
  56. package/skills/massa-ai/references/maestro/mcp.md +1 -1
  57. package/skills/massa-ai/references/maestro/patterns.md +1 -1
  58. package/skills/massa-ai/references/maestro/selectors.md +1 -1
  59. package/skills/massa-ai/references/maestro/workspace-execution.md +1 -1
  60. package/skills/massa-ai/references/maestro/yaml-commands.md +1 -1
  61. package/skills/massa-ai/references/maestro.md +1 -1
  62. package/skills/massa-ai/references/mcp-tools.md +2 -2
  63. package/skills/massa-ai/references/memory-policy.md +2 -2
  64. package/skills/massa-ai/references/mobile-context.md +9 -5
  65. package/skills/massa-ai/references/mobile-diagnosis.md +2 -2
  66. package/skills/massa-ai/references/mobile-figma-matcher/android-compose.md +1 -1
  67. package/skills/massa-ai/references/mobile-figma-matcher/android-views.md +1 -1
  68. package/skills/massa-ai/references/mobile-figma-matcher/core.md +1 -1
  69. package/skills/massa-ai/references/mobile-figma-matcher/ios-swiftui.md +1 -1
  70. package/skills/massa-ai/references/mobile-figma-matcher/ios-uikit.md +1 -1
  71. package/skills/massa-ai/references/mobile-figma-matcher/kmp-compose-multiplatform.md +1 -1
  72. package/skills/massa-ai/references/mobile-figma-matcher/repository-detection.md +1 -1
  73. package/skills/massa-ai/references/naming-standards.md +1 -1
  74. package/skills/massa-ai/references/pr-task-fix.md +2 -2
  75. package/skills/massa-ai/references/project-context.md +2 -2
  76. package/skills/massa-ai/references/repo-rules-discovery.md +4 -4
  77. package/skills/massa-ai/references/rfc/discovery-and-sizing.md +1 -1
  78. package/skills/massa-ai/references/rfc/document-contract.md +1 -1
  79. package/skills/massa-ai/references/rfc/quality-and-lifecycle.md +1 -1
  80. package/skills/massa-ai/references/root-cause-scripts.md +2 -2
  81. package/skills/massa-ai/references/sonarqube-mcp.md +73 -0
  82. package/skills/massa-ai/references/spec-driven/artifact-store.md +1 -1
  83. package/skills/massa-ai/references/spec-driven/brownfield-mapping.md +16 -0
  84. package/skills/massa-ai/references/spec-driven/code-analysis.md +1 -1
  85. package/skills/massa-ai/references/spec-driven/coding-principles.md +1 -1
  86. package/skills/massa-ai/references/spec-driven/context-limits.md +1 -1
  87. package/skills/massa-ai/references/spec-driven/design.md +22 -1
  88. package/skills/massa-ai/references/spec-driven/discuss.md +1 -1
  89. package/skills/massa-ai/references/spec-driven/execute.md +3 -1
  90. package/skills/massa-ai/references/spec-driven/memory.md +1 -1
  91. package/skills/massa-ai/references/spec-driven/specify.md +3 -3
  92. package/skills/massa-ai/references/spec-driven/sub-agents.md +1 -1
  93. package/skills/massa-ai/references/spec-driven/tasks.md +1 -1
  94. package/skills/massa-ai/references/spec-driven/validate.md +1 -1
  95. package/skills/massa-ai/references/subagent-design.md +4 -4
  96. package/skills/massa-ai/references/synapse-policy.md +1 -1
  97. package/skills/massa-ai/references/tdd/calibrated-examples.md +1 -1
  98. package/skills/massa-ai/references/tdd/discovery-and-sizing.md +1 -1
  99. package/skills/massa-ai/references/tdd/document-contract.md +1 -1
  100. package/skills/massa-ai/references/tdd/quality-and-lifecycle.md +1 -1
  101. package/skills/massa-ai/references/the-fool/cognitive-bias-inventory.md +1 -1
  102. package/skills/massa-ai/references/the-fool/dialectic-synthesis.md +1 -1
  103. package/skills/massa-ai/references/the-fool/evidence-audit.md +1 -1
  104. package/skills/massa-ai/references/the-fool/pre-mortem-analysis.md +1 -1
  105. package/skills/massa-ai/references/the-fool/red-team-adversarial.md +1 -1
  106. package/skills/massa-ai/references/the-fool/socratic-questioning.md +1 -1
  107. package/skills/massa-ai/references/ticket/atlassian-fix.md +1 -1
  108. package/skills/massa-ai/references/ticket/intake-and-sources.md +1 -1
  109. package/skills/massa-ai/references/ticket/templates-and-quality.md +1 -1
  110. package/skills/massa-ai/references/verification-ladder.md +1 -1
  111. package/skills/massa-ai/scripts/validate_audit_report.ts +382 -0
  112. package/skills/massa-ai/scripts/validate_design.ts +264 -0
  113. package/skills/massa-ai/workflows/adr.md +16 -8
  114. package/skills/massa-ai/workflows/architecture/architecture-audit.md +23 -40
  115. package/skills/massa-ai/workflows/architecture/architecture-fix.md +14 -6
  116. package/skills/massa-ai/workflows/bugs/bugs-audit.md +19 -35
  117. package/skills/massa-ai/workflows/bugs/bugs-fix.md +13 -5
  118. package/skills/massa-ai/workflows/code-quality/code-quality-audit.md +25 -41
  119. package/skills/massa-ai/workflows/code-quality/code-quality-fix.md +13 -5
  120. package/skills/massa-ai/workflows/commit.md +13 -5
  121. package/skills/massa-ai/workflows/debug.md +11 -3
  122. package/skills/massa-ai/workflows/design.md +15 -7
  123. package/skills/massa-ai/workflows/exploration.md +12 -4
  124. package/skills/massa-ai/workflows/feature.md +14 -13
  125. package/skills/massa-ai/workflows/general.md +13 -8
  126. package/skills/massa-ai/workflows/implementation/implementation-audit.md +15 -15
  127. package/skills/massa-ai/workflows/implementation/implementation-fix.md +13 -5
  128. package/skills/massa-ai/workflows/judge-with-debate.md +12 -4
  129. package/skills/massa-ai/workflows/long-session.md +10 -2
  130. package/skills/massa-ai/workflows/maestro/maestro-audit.md +11 -3
  131. package/skills/massa-ai/workflows/maestro/maestro-fix.md +12 -4
  132. package/skills/massa-ai/workflows/maestro/maestro.md +12 -4
  133. package/skills/massa-ai/workflows/mobile-figma/mobile-figma-audit.md +11 -3
  134. package/skills/massa-ai/workflows/mobile-figma/mobile-figma-fix.md +12 -4
  135. package/skills/massa-ai/workflows/onboarding.md +10 -2
  136. package/skills/massa-ai/workflows/refactor.md +12 -4
  137. package/skills/massa-ai/workflows/refinement/furps-refinement.md +12 -4
  138. package/skills/massa-ai/workflows/requirements/requirements-audit.md +19 -36
  139. package/skills/massa-ai/workflows/requirements/requirements-fix.md +13 -5
  140. package/skills/massa-ai/workflows/rfc.md +10 -2
  141. package/skills/massa-ai/workflows/security/security-audit.md +19 -35
  142. package/skills/massa-ai/workflows/security/security-fix.md +13 -5
  143. package/skills/massa-ai/workflows/spec-driven.md +20 -23
  144. package/skills/massa-ai/workflows/tdd.md +10 -2
  145. package/skills/massa-ai/workflows/tests/tests-audit.md +19 -35
  146. package/skills/massa-ai/workflows/tests/tests-fix.md +13 -5
  147. package/skills/massa-ai/workflows/the-fool.md +11 -3
  148. package/skills/massa-ai/workflows/ticket.md +10 -2
@@ -0,0 +1,382 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * validate_audit_report.ts - deterministic schema/ID checks for a saved
4
+ * audit report markdown file (STO-9 top validator pack).
5
+ *
6
+ * Replaces the inline "reread the report and eyeball the finding IDs"
7
+ * checklist prose that used to live in `references/audit-report-io.md` and
8
+ * the 9 `*-fix.md` workflows with a scripted check, parameterized by report
9
+ * family:
10
+ *
11
+ * - metadata field presence (common freshness header + per-family extras)
12
+ * - PREFIX-N finding-ID format (or `<Area>/<PREFIX>-N` for the
13
+ * implementation composite family)
14
+ * - Area<->Prefix table membership (implementation family only) - rejects
15
+ * unrecognized Areas, mismatched Area/Prefix pairs, and raw `SONAR-*`
16
+ * executable IDs
17
+ * - finding-ID uniqueness within the report
18
+ * - gap-free per-prefix (or per-area) sequencing: N, N+1, N+2, ... with no
19
+ * skipped numbers
20
+ *
21
+ * Operates only on the saved report markdown - never on the target
22
+ * codebase - so it stays stack-agnostic. Bun builtins only, zero
23
+ * dependencies, same heuristic-markdown-inspection style as validate_spec.ts.
24
+ *
25
+ * Usage:
26
+ * bun skills/massa-ai/scripts/validate_audit_report.ts <report.md> [--family NAME] [--strict]
27
+ *
28
+ * report.md Path to a saved audit report markdown file.
29
+ * --family One of: architecture | bugs | code-quality | security |
30
+ * requirements | tests | maestro | mobile-figma | implementation
31
+ * Auto-detected from the report's `Workflow:` metadata line
32
+ * when omitted.
33
+ * --strict Treat warnings as errors.
34
+ *
35
+ * Exit codes: 0 pass, 1 errors found (or warnings under --strict), 2 usage error.
36
+ */
37
+
38
+ import { existsSync, readFileSync } from "node:fs";
39
+
40
+ interface FamilyConfig {
41
+ workflow: string;
42
+ /** Single-lens/MFM/MST prefix, or null for the implementation composite family. */
43
+ prefix: string | null;
44
+ extraMetadata: string[];
45
+ }
46
+
47
+ const COMMON_METADATA = [
48
+ "Date",
49
+ "Workflow",
50
+ "ProjectId",
51
+ "WorkflowSessionId",
52
+ "Target",
53
+ "Target Focus",
54
+ "Scope",
55
+ "Git Base",
56
+ "Git Head",
57
+ "Source Evidence Timestamp",
58
+ ];
59
+
60
+ /** Canonical area/prefix pairs (audit-report-io.md, Source-Qualified Finding IDs). */
61
+ const AREA_PREFIX: Array<{ area: string; prefix: string }> = [
62
+ { area: "Correctness", prefix: "BUG" },
63
+ { area: "Architecture", prefix: "ARCH" },
64
+ { area: "Code Quality", prefix: "CQ" },
65
+ { area: "Security", prefix: "SEC" },
66
+ { area: "Requirements", prefix: "REQ" },
67
+ { area: "Tests", prefix: "TST" },
68
+ ];
69
+
70
+ const FAMILIES: Record<string, FamilyConfig> = {
71
+ architecture: { workflow: "architecture-audit", prefix: "ARCH", extraMetadata: ["Requirements Source"] },
72
+ bugs: { workflow: "bugs-audit", prefix: "BUG", extraMetadata: ["Requirements Source"] },
73
+ "code-quality": { workflow: "code-quality-audit", prefix: "CQ", extraMetadata: ["Requirements Source"] },
74
+ security: { workflow: "security-audit", prefix: "SEC", extraMetadata: ["Requirements Source"] },
75
+ requirements: { workflow: "requirements-audit", prefix: "REQ", extraMetadata: ["Requirements Source"] },
76
+ tests: { workflow: "tests-audit", prefix: "TST", extraMetadata: ["Requirements Source"] },
77
+ maestro: {
78
+ workflow: "maestro-audit",
79
+ prefix: "MST",
80
+ extraMetadata: ["Scenario Source", "Maestro CLI", "Device/Emulator Readiness"],
81
+ },
82
+ "mobile-figma": {
83
+ workflow: "mobile-figma-audit",
84
+ prefix: "MFM",
85
+ extraMetadata: ["Repository Classification", "Figma Source", "Figma Evidence Timestamp", "Requirements Source"],
86
+ },
87
+ implementation: { workflow: "implementation-audit", prefix: null, extraMetadata: ["Requirements Source"] },
88
+ };
89
+
90
+ /** Mirrors Python's str.splitlines(): universal newline split, no trailing empty element. */
91
+ function splitLines(text: string): string[] {
92
+ if (text === "") return [];
93
+ const result: string[] = [];
94
+ const lineBreakRe = /\r\n|\r|\n/g;
95
+ let start = 0;
96
+ let match: RegExpExecArray | null;
97
+ while ((match = lineBreakRe.exec(text)) !== null) {
98
+ result.push(text.slice(start, match.index));
99
+ start = match.index + match[0].length;
100
+ }
101
+ if (start < text.length) result.push(text.slice(start));
102
+ return result;
103
+ }
104
+
105
+ /** Metadata lines live between the `# Title` line and the first `##` heading. */
106
+ function metadataBounds(lines: string[]): [number, number] {
107
+ let start = 0;
108
+ for (let i = 0; i < lines.length; i++) {
109
+ if (/^#\s+\S/.test(lines[i]!)) {
110
+ start = i + 1;
111
+ break;
112
+ }
113
+ }
114
+ let end = lines.length;
115
+ for (let i = start; i < lines.length; i++) {
116
+ if (/^##\s+\S/.test(lines[i]!)) {
117
+ end = i;
118
+ break;
119
+ }
120
+ }
121
+ return [start, end];
122
+ }
123
+
124
+ function parseMetadata(lines: string[]): Map<string, string> {
125
+ const [start, end] = metadataBounds(lines);
126
+ const fields = new Map<string, string>();
127
+ const lineRe = /^([A-Za-z][A-Za-z /]*?):\s*(.*)$/;
128
+ for (let i = start; i < end; i++) {
129
+ const m = lineRe.exec(lines[i]!.trim());
130
+ if (m) fields.set(m[1]!.trim(), m[2]!.trim());
131
+ }
132
+ return fields;
133
+ }
134
+
135
+ const PLACEHOLDER_RE = /^<.*>$/;
136
+
137
+ function checkMetadata(fields: Map<string, string>, required: string[], errors: string[]): void {
138
+ for (const name of required) {
139
+ const value = fields.get(name);
140
+ if (value === undefined || value === "") {
141
+ errors.push(`missing required metadata field: ${name}`);
142
+ continue;
143
+ }
144
+ if (PLACEHOLDER_RE.test(value)) {
145
+ // "Requirements Source" and similar fields explicitly allow the
146
+ // literal value "n/a"; only an unfilled `<template>` token is an error.
147
+ errors.push(`metadata field '${name}' still has an unfilled template value: ${value}`);
148
+ }
149
+ }
150
+ }
151
+
152
+ interface RawFinding {
153
+ raw: string;
154
+ area: string | null;
155
+ prefix: string;
156
+ num: number | null;
157
+ line: number;
158
+ }
159
+
160
+ const FINDING_HEADER_RE = /^###\s+([^:]+):/;
161
+ /** `<PREFIX>-<N>` (single-lens/MFM/MST) or `<Area>/<PREFIX>-<N>` (implementation composite). */
162
+ const SIMPLE_ID_RE = /^([A-Z][A-Z]*)-(\d+)$/;
163
+ const COMPOSITE_ID_RE = /^([A-Za-z][A-Za-z ]*?)\/([A-Z][A-Z]*)-(\d+)$/;
164
+
165
+ /** Every `### <id>: <title>` header inside the `## Findings` section, parsed best-effort. */
166
+ function parseFindings(lines: string[], composite: boolean): RawFinding[] {
167
+ const findings: RawFinding[] = [];
168
+ let inFindings = false;
169
+ for (let i = 0; i < lines.length; i++) {
170
+ const line = lines[i]!;
171
+ if (/^##\s+Findings\s*$/.test(line.trim())) {
172
+ inFindings = true;
173
+ continue;
174
+ }
175
+ if (inFindings && /^##\s+\S/.test(line)) {
176
+ inFindings = false;
177
+ continue;
178
+ }
179
+ if (!inFindings) continue;
180
+ const m = FINDING_HEADER_RE.exec(line.trim());
181
+ if (!m) continue;
182
+ const idText = m[1]!.trim();
183
+ if (composite) {
184
+ const cm = COMPOSITE_ID_RE.exec(idText);
185
+ if (cm) {
186
+ findings.push({ raw: idText, area: cm[1]!.trim(), prefix: cm[2]!, num: Number(cm[3]), line: i + 1 });
187
+ } else {
188
+ findings.push({ raw: idText, area: null, prefix: "", num: null, line: i + 1 });
189
+ }
190
+ } else {
191
+ const sm = SIMPLE_ID_RE.exec(idText);
192
+ if (sm) {
193
+ findings.push({ raw: idText, area: null, prefix: sm[1]!, num: Number(sm[2]), line: i + 1 });
194
+ } else {
195
+ findings.push({ raw: idText, area: null, prefix: "", num: null, line: i + 1 });
196
+ }
197
+ }
198
+ }
199
+ return findings;
200
+ }
201
+
202
+ function checkFindingIds(
203
+ findings: RawFinding[],
204
+ family: FamilyConfig,
205
+ composite: boolean,
206
+ errors: string[],
207
+ ): void {
208
+ const seen = new Map<string, number>();
209
+ const byGroup = new Map<string, number[]>();
210
+
211
+ for (const f of findings) {
212
+ if (f.num === null) {
213
+ errors.push(`L${f.line}: finding id '${f.raw}' is not in the expected format`);
214
+ continue;
215
+ }
216
+ if (f.prefix.startsWith("SONAR")) {
217
+ errors.push(`L${f.line}: raw '${f.raw}' is not an executable finding id - normalize SonarQube output to a supported source-qualified id first`);
218
+ continue;
219
+ }
220
+
221
+ if (composite) {
222
+ const areaEntry = AREA_PREFIX.find((a) => a.area === f.area);
223
+ if (!areaEntry) {
224
+ errors.push(`L${f.line}: '${f.area}' is not a recognized Area (expected one of ${AREA_PREFIX.map((a) => a.area).join(", ")})`);
225
+ continue;
226
+ }
227
+ if (areaEntry.prefix !== f.prefix) {
228
+ errors.push(`L${f.line}: '${f.raw}' mixes Area '${f.area}' with prefix '${f.prefix}' (expected '${areaEntry.prefix}-N')`);
229
+ continue;
230
+ }
231
+ } else if (family.prefix !== null && f.prefix !== family.prefix) {
232
+ errors.push(`L${f.line}: '${f.raw}' uses prefix '${f.prefix}', expected '${family.prefix}-N' for this report family`);
233
+ continue;
234
+ }
235
+
236
+ const key = composite ? `${f.area}/${f.prefix}` : f.prefix;
237
+ const prior = seen.get(f.raw);
238
+ if (prior !== undefined) {
239
+ errors.push(`L${f.line}: duplicate finding id '${f.raw}' (first seen at L${prior})`);
240
+ } else {
241
+ seen.set(f.raw, f.line);
242
+ }
243
+ if (!byGroup.has(key)) byGroup.set(key, []);
244
+ byGroup.get(key)!.push(f.num);
245
+ }
246
+
247
+ for (const [key, nums] of byGroup) {
248
+ const sorted = [...new Set(nums)].sort((a, b) => a - b);
249
+ for (let expected = 1; expected <= sorted[sorted.length - 1]!; expected++) {
250
+ if (!sorted.includes(expected)) {
251
+ errors.push(`${key} finding numbering has a gap: missing ${key}-${expected} (present: ${sorted.map((n) => `${key}-${n}`).join(", ")})`);
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ function detectFamily(fields: Map<string, string>): string | null {
258
+ const workflow = fields.get("Workflow");
259
+ if (!workflow) return null;
260
+ for (const [name, cfg] of Object.entries(FAMILIES)) {
261
+ if (cfg.workflow === workflow) return name;
262
+ }
263
+ return null;
264
+ }
265
+
266
+ interface CheckResult {
267
+ errors: string[];
268
+ warnings: string[];
269
+ family: string | null;
270
+ findingCount: number;
271
+ }
272
+
273
+ function check(reportPath: string, familyArg: string | null): CheckResult {
274
+ const text = readFileSync(reportPath, "utf-8");
275
+ const lines = splitLines(text);
276
+ const errors: string[] = [];
277
+ const warnings: string[] = [];
278
+
279
+ const fields = parseMetadata(lines);
280
+ const family = familyArg ?? detectFamily(fields);
281
+ if (!family || !(family in FAMILIES)) {
282
+ errors.push(
283
+ `could not determine report family (pass --family or a recognized 'Workflow:' value); known families: ${Object.keys(FAMILIES).join(", ")}`,
284
+ );
285
+ return { errors, warnings, family: null, findingCount: 0 };
286
+ }
287
+ const cfg = FAMILIES[family]!;
288
+ const composite = cfg.prefix === null;
289
+
290
+ checkMetadata(fields, [...COMMON_METADATA, ...cfg.extraMetadata], errors);
291
+
292
+ const declaredWorkflow = fields.get("Workflow");
293
+ if (declaredWorkflow !== undefined && declaredWorkflow !== cfg.workflow) {
294
+ errors.push(`Workflow field is '${declaredWorkflow}', expected '${cfg.workflow}' for family '${family}'`);
295
+ }
296
+
297
+ const findings = parseFindings(lines, composite);
298
+ checkFindingIds(findings, cfg, composite, errors);
299
+
300
+ return { errors, warnings, family, findingCount: findings.length };
301
+ }
302
+
303
+ const USAGE = "usage: validate_audit_report.ts [-h] [--family NAME] [--strict] <report.md>";
304
+ const HELP = `${USAGE}
305
+
306
+ Deterministic schema/ID checks for a saved audit report markdown file.
307
+
308
+ positional arguments:
309
+ report.md Path to a saved audit report markdown file
310
+
311
+ options:
312
+ -h, --help show this help message and exit
313
+ --family NAME One of: ${Object.keys(FAMILIES).join(", ")}
314
+ --strict Treat warnings as errors`;
315
+
316
+ interface Args {
317
+ target: string | null;
318
+ family: string | null;
319
+ strict: boolean;
320
+ }
321
+
322
+ function printUsageError(msg: string): void {
323
+ process.stderr.write(`${USAGE}\nvalidate_audit_report.ts: error: ${msg}\n`);
324
+ }
325
+
326
+ function parseArgs(argv: string[]): Args | null {
327
+ let family: string | null = null;
328
+ let strict = false;
329
+ const positionals: string[] = [];
330
+ for (let i = 0; i < argv.length; i++) {
331
+ const a = argv[i]!;
332
+ if (a === "--family") {
333
+ if (i + 1 >= argv.length) {
334
+ printUsageError("argument --family: expected one argument");
335
+ return null;
336
+ }
337
+ family = argv[++i]!;
338
+ } else if (a.startsWith("--family=")) {
339
+ family = a.slice("--family=".length);
340
+ } else if (a === "--strict") {
341
+ strict = true;
342
+ } else if (a === "-h" || a === "--help") {
343
+ console.log(HELP);
344
+ process.exit(0);
345
+ } else if (a.startsWith("-") && a !== "-") {
346
+ printUsageError(`unrecognized arguments: ${a}`);
347
+ return null;
348
+ } else {
349
+ positionals.push(a);
350
+ }
351
+ }
352
+ if (positionals.length !== 1) {
353
+ printUsageError("exactly one report.md path is required");
354
+ return null;
355
+ }
356
+ return { target: positionals[0]!, family, strict };
357
+ }
358
+
359
+ function main(argv: string[]): number {
360
+ const args = parseArgs(argv);
361
+ if (args === null) return 2;
362
+
363
+ if (!args.target || !existsSync(args.target)) {
364
+ console.error(`validate_audit_report: report not found: ${args.target}`);
365
+ return 2;
366
+ }
367
+
368
+ const { errors, warnings, family, findingCount } = check(args.target, args.family);
369
+ for (const w of warnings) console.log(` WARN ${w}`);
370
+ for (const e of errors) console.log(` ERROR ${e}`);
371
+ const fail = errors.length > 0 || (warnings.length > 0 && args.strict);
372
+ console.log(
373
+ `\nvalidate_audit_report: ${errors.length} error(s), ${warnings.length} warning(s), family=${family ?? "unknown"}, findings=${findingCount} in ${args.target}`,
374
+ );
375
+ return fail ? 1 : 0;
376
+ }
377
+
378
+ if (import.meta.main) {
379
+ process.exit(main(process.argv.slice(2)));
380
+ }
381
+
382
+ export { check, FAMILIES, AREA_PREFIX };
@@ -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 };
@@ -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`.