kodelyth-ecc 1.5.9 → 1.7.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/.github/workflows/publish.yml +19 -0
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +455 -0
- package/CLAUDE.md +48 -16
- package/README.md +265 -81
- package/VERSION +1 -1
- package/actions/ecc-review/README.md +243 -0
- package/actions/ecc-review/action.yml +158 -0
- package/actions/ecc-review/post-comment.js +171 -0
- package/actions/ecc-review/run-review.js +285 -0
- package/agents/backdoor-hunter.md +260 -0
- package/agents/chaos-engineer.md +251 -0
- package/agents/code-stealer-detector.md +228 -0
- package/agents/jailbreak-tester.md +222 -0
- package/agents/license-violation-finder.md +212 -0
- package/agents/prompt-injection-hunter.md +126 -0
- package/agents/secret-hunter.md +209 -0
- package/agents/supply-chain-auditor.md +195 -0
- package/bin/kodelyth-ecc.js +875 -1
- package/bundles/enterprise.md +172 -0
- package/bundles/indie-hacker.md +106 -0
- package/bundles/red-team.md +138 -0
- package/cat +0 -0
- package/commands/dashboard.md +67 -0
- package/commands/devil-mode.md +121 -0
- package/commands/memory-evolve.md +71 -0
- package/commands/replay.md +61 -0
- package/commands/route-model.md +48 -0
- package/commands/swarm.md +68 -0
- package/commands/verify-supply-chain.md +59 -0
- package/docs/dashboard.md +211 -0
- package/docs/evolve.md +303 -0
- package/docs/mcp-clients.md +167 -0
- package/docs/mcp.md +178 -0
- package/docs/replay.md +244 -0
- package/docs/supply-chain.md +207 -0
- package/docs/swarm.md +243 -0
- package/hooks/hooks.json +52 -0
- package/hooks/memory/auto-recall.js +29 -1
- package/hooks/safety/README.md +124 -0
- package/hooks/safety/lib/patterns.js +179 -0
- package/hooks/safety/prompt-injection-guard.js +179 -0
- package/hooks/safety/token-budget.js +229 -0
- package/install.ps1 +199 -2
- package/install.sh +286 -4
- package/package.json +30 -4
- package/rules/common/agent-intent-routing.md +182 -0
- package/rules/common/cost-aware-model-routing.md +152 -0
- package/scripts/dashboard/data.js +353 -0
- package/scripts/dashboard/server.js +324 -0
- package/scripts/dashboard/static/index.html +645 -0
- package/scripts/evolve/analyze.js +303 -0
- package/scripts/evolve/proposals.js +162 -0
- package/scripts/evolve/stats.js +219 -0
- package/scripts/mcp/catalog.js +244 -0
- package/scripts/mcp/client.js +181 -0
- package/scripts/mcp/prompts.js +133 -0
- package/scripts/mcp/resources.js +94 -0
- package/scripts/mcp/server.js +153 -0
- package/scripts/mcp/tools.js +465 -0
- package/scripts/replay/bundle.js +191 -0
- package/scripts/replay/replay.js +107 -0
- package/scripts/router/classify.js +232 -0
- package/scripts/supply-chain/manifest.js +154 -0
- package/scripts/supply-chain/sbom.js +202 -0
- package/scripts/supply-chain/verify.js +102 -0
- package/scripts/swarm/build-plan.js +193 -0
- package/skills/cost-aware-model-routing/SKILL.md +153 -0
- package/skills/kodelyth-quickstart/SKILL.md +7 -0
- package/skills/observability-dashboard/SKILL.md +119 -0
- package/skills/self-evolving-memory/SKILL.md +175 -0
- package/skills/session-replay/SKILL.md +199 -0
- package/skills/supply-chain-verification/SKILL.md +201 -0
- package/skills/swarm-orchestrator/SKILL.md +177 -0
- package/social/card-install.svg +1 -1
- package/social/facebook-group/POST.md +121 -0
- package/social/facebook-group/fb-1-3am-debug.png +0 -0
- package/social/facebook-group/fb-1-3am-debug.svg +97 -0
- package/social/facebook-group/fb-2-cpu-upgrade.png +0 -0
- package/social/facebook-group/fb-2-cpu-upgrade.svg +132 -0
- package/social/facebook-group/fb-3-before-after.png +0 -0
- package/social/facebook-group/fb-3-before-after.svg +94 -0
- package/social/facebook-v150.svg +5 -5
- package/social/github-social-preview.svg +119 -100
- package/social/readme-hero.svg +11 -11
- package/social/section-agents.svg +57 -0
- package/social/section-author.svg +54 -0
- package/social/section-dashboard.svg +59 -0
- package/social/section-devil.svg +54 -0
- package/social/section-hooks.svg +51 -0
- package/social/section-install.svg +42 -0
- package/social/section-learning.svg +52 -0
- package/social/section-mcp.svg +46 -0
- package/social/section-memory.svg +57 -0
- package/social/section-parallel.svg +72 -0
- package/social/section-routing.svg +51 -0
- package/social/x-card-agents-grid.svg +6 -6
- package/tests/dashboard/data.test.js +235 -0
- package/tests/dashboard/server.test.js +240 -0
- package/tests/evolve/analyze.test.js +169 -0
- package/tests/evolve/proposals.test.js +173 -0
- package/tests/evolve/stats.test.js +159 -0
- package/tests/mcp/catalog.test.js +98 -0
- package/tests/mcp/client.test.js +109 -0
- package/tests/mcp/resources-prompts.test.js +70 -0
- package/tests/mcp/tools.test.js +159 -0
- package/tests/replay/bundle.test.js +181 -0
- package/tests/replay/replay.test.js +131 -0
- package/tests/router/classify.test.js +164 -0
- package/tests/safety/patterns.test.js +94 -0
- package/tests/safety/prompt-injection-guard.test.js +111 -0
- package/tests/safety/token-budget.test.js +119 -0
- package/tests/supply-chain/manifest.test.js +147 -0
- package/tests/supply-chain/sbom.test.js +170 -0
- package/tests/supply-chain/verify.test.js +146 -0
- package/tests/swarm/build-plan.test.js +188 -0
- package/wiki/Agent-Reference.md +58 -7
- package/wiki/FAQ.md +204 -7
- package/wiki/Home.md +104 -28
- package/wiki/Hook-Reference.md +1 -1
- package/wiki/Installation-Guide.md +109 -6
- package/wiki/Platform-Support.md +239 -25
- package/wiki/Skill-Reference.md +101 -6
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# Kodelyth ECC — PR Review GitHub Action
|
|
2
|
+
|
|
3
|
+
A one-line GitHub Action that runs the Kodelyth ECC adversarial agent crew against your pull-request diff, posts findings as a PR comment, and optionally fails CI on critical issues.
|
|
4
|
+
|
|
5
|
+
> **Phase 1.6 of the Devil Roadmap.** Bigger reach lever than the VS Code extension — every GitHub repo is a potential install target.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
Drop this into `.github/workflows/ecc-review.yml` in any repo:
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
name: Kodelyth ECC Review
|
|
15
|
+
|
|
16
|
+
on:
|
|
17
|
+
pull_request:
|
|
18
|
+
branches: [main, master]
|
|
19
|
+
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
pull-requests: write # required to post the review comment
|
|
23
|
+
|
|
24
|
+
jobs:
|
|
25
|
+
review:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
with:
|
|
30
|
+
fetch-depth: 0
|
|
31
|
+
|
|
32
|
+
- uses: sifxprime/kodelyth-ecc/actions/ecc-review@v1.6.0
|
|
33
|
+
with:
|
|
34
|
+
bundle: red-team
|
|
35
|
+
fail-on: critical
|
|
36
|
+
api-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
That's it. Every PR now gets reviewed by ECC's adversarial crew and the findings appear as a sticky PR comment.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Inputs
|
|
44
|
+
|
|
45
|
+
| Input | Default | Description |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `bundle` | `red-team` | Which ECC bundle: `indie-hacker` / `red-team` / `enterprise` |
|
|
48
|
+
| `agents` | (none) | Comma-separated agent names. Overrides `bundle` if set. Example: `prompt-injection-hunter,secret-hunter,backdoor-hunter` |
|
|
49
|
+
| `fail-on` | `critical` | Severity threshold that fails the build: `critical` / `high` / `medium` / `none` |
|
|
50
|
+
| `api-key` | (none) | Anthropic API key. Pass via `${{ secrets.ANTHROPIC_API_KEY }}`. **Required** for live model review. |
|
|
51
|
+
| `model` | `claude-sonnet-4-5-20250929` | Claude model to use |
|
|
52
|
+
| `comment` | `true` | Post findings as a PR comment |
|
|
53
|
+
| `max-files` | `20` | Max files reviewed per run (cost guard) |
|
|
54
|
+
| `ecc-version` | `latest` | Pin a specific Kodelyth ECC version (e.g., `1.6.0`) |
|
|
55
|
+
|
|
56
|
+
## Outputs
|
|
57
|
+
|
|
58
|
+
| Output | Description |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `critical-count` | Number of CRITICAL findings |
|
|
61
|
+
| `high-count` | Number of HIGH findings |
|
|
62
|
+
| `medium-count` | Number of MEDIUM findings |
|
|
63
|
+
| `report-path` | Path to the JSON report on the runner |
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Bundle → Agents Mapping
|
|
68
|
+
|
|
69
|
+
| Bundle | Agents Run on Each PR |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `indie-hacker` | `security-reviewer` · `code-reviewer` · `ux-reviewer` · `dependency-doctor` |
|
|
72
|
+
| `red-team` (default) | `prompt-injection-hunter` · `supply-chain-auditor` · `secret-hunter` · `backdoor-hunter` |
|
|
73
|
+
| `enterprise` | `code-reviewer` · `security-reviewer` · `license-violation-finder` · `supply-chain-auditor` · `api-guardian` |
|
|
74
|
+
|
|
75
|
+
To run a custom set, use `agents:` instead:
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
- uses: sifxprime/kodelyth-ecc/actions/ecc-review@v1.6.0
|
|
79
|
+
with:
|
|
80
|
+
agents: secret-hunter,license-violation-finder,jailbreak-tester
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Real-World Example Workflows
|
|
86
|
+
|
|
87
|
+
### 1. Indie Hacker — fast feedback, never block
|
|
88
|
+
|
|
89
|
+
```yaml
|
|
90
|
+
- uses: sifxprime/kodelyth-ecc/actions/ecc-review@v1.6.0
|
|
91
|
+
with:
|
|
92
|
+
bundle: indie-hacker
|
|
93
|
+
fail-on: none
|
|
94
|
+
api-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
PR gets a comment, but CI never fails. You decide what to address.
|
|
98
|
+
|
|
99
|
+
### 2. Red Team — full adversarial sweep, block on critical
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
- uses: sifxprime/kodelyth-ecc/actions/ecc-review@v1.6.0
|
|
103
|
+
with:
|
|
104
|
+
bundle: red-team
|
|
105
|
+
fail-on: critical
|
|
106
|
+
api-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Default. Recommended for any user-facing product.
|
|
110
|
+
|
|
111
|
+
### 3. Enterprise — strict, block on high
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
- uses: sifxprime/kodelyth-ecc/actions/ecc-review@v1.6.0
|
|
115
|
+
with:
|
|
116
|
+
bundle: enterprise
|
|
117
|
+
fail-on: high
|
|
118
|
+
api-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 4. Pre-public-release — secret + license + AI safety only
|
|
122
|
+
|
|
123
|
+
```yaml
|
|
124
|
+
- uses: sifxprime/kodelyth-ecc/actions/ecc-review@v1.6.0
|
|
125
|
+
with:
|
|
126
|
+
agents: secret-hunter,license-violation-finder,prompt-injection-hunter
|
|
127
|
+
fail-on: critical
|
|
128
|
+
api-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 5. Cost-controlled — only review on changes to security-sensitive paths
|
|
132
|
+
|
|
133
|
+
```yaml
|
|
134
|
+
on:
|
|
135
|
+
pull_request:
|
|
136
|
+
paths:
|
|
137
|
+
- 'src/auth/**'
|
|
138
|
+
- 'src/api/**'
|
|
139
|
+
- 'src/payments/**'
|
|
140
|
+
- 'package*.json'
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Cost & Rate-Limit Guards
|
|
146
|
+
|
|
147
|
+
The action ships with sensible defaults to keep your Anthropic spend under control:
|
|
148
|
+
|
|
149
|
+
- `max-files: 20` — caps how many changed files get sent per run
|
|
150
|
+
- Each file capped at 20KB per send (truncated with marker)
|
|
151
|
+
- Default `red-team` bundle = 4 agents = 4 API calls per PR
|
|
152
|
+
- Each call uses `max_tokens: 4096`
|
|
153
|
+
- A typical PR review: 4 calls × ~6k input + 4k output tokens ≈ ~$0.06-0.15 with Sonnet
|
|
154
|
+
|
|
155
|
+
For larger codebases or stricter cost control:
|
|
156
|
+
|
|
157
|
+
```yaml
|
|
158
|
+
with:
|
|
159
|
+
max-files: 10
|
|
160
|
+
agents: secret-hunter # single agent only
|
|
161
|
+
ecc-version: 1.6.0 # pin version for reproducibility
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## How It Works
|
|
167
|
+
|
|
168
|
+
1. **Setup** — installs the latest (or pinned) Kodelyth ECC into the runner via `npx kodelyth-ecc --target claude-home --bundle <bundle>`
|
|
169
|
+
2. **Diff** — resolves the changed files between PR base and head SHAs
|
|
170
|
+
3. **Filter** — drops binary, vendored, and oversized files; caps at `max-files`
|
|
171
|
+
4. **Review loop** — for each agent in the bundle:
|
|
172
|
+
- Loads the agent's playbook from `~/.claude/agents/<name>.md`
|
|
173
|
+
- Sends `[playbook + changed files + JSON output schema]` to Claude
|
|
174
|
+
- Parses the structured findings response
|
|
175
|
+
5. **Aggregate** — combines all findings, tallies severities
|
|
176
|
+
6. **Comment** — posts a single PR comment (upserts on subsequent pushes — never spammy)
|
|
177
|
+
7. **Gate** — exits non-zero if findings exceed `fail-on` threshold
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## What This Action Does NOT Do
|
|
182
|
+
|
|
183
|
+
- ❌ Auto-fix issues (advisory only)
|
|
184
|
+
- ❌ Send any data anywhere except Anthropic API + GitHub PR comments
|
|
185
|
+
- ❌ Cache anything between runs (each PR = clean slate)
|
|
186
|
+
- ❌ Run on commits that don't touch reviewable file types
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Privacy & Security
|
|
191
|
+
|
|
192
|
+
- **Code never leaves your control:** PR diff is sent to Anthropic API (using your key) and GitHub PR comments (using `github.token`). Nothing else.
|
|
193
|
+
- **No telemetry:** This action sends no analytics anywhere.
|
|
194
|
+
- **No persistence:** No data stored beyond the runtime of the workflow.
|
|
195
|
+
- **API key:** Pass via `secrets.*` only. Never hardcode.
|
|
196
|
+
|
|
197
|
+
If your repo has compliance requirements that prohibit sending code to Anthropic, **don't use this action**. Run ECC locally instead.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Troubleshooting
|
|
202
|
+
|
|
203
|
+
### "No ANTHROPIC_API_KEY set — review skipped"
|
|
204
|
+
|
|
205
|
+
You forgot to pass the secret. Add it under repo Settings → Secrets and variables → Actions, then reference it in the workflow:
|
|
206
|
+
|
|
207
|
+
```yaml
|
|
208
|
+
api-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### "::warning::Agent file not found"
|
|
212
|
+
|
|
213
|
+
The bundle resolved an agent name that doesn't exist in the installed ECC. Pin a known-good version:
|
|
214
|
+
|
|
215
|
+
```yaml
|
|
216
|
+
ecc-version: 1.6.0
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### "Failed to post PR comment: 403"
|
|
220
|
+
|
|
221
|
+
Your workflow needs `pull-requests: write` permission. Add to top of workflow:
|
|
222
|
+
|
|
223
|
+
```yaml
|
|
224
|
+
permissions:
|
|
225
|
+
contents: read
|
|
226
|
+
pull-requests: write
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Comment is too long (GitHub 65k limit)
|
|
230
|
+
|
|
231
|
+
Reduce findings by tightening `agents:` or `max-files:`. The action caps to first 30 findings in the comment but writes the full report to the runner artifacts (downloadable from Actions tab).
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Related
|
|
236
|
+
|
|
237
|
+
- [Kodelyth ECC repo](https://github.com/sifxprime/kodelyth-ecc) — the toolkit this action runs
|
|
238
|
+
- [`/devil-mode` command](../../commands/devil-mode.md) — same agents, run locally
|
|
239
|
+
- [Devil Roadmap](../../) — the broader v1.6.0 → v2.0.0 plan
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
**Powered by Kodelyth ECC** · MIT license · No telemetry · No cloud
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
name: 'Kodelyth ECC PR Review'
|
|
2
|
+
description: 'Run Kodelyth ECC adversarial agents against a PR diff. Posts findings as a PR comment, optionally fails CI on critical issues.'
|
|
3
|
+
author: 'Kodelyth <github.com/sifxprime>'
|
|
4
|
+
branding:
|
|
5
|
+
icon: 'shield'
|
|
6
|
+
color: 'red'
|
|
7
|
+
|
|
8
|
+
inputs:
|
|
9
|
+
bundle:
|
|
10
|
+
description: 'Which ECC bundle to use: indie-hacker, red-team, enterprise'
|
|
11
|
+
required: false
|
|
12
|
+
default: 'red-team'
|
|
13
|
+
agents:
|
|
14
|
+
description: 'Comma-separated agent names to run. Overrides bundle if set. Examples: prompt-injection-hunter,supply-chain-auditor,secret-hunter'
|
|
15
|
+
required: false
|
|
16
|
+
default: ''
|
|
17
|
+
fail-on:
|
|
18
|
+
description: 'Severity threshold that fails CI: critical | high | medium | none'
|
|
19
|
+
required: false
|
|
20
|
+
default: 'critical'
|
|
21
|
+
api-key:
|
|
22
|
+
description: 'Anthropic API key (recommended). Falls back to OPENAI_API_KEY if not set.'
|
|
23
|
+
required: false
|
|
24
|
+
default: ''
|
|
25
|
+
model:
|
|
26
|
+
description: 'Claude model to use'
|
|
27
|
+
required: false
|
|
28
|
+
default: 'claude-sonnet-4-5-20250929'
|
|
29
|
+
comment:
|
|
30
|
+
description: 'Post findings as a PR comment'
|
|
31
|
+
required: false
|
|
32
|
+
default: 'true'
|
|
33
|
+
max-files:
|
|
34
|
+
description: 'Max number of changed files to review per run (cost guard)'
|
|
35
|
+
required: false
|
|
36
|
+
default: '20'
|
|
37
|
+
ecc-version:
|
|
38
|
+
description: 'Pin to a specific ECC version (default: latest from npm)'
|
|
39
|
+
required: false
|
|
40
|
+
default: 'latest'
|
|
41
|
+
|
|
42
|
+
outputs:
|
|
43
|
+
critical-count:
|
|
44
|
+
description: 'Number of CRITICAL findings'
|
|
45
|
+
value: ${{ steps.review.outputs.critical-count }}
|
|
46
|
+
high-count:
|
|
47
|
+
description: 'Number of HIGH findings'
|
|
48
|
+
value: ${{ steps.review.outputs.high-count }}
|
|
49
|
+
medium-count:
|
|
50
|
+
description: 'Number of MEDIUM findings'
|
|
51
|
+
value: ${{ steps.review.outputs.medium-count }}
|
|
52
|
+
report-path:
|
|
53
|
+
description: 'Filesystem path of the JSON findings report'
|
|
54
|
+
value: ${{ steps.review.outputs.report-path }}
|
|
55
|
+
|
|
56
|
+
runs:
|
|
57
|
+
using: 'composite'
|
|
58
|
+
steps:
|
|
59
|
+
- name: Setup Node 20
|
|
60
|
+
uses: actions/setup-node@v4
|
|
61
|
+
with:
|
|
62
|
+
node-version: '20'
|
|
63
|
+
|
|
64
|
+
- name: Install Kodelyth ECC
|
|
65
|
+
shell: bash
|
|
66
|
+
run: |
|
|
67
|
+
echo "::group::Install Kodelyth ECC"
|
|
68
|
+
npx --yes kodelyth-ecc@${{ inputs.ecc-version }} \
|
|
69
|
+
--target claude-home \
|
|
70
|
+
--bundle ${{ inputs.bundle }}
|
|
71
|
+
echo "::endgroup::"
|
|
72
|
+
|
|
73
|
+
- name: Resolve PR diff
|
|
74
|
+
id: diff
|
|
75
|
+
shell: bash
|
|
76
|
+
run: |
|
|
77
|
+
echo "::group::Resolve PR diff"
|
|
78
|
+
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
|
79
|
+
BASE_SHA="${{ github.event.pull_request.base.sha }}"
|
|
80
|
+
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
|
|
81
|
+
else
|
|
82
|
+
# push event — diff against previous commit
|
|
83
|
+
BASE_SHA="${{ github.event.before }}"
|
|
84
|
+
HEAD_SHA="${{ github.sha }}"
|
|
85
|
+
fi
|
|
86
|
+
echo "base-sha=$BASE_SHA" >> "$GITHUB_OUTPUT"
|
|
87
|
+
echo "head-sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"
|
|
88
|
+
git fetch origin "$BASE_SHA" --depth=1 2>/dev/null || true
|
|
89
|
+
git diff --name-only "$BASE_SHA" "$HEAD_SHA" > /tmp/ecc-changed-files.txt || \
|
|
90
|
+
git diff --name-only "$HEAD_SHA~1" "$HEAD_SHA" > /tmp/ecc-changed-files.txt
|
|
91
|
+
echo "Changed files:"
|
|
92
|
+
cat /tmp/ecc-changed-files.txt
|
|
93
|
+
echo "::endgroup::"
|
|
94
|
+
|
|
95
|
+
- name: Run ECC review
|
|
96
|
+
id: review
|
|
97
|
+
shell: bash
|
|
98
|
+
env:
|
|
99
|
+
ANTHROPIC_API_KEY: ${{ inputs.api-key }}
|
|
100
|
+
ECC_BUNDLE: ${{ inputs.bundle }}
|
|
101
|
+
ECC_AGENTS: ${{ inputs.agents }}
|
|
102
|
+
ECC_MODEL: ${{ inputs.model }}
|
|
103
|
+
ECC_FAIL_ON: ${{ inputs.fail-on }}
|
|
104
|
+
ECC_MAX_FILES: ${{ inputs.max-files }}
|
|
105
|
+
ECC_BASE_SHA: ${{ steps.diff.outputs.base-sha }}
|
|
106
|
+
ECC_HEAD_SHA: ${{ steps.diff.outputs.head-sha }}
|
|
107
|
+
GITHUB_REPOSITORY: ${{ github.repository }}
|
|
108
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
109
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
110
|
+
run: |
|
|
111
|
+
node ${{ github.action_path }}/run-review.js
|
|
112
|
+
|
|
113
|
+
- name: Post PR comment
|
|
114
|
+
if: ${{ inputs.comment == 'true' && github.event_name == 'pull_request' && steps.review.outputs.report-path != '' }}
|
|
115
|
+
shell: bash
|
|
116
|
+
env:
|
|
117
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
118
|
+
REPORT_PATH: ${{ steps.review.outputs.report-path }}
|
|
119
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
120
|
+
REPO: ${{ github.repository }}
|
|
121
|
+
run: |
|
|
122
|
+
node ${{ github.action_path }}/post-comment.js
|
|
123
|
+
|
|
124
|
+
- name: Enforce fail-on threshold
|
|
125
|
+
shell: bash
|
|
126
|
+
env:
|
|
127
|
+
FAIL_ON: ${{ inputs.fail-on }}
|
|
128
|
+
CRITICAL: ${{ steps.review.outputs.critical-count }}
|
|
129
|
+
HIGH: ${{ steps.review.outputs.high-count }}
|
|
130
|
+
MEDIUM: ${{ steps.review.outputs.medium-count }}
|
|
131
|
+
run: |
|
|
132
|
+
critical=${CRITICAL:-0}
|
|
133
|
+
high=${HIGH:-0}
|
|
134
|
+
medium=${MEDIUM:-0}
|
|
135
|
+
case "$FAIL_ON" in
|
|
136
|
+
critical)
|
|
137
|
+
if [[ "$critical" -gt 0 ]]; then
|
|
138
|
+
echo "::error::Found $critical CRITICAL issues. Failing build (fail-on=critical)."
|
|
139
|
+
exit 1
|
|
140
|
+
fi ;;
|
|
141
|
+
high)
|
|
142
|
+
if [[ "$critical" -gt 0 ]] || [[ "$high" -gt 0 ]]; then
|
|
143
|
+
echo "::error::Found $critical CRITICAL + $high HIGH issues. Failing build (fail-on=high)."
|
|
144
|
+
exit 1
|
|
145
|
+
fi ;;
|
|
146
|
+
medium)
|
|
147
|
+
if [[ "$critical" -gt 0 ]] || [[ "$high" -gt 0 ]] || [[ "$medium" -gt 0 ]]; then
|
|
148
|
+
echo "::error::Found $critical CRITICAL + $high HIGH + $medium MEDIUM issues. Failing build (fail-on=medium)."
|
|
149
|
+
exit 1
|
|
150
|
+
fi ;;
|
|
151
|
+
none)
|
|
152
|
+
echo "::notice::Review complete. fail-on=none — never blocking."
|
|
153
|
+
;;
|
|
154
|
+
*)
|
|
155
|
+
echo "::warning::Unknown fail-on value: $FAIL_ON. Treating as 'none'."
|
|
156
|
+
;;
|
|
157
|
+
esac
|
|
158
|
+
echo "::notice::ECC review complete: $critical CRITICAL, $high HIGH, $medium MEDIUM."
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Kodelyth ECC — GitHub Action PR comment poster
|
|
3
|
+
// Reads the JSON report and posts a formatted PR comment via GitHub API.
|
|
4
|
+
//
|
|
5
|
+
// Env contract:
|
|
6
|
+
// GITHUB_TOKEN Provided by github.token
|
|
7
|
+
// REPORT_PATH JSON report file
|
|
8
|
+
// PR_NUMBER PR number
|
|
9
|
+
// REPO owner/repo
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const https = require('https');
|
|
15
|
+
|
|
16
|
+
const reportPath = process.env.REPORT_PATH;
|
|
17
|
+
const prNumber = process.env.PR_NUMBER;
|
|
18
|
+
const repo = process.env.REPO;
|
|
19
|
+
const token = process.env.GITHUB_TOKEN;
|
|
20
|
+
|
|
21
|
+
if (!reportPath || !fs.existsSync(reportPath)) {
|
|
22
|
+
console.log('::warning::No report found, skipping PR comment.');
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
if (!prNumber || !repo || !token) {
|
|
26
|
+
console.log('::warning::Missing PR_NUMBER, REPO, or GITHUB_TOKEN. Skipping comment.');
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
|
|
31
|
+
|
|
32
|
+
// ── Format PR comment ───────────────────────────────────────────────────────
|
|
33
|
+
function severityEmoji(sev) {
|
|
34
|
+
const s = (sev || '').toLowerCase();
|
|
35
|
+
if (s === 'critical') return '🔴';
|
|
36
|
+
if (s === 'high') return '🟠';
|
|
37
|
+
if (s === 'medium') return '🟡';
|
|
38
|
+
if (s === 'low') return '🔵';
|
|
39
|
+
return '⚪';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildComment() {
|
|
43
|
+
const c = report.counts || { critical: 0, high: 0, medium: 0, low: 0 };
|
|
44
|
+
const total = (c.critical || 0) + (c.high || 0) + (c.medium || 0) + (c.low || 0);
|
|
45
|
+
const verdict = total === 0
|
|
46
|
+
? '✅ **Clean.** No issues found by the selected agents.'
|
|
47
|
+
: `**${total}** finding${total === 1 ? '' : 's'} across ${(report.agents || []).length} agents.`;
|
|
48
|
+
|
|
49
|
+
const summaryTable = `
|
|
50
|
+
| Severity | Count |
|
|
51
|
+
|---|---|
|
|
52
|
+
| 🔴 Critical | ${c.critical || 0} |
|
|
53
|
+
| 🟠 High | ${c.high || 0} |
|
|
54
|
+
| 🟡 Medium | ${c.medium || 0} |
|
|
55
|
+
| 🔵 Low | ${c.low || 0} |
|
|
56
|
+
`.trim();
|
|
57
|
+
|
|
58
|
+
const findings = (report.findings || []).slice(0, 30); // cap to first 30 to fit comment limits
|
|
59
|
+
const findingsBlock = findings.length === 0 ? '' : `
|
|
60
|
+
|
|
61
|
+
## Findings
|
|
62
|
+
|
|
63
|
+
${findings.map((f, i) => {
|
|
64
|
+
const emoji = severityEmoji(f.severity);
|
|
65
|
+
const loc = f.line ? `\`${f.file}:${f.line}\`` : `\`${f.file || 'unknown'}\``;
|
|
66
|
+
return `### ${i + 1}. ${emoji} ${(f.severity || 'unknown').toUpperCase()} — ${f.title || '(no title)'}
|
|
67
|
+
**Agent:** \`${f.agent || 'unknown'}\` · **Location:** ${loc} · **Category:** ${f.category || 'general'}
|
|
68
|
+
|
|
69
|
+
${f.description || ''}
|
|
70
|
+
|
|
71
|
+
${f.evidence ? `<details><summary>Evidence</summary>\n\n\`\`\`\n${(f.evidence || '').slice(0, 1000)}\n\`\`\`\n\n</details>` : ''}
|
|
72
|
+
|
|
73
|
+
**Recommended fix:** ${f.fix || '(none provided)'}
|
|
74
|
+
`;
|
|
75
|
+
}).join('\n---\n')}
|
|
76
|
+
${(report.findings || []).length > 30 ? `\n_…and ${(report.findings || []).length - 30} more findings (see full report artifact)._\n` : ''}`;
|
|
77
|
+
|
|
78
|
+
return `## 🦞 Kodelyth ECC — Devil Mode Review
|
|
79
|
+
|
|
80
|
+
${verdict}
|
|
81
|
+
|
|
82
|
+
**Bundle:** \`${report.bundle || 'red-team'}\` · **Agents:** ${(report.agents || []).map(a => `\`${a}\``).join(', ')} · **Files reviewed:** ${(report.files_reviewed || []).length}
|
|
83
|
+
|
|
84
|
+
${summaryTable}
|
|
85
|
+
${findingsBlock}
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
<sub>Powered by [Kodelyth ECC](https://github.com/sifxprime/kodelyth-ecc) · [Configure this action](https://github.com/sifxprime/kodelyth-ecc/tree/main/actions/ecc-review)</sub>`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Find existing comment from this action (for upsert) ─────────────────────
|
|
93
|
+
function listComments() {
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
const req = https.request({
|
|
96
|
+
hostname: 'api.github.com',
|
|
97
|
+
path: `/repos/${repo}/issues/${prNumber}/comments?per_page=100`,
|
|
98
|
+
method: 'GET',
|
|
99
|
+
headers: {
|
|
100
|
+
'Authorization': `token ${token}`,
|
|
101
|
+
'User-Agent': 'kodelyth-ecc-action',
|
|
102
|
+
'Accept': 'application/vnd.github+json',
|
|
103
|
+
},
|
|
104
|
+
}, (res) => {
|
|
105
|
+
let data = '';
|
|
106
|
+
res.on('data', c => data += c);
|
|
107
|
+
res.on('end', () => {
|
|
108
|
+
try { resolve(JSON.parse(data)); }
|
|
109
|
+
catch (e) { reject(e); }
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
req.on('error', reject);
|
|
113
|
+
req.end();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function upsertComment(body, existingId) {
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
const payload = JSON.stringify({ body });
|
|
120
|
+
const opts = existingId ? {
|
|
121
|
+
hostname: 'api.github.com',
|
|
122
|
+
path: `/repos/${repo}/issues/comments/${existingId}`,
|
|
123
|
+
method: 'PATCH',
|
|
124
|
+
} : {
|
|
125
|
+
hostname: 'api.github.com',
|
|
126
|
+
path: `/repos/${repo}/issues/${prNumber}/comments`,
|
|
127
|
+
method: 'POST',
|
|
128
|
+
};
|
|
129
|
+
opts.headers = {
|
|
130
|
+
'Authorization': `token ${token}`,
|
|
131
|
+
'User-Agent': 'kodelyth-ecc-action',
|
|
132
|
+
'Accept': 'application/vnd.github+json',
|
|
133
|
+
'Content-Type': 'application/json',
|
|
134
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
135
|
+
};
|
|
136
|
+
const req = https.request(opts, (res) => {
|
|
137
|
+
let data = '';
|
|
138
|
+
res.on('data', c => data += c);
|
|
139
|
+
res.on('end', () => {
|
|
140
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
141
|
+
resolve(JSON.parse(data));
|
|
142
|
+
} else {
|
|
143
|
+
reject(new Error(`GitHub API ${res.statusCode}: ${data}`));
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
req.on('error', reject);
|
|
148
|
+
req.write(payload);
|
|
149
|
+
req.end();
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
(async () => {
|
|
154
|
+
try {
|
|
155
|
+
const body = buildComment();
|
|
156
|
+
let comments = [];
|
|
157
|
+
try {
|
|
158
|
+
comments = await listComments();
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.log(`::warning::Could not list existing comments (${err.message}); will create new.`);
|
|
161
|
+
}
|
|
162
|
+
const existing = Array.isArray(comments)
|
|
163
|
+
? comments.find(c => c.body && c.body.includes('Kodelyth ECC — Devil Mode Review'))
|
|
164
|
+
: null;
|
|
165
|
+
const result = await upsertComment(body, existing ? existing.id : null);
|
|
166
|
+
console.log(`::notice::PR comment ${existing ? 'updated' : 'created'}: ${result.html_url || ''}`);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.log(`::error::Failed to post PR comment: ${err.message}`);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
})();
|