crewscore 0.2.2__tar.gz → 0.2.4__tar.gz
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.
- crewscore-0.2.4/.github/workflows/crewscore-selftest.yml +106 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/.github/workflows/example-ci.yml +3 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/AGENTS.md +5 -2
- {crewscore-0.2.2 → crewscore-0.2.4}/PKG-INFO +26 -3
- {crewscore-0.2.2 → crewscore-0.2.4}/README.md +25 -2
- crewscore-0.2.4/action.yml +186 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/crewscore/__init__.py +1 -1
- {crewscore-0.2.2 → crewscore-0.2.4}/crewscore/cli.py +93 -2
- crewscore-0.2.4/crewscore/export_eval.py +100 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/crewscore/scan.py +35 -2
- {crewscore-0.2.2 → crewscore-0.2.4}/crewscore/scoring.py +1 -1
- crewscore-0.2.4/crewscore/summary.py +127 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/docs/launch/show-hn.md +5 -1
- {crewscore-0.2.2 → crewscore-0.2.4}/docs/publish-checklist.md +12 -10
- crewscore-0.2.4/examples/corpus/LEADERBOARD.md +30 -0
- crewscore-0.2.4/examples/corpus/README.md +27 -0
- crewscore-0.2.4/examples/corpus/prompts/01-bare-assistant.md +1 -0
- crewscore-0.2.4/examples/corpus/prompts/02-demo-agent.md +11 -0
- crewscore-0.2.4/examples/corpus/prompts/03-agents-md-weak.md +12 -0
- crewscore-0.2.4/examples/corpus/prompts/04-partial-hygiene.md +14 -0
- crewscore-0.2.4/examples/corpus/prompts/05-hardened-ops.md +65 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/pyproject.toml +1 -1
- {crewscore-0.2.2 → crewscore-0.2.4}/score-engine.js +2 -2
- crewscore-0.2.4/scripts/score_corpus.py +91 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/tests/test_action_manifest.py +10 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/tests/test_cli.py +23 -2
- crewscore-0.2.4/tests/test_corpus.py +35 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/tests/test_explain.py +3 -1
- crewscore-0.2.4/tests/test_export_eval.py +38 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/tests/test_scan.py +4 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/tests/test_structural_analysis.py +2 -2
- crewscore-0.2.4/tests/test_summary.py +43 -0
- crewscore-0.2.2/.github/workflows/crewscore-selftest.yml +0 -46
- crewscore-0.2.2/action.yml +0 -98
- {crewscore-0.2.2 → crewscore-0.2.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/.github/ISSUE_TEMPLATE/scoring_feedback.md +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/.github/workflows/pytest.yml +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/.gitignore +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/CNAME +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/CONTRIBUTING.md +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/LICENSE +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/crewscore/report.py +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/crewscore/rules_catalog.py +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/crewscore/scorers/__init__.py +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/crewscore/scorers/fix_patterns.py +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/crewscore/scorers/structural_analysis.py +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/crewscore/vendor_scorecard.py +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/crewscore/web_export.py +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/docs/demo.svg +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/docs/launch/README.md +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/docs/launch/devto.md +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/docs/launch/linkedin.md +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/docs/launch/reddit.md +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/docs/launch/x-post.md +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/docs/next-steps-eval.md +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/docs/pmf-research-2026-07-28.md +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/docs/research/2026-07-28-useful-product-and-virality-research.md +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/docs/superpowers/plans/2026-07-28-crewscore-viral-launch.md +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/docs/superpowers/plans/2026-07-28-useful-product-slice.md +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/docs/superpowers/plans/2026-07-28-workstream-a-scoring-report.md +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/docs/superpowers/plans/2026-07-28-workstream-b-scan-report.md +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/docs/superpowers/plans/2026-07-28-workstream-cd-product-action-report.md +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/docs/viral-product-spec-2026-07-28.md +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/examples/sample-prompt.md +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/favicon.ico +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/favicon.svg +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/index.html +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/scripts/export_web_engine.py +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/tests/test_report.py +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/tests/test_rules_catalog.py +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/tests/test_vendor.py +0 -0
- {crewscore-0.2.2 → crewscore-0.2.4}/tests/test_web_engine.py +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Self-test for the composite CrewScore action in this repo.
|
|
2
|
+
# Installs from github.action_path so it works before PyPI publish.
|
|
3
|
+
name: CrewScore Self-Test
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
selftest:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
pull-requests: write
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout
|
|
17
|
+
uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Score sample prompt (smoke, threshold 0)
|
|
20
|
+
id: score
|
|
21
|
+
uses: ./
|
|
22
|
+
with:
|
|
23
|
+
prompt-file: examples/sample-prompt.md
|
|
24
|
+
threshold: "0"
|
|
25
|
+
|
|
26
|
+
- name: Assert score and tier outputs parse
|
|
27
|
+
run: |
|
|
28
|
+
echo "score=${{ steps.score.outputs.score }}"
|
|
29
|
+
echo "tier=${{ steps.score.outputs.tier }}"
|
|
30
|
+
python -c "
|
|
31
|
+
import sys
|
|
32
|
+
score = int(sys.argv[1])
|
|
33
|
+
tier = sys.argv[2]
|
|
34
|
+
assert 0 <= score <= 100, score
|
|
35
|
+
assert tier, 'tier output empty'
|
|
36
|
+
print(f'OK score={score} tier={tier!r}')
|
|
37
|
+
" "${{ steps.score.outputs.score }}" "${{ steps.score.outputs.tier }}"
|
|
38
|
+
|
|
39
|
+
- name: Threshold gate fails when score is below threshold
|
|
40
|
+
run: |
|
|
41
|
+
set +e
|
|
42
|
+
crewscore test --prompt-file examples/sample-prompt.md --json --threshold 50
|
|
43
|
+
EXIT=$?
|
|
44
|
+
set -e
|
|
45
|
+
if [ "$EXIT" -ne 2 ]; then
|
|
46
|
+
echo "Expected exit code 2 for bare sample prompt under threshold 50, got $EXIT"
|
|
47
|
+
exit 1
|
|
48
|
+
fi
|
|
49
|
+
echo "Threshold failure behaved as expected (exit 2)"
|
|
50
|
+
|
|
51
|
+
- name: Open rules catalog is non-empty
|
|
52
|
+
run: |
|
|
53
|
+
crewscore rules --json > /tmp/rules.json
|
|
54
|
+
python -c "
|
|
55
|
+
import json
|
|
56
|
+
data = json.load(open('/tmp/rules.json'))
|
|
57
|
+
assert data['rule_count'] > 0
|
|
58
|
+
assert data['method']['llm_calls'] is False
|
|
59
|
+
assert 'structural_analysis' in data['method']['source_of_truth']
|
|
60
|
+
print('OK rules', data['rule_count'], data['ruleset'])
|
|
61
|
+
"
|
|
62
|
+
|
|
63
|
+
- name: Repo scan discovers AGENTS.md (threshold 0)
|
|
64
|
+
id: scan
|
|
65
|
+
uses: ./
|
|
66
|
+
with:
|
|
67
|
+
scan-path: "."
|
|
68
|
+
threshold: "0"
|
|
69
|
+
|
|
70
|
+
- name: Assert scan outputs
|
|
71
|
+
run: |
|
|
72
|
+
echo "scan_score=${{ steps.scan.outputs.score }}"
|
|
73
|
+
echo "scan_tier=${{ steps.scan.outputs.tier }}"
|
|
74
|
+
python -c "
|
|
75
|
+
import sys
|
|
76
|
+
score = int(sys.argv[1])
|
|
77
|
+
assert 0 <= score <= 100, score
|
|
78
|
+
print('OK scan score', score)
|
|
79
|
+
" "${{ steps.scan.outputs.score }}"
|
|
80
|
+
|
|
81
|
+
- name: Markdown summary writer
|
|
82
|
+
run: |
|
|
83
|
+
crewscore test --prompt-file examples/sample-prompt.md --summary /tmp/cs-summary.md --no-explain
|
|
84
|
+
test -s /tmp/cs-summary.md
|
|
85
|
+
grep -q 'crewscore-hygiene@' /tmp/cs-summary.md
|
|
86
|
+
grep -q 'rules' /tmp/cs-summary.md
|
|
87
|
+
echo "OK summary markdown"
|
|
88
|
+
|
|
89
|
+
- name: Scan summary + corpus gradient
|
|
90
|
+
run: |
|
|
91
|
+
crewscore scan examples/corpus --summary /tmp/cs-scan.md --json > /tmp/cs-scan.json
|
|
92
|
+
test -s /tmp/cs-scan.md
|
|
93
|
+
grep -q 'Worst score' /tmp/cs-scan.md || grep -q 'Path' /tmp/cs-scan.md
|
|
94
|
+
python -c "
|
|
95
|
+
import json
|
|
96
|
+
from pathlib import PurePosixPath, PureWindowsPath
|
|
97
|
+
rows = json.load(open('/tmp/cs-scan.json'))
|
|
98
|
+
by = {}
|
|
99
|
+
for r in rows:
|
|
100
|
+
p = r['path'].replace('\\\\', '/')
|
|
101
|
+
by[PurePosixPath(p).name] = int(r['overall'])
|
|
102
|
+
assert by['01-bare-assistant.md'] == 0
|
|
103
|
+
assert by['05-hardened-ops.md'] >= 70
|
|
104
|
+
assert by['05-hardened-ops.md'] > by['04-partial-hygiene.md']
|
|
105
|
+
print('OK corpus', by)
|
|
106
|
+
"
|
|
@@ -27,12 +27,14 @@ jobs:
|
|
|
27
27
|
uses: actions/checkout@v4
|
|
28
28
|
|
|
29
29
|
# Published action (recommended for consumer repos):
|
|
30
|
+
# permissions: { contents: read, pull-requests: write } # for sticky PR comments
|
|
30
31
|
# - name: CrewScore
|
|
31
32
|
# uses: shmindmaster/crewscore@v1
|
|
32
33
|
# with:
|
|
33
34
|
# prompt-file: agents/system-prompt.md
|
|
34
35
|
# threshold: "50"
|
|
35
36
|
# # explain: "true"
|
|
37
|
+
# # pr-comment: "true" # default on pull_request
|
|
36
38
|
|
|
37
39
|
# Repo-wide structural scan (preferred when you have multiple agent files):
|
|
38
40
|
# - name: CrewScore scan
|
|
@@ -49,6 +51,7 @@ jobs:
|
|
|
49
51
|
with:
|
|
50
52
|
prompt-file: examples/sample-prompt.md
|
|
51
53
|
threshold: "50"
|
|
54
|
+
pr-comment: "false"
|
|
52
55
|
|
|
53
56
|
# Local scan example (uncomment when `crewscore scan` is available):
|
|
54
57
|
# - name: CrewScore scan
|
|
@@ -53,8 +53,9 @@ Legacy CLI entry point `agent-guard` still maps to the same `crewscore.cli:main`
|
|
|
53
53
|
|
|
54
54
|
```
|
|
55
55
|
crewscore/
|
|
56
|
-
cli.py # click entry (test, fix, scan)
|
|
56
|
+
cli.py # click entry (test, fix, scan, rules, export-eval)
|
|
57
57
|
scoring.py # shared result model / tiers
|
|
58
|
+
summary.py # PR/job markdown (transparent)
|
|
58
59
|
vendor_scorecard.py # assess-vendor command
|
|
59
60
|
web_export.py # builds score-engine.js payload
|
|
60
61
|
report.py # HTML report + SVG badge
|
|
@@ -62,9 +63,11 @@ crewscore/
|
|
|
62
63
|
structural_analysis.py
|
|
63
64
|
fix_patterns.py
|
|
64
65
|
scripts/export_web_engine.py
|
|
66
|
+
scripts/score_corpus.py # examples/corpus leaderboard
|
|
67
|
+
examples/corpus/ # synthetic bare→hardened demo fixtures
|
|
65
68
|
score-engine.js # generated — commit after pattern changes
|
|
66
69
|
index.html # builder-first site (uses score-engine.js)
|
|
67
|
-
action.yml # composite GH Action (
|
|
70
|
+
action.yml # composite GH Action (scan/test + sticky PR comment)
|
|
68
71
|
docs/launch/ # launch copy kit
|
|
69
72
|
docs/next-steps-eval.md # Promptfoo / garak handoff
|
|
70
73
|
tests/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crewscore
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: CrewScore — offline structural production-readiness scorecard for AI agent system prompts.
|
|
5
5
|
Project-URL: Homepage, https://crewscore.ai
|
|
6
6
|
Project-URL: Repository, https://github.com/shmindmaster/crewscore
|
|
@@ -97,7 +97,7 @@ Scores reflect **prompt-text signals**. They are a useful smoke test, not a guar
|
|
|
97
97
|
Honest principles we ship by:
|
|
98
98
|
|
|
99
99
|
1. CrewScore measures **presence of hygiene signals in text**, not agent behavior.
|
|
100
|
-
2. Scores are **rule-pack versioned** (`crewscore-hygiene@0.2.
|
|
100
|
+
2. Scores are **rule-pack versioned** (`crewscore-hygiene@0.2.3`) and **deterministic** — no LLM, no hidden model.
|
|
101
101
|
3. **Every rule is public.** List them anytime:
|
|
102
102
|
```bash
|
|
103
103
|
crewscore rules # human: formula + every rule_id + regex
|
|
@@ -137,12 +137,19 @@ crewscore scan .
|
|
|
137
137
|
crewscore scan ./agents --json
|
|
138
138
|
crewscore scan . --threshold 50
|
|
139
139
|
crewscore scan . --json --threshold 50
|
|
140
|
+
crewscore scan . --summary crewscore-summary.md
|
|
141
|
+
crewscore test --prompt-file ./AGENTS.md --summary crewscore-summary.md
|
|
142
|
+
|
|
143
|
+
# Synthetic demo gradient (bare → hardened)
|
|
144
|
+
crewscore scan examples/corpus
|
|
140
145
|
```
|
|
141
146
|
|
|
142
147
|
- Prints a table of path → overall → tier (JSON with `--json`).
|
|
143
148
|
- `--threshold N` exits `2` if **any** file scores below `N`.
|
|
149
|
+
- `--summary PATH` writes transparent PR/job markdown (formula + open rule IDs for single-file; table for scan).
|
|
144
150
|
- Exit `1` if no candidate files are found.
|
|
145
151
|
- Skips `node_modules`, `.git`, `venv`, `dist`, `__pycache__`, `.venv`.
|
|
152
|
+
- Demo fixtures: [examples/corpus/](examples/corpus/) + [LEADERBOARD.md](examples/corpus/LEADERBOARD.md).
|
|
146
153
|
|
|
147
154
|
Use this as the default CI gate for monorepos with multiple agent artifacts.
|
|
148
155
|
|
|
@@ -264,6 +271,9 @@ on: [pull_request]
|
|
|
264
271
|
jobs:
|
|
265
272
|
score:
|
|
266
273
|
runs-on: ubuntu-latest
|
|
274
|
+
permissions:
|
|
275
|
+
contents: read
|
|
276
|
+
pull-requests: write # sticky PR comment with open rule findings
|
|
267
277
|
steps:
|
|
268
278
|
- uses: actions/checkout@v4
|
|
269
279
|
- name: CrewScore
|
|
@@ -275,6 +285,8 @@ jobs:
|
|
|
275
285
|
prompt-file: ./agents/system-prompt.md
|
|
276
286
|
threshold: "50"
|
|
277
287
|
# explain: "true" # optional: matched vs missing signals
|
|
288
|
+
# pr-comment: "true" # default: sticky PR comment on pull_request
|
|
289
|
+
# summary: crewscore-summary.md
|
|
278
290
|
- name: Report
|
|
279
291
|
if: always()
|
|
280
292
|
run: |
|
|
@@ -290,10 +302,14 @@ jobs:
|
|
|
290
302
|
| `scan-path` | one of | — | Path for `crewscore scan` (worst score becomes the output) |
|
|
291
303
|
| `threshold` | no | `50` | Fail the step (exit 2) when overall score is below this |
|
|
292
304
|
| `explain` | no | `false` | Pass `true` to include matched/missing signals |
|
|
305
|
+
| `summary` | no | `crewscore-summary.md` | Markdown path (also appends to `GITHUB_STEP_SUMMARY`) |
|
|
306
|
+
| `pr-comment` | no | `true` | On `pull_request`, post/update a sticky comment with the summary |
|
|
293
307
|
|
|
294
308
|
Provide **either** `prompt-file` **or** `scan-path` (not neither). For scan mode, outputs use the **minimum** overall across discovered files.
|
|
295
309
|
|
|
296
|
-
**Outputs:** `score` (0–100), `tier` (label string).
|
|
310
|
+
**Outputs:** `score` (0–100), `tier` (label string), `summary-path` (markdown file if written).
|
|
311
|
+
|
|
312
|
+
Sticky PR comments need `permissions: pull-requests: write` on the job. Set `pr-comment: "false"` to disable.
|
|
297
313
|
|
|
298
314
|
The composite action installs CrewScore from the action path (`pip install "${{ github.action_path }}"`), so monorepo / pre-PyPI self-tests work with `uses: ./`.
|
|
299
315
|
|
|
@@ -341,6 +357,13 @@ CrewScore is the cheap lint / structural pre-gate. When you need **live** behavi
|
|
|
341
357
|
|
|
342
358
|
Structural scores do **not** measure jailbreak resistance or multi-turn tool abuse. Use those tools after the prompt text has basic hygiene. Details: [docs/next-steps-eval.md](docs/next-steps-eval.md).
|
|
343
359
|
|
|
360
|
+
Generate starter stubs (does **not** run live evals):
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
crewscore export-eval --prompt-file ./agents/system-prompt.md -o ./crewscore-eval
|
|
364
|
+
# → promptfooconfig.yaml + README-EVAL.md (Promptfoo + garak notes)
|
|
365
|
+
```
|
|
366
|
+
|
|
344
367
|
---
|
|
345
368
|
|
|
346
369
|
## Fix patterns
|
|
@@ -70,7 +70,7 @@ Scores reflect **prompt-text signals**. They are a useful smoke test, not a guar
|
|
|
70
70
|
Honest principles we ship by:
|
|
71
71
|
|
|
72
72
|
1. CrewScore measures **presence of hygiene signals in text**, not agent behavior.
|
|
73
|
-
2. Scores are **rule-pack versioned** (`crewscore-hygiene@0.2.
|
|
73
|
+
2. Scores are **rule-pack versioned** (`crewscore-hygiene@0.2.3`) and **deterministic** — no LLM, no hidden model.
|
|
74
74
|
3. **Every rule is public.** List them anytime:
|
|
75
75
|
```bash
|
|
76
76
|
crewscore rules # human: formula + every rule_id + regex
|
|
@@ -110,12 +110,19 @@ crewscore scan .
|
|
|
110
110
|
crewscore scan ./agents --json
|
|
111
111
|
crewscore scan . --threshold 50
|
|
112
112
|
crewscore scan . --json --threshold 50
|
|
113
|
+
crewscore scan . --summary crewscore-summary.md
|
|
114
|
+
crewscore test --prompt-file ./AGENTS.md --summary crewscore-summary.md
|
|
115
|
+
|
|
116
|
+
# Synthetic demo gradient (bare → hardened)
|
|
117
|
+
crewscore scan examples/corpus
|
|
113
118
|
```
|
|
114
119
|
|
|
115
120
|
- Prints a table of path → overall → tier (JSON with `--json`).
|
|
116
121
|
- `--threshold N` exits `2` if **any** file scores below `N`.
|
|
122
|
+
- `--summary PATH` writes transparent PR/job markdown (formula + open rule IDs for single-file; table for scan).
|
|
117
123
|
- Exit `1` if no candidate files are found.
|
|
118
124
|
- Skips `node_modules`, `.git`, `venv`, `dist`, `__pycache__`, `.venv`.
|
|
125
|
+
- Demo fixtures: [examples/corpus/](examples/corpus/) + [LEADERBOARD.md](examples/corpus/LEADERBOARD.md).
|
|
119
126
|
|
|
120
127
|
Use this as the default CI gate for monorepos with multiple agent artifacts.
|
|
121
128
|
|
|
@@ -237,6 +244,9 @@ on: [pull_request]
|
|
|
237
244
|
jobs:
|
|
238
245
|
score:
|
|
239
246
|
runs-on: ubuntu-latest
|
|
247
|
+
permissions:
|
|
248
|
+
contents: read
|
|
249
|
+
pull-requests: write # sticky PR comment with open rule findings
|
|
240
250
|
steps:
|
|
241
251
|
- uses: actions/checkout@v4
|
|
242
252
|
- name: CrewScore
|
|
@@ -248,6 +258,8 @@ jobs:
|
|
|
248
258
|
prompt-file: ./agents/system-prompt.md
|
|
249
259
|
threshold: "50"
|
|
250
260
|
# explain: "true" # optional: matched vs missing signals
|
|
261
|
+
# pr-comment: "true" # default: sticky PR comment on pull_request
|
|
262
|
+
# summary: crewscore-summary.md
|
|
251
263
|
- name: Report
|
|
252
264
|
if: always()
|
|
253
265
|
run: |
|
|
@@ -263,10 +275,14 @@ jobs:
|
|
|
263
275
|
| `scan-path` | one of | — | Path for `crewscore scan` (worst score becomes the output) |
|
|
264
276
|
| `threshold` | no | `50` | Fail the step (exit 2) when overall score is below this |
|
|
265
277
|
| `explain` | no | `false` | Pass `true` to include matched/missing signals |
|
|
278
|
+
| `summary` | no | `crewscore-summary.md` | Markdown path (also appends to `GITHUB_STEP_SUMMARY`) |
|
|
279
|
+
| `pr-comment` | no | `true` | On `pull_request`, post/update a sticky comment with the summary |
|
|
266
280
|
|
|
267
281
|
Provide **either** `prompt-file` **or** `scan-path` (not neither). For scan mode, outputs use the **minimum** overall across discovered files.
|
|
268
282
|
|
|
269
|
-
**Outputs:** `score` (0–100), `tier` (label string).
|
|
283
|
+
**Outputs:** `score` (0–100), `tier` (label string), `summary-path` (markdown file if written).
|
|
284
|
+
|
|
285
|
+
Sticky PR comments need `permissions: pull-requests: write` on the job. Set `pr-comment: "false"` to disable.
|
|
270
286
|
|
|
271
287
|
The composite action installs CrewScore from the action path (`pip install "${{ github.action_path }}"`), so monorepo / pre-PyPI self-tests work with `uses: ./`.
|
|
272
288
|
|
|
@@ -314,6 +330,13 @@ CrewScore is the cheap lint / structural pre-gate. When you need **live** behavi
|
|
|
314
330
|
|
|
315
331
|
Structural scores do **not** measure jailbreak resistance or multi-turn tool abuse. Use those tools after the prompt text has basic hygiene. Details: [docs/next-steps-eval.md](docs/next-steps-eval.md).
|
|
316
332
|
|
|
333
|
+
Generate starter stubs (does **not** run live evals):
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
crewscore export-eval --prompt-file ./agents/system-prompt.md -o ./crewscore-eval
|
|
337
|
+
# → promptfooconfig.yaml + README-EVAL.md (Promptfoo + garak notes)
|
|
338
|
+
```
|
|
339
|
+
|
|
317
340
|
---
|
|
318
341
|
|
|
319
342
|
## Fix patterns
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
name: CrewScore
|
|
2
|
+
description: Structural production-readiness scorecard for AI agent system prompts
|
|
3
|
+
author: shmindmaster
|
|
4
|
+
branding:
|
|
5
|
+
icon: shield
|
|
6
|
+
color: blue
|
|
7
|
+
|
|
8
|
+
inputs:
|
|
9
|
+
prompt-file:
|
|
10
|
+
description: Path to a single system prompt file to score (required if scan-path is empty)
|
|
11
|
+
required: false
|
|
12
|
+
default: ""
|
|
13
|
+
scan-path:
|
|
14
|
+
description: Directory/path to scan for agent instruction files via crewscore scan. When set, scores all discovered files and reports the worst (minimum) overall.
|
|
15
|
+
required: false
|
|
16
|
+
default: ""
|
|
17
|
+
threshold:
|
|
18
|
+
description: Exit non-zero (code 2) if overall score is below this value (0-100)
|
|
19
|
+
required: false
|
|
20
|
+
default: "50"
|
|
21
|
+
explain:
|
|
22
|
+
description: Include matched vs missing guardrail signals (pass "true" to enable)
|
|
23
|
+
required: false
|
|
24
|
+
default: "false"
|
|
25
|
+
summary:
|
|
26
|
+
description: Write transparent markdown summary to this path (also appends to GITHUB_STEP_SUMMARY when set)
|
|
27
|
+
required: false
|
|
28
|
+
default: "crewscore-summary.md"
|
|
29
|
+
pr-comment:
|
|
30
|
+
description: On pull_request events, post or update a sticky PR comment with the summary (requires pull-requests: write)
|
|
31
|
+
required: false
|
|
32
|
+
default: "true"
|
|
33
|
+
|
|
34
|
+
outputs:
|
|
35
|
+
score:
|
|
36
|
+
description: Overall structural score (0-100); for scan mode, the worst (minimum) overall across files
|
|
37
|
+
value: ${{ steps.run.outputs.score }}
|
|
38
|
+
tier:
|
|
39
|
+
description: Score tier label
|
|
40
|
+
value: ${{ steps.run.outputs.tier }}
|
|
41
|
+
summary-path:
|
|
42
|
+
description: Path to the markdown summary file (if written)
|
|
43
|
+
value: ${{ steps.run.outputs.summary-path }}
|
|
44
|
+
|
|
45
|
+
runs:
|
|
46
|
+
using: composite
|
|
47
|
+
steps:
|
|
48
|
+
- name: Set up Python
|
|
49
|
+
uses: actions/setup-python@v5
|
|
50
|
+
with:
|
|
51
|
+
python-version: "3.12"
|
|
52
|
+
|
|
53
|
+
- name: Install CrewScore from action path
|
|
54
|
+
shell: bash
|
|
55
|
+
run: pip install "${{ github.action_path }}"
|
|
56
|
+
|
|
57
|
+
- name: Run CrewScore
|
|
58
|
+
id: run
|
|
59
|
+
shell: bash
|
|
60
|
+
run: |
|
|
61
|
+
set -euo pipefail
|
|
62
|
+
PROMPT_FILE="${{ inputs.prompt-file }}"
|
|
63
|
+
SCAN_PATH="${{ inputs.scan-path }}"
|
|
64
|
+
THRESHOLD="${{ inputs.threshold }}"
|
|
65
|
+
EXPLAIN="${{ inputs.explain }}"
|
|
66
|
+
SUMMARY="${{ inputs.summary }}"
|
|
67
|
+
|
|
68
|
+
if [ -z "$SCAN_PATH" ] && [ -z "$PROMPT_FILE" ]; then
|
|
69
|
+
echo "Either prompt-file or scan-path is required" >&2
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
set +e
|
|
74
|
+
if [ -n "$SCAN_PATH" ]; then
|
|
75
|
+
ARGS=(scan "$SCAN_PATH" --json --threshold "$THRESHOLD")
|
|
76
|
+
if [ "$EXPLAIN" = "true" ]; then
|
|
77
|
+
ARGS+=(--explain)
|
|
78
|
+
fi
|
|
79
|
+
if [ -n "$SUMMARY" ]; then
|
|
80
|
+
ARGS+=(--summary "$SUMMARY")
|
|
81
|
+
fi
|
|
82
|
+
OUTPUT=$(crewscore "${ARGS[@]}")
|
|
83
|
+
EXIT=$?
|
|
84
|
+
else
|
|
85
|
+
ARGS=(test --prompt-file "$PROMPT_FILE" --json --threshold "$THRESHOLD")
|
|
86
|
+
if [ "$EXPLAIN" = "true" ]; then
|
|
87
|
+
ARGS+=(--explain)
|
|
88
|
+
fi
|
|
89
|
+
if [ -n "$SUMMARY" ]; then
|
|
90
|
+
ARGS+=(--summary "$SUMMARY")
|
|
91
|
+
fi
|
|
92
|
+
OUTPUT=$(crewscore "${ARGS[@]}")
|
|
93
|
+
EXIT=$?
|
|
94
|
+
fi
|
|
95
|
+
set -e
|
|
96
|
+
echo "$OUTPUT"
|
|
97
|
+
if [ -n "$SUMMARY" ] && [ -f "$SUMMARY" ]; then
|
|
98
|
+
echo "Wrote summary markdown: $SUMMARY"
|
|
99
|
+
echo "summary-path=$SUMMARY" >> "$GITHUB_OUTPUT"
|
|
100
|
+
else
|
|
101
|
+
echo "summary-path=" >> "$GITHUB_OUTPUT"
|
|
102
|
+
fi
|
|
103
|
+
# Single-file test JSON: {overall, tier, ...}
|
|
104
|
+
# scan --json: list of {path, overall, tier, ...} — take min overall (worst).
|
|
105
|
+
echo "$OUTPUT" | python -c "
|
|
106
|
+
import json, os, sys
|
|
107
|
+
data = json.load(sys.stdin)
|
|
108
|
+
if isinstance(data, list):
|
|
109
|
+
worst = min(data, key=lambda x: int(x['overall']))
|
|
110
|
+
score, tier = worst['overall'], worst['tier']
|
|
111
|
+
else:
|
|
112
|
+
items = data.get('files') or data.get('results')
|
|
113
|
+
if items:
|
|
114
|
+
worst = min(items, key=lambda x: int(x['overall']))
|
|
115
|
+
score, tier = worst['overall'], worst['tier']
|
|
116
|
+
else:
|
|
117
|
+
score, tier = data['overall'], data['tier']
|
|
118
|
+
with open(os.environ['GITHUB_OUTPUT'], 'a', encoding='utf-8') as f:
|
|
119
|
+
f.write(f'score={score}\n')
|
|
120
|
+
f.write(f'tier={tier}\n')
|
|
121
|
+
"
|
|
122
|
+
exit "$EXIT"
|
|
123
|
+
|
|
124
|
+
- name: Sticky PR comment
|
|
125
|
+
if: ${{ always() && inputs.pr-comment == 'true' && github.event_name == 'pull_request' && steps.run.outputs.summary-path != '' }}
|
|
126
|
+
shell: bash
|
|
127
|
+
env:
|
|
128
|
+
GH_TOKEN: ${{ github.token }}
|
|
129
|
+
SUMMARY_PATH: ${{ steps.run.outputs.summary-path }}
|
|
130
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
131
|
+
REPO: ${{ github.repository }}
|
|
132
|
+
run: |
|
|
133
|
+
set -euo pipefail
|
|
134
|
+
if [ ! -f "$SUMMARY_PATH" ]; then
|
|
135
|
+
echo "No summary file at $SUMMARY_PATH; skip PR comment"
|
|
136
|
+
exit 0
|
|
137
|
+
fi
|
|
138
|
+
if [ -z "${PR_NUMBER:-}" ]; then
|
|
139
|
+
echo "No PR number; skip PR comment"
|
|
140
|
+
exit 0
|
|
141
|
+
fi
|
|
142
|
+
MARKER="<!-- crewscore-sticky -->"
|
|
143
|
+
BODY_FILE=$(mktemp)
|
|
144
|
+
{
|
|
145
|
+
printf '%s\n\n' "$MARKER"
|
|
146
|
+
cat "$SUMMARY_PATH"
|
|
147
|
+
} > "$BODY_FILE"
|
|
148
|
+
# Find existing sticky comment by marker (gh --paginate yields JSON array).
|
|
149
|
+
COMMENT_ID=$(
|
|
150
|
+
gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" --paginate \
|
|
151
|
+
| MARKER="$MARKER" python -c "
|
|
152
|
+
import json, os, sys
|
|
153
|
+
marker = os.environ['MARKER']
|
|
154
|
+
raw = sys.stdin.read().strip()
|
|
155
|
+
comments = []
|
|
156
|
+
if raw:
|
|
157
|
+
try:
|
|
158
|
+
data = json.loads(raw)
|
|
159
|
+
comments = data if isinstance(data, list) else []
|
|
160
|
+
except json.JSONDecodeError:
|
|
161
|
+
# Multiple pages can appear as ][ between arrays.
|
|
162
|
+
fixed = raw.replace('][', ',')
|
|
163
|
+
data = json.loads(fixed)
|
|
164
|
+
comments = data if isinstance(data, list) else []
|
|
165
|
+
for c in comments:
|
|
166
|
+
if marker in (c.get('body') or ''):
|
|
167
|
+
print(c['id'])
|
|
168
|
+
break
|
|
169
|
+
"
|
|
170
|
+
)
|
|
171
|
+
# Prefer raw JSON input for reliable multiline body.
|
|
172
|
+
python -c "
|
|
173
|
+
import json, pathlib, sys
|
|
174
|
+
body = pathlib.Path(sys.argv[1]).read_text(encoding='utf-8')
|
|
175
|
+
print(json.dumps({'body': body}))
|
|
176
|
+
" "$BODY_FILE" > "${BODY_FILE}.json"
|
|
177
|
+
if [ -n "${COMMENT_ID:-}" ]; then
|
|
178
|
+
gh api --method PATCH "repos/${REPO}/issues/comments/${COMMENT_ID}" \
|
|
179
|
+
--input "${BODY_FILE}.json"
|
|
180
|
+
echo "Updated sticky PR comment id=$COMMENT_ID"
|
|
181
|
+
else
|
|
182
|
+
gh api --method POST "repos/${REPO}/issues/${PR_NUMBER}/comments" \
|
|
183
|
+
--input "${BODY_FILE}.json"
|
|
184
|
+
echo "Created sticky PR comment"
|
|
185
|
+
fi
|
|
186
|
+
rm -f "$BODY_FILE" "${BODY_FILE}.json"
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import os
|
|
6
7
|
import sys
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
@@ -11,11 +12,13 @@ from rich.console import Console
|
|
|
11
12
|
from rich.panel import Panel
|
|
12
13
|
|
|
13
14
|
from crewscore import __version__
|
|
15
|
+
from crewscore.export_eval import write_eval_stubs
|
|
14
16
|
from crewscore.report import render_badge_svg, render_html_report, share_text
|
|
15
17
|
from crewscore.rules_catalog import SCORING_METHOD, catalog_payload, scoring_transparency_block
|
|
16
18
|
from crewscore.scan import discover_prompt_files, score_paths
|
|
17
19
|
from crewscore.scoring import DIMENSIONS, RULESET_ID, build_result, tier_color
|
|
18
20
|
from crewscore.scorers import structural_analysis
|
|
21
|
+
from crewscore.summary import format_scan_markdown, format_score_markdown
|
|
19
22
|
from crewscore.vendor_scorecard import assess_vendor
|
|
20
23
|
|
|
21
24
|
console = Console()
|
|
@@ -94,7 +97,13 @@ main.add_command(assess_vendor)
|
|
|
94
97
|
default=None,
|
|
95
98
|
help="Write an SVG badge to this path",
|
|
96
99
|
)
|
|
97
|
-
|
|
100
|
+
@click.option(
|
|
101
|
+
"--summary",
|
|
102
|
+
type=click.Path(),
|
|
103
|
+
default=None,
|
|
104
|
+
help="Write GitHub-flavored markdown summary (PR/step comment body) to this path",
|
|
105
|
+
)
|
|
106
|
+
def test(prompt, prompt_file, as_json, threshold, explain, report, badge, summary):
|
|
98
107
|
"""Run structural production-readiness analysis on an agent system prompt.
|
|
99
108
|
|
|
100
109
|
Offline, deterministic regex scan — not live red-teaming.
|
|
@@ -137,6 +146,17 @@ def test(prompt, prompt_file, as_json, threshold, explain, report, badge):
|
|
|
137
146
|
badge_path.parent.mkdir(parents=True, exist_ok=True)
|
|
138
147
|
badge_path.write_text(render_badge_svg(result), encoding="utf-8")
|
|
139
148
|
|
|
149
|
+
md_body = format_score_markdown(result, findings=findings)
|
|
150
|
+
if summary:
|
|
151
|
+
summary_path = Path(summary)
|
|
152
|
+
summary_path.parent.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
summary_path.write_text(md_body, encoding="utf-8")
|
|
154
|
+
# Always append to GitHub Actions job summary when running in GHA.
|
|
155
|
+
if os.environ.get("GITHUB_STEP_SUMMARY"):
|
|
156
|
+
with Path(os.environ["GITHUB_STEP_SUMMARY"]).open("a", encoding="utf-8") as fh:
|
|
157
|
+
fh.write(md_body)
|
|
158
|
+
fh.write("\n")
|
|
159
|
+
|
|
140
160
|
if as_json:
|
|
141
161
|
payload = result.to_dict()
|
|
142
162
|
payload["findings"] = findings
|
|
@@ -336,6 +356,59 @@ def rules_cmd(as_json: bool, dimension: str | None):
|
|
|
336
356
|
console.print()
|
|
337
357
|
|
|
338
358
|
|
|
359
|
+
@main.command("export-eval")
|
|
360
|
+
@click.option("--prompt", "-p", help="System prompt string to export")
|
|
361
|
+
@click.option(
|
|
362
|
+
"--prompt-file",
|
|
363
|
+
"-f",
|
|
364
|
+
type=click.Path(exists=True),
|
|
365
|
+
help="Path to system prompt file",
|
|
366
|
+
)
|
|
367
|
+
@click.option(
|
|
368
|
+
"--output-dir",
|
|
369
|
+
"-o",
|
|
370
|
+
type=click.Path(),
|
|
371
|
+
default="./crewscore-eval",
|
|
372
|
+
help="Directory for Promptfoo / garak handoff stubs (default: ./crewscore-eval)",
|
|
373
|
+
)
|
|
374
|
+
def export_eval(prompt, prompt_file, output_dir):
|
|
375
|
+
"""Write live-eval stubs (Promptfoo config + garak notes) after structural gate.
|
|
376
|
+
|
|
377
|
+
Does not run Promptfoo or garak. Honest handoff only.
|
|
378
|
+
"""
|
|
379
|
+
if prompt:
|
|
380
|
+
system_prompt = prompt
|
|
381
|
+
source = "prompt"
|
|
382
|
+
elif prompt_file:
|
|
383
|
+
system_prompt = Path(prompt_file).read_text(encoding="utf-8")
|
|
384
|
+
source = str(prompt_file)
|
|
385
|
+
else:
|
|
386
|
+
err_console.print("[red]Error: Provide --prompt or --prompt-file[/red]")
|
|
387
|
+
sys.exit(1)
|
|
388
|
+
|
|
389
|
+
paths = write_eval_stubs(
|
|
390
|
+
Path(output_dir),
|
|
391
|
+
system_prompt=system_prompt,
|
|
392
|
+
prompt_source=source,
|
|
393
|
+
)
|
|
394
|
+
console.print()
|
|
395
|
+
console.print(
|
|
396
|
+
Panel(
|
|
397
|
+
"[bold]Live eval handoff stubs[/bold]",
|
|
398
|
+
border_style="cyan",
|
|
399
|
+
expand=False,
|
|
400
|
+
)
|
|
401
|
+
)
|
|
402
|
+
console.print()
|
|
403
|
+
for p in paths:
|
|
404
|
+
console.print(f" [green]wrote[/green] {p}")
|
|
405
|
+
console.print()
|
|
406
|
+
console.print(
|
|
407
|
+
" [dim]CrewScore remains structural only. "
|
|
408
|
+
"Run Promptfoo/garak yourself — see README-EVAL.md[/dim]"
|
|
409
|
+
)
|
|
410
|
+
console.print()
|
|
411
|
+
|
|
339
412
|
|
|
340
413
|
@main.command()
|
|
341
414
|
@click.option("--prompt", "-p", help="System prompt string to fix")
|
|
@@ -513,7 +586,13 @@ def fix(prompt, prompt_file, apply, output, as_json):
|
|
|
513
586
|
is_flag=True,
|
|
514
587
|
help="Show matched vs missing signals for the lowest-scoring file",
|
|
515
588
|
)
|
|
516
|
-
|
|
589
|
+
@click.option(
|
|
590
|
+
"--summary",
|
|
591
|
+
type=click.Path(),
|
|
592
|
+
default=None,
|
|
593
|
+
help="Write GitHub-flavored markdown summary (PR/step comment body) to this path",
|
|
594
|
+
)
|
|
595
|
+
def scan(path, as_json, threshold, explain, summary):
|
|
517
596
|
"""Discover and score agent prompt files under PATH (default: .).
|
|
518
597
|
|
|
519
598
|
Looks for AGENTS.md, CLAUDE.md, system-prompt.md, and files under
|
|
@@ -546,6 +625,18 @@ def scan(path, as_json, threshold, explain):
|
|
|
546
625
|
abs_by_rel[rel] = abs_path
|
|
547
626
|
item["path"] = rel
|
|
548
627
|
|
|
628
|
+
md_body = format_scan_markdown(scored)
|
|
629
|
+
if summary:
|
|
630
|
+
summary_path = Path(summary)
|
|
631
|
+
summary_path.parent.mkdir(parents=True, exist_ok=True)
|
|
632
|
+
summary_path.write_text(md_body, encoding="utf-8")
|
|
633
|
+
if os.environ.get("GITHUB_STEP_SUMMARY"):
|
|
634
|
+
with Path(os.environ["GITHUB_STEP_SUMMARY"]).open(
|
|
635
|
+
"a", encoding="utf-8"
|
|
636
|
+
) as fh:
|
|
637
|
+
fh.write(md_body)
|
|
638
|
+
fh.write("\n")
|
|
639
|
+
|
|
549
640
|
if as_json:
|
|
550
641
|
click.echo(json.dumps(scored, indent=2, sort_keys=True))
|
|
551
642
|
else:
|