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.
Files changed (73) hide show
  1. crewscore-0.2.4/.github/workflows/crewscore-selftest.yml +106 -0
  2. {crewscore-0.2.2 → crewscore-0.2.4}/.github/workflows/example-ci.yml +3 -0
  3. {crewscore-0.2.2 → crewscore-0.2.4}/AGENTS.md +5 -2
  4. {crewscore-0.2.2 → crewscore-0.2.4}/PKG-INFO +26 -3
  5. {crewscore-0.2.2 → crewscore-0.2.4}/README.md +25 -2
  6. crewscore-0.2.4/action.yml +186 -0
  7. {crewscore-0.2.2 → crewscore-0.2.4}/crewscore/__init__.py +1 -1
  8. {crewscore-0.2.2 → crewscore-0.2.4}/crewscore/cli.py +93 -2
  9. crewscore-0.2.4/crewscore/export_eval.py +100 -0
  10. {crewscore-0.2.2 → crewscore-0.2.4}/crewscore/scan.py +35 -2
  11. {crewscore-0.2.2 → crewscore-0.2.4}/crewscore/scoring.py +1 -1
  12. crewscore-0.2.4/crewscore/summary.py +127 -0
  13. {crewscore-0.2.2 → crewscore-0.2.4}/docs/launch/show-hn.md +5 -1
  14. {crewscore-0.2.2 → crewscore-0.2.4}/docs/publish-checklist.md +12 -10
  15. crewscore-0.2.4/examples/corpus/LEADERBOARD.md +30 -0
  16. crewscore-0.2.4/examples/corpus/README.md +27 -0
  17. crewscore-0.2.4/examples/corpus/prompts/01-bare-assistant.md +1 -0
  18. crewscore-0.2.4/examples/corpus/prompts/02-demo-agent.md +11 -0
  19. crewscore-0.2.4/examples/corpus/prompts/03-agents-md-weak.md +12 -0
  20. crewscore-0.2.4/examples/corpus/prompts/04-partial-hygiene.md +14 -0
  21. crewscore-0.2.4/examples/corpus/prompts/05-hardened-ops.md +65 -0
  22. {crewscore-0.2.2 → crewscore-0.2.4}/pyproject.toml +1 -1
  23. {crewscore-0.2.2 → crewscore-0.2.4}/score-engine.js +2 -2
  24. crewscore-0.2.4/scripts/score_corpus.py +91 -0
  25. {crewscore-0.2.2 → crewscore-0.2.4}/tests/test_action_manifest.py +10 -0
  26. {crewscore-0.2.2 → crewscore-0.2.4}/tests/test_cli.py +23 -2
  27. crewscore-0.2.4/tests/test_corpus.py +35 -0
  28. {crewscore-0.2.2 → crewscore-0.2.4}/tests/test_explain.py +3 -1
  29. crewscore-0.2.4/tests/test_export_eval.py +38 -0
  30. {crewscore-0.2.2 → crewscore-0.2.4}/tests/test_scan.py +4 -0
  31. {crewscore-0.2.2 → crewscore-0.2.4}/tests/test_structural_analysis.py +2 -2
  32. crewscore-0.2.4/tests/test_summary.py +43 -0
  33. crewscore-0.2.2/.github/workflows/crewscore-selftest.yml +0 -46
  34. crewscore-0.2.2/action.yml +0 -98
  35. {crewscore-0.2.2 → crewscore-0.2.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  36. {crewscore-0.2.2 → crewscore-0.2.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  37. {crewscore-0.2.2 → crewscore-0.2.4}/.github/ISSUE_TEMPLATE/scoring_feedback.md +0 -0
  38. {crewscore-0.2.2 → crewscore-0.2.4}/.github/workflows/pytest.yml +0 -0
  39. {crewscore-0.2.2 → crewscore-0.2.4}/.gitignore +0 -0
  40. {crewscore-0.2.2 → crewscore-0.2.4}/CNAME +0 -0
  41. {crewscore-0.2.2 → crewscore-0.2.4}/CONTRIBUTING.md +0 -0
  42. {crewscore-0.2.2 → crewscore-0.2.4}/LICENSE +0 -0
  43. {crewscore-0.2.2 → crewscore-0.2.4}/crewscore/report.py +0 -0
  44. {crewscore-0.2.2 → crewscore-0.2.4}/crewscore/rules_catalog.py +0 -0
  45. {crewscore-0.2.2 → crewscore-0.2.4}/crewscore/scorers/__init__.py +0 -0
  46. {crewscore-0.2.2 → crewscore-0.2.4}/crewscore/scorers/fix_patterns.py +0 -0
  47. {crewscore-0.2.2 → crewscore-0.2.4}/crewscore/scorers/structural_analysis.py +0 -0
  48. {crewscore-0.2.2 → crewscore-0.2.4}/crewscore/vendor_scorecard.py +0 -0
  49. {crewscore-0.2.2 → crewscore-0.2.4}/crewscore/web_export.py +0 -0
  50. {crewscore-0.2.2 → crewscore-0.2.4}/docs/demo.svg +0 -0
  51. {crewscore-0.2.2 → crewscore-0.2.4}/docs/launch/README.md +0 -0
  52. {crewscore-0.2.2 → crewscore-0.2.4}/docs/launch/devto.md +0 -0
  53. {crewscore-0.2.2 → crewscore-0.2.4}/docs/launch/linkedin.md +0 -0
  54. {crewscore-0.2.2 → crewscore-0.2.4}/docs/launch/reddit.md +0 -0
  55. {crewscore-0.2.2 → crewscore-0.2.4}/docs/launch/x-post.md +0 -0
  56. {crewscore-0.2.2 → crewscore-0.2.4}/docs/next-steps-eval.md +0 -0
  57. {crewscore-0.2.2 → crewscore-0.2.4}/docs/pmf-research-2026-07-28.md +0 -0
  58. {crewscore-0.2.2 → crewscore-0.2.4}/docs/research/2026-07-28-useful-product-and-virality-research.md +0 -0
  59. {crewscore-0.2.2 → crewscore-0.2.4}/docs/superpowers/plans/2026-07-28-crewscore-viral-launch.md +0 -0
  60. {crewscore-0.2.2 → crewscore-0.2.4}/docs/superpowers/plans/2026-07-28-useful-product-slice.md +0 -0
  61. {crewscore-0.2.2 → crewscore-0.2.4}/docs/superpowers/plans/2026-07-28-workstream-a-scoring-report.md +0 -0
  62. {crewscore-0.2.2 → crewscore-0.2.4}/docs/superpowers/plans/2026-07-28-workstream-b-scan-report.md +0 -0
  63. {crewscore-0.2.2 → crewscore-0.2.4}/docs/superpowers/plans/2026-07-28-workstream-cd-product-action-report.md +0 -0
  64. {crewscore-0.2.2 → crewscore-0.2.4}/docs/viral-product-spec-2026-07-28.md +0 -0
  65. {crewscore-0.2.2 → crewscore-0.2.4}/examples/sample-prompt.md +0 -0
  66. {crewscore-0.2.2 → crewscore-0.2.4}/favicon.ico +0 -0
  67. {crewscore-0.2.2 → crewscore-0.2.4}/favicon.svg +0 -0
  68. {crewscore-0.2.2 → crewscore-0.2.4}/index.html +0 -0
  69. {crewscore-0.2.2 → crewscore-0.2.4}/scripts/export_web_engine.py +0 -0
  70. {crewscore-0.2.2 → crewscore-0.2.4}/tests/test_report.py +0 -0
  71. {crewscore-0.2.2 → crewscore-0.2.4}/tests/test_rules_catalog.py +0 -0
  72. {crewscore-0.2.2 → crewscore-0.2.4}/tests/test_vendor.py +0 -0
  73. {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 (prompt-file or scan-path)
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.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.2`) and **deterministic** — no LLM, no hidden model.
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.2`) and **deterministic** — no LLM, no hidden model.
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"
@@ -1,3 +1,3 @@
1
1
  """CrewScore — offline structural production-readiness scorecard for AI agent prompts."""
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.2.4"
@@ -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
- def test(prompt, prompt_file, as_json, threshold, explain, report, badge):
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
- def scan(path, as_json, threshold, explain):
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: