opencode-skills-collection 4.0.0 → 4.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 (241) hide show
  1. package/bundled-skills/.antigravity-install-manifest.json +24 -1
  2. package/bundled-skills/007/SKILL.md +2 -2
  3. package/bundled-skills/10-andruia-skill-smith/SKILL.md +1 -1
  4. package/bundled-skills/agent-squad/SKILL.md +5 -0
  5. package/bundled-skills/agent-squad/alex/SKILL.md +3 -0
  6. package/bundled-skills/agent-squad/aria/SKILL.md +3 -0
  7. package/bundled-skills/agent-squad/dep/SKILL.md +3 -0
  8. package/bundled-skills/agent-squad/luna/SKILL.md +4 -1
  9. package/bundled-skills/agent-squad/mason/SKILL.md +3 -0
  10. package/bundled-skills/agent-squad/max/SKILL.md +3 -0
  11. package/bundled-skills/agent-squad/quinn/SKILL.md +3 -0
  12. package/bundled-skills/agent-squad/rex/SKILL.md +3 -0
  13. package/bundled-skills/angular/metadata.json +1 -1
  14. package/bundled-skills/angular-best-practices/metadata.json +1 -1
  15. package/bundled-skills/angular-state-management/metadata.json +1 -1
  16. package/bundled-skills/angular-ui-patterns/metadata.json +1 -1
  17. package/bundled-skills/antigravity-skill-orchestrator/SKILL.md +2 -2
  18. package/bundled-skills/apify-actor-development/SKILL.md +2 -2
  19. package/bundled-skills/apple-container/SKILL.md +165 -0
  20. package/bundled-skills/apple-container/references/commands.md +1593 -0
  21. package/bundled-skills/apple-container/references/concepts.md +294 -0
  22. package/bundled-skills/apple-container/references/configuration.md +499 -0
  23. package/bundled-skills/apple-container/references/workflows.md +498 -0
  24. package/bundled-skills/ask-copilot/SKILL.md +112 -0
  25. package/bundled-skills/auto-research/SKILL.md +126 -0
  26. package/bundled-skills/brooks-lint/SKILL.md +1 -1
  27. package/bundled-skills/browser-harness/references/install.md +3 -1
  28. package/bundled-skills/chrome-extension-developer/SKILL.md +1 -1
  29. package/bundled-skills/clean-code-guard/SKILL.md +184 -0
  30. package/bundled-skills/clean-code-guard/references/ai-failure-modes.md +345 -0
  31. package/bundled-skills/clean-code-guard/references/comments-and-formatting.md +115 -0
  32. package/bundled-skills/clean-code-guard/references/dry-kiss-yagni.md +133 -0
  33. package/bundled-skills/clean-code-guard/references/naming-and-functions.md +187 -0
  34. package/bundled-skills/clean-code-guard/references/review-checklist.md +150 -0
  35. package/bundled-skills/clean-code-guard/references/solid.md +280 -0
  36. package/bundled-skills/clean-code-guard/references/sources.md +47 -0
  37. package/bundled-skills/cloudflare-security-audit/SKILL.md +137 -0
  38. package/bundled-skills/cloudflare-security-audit/references/AI-AND-LLM.md +67 -0
  39. package/bundled-skills/cloudflare-security-audit/references/ATTACK-CLASSES.md +116 -0
  40. package/bundled-skills/cloudflare-security-audit/references/CLIENT-SIDE.md +67 -0
  41. package/bundled-skills/cloudflare-security-audit/references/HUNTING.md +110 -0
  42. package/bundled-skills/cloudflare-security-audit/references/MEMORY-SAFETY-AND-BINARY.md +58 -0
  43. package/bundled-skills/cloudflare-security-audit/references/RECONNAISSANCE.md +46 -0
  44. package/bundled-skills/cloudflare-security-audit/references/VALIDATION-AND-REPORTING.md +109 -0
  45. package/bundled-skills/cloudflare-security-audit/references/WEB-PROTOCOL-AND-AUTH.md +85 -0
  46. package/bundled-skills/cloudflare-security-audit/resources/report-schema.json +210 -0
  47. package/bundled-skills/comfyui-gateway/references/troubleshooting.md +1 -1
  48. package/bundled-skills/commit/SKILL.md +2 -1
  49. package/bundled-skills/deepapi/SKILL.md +6 -6
  50. package/bundled-skills/docs/README.md +1 -1
  51. package/bundled-skills/docs/contributors/quality-bar.md +1 -1
  52. package/bundled-skills/docs/contributors/security-guardrails.md +1 -1
  53. package/bundled-skills/docs/integrations/jetski-cortex.md +5 -5
  54. package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +3 -3
  55. package/bundled-skills/docs/maintainers/ci-drift-fix.md +7 -5
  56. package/bundled-skills/docs/maintainers/merge-batch.md +24 -7
  57. package/bundled-skills/docs/maintainers/pr-autonomy.md +89 -0
  58. package/bundled-skills/docs/maintainers/release-notes-7.2.0.md +1 -1
  59. package/bundled-skills/docs/maintainers/release-process.md +9 -9
  60. package/bundled-skills/docs/maintainers/repo-growth-seo.md +5 -5
  61. package/bundled-skills/docs/maintainers/skills-date-tracking.md +1 -1
  62. package/bundled-skills/docs/maintainers/skills-update-guide.md +2 -2
  63. package/bundled-skills/docs/users/{antigravity-awesome-skills-vs-awesome-claude-skills.md → agentic-awesome-skills-vs-awesome-claude-skills.md} +5 -5
  64. package/bundled-skills/docs/users/ai-agent-skills.md +4 -4
  65. package/bundled-skills/docs/users/best-claude-code-skills-github.md +3 -3
  66. package/bundled-skills/docs/users/best-cursor-skills-github.md +3 -3
  67. package/bundled-skills/docs/users/bundles.md +56 -57
  68. package/bundled-skills/docs/users/claude-code-skills.md +6 -6
  69. package/bundled-skills/docs/users/codex-cli-skills.md +5 -5
  70. package/bundled-skills/docs/users/cursor-skills.md +3 -3
  71. package/bundled-skills/docs/users/faq.md +27 -15
  72. package/bundled-skills/docs/users/gemini-cli-skills.md +4 -4
  73. package/bundled-skills/docs/users/getting-started.md +22 -12
  74. package/bundled-skills/docs/users/kiro-integration.md +7 -7
  75. package/bundled-skills/docs/users/plugins.md +8 -9
  76. package/bundled-skills/docs/users/security-skills.md +1 -1
  77. package/bundled-skills/docs/users/skills-vs-mcp-tools.md +1 -1
  78. package/bundled-skills/docs/users/specialized-plugin-roadmap.md +3 -4
  79. package/bundled-skills/docs/users/usage.md +9 -9
  80. package/bundled-skills/docs/users/visual-guide.md +7 -7
  81. package/bundled-skills/docs/users/windows-truncation-recovery.md +1 -1
  82. package/bundled-skills/docs/users/workflows.md +1 -1
  83. package/bundled-skills/docs/vietnamese/BUNDLES.vi.md +1 -1
  84. package/bundled-skills/docs/vietnamese/FAQ.vi.md +15 -8
  85. package/bundled-skills/docs/vietnamese/GETTING_STARTED.vi.md +12 -5
  86. package/bundled-skills/docs/vietnamese/QUALITY_BAR.vi.md +1 -1
  87. package/bundled-skills/docs/vietnamese/README.vi.md +21 -17
  88. package/bundled-skills/docs/vietnamese/SECURITY_GUARDRAILS.vi.md +1 -1
  89. package/bundled-skills/docs/vietnamese/SKILLS_README.vi.md +2 -2
  90. package/bundled-skills/docs/vietnamese/TRANSLATION_PLAN.vi.md +1 -1
  91. package/bundled-skills/docs/vietnamese/VISUAL_GUIDE.vi.md +8 -8
  92. package/bundled-skills/docs-guard/SKILL.md +109 -0
  93. package/bundled-skills/docs-guard/references/code-samples.md +46 -0
  94. package/bundled-skills/docs-guard/references/docstrings.md +60 -0
  95. package/bundled-skills/docs-guard/references/review-checklist.md +45 -0
  96. package/bundled-skills/docs-guard/references/sources.md +22 -0
  97. package/bundled-skills/docs-guard/references/verification.md +51 -0
  98. package/bundled-skills/efficient-web-research/SKILL.md +4 -0
  99. package/bundled-skills/electron-development/SKILL.md +1 -1
  100. package/bundled-skills/event-staffing-compliance/SKILL.md +3 -0
  101. package/bundled-skills/event-staffing-ordering/SKILL.md +3 -0
  102. package/bundled-skills/feature-tracking/SKILL.md +222 -0
  103. package/bundled-skills/flutter-expert/SKILL.md +6 -1
  104. package/bundled-skills/flutter-expert/resources/implementation-playbook.md +214 -0
  105. package/bundled-skills/gemini-deep-research/README.md +246 -0
  106. package/bundled-skills/gemini-deep-research/SKILL.md +136 -0
  107. package/bundled-skills/gemini-deep-research/requirements.txt +2 -0
  108. package/bundled-skills/gemini-deep-research/scripts/research.py +692 -0
  109. package/bundled-skills/git-pushing/SKILL.md +21 -4
  110. package/bundled-skills/git-pushing/scripts/smart_commit.sh +206 -11
  111. package/bundled-skills/go-in-depth/scripts/workflow-script.js +4 -1
  112. package/bundled-skills/grok-build/SKILL.md +167 -0
  113. package/bundled-skills/grok-build/references/cli.md +99 -0
  114. package/bundled-skills/huggingface-tool-builder/references/find_models_by_paper.sh +1 -1
  115. package/bundled-skills/idea-autopsy/SKILL.md +147 -0
  116. package/bundled-skills/it-manager-hospital/README.md +1 -1
  117. package/bundled-skills/it-manager-pro/README.md +1 -1
  118. package/bundled-skills/itil-expert/README.md +1 -1
  119. package/bundled-skills/langgraph/SKILL.md +1 -1
  120. package/bundled-skills/linkedin-content-generator/SKILL.md +1 -1
  121. package/bundled-skills/lint-and-validate/scripts/lint_runner.py +23 -23
  122. package/bundled-skills/lint-and-validate/scripts/type_coverage.py +30 -30
  123. package/bundled-skills/logic-lens/SKILL.md +1 -1
  124. package/bundled-skills/lore/LICENSE +21 -0
  125. package/bundled-skills/lore/README.md +386 -0
  126. package/bundled-skills/lore/README.zh-CN.md +386 -0
  127. package/bundled-skills/lore/SKILL.md +453 -0
  128. package/bundled-skills/lore/WORKFLOWS.md +216 -0
  129. package/bundled-skills/lore/WORKFLOWS.zh-CN.md +216 -0
  130. package/bundled-skills/lore/references/audit-template.md +60 -0
  131. package/bundled-skills/lore/references/compatibility.md +228 -0
  132. package/bundled-skills/lore/references/config.md +125 -0
  133. package/bundled-skills/lore/references/entry-format.md +70 -0
  134. package/bundled-skills/lore/references/history-command.md +106 -0
  135. package/bundled-skills/lore/references/monorepo-detection.md +78 -0
  136. package/bundled-skills/lore/references/platform-mirrors.md +377 -0
  137. package/bundled-skills/lore/references/stale-new-markers.md +62 -0
  138. package/bundled-skills/lore/references/summary-template.md +98 -0
  139. package/bundled-skills/lore/scripts/README.md +54 -0
  140. package/bundled-skills/lore/scripts/README.zh-CN.md +54 -0
  141. package/bundled-skills/lore/scripts/find_duplicates.py +211 -0
  142. package/bundled-skills/lore/scripts/find_stale.py +116 -0
  143. package/bundled-skills/lore/scripts/history.py +527 -0
  144. package/bundled-skills/lore/scripts/id_hash.py +32 -0
  145. package/bundled-skills/lore/scripts/list_entries.py +183 -0
  146. package/bundled-skills/lovable-cleanup/SKILL.md +3 -3
  147. package/bundled-skills/makepad-splash/SKILL.md +1 -1
  148. package/bundled-skills/mmx-cli/SKILL.md +14 -1
  149. package/bundled-skills/monopoly/SKILL.md +5 -0
  150. package/bundled-skills/monopoly/patterns/SKILL.md +4 -0
  151. package/bundled-skills/monopoly/scale-benchmarks/SKILL.md +4 -0
  152. package/bundled-skills/monopoly/security-checklist/SKILL.md +4 -0
  153. package/bundled-skills/monopoly/tech-matrix/SKILL.md +4 -0
  154. package/bundled-skills/mtls-configuration/SKILL.md +1 -1
  155. package/bundled-skills/network-101/SKILL.md +4 -5
  156. package/bundled-skills/nika/SKILL.md +270 -0
  157. package/bundled-skills/not-a-vibe-coder/SKILL.md +4 -0
  158. package/bundled-skills/ontoly-software-graph/SKILL.md +160 -0
  159. package/bundled-skills/openclaw-github-repo-commander/SKILL.md +1 -1
  160. package/bundled-skills/pilot-protocol/SKILL.md +6 -3
  161. package/bundled-skills/postgres-readonly-queries/README.md +77 -0
  162. package/bundled-skills/postgres-readonly-queries/SKILL.md +152 -0
  163. package/bundled-skills/postgres-readonly-queries/connections.example.json +34 -0
  164. package/bundled-skills/postgres-readonly-queries/requirements.txt +1 -0
  165. package/bundled-skills/postgres-readonly-queries/scripts/query.py +262 -0
  166. package/bundled-skills/pptx-deck-creation/SKILL.md +275 -0
  167. package/bundled-skills/pptx-deck-creation/references/audit-checklist.md +156 -0
  168. package/bundled-skills/pptx-deck-creation/references/design-profiles.md +96 -0
  169. package/bundled-skills/pptx-deck-creation/references/python-snippets.md +150 -0
  170. package/bundled-skills/pptx-deck-creation/references/reference-deck-analysis.md +91 -0
  171. package/bundled-skills/pptx-deck-creation/references/visual-asset-adapters.md +158 -0
  172. package/bundled-skills/privilege-escalation-methods/SKILL.md +1 -1
  173. package/bundled-skills/product-decision-agent/SKILL.md +94 -0
  174. package/bundled-skills/product-decision-agent/agents/openai.yaml +7 -0
  175. package/bundled-skills/product-decision-agent/references/methodology-basis.md +232 -0
  176. package/bundled-skills/product-decision-agent/references/product-playbooks.md +813 -0
  177. package/bundled-skills/product-decision-agent/references/reasoning-engine.md +289 -0
  178. package/bundled-skills/product-decision-agent/references/response-examples.md +279 -0
  179. package/bundled-skills/product-decision-agent/scripts/quality_gate.py +221 -0
  180. package/bundled-skills/product-decision-agent/scripts/test_quality_gate.py +94 -0
  181. package/bundled-skills/pytest-skill/reference/playbook.md +358 -0
  182. package/bundled-skills/quit-sponsor/SKILL.md +96 -0
  183. package/bundled-skills/rclone-cli/references/providers/s3.md +1 -1
  184. package/bundled-skills/security-and-hardening/SKILL.md +1 -1
  185. package/bundled-skills/semgrep-rule-creator/SKILL.md +6 -6
  186. package/bundled-skills/skill-creator/README.md +3 -3
  187. package/bundled-skills/skill-creator/SKILL.md +8 -4
  188. package/bundled-skills/skill-creator/scripts/quick_validate.py +1 -1
  189. package/bundled-skills/sql-injection-testing/SKILL.md +4 -4
  190. package/bundled-skills/subagent-orchestrator/README.md +1 -1
  191. package/bundled-skills/super-code/bash/SKILL.md +3 -0
  192. package/bundled-skills/super-code/c/SKILL.md +3 -0
  193. package/bundled-skills/super-code/cpp/SKILL.md +3 -0
  194. package/bundled-skills/super-code/csharp/SKILL.md +3 -0
  195. package/bundled-skills/super-code/dart/SKILL.md +3 -0
  196. package/bundled-skills/super-code/elixir/SKILL.md +3 -0
  197. package/bundled-skills/super-code/go/SKILL.md +3 -0
  198. package/bundled-skills/super-code/java/SKILL.md +3 -0
  199. package/bundled-skills/super-code/kotlin/SKILL.md +3 -0
  200. package/bundled-skills/super-code/php/SKILL.md +3 -0
  201. package/bundled-skills/super-code/python/SKILL.md +3 -0
  202. package/bundled-skills/super-code/ruby/SKILL.md +3 -0
  203. package/bundled-skills/super-code/rust/SKILL.md +3 -0
  204. package/bundled-skills/super-code/scala/SKILL.md +3 -0
  205. package/bundled-skills/super-code/swift/SKILL.md +3 -0
  206. package/bundled-skills/super-code/typescript/SKILL.md +3 -0
  207. package/bundled-skills/telegram-bot-messaging/README.md +185 -0
  208. package/bundled-skills/telegram-bot-messaging/SKILL.md +101 -0
  209. package/bundled-skills/telegram-bot-messaging/scripts/telegram.sh +420 -0
  210. package/bundled-skills/test-guard/SKILL.md +132 -0
  211. package/bundled-skills/test-guard/references/jest.md +50 -0
  212. package/bundled-skills/test-guard/references/llm-app-testing.md +40 -0
  213. package/bundled-skills/test-guard/references/phpunit.md +56 -0
  214. package/bundled-skills/test-guard/references/pytest.md +65 -0
  215. package/bundled-skills/trading-ledger/SKILL.md +11 -3
  216. package/bundled-skills/uizze-ui-research/SKILL.md +93 -0
  217. package/bundled-skills/vibe-code-auditor/SKILL.md +1 -1
  218. package/bundled-skills/vibecode-production-qa-validator/SKILL.md +1 -1
  219. package/bundled-skills/videodb/scripts/ws_listener.py +29 -15
  220. package/bundled-skills/voice-ai-engine-development/README.md +1 -1
  221. package/bundled-skills/vulnerability-scanner/SKILL.md +1 -1
  222. package/bundled-skills/warehouse/SKILL.md +154 -0
  223. package/bundled-skills/woo-guard/SKILL.md +113 -0
  224. package/bundled-skills/woo-guard/references/checkout-and-money.md +64 -0
  225. package/bundled-skills/woo-guard/references/hpos-and-crud.md +87 -0
  226. package/bundled-skills/woo-guard/references/review-checklist.md +44 -0
  227. package/bundled-skills/woo-guard/references/sources.md +17 -0
  228. package/bundled-skills/wp-guard/SKILL.md +119 -0
  229. package/bundled-skills/wp-guard/references/i18n.md +88 -0
  230. package/bundled-skills/wp-guard/references/performance.md +70 -0
  231. package/bundled-skills/wp-guard/references/review-checklist.md +57 -0
  232. package/bundled-skills/wp-guard/references/security.md +100 -0
  233. package/bundled-skills/wp-guard/references/sources.md +27 -0
  234. package/bundled-skills/writing-skills/gotchas.md +1 -1
  235. package/bundled-skills/writing-skills/references/standards/metadata-standard.md +1 -1
  236. package/bundled-skills/x-twitter-scraper/SKILL.md +14 -32
  237. package/bundled-skills/xss-html-injection/SKILL.md +3 -3
  238. package/bundled-skills/youtube-seo-optimizer/SKILL.md +1 -1
  239. package/bundled-skills/youtube-summarizer/CHANGELOG.md +2 -2
  240. package/package.json +1 -1
  241. package/skills_index.json +273 -0
