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
package/scripts/pin.sh
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/pin.sh — manage user-pinned slash aliases for DevRites.
|
|
3
|
+
#
|
|
4
|
+
# Adopts the same "thin wrapper SKILL.md that delegates" pattern as install.sh
|
|
5
|
+
# uses for --short-aliases=all, but exposes it as a runtime verb so users can
|
|
6
|
+
# add / remove arbitrary aliases against any rite-* skill without re-running
|
|
7
|
+
# the installer.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# scripts/pin.sh add <alias> <target> # /alias → /target (target = rite-spec | rite-build | ...)
|
|
11
|
+
# scripts/pin.sh remove <alias> # remove a previously-pinned alias
|
|
12
|
+
# scripts/pin.sh list # print currently-pinned aliases
|
|
13
|
+
# scripts/pin.sh --help
|
|
14
|
+
#
|
|
15
|
+
# Examples:
|
|
16
|
+
# ./scripts/pin.sh add b rite-build # /b == /rite-build
|
|
17
|
+
# ./scripts/pin.sh add ship rite-seal # /ship == /rite-seal
|
|
18
|
+
# ./scripts/pin.sh remove b
|
|
19
|
+
#
|
|
20
|
+
# Targets:
|
|
21
|
+
# - Default: $PWD (the installed project, which holds .claude/skills/).
|
|
22
|
+
# - --target <dir> to operate on a project elsewhere.
|
|
23
|
+
#
|
|
24
|
+
# Safety:
|
|
25
|
+
# - Refuses to write inside ~/.claude (DevRites is project-local only).
|
|
26
|
+
# - Refuses to overwrite a non-alias skill at .claude/skills/<alias>/.
|
|
27
|
+
# - Refuses if <target> is not a known rite-* skill in the target's pack.
|
|
28
|
+
# - Manifest-managed: pinned wrappers are recorded in .claude/devrites.manifest
|
|
29
|
+
# so the standard uninstall.sh cleans them up automatically.
|
|
30
|
+
|
|
31
|
+
set -u
|
|
32
|
+
|
|
33
|
+
# ---- locate install-lib + load helpers ----------------------------------
|
|
34
|
+
SELF_DIR="$( cd "$(dirname "$0")" && pwd -P )"
|
|
35
|
+
ROOT="$( cd "$SELF_DIR/.." && pwd -P )"
|
|
36
|
+
LIB="$SELF_DIR/install-lib.sh"
|
|
37
|
+
if [ ! -r "$LIB" ]; then
|
|
38
|
+
printf 'error: cannot find %s\n' "$LIB" >&2
|
|
39
|
+
exit 2
|
|
40
|
+
fi
|
|
41
|
+
# shellcheck source=scripts/install-lib.sh
|
|
42
|
+
. "$LIB"
|
|
43
|
+
|
|
44
|
+
# ---- parse args ----------------------------------------------------------
|
|
45
|
+
TARGET="$PWD"
|
|
46
|
+
SUBCMD=""
|
|
47
|
+
ALIAS=""
|
|
48
|
+
DEST=""
|
|
49
|
+
while [ $# -gt 0 ]; do
|
|
50
|
+
case "$1" in
|
|
51
|
+
--target) TARGET="$2"; shift 2 ;;
|
|
52
|
+
--target=*) TARGET="${1#*=}"; shift ;;
|
|
53
|
+
-h|--help)
|
|
54
|
+
sed -n '2,30p' "$0"; exit 0 ;;
|
|
55
|
+
add|remove|list)
|
|
56
|
+
SUBCMD="$1"; shift
|
|
57
|
+
[ "$SUBCMD" = "add" ] && { ALIAS="${1:-}"; DEST="${2:-}"; shift 2 || true; }
|
|
58
|
+
[ "$SUBCMD" = "remove" ] && { ALIAS="${1:-}"; shift || true; }
|
|
59
|
+
;;
|
|
60
|
+
*)
|
|
61
|
+
dr_die "unknown arg: $1 (try --help)" ;;
|
|
62
|
+
esac
|
|
63
|
+
done
|
|
64
|
+
|
|
65
|
+
[ -z "$SUBCMD" ] && dr_die "missing subcommand (add | remove | list)"
|
|
66
|
+
|
|
67
|
+
TARGET="$(dr_abspath_dir "$TARGET")" || dr_die "target dir not found: $TARGET"
|
|
68
|
+
dr_is_global_claude "$TARGET" && dr_die "refusing to operate on ~/.claude — DevRites is project-local only"
|
|
69
|
+
|
|
70
|
+
SKILLS_DIR="$TARGET/.claude/skills"
|
|
71
|
+
MF="$TARGET/$DR_MANIFEST_NAME"
|
|
72
|
+
|
|
73
|
+
[ -d "$SKILLS_DIR" ] || dr_die "no .claude/skills at $TARGET — run install.sh first?"
|
|
74
|
+
[ -f "$MF" ] || dr_die "no manifest at $MF — run install.sh first?"
|
|
75
|
+
|
|
76
|
+
# ---- helpers -------------------------------------------------------------
|
|
77
|
+
valid_alias_name() {
|
|
78
|
+
# lowercase ASCII, digits, hyphens. No /, ., spaces. Not "rite" or "rite-*".
|
|
79
|
+
case "$1" in
|
|
80
|
+
""|/*|*/*|*[!a-z0-9-]*) return 1 ;;
|
|
81
|
+
rite|rite-*) return 1 ;;
|
|
82
|
+
esac
|
|
83
|
+
return 0
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
is_known_target() {
|
|
87
|
+
# Target must be a rite-* skill present in the installed pack.
|
|
88
|
+
case "$1" in
|
|
89
|
+
rite-*) [ -f "$SKILLS_DIR/$1/SKILL.md" ] ;;
|
|
90
|
+
*) return 1 ;;
|
|
91
|
+
esac
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
is_pinned_alias() {
|
|
95
|
+
# Detect a wrapper we generated: SKILL.md description must contain "Alias of DevRites /<target>".
|
|
96
|
+
[ -f "$SKILLS_DIR/$1/SKILL.md" ] || return 1
|
|
97
|
+
grep -q 'description: Alias of DevRites /' "$SKILLS_DIR/$1/SKILL.md"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# ---- subcommands ---------------------------------------------------------
|
|
101
|
+
do_add() {
|
|
102
|
+
valid_alias_name "$ALIAS" || dr_die "invalid alias name '$ALIAS' (lowercase / digits / hyphens; not 'rite' or 'rite-*')"
|
|
103
|
+
is_known_target "$DEST" || dr_die "unknown target '$DEST' — not a rite-* skill in $SKILLS_DIR"
|
|
104
|
+
[ "$ALIAS" = "$DEST" ] && dr_die "alias and target are the same"
|
|
105
|
+
|
|
106
|
+
ALIAS_DIR="$SKILLS_DIR/$ALIAS"
|
|
107
|
+
ALIAS_FILE="$ALIAS_DIR/SKILL.md"
|
|
108
|
+
ALIAS_REL=".claude/skills/$ALIAS/SKILL.md"
|
|
109
|
+
|
|
110
|
+
if [ -e "$ALIAS_FILE" ]; then
|
|
111
|
+
if is_pinned_alias "$ALIAS"; then
|
|
112
|
+
dr_warn "already pinned: /$ALIAS — overwriting"
|
|
113
|
+
else
|
|
114
|
+
dr_die "$ALIAS_FILE exists and is NOT a pinned alias — refusing to overwrite"
|
|
115
|
+
fi
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
mkdir -p "$ALIAS_DIR"
|
|
119
|
+
dr_gen_alias_wrapper "$ALIAS" "$DEST" "$ALIAS_FILE"
|
|
120
|
+
|
|
121
|
+
if ! dr_manifest_contains "$MF" "$ALIAS_REL"; then
|
|
122
|
+
printf '%s\n' "$ALIAS_REL" >> "$MF"
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
dr_ok "pinned: /$ALIAS → /$DEST ($ALIAS_FILE)"
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
do_remove() {
|
|
129
|
+
valid_alias_name "$ALIAS" || dr_die "invalid alias name '$ALIAS'"
|
|
130
|
+
ALIAS_DIR="$SKILLS_DIR/$ALIAS"
|
|
131
|
+
ALIAS_FILE="$ALIAS_DIR/SKILL.md"
|
|
132
|
+
ALIAS_REL=".claude/skills/$ALIAS/SKILL.md"
|
|
133
|
+
|
|
134
|
+
[ -f "$ALIAS_FILE" ] || dr_die "no pinned alias at $ALIAS_FILE"
|
|
135
|
+
is_pinned_alias "$ALIAS" || dr_die "$ALIAS_FILE exists but is not a pinned alias — refusing to remove"
|
|
136
|
+
|
|
137
|
+
rm -f "$ALIAS_FILE"
|
|
138
|
+
rmdir "$ALIAS_DIR" 2>/dev/null || true
|
|
139
|
+
|
|
140
|
+
# Drop the alias line from the manifest (preserve header + the rest)
|
|
141
|
+
TMP="$(mktemp)"
|
|
142
|
+
grep -Fvx "$ALIAS_REL" "$MF" > "$TMP" && mv "$TMP" "$MF"
|
|
143
|
+
|
|
144
|
+
dr_ok "unpinned: /$ALIAS"
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
do_list() {
|
|
148
|
+
found=0
|
|
149
|
+
for d in "$SKILLS_DIR"/*/; do
|
|
150
|
+
[ -d "$d" ] || continue
|
|
151
|
+
nm="$(basename "$d")"
|
|
152
|
+
if is_pinned_alias "$nm"; then
|
|
153
|
+
to="$(awk '/^description: Alias of DevRites \//{ sub(/.*Alias of DevRites \//,""); sub(/\..*/,""); print; exit }' "$d/SKILL.md")"
|
|
154
|
+
printf ' /%-20s → /%s\n' "$nm" "$to"
|
|
155
|
+
found=1
|
|
156
|
+
fi
|
|
157
|
+
done
|
|
158
|
+
[ "$found" -eq 0 ] && dr_say "(no pinned aliases at $TARGET)"
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
case "$SUBCMD" in
|
|
162
|
+
add) [ -n "$ALIAS" ] && [ -n "$DEST" ] || dr_die "usage: pin.sh add <alias> <target>"; do_add ;;
|
|
163
|
+
remove) [ -n "$ALIAS" ] || dr_die "usage: pin.sh remove <alias>"; do_remove ;;
|
|
164
|
+
list) do_list ;;
|
|
165
|
+
*) dr_die "unknown subcommand: $SUBCMD" ;;
|
|
166
|
+
esac
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Render a GitHub-Actions-flavored markdown table from an eval summary JSONL.
|
|
3
|
+
|
|
4
|
+
Reads eval-summary.jsonl (one JSON object per line, written by
|
|
5
|
+
`scripts/eval-runner.py --summary-file`) and prints a markdown table to
|
|
6
|
+
stdout. Used by `.github/workflows/evals.yml` to populate the job summary.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import pathlib
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main() -> int:
|
|
17
|
+
path = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else "eval-summary.jsonl")
|
|
18
|
+
model = os.environ.get("DEVRITES_EVAL_MODEL", "claude-haiku-4-5-20251001")
|
|
19
|
+
if not path.is_file():
|
|
20
|
+
print(f"(no summary file found at {path})")
|
|
21
|
+
return 0
|
|
22
|
+
print(f"## DevRites trigger evals\n")
|
|
23
|
+
print(f"Model: `{model}`\n")
|
|
24
|
+
print("| Skill | Correct | Accuracy | FP | FN | Passed |")
|
|
25
|
+
print("|---|---|---|---|---|---|")
|
|
26
|
+
total_correct = total_n = total_fp = total_fn = 0
|
|
27
|
+
any_failed = False
|
|
28
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
29
|
+
line = line.strip()
|
|
30
|
+
if not line:
|
|
31
|
+
continue
|
|
32
|
+
r = json.loads(line)
|
|
33
|
+
tick = "✅" if r["passed"] else "❌"
|
|
34
|
+
if not r["passed"]:
|
|
35
|
+
any_failed = True
|
|
36
|
+
total_correct += r["correct"]
|
|
37
|
+
total_n += r["total"]
|
|
38
|
+
total_fp += r["false_positives"]
|
|
39
|
+
total_fn += r["false_negatives"]
|
|
40
|
+
print(f"| `{r['skill']}` | {r['correct']}/{r['total']} | {r['accuracy']:.0%} | {r['false_positives']} | {r['false_negatives']} | {tick} |")
|
|
41
|
+
if total_n:
|
|
42
|
+
overall = total_correct / total_n
|
|
43
|
+
print(f"| **overall** | **{total_correct}/{total_n}** | **{overall:.0%}** | **{total_fp}** | **{total_fn}** | {'✅' if not any_failed else '❌'} |")
|
|
44
|
+
return 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
sys.exit(main())
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/run-evals.sh — validate the structure of DevRites trigger evals.
|
|
3
|
+
#
|
|
4
|
+
# Schema check + summary. Does NOT actually invoke Claude — full eval
|
|
5
|
+
# execution requires CLAUDE_API_KEY and a `claude` CLI invocation that is
|
|
6
|
+
# gated to the user, not CI. CI runs this script to enforce the shape and
|
|
7
|
+
# catch broken JSON / missing skills / wrong query counts.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# scripts/run-evals.sh # validate every evals/*.json
|
|
11
|
+
# scripts/run-evals.sh evals/rite-spec.json # validate one file
|
|
12
|
+
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
16
|
+
EVALS_DIR="$ROOT/evals"
|
|
17
|
+
EXPECTED_QUERIES=20
|
|
18
|
+
|
|
19
|
+
if [[ $# -gt 0 ]]; then
|
|
20
|
+
FILES=("$@")
|
|
21
|
+
else
|
|
22
|
+
if [[ ! -d "$EVALS_DIR" ]]; then
|
|
23
|
+
echo "No evals/ directory at $EVALS_DIR" >&2
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
FILES=()
|
|
27
|
+
while IFS= read -r f; do
|
|
28
|
+
[[ "$f" == */README.md ]] && continue
|
|
29
|
+
FILES+=("$f")
|
|
30
|
+
done < <(find "$EVALS_DIR" -type f -name '*.json' | sort)
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
if [[ ${#FILES[@]} -eq 0 ]]; then
|
|
34
|
+
echo "No eval files found." >&2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Need either python3 or jq for JSON parsing. Prefer python3 (already a
|
|
39
|
+
# DevRites build dep).
|
|
40
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
41
|
+
PARSER="python3"
|
|
42
|
+
elif command -v jq >/dev/null 2>&1; then
|
|
43
|
+
PARSER="jq"
|
|
44
|
+
else
|
|
45
|
+
echo "Need python3 or jq to validate JSON." >&2
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
FAILED=0
|
|
50
|
+
TOTAL=0
|
|
51
|
+
|
|
52
|
+
for file in "${FILES[@]}"; do
|
|
53
|
+
TOTAL=$((TOTAL + 1))
|
|
54
|
+
printf '== %s ==\n' "$file"
|
|
55
|
+
|
|
56
|
+
if [[ "$PARSER" == "python3" ]]; then
|
|
57
|
+
OUT=$(python3 - "$file" <<'PY'
|
|
58
|
+
import json, sys, pathlib
|
|
59
|
+
path = pathlib.Path(sys.argv[1])
|
|
60
|
+
try:
|
|
61
|
+
data = json.loads(path.read_text())
|
|
62
|
+
except Exception as e:
|
|
63
|
+
print(f"INVALID JSON: {e}")
|
|
64
|
+
sys.exit(1)
|
|
65
|
+
|
|
66
|
+
errors = []
|
|
67
|
+
|
|
68
|
+
for key in ("skill", "description", "queries"):
|
|
69
|
+
if key not in data:
|
|
70
|
+
errors.append(f"missing top-level key: {key}")
|
|
71
|
+
|
|
72
|
+
queries = data.get("queries", [])
|
|
73
|
+
if not isinstance(queries, list):
|
|
74
|
+
errors.append("queries is not a list")
|
|
75
|
+
elif len(queries) != 20:
|
|
76
|
+
errors.append(f"expected 20 queries, got {len(queries)}")
|
|
77
|
+
|
|
78
|
+
trig = noTrig = 0
|
|
79
|
+
for i, q in enumerate(queries):
|
|
80
|
+
if not isinstance(q, dict):
|
|
81
|
+
errors.append(f"query[{i}] not an object")
|
|
82
|
+
continue
|
|
83
|
+
for k in ("text", "expected", "rationale"):
|
|
84
|
+
if k not in q:
|
|
85
|
+
errors.append(f"query[{i}] missing key: {k}")
|
|
86
|
+
if q.get("expected") == "should_trigger":
|
|
87
|
+
trig += 1
|
|
88
|
+
elif q.get("expected") == "should_not_trigger":
|
|
89
|
+
noTrig += 1
|
|
90
|
+
else:
|
|
91
|
+
errors.append(f"query[{i}] invalid expected: {q.get('expected')!r}")
|
|
92
|
+
|
|
93
|
+
if errors:
|
|
94
|
+
for e in errors:
|
|
95
|
+
print(f" FAIL: {e}")
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
|
|
98
|
+
print(f" skill: {data['skill']}")
|
|
99
|
+
print(f" queries: {len(queries)} (should_trigger={trig}, should_not_trigger={noTrig})")
|
|
100
|
+
PY
|
|
101
|
+
)
|
|
102
|
+
rc=$?
|
|
103
|
+
else
|
|
104
|
+
OUT=$(jq -r '
|
|
105
|
+
if (.skill and .description and (.queries|type=="array")) then
|
|
106
|
+
if (.queries|length) == 20 then
|
|
107
|
+
" skill: \(.skill)\n queries: \(.queries|length) (should_trigger=\(.queries|map(select(.expected=="should_trigger"))|length), should_not_trigger=\(.queries|map(select(.expected=="should_not_trigger"))|length))"
|
|
108
|
+
else
|
|
109
|
+
" FAIL: expected 20 queries, got \(.queries|length)"
|
|
110
|
+
end
|
|
111
|
+
else
|
|
112
|
+
" FAIL: missing required keys"
|
|
113
|
+
end
|
|
114
|
+
' "$file") || rc=1
|
|
115
|
+
rc=${rc:-0}
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
printf '%s\n' "$OUT"
|
|
119
|
+
if [[ ${rc:-0} -ne 0 ]] || [[ "$OUT" == *"FAIL"* ]]; then
|
|
120
|
+
FAILED=$((FAILED + 1))
|
|
121
|
+
fi
|
|
122
|
+
done
|
|
123
|
+
|
|
124
|
+
echo
|
|
125
|
+
printf 'Validated %d eval files; %d failed.\n' "$TOTAL" "$FAILED"
|
|
126
|
+
|
|
127
|
+
if [[ $FAILED -gt 0 ]]; then
|
|
128
|
+
exit 1
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
if [[ -z "${CLAUDE_API_KEY:-}" ]]; then
|
|
132
|
+
echo
|
|
133
|
+
echo "Note: CLAUDE_API_KEY is not set; ran schema validation only."
|
|
134
|
+
echo "To execute the evals against a live Claude model:"
|
|
135
|
+
echo " pip install anthropic"
|
|
136
|
+
echo " CLAUDE_API_KEY=sk-... python3 scripts/eval-runner.py evals/*.json"
|
|
137
|
+
echo "Override the model with DEVRITES_EVAL_MODEL=claude-... ."
|
|
138
|
+
exit 0
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
# Live execution path — runs each eval against a real Claude model.
|
|
142
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
143
|
+
echo "error: CLAUDE_API_KEY is set but python3 is required for the live runner." >&2
|
|
144
|
+
exit 1
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
echo
|
|
148
|
+
echo "CLAUDE_API_KEY set — executing live trigger evals via scripts/eval-runner.py …"
|
|
149
|
+
exec python3 "$ROOT/scripts/eval-runner.py" "${FILES[@]}"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Outcome-eval harness — proves the deterministic feature grader BOTH passes a
|
|
3
|
+
# known-shippable workspace AND fails a known-blocked one (see-it-fail-first:
|
|
4
|
+
# a grader that never returns NO-GO proves nothing). Runs in CI; no API key.
|
|
5
|
+
#
|
|
6
|
+
# Usage: run-outcome-evals.sh
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
11
|
+
GRADER="$ROOT/scripts/grade-feature.sh"
|
|
12
|
+
good="$ROOT/evals/golden/shippable-feature"
|
|
13
|
+
bad="$ROOT/evals/golden/blocked-feature"
|
|
14
|
+
nearmiss="$ROOT/evals/golden/near-miss-unproven-ac"
|
|
15
|
+
fail=0
|
|
16
|
+
|
|
17
|
+
echo "== grade golden/shippable-feature (expect GO) =="
|
|
18
|
+
if bash "$GRADER" "$good"; then
|
|
19
|
+
echo " PASS — graded GO"
|
|
20
|
+
else
|
|
21
|
+
echo " FAIL — a known-shippable workspace should grade GO"; fail=1
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
echo
|
|
25
|
+
echo "== grade golden/blocked-feature (expect NO-GO) =="
|
|
26
|
+
if bash "$GRADER" "$bad"; then
|
|
27
|
+
echo " FAIL — a known-blocked workspace should NOT grade GO"; fail=1
|
|
28
|
+
else
|
|
29
|
+
echo " PASS — correctly graded NO-GO"
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
echo
|
|
33
|
+
echo "== grade golden/near-miss-unproven-ac (expect NO-GO on ONE invariant) =="
|
|
34
|
+
# Isolates invariant 2 (every acceptance criterion proven): identical to the
|
|
35
|
+
# shippable fixture except a single AC is left unchecked. Proves that gate fails
|
|
36
|
+
# independently — not only when six invariants trip at once.
|
|
37
|
+
if bash "$GRADER" "$nearmiss"; then
|
|
38
|
+
echo " FAIL — an unproven acceptance criterion must grade NO-GO"; fail=1
|
|
39
|
+
else
|
|
40
|
+
echo " PASS — correctly graded NO-GO on the lone unchecked AC"
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
echo
|
|
44
|
+
if [ "$fail" -eq 0 ]; then
|
|
45
|
+
echo "Outcome evals passed."
|
|
46
|
+
else
|
|
47
|
+
echo "Outcome evals FAILED."
|
|
48
|
+
fi
|
|
49
|
+
exit "$fail"
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Scan the shipped DevRites pack for prompt-injection and hidden-unicode risks.
|
|
3
|
+
|
|
4
|
+
DevRites ships executable instruction files (skills, agents, rules, hooks) into
|
|
5
|
+
other people's projects. A poisoned or invisibly-altered file is a supply-chain
|
|
6
|
+
vulnerability, not a style nit (cf. the Snyk Feb-2026 "ToxicSkills" study: 36%
|
|
7
|
+
of public skills carried prompt injection). This is the BLOCKING gate that keeps
|
|
8
|
+
such a file from ever being released.
|
|
9
|
+
|
|
10
|
+
Usage: scan-pack-security.py PATH [PATH ...]
|
|
11
|
+
PATH may be a file or a directory (directories are walked for text files).
|
|
12
|
+
Exits non-zero if any finding is reported.
|
|
13
|
+
|
|
14
|
+
Two finding classes:
|
|
15
|
+
|
|
16
|
+
1. Hidden unicode (always a finding — no legitimate reason to exist in a
|
|
17
|
+
Markdown/shell instruction file): bidi controls, zero-width characters,
|
|
18
|
+
other invisibles, and homoglyph confusables (a word that mixes ASCII with
|
|
19
|
+
look-alike Cyrillic/Greek letters).
|
|
20
|
+
|
|
21
|
+
2. Prompt-injection patterns (a finding unless explicitly suppressed): the
|
|
22
|
+
"ignore previous instructions" family, system-prompt overrides, tool /
|
|
23
|
+
permission escalation, and data-exfiltration phrasing.
|
|
24
|
+
|
|
25
|
+
Suppression (for DevRites' own files that legitimately *discuss* injection
|
|
26
|
+
defensively — e.g. security.md, the reviewer agents): put a marker on the same
|
|
27
|
+
line OR anywhere in the file:
|
|
28
|
+
|
|
29
|
+
... ignore previous instructions ... <!-- pack-scan-ignore: defensive doc -->
|
|
30
|
+
|
|
31
|
+
A whole file can opt a class out with a top-of-file marker:
|
|
32
|
+
|
|
33
|
+
<!-- pack-scan-ignore-file: injection -->
|
|
34
|
+
|
|
35
|
+
Suppressions live in the file, so every exception is visible in the diff and
|
|
36
|
+
reviewable — consistent with DevRites' "no silent state" thesis. Hidden-unicode
|
|
37
|
+
findings can be suppressed the same way but should essentially never be.
|
|
38
|
+
"""
|
|
39
|
+
import os
|
|
40
|
+
import re
|
|
41
|
+
import sys
|
|
42
|
+
import unicodedata
|
|
43
|
+
|
|
44
|
+
# --- hidden unicode -------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
# Invisible / formatting code points with no business in an instruction file.
|
|
47
|
+
ZERO_WIDTH = {
|
|
48
|
+
0x200B, # zero width space
|
|
49
|
+
0x200C, # zero width non-joiner
|
|
50
|
+
0x200D, # zero width joiner
|
|
51
|
+
0x2060, # word joiner
|
|
52
|
+
0xFEFF, # zero width no-break space / BOM (mid-file)
|
|
53
|
+
0x180E, # mongolian vowel separator
|
|
54
|
+
0x00AD, # soft hyphen
|
|
55
|
+
}
|
|
56
|
+
BIDI_CONTROLS = {
|
|
57
|
+
0x202A, 0x202B, 0x202C, 0x202D, 0x202E, # LRE RLE PDF LRO RLO
|
|
58
|
+
0x2066, 0x2067, 0x2068, 0x2069, # LRI RLI FSI PDI
|
|
59
|
+
0x200E, 0x200F, # LRM RLM
|
|
60
|
+
}
|
|
61
|
+
HIDDEN = ZERO_WIDTH | BIDI_CONTROLS
|
|
62
|
+
|
|
63
|
+
# Scripts whose letters are commonly used as ASCII look-alikes.
|
|
64
|
+
CONFUSABLE_SCRIPTS = ("CYRILLIC", "GREEK")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _char_script(ch):
|
|
68
|
+
try:
|
|
69
|
+
return unicodedata.name(ch).split(" ")[0]
|
|
70
|
+
except ValueError:
|
|
71
|
+
return ""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def find_hidden_unicode(text):
|
|
75
|
+
"""Yield (line_no, col, label) for hidden code points and homoglyph words."""
|
|
76
|
+
for lineno, line in enumerate(text.splitlines(), start=1):
|
|
77
|
+
for col, ch in enumerate(line, start=1):
|
|
78
|
+
cp = ord(ch)
|
|
79
|
+
if cp == 0xFEFF and lineno == 1 and col == 1:
|
|
80
|
+
continue # a leading BOM is legal; only mid-file ZWNBSP is suspect
|
|
81
|
+
if cp in HIDDEN:
|
|
82
|
+
yield lineno, col, "hidden char U+%04X (%s)" % (cp, _hidden_name(cp))
|
|
83
|
+
# Homoglyph: a token mixing ASCII letters with confusable-script letters.
|
|
84
|
+
for m in re.finditer(r"\S+", line):
|
|
85
|
+
tok = m.group(0)
|
|
86
|
+
has_ascii_alpha = any(("a" <= c.lower() <= "z") for c in tok)
|
|
87
|
+
mixed = [c for c in tok if c.isalpha() and _char_script(c) in CONFUSABLE_SCRIPTS]
|
|
88
|
+
if has_ascii_alpha and mixed:
|
|
89
|
+
yield lineno, m.start() + 1, (
|
|
90
|
+
"homoglyph: ASCII word mixes %s look-alike(s) %r" % (
|
|
91
|
+
_char_script(mixed[0]).title(), "".join(sorted(set(mixed)))))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _hidden_name(cp):
|
|
95
|
+
try:
|
|
96
|
+
return unicodedata.name(chr(cp))
|
|
97
|
+
except ValueError:
|
|
98
|
+
return "unnamed"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# --- prompt-injection patterns -------------------------------------------
|
|
102
|
+
|
|
103
|
+
# Each: (label, compiled regex). Kept high-signal to limit false positives on
|
|
104
|
+
# legitimate prose; defensive discussion is handled via suppression markers.
|
|
105
|
+
# Gaps use '.' (any non-newline char): scanning is per-line, so a match can't
|
|
106
|
+
# cross lines, and excluding periods would miss tokens like ".env" / ".ssh".
|
|
107
|
+
_INJECTION = [
|
|
108
|
+
("ignore-previous-instructions",
|
|
109
|
+
r"\b(ignore|disregard|forget)\b.{0,40}\b(previous|prior|above|earlier|all)\b"
|
|
110
|
+
r".{0,20}\b(instruction|instructions|prompt|prompts|direction|directions|context)\b"),
|
|
111
|
+
("system-prompt-override",
|
|
112
|
+
r"\b(you are now|act as|pretend to be|new instructions\s*:|override (your|the) (system|prompt|rules))"),
|
|
113
|
+
("permission-escalation",
|
|
114
|
+
r"\b(bypass|ignore|disable|skip)\b.{0,30}\b(approval|permission|permissions|guardrail|guardrails|safety|sandbox|restriction|restrictions)\b"),
|
|
115
|
+
("data-exfiltration",
|
|
116
|
+
r"\b(exfiltrat\w+|send|post|upload|leak|curl|wget)\b.{0,40}\b(env|environment|secret|secrets|credential|credentials|token|tokens|api[_-]?key|\.ssh|private key)\b"),
|
|
117
|
+
]
|
|
118
|
+
INJECTION = [(label, re.compile(rx, re.IGNORECASE)) for label, rx in _INJECTION]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def find_injection(text, file_suppressed):
|
|
122
|
+
"""Yield (line_no, label, excerpt) for injection patterns, honoring suppression."""
|
|
123
|
+
for lineno, line in enumerate(text.splitlines(), start=1):
|
|
124
|
+
if _line_suppressed(line):
|
|
125
|
+
continue
|
|
126
|
+
for label, rx in INJECTION:
|
|
127
|
+
if "injection" in file_suppressed:
|
|
128
|
+
break
|
|
129
|
+
m = rx.search(line)
|
|
130
|
+
if m:
|
|
131
|
+
yield lineno, label, line.strip()[:120]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
_LINE_SUPPRESS = re.compile(r"pack-scan-ignore\b(?!-file)", re.IGNORECASE)
|
|
135
|
+
# Class list is word/comma/space only — a trailing "-->" comment closer must not
|
|
136
|
+
# get swallowed into the captured class name.
|
|
137
|
+
_FILE_SUPPRESS = re.compile(r"pack-scan-ignore-file\s*:\s*([\w, ]+)", re.IGNORECASE)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _line_suppressed(line):
|
|
141
|
+
return bool(_LINE_SUPPRESS.search(line))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _file_suppressions(text):
|
|
145
|
+
classes = set()
|
|
146
|
+
for m in _FILE_SUPPRESS.finditer(text):
|
|
147
|
+
for c in m.group(1).split(","):
|
|
148
|
+
classes.add(c.strip().lower())
|
|
149
|
+
return classes
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# --- driver ---------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
TEXT_EXTS = {".md", ".sh", ".json", ".txt", ".py", ".js", ".yaml", ".yml", ""}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def iter_files(paths):
|
|
158
|
+
for p in paths:
|
|
159
|
+
if os.path.isdir(p):
|
|
160
|
+
for root, _dirs, names in os.walk(p):
|
|
161
|
+
for n in sorted(names):
|
|
162
|
+
fp = os.path.join(root, n)
|
|
163
|
+
ext = os.path.splitext(n)[1].lower()
|
|
164
|
+
if ext in TEXT_EXTS:
|
|
165
|
+
yield fp
|
|
166
|
+
else:
|
|
167
|
+
yield p
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def scan(paths):
|
|
171
|
+
"""Return a list of finding strings. Empty list == clean."""
|
|
172
|
+
findings = []
|
|
173
|
+
for path in iter_files(paths):
|
|
174
|
+
try:
|
|
175
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
176
|
+
text = fh.read()
|
|
177
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
178
|
+
findings.append("ERROR %s: cannot read as utf-8 (%s)" % (path, e))
|
|
179
|
+
continue
|
|
180
|
+
file_sup = _file_suppressions(text)
|
|
181
|
+
hidden_off = ("unicode" in file_sup) or ("hidden" in file_sup)
|
|
182
|
+
if not hidden_off:
|
|
183
|
+
for lineno, col, label in find_hidden_unicode(text):
|
|
184
|
+
if _line_suppressed(text.splitlines()[lineno - 1]):
|
|
185
|
+
continue
|
|
186
|
+
findings.append("FINDING %s:%d:%d: %s" % (path, lineno, col, label))
|
|
187
|
+
for lineno, label, excerpt in find_injection(text, file_sup):
|
|
188
|
+
findings.append("FINDING %s:%d: injection/%s: %s" % (path, lineno, label, excerpt))
|
|
189
|
+
return findings
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def main(argv):
|
|
193
|
+
paths = argv[1:]
|
|
194
|
+
if not paths:
|
|
195
|
+
print("usage: scan-pack-security.py PATH [PATH ...]", file=sys.stderr)
|
|
196
|
+
return 2
|
|
197
|
+
findings = scan(paths)
|
|
198
|
+
if findings:
|
|
199
|
+
for f in findings:
|
|
200
|
+
print(f)
|
|
201
|
+
print("\n%d security finding(s). Fix, or add an auditable "
|
|
202
|
+
"pack-scan-ignore marker if this is defensive content." % len(findings))
|
|
203
|
+
return 1
|
|
204
|
+
print("OK pack security scan clean (%s)" % ", ".join(paths))
|
|
205
|
+
return 0
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
if __name__ == "__main__":
|
|
209
|
+
sys.exit(main(sys.argv))
|