workflow-toolkit 1.0.1

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 (142) hide show
  1. package/.agents/skills/ponytail/SKILL.md +111 -0
  2. package/.agents/skills/ponytail-audit/SKILL.md +37 -0
  3. package/.agents/skills/ponytail-debt/SKILL.md +41 -0
  4. package/.agents/skills/ponytail-gain/SKILL.md +47 -0
  5. package/.agents/skills/ponytail-help/SKILL.md +70 -0
  6. package/.agents/skills/ponytail-review/SKILL.md +52 -0
  7. package/.agents/skills/prompt-review/SKILL.md +60 -0
  8. package/.agents/skills/wtk/SKILL.md +64 -0
  9. package/.agents/skills/wtk/references/artifacts.md +82 -0
  10. package/.agents/skills/wtk/references/evidence.md +68 -0
  11. package/.agents/skills/wtk/references/git.md +75 -0
  12. package/.agents/skills/wtk/references/test-contract.md +76 -0
  13. package/.agents/skills/wtk/references/validation.md +127 -0
  14. package/.agents/skills/wtk-config/SKILL.md +102 -0
  15. package/.agents/skills/wtk-config/assets/agents/claude/deep-reviewer.md +40 -0
  16. package/.agents/skills/wtk-config/assets/agents/claude/designer.md +57 -0
  17. package/.agents/skills/wtk-config/assets/agents/claude/explorer.md +41 -0
  18. package/.agents/skills/wtk-config/assets/agents/claude/implementer.md +57 -0
  19. package/.agents/skills/wtk-config/assets/agents/claude/planner.md +68 -0
  20. package/.agents/skills/wtk-config/assets/agents/claude/verifier.md +80 -0
  21. package/.agents/skills/wtk-config/assets/agents/codex/deep-reviewer.toml +41 -0
  22. package/.agents/skills/wtk-config/assets/agents/codex/designer.toml +55 -0
  23. package/.agents/skills/wtk-config/assets/agents/codex/explorer.toml +35 -0
  24. package/.agents/skills/wtk-config/assets/agents/codex/implementer.toml +52 -0
  25. package/.agents/skills/wtk-config/assets/agents/codex/planner.toml +66 -0
  26. package/.agents/skills/wtk-config/assets/agents/codex/verifier.toml +78 -0
  27. package/.agents/skills/wtk-config/assets/agents/cursor/deep-reviewer.md +38 -0
  28. package/.agents/skills/wtk-config/assets/agents/cursor/designer.md +55 -0
  29. package/.agents/skills/wtk-config/assets/agents/cursor/explorer.md +39 -0
  30. package/.agents/skills/wtk-config/assets/agents/cursor/implementer.md +55 -0
  31. package/.agents/skills/wtk-config/assets/agents/cursor/planner.md +66 -0
  32. package/.agents/skills/wtk-config/assets/agents/cursor/verifier.md +79 -0
  33. package/.agents/skills/wtk-config/scripts/ad-index.py +123 -0
  34. package/.agents/skills/wtk-config/scripts/repository_intelligence.py +671 -0
  35. package/.agents/skills/wtk-config/scripts/workflow_config.py +972 -0
  36. package/.agents/skills/wtk-deep-review/SKILL.md +164 -0
  37. package/.agents/skills/wtk-deep-review/assets/PROMPT.md +56 -0
  38. package/.agents/skills/wtk-deep-review/assets/REVIEW_UI.html +1340 -0
  39. package/.agents/skills/wtk-deep-review/assets/findings.schema.json +113 -0
  40. package/.agents/skills/wtk-deep-review/references/context-pack.md +88 -0
  41. package/.agents/skills/wtk-deep-review/references/orchestration.md +164 -0
  42. package/.agents/skills/wtk-deep-review/references/output-contracts.md +149 -0
  43. package/.agents/skills/wtk-deep-review/references/publish-github.md +83 -0
  44. package/.agents/skills/wtk-deep-review/references/state-and-learnings.md +64 -0
  45. package/.agents/skills/wtk-deep-review/references/subagent-runtimes.md +33 -0
  46. package/.agents/skills/wtk-deep-review/references/taxonomy.md +70 -0
  47. package/.agents/skills/wtk-deep-review/scripts/_common.py +371 -0
  48. package/.agents/skills/wtk-deep-review/scripts/build_jobs.py +561 -0
  49. package/.agents/skills/wtk-deep-review/scripts/build_knowledge.py +349 -0
  50. package/.agents/skills/wtk-deep-review/scripts/build_manifest.py +479 -0
  51. package/.agents/skills/wtk-deep-review/scripts/graft_context.py +160 -0
  52. package/.agents/skills/wtk-deep-review/scripts/graphify_context.py +75 -0
  53. package/.agents/skills/wtk-deep-review/scripts/merge_findings.py +313 -0
  54. package/.agents/skills/wtk-deep-review/scripts/render_html.py +240 -0
  55. package/.agents/skills/wtk-deep-review/scripts/render_review.py +282 -0
  56. package/.agents/skills/wtk-deep-review/scripts/run_jobs.py +436 -0
  57. package/.agents/skills/wtk-deep-review/scripts/token_metrics.py +369 -0
  58. package/.agents/skills/wtk-discover/SKILL.md +270 -0
  59. package/.agents/skills/wtk-discover/references/document-format.md +149 -0
  60. package/.agents/skills/wtk-implement/SKILL.md +95 -0
  61. package/.agents/skills/wtk-implement/references/checklist-format.md +116 -0
  62. package/.agents/skills/wtk-implement/references/screens.md +129 -0
  63. package/.agents/skills/wtk-implement/references/test-policy.md +152 -0
  64. package/.agents/skills/wtk-implement/references/verify.md +297 -0
  65. package/.agents/skills/wtk-knowledge-check/SKILL.md +16 -0
  66. package/.agents/skills/wtk-knowledge-check/scripts/check.ts +502 -0
  67. package/.agents/skills/wtk-knowledge-check/scripts/cli.ts +32 -0
  68. package/.agents/skills/wtk-knowledge-check/scripts/frontmatter.ts +44 -0
  69. package/.agents/skills/wtk-lean/.skill-meta.json +6 -0
  70. package/.agents/skills/wtk-lean/NOTICE.md +18 -0
  71. package/.agents/skills/wtk-lean/SKILL.md +137 -0
  72. package/.agents/skills/wtk-lean/references/build.md +168 -0
  73. package/.agents/skills/wtk-lean/references/checks.md +253 -0
  74. package/.agents/skills/wtk-lean/references/memory.md +156 -0
  75. package/.agents/skills/wtk-lean/references/plan.md +514 -0
  76. package/.agents/skills/wtk-lean/references/verify.md +336 -0
  77. package/.agents/skills/wtk-lean/scripts/check_commit.py +121 -0
  78. package/.agents/skills/wtk-lean/scripts/fixtures/checks.md +98 -0
  79. package/.agents/skills/wtk-lean/scripts/fixtures/plan.md +119 -0
  80. package/.agents/skills/wtk-lean/scripts/fixtures/verification.md +41 -0
  81. package/.agents/skills/wtk-lean/scripts/lessons.py +412 -0
  82. package/.agents/skills/wtk-lean/scripts/selftest.py +401 -0
  83. package/.agents/skills/wtk-lean/scripts/validate_checks.py +417 -0
  84. package/.agents/skills/wtk-lean/scripts/validate_plan.py +580 -0
  85. package/.agents/skills/wtk-lean/scripts/validate_verification.py +357 -0
  86. package/.agents/skills/wtk-plan/SKILL.md +102 -0
  87. package/.agents/skills/wtk-plan/references/document-format.md +222 -0
  88. package/.agents/skills/wtk-qa/SKILL.md +14 -0
  89. package/.agents/skills/wtk-qa-execute/SKILL.md +115 -0
  90. package/.agents/skills/wtk-qa-execute/references/fix-loop.md +23 -0
  91. package/.agents/skills/wtk-qa-execute/references/session-protocol.md +25 -0
  92. package/.agents/skills/wtk-qa-plan/SKILL.md +111 -0
  93. package/.agents/skills/wtk-qa-plan/references/profile.md +35 -0
  94. package/.agents/skills/wtk-ship/SKILL.md +49 -0
  95. package/.agents/skills/wtk-ship/remediation.py +170 -0
  96. package/.agents/skills/wtk-ship/scripts/close_feature.py +83 -0
  97. package/.agents/skills/wtk-ship/scripts/review_convergence.py +373 -0
  98. package/.wtk.toml.example +93 -0
  99. package/AGENTS.md +129 -0
  100. package/NOTICE.md +12 -0
  101. package/README.md +440 -0
  102. package/bin/wtk.js +25 -0
  103. package/docs/toolkit/README.md +56 -0
  104. package/docs/toolkit/decisions.md +61 -0
  105. package/docs/toolkit/guidelines/CONTEXT-BUDGET.md +64 -0
  106. package/docs/toolkit/guidelines/DX.md +67 -0
  107. package/docs/toolkit/guidelines/FRONTEND.md +73 -0
  108. package/docs/toolkit/guidelines/KNOWLEDGE-WIKI.md +128 -0
  109. package/docs/toolkit/guidelines/MODELING.md +134 -0
  110. package/docs/toolkit/guidelines/QA-EXECUTION.md +46 -0
  111. package/docs/toolkit/guidelines/QA-SCENARIOS.md +165 -0
  112. package/docs/toolkit/guidelines/REVIEW-ROUNDS.md +160 -0
  113. package/docs/toolkit/guidelines/SECURITY.md +160 -0
  114. package/docs/toolkit/guidelines/UI-UX.md +117 -0
  115. package/docs/toolkit/guidelines/WORKFLOW-MEMORY.md +73 -0
  116. package/docs/toolkit/guidelines.md +50 -0
  117. package/docs/toolkit/loop.md +77 -0
  118. package/docs/toolkit/purpose.md +51 -0
  119. package/docs/toolkit/repository-intelligence.md +53 -0
  120. package/docs/toolkit/reviews.md +77 -0
  121. package/knowledge/AGENTS.md +316 -0
  122. package/knowledge/raw/README.md +17 -0
  123. package/package.json +74 -0
  124. package/scripts/install_security_skills.py +816 -0
  125. package/scripts/installer/engine.js +254 -0
  126. package/scripts/installer/knowledge.js +28 -0
  127. package/scripts/installer/packets.js +77 -0
  128. package/scripts/installer/terminal.js +98 -0
  129. package/scripts/installer/transaction.js +75 -0
  130. package/skills-lock.json +92 -0
  131. package/templates/adoption/agents/core.md +23 -0
  132. package/templates/adoption/agents/quality.md +3 -0
  133. package/templates/adoption/knowledge/wiki/architecture/index.md +3 -0
  134. package/templates/adoption/knowledge/wiki/decisions/index.md +3 -0
  135. package/templates/adoption/knowledge/wiki/design/index.md +3 -0
  136. package/templates/adoption/knowledge/wiki/domain/index.md +3 -0
  137. package/templates/adoption/knowledge/wiki/index.md +3 -0
  138. package/templates/adoption/knowledge/wiki/log.md +3 -0
  139. package/templates/adoption/knowledge/wiki/open-questions/index.md +3 -0
  140. package/templates/adoption/knowledge/wiki/product/index.md +3 -0
  141. package/templates/adoption/knowledge/wiki/research/index.md +3 -0
  142. package/templates/adoption/product/AGENT-CONTEXT.md +25 -0