@@ -0,0 +1,527 @@
1
+ #!/usr/bin/env python3
2
+ """`lore history` — list git commits related to an entry, file, or scope.
3
+
4
+ Usage:
5
+ lore history <entry-id>
6
+ lore history <file-path>
7
+ lore history --scope=<name>
8
+ lore history --since=<YYYY-MM-DD>
9
+ lore history --json
10
+
11
+ See references/history-command.md for the full specification.
12
+ """
13
+ import re
14
+ import subprocess
15
+ import sys
16
+ from pathlib import Path
17
+ import json as _json # standard library; aliased to avoid clashing with future vars
18
+
19
+
20
+ # Entry ID pattern: LAYER-YYYY-MM-DD-xxxx (4 hex chars)
21
+ ENTRY_ID_RE = re.compile(r"^[A-Z]+-\d{4}-\d{2}-\d{2}-[a-f0-9]{4}$")
22
+
23
+
24
+ def parse_arg(arg: str):
25
+ """Dispatch the first positional argument to entry / file / scope form.
26
+
27
+ Returns a dict {"form": "entry"|"file"|"scope", "value": str}, or None
28
+ if the argument matches none of the recognized patterns.
29
+ """
30
+ if not arg:
31
+ return None
32
+ if arg.startswith("--scope="):
33
+ return {"form": "scope", "value": arg.split("=", 1)[1]}
34
+ if ENTRY_ID_RE.match(arg):
35
+ return {"form": "entry", "value": arg}
36
+ if "/" in arg or arg.startswith("."):
37
+ return {"form": "file", "value": arg}
38
+ return None
39
+
40
+
41
+ def find_entry(entries, entry_id):
42
+ """Look up an entry by ID in the list from list_entries.py --json.
43
+
44
+ Returns the entry dict, or None if not found.
45
+ """
46
+ for e in entries:
47
+ if e.get("id") == entry_id:
48
+ return e
49
+ return None
50
+
51
+
52
+ def extract_added_date(tags):
53
+ """Return the value of the 'added' tag, or None if absent.
54
+
55
+ The entry dict's `tags` field is {name: value, ...} as produced
56
+ by list_entries.py.
57
+ """
58
+ if not tags:
59
+ return None
60
+ return tags.get("added")
61
+
62
+
63
+ # Match a backtick-quoted path inside an entry's text. The path must
64
+ # contain at least one slash OR start with a dot OR end with a common
65
+ # code extension, to avoid false positives like `Zustand`.
66
+ BACKTICK_PATH_RE = re.compile(
67
+ r"`([^\s`]+\.[a-zA-Z0-9]{1,8}(?:\.[a-zA-Z0-9]{1,8})*"
68
+ r"|[^\s`]+/[^\s`]+"
69
+ r"|\.[a-zA-Z][^\s`]*)`"
70
+ )
71
+
72
+
73
+ def resolve_code_file(entry):
74
+ """Decide which file path to git-log for this entry.
75
+
76
+ Priority:
77
+ 1. First backtick-quoted path in entry.text (looks like a file).
78
+ 2. Scope directory at project root (e.g. "frontend" for scope "frontend").
79
+ 3. "." for the _global scope (project root).
80
+
81
+ The path returned is relative to the project root. git log handles
82
+ "." to mean the whole repo.
83
+ """
84
+ if entry.get("text"):
85
+ m = BACKTICK_PATH_RE.search(entry["text"])
86
+ if m:
87
+ return m.group(1)
88
+ scope = entry.get("scope", "_global")
89
+ if scope == "_global":
90
+ return "."
91
+ return scope
92
+
93
+
94
+ # Single-line per commit. The trailing %s for body is multi-line content
95
+ # that we capture separately (not in the delimited format string) by
96
+ # running a second pass with a different format. For v1 we use a simple
97
+ # format and parse body via a follow-up `git show` only if needed.
98
+ #
99
+ # To keep parsing simple, we use a delimiter unlikely to appear in real
100
+ # commit metadata: ASCII Unit Separator (\x1f).
101
+ COMMIT_DELIM = "\x1f"
102
+
103
+ # git log format: hash\x1fauthor\x1fdate(iso)\x1fsubject
104
+ # We use %x1f (the same delimiter) inline so the format string is portable.
105
+ # The body is fetched separately via the second invocation below.
106
+ FORMAT_STRING = "%H%x1f%an%x1f%ai%x1f%s"
107
+
108
+
109
+ def run_git_log(project_root, since, code_file, n=None):
110
+ """Run `git log` and return a list of commit dicts.
111
+
112
+ Args:
113
+ project_root: Path to the git repo root.
114
+ since: ISO date string, or None for full history.
115
+ code_file: Path relative to project_root to filter by.
116
+ n: Optional int cap on number of commits.
117
+
118
+ Returns:
119
+ List of dicts as produced by parse_commit_line + body-fetch.
120
+
121
+ Raises:
122
+ RuntimeError: if git exits non-zero or is missing.
123
+ """
124
+ cmd = [
125
+ "git",
126
+ "-C", str(project_root),
127
+ "log",
128
+ f"--pretty=format:{FORMAT_STRING}",
129
+ ]
130
+ if since:
131
+ cmd.append(f"--since={since}")
132
+ if n is not None:
133
+ cmd.append(f"-n{n}")
134
+ cmd.extend(["--", code_file])
135
+
136
+ try:
137
+ proc = subprocess.run(
138
+ cmd,
139
+ capture_output=True,
140
+ text=True,
141
+ encoding="utf-8",
142
+ errors="replace",
143
+ check=False,
144
+ )
145
+ except FileNotFoundError as exc:
146
+ raise RuntimeError(f"git executable not found on PATH: {exc}")
147
+
148
+ if proc.returncode != 0:
149
+ raise RuntimeError(f"git log failed: {proc.stderr.strip()}")
150
+
151
+ commits = []
152
+ for line in proc.stdout.splitlines():
153
+ if not line:
154
+ continue
155
+ parsed = parse_commit_line(line)
156
+ if parsed is None:
157
+ continue
158
+ parsed["body"] = "" # filled in by fetch_body if requested later
159
+ commits.append(parsed)
160
+ return commits
161
+
162
+
163
+ def parse_commit_line(line):
164
+ """Parse one delimited git log line. Returns dict or None on malformed input."""
165
+ parts = line.split(COMMIT_DELIM)
166
+ if len(parts) != 4:
167
+ return None
168
+ full_hash, author, date, subject = parts
169
+ if len(full_hash) < 7:
170
+ return None
171
+ return {
172
+ "hash": full_hash,
173
+ "short": full_hash[:7],
174
+ "author": author,
175
+ "date": date[:10], # take YYYY-MM-DD from full ISO timestamp
176
+ "subject": subject,
177
+ "body": "", # populated by fetch_commit_body
178
+ }
179
+
180
+
181
+ # Match PR/issue references. Order matters: longer keywords first so
182
+ # "Closes" doesn't get eaten by "#NNN" alone. We require word boundary
183
+ # (or start of string) before the keyword to avoid matching substrings
184
+ # like "address#N" mid-word.
185
+ REFS_RE = re.compile(
186
+ r"(?:\(|\b(?:Closes|Refs|Fixes|Resolves)\s+)"
187
+ r"(#\d+)",
188
+ re.IGNORECASE,
189
+ )
190
+
191
+
192
+ def extract_refs(message):
193
+ """Return a list of PR/issue references found in a commit message.
194
+
195
+ Each item is either "#NNN" (from parens form) or "Keyword #NNN"
196
+ (from Closes/Refs/Fixes/Resolves form). Duplicates are removed
197
+ in order of appearance.
198
+ """
199
+ matches = []
200
+ seen = set()
201
+ for m in REFS_RE.finditer(message):
202
+ prefix = m.group(0).split("#")[0]
203
+ ref = "#" + m.group(1)[1:] # normalize to "#NNN"
204
+ if ref in seen:
205
+ continue
206
+ seen.add(ref)
207
+ if prefix.startswith("("):
208
+ matches.append(ref)
209
+ else:
210
+ matches.append(f"{prefix.strip()} {ref}")
211
+ return matches
212
+
213
+
214
+ def truncate_body(body, max_lines=3):
215
+ """Trim a multi-line string to at most `max_lines`, stripping blank tails.
216
+
217
+ Used to keep commit bodies short in the Markdown output. The subject
218
+ is already shown separately; the body is supplementary context.
219
+ """
220
+ lines = body.splitlines()
221
+ trimmed = lines[:max_lines]
222
+ while trimmed and not trimmed[-1].strip():
223
+ trimmed.pop()
224
+ return "\n".join(trimmed)
225
+
226
+
227
+ def fetch_commit_body(project_root, commit_hash):
228
+ """Fetch the full commit message (subject + body) via `git show`.
229
+
230
+ Returns a string with the subject as the first line and the body
231
+ (if any) following a blank line. Trailing blank lines are removed.
232
+ """
233
+ cmd = [
234
+ "git", "-C", str(project_root),
235
+ "show", "-s", "--format=%B", commit_hash,
236
+ ]
237
+ try:
238
+ proc = subprocess.run(
239
+ cmd, capture_output=True, text=True,
240
+ encoding="utf-8", errors="replace", check=False,
241
+ )
242
+ except FileNotFoundError:
243
+ return ""
244
+ if proc.returncode != 0:
245
+ return ""
246
+ return proc.stdout.rstrip()
247
+
248
+
249
+ def render_json(meta, commits):
250
+ """Render the JSON output for a `lore history` invocation.
251
+
252
+ Output matches the schema documented in the spec.
253
+ """
254
+ payload = {
255
+ "entry_id": meta["entry_id"],
256
+ "lore_file": meta["lore_file"],
257
+ "code_file": meta["code_file"],
258
+ "since": meta["since"],
259
+ "since_source": meta["since_source"],
260
+ "commits": commits,
261
+ }
262
+ return _json.dumps(payload, indent=2, ensure_ascii=False)
263
+
264
+
265
+ def render_markdown(meta, commits):
266
+ """Render the Markdown output for a `lore history` invocation.
267
+
268
+ Args:
269
+ meta: dict with keys entry_id, lore_file, code_file, since,
270
+ since_source.
271
+ commits: list of commit dicts (see parse_commit_line + extract_refs).
272
+
273
+ Returns:
274
+ Markdown string ready for stdout.
275
+ """
276
+ lines = []
277
+ lines.append(f"# history: [{meta['entry_id']}]")
278
+ lines.append("")
279
+ lines.append(f"> Entry: {meta['lore_file']}")
280
+ since_suffix = " (entry #added date)" if meta.get("since_source") == "entry_added" else ""
281
+ lines.append(f"> Since: {meta['since']}{since_suffix}")
282
+ lines.append(f"> File: {meta['code_file']}")
283
+ lines.append(f"> Commits: {len(commits)} (showing all)")
284
+ lines.append("")
285
+
286
+ if not commits:
287
+ return "\n".join(lines) + "\n"
288
+
289
+ for c in commits:
290
+ lines.append(f"## {c['short']} ({c['date']}, {c['author']})")
291
+ lines.append(c["subject"])
292
+ if c.get("body"):
293
+ body = truncate_body(c["body"], max_lines=3)
294
+ lines.append(f' Body: "{body}"')
295
+ if c.get("refs"):
296
+ lines.append(f" Refs: {', '.join(c['refs'])}")
297
+ lines.append("")
298
+
299
+ lines.append("## Suggested next step")
300
+ lines.append("Run `lore sync` to check whether any of these commits")
301
+ lines.append("introduce a [REFINED] candidate for this entry.")
302
+ lines.append("")
303
+ return "\n".join(lines)
304
+
305
+
306
+ # Exit codes per spec section "Error handling".
307
+ ERR_USAGE = 2 # no arg / unrecognized arg (also used by argparse path)
308
+ ERR_NO_LORE = 2 # .lore/ not found
309
+ ERR_NO_ENTRY = 3 # entry ID not in index
310
+ ERR_NOT_GIT = 4 # not a git repository
311
+ ERR_NO_GIT = 5 # git CLI missing
312
+ ERR_BAD_SCOPE = 6 # scope name not in scopes/
313
+ ERR_GIT_FAIL = 7 # git log returned non-zero for other reasons
314
+
315
+
316
+ def die(code, message):
317
+ """Print message to stderr and exit with the given code."""
318
+ print(f"error: {message}", file=sys.stderr)
319
+ sys.exit(code)
320
+
321
+
322
+ def _load_entries_via_subprocess():
323
+ """Run scripts/list_entries.py --json and return the parsed list.
324
+
325
+ Mirrors the pattern in find_duplicates.py / find_stale.py.
326
+ Returns [] if no entries.
327
+ """
328
+ here = Path(__file__).resolve().parent
329
+ cmd = [sys.executable, str(here / "list_entries.py"), "--json"]
330
+ try:
331
+ proc = subprocess.run(cmd, capture_output=True, text=True,
332
+ encoding="utf-8", errors="replace", check=False)
333
+ except FileNotFoundError as exc:
334
+ die(ERR_NO_GIT, f"python executable not found: {exc}")
335
+ if proc.returncode != 0:
336
+ die(ERR_NO_LORE, f"list_entries.py failed: {proc.stderr.strip()}")
337
+ try:
338
+ return _json.loads(proc.stdout)
339
+ except _json.JSONDecodeError as exc:
340
+ die(ERR_NO_LORE, f"list_entries.py returned invalid JSON: {exc}")
341
+
342
+
343
+ def _find_lore_root_or_die():
344
+ """Walk up from CWD to find .lore/. Die with ERR_NO_LORE if not found."""
345
+ p = Path(".").resolve()
346
+ while p != p.parent:
347
+ if (p / ".lore").is_dir():
348
+ return p
349
+ p = p.parent
350
+ die(ERR_NO_LORE, ".lore/ not found. Run 'lore init' first.")
351
+
352
+
353
+ def _build_meta_entry(entry, code_file, since, since_source):
354
+ return {
355
+ "entry_id": entry["id"],
356
+ "lore_file": entry["file"],
357
+ "code_file": code_file,
358
+ "since": since,
359
+ "since_source": since_source,
360
+ }
361
+
362
+
363
+ def _resolve_scope_to_md_files(project_root, scope_name):
364
+ """For scope form: list the (layer_file, md_path) tuples under the scope."""
365
+ scopes_dir = project_root / ".lore" / "scopes" / scope_name
366
+ if not scopes_dir.is_dir():
367
+ available = sorted(
368
+ p.name for p in (project_root / ".lore" / "scopes").iterdir()
369
+ if p.is_dir()
370
+ ) if (project_root / ".lore" / "scopes").is_dir() else []
371
+ available_display = ", ".join(available) if available else "(none)"
372
+ die(ERR_BAD_SCOPE, f"Scope '{scope_name}' not found. Available: {available_display}")
373
+ files = []
374
+ for md in sorted(scopes_dir.glob("*.md")):
375
+ files.append((md.stem, md))
376
+ return files
377
+
378
+
379
+ def _is_git_repo(project_root):
380
+ try:
381
+ proc = subprocess.run(
382
+ ["git", "-C", str(project_root), "rev-parse", "--git-dir"],
383
+ capture_output=True, text=True, check=False,
384
+ )
385
+ except FileNotFoundError:
386
+ die(ERR_NO_GIT, "git executable not found on PATH.")
387
+ return proc.returncode == 0
388
+
389
+
390
+ def _enrich_commits_with_body_and_refs(project_root, commits):
391
+ """For each commit, fetch body and extract refs. Mutates in place."""
392
+ for c in commits:
393
+ msg = fetch_commit_body(project_root, c["hash"])
394
+ if msg:
395
+ # Body is everything after the first line.
396
+ parts = msg.split("\n", 1)
397
+ subject = parts[0]
398
+ body = parts[1].strip() if len(parts) > 1 else ""
399
+ c["subject"] = subject
400
+ c["body"] = truncate_body(body, max_lines=3)
401
+ c["refs"] = extract_refs(msg)
402
+
403
+
404
+ def main():
405
+ args = sys.argv[1:]
406
+ json_mode = "--json" in args
407
+ since_override = None
408
+ for a in args:
409
+ if a.startswith("--since="):
410
+ since_override = a.split("=", 1)[1]
411
+
412
+ positional = [a for a in args if a != "--json" and not a.startswith("--since=")]
413
+ if not positional:
414
+ print("usage: lore history <entry-id|file-path|--scope=NAME>",
415
+ file=sys.stderr)
416
+ die(ERR_USAGE, "missing argument")
417
+
418
+ parsed = parse_arg(positional[0])
419
+ if parsed is None:
420
+ die(ERR_USAGE, f"unrecognized argument: {positional[0]}")
421
+
422
+ project_root = _find_lore_root_or_die()
423
+
424
+ if not _is_git_repo(project_root):
425
+ die(ERR_NOT_GIT,
426
+ "Not a git repository. 'lore history' requires git; "
427
+ "use 'lore query' for in-memory answers.")
428
+
429
+ if parsed["form"] == "entry":
430
+ entries = _load_entries_via_subprocess()
431
+ entry = find_entry(entries, parsed["value"])
432
+ if entry is None:
433
+ ids = ", ".join(e["id"] for e in entries[:20])
434
+ more = "" if len(entries) <= 20 else f" (and {len(entries)-20} more)"
435
+ die(ERR_NO_ENTRY,
436
+ f"Entry {parsed['value']} not found. Available: {ids}{more}")
437
+ since = since_override or extract_added_date(entry.get("tags", {}))
438
+ if since is None:
439
+ print("warning: entry has no #added tag; using full history",
440
+ file=sys.stderr)
441
+ since = "1970-01-01"
442
+ code_file = resolve_code_file(entry)
443
+ try:
444
+ commits = run_git_log(project_root, since, code_file)
445
+ except RuntimeError as exc:
446
+ die(ERR_GIT_FAIL, str(exc))
447
+ _enrich_commits_with_body_and_refs(project_root, commits)
448
+ meta = _build_meta_entry(entry, code_file, since, "entry_added")
449
+ out = render_json(meta, commits) if json_mode else render_markdown(meta, commits)
450
+ print(out)
451
+ return
452
+
453
+ if parsed["form"] == "file":
454
+ since = since_override or "1970-01-01"
455
+ code_file = parsed["value"]
456
+ try:
457
+ commits = run_git_log(project_root, since, code_file)
458
+ except RuntimeError as exc:
459
+ die(ERR_GIT_FAIL, str(exc))
460
+ _enrich_commits_with_body_and_refs(project_root, commits)
461
+ meta = {
462
+ "entry_id": f"<file:{code_file}>",
463
+ "lore_file": "(direct file query)",
464
+ "code_file": code_file,
465
+ "since": since,
466
+ "since_source": "user_arg" if since_override else "default",
467
+ }
468
+ out = render_json(meta, commits) if json_mode else render_markdown(meta, commits)
469
+ print(out)
470
+ return
471
+
472
+ if parsed["form"] == "scope":
473
+ layer_files = _resolve_scope_to_md_files(project_root, parsed["value"])
474
+ scope_payloads = [] # only used when json_mode is True
475
+ for layer_name, md_path in layer_files:
476
+ # For scope form we treat each .md file as a "code file" stand-in:
477
+ # we git log the md file's project-relative path to find commits
478
+ # that touched that lore file. (Useful for tracking lore edits.)
479
+ rel = str(md_path.relative_to(project_root))
480
+ try:
481
+ commits = run_git_log(project_root, "1970-01-01", rel)
482
+ except RuntimeError as exc:
483
+ die(ERR_GIT_FAIL, str(exc))
484
+ _enrich_commits_with_body_and_refs(project_root, commits)
485
+ if json_mode:
486
+ meta = {
487
+ "entry_id": f"<scope:{parsed['value']}/{layer_name}>",
488
+ "lore_file": rel,
489
+ "code_file": rel,
490
+ "since": "1970-01-01",
491
+ "since_source": "scope_form",
492
+ }
493
+ scope_payloads.append({
494
+ "layer": layer_name,
495
+ "payload": _json.loads(render_json(meta, commits)),
496
+ })
497
+ else:
498
+ print(f"## Scope: {parsed['value']} / {layer_name}")
499
+ print("")
500
+ if not commits:
501
+ print("(no commits)")
502
+ print("")
503
+ continue
504
+ for c in commits:
505
+ print(f"### {c['short']} ({c['date']}, {c['author']})")
506
+ print(c["subject"])
507
+ if c.get("body"):
508
+ print(f' Body: "{c["body"]}"')
509
+ if c.get("refs"):
510
+ print(f" Refs: {', '.join(c['refs'])}")
511
+ print("")
512
+ if json_mode:
513
+ print(_json.dumps(
514
+ {
515
+ "form": "scope",
516
+ "scope": parsed["value"],
517
+ "layers": [item["layer"] for item in scope_payloads],
518
+ "results": scope_payloads,
519
+ },
520
+ indent=2,
521
+ ensure_ascii=False,
522
+ ))
523
+ return
524
+
525
+
526
+ if __name__ == "__main__":
527
+ main()
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env python3
2
+ """Compute the 4-char content hash for a lore entry ID.
3
+
4
+ Usage:
5
+ python id_hash.py "Use Next.js App Router; reason: streaming + RSC"
6
+
7
+ Output:
8
+ The 4-char lowercase hex hash that goes into an entry's ID, e.g. `a3f2`.
9
+
10
+ The hash is `sha256(text).hexdigest()[:4]`. This is the same algorithm
11
+ described in `references/entry-format.md` (ID generation section), so
12
+ running this script always produces the ID component a lore agent
13
+ would assign.
14
+
15
+ Cross-platform: works on Windows / Linux / macOS with Python 3.6+.
16
+ """
17
+ import sys
18
+ import hashlib
19
+
20
+
21
+ def main():
22
+ if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help"):
23
+ print(__doc__, file=sys.stderr)
24
+ sys.exit(0)
25
+
26
+ text = sys.argv[1]
27
+ h = hashlib.sha256(text.encode("utf-8")).hexdigest()[:4]
28
+ print(h)
29
+
30
+
31
+ if __name__ == "__main__":
32
+ main()