devrites 1.19.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 (232) hide show
  1. package/.claude-plugin/marketplace.json +24 -0
  2. package/.claude-plugin/plugin.json +43 -0
  3. package/CHANGELOG.md +391 -0
  4. package/LICENSE +56 -0
  5. package/NOTICE.md +18 -0
  6. package/README.md +582 -0
  7. package/SECURITY.md +193 -0
  8. package/bin/devrites.mjs +100 -0
  9. package/docs/architecture.md +272 -0
  10. package/docs/cli-mcp.md +57 -0
  11. package/docs/command-map.md +143 -0
  12. package/docs/flow.md +360 -0
  13. package/docs/release.md +29 -0
  14. package/docs/skills.md +214 -0
  15. package/docs/usage.md +325 -0
  16. package/install.sh +359 -0
  17. package/mcp/devrites-mcp.mjs +103 -0
  18. package/pack/.claude/agents/devrites-code-reviewer.md +50 -0
  19. package/pack/.claude/agents/devrites-doubt-reviewer.md +55 -0
  20. package/pack/.claude/agents/devrites-frontend-reviewer.md +52 -0
  21. package/pack/.claude/agents/devrites-performance-reviewer.md +47 -0
  22. package/pack/.claude/agents/devrites-plan-reviewer.md +79 -0
  23. package/pack/.claude/agents/devrites-security-auditor.md +53 -0
  24. package/pack/.claude/agents/devrites-simplifier-reviewer.md +75 -0
  25. package/pack/.claude/agents/devrites-slice-wright.md +181 -0
  26. package/pack/.claude/agents/devrites-spec-reviewer.md +72 -0
  27. package/pack/.claude/agents/devrites-strategy-reviewer.md +62 -0
  28. package/pack/.claude/agents/devrites-test-analyst.md +47 -0
  29. package/pack/.claude/hooks/devrites-a1-guard.sh +81 -0
  30. package/pack/.claude/hooks/devrites-allow.sh +44 -0
  31. package/pack/.claude/hooks/devrites-cursor.sh +28 -0
  32. package/pack/.claude/hooks/devrites-orient.sh +53 -0
  33. package/pack/.claude/hooks/devrites-redwatch.sh +39 -0
  34. package/pack/.claude/hooks/devrites-refresh-indexes.sh +127 -0
  35. package/pack/.claude/hooks/devrites-reviewer-readonly.sh +28 -0
  36. package/pack/.claude/hooks/devrites-statusline.sh +18 -0
  37. package/pack/.claude/hooks/devrites-stop-gate.sh +45 -0
  38. package/pack/.claude/hooks/devrites-wright-scope.sh +35 -0
  39. package/pack/.claude/hooks/hooks.json +52 -0
  40. package/pack/.claude/rules/README.md +48 -0
  41. package/pack/.claude/rules/afk-hitl.md +245 -0
  42. package/pack/.claude/rules/agents.md +98 -0
  43. package/pack/.claude/rules/anti-patterns.md +48 -0
  44. package/pack/.claude/rules/code-review.md +38 -0
  45. package/pack/.claude/rules/coding-style.md +55 -0
  46. package/pack/.claude/rules/context-hygiene.md +97 -0
  47. package/pack/.claude/rules/core.md +119 -0
  48. package/pack/.claude/rules/development-workflow.md +40 -0
  49. package/pack/.claude/rules/documentation.md +27 -0
  50. package/pack/.claude/rules/error-handling.md +33 -0
  51. package/pack/.claude/rules/git-workflow.md +35 -0
  52. package/pack/.claude/rules/hooks.md +38 -0
  53. package/pack/.claude/rules/patterns.md +45 -0
  54. package/pack/.claude/rules/performance.md +27 -0
  55. package/pack/.claude/rules/prose-style.md +101 -0
  56. package/pack/.claude/rules/security.md +63 -0
  57. package/pack/.claude/rules/testing.md +88 -0
  58. package/pack/.claude/rules/tooling.md +72 -0
  59. package/pack/.claude/settings.json +53 -0
  60. package/pack/.claude/skills/devrites-api-interface/SKILL.md +45 -0
  61. package/pack/.claude/skills/devrites-audit/SKILL.md +73 -0
  62. package/pack/.claude/skills/devrites-browser-proof/SKILL.md +38 -0
  63. package/pack/.claude/skills/devrites-debug-recovery/SKILL.md +50 -0
  64. package/pack/.claude/skills/devrites-debug-recovery/reference/build-the-loop.md +47 -0
  65. package/pack/.claude/skills/devrites-debug-recovery/reference/cleanup-and-classify.md +17 -0
  66. package/pack/.claude/skills/devrites-debug-recovery/reference/hypotheses.md +17 -0
  67. package/pack/.claude/skills/devrites-debug-recovery/reference/instrumentation.md +21 -0
  68. package/pack/.claude/skills/devrites-debug-recovery/reference/regression-test.md +31 -0
  69. package/pack/.claude/skills/devrites-doubt/SKILL.md +75 -0
  70. package/pack/.claude/skills/devrites-frontend-craft/SKILL.md +96 -0
  71. package/pack/.claude/skills/devrites-frontend-craft/reference/craft.md +59 -0
  72. package/pack/.claude/skills/devrites-frontend-craft/reference/design-references.md +116 -0
  73. package/pack/.claude/skills/devrites-frontend-craft/reference/fullstack.md +45 -0
  74. package/pack/.claude/skills/devrites-frontend-craft/reference/quality-standards.md +215 -0
  75. package/pack/.claude/skills/devrites-frontend-craft/reference/reuse-first.md +59 -0
  76. package/pack/.claude/skills/devrites-frontend-craft/reference/shape.md +60 -0
  77. package/pack/.claude/skills/devrites-interview/SKILL.md +81 -0
  78. package/pack/.claude/skills/devrites-lib/SKILL.md +76 -0
  79. package/pack/.claude/skills/devrites-lib/scripts/analyze.sh +78 -0
  80. package/pack/.claude/skills/devrites-lib/scripts/check-acceptance.sh +75 -0
  81. package/pack/.claude/skills/devrites-lib/scripts/close-out.sh +47 -0
  82. package/pack/.claude/skills/devrites-lib/scripts/conventions.py +273 -0
  83. package/pack/.claude/skills/devrites-lib/scripts/coverage.sh +51 -0
  84. package/pack/.claude/skills/devrites-lib/scripts/devrites.sh +69 -0
  85. package/pack/.claude/skills/devrites-lib/scripts/doctor.sh +92 -0
  86. package/pack/.claude/skills/devrites-lib/scripts/evidence-fresh.sh +63 -0
  87. package/pack/.claude/skills/devrites-lib/scripts/footprint.sh +45 -0
  88. package/pack/.claude/skills/devrites-lib/scripts/learnings.sh +74 -0
  89. package/pack/.claude/skills/devrites-lib/scripts/mutation-gate.sh +52 -0
  90. package/pack/.claude/skills/devrites-lib/scripts/package-existence.sh +68 -0
  91. package/pack/.claude/skills/devrites-lib/scripts/preamble.sh +76 -0
  92. package/pack/.claude/skills/devrites-lib/scripts/progress.sh +103 -0
  93. package/pack/.claude/skills/devrites-lib/scripts/readiness.sh +62 -0
  94. package/pack/.claude/skills/devrites-lib/scripts/reconcile.sh +123 -0
  95. package/pack/.claude/skills/devrites-lib/scripts/resolve.sh +279 -0
  96. package/pack/.claude/skills/devrites-lib/scripts/stuck.sh +67 -0
  97. package/pack/.claude/skills/devrites-lib/scripts/test-integrity.sh +87 -0
  98. package/pack/.claude/skills/devrites-lib/scripts/tick-afk.sh +52 -0
  99. package/pack/.claude/skills/devrites-prose-craft/SKILL.md +105 -0
  100. package/pack/.claude/skills/devrites-prose-craft/reference/banned-phrases.md +95 -0
  101. package/pack/.claude/skills/devrites-prose-craft/reference/examples.md +88 -0
  102. package/pack/.claude/skills/devrites-prose-craft/reference/structures.md +134 -0
  103. package/pack/.claude/skills/devrites-refresh-indexes/SKILL.md +54 -0
  104. package/pack/.claude/skills/devrites-source-driven/SKILL.md +36 -0
  105. package/pack/.claude/skills/devrites-ux-shape/SKILL.md +121 -0
  106. package/pack/.claude/skills/devrites-ux-shape/reference/brief-template.md +93 -0
  107. package/pack/.claude/skills/devrites-ux-shape/reference/visual-direction-probe.md +48 -0
  108. package/pack/.claude/skills/rite/SKILL.md +135 -0
  109. package/pack/.claude/skills/rite/reference/menu.md +32 -0
  110. package/pack/.claude/skills/rite-adopt/SKILL.md +83 -0
  111. package/pack/.claude/skills/rite-adopt/reference/adoption.md +58 -0
  112. package/pack/.claude/skills/rite-adopt/reference/anti-patterns.md +19 -0
  113. package/pack/.claude/skills/rite-autocomplete/SKILL.md +96 -0
  114. package/pack/.claude/skills/rite-autocomplete/reference/decision-policy.md +35 -0
  115. package/pack/.claude/skills/rite-autocomplete/reference/loop.md +54 -0
  116. package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +59 -0
  117. package/pack/.claude/skills/rite-build/SKILL.md +261 -0
  118. package/pack/.claude/skills/rite-build/reference/afk-discipline.md +145 -0
  119. package/pack/.claude/skills/rite-build/reference/anti-patterns.md +25 -0
  120. package/pack/.claude/skills/rite-build/reference/checkpoint-protocol.md +149 -0
  121. package/pack/.claude/skills/rite-build/reference/evidence-standard.md +32 -0
  122. package/pack/.claude/skills/rite-build/reference/frontend-trigger.md +39 -0
  123. package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +38 -0
  124. package/pack/.claude/skills/rite-build/reference/spec-drift-guard.md +43 -0
  125. package/pack/.claude/skills/rite-build/reference/tdd.md +26 -0
  126. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +115 -0
  127. package/pack/.claude/skills/rite-define/SKILL.md +157 -0
  128. package/pack/.claude/skills/rite-define/reference/anti-patterns.md +25 -0
  129. package/pack/.claude/skills/rite-define/reference/gates.md +152 -0
  130. package/pack/.claude/skills/rite-define/reference/plan-template.md +65 -0
  131. package/pack/.claude/skills/rite-doctor/SKILL.md +50 -0
  132. package/pack/.claude/skills/rite-frame/SKILL.md +116 -0
  133. package/pack/.claude/skills/rite-frame/reference/failure-modes.md +68 -0
  134. package/pack/.claude/skills/rite-handoff/SKILL.md +95 -0
  135. package/pack/.claude/skills/rite-handoff/reference/handoff-template.md +34 -0
  136. package/pack/.claude/skills/rite-learn/SKILL.md +82 -0
  137. package/pack/.claude/skills/rite-plan/SKILL.md +82 -0
  138. package/pack/.claude/skills/rite-plan/reference/anti-patterns.md +24 -0
  139. package/pack/.claude/skills/rite-plan/reference/dependency-graph.md +33 -0
  140. package/pack/.claude/skills/rite-plan/reference/replan-and-repair.md +42 -0
  141. package/pack/.claude/skills/rite-plan/reference/slicing.md +52 -0
  142. package/pack/.claude/skills/rite-plan/reference/task-breakdown.md +34 -0
  143. package/pack/.claude/skills/rite-polish/SKILL.md +90 -0
  144. package/pack/.claude/skills/rite-polish/reference/anti-ai-slop.md +177 -0
  145. package/pack/.claude/skills/rite-polish/reference/anti-patterns.md +27 -0
  146. package/pack/.claude/skills/rite-polish/reference/backend-polish.md +80 -0
  147. package/pack/.claude/skills/rite-polish/reference/browser-polish-evidence.md +31 -0
  148. package/pack/.claude/skills/rite-polish/reference/code.md +85 -0
  149. package/pack/.claude/skills/rite-polish/reference/design-system-discovery.md +35 -0
  150. package/pack/.claude/skills/rite-polish/reference/harden-checklist.md +109 -0
  151. package/pack/.claude/skills/rite-polish/reference/ui.md +136 -0
  152. package/pack/.claude/skills/rite-pressure-test/SKILL.md +43 -0
  153. package/pack/.claude/skills/rite-prototype/SKILL.md +87 -0
  154. package/pack/.claude/skills/rite-prove/SKILL.md +120 -0
  155. package/pack/.claude/skills/rite-prove/reference/anti-patterns.md +25 -0
  156. package/pack/.claude/skills/rite-prove/reference/browser-proof.md +26 -0
  157. package/pack/.claude/skills/rite-prove/reference/failure-triage.md +25 -0
  158. package/pack/.claude/skills/rite-prove/reference/proof-ladder.md +26 -0
  159. package/pack/.claude/skills/rite-prove/reference/test-command-discovery.md +30 -0
  160. package/pack/.claude/skills/rite-quick/SKILL.md +81 -0
  161. package/pack/.claude/skills/rite-resolve/SKILL.md +113 -0
  162. package/pack/.claude/skills/rite-resolve/reference/answer-protocol.md +114 -0
  163. package/pack/.claude/skills/rite-review/SKILL.md +170 -0
  164. package/pack/.claude/skills/rite-review/reference/anti-patterns.md +32 -0
  165. package/pack/.claude/skills/rite-review/reference/cognitive-load.md +90 -0
  166. package/pack/.claude/skills/rite-review/reference/feature-scoped-review.md +26 -0
  167. package/pack/.claude/skills/rite-review/reference/five-axis-review.md +46 -0
  168. package/pack/.claude/skills/rite-review/reference/nielsen-heuristics.md +130 -0
  169. package/pack/.claude/skills/rite-review/reference/parallel-dispatch.md +62 -0
  170. package/pack/.claude/skills/rite-review/reference/performance-review.md +28 -0
  171. package/pack/.claude/skills/rite-review/reference/security-review.md +32 -0
  172. package/pack/.claude/skills/rite-seal/SKILL.md +183 -0
  173. package/pack/.claude/skills/rite-seal/reference/anti-patterns.md +27 -0
  174. package/pack/.claude/skills/rite-seal/reference/conventions-ledger.md +63 -0
  175. package/pack/.claude/skills/rite-seal/reference/final-evidence.md +72 -0
  176. package/pack/.claude/skills/rite-seal/reference/go-no-go.md +37 -0
  177. package/pack/.claude/skills/rite-seal/reference/parallel-dispatch.md +69 -0
  178. package/pack/.claude/skills/rite-seal/reference/risk-and-rollback.md +30 -0
  179. package/pack/.claude/skills/rite-seal/reference/seal-template.md +36 -0
  180. package/pack/.claude/skills/rite-ship/SKILL.md +120 -0
  181. package/pack/.claude/skills/rite-ship/reference/anti-patterns.md +25 -0
  182. package/pack/.claude/skills/rite-ship/reference/close-out.md +31 -0
  183. package/pack/.claude/skills/rite-ship/reference/design-memory.md +120 -0
  184. package/pack/.claude/skills/rite-ship/reference/git-ship.md +42 -0
  185. package/pack/.claude/skills/rite-ship/reference/ship-template.md +33 -0
  186. package/pack/.claude/skills/rite-spec/SKILL.md +126 -0
  187. package/pack/.claude/skills/rite-spec/reference/acceptance-criteria.md +31 -0
  188. package/pack/.claude/skills/rite-spec/reference/anti-patterns.md +25 -0
  189. package/pack/.claude/skills/rite-spec/reference/interview-patterns.md +56 -0
  190. package/pack/.claude/skills/rite-spec/reference/investigation.md +64 -0
  191. package/pack/.claude/skills/rite-spec/reference/question-protocol.md +61 -0
  192. package/pack/.claude/skills/rite-spec/reference/references-intake.md +57 -0
  193. package/pack/.claude/skills/rite-spec/reference/spec-checklists.md +73 -0
  194. package/pack/.claude/skills/rite-spec/reference/spec-template.md +124 -0
  195. package/pack/.claude/skills/rite-spec/reference/state-workspace.md +159 -0
  196. package/pack/.claude/skills/rite-status/SKILL.md +101 -0
  197. package/pack/.claude/skills/rite-temper/SKILL.md +119 -0
  198. package/pack/.claude/skills/rite-temper/reference/anti-patterns.md +29 -0
  199. package/pack/.claude/skills/rite-temper/reference/review-dimensions.md +65 -0
  200. package/pack/.claude/skills/rite-temper/reference/scope-modes.md +53 -0
  201. package/pack/.claude/skills/rite-temper/reference/significance.md +46 -0
  202. package/pack/.claude/skills/rite-temper/reference/strategy-template.md +90 -0
  203. package/pack/.claude/skills/rite-vet/SKILL.md +155 -0
  204. package/pack/.claude/skills/rite-vet/reference/anti-patterns.md +29 -0
  205. package/pack/.claude/skills/rite-vet/reference/artifacts.md +135 -0
  206. package/pack/.claude/skills/rite-vet/reference/cross-model.md +41 -0
  207. package/pack/.claude/skills/rite-vet/reference/depth.md +53 -0
  208. package/pack/.claude/skills/rite-vet/reference/eng-lenses.md +48 -0
  209. package/pack/.claude/skills/rite-vet/reference/review-axes.md +167 -0
  210. package/pack/.claude/skills/rite-zoom-out/SKILL.md +75 -0
  211. package/package.json +68 -0
  212. package/scripts/build-release-tarball.sh +74 -0
  213. package/scripts/check-cross-refs.py +121 -0
  214. package/scripts/check-no-global-writes.sh +44 -0
  215. package/scripts/check-rule-uniqueness.sh +73 -0
  216. package/scripts/devrites-detect.sh +175 -0
  217. package/scripts/eval-runner.py +273 -0
  218. package/scripts/grade-feature.sh +104 -0
  219. package/scripts/install-lib.sh +83 -0
  220. package/scripts/pin.sh +166 -0
  221. package/scripts/render-eval-summary.py +48 -0
  222. package/scripts/run-evals.sh +149 -0
  223. package/scripts/run-outcome-evals.sh +49 -0
  224. package/scripts/scan-pack-security.py +209 -0
  225. package/scripts/scan-supply-chain-iocs.py +127 -0
  226. package/scripts/supply-chain-iocs.json +11 -0
  227. package/scripts/sync-version.sh +56 -0
  228. package/scripts/validate-frontmatter.py +149 -0
  229. package/scripts/validate-workflow-security.py +86 -0
  230. package/scripts/validate.sh +234 -0
  231. package/uninstall.sh +137 -0
  232. package/update.sh +196 -0
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env python3
2
+ """Scan an npm lockfile against known supply-chain indicators of compromise.
3
+
4
+ Dependabot manages version *updates*; this gate catches a *known-malicious* installed
5
+ version (a compromised release that slipped in). It checks every name@version in the
6
+ lockfile against the bundled IOC dataset (scripts/supply-chain-iocs.json) — an offline,
7
+ always-available advisory source, so CI is deterministic and needs no network.
8
+
9
+ `--online` additionally queries the OSV API per package and is best-effort: if the
10
+ advisory source is unreachable it prints a notice and falls back to the bundled dataset
11
+ (it never fails the build on a network error — only on a real IOC match).
12
+
13
+ Usage: scan-supply-chain-iocs.py [LOCKFILE] [--iocs FILE] [--online]
14
+ LOCKFILE defaults to package-lock.json.
15
+ Exit: 0 clean; 1 on an IOC match; 2 on a usage/parse error.
16
+ """
17
+ import json
18
+ import os
19
+ import sys
20
+
21
+ HERE = os.path.dirname(os.path.abspath(__file__))
22
+ DEFAULT_IOCS = os.path.join(HERE, "supply-chain-iocs.json")
23
+
24
+
25
+ def load_iocs(path):
26
+ with open(path, "r", encoding="utf-8") as fh:
27
+ data = json.load(fh)
28
+ index = {}
29
+ for e in data.get("iocs", []):
30
+ index[e["name"]] = e
31
+ return index
32
+
33
+
34
+ def installed_packages(lockfile):
35
+ """Yield (name, version) for every package in an npm lockfile (v2/v3 or v1)."""
36
+ with open(lockfile, "r", encoding="utf-8") as fh:
37
+ data = json.load(fh)
38
+ pkgs = data.get("packages")
39
+ if isinstance(pkgs, dict): # lockfile v2/v3
40
+ for key, meta in pkgs.items():
41
+ if not key or "node_modules/" not in key:
42
+ continue
43
+ name = key.split("node_modules/")[-1]
44
+ ver = (meta or {}).get("version")
45
+ if name and ver:
46
+ yield name, ver
47
+ deps = data.get("dependencies")
48
+ if isinstance(deps, dict): # lockfile v1 (recursive)
49
+ stack = list(deps.items())
50
+ while stack:
51
+ name, meta = stack.pop()
52
+ meta = meta or {}
53
+ ver = meta.get("version")
54
+ if name and ver:
55
+ yield name, ver
56
+ nested = meta.get("dependencies")
57
+ if isinstance(nested, dict):
58
+ stack.extend(nested.items())
59
+
60
+
61
+ def query_osv(name, version):
62
+ """Best-effort OSV lookup. Returns a finding note or None; never raises."""
63
+ try:
64
+ import json as _json
65
+ import urllib.request
66
+ body = _json.dumps({"package": {"name": name, "ecosystem": "npm"},
67
+ "version": version}).encode()
68
+ req = urllib.request.Request("https://api.osv.dev/v1/query", data=body,
69
+ headers={"Content-Type": "application/json"})
70
+ with urllib.request.urlopen(req, timeout=8) as r:
71
+ vulns = _json.loads(r.read()).get("vulns", [])
72
+ ids = [v.get("id") for v in vulns if v.get("id")]
73
+ return ("OSV: " + ", ".join(ids)) if ids else None
74
+ except Exception:
75
+ return "__unreachable__"
76
+
77
+
78
+ def main(argv):
79
+ args = [a for a in argv[1:]]
80
+ online = "--online" in args
81
+ args = [a for a in args if a != "--online"]
82
+ iocs_path = DEFAULT_IOCS
83
+ if "--iocs" in args:
84
+ i = args.index("--iocs")
85
+ iocs_path = args[i + 1]
86
+ del args[i:i + 2]
87
+ lockfile = args[0] if args else "package-lock.json"
88
+
89
+ if not os.path.isfile(lockfile):
90
+ print("OK no lockfile at %s — nothing to scan" % lockfile)
91
+ return 0
92
+ try:
93
+ index = load_iocs(iocs_path)
94
+ pkgs = list(installed_packages(lockfile))
95
+ except (OSError, ValueError, KeyError) as e:
96
+ print("ERROR cannot scan (%s)" % e, file=sys.stderr)
97
+ return 2
98
+
99
+ findings = []
100
+ for name, ver in pkgs:
101
+ e = index.get(name)
102
+ if e and ver in e.get("versions", []):
103
+ findings.append("FINDING %s@%s — %s (%s)" % (name, ver, e["note"], e["source"]))
104
+
105
+ if online:
106
+ offline_notice = False
107
+ for name, ver in pkgs:
108
+ res = query_osv(name, ver)
109
+ if res == "__unreachable__":
110
+ offline_notice = True
111
+ break
112
+ if res:
113
+ findings.append("FINDING %s@%s — %s" % (name, ver, res))
114
+ if offline_notice:
115
+ print("note: OSV unreachable — fell back to the bundled IOC dataset only")
116
+
117
+ if findings:
118
+ for f in sorted(set(findings)):
119
+ print(f)
120
+ print("\n%d supply-chain finding(s) across %d packages." % (len(set(findings)), len(pkgs)))
121
+ return 1
122
+ print("OK supply-chain scan clean (%d packages, %d IOCs)" % (len(pkgs), len(index)))
123
+ return 0
124
+
125
+
126
+ if __name__ == "__main__":
127
+ sys.exit(main(sys.argv))
@@ -0,0 +1,11 @@
1
+ {
2
+ "_comment": "Bundled supply-chain indicators-of-compromise: documented malicious npm releases. This offline dataset is the always-available advisory source the scanner checks against, so CI is deterministic and needs no network. Extend it as new incidents are confirmed; keep each entry sourced.",
3
+ "iocs": [
4
+ {"name": "event-stream", "versions": ["3.3.6"], "note": "pulled in malicious flatmap-stream (bitcoin-wallet stealer)", "source": "GHSA-mh6f-8j2x-4483 (2018)"},
5
+ {"name": "flatmap-stream", "versions": ["0.1.1"], "note": "malicious payload targeting Copay bitcoin wallets", "source": "npm advisory (2018)"},
6
+ {"name": "ua-parser-js", "versions": ["0.7.29", "0.8.0", "1.0.0"], "note": "account takeover shipped a crypto-miner + password stealer", "source": "CVE-2021-41265 / GHSA-pjwm-rvh2-c87w (Oct 2021)"},
7
+ {"name": "coa", "versions": ["2.0.3", "2.0.4", "2.1.1", "2.1.3", "3.0.1", "3.1.3"], "note": "maintainer-account compromise shipped malware", "source": "GHSA-73qr-pfmq-6rp8 (Nov 2021)"},
8
+ {"name": "rc", "versions": ["1.2.9", "1.3.9", "2.3.9"], "note": "maintainer-account compromise shipped malware", "source": "GHSA-g2q5-5433-rhrf (Nov 2021)"},
9
+ {"name": "node-ipc", "versions": ["10.1.1", "10.1.2", "11.0.0"], "note": "protestware that wiped files based on geolocation", "source": "CVE-2022-23812 (Mar 2022)"}
10
+ ]
11
+ }
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bash
2
+ # Sync the semantic-release-determined version across every DevRites manifest
3
+ # so plugin.json, marketplace.json and package.json stay in lockstep.
4
+ #
5
+ # Usage: sync-version.sh <version>
6
+ set -euo pipefail
7
+
8
+ VERSION="${1:-}"
9
+ if [[ -z "$VERSION" ]]; then
10
+ echo "usage: sync-version.sh <version>" >&2
11
+ exit 1
12
+ fi
13
+
14
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
15
+ cd "$ROOT"
16
+
17
+ echo "Syncing version ${VERSION} across DevRites manifests"
18
+
19
+ node - "$VERSION" <<'NODE'
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const version = process.argv[2];
23
+
24
+ const updates = [
25
+ { file: 'package.json', set: (j) => { j.version = version; } },
26
+ { file: '.claude-plugin/plugin.json', set: (j) => { j.version = version; } },
27
+ { file: '.claude-plugin/marketplace.json', set: (j) => { j.plugins.forEach(p => { if (p.name === 'devrites') p.version = version; }); } },
28
+ ];
29
+
30
+ for (const u of updates) {
31
+ const p = path.resolve(u.file);
32
+ const data = JSON.parse(fs.readFileSync(p, 'utf8'));
33
+ u.set(data);
34
+ fs.writeFileSync(p, JSON.stringify(data, null, 2) + '\n');
35
+ console.log(` → ${u.file}`);
36
+ }
37
+
38
+ // Update README status line so the published version shows on the repo
39
+ // landing page. Matches the previous status block (single- or two-line form)
40
+ // and rewrites it to a one-liner pointing at the new tag.
41
+ const readmePath = path.resolve('README.md');
42
+ const readme = fs.readFileSync(readmePath, 'utf8');
43
+ const statusLine =
44
+ `**Status:** [\`v${version}\`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v${version}) — ` +
45
+ "see [`CHANGELOG.md`](CHANGELOG.md) for release notes.";
46
+ const re = /\*\*Status:\*\*[^\n]*(?:\n(?!\n)[^\n]*)*/;
47
+ if (re.test(readme)) {
48
+ const updated = readme.replace(re, statusLine);
49
+ if (updated !== readme) {
50
+ fs.writeFileSync(readmePath, updated);
51
+ console.log(' → README.md');
52
+ }
53
+ } else {
54
+ console.warn(' ! README.md status line not found — skipping');
55
+ }
56
+ NODE
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env python3
2
+ """Validate YAML frontmatter of DevRites SKILL.md and agent files.
3
+
4
+ Usage: validate-frontmatter.py FILE [FILE ...]
5
+ Exits non-zero if any file fails. Uses PyYAML if present, else a minimal parser
6
+ (frontmatter here is simple key: value, no nested structures needed).
7
+ """
8
+ import sys
9
+
10
+ KNOWN_SKILL_FIELDS = {
11
+ "name", "description", "argument-hint", "user-invocable",
12
+ "disable-model-invocation",
13
+ }
14
+ KNOWN_AGENT_FIELDS = {
15
+ "name", "description", "tools", "disallowedTools", "model", "permissionMode",
16
+ "mcpServers", "hooks", "maxTurns", "skills", "initialPrompt", "memory",
17
+ "effort", "background", "isolation", "color",
18
+ }
19
+
20
+
21
+ def extract_frontmatter(text):
22
+ lines = text.splitlines()
23
+ if not lines or lines[0].strip() != "---":
24
+ return None, "missing opening '---' frontmatter fence"
25
+ body = []
26
+ for i in range(1, len(lines)):
27
+ if lines[i].strip() == "---":
28
+ return "\n".join(body), None
29
+ body.append(lines[i])
30
+ return None, "missing closing '---' frontmatter fence"
31
+
32
+
33
+ def parse_simple(fm):
34
+ """Minimal top-level 'key: value' parser. Good enough for these files.
35
+
36
+ Handles YAML block scalars ('|', '>') by collecting indented continuation
37
+ lines so multi-line values are preserved (and can be detected/rejected).
38
+ """
39
+ data = {}
40
+ lines = fm.splitlines()
41
+ i = 0
42
+ while i < len(lines):
43
+ raw = lines[i]
44
+ if not raw.strip() or raw.lstrip().startswith("#"):
45
+ i += 1
46
+ continue
47
+ if raw[0] in (" ", "\t"):
48
+ i += 1
49
+ continue
50
+ if ":" not in raw:
51
+ i += 1
52
+ continue
53
+ key, val = raw.split(":", 1)
54
+ v = val.strip()
55
+ # Block scalar indicators: gather indented following lines.
56
+ if v and v[0] in ("|", ">"):
57
+ collected = []
58
+ j = i + 1
59
+ while j < len(lines) and (lines[j] == "" or lines[j].startswith((" ", "\t"))):
60
+ collected.append(lines[j].lstrip())
61
+ j += 1
62
+ data[key.strip()] = "\n".join(collected).rstrip()
63
+ i = j
64
+ continue
65
+ data[key.strip()] = v.strip('"').strip("'")
66
+ i += 1
67
+ return data
68
+
69
+
70
+ def load_fm(fm):
71
+ try:
72
+ import yaml # type: ignore
73
+ d = yaml.safe_load(fm)
74
+ if isinstance(d, dict):
75
+ return {str(k): d[k] for k in d}
76
+ except Exception:
77
+ pass
78
+ return parse_simple(fm)
79
+
80
+
81
+ def is_agent(path):
82
+ return "/agents/" in path.replace("\\", "/")
83
+
84
+
85
+ def main(argv):
86
+ files = argv[1:]
87
+ if not files:
88
+ print("usage: validate-frontmatter.py FILE [FILE ...]", file=sys.stderr)
89
+ return 2
90
+ errors = 0
91
+ for path in files:
92
+ try:
93
+ with open(path, "r", encoding="utf-8") as fh:
94
+ text = fh.read()
95
+ except OSError as e:
96
+ print("ERROR %s: cannot read (%s)" % (path, e))
97
+ errors += 1
98
+ continue
99
+ fm, err = extract_frontmatter(text)
100
+ if err:
101
+ print("ERROR %s: %s" % (path, err))
102
+ errors += 1
103
+ continue
104
+ data = load_fm(fm)
105
+ if not isinstance(data, dict) or not data:
106
+ print("ERROR %s: frontmatter did not parse to key/value fields" % path)
107
+ errors += 1
108
+ continue
109
+ if "description" not in data or not str(data.get("description", "")).strip():
110
+ print("ERROR %s: missing/empty 'description'" % path)
111
+ errors += 1
112
+ continue
113
+ known = KNOWN_AGENT_FIELDS if is_agent(path) else KNOWN_SKILL_FIELDS
114
+ unknown = [k for k in data if k not in known]
115
+ if unknown:
116
+ print("ERROR %s: unknown field(s) not in canonical SKILL.md spec: %s"
117
+ % (path, ", ".join(unknown)))
118
+ errors += 1
119
+ continue
120
+ # description length cap is 1024 chars per Anthropic SKILL.md spec
121
+ warn = ""
122
+ desc = str(data.get("description", ""))
123
+ dlen = len(desc)
124
+ if dlen > 1024:
125
+ print("ERROR %s: description %d chars > 1024 cap (Anthropic SKILL.md spec)"
126
+ % (path, dlen))
127
+ errors += 1
128
+ continue
129
+ if "\n" in desc:
130
+ print("ERROR %s: description must be a single line (no newlines)" % path)
131
+ errors += 1
132
+ continue
133
+ if "when_to_use" in data:
134
+ dlen += len(str(data.get("when_to_use", "")))
135
+ if dlen > 1536:
136
+ warn += " [warn: description+when_to_use %d>1536 chars]" % dlen
137
+ ui = data.get("user-invocable", "(default)")
138
+ ctx = data.get("context", "")
139
+ flag = (" context=%s" % ctx) if ctx else ""
140
+ print("OK %s (user-invocable=%s%s)%s" % (path, ui, flag, warn))
141
+ if errors:
142
+ print("\n%d file(s) failed frontmatter validation." % errors)
143
+ return 1
144
+ print("\nAll %d file(s) passed frontmatter validation." % len(files))
145
+ return 0
146
+
147
+
148
+ if __name__ == "__main__":
149
+ sys.exit(main(sys.argv))
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env python3
2
+ """Scan GitHub Actions workflows for supply-chain + permission risks.
3
+
4
+ DevRites' publish (release.yml) and auto-merge paths are high-value targets, so this
5
+ gate fails CI when a workflow:
6
+
7
+ - uses a THIRD-PARTY action not pinned to a full 40-char commit SHA — a moving tag
8
+ like `@v2` lets a compromised upstream inject code into the pipeline (GitHub-owned
9
+ `actions/*` and `github/*` tags are tolerated);
10
+ - declares no `permissions:` scope anywhere — the default token is broad;
11
+ - uses `permissions: write-all` (over-broad);
12
+ - uses `pull_request_target` (runs with secrets on untrusted PR code — review required).
13
+
14
+ Usage: validate-workflow-security.py [DIR] (default: .github/workflows)
15
+ Exit: 0 clean; 1 on any finding.
16
+ """
17
+ import os
18
+ import re
19
+ import sys
20
+
21
+ FIRST_PARTY = {"actions", "github"} # GitHub-owned; major-tag refs tolerated
22
+ SHA_RE = re.compile(r"^[0-9a-f]{40}$")
23
+ USES_RE = re.compile(r"^\s*-?\s*uses:\s*([^\s#]+)")
24
+
25
+
26
+ def scan_text(path, text):
27
+ findings = []
28
+ lines = text.splitlines()
29
+ if not re.search(r"^\s*permissions:", text, re.MULTILINE):
30
+ findings.append("%s: no permissions: scope — the default GITHUB_TOKEN is broad; "
31
+ "add an explicit least-privilege permissions block" % path)
32
+ for i, line in enumerate(lines, 1):
33
+ if "write-all" in line:
34
+ findings.append("%s:%d: permissions: write-all is over-broad — scope to the "
35
+ "minimum needed" % (path, i))
36
+ if "pull_request_target" in line:
37
+ findings.append("%s:%d: pull_request_target runs with secrets on untrusted PR "
38
+ "code — review carefully or use pull_request" % (path, i))
39
+ m = USES_RE.match(line)
40
+ if not m:
41
+ continue
42
+ ref = m.group(1)
43
+ if ref.startswith("./") or ref.startswith("."):
44
+ continue # local action
45
+ owner = ref.split("/", 1)[0]
46
+ if owner in FIRST_PARTY:
47
+ continue
48
+ at = ref.rsplit("@", 1)
49
+ pin = at[1] if len(at) == 2 else ""
50
+ if not SHA_RE.match(pin):
51
+ findings.append("%s:%d: third-party action '%s' not pinned to a full commit "
52
+ "SHA — pin it (a moving tag is a supply-chain risk)"
53
+ % (path, i, ref))
54
+ return findings
55
+
56
+
57
+ def iter_workflows(d):
58
+ if os.path.isfile(d):
59
+ yield d
60
+ return
61
+ for root, _dirs, names in os.walk(d):
62
+ for n in sorted(names):
63
+ if n.endswith((".yml", ".yaml")):
64
+ yield os.path.join(root, n)
65
+
66
+
67
+ def main(argv):
68
+ target = argv[1] if len(argv) > 1 else ".github/workflows"
69
+ if not os.path.exists(target):
70
+ print("OK no workflows at %s" % target)
71
+ return 0
72
+ findings = []
73
+ for path in iter_workflows(target):
74
+ with open(path, "r", encoding="utf-8") as fh:
75
+ findings.extend(scan_text(path, fh.read()))
76
+ if findings:
77
+ for f in findings:
78
+ print("FINDING " + f)
79
+ print("\n%d workflow-security finding(s)." % len(findings))
80
+ return 1
81
+ print("OK workflow security clean (%s)" % target)
82
+ return 0
83
+
84
+
85
+ if __name__ == "__main__":
86
+ sys.exit(main(sys.argv))
@@ -0,0 +1,234 @@
1
+ #!/usr/bin/env bash
2
+ # validate.sh — static validation of the DevRites pack (no install required).
3
+ # Run from anywhere. Exits non-zero if any hard check fails.
4
+
5
+ set -u
6
+ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
7
+ PACK="$ROOT/pack/.claude"
8
+ SKILLS="$PACK/skills"
9
+ AGENTS="$PACK/agents"
10
+ fail=0
11
+ section() { printf '\n=== %s ===\n' "$1"; }
12
+ bad() { printf 'FAIL: %s\n' "$*"; fail=1; }
13
+ good() { printf 'ok: %s\n' "$*"; }
14
+
15
+ PUBLIC="rite rite-spec rite-adopt rite-temper rite-define rite-vet rite-plan rite-build rite-prove rite-polish rite-review rite-seal rite-ship rite-status rite-doctor rite-resolve rite-pressure-test rite-zoom-out rite-prototype rite-handoff rite-autocomplete"
16
+ INTERNAL="devrites-interview devrites-source-driven devrites-doubt devrites-ux-shape devrites-frontend-craft devrites-browser-proof devrites-debug-recovery devrites-api-interface devrites-audit"
17
+ AGENT_FILES="devrites-spec-reviewer devrites-code-reviewer devrites-test-analyst devrites-frontend-reviewer devrites-security-auditor devrites-performance-reviewer devrites-doubt-reviewer devrites-simplifier-reviewer devrites-strategy-reviewer devrites-plan-reviewer devrites-slice-wright"
18
+
19
+ # ---- 1. bash -n on every shell script ------------------------------------
20
+ section "bash syntax (bash -n)"
21
+ SH_LIST="$ROOT/install.sh $ROOT/uninstall.sh"
22
+ for f in "$ROOT"/scripts/*.sh "$ROOT"/tests/*.sh "$ROOT"/pack/.claude/skills/*/scripts/*.sh; do [ -f "$f" ] && SH_LIST="$SH_LIST $f"; done
23
+ for f in $SH_LIST; do
24
+ if bash -n "$f" 2>/tmp/dr_synerr; then good "syntax ${f#$ROOT/}"; else bad "syntax ${f#$ROOT/}: $(cat /tmp/dr_synerr)"; fi
25
+ done
26
+
27
+ # ---- 2. python syntax ----------------------------------------------------
28
+ section "python syntax"
29
+ if command -v python3 >/dev/null 2>&1; then
30
+ for f in "$ROOT"/scripts/*.py; do
31
+ [ -f "$f" ] || continue
32
+ if python3 -c "import py_compile,sys; py_compile.compile('$f', doraise=True)" 2>/tmp/dr_pyerr; then
33
+ good "compiles ${f#$ROOT/}"; else bad "py ${f#$ROOT/}: $(cat /tmp/dr_pyerr)"; fi
34
+ done
35
+ else
36
+ echo "skip: python3 not found"
37
+ fi
38
+
39
+ # ---- 2b. MCP server syntax (node --check) --------------------------------
40
+ section "mcp server syntax (node --check)"
41
+ if command -v node >/dev/null 2>&1; then
42
+ found=0
43
+ for f in "$ROOT"/mcp/*.mjs; do
44
+ [ -f "$f" ] || continue; found=1
45
+ if node --check "$f" 2>/tmp/dr_mjs; then good "node --check ${f#$ROOT/}"; else bad "node --check ${f#$ROOT/}: $(cat /tmp/dr_mjs)"; fi
46
+ done
47
+ [ "$found" -eq 0 ] && echo "skip: no mcp/*.mjs"
48
+ else
49
+ echo "skip: node not found"
50
+ fi
51
+
52
+ # ---- 3. required skills exist, each with SKILL.md ------------------------
53
+ section "skills present + SKILL.md"
54
+ for s in $PUBLIC $INTERNAL; do
55
+ if [ -f "$SKILLS/$s/SKILL.md" ]; then good "$s"; else bad "missing skill or SKILL.md: $s"; fi
56
+ done
57
+ # any stray skill dir without SKILL.md?
58
+ for d in "$SKILLS"/*/; do
59
+ [ -f "${d}SKILL.md" ] || bad "skill dir without SKILL.md: ${d#$PACK/}"
60
+ done
61
+
62
+ # ---- 4. agents present ---------------------------------------------------
63
+ section "agents present"
64
+ for a in $AGENT_FILES; do
65
+ if [ -f "$AGENTS/$a.md" ]; then good "$a"; else bad "missing agent: $a"; fi
66
+ done
67
+
68
+ section "plugin manifest agents list matches pack/.claude/agents"
69
+ PLUGIN_JSON="$ROOT/.claude-plugin/plugin.json"
70
+ if [ -r "$PLUGIN_JSON" ] && command -v python3 >/dev/null 2>&1; then
71
+ manifest_list="$(python3 -c '
72
+ import json, sys
73
+ m = json.load(open(sys.argv[1]))
74
+ a = m.get("agents", [])
75
+ if isinstance(a, str):
76
+ a = [a]
77
+ for p in a:
78
+ print(p.lstrip("./"))
79
+ ' "$PLUGIN_JSON" | sort -u)"
80
+ disk_list="$(cd "$ROOT" && ls pack/.claude/agents/*.md 2>/dev/null | sort -u)"
81
+ if [ "$manifest_list" = "$disk_list" ]; then
82
+ good "plugin.json agents array matches pack/.claude/agents/*.md"
83
+ else
84
+ bad "plugin.json agents array out of sync with pack/.claude/agents/*.md"
85
+ diff <(printf '%s\n' "$manifest_list") <(printf '%s\n' "$disk_list") || true
86
+ fi
87
+ else
88
+ echo "skip: cannot validate manifest sync (missing python3 or plugin.json)"
89
+ fi
90
+
91
+ # ---- 5. frontmatter validation ------------------------------------------
92
+ section "frontmatter"
93
+ if command -v python3 >/dev/null 2>&1; then
94
+ FM_FILES=""
95
+ for d in "$SKILLS"/*/; do FM_FILES="$FM_FILES ${d}SKILL.md"; done
96
+ for a in "$AGENTS"/*.md; do FM_FILES="$FM_FILES $a"; done
97
+ if python3 "$ROOT/scripts/validate-frontmatter.py" $FM_FILES; then good "frontmatter parses"; else bad "frontmatter validation failed"; fi
98
+ else
99
+ echo "skip: python3 not found"
100
+ fi
101
+
102
+ # ---- 6. /rite-polish orchestrator references its phase reference files ---
103
+ section "rite-polish orchestrator → reference files"
104
+ for s in reference/code.md reference/ui.md; do
105
+ if grep -q "$s" "$SKILLS/rite-polish/SKILL.md" 2>/dev/null; then good "rite-polish references $s"; else bad "rite-polish does not reference $s"; fi
106
+ if [ -f "$SKILLS/rite-polish/$s" ]; then good "$s present"; else bad "$s missing"; fi
107
+ done
108
+
109
+ # ---- 7. broken reference links -------------------------------------------
110
+ section "reference links resolve"
111
+ if command -v python3 >/dev/null 2>&1; then
112
+ python3 - "$SKILLS" <<'PY' || fail=1
113
+ import os, re, sys
114
+ skills = sys.argv[1]
115
+ link = re.compile(r"\]\(([^)]+?\.md)\)")
116
+ broken = 0; checked = 0
117
+ for name in os.listdir(skills):
118
+ sd = os.path.join(skills, name)
119
+ sm = os.path.join(sd, "SKILL.md")
120
+ if not os.path.isfile(sm):
121
+ continue
122
+ text = open(sm, encoding="utf-8").read()
123
+ for m in link.finditer(text):
124
+ tgt = m.group(1)
125
+ if tgt.startswith("http"):
126
+ continue
127
+ checked += 1
128
+ cands = [
129
+ os.path.normpath(os.path.join(sd, tgt)), # skill-dir relative
130
+ os.path.normpath(os.path.join(skills, tgt)), # skills-root relative
131
+ ]
132
+ if not any(os.path.isfile(c) for c in cands):
133
+ print("FAIL: broken link in %s/SKILL.md -> %s" % (name, tgt)); broken += 1
134
+ print("checked %d links, %d broken" % (checked, broken))
135
+ sys.exit(1 if broken else 0)
136
+ PY
137
+ [ "$fail" -eq 0 ] && good "all reference links resolve" || true
138
+ else
139
+ echo "skip: python3 not found"
140
+ fi
141
+
142
+ # ---- 8. size advisory (not a hard failure) -------------------------------
143
+ section "SKILL.md size advisory"
144
+ for d in "$SKILLS"/*/; do
145
+ n=$(wc -l < "${d}SKILL.md" | tr -d ' ')
146
+ [ "$n" -gt 200 ] && printf 'warn: %s is %s lines (recommended <=180 public / <=160 internal)\n' "$(basename "$d")" "$n"
147
+ done
148
+ echo "ok: size advisory complete"
149
+
150
+ # ---- 9. DevRites engineering rules present -------------------------------
151
+ section "DevRites rules present"
152
+ if [ -f "$ROOT/pack/.claude/rules/README.md" ] && [ -f "$ROOT/pack/.claude/rules/security.md" ]; then
153
+ good "pack/.claude/rules present ($(find "$ROOT/pack/.claude/rules" -name '*.md' | wc -l | tr -d ' ') rule files)"
154
+ else
155
+ bad "DevRites rules missing (need pack/.claude/rules/*.md)"
156
+ fi
157
+
158
+ # ---- 10. no global writes ------------------------------------------------
159
+ section "no global ~/.claude writes"
160
+ if bash "$ROOT/scripts/check-no-global-writes.sh" >/tmp/dr_glob 2>&1; then good "no-global-writes check passed"; else bad "no-global-writes check failed"; cat /tmp/dr_glob; fi
161
+
162
+ # ---- 11. principle uniqueness — each canonical heading appears exactly once
163
+ section "principle uniqueness"
164
+ if bash "$ROOT/scripts/check-rule-uniqueness.sh" >/tmp/dr_uniq 2>&1; then
165
+ cat /tmp/dr_uniq
166
+ good "rule-uniqueness check passed"
167
+ else
168
+ cat /tmp/dr_uniq
169
+ bad "rule-uniqueness check failed (see scripts/check-rule-uniqueness.sh)"
170
+ fi
171
+
172
+ # ---- 12. version lockstep across manifests -------------------------------
173
+ section "version lockstep (package.json == plugin.json == marketplace.json)"
174
+ if command -v python3 >/dev/null 2>&1; then
175
+ if python3 - "$ROOT" <<'PY'
176
+ import json, sys, os
177
+ root = sys.argv[1]
178
+ pkg = json.load(open(os.path.join(root, "package.json")))["version"]
179
+ plug = json.load(open(os.path.join(root, ".claude-plugin/plugin.json")))["version"]
180
+ mkt = json.load(open(os.path.join(root, ".claude-plugin/marketplace.json")))
181
+ mver = next((p.get("version") for p in mkt.get("plugins", []) if p.get("name") == "devrites"), None)
182
+ print(" package.json %s" % pkg)
183
+ print(" plugin.json %s" % plug)
184
+ print(" marketplace.json %s" % mver)
185
+ if pkg == plug == mver and mver is not None:
186
+ sys.exit(0)
187
+ print("FAIL: version mismatch across manifests")
188
+ sys.exit(1)
189
+ PY
190
+ then good "versions match across package.json / plugin.json / marketplace.json"; else bad "version mismatch across manifests (run scripts/sync-version.sh)"; fi
191
+ else
192
+ echo "skip: python3 not found"
193
+ fi
194
+
195
+ # ---- 13. no runtime-broken pack/.claude/ path in installed prose ---------
196
+ # After install the leading pack/ is stripped, so any literal pack/.claude/rules/
197
+ # or pack/.claude/skills/ in shipped SKILL.md / reference prose is a dead path
198
+ # at runtime. (Repo README/docs links are out of scope — they're GitHub links.)
199
+ section "no literal pack/.claude/ paths in shipped skill prose"
200
+ # Exclude the intentional resolution-snippet fallback (`... || P=pack/.claude/...`): the
201
+ # preamble snippet tries the installed `.claude/` path first, then `${CLAUDE_SKILL_DIR}`
202
+ # (plugin, best-effort), then the repo `pack/.claude/...` for DevRites self-development.
203
+ PACKPATH_HITS="$(grep -rnI -e 'pack/\.claude/rules/' -e 'pack/\.claude/skills/' "$SKILLS" 2>/dev/null | grep -vE '\|\| [A-Z]+=pack/\.claude/skills/' || true)"
204
+ if [ -n "$PACKPATH_HITS" ]; then
205
+ bad "literal pack/.claude/ path in shipped skill prose (strips to .claude/ on install):"
206
+ printf '%s\n' "$PACKPATH_HITS" | sed "s|$ROOT/||"
207
+ else
208
+ good "no literal pack/.claude/rules/ or pack/.claude/skills/ in shipped skill prose"
209
+ fi
210
+
211
+ # ---- 14. no false session-start autoload claim ---------------------------
212
+ # DevRites ships no autoload wiring; skills Read .claude/rules/core.md at step 0.
213
+ # Fail if any shipped skill or doc asserts native/session-start autoload.
214
+ section "no false session-start autoload claim"
215
+ AUTOLOAD_HITS="$(grep -rl 'autoloaded by Claude Code' "$ROOT/pack" "$ROOT/docs" "$ROOT/README.md" 2>/dev/null || true)"
216
+ if [ -n "$AUTOLOAD_HITS" ]; then
217
+ bad "false 'autoloaded by Claude Code' claim — the pack ships no autoload wiring:"
218
+ printf '%s\n' "$AUTOLOAD_HITS" | sed "s|$ROOT/||"
219
+ else
220
+ good "no false session-start autoload claim in pack/ docs/ README.md"
221
+ fi
222
+
223
+ # ---- 15. shellcheck (advisory) -------------------------------------------
224
+ section "shellcheck (advisory)"
225
+ if command -v shellcheck >/dev/null 2>&1; then
226
+ for f in $SH_LIST; do shellcheck -S warning "$f" || echo " (shellcheck advisory only — not failing the build)"; done
227
+ else
228
+ echo "skip: shellcheck not installed (optional)"
229
+ fi
230
+
231
+ # ---- summary -------------------------------------------------------------
232
+ printf '\n========================================\n'
233
+ if [ "$fail" -eq 0 ]; then printf 'VALIDATION PASSED\n'; else printf 'VALIDATION FAILED\n'; fi
234
+ exit "$fail"