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.
- package/.claude-plugin/marketplace.json +24 -0
- package/.claude-plugin/plugin.json +43 -0
- package/CHANGELOG.md +391 -0
- package/LICENSE +56 -0
- package/NOTICE.md +18 -0
- package/README.md +582 -0
- package/SECURITY.md +193 -0
- package/bin/devrites.mjs +100 -0
- package/docs/architecture.md +272 -0
- package/docs/cli-mcp.md +57 -0
- package/docs/command-map.md +143 -0
- package/docs/flow.md +360 -0
- package/docs/release.md +29 -0
- package/docs/skills.md +214 -0
- package/docs/usage.md +325 -0
- package/install.sh +359 -0
- package/mcp/devrites-mcp.mjs +103 -0
- package/pack/.claude/agents/devrites-code-reviewer.md +50 -0
- package/pack/.claude/agents/devrites-doubt-reviewer.md +55 -0
- package/pack/.claude/agents/devrites-frontend-reviewer.md +52 -0
- package/pack/.claude/agents/devrites-performance-reviewer.md +47 -0
- package/pack/.claude/agents/devrites-plan-reviewer.md +79 -0
- package/pack/.claude/agents/devrites-security-auditor.md +53 -0
- package/pack/.claude/agents/devrites-simplifier-reviewer.md +75 -0
- package/pack/.claude/agents/devrites-slice-wright.md +181 -0
- package/pack/.claude/agents/devrites-spec-reviewer.md +72 -0
- package/pack/.claude/agents/devrites-strategy-reviewer.md +62 -0
- package/pack/.claude/agents/devrites-test-analyst.md +47 -0
- package/pack/.claude/hooks/devrites-a1-guard.sh +81 -0
- package/pack/.claude/hooks/devrites-allow.sh +44 -0
- package/pack/.claude/hooks/devrites-cursor.sh +28 -0
- package/pack/.claude/hooks/devrites-orient.sh +53 -0
- package/pack/.claude/hooks/devrites-redwatch.sh +39 -0
- package/pack/.claude/hooks/devrites-refresh-indexes.sh +127 -0
- package/pack/.claude/hooks/devrites-reviewer-readonly.sh +28 -0
- package/pack/.claude/hooks/devrites-statusline.sh +18 -0
- package/pack/.claude/hooks/devrites-stop-gate.sh +45 -0
- package/pack/.claude/hooks/devrites-wright-scope.sh +35 -0
- package/pack/.claude/hooks/hooks.json +52 -0
- package/pack/.claude/rules/README.md +48 -0
- package/pack/.claude/rules/afk-hitl.md +245 -0
- package/pack/.claude/rules/agents.md +98 -0
- package/pack/.claude/rules/anti-patterns.md +48 -0
- package/pack/.claude/rules/code-review.md +38 -0
- package/pack/.claude/rules/coding-style.md +55 -0
- package/pack/.claude/rules/context-hygiene.md +97 -0
- package/pack/.claude/rules/core.md +119 -0
- package/pack/.claude/rules/development-workflow.md +40 -0
- package/pack/.claude/rules/documentation.md +27 -0
- package/pack/.claude/rules/error-handling.md +33 -0
- package/pack/.claude/rules/git-workflow.md +35 -0
- package/pack/.claude/rules/hooks.md +38 -0
- package/pack/.claude/rules/patterns.md +45 -0
- package/pack/.claude/rules/performance.md +27 -0
- package/pack/.claude/rules/prose-style.md +101 -0
- package/pack/.claude/rules/security.md +63 -0
- package/pack/.claude/rules/testing.md +88 -0
- package/pack/.claude/rules/tooling.md +72 -0
- package/pack/.claude/settings.json +53 -0
- package/pack/.claude/skills/devrites-api-interface/SKILL.md +45 -0
- package/pack/.claude/skills/devrites-audit/SKILL.md +73 -0
- package/pack/.claude/skills/devrites-browser-proof/SKILL.md +38 -0
- package/pack/.claude/skills/devrites-debug-recovery/SKILL.md +50 -0
- package/pack/.claude/skills/devrites-debug-recovery/reference/build-the-loop.md +47 -0
- package/pack/.claude/skills/devrites-debug-recovery/reference/cleanup-and-classify.md +17 -0
- package/pack/.claude/skills/devrites-debug-recovery/reference/hypotheses.md +17 -0
- package/pack/.claude/skills/devrites-debug-recovery/reference/instrumentation.md +21 -0
- package/pack/.claude/skills/devrites-debug-recovery/reference/regression-test.md +31 -0
- package/pack/.claude/skills/devrites-doubt/SKILL.md +75 -0
- package/pack/.claude/skills/devrites-frontend-craft/SKILL.md +96 -0
- package/pack/.claude/skills/devrites-frontend-craft/reference/craft.md +59 -0
- package/pack/.claude/skills/devrites-frontend-craft/reference/design-references.md +116 -0
- package/pack/.claude/skills/devrites-frontend-craft/reference/fullstack.md +45 -0
- package/pack/.claude/skills/devrites-frontend-craft/reference/quality-standards.md +215 -0
- package/pack/.claude/skills/devrites-frontend-craft/reference/reuse-first.md +59 -0
- package/pack/.claude/skills/devrites-frontend-craft/reference/shape.md +60 -0
- package/pack/.claude/skills/devrites-interview/SKILL.md +81 -0
- package/pack/.claude/skills/devrites-lib/SKILL.md +76 -0
- package/pack/.claude/skills/devrites-lib/scripts/analyze.sh +78 -0
- package/pack/.claude/skills/devrites-lib/scripts/check-acceptance.sh +75 -0
- package/pack/.claude/skills/devrites-lib/scripts/close-out.sh +47 -0
- package/pack/.claude/skills/devrites-lib/scripts/conventions.py +273 -0
- package/pack/.claude/skills/devrites-lib/scripts/coverage.sh +51 -0
- package/pack/.claude/skills/devrites-lib/scripts/devrites.sh +69 -0
- package/pack/.claude/skills/devrites-lib/scripts/doctor.sh +92 -0
- package/pack/.claude/skills/devrites-lib/scripts/evidence-fresh.sh +63 -0
- package/pack/.claude/skills/devrites-lib/scripts/footprint.sh +45 -0
- package/pack/.claude/skills/devrites-lib/scripts/learnings.sh +74 -0
- package/pack/.claude/skills/devrites-lib/scripts/mutation-gate.sh +52 -0
- package/pack/.claude/skills/devrites-lib/scripts/package-existence.sh +68 -0
- package/pack/.claude/skills/devrites-lib/scripts/preamble.sh +76 -0
- package/pack/.claude/skills/devrites-lib/scripts/progress.sh +103 -0
- package/pack/.claude/skills/devrites-lib/scripts/readiness.sh +62 -0
- package/pack/.claude/skills/devrites-lib/scripts/reconcile.sh +123 -0
- package/pack/.claude/skills/devrites-lib/scripts/resolve.sh +279 -0
- package/pack/.claude/skills/devrites-lib/scripts/stuck.sh +67 -0
- package/pack/.claude/skills/devrites-lib/scripts/test-integrity.sh +87 -0
- package/pack/.claude/skills/devrites-lib/scripts/tick-afk.sh +52 -0
- package/pack/.claude/skills/devrites-prose-craft/SKILL.md +105 -0
- package/pack/.claude/skills/devrites-prose-craft/reference/banned-phrases.md +95 -0
- package/pack/.claude/skills/devrites-prose-craft/reference/examples.md +88 -0
- package/pack/.claude/skills/devrites-prose-craft/reference/structures.md +134 -0
- package/pack/.claude/skills/devrites-refresh-indexes/SKILL.md +54 -0
- package/pack/.claude/skills/devrites-source-driven/SKILL.md +36 -0
- package/pack/.claude/skills/devrites-ux-shape/SKILL.md +121 -0
- package/pack/.claude/skills/devrites-ux-shape/reference/brief-template.md +93 -0
- package/pack/.claude/skills/devrites-ux-shape/reference/visual-direction-probe.md +48 -0
- package/pack/.claude/skills/rite/SKILL.md +135 -0
- package/pack/.claude/skills/rite/reference/menu.md +32 -0
- package/pack/.claude/skills/rite-adopt/SKILL.md +83 -0
- package/pack/.claude/skills/rite-adopt/reference/adoption.md +58 -0
- package/pack/.claude/skills/rite-adopt/reference/anti-patterns.md +19 -0
- package/pack/.claude/skills/rite-autocomplete/SKILL.md +96 -0
- package/pack/.claude/skills/rite-autocomplete/reference/decision-policy.md +35 -0
- package/pack/.claude/skills/rite-autocomplete/reference/loop.md +54 -0
- package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +59 -0
- package/pack/.claude/skills/rite-build/SKILL.md +261 -0
- package/pack/.claude/skills/rite-build/reference/afk-discipline.md +145 -0
- package/pack/.claude/skills/rite-build/reference/anti-patterns.md +25 -0
- package/pack/.claude/skills/rite-build/reference/checkpoint-protocol.md +149 -0
- package/pack/.claude/skills/rite-build/reference/evidence-standard.md +32 -0
- package/pack/.claude/skills/rite-build/reference/frontend-trigger.md +39 -0
- package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +38 -0
- package/pack/.claude/skills/rite-build/reference/spec-drift-guard.md +43 -0
- package/pack/.claude/skills/rite-build/reference/tdd.md +26 -0
- package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +115 -0
- package/pack/.claude/skills/rite-define/SKILL.md +157 -0
- package/pack/.claude/skills/rite-define/reference/anti-patterns.md +25 -0
- package/pack/.claude/skills/rite-define/reference/gates.md +152 -0
- package/pack/.claude/skills/rite-define/reference/plan-template.md +65 -0
- package/pack/.claude/skills/rite-doctor/SKILL.md +50 -0
- package/pack/.claude/skills/rite-frame/SKILL.md +116 -0
- package/pack/.claude/skills/rite-frame/reference/failure-modes.md +68 -0
- package/pack/.claude/skills/rite-handoff/SKILL.md +95 -0
- package/pack/.claude/skills/rite-handoff/reference/handoff-template.md +34 -0
- package/pack/.claude/skills/rite-learn/SKILL.md +82 -0
- package/pack/.claude/skills/rite-plan/SKILL.md +82 -0
- package/pack/.claude/skills/rite-plan/reference/anti-patterns.md +24 -0
- package/pack/.claude/skills/rite-plan/reference/dependency-graph.md +33 -0
- package/pack/.claude/skills/rite-plan/reference/replan-and-repair.md +42 -0
- package/pack/.claude/skills/rite-plan/reference/slicing.md +52 -0
- package/pack/.claude/skills/rite-plan/reference/task-breakdown.md +34 -0
- package/pack/.claude/skills/rite-polish/SKILL.md +90 -0
- package/pack/.claude/skills/rite-polish/reference/anti-ai-slop.md +177 -0
- package/pack/.claude/skills/rite-polish/reference/anti-patterns.md +27 -0
- package/pack/.claude/skills/rite-polish/reference/backend-polish.md +80 -0
- package/pack/.claude/skills/rite-polish/reference/browser-polish-evidence.md +31 -0
- package/pack/.claude/skills/rite-polish/reference/code.md +85 -0
- package/pack/.claude/skills/rite-polish/reference/design-system-discovery.md +35 -0
- package/pack/.claude/skills/rite-polish/reference/harden-checklist.md +109 -0
- package/pack/.claude/skills/rite-polish/reference/ui.md +136 -0
- package/pack/.claude/skills/rite-pressure-test/SKILL.md +43 -0
- package/pack/.claude/skills/rite-prototype/SKILL.md +87 -0
- package/pack/.claude/skills/rite-prove/SKILL.md +120 -0
- package/pack/.claude/skills/rite-prove/reference/anti-patterns.md +25 -0
- package/pack/.claude/skills/rite-prove/reference/browser-proof.md +26 -0
- package/pack/.claude/skills/rite-prove/reference/failure-triage.md +25 -0
- package/pack/.claude/skills/rite-prove/reference/proof-ladder.md +26 -0
- package/pack/.claude/skills/rite-prove/reference/test-command-discovery.md +30 -0
- package/pack/.claude/skills/rite-quick/SKILL.md +81 -0
- package/pack/.claude/skills/rite-resolve/SKILL.md +113 -0
- package/pack/.claude/skills/rite-resolve/reference/answer-protocol.md +114 -0
- package/pack/.claude/skills/rite-review/SKILL.md +170 -0
- package/pack/.claude/skills/rite-review/reference/anti-patterns.md +32 -0
- package/pack/.claude/skills/rite-review/reference/cognitive-load.md +90 -0
- package/pack/.claude/skills/rite-review/reference/feature-scoped-review.md +26 -0
- package/pack/.claude/skills/rite-review/reference/five-axis-review.md +46 -0
- package/pack/.claude/skills/rite-review/reference/nielsen-heuristics.md +130 -0
- package/pack/.claude/skills/rite-review/reference/parallel-dispatch.md +62 -0
- package/pack/.claude/skills/rite-review/reference/performance-review.md +28 -0
- package/pack/.claude/skills/rite-review/reference/security-review.md +32 -0
- package/pack/.claude/skills/rite-seal/SKILL.md +183 -0
- package/pack/.claude/skills/rite-seal/reference/anti-patterns.md +27 -0
- package/pack/.claude/skills/rite-seal/reference/conventions-ledger.md +63 -0
- package/pack/.claude/skills/rite-seal/reference/final-evidence.md +72 -0
- package/pack/.claude/skills/rite-seal/reference/go-no-go.md +37 -0
- package/pack/.claude/skills/rite-seal/reference/parallel-dispatch.md +69 -0
- package/pack/.claude/skills/rite-seal/reference/risk-and-rollback.md +30 -0
- package/pack/.claude/skills/rite-seal/reference/seal-template.md +36 -0
- package/pack/.claude/skills/rite-ship/SKILL.md +120 -0
- package/pack/.claude/skills/rite-ship/reference/anti-patterns.md +25 -0
- package/pack/.claude/skills/rite-ship/reference/close-out.md +31 -0
- package/pack/.claude/skills/rite-ship/reference/design-memory.md +120 -0
- package/pack/.claude/skills/rite-ship/reference/git-ship.md +42 -0
- package/pack/.claude/skills/rite-ship/reference/ship-template.md +33 -0
- package/pack/.claude/skills/rite-spec/SKILL.md +126 -0
- package/pack/.claude/skills/rite-spec/reference/acceptance-criteria.md +31 -0
- package/pack/.claude/skills/rite-spec/reference/anti-patterns.md +25 -0
- package/pack/.claude/skills/rite-spec/reference/interview-patterns.md +56 -0
- package/pack/.claude/skills/rite-spec/reference/investigation.md +64 -0
- package/pack/.claude/skills/rite-spec/reference/question-protocol.md +61 -0
- package/pack/.claude/skills/rite-spec/reference/references-intake.md +57 -0
- package/pack/.claude/skills/rite-spec/reference/spec-checklists.md +73 -0
- package/pack/.claude/skills/rite-spec/reference/spec-template.md +124 -0
- package/pack/.claude/skills/rite-spec/reference/state-workspace.md +159 -0
- package/pack/.claude/skills/rite-status/SKILL.md +101 -0
- package/pack/.claude/skills/rite-temper/SKILL.md +119 -0
- package/pack/.claude/skills/rite-temper/reference/anti-patterns.md +29 -0
- package/pack/.claude/skills/rite-temper/reference/review-dimensions.md +65 -0
- package/pack/.claude/skills/rite-temper/reference/scope-modes.md +53 -0
- package/pack/.claude/skills/rite-temper/reference/significance.md +46 -0
- package/pack/.claude/skills/rite-temper/reference/strategy-template.md +90 -0
- package/pack/.claude/skills/rite-vet/SKILL.md +155 -0
- package/pack/.claude/skills/rite-vet/reference/anti-patterns.md +29 -0
- package/pack/.claude/skills/rite-vet/reference/artifacts.md +135 -0
- package/pack/.claude/skills/rite-vet/reference/cross-model.md +41 -0
- package/pack/.claude/skills/rite-vet/reference/depth.md +53 -0
- package/pack/.claude/skills/rite-vet/reference/eng-lenses.md +48 -0
- package/pack/.claude/skills/rite-vet/reference/review-axes.md +167 -0
- package/pack/.claude/skills/rite-zoom-out/SKILL.md +75 -0
- package/package.json +68 -0
- package/scripts/build-release-tarball.sh +74 -0
- package/scripts/check-cross-refs.py +121 -0
- package/scripts/check-no-global-writes.sh +44 -0
- package/scripts/check-rule-uniqueness.sh +73 -0
- package/scripts/devrites-detect.sh +175 -0
- package/scripts/eval-runner.py +273 -0
- package/scripts/grade-feature.sh +104 -0
- package/scripts/install-lib.sh +83 -0
- package/scripts/pin.sh +166 -0
- package/scripts/render-eval-summary.py +48 -0
- package/scripts/run-evals.sh +149 -0
- package/scripts/run-outcome-evals.sh +49 -0
- package/scripts/scan-pack-security.py +209 -0
- package/scripts/scan-supply-chain-iocs.py +127 -0
- package/scripts/supply-chain-iocs.json +11 -0
- package/scripts/sync-version.sh +56 -0
- package/scripts/validate-frontmatter.py +149 -0
- package/scripts/validate-workflow-security.py +86 -0
- package/scripts/validate.sh +234 -0
- package/uninstall.sh +137 -0
- 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"
|