@massa-ai/cursor-plugin 1.6.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 (172) hide show
  1. package/.cursor-plugin/plugin.json +5 -0
  2. package/README.md +87 -0
  3. package/agents/massa-ai-architecture-specialist.md +65 -0
  4. package/agents/massa-ai-audit-specialist.md +81 -0
  5. package/agents/massa-ai-builder.md +67 -0
  6. package/agents/massa-ai-context-curator.md +67 -0
  7. package/agents/massa-ai-documentation-agent.md +65 -0
  8. package/agents/massa-ai-furps-analyst.md +71 -0
  9. package/agents/massa-ai-investigator.md +68 -0
  10. package/agents/massa-ai-mobile-specialist.md +82 -0
  11. package/agents/massa-ai-navigator.md +75 -0
  12. package/agents/massa-ai-plan-critic.md +90 -0
  13. package/agents/massa-ai-planner.md +65 -0
  14. package/agents/massa-ai-requirements-analyst.md +64 -0
  15. package/agents/massa-ai-reviewer.md +66 -0
  16. package/agents/massa-ai-test-engineer.md +66 -0
  17. package/agents/massa-ai-verification-agent.md +65 -0
  18. package/hooks/hooks.json +26 -0
  19. package/hooks/massa-ai-hook +298 -0
  20. package/install.sh +413 -0
  21. package/package.json +30 -0
  22. package/skills/agents/architecture-specialist/SKILL.md +69 -0
  23. package/skills/agents/audit-specialist/SKILL.md +85 -0
  24. package/skills/agents/builder/SKILL.md +71 -0
  25. package/skills/agents/context-curator/SKILL.md +71 -0
  26. package/skills/agents/documentation-agent/SKILL.md +69 -0
  27. package/skills/agents/furps-analyst/SKILL.md +74 -0
  28. package/skills/agents/investigator/SKILL.md +72 -0
  29. package/skills/agents/mobile-specialist/SKILL.md +86 -0
  30. package/skills/agents/navigator/SKILL.md +79 -0
  31. package/skills/agents/plan-critic/SKILL.md +93 -0
  32. package/skills/agents/planner/SKILL.md +69 -0
  33. package/skills/agents/requirements-analyst/SKILL.md +68 -0
  34. package/skills/agents/reviewer/SKILL.md +70 -0
  35. package/skills/agents/test-engineer/SKILL.md +70 -0
  36. package/skills/agents/verification-agent/SKILL.md +69 -0
  37. package/skills/def/SKILL.md +17 -0
  38. package/skills/find/SKILL.md +19 -0
  39. package/skills/graph/SKILL.md +16 -0
  40. package/skills/index/SKILL.md +19 -0
  41. package/skills/map/SKILL.md +24 -0
  42. package/skills/massa-ai/SKILL.md +315 -0
  43. package/skills/massa-ai/personas/README.md +35 -0
  44. package/skills/massa-ai/personas/ai-native-nodejs-cli-architect.md +76 -0
  45. package/skills/massa-ai/personas/catalog.json +157 -0
  46. package/skills/massa-ai/personas/context-skill-harness-engineer-architect.md +74 -0
  47. package/skills/massa-ai/personas/product-manager.md +67 -0
  48. package/skills/massa-ai/personas/senior-mobile-engineer.md +74 -0
  49. package/skills/massa-ai/personas/senior-mobile-qa-automation-engineer.md +75 -0
  50. package/skills/massa-ai/references/adr-authoring.md +189 -0
  51. package/skills/massa-ai/references/agent-orchestration.md +221 -0
  52. package/skills/massa-ai/references/architecture-coupling-lens.md +239 -0
  53. package/skills/massa-ai/references/architecture-deepening-lens.md +136 -0
  54. package/skills/massa-ai/references/architecture-domain-lens.md +186 -0
  55. package/skills/massa-ai/references/architecture-lenses.md +108 -0
  56. package/skills/massa-ai/references/audit-report-io.md +459 -0
  57. package/skills/massa-ai/references/audit-scope.md +103 -0
  58. package/skills/massa-ai/references/code-annotation.md +111 -0
  59. package/skills/massa-ai/references/codebase-investigation.md +96 -0
  60. package/skills/massa-ai/references/context-firewall.md +62 -0
  61. package/skills/massa-ai/references/conversation-feedback.md +104 -0
  62. package/skills/massa-ai/references/debug-diagnosis-loop.md +140 -0
  63. package/skills/massa-ai/references/decision-engine.md +73 -0
  64. package/skills/massa-ai/references/evidence-gate.md +53 -0
  65. package/skills/massa-ai/references/furps/analyst-role.md +49 -0
  66. package/skills/massa-ai/references/furps/checklist.md +92 -0
  67. package/skills/massa-ai/references/furps/intake.md +104 -0
  68. package/skills/massa-ai/references/furps/report-contract.md +140 -0
  69. package/skills/massa-ai/references/hook-enforcement.md +137 -0
  70. package/skills/massa-ai/references/implementation-delivery.md +101 -0
  71. package/skills/massa-ai/references/installation.md +110 -0
  72. package/skills/massa-ai/references/lessons.md +119 -0
  73. package/skills/massa-ai/references/maestro/artifacts-reports.md +69 -0
  74. package/skills/massa-ai/references/maestro/cli-device.md +65 -0
  75. package/skills/massa-ai/references/maestro/cloud.md +67 -0
  76. package/skills/massa-ai/references/maestro/config-env-output.md +76 -0
  77. package/skills/massa-ai/references/maestro/fact-ledger.md +71 -0
  78. package/skills/massa-ai/references/maestro/js-scripting.md +70 -0
  79. package/skills/massa-ai/references/maestro/mcp.md +59 -0
  80. package/skills/massa-ai/references/maestro/patterns.md +96 -0
  81. package/skills/massa-ai/references/maestro/selectors.md +91 -0
  82. package/skills/massa-ai/references/maestro/workspace-execution.md +81 -0
  83. package/skills/massa-ai/references/maestro/yaml-commands.md +203 -0
  84. package/skills/massa-ai/references/maestro.md +47 -0
  85. package/skills/massa-ai/references/mcp-tools.md +296 -0
  86. package/skills/massa-ai/references/memory-policy.md +103 -0
  87. package/skills/massa-ai/references/mobile-context.md +113 -0
  88. package/skills/massa-ai/references/mobile-diagnosis.md +106 -0
  89. package/skills/massa-ai/references/mobile-figma-matcher/ATTRIBUTION.md +5 -0
  90. package/skills/massa-ai/references/mobile-figma-matcher/android-compose.md +13 -0
  91. package/skills/massa-ai/references/mobile-figma-matcher/android-views.md +13 -0
  92. package/skills/massa-ai/references/mobile-figma-matcher/core.md +117 -0
  93. package/skills/massa-ai/references/mobile-figma-matcher/ios-swiftui.md +12 -0
  94. package/skills/massa-ai/references/mobile-figma-matcher/ios-uikit.md +12 -0
  95. package/skills/massa-ai/references/mobile-figma-matcher/kmp-compose-multiplatform.md +14 -0
  96. package/skills/massa-ai/references/mobile-figma-matcher/repository-detection.md +77 -0
  97. package/skills/massa-ai/references/naming-standards.md +47 -0
  98. package/skills/massa-ai/references/pr-task-fix.md +80 -0
  99. package/skills/massa-ai/references/project-context.md +76 -0
  100. package/skills/massa-ai/references/rfc/ATTRIBUTION.md +5 -0
  101. package/skills/massa-ai/references/rfc/discovery-and-sizing.md +120 -0
  102. package/skills/massa-ai/references/rfc/document-contract.md +85 -0
  103. package/skills/massa-ai/references/rfc/quality-and-lifecycle.md +101 -0
  104. package/skills/massa-ai/references/root-cause-scripts.md +97 -0
  105. package/skills/massa-ai/references/spec-driven/artifact-store.md +98 -0
  106. package/skills/massa-ai/references/spec-driven/code-analysis.md +119 -0
  107. package/skills/massa-ai/references/spec-driven/coding-principles.md +80 -0
  108. package/skills/massa-ai/references/spec-driven/context-limits.md +64 -0
  109. package/skills/massa-ai/references/spec-driven/design.md +257 -0
  110. package/skills/massa-ai/references/spec-driven/discuss.md +182 -0
  111. package/skills/massa-ai/references/spec-driven/execute.md +471 -0
  112. package/skills/massa-ai/references/spec-driven/lessons.md +5 -0
  113. package/skills/massa-ai/references/spec-driven/memory.md +214 -0
  114. package/skills/massa-ai/references/spec-driven/specify.md +283 -0
  115. package/skills/massa-ai/references/spec-driven/sub-agents.md +151 -0
  116. package/skills/massa-ai/references/spec-driven/tasks.md +494 -0
  117. package/skills/massa-ai/references/spec-driven/validate.md +397 -0
  118. package/skills/massa-ai/references/subagent-design.md +132 -0
  119. package/skills/massa-ai/references/synapse-policy.md +160 -0
  120. package/skills/massa-ai/references/tdd/calibrated-examples.md +54 -0
  121. package/skills/massa-ai/references/tdd/discovery-and-sizing.md +83 -0
  122. package/skills/massa-ai/references/tdd/document-contract.md +136 -0
  123. package/skills/massa-ai/references/tdd/quality-and-lifecycle.md +83 -0
  124. package/skills/massa-ai/references/the-fool/cognitive-bias-inventory.md +103 -0
  125. package/skills/massa-ai/references/the-fool/dialectic-synthesis.md +170 -0
  126. package/skills/massa-ai/references/the-fool/evidence-audit.md +202 -0
  127. package/skills/massa-ai/references/the-fool/mode-selection-guide.md +113 -0
  128. package/skills/massa-ai/references/the-fool/pre-mortem-analysis.md +200 -0
  129. package/skills/massa-ai/references/the-fool/red-team-adversarial.md +206 -0
  130. package/skills/massa-ai/references/the-fool/socratic-questioning.md +153 -0
  131. package/skills/massa-ai/references/ticket/atlassian-fix.md +130 -0
  132. package/skills/massa-ai/references/ticket/intake-and-sources.md +65 -0
  133. package/skills/massa-ai/references/ticket/templates-and-quality.md +129 -0
  134. package/skills/massa-ai/references/verification-ladder.md +62 -0
  135. package/skills/massa-ai/scripts/lessons.py +590 -0
  136. package/skills/massa-ai/workflows/adr.md +33 -0
  137. package/skills/massa-ai/workflows/architecture/architecture-audit.md +125 -0
  138. package/skills/massa-ai/workflows/architecture/architecture-fix.md +110 -0
  139. package/skills/massa-ai/workflows/bugs/bugs-audit.md +113 -0
  140. package/skills/massa-ai/workflows/bugs/bugs-fix.md +97 -0
  141. package/skills/massa-ai/workflows/code-quality/code-quality-audit.md +154 -0
  142. package/skills/massa-ai/workflows/code-quality/code-quality-fix.md +99 -0
  143. package/skills/massa-ai/workflows/commit.md +61 -0
  144. package/skills/massa-ai/workflows/debug.md +86 -0
  145. package/skills/massa-ai/workflows/design.md +54 -0
  146. package/skills/massa-ai/workflows/exploration.md +119 -0
  147. package/skills/massa-ai/workflows/feature.md +52 -0
  148. package/skills/massa-ai/workflows/general.md +46 -0
  149. package/skills/massa-ai/workflows/implementation/implementation-audit.md +87 -0
  150. package/skills/massa-ai/workflows/implementation/implementation-fix.md +90 -0
  151. package/skills/massa-ai/workflows/long-session.md +44 -0
  152. package/skills/massa-ai/workflows/maestro/maestro-audit.md +56 -0
  153. package/skills/massa-ai/workflows/maestro/maestro-fix.md +74 -0
  154. package/skills/massa-ai/workflows/maestro/maestro.md +68 -0
  155. package/skills/massa-ai/workflows/mobile-figma/mobile-figma-audit.md +68 -0
  156. package/skills/massa-ai/workflows/mobile-figma/mobile-figma-fix.md +74 -0
  157. package/skills/massa-ai/workflows/onboarding.md +23 -0
  158. package/skills/massa-ai/workflows/refactor.md +47 -0
  159. package/skills/massa-ai/workflows/refinement/furps-refinement.md +81 -0
  160. package/skills/massa-ai/workflows/requirements/requirements-audit.md +114 -0
  161. package/skills/massa-ai/workflows/requirements/requirements-fix.md +93 -0
  162. package/skills/massa-ai/workflows/rfc.md +55 -0
  163. package/skills/massa-ai/workflows/security/security-audit.md +113 -0
  164. package/skills/massa-ai/workflows/security/security-fix.md +97 -0
  165. package/skills/massa-ai/workflows/spec-driven.md +217 -0
  166. package/skills/massa-ai/workflows/tdd.md +71 -0
  167. package/skills/massa-ai/workflows/tests/tests-audit.md +114 -0
  168. package/skills/massa-ai/workflows/tests/tests-fix.md +96 -0
  169. package/skills/massa-ai/workflows/the-fool.md +82 -0
  170. package/skills/massa-ai/workflows/ticket.md +42 -0
  171. package/skills/persona-router/SKILL.md +158 -0
  172. package/skills/status/SKILL.md +15 -0