@@ -0,0 +1,41 @@
1
+ # Dunning on failed charge - verification
2
+
3
+ **Verdict**: PASS
4
+ **Profile**: standard
5
+ **Diff range**: a1b2c3d..e4f5g6h
6
+ **Round**: 1 - full
7
+ **Verifier**: independent sub-agent (author != verifier)
8
+
9
+ ## Checks
10
+
11
+ | Check | Claim | Proof run | Evidence | Result |
12
+ | --- | --- | --- | --- | --- |
13
+ | C1 | suspends, never cancels | `bin/rails test ... -n "/failed_charge_suspends/"` exit 0 | `test/billing/dunning_test.rb:118` - `assert_equal "suspended", sub.status` | PASS |
14
+ | C8 | retry changes nothing | `bin/rails test ... -n "/retry_is_idempotent/"` exit 0 | `test/webhooks/ingest_test.rb:64` - `assert_equal 1, WebhookDelivery.count` | PASS |
15
+
16
+ ## Coverage
17
+
18
+ | Set (size) | Recomputed from | Member -> proof | Unproven |
19
+ | --- | --- | --- | --- |
20
+ | provider status -> local (9) | provider webhook reference | C2, table-driven over all 9 | - |
21
+ | webhook event types (5) | provider webhook reference | `charge_failed` C1 · `charge_succeeded` C6 · `updated` C10 · `deleted` C10 · `trial_will_end` C10 | - |
22
+
23
+ ## Test policy rows
24
+
25
+ | Row | Files it classifies | Required proof | Expectation met |
26
+ | --- | --- | --- | --- |
27
+ | Decides, reached across a boundary | `app/webhooks/ingest.rb` | boundary C9 · own layer C10 | yes |
28
+ | Decides, not reached across a boundary | `app/models/billing/status_map.rb` | own layer C2 | yes |
29
+ | Instrumentation, pass-throughs | `app/webhooks/payload.rb` | none of its own | yes |
30
+
31
+ ## Faults injected
32
+
33
+ | Mutation | Location | Killed |
34
+ | --- | --- | --- |
35
+ | returned status `suspended` -> `cancelled` | `app/models/billing/subscription.rb:88` | yes |
36
+ | removed the delivery-id guard | `app/webhooks/ingest.rb:31` | yes |
37
+ | bound `>= 3` -> `> 3` on retry count | `app/models/billing/dunning.rb:22` | yes |
38
+
39
+ ## Gate
40
+
41
+ `bin/rails test` - 138 passed, 0 failed
@@ -0,0 +1,412 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ lessons.py - deterministic bookkeeping for the tlc-spec-lean 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. The script file lives in this skill's
15
+ `scripts/` directory - invoke it as `python3 <skill-dir>/scripts/lessons.py ...`
16
+ (never `python3 scripts/lessons.py` from a consuming project root). Run with
17
+ cwd at the project root (the dir that contains .specs), or pass --root.
18
+
19
+ Commands:
20
+ add Record a grounded lesson from a verification signal.
21
+ list Print lessons (default: confirmed) for loading at Plan/Checks.
22
+ penalize Mark a confirmed lesson as having failed when applied (-> quarantine).
23
+ prune Drop stale uncorroborated candidates (also runs automatically on add/list).
24
+ status Print counts (used by the self-check in verify.md).
25
+ init Create empty store + rendered file.
26
+ selftest Run stdlib regressions (normalization).
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 unicodedata
38
+
39
+ STORE_REL = os.path.join(".specs", "lessons.json")
40
+ RENDER_REL = os.path.join(".specs", "LESSONS.md")
41
+
42
+ SIGNALS = {
43
+ "ac_gap": "Acceptance criterion not covered / failed",
44
+ "surviving_mutant": "Discrimination sensor mutant survived (weak test)",
45
+ "spec_precision_gap": "Spec did not define a precise outcome",
46
+ "spec_deviation": "Implementation diverged from spec/design (SPEC_DEVIATION)",
47
+ "gate_fail": "Build-level gate check failed",
48
+ }
49
+
50
+ DEFAULTS = {"promote_threshold": 2, "window_days": 45, "quarantine_threshold": 2}
51
+
52
+
53
+ def _now():
54
+ return _dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
55
+
56
+
57
+ def _parse_date(s):
58
+ try:
59
+ return _dt.datetime.strptime(s, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=_dt.timezone.utc)
60
+ except Exception:
61
+ return _dt.datetime.now(_dt.timezone.utc)
62
+
63
+
64
+ def _store_path(root):
65
+ return os.path.join(root, STORE_REL)
66
+
67
+
68
+ def _render_path(root):
69
+ return os.path.join(root, RENDER_REL)
70
+
71
+
72
+ def _load(root):
73
+ path = _store_path(root)
74
+ if not os.path.exists(path):
75
+ return {
76
+ "schema": 1,
77
+ "promote_threshold": DEFAULTS["promote_threshold"],
78
+ "window_days": DEFAULTS["window_days"],
79
+ "quarantine_threshold": DEFAULTS["quarantine_threshold"],
80
+ "next_id": 1,
81
+ "lessons": [],
82
+ }
83
+ with open(path, "r", encoding="utf-8") as f:
84
+ data = json.load(f)
85
+ for k, v in DEFAULTS.items():
86
+ data.setdefault(k, v)
87
+ data.setdefault("schema", 1)
88
+ data.setdefault("next_id", 1)
89
+ data.setdefault("lessons", [])
90
+ return data
91
+
92
+
93
+ def _save(root, data):
94
+ os.makedirs(os.path.join(root, ".specs"), exist_ok=True)
95
+ with open(_store_path(root), "w", encoding="utf-8") as f:
96
+ json.dump(data, f, indent=2, ensure_ascii=False)
97
+ f.write("\n")
98
+ _render(root, data)
99
+
100
+
101
+ def _norm(text):
102
+ """Normalized dedup key for lesson text.
103
+
104
+ - casefold + NFD, strip combining marks (so Portuguese diacritics match ASCII peers)
105
+ - keep characters where str.isalnum() is true (any script) and whitespace
106
+ - drop other punctuation, collapse whitespace
107
+
108
+ Exact-after-normalization only - no semantic matching (stdlib-only limitation).
109
+ Phrase lessons tersely and canonically so recurrences actually merge.
110
+ """
111
+ t = unicodedata.normalize("NFD", text.casefold())
112
+ t = "".join(c for c in t if unicodedata.category(c) != "Mn")
113
+ t = "".join(c if (c.isalnum() or c.isspace()) else " " for c in t)
114
+ t = re.sub(r"\s+", " ", t).strip()
115
+ return t
116
+
117
+
118
+ def _selftest_norm():
119
+ """Regressions for #158: Portuguese diacritics + distinct non-Latin text."""
120
+ failures = []
121
+
122
+ def check(cond, msg):
123
+ if not cond:
124
+ failures.append(msg)
125
+
126
+ a = _norm("Não use datas locais")
127
+ b = _norm("Nao use datas locais")
128
+ check(a == b == "nao use datas locais", f"PT diacritics: {a!r} vs {b!r}")
129
+
130
+ jp1 = _norm("日本語の文です")
131
+ jp2 = _norm("別の日本語文")
132
+ check(jp1 != "", f"JP1 empty: {jp1!r}")
133
+ check(jp2 != "", f"JP2 empty: {jp2!r}")
134
+ check(jp1 != jp2, f"JP sentences collapsed: {jp1!r} == {jp2!r}")
135
+
136
+ check(_norm("café") == _norm("cafe") == "cafe", f"cafe: {_norm('café')!r}")
137
+
138
+ if failures:
139
+ for f in failures:
140
+ print(f"FAIL: {f}", file=sys.stderr)
141
+ return 1
142
+ print("selftest_norm: ok")
143
+ return 0
144
+
145
+
146
+ def _key(signal, text):
147
+ return signal + "::" + _norm(text)
148
+
149
+
150
+ def _auto_prune(data):
151
+ """Drop candidates that never recurred within the window. Mutates data."""
152
+ threshold = data["promote_threshold"]
153
+ window = data["window_days"]
154
+ now = _dt.datetime.now(_dt.timezone.utc)
155
+ kept = []
156
+ dropped = []
157
+ for l in data["lessons"]:
158
+ if l["status"] == "candidate" and l["recurrence"] < threshold:
159
+ age_days = (now - _parse_date(l.get("last_seen", l.get("created", _now())))).days
160
+ if age_days > window:
161
+ dropped.append(l["id"])
162
+ continue
163
+ kept.append(l)
164
+ data["lessons"] = kept
165
+ return dropped
166
+
167
+
168
+ def _find(data, signal, text):
169
+ k = _key(signal, text)
170
+ for l in data["lessons"]:
171
+ if l.get("key") == k:
172
+ return l
173
+ return None
174
+
175
+
176
+ def _render(root, data):
177
+ lines = []
178
+ lines.append("# LESSONS - auto-maintained by scripts/lessons.py")
179
+ lines.append("")
180
+ lines.append("> Machine-owned. Do NOT hand-edit. Changes are overwritten on the next `lessons.py` write.")
181
+ lines.append("> Canonical state lives in `.specs/lessons.json`. Edit lessons only via the script.")
182
+ lines.append(f"> promote_threshold={data['promote_threshold']} distinct features · window_days={data['window_days']} · quarantine_threshold={data['quarantine_threshold']}")
183
+ lines.append("")
184
+
185
+ by_status = {"confirmed": [], "candidate": [], "quarantined": []}
186
+ for l in data["lessons"]:
187
+ by_status.get(l["status"], by_status["candidate"]).append(l)
188
+
189
+ def block(title, items, note):
190
+ out = [f"## {title}", ""]
191
+ if note:
192
+ out.append(note)
193
+ out.append("")
194
+ if not items:
195
+ out.append("_none_")
196
+ out.append("")
197
+ return out
198
+ for l in sorted(items, key=lambda x: x["id"]):
199
+ scope = f" · scope: `{l['scope']}`" if l.get("scope") else ""
200
+ out.append(f"### {l['id']} - {l['text']}")
201
+ out.append(
202
+ f"- signal: `{l['signal']}` · recurrence: {l['recurrence']} feature(s){scope} · harmful: {l.get('harmful', 0)}"
203
+ )
204
+ feats = ", ".join(l.get("features", [])) or "-"
205
+ out.append(f"- features: {feats}")
206
+ ev = l.get("evidence", [])
207
+ if ev:
208
+ out.append(f"- evidence: {ev[0]}" + (f" (+{len(ev) - 1} more)" if len(ev) > 1 else ""))
209
+ out.append(f"- last seen: {l.get('last_seen', '-')}")
210
+ out.append("")
211
+ return out
212
+
213
+ lines += block(
214
+ "Confirmed (load these at Plan/Checks)",
215
+ by_status["confirmed"],
216
+ "Corroborated across multiple features. Safe to apply as guidance.",
217
+ )
218
+ lines += block(
219
+ "Candidates (under observation - do NOT load as guidance yet)",
220
+ by_status["candidate"],
221
+ "Seen once or not yet corroborated. Tracked, not trusted.",
222
+ )
223
+ lines += block(
224
+ "Quarantined (failed when applied - ignore)",
225
+ by_status["quarantined"],
226
+ "A confirmed lesson that recurred alongside failure. Kept for the maintainer to review.",
227
+ )
228
+
229
+ with open(_render_path(root), "w", encoding="utf-8") as f:
230
+ f.write("\n".join(lines).rstrip() + "\n")
231
+
232
+
233
+ # ----------------------------- commands -----------------------------
234
+
235
+ def cmd_init(root, args):
236
+ data = _load(root)
237
+ _save(root, data)
238
+ print(f"Initialized lessons store at {_store_path(root)} and {_render_path(root)}")
239
+ return 0
240
+
241
+
242
+ def cmd_add(root, args):
243
+ signal = args.signal
244
+ source = (args.source or "").strip()
245
+ text = (args.text or "").strip()
246
+ feature = (args.feature or "").strip()
247
+
248
+ # Grounding is enforced here, deterministically - not left to the prompt.
249
+ if signal not in SIGNALS:
250
+ print(f"ERROR: --signal must be one of {sorted(SIGNALS)}", file=sys.stderr)
251
+ return 2
252
+ if not feature:
253
+ print("ERROR: --feature is required (the feature the signal came from).", file=sys.stderr)
254
+ return 2
255
+ if not source:
256
+ print("ERROR: --source is required (file:line / AC id / mutant id / SPEC_DEVIATION ref).", file=sys.stderr)
257
+ print(" A lesson with no grounding in verification.md is an opinion, not a lesson. Refused.", file=sys.stderr)
258
+ return 2
259
+ if len(text) < 12:
260
+ print("ERROR: --text too short. State the actionable lesson in one terse sentence.", file=sys.stderr)
261
+ return 2
262
+
263
+ data = _load(root)
264
+ _auto_prune(data)
265
+ existing = _find(data, signal, text)
266
+ now = _now()
267
+
268
+ if existing:
269
+ if feature not in existing["features"]:
270
+ existing["features"].append(feature)
271
+ existing["recurrence"] = len(existing["features"])
272
+ existing["last_seen"] = now
273
+ ev = source if not args.scope else f"{source} ({args.scope})"
274
+ if ev not in existing["evidence"]:
275
+ existing["evidence"].append(ev)
276
+ promoted = False
277
+ if existing["status"] == "candidate" and existing["recurrence"] >= data["promote_threshold"]:
278
+ existing["status"] = "confirmed"
279
+ promoted = True
280
+ _save(root, data)
281
+ msg = f"UPDATED {existing['id']} (recurrence={existing['recurrence']}, status={existing['status']})"
282
+ if promoted:
283
+ msg += " - PROMOTED to confirmed"
284
+ print(msg)
285
+ else:
286
+ lid = f"L-{data['next_id']:03d}"
287
+ data["next_id"] += 1
288
+ data["lessons"].append(
289
+ {
290
+ "id": lid,
291
+ "key": _key(signal, text),
292
+ "text": text,
293
+ "signal": signal,
294
+ "scope": (args.scope or "").strip(),
295
+ "status": "candidate",
296
+ "features": [feature],
297
+ "recurrence": 1,
298
+ "harmful": 0,
299
+ "evidence": [source if not args.scope else f"{source} ({args.scope})"],
300
+ "created": now,
301
+ "last_seen": now,
302
+ }
303
+ )
304
+ _save(root, data)
305
+ print(f"ADDED {lid} (status=candidate, recurrence=1)")
306
+ return 0
307
+
308
+
309
+ def cmd_penalize(root, args):
310
+ data = _load(root)
311
+ target = None
312
+ for l in data["lessons"]:
313
+ if l["id"].lower() == args.id.lower():
314
+ target = l
315
+ break
316
+ if not target:
317
+ print(f"ERROR: no lesson with id {args.id}", file=sys.stderr)
318
+ return 2
319
+ target["harmful"] = target.get("harmful", 0) + 1
320
+ target["last_seen"] = _now()
321
+ if target["harmful"] >= data["quarantine_threshold"]:
322
+ target["status"] = "quarantined"
323
+ _save(root, data)
324
+ print(f"PENALIZED {target['id']} (harmful={target['harmful']}, status={target['status']})")
325
+ return 0
326
+
327
+
328
+ def cmd_list(root, args):
329
+ data = _load(root)
330
+ if _auto_prune(data):
331
+ _save(root, data)
332
+ want = args.status
333
+ q = (args.query or "").lower().strip()
334
+ scope = (args.scope or "").lower().strip()
335
+ rows = []
336
+ for l in data["lessons"]:
337
+ if want != "all" and l["status"] != want:
338
+ continue
339
+ if q and q not in l["text"].lower():
340
+ continue
341
+ if scope and scope not in (l.get("scope", "").lower()):
342
+ continue
343
+ rows.append(l)
344
+ if not rows:
345
+ print(f"(no {want} lessons" + (f" matching '{q or scope}'" if (q or scope) else "") + ")")
346
+ return 0
347
+ for l in sorted(rows, key=lambda x: x["id"]):
348
+ sc = f" [scope:{l['scope']}]" if l.get("scope") else ""
349
+ print(f"{l['id']} ({l['status']}, x{l['recurrence']}){sc}: {l['text']}")
350
+ return 0
351
+
352
+
353
+ def cmd_prune(root, args):
354
+ data = _load(root)
355
+ dropped = _auto_prune(data)
356
+ _save(root, data)
357
+ print(f"Pruned {len(dropped)} stale candidate(s): {', '.join(dropped) if dropped else '-'}")
358
+ return 0
359
+
360
+
361
+ def cmd_status(root, args):
362
+ data = _load(root)
363
+ counts = {"confirmed": 0, "candidate": 0, "quarantined": 0}
364
+ for l in data["lessons"]:
365
+ counts[l["status"]] = counts.get(l["status"], 0) + 1
366
+ total = len(data["lessons"])
367
+ print(f"lessons: {total} total | confirmed={counts['confirmed']} candidate={counts['candidate']} quarantined={counts['quarantined']}")
368
+ return 0
369
+
370
+
371
+ def main(argv=None):
372
+ p = argparse.ArgumentParser(prog="lessons.py", description="Deterministic lessons bookkeeping for tlc-spec-lean.")
373
+ p.add_argument("--root", default=".", help="Project root containing .specs/ (default: current dir)")
374
+ sub = p.add_subparsers(dest="cmd", required=True)
375
+
376
+ sp = sub.add_parser("init", help="Create empty store + rendered file")
377
+ sp.set_defaults(fn=cmd_init)
378
+
379
+ sp = sub.add_parser("add", help="Record a grounded lesson")
380
+ sp.add_argument("--feature", required=True)
381
+ sp.add_argument("--signal", required=True, choices=sorted(SIGNALS))
382
+ sp.add_argument("--source", required=True, help="file:line / AC id / mutant id / SPEC_DEVIATION ref")
383
+ sp.add_argument("--text", required=True, help="One terse, actionable sentence")
384
+ sp.add_argument("--scope", default="", help="Optional: path/layer/tag for retrieval filtering")
385
+ sp.set_defaults(fn=cmd_add)
386
+
387
+ sp = sub.add_parser("penalize", help="Mark a confirmed lesson as failed-when-applied")
388
+ sp.add_argument("--id", required=True)
389
+ sp.set_defaults(fn=cmd_penalize)
390
+
391
+ sp = sub.add_parser("list", help="Print lessons for loading")
392
+ sp.add_argument("--status", default="confirmed", choices=["confirmed", "candidate", "quarantined", "all"])
393
+ sp.add_argument("--query", default="", help="Substring filter on lesson text")
394
+ sp.add_argument("--scope", default="", help="Substring filter on scope")
395
+ sp.set_defaults(fn=cmd_list)
396
+
397
+ sp = sub.add_parser("prune", help="Drop stale uncorroborated candidates")
398
+ sp.set_defaults(fn=cmd_prune)
399
+
400
+ sp = sub.add_parser("status", help="Print counts")
401
+ sp.set_defaults(fn=cmd_status)
402
+
403
+ sp = sub.add_parser("selftest", help="Run stdlib regressions (normalization)")
404
+ sp.set_defaults(fn=lambda root, args: _selftest_norm())
405
+
406
+ args = p.parse_args(argv)
407
+ root = os.path.abspath(args.root)
408
+ return args.fn(root, args)
409
+
410
+
411
+ if __name__ == "__main__":
412
+ raise SystemExit(main())