@massa-ai/cursor-plugin 1.19.0 → 1.21.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 (30) hide show
  1. package/.cursor-plugin/plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/skills/agents/audit-specialist/SKILL.md +1 -1
  4. package/skills/agents/context-curator/SKILL.md +1 -1
  5. package/skills/agents/furps-analyst/SKILL.md +1 -1
  6. package/skills/agents/investigator/SKILL.md +1 -1
  7. package/skills/agents/mobile-specialist/SKILL.md +1 -1
  8. package/skills/agents/navigator/SKILL.md +1 -1
  9. package/skills/agents/requirements-analyst/SKILL.md +1 -1
  10. package/skills/agents/reviewer/SKILL.md +1 -1
  11. package/skills/agents/verification-agent/SKILL.md +1 -1
  12. package/skills/massa-ai/SKILL.md +10 -0
  13. package/skills/massa-ai/references/implementation-delivery.md +12 -1
  14. package/skills/massa-ai/references/spec-driven/coding-principles.md +16 -0
  15. package/skills/massa-ai/references/spec-driven/design.md +2 -2
  16. package/skills/massa-ai/references/spec-driven/discuss.md +35 -12
  17. package/skills/massa-ai/references/spec-driven/execute.md +41 -24
  18. package/skills/massa-ai/references/spec-driven/memory.md +12 -2
  19. package/skills/massa-ai/references/spec-driven/specify.md +30 -12
  20. package/skills/massa-ai/references/spec-driven/sub-agents.md +33 -6
  21. package/skills/massa-ai/references/spec-driven/tasks.md +8 -6
  22. package/skills/massa-ai/references/spec-driven/validate.md +15 -10
  23. package/skills/massa-ai/scripts/check_commit.py +128 -0
  24. package/skills/massa-ai/scripts/check_specs_delivered.py +137 -0
  25. package/skills/massa-ai/scripts/lessons.py +44 -4
  26. package/skills/massa-ai/scripts/validate_spec.py +272 -0
  27. package/skills/massa-ai/scripts/validate_state.py +183 -0
  28. package/skills/massa-ai/scripts/validate_tasks.py +302 -0
  29. package/skills/massa-ai/workflows/exploration.md +1 -1
  30. package/skills/massa-ai/workflows/spec-driven.md +5 -4
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ validate_spec.py - deterministic closure-gate checks for a feature spec.md.
4
+
5
+ Turns the Requirement Closure Gate (Specify phase) into a checkable pass/fail
6
+ run BEFORE a spec is presented for confirmation, instead of trusting the model
7
+ to remember the checks. Pure standard library, zero dependencies. Operates only
8
+ on the spec.md markdown artifact - never on the target codebase - so it stays
9
+ stack-agnostic and tool-agnostic.
10
+
11
+ What it checks (heuristic markdown inspection, not a full parser):
12
+ ERROR - a required section is missing
13
+ ERROR - an acceptance criterion has no SHALL (not testable / not EARS-shaped)
14
+ ERROR - an Assumptions row has an empty "Chosen default" or "Rationale" cell
15
+ ERROR - a Requirement Traceability row has a malformed ID
16
+ WARN - an AC has SHALL but no recognizable EARS lead keyword
17
+ WARN - template placeholder rows are still present (spec not filled in)
18
+ WARN - open questions are not explicitly resolved
19
+
20
+ Usage:
21
+ python3 skills/massa-ai/scripts/validate_spec.py [target] [--root DIR] [--strict]
22
+
23
+ Invoke with the repo-root-relative script path shown above (matches
24
+ lessons.py's convention), not a project-local copy.
25
+ target Path to a spec.md, a feature directory, or a project root.
26
+ Omitted -> auto-detect the single feature under <root>/.specs/features/.
27
+ --root Project root that contains .specs/ (default: current dir).
28
+ --strict Treat warnings as errors.
29
+
30
+ Exit codes: 0 pass, 1 errors found (or warnings under --strict), 2 usage error.
31
+ """
32
+
33
+ import argparse
34
+ import os
35
+ import re
36
+ import sys
37
+
38
+ REQUIRED_SECTIONS = [
39
+ "Problem Statement",
40
+ "Out of Scope",
41
+ "Assumptions & Open Questions",
42
+ "User Stories",
43
+ "Requirement Traceability",
44
+ ]
45
+
46
+ ID_RE = re.compile(r"^[A-Z][A-Z0-9]*-\d+$")
47
+ PLACEHOLDER_RE = re.compile(r"^\s*\[.+\]\s*$")
48
+ STATUS_VALUES = {"pending", "in design", "in tasks", "implementing", "verified"}
49
+
50
+
51
+ def resolve_spec(target, root):
52
+ """Return the path to a spec.md from a file, dir, or auto-detect."""
53
+ if target:
54
+ if os.path.isfile(target):
55
+ return target
56
+ if os.path.isdir(target):
57
+ cand = os.path.join(target, "spec.md")
58
+ if os.path.isfile(cand):
59
+ return cand
60
+ # maybe it's a project root
61
+ return _autodetect(target)
62
+ # Not a path: treat as a feature name under <root>/.specs/features/<name>/
63
+ cand = os.path.join(root, ".specs", "features", target, "spec.md")
64
+ if os.path.isfile(cand):
65
+ return cand
66
+ return None
67
+ return _autodetect(root)
68
+
69
+
70
+ def _autodetect(root):
71
+ base = os.path.join(root, ".specs", "features")
72
+ if not os.path.isdir(base):
73
+ return None
74
+ features = [
75
+ d for d in sorted(os.listdir(base))
76
+ if os.path.isfile(os.path.join(base, d, "spec.md"))
77
+ ]
78
+ if len(features) == 1:
79
+ return os.path.join(base, features[0], "spec.md")
80
+ if len(features) == 0:
81
+ return None
82
+ # Ambiguous: signal the caller with the list.
83
+ raise SystemExit(
84
+ "validate_spec: multiple features found; pass one explicitly:\n "
85
+ + "\n ".join(os.path.join(base, f, "spec.md") for f in features)
86
+ )
87
+
88
+
89
+ def split_row(line):
90
+ cells = line.strip().strip("|").split("|")
91
+ return [c.strip() for c in cells]
92
+
93
+
94
+ def is_separator(line):
95
+ return bool(re.match(r"^\s*\|?[\s:|-]+\|?\s*$", line)) and "-" in line
96
+
97
+
98
+ def section_bounds(lines, name):
99
+ """Return (start, end) line indices for a `## name` section body."""
100
+ start = None
101
+ for i, ln in enumerate(lines):
102
+ if re.match(r"^#{1,3}\s+" + re.escape(name) + r"\s*$", ln.strip()):
103
+ start = i + 1
104
+ break
105
+ if start is None:
106
+ return None
107
+ end = len(lines)
108
+ for j in range(start, len(lines)):
109
+ if re.match(r"^#{1,3}\s+\S", lines[j]):
110
+ end = j
111
+ break
112
+ return (start, end)
113
+
114
+
115
+ def classify_ears(text):
116
+ """Return (ok, note). ok requires a SHALL; note records the EARS pattern."""
117
+ t = text.strip()
118
+ low = t.lower()
119
+ has_shall = bool(re.search(r"\bshall\b", low))
120
+ if not has_shall:
121
+ return (False, "no SHALL")
122
+ kws = []
123
+ if re.search(r"\bwhile\b", low):
124
+ kws.append("WHILE")
125
+ if re.search(r"\bwhen\b", low):
126
+ kws.append("WHEN")
127
+ if re.match(r"^\s*if\b", low) or re.search(r"\bif\b.*\bthen\b", low):
128
+ kws.append("IF/THEN")
129
+ if re.search(r"\bwhere\b", low):
130
+ kws.append("WHERE")
131
+ if len(kws) >= 2:
132
+ return (True, "complex (" + "+".join(kws) + ")")
133
+ if kws:
134
+ pattern = {
135
+ "WHILE": "state-driven",
136
+ "WHEN": "event-driven",
137
+ "IF/THEN": "unwanted-behavior",
138
+ "WHERE": "optional-feature",
139
+ }[kws[0]]
140
+ return (True, pattern)
141
+ if re.match(r"^\s*the\b", low):
142
+ return (True, "ubiquitous")
143
+ return (True, "warn: SHALL present but no EARS lead keyword")
144
+
145
+
146
+ def check(spec_path):
147
+ with open(spec_path, "r", encoding="utf-8") as f:
148
+ text = f.read()
149
+ lines = text.splitlines()
150
+ errors, warnings = [], []
151
+
152
+ # 1. Required sections.
153
+ for name in REQUIRED_SECTIONS:
154
+ if section_bounds(lines, name) is None:
155
+ errors.append(f"missing required section: ## {name}")
156
+
157
+ # 2. Acceptance criteria are EARS-shaped (have a SHALL).
158
+ #
159
+ # massa-ai patch (beyond D1): upstream terminated the AC scan on the FIRST
160
+ # blank line after the "**Acceptance Criteria**:" header, before any item
161
+ # was ever read - massa-ai's (and TLC's own) template puts a blank line
162
+ # between the header and the numbered list, so the SHALL check was a
163
+ # silent no-op against every realistically-formatted spec. Track whether
164
+ # an item has been seen and only let a blank line end the block once it
165
+ # has, so leading blank lines are skipped instead of ending the scan.
166
+ in_ac = False
167
+ seen_item = False
168
+ for i, ln in enumerate(lines, start=1):
169
+ stripped = ln.strip()
170
+ if re.match(r"^\*{0,2}Acceptance Criteria\*{0,2}\s*:?\s*$", stripped):
171
+ in_ac = True
172
+ seen_item = False
173
+ continue
174
+ if in_ac:
175
+ m = re.match(r"^\s*\d+\.\s+(.*)$", ln)
176
+ if m:
177
+ seen_item = True
178
+ item = m.group(1).strip()
179
+ if PLACEHOLDER_RE.match(item):
180
+ continue # untouched template row
181
+ ok, note = classify_ears(item)
182
+ if not ok:
183
+ errors.append(f"L{i}: acceptance criterion has no SHALL (not testable): {item[:70]}")
184
+ elif note.startswith("warn"):
185
+ warnings.append(f"L{i}: AC has SHALL but no EARS keyword (WHEN/WHILE/WHERE/IF or ubiquitous 'The … shall'): {item[:60]}")
186
+ elif re.match(r"^#{1,3}\s", ln) or stripped.startswith("**") or (stripped == "" and seen_item):
187
+ in_ac = False
188
+
189
+ # 3. Assumptions table cells filled.
190
+ b = section_bounds(lines, "Assumptions & Open Questions")
191
+ if b:
192
+ rows = [lines[i] for i in range(*b) if lines[i].strip().startswith("|")]
193
+ data = [r for r in rows if not is_separator(r)]
194
+ # drop the header row (first table row)
195
+ if data:
196
+ data = data[1:]
197
+ template_seen = False
198
+ for r in data:
199
+ cells = split_row(r)
200
+ if len(cells) < 3:
201
+ continue
202
+ assumption, chosen, rationale = cells[0], cells[1], cells[2]
203
+ if PLACEHOLDER_RE.match(assumption) and PLACEHOLDER_RE.match(chosen):
204
+ template_seen = True
205
+ continue
206
+ if not chosen or PLACEHOLDER_RE.match(chosen):
207
+ errors.append(f"assumption '{assumption[:40]}' has empty 'Chosen default'")
208
+ if not rationale or PLACEHOLDER_RE.match(rationale):
209
+ errors.append(f"assumption '{assumption[:40]}' has empty 'Rationale'")
210
+ if template_seen:
211
+ warnings.append("Assumptions table still contains template placeholder rows")
212
+ # open questions line
213
+ oq = [lines[i] for i in range(*b) if "open questions" in lines[i].lower()]
214
+ oq_clean = re.sub(r"[*_]", "", " ".join(oq)).lower()
215
+ if not oq:
216
+ warnings.append("no 'Open questions:' line in Assumptions section")
217
+ elif not re.search(r"open questions.*:\s*none", oq_clean):
218
+ warnings.append("open questions do not read as resolved ('Open questions: none')")
219
+
220
+ # 4. Requirement traceability IDs.
221
+ b = section_bounds(lines, "Requirement Traceability")
222
+ if b:
223
+ rows = [lines[i] for i in range(*b) if lines[i].strip().startswith("|")]
224
+ data = [r for r in rows if not is_separator(r)]
225
+ if data:
226
+ data = data[1:]
227
+ template_seen = False
228
+ real_ids = 0
229
+ for r in data:
230
+ cells = split_row(r)
231
+ if not cells:
232
+ continue
233
+ rid = cells[0]
234
+ if PLACEHOLDER_RE.match(rid) or "[" in rid:
235
+ template_seen = True
236
+ continue
237
+ if not rid:
238
+ continue
239
+ if not ID_RE.match(rid):
240
+ errors.append(f"malformed requirement ID: '{rid}' (expected e.g. AUTH-01)")
241
+ else:
242
+ real_ids += 1
243
+ if template_seen and real_ids == 0:
244
+ warnings.append("Requirement Traceability has only template rows (no real IDs yet)")
245
+
246
+ return errors, warnings
247
+
248
+
249
+ def main(argv=None):
250
+ p = argparse.ArgumentParser(prog="validate_spec.py", description="Closure-gate checks for a feature spec.md.")
251
+ p.add_argument("target", nargs="?", default=None)
252
+ p.add_argument("--root", default=".")
253
+ p.add_argument("--strict", action="store_true")
254
+ args = p.parse_args(argv)
255
+
256
+ spec = resolve_spec(args.target, args.root)
257
+ if not spec:
258
+ print("validate_spec: could not locate a spec.md. Pass a path or run from the project root.", file=sys.stderr)
259
+ return 2
260
+
261
+ errors, warnings = check(spec)
262
+ for w in warnings:
263
+ print(f" WARN {w}")
264
+ for e in errors:
265
+ print(f" ERROR {e}")
266
+ fail = errors or (warnings and args.strict)
267
+ print(f"\nvalidate_spec: {len(errors)} error(s), {len(warnings)} warning(s) in {spec}")
268
+ return 1 if fail else 0
269
+
270
+
271
+ if __name__ == "__main__":
272
+ raise SystemExit(main())
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ validate_state.py - deterministic completion gate for a feature.
4
+
5
+ The skill's strongest invariant is "the Verifier is always-on, never prompted;
6
+ Execute is not done until validation.md reports PASS." That is prose the model
7
+ must remember. This turns it into a checkable pass/fail the closing step runs
8
+ automatically, so declaring a feature done without a real Verifier report fails
9
+ loudly instead of slipping through.
10
+
11
+ It does NOT merely check that validation.md exists - a report that exists but is
12
+ empty, still holds the template placeholder, or has no evidence would pass a
13
+ shallow existence check while proving nothing. This gate requires a real,
14
+ filled verdict plus at least one file:line evidence citation.
15
+
16
+ Operates only on the .specs/ markdown artifacts (stack- and tool-agnostic). No
17
+ dependencies. Run from the project root (the dir that contains .specs), or pass
18
+ --root. Meant to be invoked by the skill as the closing gate of Execute, the
19
+ same way lessons.py is invoked at distillation - not a manual step.
20
+
21
+ Usage:
22
+ python3 skills/massa-ai/scripts/validate_state.py [feature]
23
+ python3 skills/massa-ai/scripts/validate_state.py
24
+
25
+ Invoke with the repo-root-relative script path shown above (matches
26
+ lessons.py's convention), not a project-local copy. Pass --root when cwd is
27
+ not the project that contains .specs/.
28
+
29
+ Exit codes: 0 ok, 1 a completed feature is missing a real PASS report,
30
+ 2 usage error.
31
+ """
32
+
33
+ import argparse
34
+ import os
35
+ import re
36
+ import sys
37
+
38
+ # A file:line citation: a path with an extension, then :<line>. e.g. src/a.ts:42
39
+ EVIDENCE_RE = re.compile(r"[\w./-]+\.[A-Za-z0-9]+:\d+")
40
+
41
+
42
+ def _feature_dirs(root):
43
+ base = os.path.join(root, ".specs", "features")
44
+ if not os.path.isdir(base):
45
+ return base, []
46
+ dirs = [
47
+ d for d in sorted(os.listdir(base))
48
+ if os.path.isdir(os.path.join(base, d))
49
+ ]
50
+ return base, dirs
51
+
52
+
53
+ def _verdict(text):
54
+ """Return 'pass', 'fail', 'unfilled', or None from a validation report."""
55
+ lines = text.splitlines()
56
+ # Scope to the '## Summary' section when it carries its own Result line:
57
+ # the Discrimination Sensor's per-mutation `**Result**:` sub-line elsewhere
58
+ # in the report can carry the opposite word (sensor PASS, overall FAIL) and
59
+ # must not collide with the report verdict.
60
+ summary_lines = []
61
+ in_summary = False
62
+ for ln in lines:
63
+ stripped = ln.strip()
64
+ if re.match(r"^#{1,4}\s*summary\b", stripped, re.IGNORECASE):
65
+ in_summary = True
66
+ continue
67
+ if in_summary and re.match(r"^#{1,4}\s", stripped):
68
+ break
69
+ if in_summary:
70
+ summary_lines.append(ln)
71
+ result_re = r"\*{0,2}result\*{0,2}\s*:"
72
+ if any(re.search(result_re, ln.strip(), re.IGNORECASE) for ln in summary_lines):
73
+ scope = summary_lines
74
+ else:
75
+ scope = lines
76
+ # Look at the '## Validation' heading first, then a '**Result**' line.
77
+ candidates = [
78
+ ln for ln in scope
79
+ if re.search(r"^#{1,4}\s*validation\b", ln.strip(), re.IGNORECASE)
80
+ or re.search(result_re, ln.strip(), re.IGNORECASE)
81
+ ]
82
+ hay = " ".join(candidates) if candidates else "\n".join(scope)
83
+ has_pass = re.search(r"\bPASS\b", hay) is not None
84
+ has_fail = re.search(r"\bFAIL\b", hay) is not None
85
+ if has_pass and has_fail:
86
+ # Both present on the verdict line = unfilled template "[PASS | FAIL]".
87
+ return "unfilled"
88
+ if has_pass:
89
+ return "pass"
90
+ if has_fail:
91
+ return "fail"
92
+ return None
93
+
94
+
95
+ def _appears_complete(fdir):
96
+ """Conservative completeness heuristic for the cross-check mode.
97
+
98
+ A feature 'appears complete' if it already has a validation.md, or if it has
99
+ a tasks.md with at least one task and no unchecked '- [ ]' boxes left. When
100
+ the signal is ambiguous (no tasks.md, Tasks phase skipped), returns False so
101
+ an in-flight feature is never falsely flagged.
102
+ """
103
+ if os.path.exists(os.path.join(fdir, "validation.md")):
104
+ return True
105
+ tasks = os.path.join(fdir, "tasks.md")
106
+ if not os.path.exists(tasks):
107
+ return False
108
+ body = open(tasks, encoding="utf-8", errors="replace").read()
109
+ if not re.search(r"^#{2,4}\s+T\d+\s*:", body, re.MULTILINE):
110
+ return False
111
+ if re.search(r"^\s*-\s*\[\s\]", body, re.MULTILINE):
112
+ return False # unchecked box remains -> still in progress
113
+ return True
114
+
115
+
116
+ def _check_feature(fdir, name):
117
+ """Return list of error strings for one feature (empty = pass)."""
118
+ errors = []
119
+ vpath = os.path.join(fdir, "validation.md")
120
+ if not os.path.exists(vpath):
121
+ errors.append(
122
+ f"{name}: no validation.md - Execute is not done until the Verifier "
123
+ f"writes it (author != verifier). Dispatch validation before marking done."
124
+ )
125
+ return errors
126
+ text = open(vpath, encoding="utf-8", errors="replace").read()
127
+ verdict = _verdict(text)
128
+ if verdict is None:
129
+ errors.append(f"{name}: validation.md has no PASS/FAIL verdict (a prose-only report does not count)")
130
+ elif verdict == "unfilled":
131
+ errors.append(f"{name}: validation.md verdict is still the template placeholder '[PASS | FAIL]' - not filled")
132
+ elif verdict == "fail":
133
+ errors.append(f"{name}: validation.md verdict is FAIL - route the ranked gaps to fix tasks, then re-verify (feature is not done)")
134
+ if verdict == "pass" and not EVIDENCE_RE.search(text):
135
+ errors.append(f"{name}: validation.md is PASS but cites no file:line evidence - evidence-or-zero not satisfied")
136
+ return errors
137
+
138
+
139
+ def _resolve(root, feature):
140
+ base, dirs = _feature_dirs(root)
141
+ if not os.path.isdir(base):
142
+ print(f"validate_state: no {base} directory - nothing to check.")
143
+ return []
144
+ if feature:
145
+ fdir = feature if os.path.isdir(feature) else os.path.join(base, feature)
146
+ if not os.path.isdir(fdir):
147
+ print(f"validate_state: feature not found: {feature}", file=sys.stderr)
148
+ raise SystemExit(2)
149
+ return [(fdir, os.path.basename(fdir.rstrip("/")))]
150
+ if len(dirs) == 1:
151
+ return [(os.path.join(base, dirs[0]), dirs[0])]
152
+ if not dirs:
153
+ print("validate_state: no features under .specs/features/ - nothing to check.")
154
+ return []
155
+ # Cross-check mode: only features that appear complete.
156
+ picked = [(os.path.join(base, d), d) for d in dirs if _appears_complete(os.path.join(base, d))]
157
+ if not picked:
158
+ print("validate_state: no completed feature detected (all in progress) - nothing to gate.")
159
+ return picked
160
+
161
+
162
+ def main(argv=None):
163
+ p = argparse.ArgumentParser(prog="validate_state.py", description="Deterministic completion gate: a done feature must have a real PASS validation report.")
164
+ p.add_argument("feature", nargs="?", default=None, help="Feature dir or name (default: sole feature, else cross-check all completed)")
165
+ p.add_argument("--root", default=".", help="Project root containing .specs/ (default: current dir)")
166
+ args = p.parse_args(argv)
167
+ root = os.path.abspath(args.root)
168
+
169
+ targets = _resolve(root, args.feature)
170
+ all_errors = []
171
+ for fdir, name in targets:
172
+ all_errors += _check_feature(fdir, name)
173
+
174
+ for e in all_errors:
175
+ print(f" ERROR {e}")
176
+ n = len(all_errors)
177
+ checked = ", ".join(name for _, name in targets) or "(none)"
178
+ print(f"\nvalidate_state: {n} error(s) across [{checked}]")
179
+ return 1 if n else 0
180
+
181
+
182
+ if __name__ == "__main__":
183
+ raise SystemExit(main())