@@ -0,0 +1,590 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Deterministic bookkeeping for the massa-ai spec-driven lessons layer.
4
+
5
+ The LLM supplies judgment (which failure happened, how to phrase the lesson, what
6
+ signal grounds it). This script owns everything mechanical: IDs, distinct-feature
7
+ recurrence counting, candidate->confirmed promotion, pruning, demotion, and
8
+ rendering the human/agent-readable playbook. Bookkeeping by hand is exactly what
9
+ rots a lessons file, so it lives here, not in a prompt.
10
+
11
+ Canonical state: .specs/lessons.json (machine-owned - do NOT hand-edit)
12
+ Rendered view: .specs/LESSONS.md (regenerated on every write)
13
+
14
+ Pure standard library. No dependencies. Pass --root with the target workspace
15
+ root so the package-local script writes that workspace's .specs directory.
16
+
17
+ Commands:
18
+ add Record a grounded lesson from a verification signal.
19
+ list Print lessons (default: confirmed) for loading at Specify/Design.
20
+ penalize Mark a confirmed lesson as having failed when applied (-> quarantine).
21
+ prune Drop stale uncorroborated candidates (also runs automatically on add/list).
22
+ status Print counts (used by the self-check in validate.md).
23
+ init Create empty store + rendered file.
24
+ observe Ingest a JSON observation into the gitignored observations buffer.
25
+ export Export the lessons store as JSON (round-trips with import).
26
+ import Import lessons from JSON (merge by dedup key; best-effort massa-ai memory).
27
+
28
+ Exit codes: 0 ok, 2 usage/validation error (e.g. missing grounding).
29
+ """
30
+
31
+ import argparse
32
+ import datetime as _dt
33
+ import json
34
+ import os
35
+ import re
36
+ import sys
37
+ import urllib.request
38
+
39
+ STORE_REL = os.path.join(".specs", "lessons.json")
40
+ RENDER_REL = os.path.join(".specs", "LESSONS.md")
41
+ OBS_REL = os.path.join(".specs", "observations.json")
42
+
43
+ SIGNALS = {
44
+ "ac_gap": "Acceptance criterion not covered / failed",
45
+ "surviving_mutant": "Discrimination sensor mutant survived (weak test)",
46
+ "spec_precision_gap": "Spec did not define a precise outcome",
47
+ "spec_deviation": "Implementation diverged from spec/design (SPEC_DEVIATION)",
48
+ "gate_fail": "Build-level gate check failed",
49
+ }
50
+
51
+ DEFAULTS = {"promote_threshold": 2, "window_days": 45, "quarantine_threshold": 2}
52
+
53
+ # massa-ai supported memory types (references/mcp-tools.md). `procedural` is a
54
+ # TAG, never a type. Lessons are procedural knowledge -> type `pattern`.
55
+ MASSA_AI_SUPPORTED_TYPES = ("critical", "conversation", "code", "decision", "pattern")
56
+ MASSA_AI_LESSON_TYPE = "pattern"
57
+
58
+
59
+ def _now():
60
+ return _dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
61
+
62
+
63
+ def _parse_date(s):
64
+ try:
65
+ return _dt.datetime.strptime(s, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=_dt.timezone.utc)
66
+ except Exception:
67
+ return _dt.datetime.now(_dt.timezone.utc)
68
+
69
+
70
+ def _store_path(root):
71
+ return os.path.join(root, STORE_REL)
72
+
73
+
74
+ def _render_path(root):
75
+ return os.path.join(root, RENDER_REL)
76
+
77
+
78
+ def _load(root):
79
+ path = _store_path(root)
80
+ if not os.path.exists(path):
81
+ return {
82
+ "schema": 1,
83
+ "promote_threshold": DEFAULTS["promote_threshold"],
84
+ "window_days": DEFAULTS["window_days"],
85
+ "quarantine_threshold": DEFAULTS["quarantine_threshold"],
86
+ "next_id": 1,
87
+ "lessons": [],
88
+ }
89
+ with open(path, "r", encoding="utf-8") as f:
90
+ data = json.load(f)
91
+ for k, v in DEFAULTS.items():
92
+ data.setdefault(k, v)
93
+ data.setdefault("schema", 1)
94
+ data.setdefault("next_id", 1)
95
+ data.setdefault("lessons", [])
96
+ return data
97
+
98
+
99
+ def _save(root, data):
100
+ os.makedirs(os.path.join(root, ".specs"), exist_ok=True)
101
+ with open(_store_path(root), "w", encoding="utf-8") as f:
102
+ json.dump(data, f, indent=2, ensure_ascii=False)
103
+ f.write("\n")
104
+ _render(root, data)
105
+
106
+
107
+ def _confidence(lesson, data):
108
+ """Deterministic 0-1 confidence from recurrence + signal + scope presence."""
109
+ rec_cap = min(lesson.get("recurrence", 1) / max(data["promote_threshold"], 1), 1.0)
110
+ sig_weight = 0.15
111
+ scope_weight = 0.10 if lesson.get("scope") else 0.0
112
+ return round(min(rec_cap * 0.75 + sig_weight + scope_weight, 1.0), 2)
113
+
114
+
115
+ def _obs_path(root):
116
+ return os.path.join(root, OBS_REL)
117
+
118
+
119
+ def _obs_load(root):
120
+ path = _obs_path(root)
121
+ if not os.path.exists(path):
122
+ return []
123
+ try:
124
+ with open(path, "r", encoding="utf-8") as f:
125
+ data = json.load(f)
126
+ return data if isinstance(data, list) else []
127
+ except (ValueError, OSError):
128
+ return []
129
+
130
+
131
+ def _obs_append(root, item):
132
+ os.makedirs(os.path.join(root, ".specs"), exist_ok=True)
133
+ items = _obs_load(root)
134
+ items.append(item)
135
+ with open(_obs_path(root), "w", encoding="utf-8") as f:
136
+ json.dump(items, f, indent=2, ensure_ascii=False)
137
+ f.write("\n")
138
+
139
+
140
+ def _remember_best_effort(root, content, tags, project_id="", session_id=""):
141
+ """Best-effort massa-ai memory write via REST (urllib, stdlib only).
142
+
143
+ massa-ai MCP is agent-side only; a CLI subprocess cannot call MCP. massa-ai exposes
144
+ REST at MASSA_AI_API_URL. Type is always `pattern` (lessons are procedural
145
+ knowledge); `procedural` is a tag, not a type. Returns True on success,
146
+ False (silent) when unavailable — the file store remains source of truth.
147
+ """
148
+ api_url = os.environ.get("MASSA_AI_API_URL")
149
+ if not api_url:
150
+ return False
151
+ path = os.environ.get("MASSA_AI_MEMORY_PATH", "/api/v1/memory")
152
+ url = api_url.rstrip("/") + path
153
+ body = json.dumps({
154
+ "content": content, "type": MASSA_AI_LESSON_TYPE, "importance": 0.6,
155
+ "projectId": project_id, "sessionId": session_id, "tags": list(tags),
156
+ }).encode("utf-8")
157
+ req = urllib.request.Request(url, data=body, method="POST",
158
+ headers={"Content-Type": "application/json"})
159
+ key = os.environ.get("MASSA_AI_API_KEY")
160
+ if key:
161
+ req.add_header("x-api-key", key)
162
+ try:
163
+ with urllib.request.urlopen(req, timeout=1.5) as resp:
164
+ return 200 <= resp.status < 300
165
+ except Exception:
166
+ return False
167
+
168
+
169
+ def _lesson_tags(lesson):
170
+ """massa-ai persistence tag contract for a lesson's massa-ai memory."""
171
+ return [
172
+ "project:%s" % lesson.get("project", ""),
173
+ "session:%s" % lesson.get("session", ""),
174
+ "workflow:%s" % (lesson.get("workflow", "") or "unset"),
175
+ "entity:%s" % (lesson.get("entity", "") or "unset"),
176
+ "memory:procedural",
177
+ ]
178
+
179
+
180
+ def _norm(text):
181
+ """Normalized dedup key: lowercase, strip punctuation, collapse whitespace.
182
+ Exact-after-normalization only - no semantic matching (stdlib-only limitation).
183
+ Phrase lessons tersely and canonically so recurrences actually merge."""
184
+ t = text.lower().strip()
185
+ t = re.sub(r"[^a-z0-9\s]", " ", t)
186
+ t = re.sub(r"\s+", " ", t).strip()
187
+ return t
188
+
189
+
190
+ def _key(signal, text):
191
+ return signal + "::" + _norm(text)
192
+
193
+
194
+ def _auto_prune(data):
195
+ """Drop candidates that never recurred within the window. Mutates data."""
196
+ threshold = data["promote_threshold"]
197
+ window = data["window_days"]
198
+ now = _dt.datetime.now(_dt.timezone.utc)
199
+ kept = []
200
+ dropped = []
201
+ for l in data["lessons"]:
202
+ if l["status"] == "candidate" and l["recurrence"] < threshold:
203
+ age_days = (now - _parse_date(l.get("last_seen", l.get("created", _now())))).days
204
+ if age_days > window:
205
+ dropped.append(l["id"])
206
+ continue
207
+ kept.append(l)
208
+ data["lessons"] = kept
209
+ return dropped
210
+
211
+
212
+ def _find(data, signal, text):
213
+ k = _key(signal, text)
214
+ for l in data["lessons"]:
215
+ if l.get("key") == k:
216
+ return l
217
+ return None
218
+
219
+
220
+ def _render(root, data):
221
+ lines = []
222
+ lines.append("# LESSONS - auto-maintained by skills/massa-ai/scripts/lessons.py")
223
+ lines.append("")
224
+ lines.append("> Machine-owned. Do NOT hand-edit. Changes are overwritten on the next `lessons.py` write.")
225
+ lines.append("> Canonical state lives in `.specs/lessons.json`. Edit lessons only via the script.")
226
+ lines.append(f"> promote_threshold={data['promote_threshold']} distinct features | window_days={data['window_days']} | quarantine_threshold={data['quarantine_threshold']}")
227
+ lines.append("")
228
+
229
+ by_status = {"confirmed": [], "candidate": [], "quarantined": []}
230
+ for l in data["lessons"]:
231
+ by_status.get(l["status"], by_status["candidate"]).append(l)
232
+
233
+ def block(title, items, note):
234
+ out = [f"## {title}", ""]
235
+ if note:
236
+ out.append(note)
237
+ out.append("")
238
+ if not items:
239
+ out.append("_none_")
240
+ out.append("")
241
+ return out
242
+ for l in sorted(items, key=lambda x: x["id"]):
243
+ scope = f" | scope: `{l['scope']}`" if l.get("scope") else ""
244
+ conf = l.get("confidence", _confidence(l, data))
245
+ out.append(f"### {l['id']} - {l['text']}")
246
+ out.append(
247
+ f"- signal: `{l['signal']}` | recurrence: {l['recurrence']} feature(s){scope} | harmful: {l.get('harmful', 0)} | confidence: {conf}"
248
+ )
249
+ feats = ", ".join(l.get("features", [])) or "-"
250
+ out.append(f"- features: {feats}")
251
+ ctx = []
252
+ for k in ("project", "session", "workflow", "entity"):
253
+ if l.get(k):
254
+ ctx.append(f"{k}={l[k]}")
255
+ if ctx:
256
+ out.append(f"- context: {' '.join(ctx)}")
257
+ ev = l.get("evidence", [])
258
+ if ev:
259
+ out.append(f"- evidence: {ev[0]}" + (f" (+{len(ev) - 1} more)" if len(ev) > 1 else ""))
260
+ out.append(f"- last seen: {l.get('last_seen', '-')}")
261
+ out.append("")
262
+ return out
263
+
264
+ lines += block(
265
+ "Confirmed (load these at Specify/Design)",
266
+ by_status["confirmed"],
267
+ "Corroborated across multiple features. Safe to apply as guidance.",
268
+ )
269
+ lines += block(
270
+ "Candidates (under observation - do NOT load as guidance yet)",
271
+ by_status["candidate"],
272
+ "Seen once or not yet corroborated. Tracked, not trusted.",
273
+ )
274
+ lines += block(
275
+ "Quarantined (failed when applied - ignore)",
276
+ by_status["quarantined"],
277
+ "A confirmed lesson that recurred alongside failure. Kept for the maintainer to review.",
278
+ )
279
+
280
+ with open(_render_path(root), "w", encoding="utf-8") as f:
281
+ f.write("\n".join(lines).rstrip() + "\n")
282
+
283
+
284
+ # ----------------------------- commands -----------------------------
285
+
286
+ def cmd_init(root, args):
287
+ data = _load(root)
288
+ _save(root, data)
289
+ print(f"Initialized lessons store at {_store_path(root)} and {_render_path(root)}")
290
+ return 0
291
+
292
+
293
+ def cmd_add(root, args):
294
+ signal = args.signal
295
+ source = (args.source or "").strip()
296
+ text = (args.text or "").strip()
297
+ feature = (args.feature or "").strip()
298
+
299
+ # Grounding is enforced here, deterministically - not left to the prompt.
300
+ if signal not in SIGNALS:
301
+ print(f"ERROR: --signal must be one of {sorted(SIGNALS)}", file=sys.stderr)
302
+ return 2
303
+ if not feature:
304
+ print("ERROR: --feature is required (the feature the signal came from).", file=sys.stderr)
305
+ return 2
306
+ if not source:
307
+ print("ERROR: --source is required (file:line / AC id / mutant id / SPEC_DEVIATION ref).", file=sys.stderr)
308
+ print(" A lesson with no grounding in validation.md is an opinion, not a lesson. Refused.", file=sys.stderr)
309
+ return 2
310
+ if len(text) < 12:
311
+ print("ERROR: --text too short. State the actionable lesson in one terse sentence.", file=sys.stderr)
312
+ return 2
313
+
314
+ data = _load(root)
315
+ _auto_prune(data)
316
+ existing = _find(data, signal, text)
317
+ now = _now()
318
+ project = (getattr(args, "project", "") or "").strip()
319
+ session = (getattr(args, "session", "") or "").strip()
320
+ workflow = (getattr(args, "workflow", "") or "").strip()
321
+ entity = (getattr(args, "entity", "") or "").strip()
322
+
323
+ def _ctx(lesson):
324
+ if project:
325
+ lesson["project"] = project
326
+ if session:
327
+ lesson["session"] = session
328
+ if workflow:
329
+ lesson["workflow"] = workflow
330
+ if entity:
331
+ lesson["entity"] = entity
332
+
333
+ if existing:
334
+ if feature not in existing["features"]:
335
+ existing["features"].append(feature)
336
+ existing["recurrence"] = len(existing["features"])
337
+ existing["last_seen"] = now
338
+ _ctx(existing)
339
+ existing["confidence"] = _confidence(existing, data)
340
+ ev = source if not args.scope else f"{source} ({args.scope})"
341
+ if ev not in existing["evidence"]:
342
+ existing["evidence"].append(ev)
343
+ promoted = False
344
+ if existing["status"] == "candidate" and existing["recurrence"] >= data["promote_threshold"]:
345
+ existing["status"] = "confirmed"
346
+ promoted = True
347
+ _save(root, data)
348
+ _remember_best_effort(root, "%s [%s] %s" % (existing["id"], signal, text),
349
+ _lesson_tags(existing), project, session)
350
+ msg = f"UPDATED {existing['id']} (recurrence={existing['recurrence']}, status={existing['status']}, confidence={existing['confidence']})"
351
+ if promoted:
352
+ msg += " - PROMOTED to confirmed"
353
+ print(msg)
354
+ else:
355
+ lid = f"L-{data['next_id']:03d}"
356
+ data["next_id"] += 1
357
+ lesson = {
358
+ "id": lid,
359
+ "key": _key(signal, text),
360
+ "text": text,
361
+ "signal": signal,
362
+ "scope": (args.scope or "").strip(),
363
+ "status": "candidate",
364
+ "features": [feature],
365
+ "recurrence": 1,
366
+ "harmful": 0,
367
+ "evidence": [source if not args.scope else f"{source} ({args.scope})"],
368
+ "created": now,
369
+ "last_seen": now,
370
+ }
371
+ _ctx(lesson)
372
+ lesson["confidence"] = _confidence(lesson, data)
373
+ data["lessons"].append(lesson)
374
+ _save(root, data)
375
+ _remember_best_effort(root, "%s [%s] %s" % (lid, signal, text),
376
+ _lesson_tags(lesson), project, session)
377
+ print(f"ADDED {lid} (status=candidate, recurrence=1, confidence={lesson['confidence']})")
378
+ return 0
379
+
380
+
381
+ def cmd_penalize(root, args):
382
+ data = _load(root)
383
+ target = None
384
+ for l in data["lessons"]:
385
+ if l["id"].lower() == args.id.lower():
386
+ target = l
387
+ break
388
+ if not target:
389
+ print(f"ERROR: no lesson with id {args.id}", file=sys.stderr)
390
+ return 2
391
+ target["harmful"] = target.get("harmful", 0) + 1
392
+ target["last_seen"] = _now()
393
+ if target["harmful"] >= data["quarantine_threshold"]:
394
+ target["status"] = "quarantined"
395
+ _save(root, data)
396
+ print(f"PENALIZED {target['id']} (harmful={target['harmful']}, status={target['status']})")
397
+ return 0
398
+
399
+
400
+ def cmd_list(root, args):
401
+ data = _load(root)
402
+ if _auto_prune(data):
403
+ _save(root, data)
404
+ want = args.status
405
+ q = (args.query or "").lower().strip()
406
+ scope = (args.scope or "").lower().strip()
407
+ project = (getattr(args, "project", "") or "").lower().strip()
408
+ rows = []
409
+ for l in data["lessons"]:
410
+ if want != "all" and l["status"] != want:
411
+ continue
412
+ if q and q not in l["text"].lower():
413
+ continue
414
+ if scope and scope not in (l.get("scope", "").lower()):
415
+ continue
416
+ if project and project not in (l.get("project", "").lower()):
417
+ continue
418
+ rows.append(l)
419
+ if not rows:
420
+ flt = " ".join(f for f in (q, scope, project) if f)
421
+ print(f"(no {want} lessons" + (f" matching '{flt}'" if flt else "") + ")")
422
+ return 0
423
+ for l in sorted(rows, key=lambda x: x["id"]):
424
+ sc = f" [scope:{l['scope']}]" if l.get("scope") else ""
425
+ conf = l.get("confidence", _confidence(l, data))
426
+ print(f"{l['id']} ({l['status']}, x{l['recurrence']}, conf={conf}){sc}: {l['text']}")
427
+ return 0
428
+
429
+
430
+ def cmd_observe(root, args):
431
+ """Ingest a JSON observation into the gitignored observations buffer.
432
+
433
+ Grounding is NOT enforced here; it is enforced when `add` consumes the
434
+ buffer. Observation fields: signal, text, source, feature, scope, project,
435
+ session, workflow, entity.
436
+ """
437
+ raw = args.json if args.json else sys.stdin.read()
438
+ try:
439
+ item = json.loads(raw)
440
+ except (ValueError, TypeError) as exc:
441
+ print(f"ERROR: observation is not valid JSON: {exc}", file=sys.stderr)
442
+ return 2
443
+ if not isinstance(item, dict):
444
+ print("ERROR: observation must be a JSON object", file=sys.stderr)
445
+ return 2
446
+ item.setdefault("observed_at", _now())
447
+ _obs_append(root, item)
448
+ print(f"OBSERVED buffer=1 (total={len(_obs_load(root))})")
449
+ return 0
450
+
451
+
452
+ def cmd_export(root, args):
453
+ """Export the lessons store as JSON (stdout or --out). Round-trips with import."""
454
+ data = _load(root)
455
+ text = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
456
+ if args.out:
457
+ with open(args.out, "w", encoding="utf-8") as f:
458
+ f.write(text)
459
+ print(f"EXPORTED {len(data['lessons'])} lessons -> {args.out}")
460
+ else:
461
+ sys.stdout.write(text)
462
+ return 0
463
+
464
+
465
+ def cmd_import(root, args):
466
+ """Import lessons from JSON (stdin or --in), merging by dedup key.
467
+
468
+ Re-emits massa-ai memory best-effort (type `pattern`, tag `memory:procedural`)
469
+ for each imported lesson so the file store and massa-ai memory stay consistent.
470
+ """
471
+ raw = sys.stdin.read() if args.in_ is None else open(args.in_, "r", encoding="utf-8").read()
472
+ try:
473
+ incoming = json.loads(raw)
474
+ except (ValueError, TypeError) as exc:
475
+ print(f"ERROR: import payload is not valid JSON: {exc}", file=sys.stderr)
476
+ return 2
477
+ if not isinstance(incoming, dict) or not isinstance(incoming.get("lessons"), list):
478
+ print("ERROR: import payload must be a lessons store object with `lessons`", file=sys.stderr)
479
+ return 2
480
+ data = _load(root)
481
+ _auto_prune(data)
482
+ now = _now()
483
+ added = merged = 0
484
+ for l in incoming["lessons"]:
485
+ key = l.get("key") or _key(l.get("signal", ""), l.get("text", ""))
486
+ existing = next((x for x in data["lessons"] if x.get("key") == key), None)
487
+ if existing:
488
+ for f in l.get("features", []):
489
+ if f not in existing["features"]:
490
+ existing["features"].append(f)
491
+ existing["recurrence"] = len(existing["features"])
492
+ existing["last_seen"] = now
493
+ existing["confidence"] = _confidence(existing, data)
494
+ merged += 1
495
+ else:
496
+ lid = f"L-{data['next_id']:03d}"
497
+ data["next_id"] += 1
498
+ l.setdefault("id", lid)
499
+ l["id"] = lid
500
+ l["key"] = key
501
+ l.setdefault("status", "candidate")
502
+ l.setdefault("recurrence", len(l.get("features", [])) or 1)
503
+ l.setdefault("harmful", 0)
504
+ l.setdefault("created", now)
505
+ l["last_seen"] = now
506
+ l["confidence"] = _confidence(l, data)
507
+ data["lessons"].append(l)
508
+ added += 1
509
+ target = existing or l
510
+ _remember_best_effort(root, "%s [%s] %s" % (target.get("id"), target.get("signal", ""), target.get("text", "")),
511
+ _lesson_tags(target), target.get("project", ""), target.get("session", ""))
512
+ _save(root, data)
513
+ print(f"IMPORTED added={added} merged={merged} massa-ai=best-effort")
514
+ return 0
515
+
516
+
517
+ def cmd_prune(root, args):
518
+ data = _load(root)
519
+ dropped = _auto_prune(data)
520
+ _save(root, data)
521
+ print(f"Pruned {len(dropped)} stale candidate(s): {', '.join(dropped) if dropped else '-'}")
522
+ return 0
523
+
524
+
525
+ def cmd_status(root, args):
526
+ data = _load(root)
527
+ counts = {"confirmed": 0, "candidate": 0, "quarantined": 0}
528
+ for l in data["lessons"]:
529
+ counts[l["status"]] = counts.get(l["status"], 0) + 1
530
+ total = len(data["lessons"])
531
+ print(f"lessons: {total} total | confirmed={counts['confirmed']} candidate={counts['candidate']} quarantined={counts['quarantined']}")
532
+ return 0
533
+
534
+
535
+ def main(argv=None):
536
+ p = argparse.ArgumentParser(prog="lessons.py", description="Deterministic lessons bookkeeping for massa-ai spec-driven.")
537
+ p.add_argument("--root", default=".", help="Project root containing .specs/ (default: current dir)")
538
+ sub = p.add_subparsers(dest="cmd", required=True)
539
+
540
+ sp = sub.add_parser("init", help="Create empty store + rendered file")
541
+ sp.set_defaults(fn=cmd_init)
542
+
543
+ sp = sub.add_parser("add", help="Record a grounded lesson")
544
+ sp.add_argument("--feature", required=True)
545
+ sp.add_argument("--signal", required=True, choices=sorted(SIGNALS))
546
+ sp.add_argument("--source", required=True, help="file:line / AC id / mutant id / SPEC_DEVIATION ref")
547
+ sp.add_argument("--text", required=True, help="One terse, actionable sentence")
548
+ sp.add_argument("--scope", default="", help="Optional: path/layer/tag for retrieval filtering")
549
+ sp.add_argument("--project", default="", help="massa-ai projectId context")
550
+ sp.add_argument("--session", default="", help="massa-ai workflowSessionId context")
551
+ sp.add_argument("--workflow", default="", help="active massa-ai workflow type")
552
+ sp.add_argument("--entity", default="", help="active massa-ai entity")
553
+ sp.set_defaults(fn=cmd_add)
554
+
555
+ sp = sub.add_parser("penalize", help="Mark a confirmed lesson as failed-when-applied")
556
+ sp.add_argument("--id", required=True)
557
+ sp.set_defaults(fn=cmd_penalize)
558
+
559
+ sp = sub.add_parser("list", help="Print lessons for loading")
560
+ sp.add_argument("--status", default="confirmed", choices=["confirmed", "candidate", "quarantined", "all"])
561
+ sp.add_argument("--query", default="", help="Substring filter on lesson text")
562
+ sp.add_argument("--scope", default="", help="Substring filter on scope")
563
+ sp.add_argument("--project", default="", help="Substring filter on project")
564
+ sp.set_defaults(fn=cmd_list)
565
+
566
+ sp = sub.add_parser("observe", help="Ingest a JSON observation into the buffer")
567
+ sp.add_argument("--json", default="", help="Observation JSON (else read stdin)")
568
+ sp.set_defaults(fn=cmd_observe)
569
+
570
+ sp = sub.add_parser("export", help="Export lessons store as JSON")
571
+ sp.add_argument("--out", default="", help="Write to file (else stdout)")
572
+ sp.set_defaults(fn=cmd_export)
573
+
574
+ sp = sub.add_parser("import", help="Import lessons from JSON (merge by dedup key)")
575
+ sp.add_argument("--in", dest="in_", default=None, help="Read from file (else stdin)")
576
+ sp.set_defaults(fn=cmd_import)
577
+
578
+ sp = sub.add_parser("prune", help="Drop stale uncorroborated candidates")
579
+ sp.set_defaults(fn=cmd_prune)
580
+
581
+ sp = sub.add_parser("status", help="Print counts")
582
+ sp.set_defaults(fn=cmd_status)
583
+
584
+ args = p.parse_args(argv)
585
+ root = os.path.abspath(args.root)
586
+ return args.fn(root, args)
587
+
588
+
589
+ if __name__ == "__main__":
590
+ raise SystemExit(main())
@@ -0,0 +1,33 @@
1
+ ### 📜 ADR (Architecture Decision Record)
2
+
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`.
4
+
5
+ Before the first substantive read, load `references/project-context.md` and run the project-context intake sweep for this repository.
6
+
7
+ 1. Resolve/reuse `projectId` and `workflowSessionId`: `adr-[entity]`.
8
+ 2. Load shared references:
9
+ - `references/adr-authoring.md` always.
10
+ - `references/context-firewall.md` before reading large Markdown files, Atlassian pages, NotebookLM outputs, Figma exports, broad research, or verbose source output.
11
+ - `references/mobile-context.md` only when ADR context touches KMP, iOS, Android, native bridges, mobile lifecycle, offline sync, permissions, push/background behavior, local persistence, or backend-mobile contracts.
12
+ 3. `recall` -> load previous decisions, related RFCs, PRDs, discussions, superseded ADRs, accepted constraints, rejected options, and project-specific ADR conventions for the entity.
13
+ 4. Gather source context using `references/adr-authoring.md`:
14
+ - Use explicitly provided Markdown files and prompt context first.
15
+ - 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.
20
+ - 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
+ - Corroborate with massa-ai search, current repo docs, existing ADRs, and code when the decision depends on current project reality.
22
+ 5. Run the ADR readiness gate from `references/adr-authoring.md`:
23
+ - If a needed PRD, RFC, template, decision detail, or source fact is absent, ask for the missing context instead of inventing it.
24
+ - 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
+ - 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.
27
+ 7. Save the generated ADR using the selected output target:
28
+ - Default: write to the project's standard ADR directory in Default mode. In Plan Mode, propose the path and content without writing.
29
+ - Confluence: when requested and a parent page link is provided, write a child page through Atlassian MCP and report the resulting page link.
30
+ - 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
+ - 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.
33
+ 9. Complete the Evidence Gate from `references/evidence-gate.md`.