crewforge 0.2.2__tar.gz → 0.3.0__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.
- {crewforge-0.2.2/src/crewforge.egg-info → crewforge-0.3.0}/PKG-INFO +30 -2
- {crewforge-0.2.2 → crewforge-0.3.0}/README.md +29 -1
- {crewforge-0.2.2 → crewforge-0.3.0}/pyproject.toml +1 -1
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/__init__.py +1 -1
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/review/cli.py +21 -6
- crewforge-0.3.0/src/crewforge/recipes/review/report.py +191 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/review/sources.py +33 -3
- {crewforge-0.2.2 → crewforge-0.3.0/src/crewforge.egg-info}/PKG-INFO +30 -2
- crewforge-0.2.2/src/crewforge/recipes/review/report.py +0 -103
- {crewforge-0.2.2 → crewforge-0.3.0}/LICENSE +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/setup.cfg +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/__main__.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/cli.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/cli_support.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/core/__init__.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/core/agent.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/core/client.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/core/config.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/core/crew.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/core/errors.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/core/parsing.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/core/providers.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/core/usage.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/py.typed +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/__init__.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/academic/__init__.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/academic/agents.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/academic/citations.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/academic/cli.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/academic/disclosure.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/academic/language.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/academic/locales/en.yaml +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/academic/locales/ms.yaml +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/academic/pipeline.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/academic/report.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/academic/settings.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/api/__init__.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/api/agents.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/api/analytics.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/api/appcheck.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/api/cli.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/api/formatters.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/api/generator.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/api/pipeline.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/api/scaffold.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/api/templates/docker-compose.j2 +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/api/templates/dockerfile.j2 +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/api/templates/env.example.j2 +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/api/templates/fastapi_main.j2 +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/api/templates/model.j2 +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/api/templates/requirements.j2 +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/api/templates/route.j2 +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/api/templates/test.j2 +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/api/utils.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/content/__init__.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/content/agents.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/content/cli.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/content/export.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/content/pipeline.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/content/settings.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/content/text.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/docs/__init__.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/docs/agents.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/docs/cli.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/docs/export.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/docs/index.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/docs/ingest.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/docs/pipeline.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/docs/retrieve.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/docs/settings.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/review/__init__.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/review/agents.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge/recipes/review/pipeline.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge.egg-info/SOURCES.txt +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge.egg-info/dependency_links.txt +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge.egg-info/entry_points.txt +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge.egg-info/requires.txt +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/src/crewforge.egg-info/top_level.txt +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/tests/test_honest_failure_e2e.py +0 -0
- {crewforge-0.2.2 → crewforge-0.3.0}/tests/test_readme.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crewforge
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: One crew of LLM agents, five recipes: code review, document Q&A, API scaffolding, content and honest academic-writing help — on any OpenAI-compatible provider.
|
|
5
5
|
Author: Muhammad Aiman
|
|
6
6
|
License-Expression: MIT
|
|
@@ -145,7 +145,7 @@ tables are trimmed to their totals.
|
|
|
145
145
|
|
|
146
146
|
| Command | Agents | What you get |
|
|
147
147
|
|---|---|---|
|
|
148
|
-
| `review PATH` | security, style, complexity, logic, testing, documentation (parallel) → summary | Findings by severity, a decision and a score; `-f json` for CI |
|
|
148
|
+
| `review PATH` | security, style, complexity, logic, testing, documentation (parallel) → summary | Findings by severity, a decision and a score; `-f json` for CI, `-f sarif` for GitHub code scanning |
|
|
149
149
|
| `ingest FILES` | ingester, indexer (no LLM) | How each file was parsed and chunked |
|
|
150
150
|
| `ask FILES -q Q [--verify] [--rerank]` | retriever → answerer → fact-checker | An answer with `[Source N]` citations and a claim-by-claim verdict |
|
|
151
151
|
| `summarize FILES` | summarizer | Summary and key points per document |
|
|
@@ -177,6 +177,30 @@ $ crewforge review app.py -f markdown -o review.md # app.py builds SQL with
|
|
|
177
177
|
total 7 calls 22,366 tokens (provider-reported)
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
+
`-f sarif` writes a [Static Analysis Results Interchange Format 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html)
|
|
181
|
+
log (`sarif`), the format GitHub code scanning reads, so findings can appear in a repository's Security tab. Each finding becomes one
|
|
182
|
+
result: `critical` and `error` map to level `error`, `warning` to `warning`, `info` to `note`; the rule is the
|
|
183
|
+
finding's CWE when it has one, otherwise the reviewer's name. When you review a directory, the line a reviewer
|
|
184
|
+
reports is mapped back to the file it came from. A finding whose line is missing or outside the code points at the
|
|
185
|
+
reviewed path without a line. Reviewers that fail are listed in `invocations`, and the exit code is the same as for
|
|
186
|
+
the other formats.
|
|
187
|
+
|
|
188
|
+
```yaml
|
|
189
|
+
# .github/workflows/review.yml (needs an API key secret, e.g. OPENAI_API_KEY)
|
|
190
|
+
permissions:
|
|
191
|
+
contents: read
|
|
192
|
+
security-events: write
|
|
193
|
+
steps:
|
|
194
|
+
- uses: actions/checkout@v4
|
|
195
|
+
- run: pipx run crewforge review src/ -f sarif -o crewforge.sarif
|
|
196
|
+
env:
|
|
197
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
198
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
199
|
+
if: always()
|
|
200
|
+
with:
|
|
201
|
+
sarif_file: crewforge.sarif
|
|
202
|
+
```
|
|
203
|
+
|
|
180
204
|
### `ask` and `summarize`
|
|
181
205
|
|
|
182
206
|
<p align="center">
|
|
@@ -473,9 +497,13 @@ pip install crewforge # Python 3.10 hingga 3.14
|
|
|
473
497
|
pip install "crewforge[docs,api]" # pilihan: PDF/DOCX dan semakan aplikasi FastAPI
|
|
474
498
|
export OPENAI_API_KEY=sk-... # atau penyedia lain, lihat `crewforge providers`
|
|
475
499
|
crewforge review src/
|
|
500
|
+
crewforge review src/ -f sarif -o crewforge.sarif # untuk GitHub code scanning
|
|
476
501
|
crewforge academic feedback draf.md --brief taklimat.md --language ms
|
|
477
502
|
```
|
|
478
503
|
|
|
504
|
+
`review -f sarif` menulis laporan `SARIF` 2.1.0 yang dibaca GitHub code scanning: setiap dapatan menjadi satu
|
|
505
|
+
keputusan dalam tab Security, dan baris dalam semakan folder dipetakan kembali ke fail asalnya.
|
|
506
|
+
|
|
479
507
|
Senarai penyedia, contoh setiap resipi dan seni bina ada dalam bahagian bahasa Inggeris di atas
|
|
480
508
|
([Providers](https://github.com/aimanmalib/crewforge#providers), [Recipes](https://github.com/aimanmalib/crewforge#recipes)).
|
|
481
509
|
|
|
@@ -87,7 +87,7 @@ tables are trimmed to their totals.
|
|
|
87
87
|
|
|
88
88
|
| Command | Agents | What you get |
|
|
89
89
|
|---|---|---|
|
|
90
|
-
| `review PATH` | security, style, complexity, logic, testing, documentation (parallel) → summary | Findings by severity, a decision and a score; `-f json` for CI |
|
|
90
|
+
| `review PATH` | security, style, complexity, logic, testing, documentation (parallel) → summary | Findings by severity, a decision and a score; `-f json` for CI, `-f sarif` for GitHub code scanning |
|
|
91
91
|
| `ingest FILES` | ingester, indexer (no LLM) | How each file was parsed and chunked |
|
|
92
92
|
| `ask FILES -q Q [--verify] [--rerank]` | retriever → answerer → fact-checker | An answer with `[Source N]` citations and a claim-by-claim verdict |
|
|
93
93
|
| `summarize FILES` | summarizer | Summary and key points per document |
|
|
@@ -119,6 +119,30 @@ $ crewforge review app.py -f markdown -o review.md # app.py builds SQL with
|
|
|
119
119
|
total 7 calls 22,366 tokens (provider-reported)
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
+
`-f sarif` writes a [Static Analysis Results Interchange Format 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html)
|
|
123
|
+
log (`sarif`), the format GitHub code scanning reads, so findings can appear in a repository's Security tab. Each finding becomes one
|
|
124
|
+
result: `critical` and `error` map to level `error`, `warning` to `warning`, `info` to `note`; the rule is the
|
|
125
|
+
finding's CWE when it has one, otherwise the reviewer's name. When you review a directory, the line a reviewer
|
|
126
|
+
reports is mapped back to the file it came from. A finding whose line is missing or outside the code points at the
|
|
127
|
+
reviewed path without a line. Reviewers that fail are listed in `invocations`, and the exit code is the same as for
|
|
128
|
+
the other formats.
|
|
129
|
+
|
|
130
|
+
```yaml
|
|
131
|
+
# .github/workflows/review.yml (needs an API key secret, e.g. OPENAI_API_KEY)
|
|
132
|
+
permissions:
|
|
133
|
+
contents: read
|
|
134
|
+
security-events: write
|
|
135
|
+
steps:
|
|
136
|
+
- uses: actions/checkout@v4
|
|
137
|
+
- run: pipx run crewforge review src/ -f sarif -o crewforge.sarif
|
|
138
|
+
env:
|
|
139
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
140
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
141
|
+
if: always()
|
|
142
|
+
with:
|
|
143
|
+
sarif_file: crewforge.sarif
|
|
144
|
+
```
|
|
145
|
+
|
|
122
146
|
### `ask` and `summarize`
|
|
123
147
|
|
|
124
148
|
<p align="center">
|
|
@@ -415,9 +439,13 @@ pip install crewforge # Python 3.10 hingga 3.14
|
|
|
415
439
|
pip install "crewforge[docs,api]" # pilihan: PDF/DOCX dan semakan aplikasi FastAPI
|
|
416
440
|
export OPENAI_API_KEY=sk-... # atau penyedia lain, lihat `crewforge providers`
|
|
417
441
|
crewforge review src/
|
|
442
|
+
crewforge review src/ -f sarif -o crewforge.sarif # untuk GitHub code scanning
|
|
418
443
|
crewforge academic feedback draf.md --brief taklimat.md --language ms
|
|
419
444
|
```
|
|
420
445
|
|
|
446
|
+
`review -f sarif` menulis laporan `SARIF` 2.1.0 yang dibaca GitHub code scanning: setiap dapatan menjadi satu
|
|
447
|
+
keputusan dalam tab Security, dan baris dalam semakan folder dipetakan kembali ke fail asalnya.
|
|
448
|
+
|
|
421
449
|
Senarai penyedia, contoh setiap resipi dan seni bina ada dalam bahagian bahasa Inggeris di atas
|
|
422
450
|
([Providers](https://github.com/aimanmalib/crewforge#providers), [Recipes](https://github.com/aimanmalib/crewforge#recipes)).
|
|
423
451
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "crewforge"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "One crew of LLM agents, five recipes: code review, document Q&A, API scaffolding, content and honest academic-writing help — on any OpenAI-compatible provider."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -22,8 +22,14 @@ from ...cli_support import (
|
|
|
22
22
|
from ...core.config import LLMConfig
|
|
23
23
|
from .agents import REVIEW_AGENTS
|
|
24
24
|
from .pipeline import CodeReviewCrew, ReviewResult
|
|
25
|
-
from .report import report_dict, report_markdown
|
|
26
|
-
from .sources import
|
|
25
|
+
from .report import report_dict, report_markdown, report_sarif
|
|
26
|
+
from .sources import (
|
|
27
|
+
DEFAULT_MAX_FILE_BYTES,
|
|
28
|
+
SourceError,
|
|
29
|
+
Sources,
|
|
30
|
+
collect,
|
|
31
|
+
detect_language,
|
|
32
|
+
)
|
|
27
33
|
|
|
28
34
|
# Kept under the old private name: codeguard's tests and users imported it.
|
|
29
35
|
_detect_language = detect_language
|
|
@@ -60,7 +66,8 @@ async def _run_review(
|
|
|
60
66
|
"fmt",
|
|
61
67
|
default="text",
|
|
62
68
|
show_default=True,
|
|
63
|
-
type=click.Choice(["text", "json", "markdown"]),
|
|
69
|
+
type=click.Choice(["text", "json", "markdown", "sarif"]),
|
|
70
|
+
help="sarif = SARIF 2.1.0 for GitHub code scanning.",
|
|
64
71
|
)
|
|
65
72
|
@click.option("--output", "-o", default="", help="Write the report to this file.")
|
|
66
73
|
@click.option(
|
|
@@ -77,6 +84,7 @@ def review(ctx, path, path_opt, language, fmt, output, allow_partial):
|
|
|
77
84
|
Example:
|
|
78
85
|
crewforge review app.py
|
|
79
86
|
crewforge review src/ -f json -o review.json
|
|
87
|
+
crewforge review src/ -f sarif -o review.sarif
|
|
80
88
|
"""
|
|
81
89
|
target = path or path_opt
|
|
82
90
|
if not target:
|
|
@@ -116,11 +124,18 @@ def review(ctx, path, path_opt, language, fmt, output, allow_partial):
|
|
|
116
124
|
allow_partial=allow_partial,
|
|
117
125
|
)
|
|
118
126
|
)
|
|
119
|
-
_display_results(result, fmt, output)
|
|
127
|
+
_display_results(result, fmt, output, sources, target)
|
|
120
128
|
|
|
121
129
|
|
|
122
|
-
def _display_results(
|
|
123
|
-
|
|
130
|
+
def _display_results(
|
|
131
|
+
result: ReviewResult, fmt: str, output: str, sources: Sources, target: str
|
|
132
|
+
) -> None:
|
|
133
|
+
if fmt == "json":
|
|
134
|
+
text = json.dumps(report_dict(result), indent=2)
|
|
135
|
+
elif fmt == "sarif":
|
|
136
|
+
text = json.dumps(report_sarif(result, sources, target), indent=2)
|
|
137
|
+
else:
|
|
138
|
+
text = report_markdown(result)
|
|
124
139
|
if output:
|
|
125
140
|
Path(output).write_text(text, encoding="utf-8")
|
|
126
141
|
err_console.print(f"[green]Report written to {output}[/]")
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Render a :class:`ReviewResult` as JSON, SARIF or Markdown."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ... import __version__
|
|
9
|
+
from .agents import SEVERITIES
|
|
10
|
+
from .pipeline import ReviewResult
|
|
11
|
+
from .sources import Sources
|
|
12
|
+
|
|
13
|
+
SARIF_SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json"
|
|
14
|
+
#: Finding severity -> SARIF ``level`` (GitHub code scanning shows error/warning/note).
|
|
15
|
+
SARIF_LEVELS = {"critical": "error", "error": "error", "warning": "warning", "info": "note"}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def report_dict(result: ReviewResult) -> dict[str, Any]:
|
|
19
|
+
"""Machine-readable report: decision, per-agent findings/status, usage."""
|
|
20
|
+
summary = result.summary_result
|
|
21
|
+
return {
|
|
22
|
+
"status": result.status,
|
|
23
|
+
"language": result.language or "auto",
|
|
24
|
+
"decision": (summary.data.get("decision") if summary else None),
|
|
25
|
+
"quality_score": (summary.data.get("quality_score") if summary else None),
|
|
26
|
+
"total_findings": result.total_findings,
|
|
27
|
+
"total_tokens": result.total_tokens,
|
|
28
|
+
"duration_s": round(result.duration_s, 2),
|
|
29
|
+
"agents": {
|
|
30
|
+
name: {
|
|
31
|
+
"status": r.status,
|
|
32
|
+
"summary": r.summary,
|
|
33
|
+
"findings": r.findings,
|
|
34
|
+
"tokens": r.tokens_used,
|
|
35
|
+
"error": r.error,
|
|
36
|
+
}
|
|
37
|
+
for name, r in result.agent_results.items()
|
|
38
|
+
},
|
|
39
|
+
"summary": (
|
|
40
|
+
{
|
|
41
|
+
"status": summary.status,
|
|
42
|
+
"report": summary.summary,
|
|
43
|
+
"top_issues": summary.data.get("top_issues", []),
|
|
44
|
+
"error": summary.error,
|
|
45
|
+
}
|
|
46
|
+
if summary
|
|
47
|
+
else None
|
|
48
|
+
),
|
|
49
|
+
"usage": result.usage,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _finding_line(agent: str, f: dict[str, Any]) -> str:
|
|
54
|
+
where = f" (line {f['line']})" if f.get("line") not in (None, "") else ""
|
|
55
|
+
cwe = f" [{f['cwe']}]" if f.get("cwe") else ""
|
|
56
|
+
text = f"- **{f.get('title') or 'Untitled'}**{where}{cwe} — _{agent}_"
|
|
57
|
+
if f.get("description"):
|
|
58
|
+
text += f": {f['description']}"
|
|
59
|
+
if f.get("suggestion"):
|
|
60
|
+
text += f"\n - Fix: {f['suggestion']}"
|
|
61
|
+
return text
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def report_markdown(result: ReviewResult) -> str:
|
|
65
|
+
"""Human-readable Markdown report."""
|
|
66
|
+
lines = ["# crewforge review", ""]
|
|
67
|
+
summary = result.summary_result
|
|
68
|
+
if summary and summary.ok:
|
|
69
|
+
lines += [
|
|
70
|
+
f"**Decision:** {summary.data.get('decision')} · "
|
|
71
|
+
f"**Quality score:** {summary.data.get('quality_score')}/100",
|
|
72
|
+
"",
|
|
73
|
+
]
|
|
74
|
+
if summary.summary.strip():
|
|
75
|
+
lines += ["## Summary", "", summary.summary.strip(), ""]
|
|
76
|
+
top = summary.data.get("top_issues") or []
|
|
77
|
+
if top:
|
|
78
|
+
lines += ["### Top issues", ""] + [f"{i}. {t}" for i, t in enumerate(top, 1)] + [""]
|
|
79
|
+
elif summary:
|
|
80
|
+
lines += ["## Summary", "", f"_Summary agent failed: {summary.error}_", ""]
|
|
81
|
+
|
|
82
|
+
lines += ["## Findings", ""]
|
|
83
|
+
any_finding = False
|
|
84
|
+
for severity in SEVERITIES:
|
|
85
|
+
rows = [
|
|
86
|
+
_finding_line(name, f)
|
|
87
|
+
for name, r in result.agent_results.items()
|
|
88
|
+
for f in r.findings
|
|
89
|
+
if f.get("severity") == severity
|
|
90
|
+
]
|
|
91
|
+
if rows:
|
|
92
|
+
any_finding = True
|
|
93
|
+
lines += [f"### {severity.capitalize()} ({len(rows)})", ""] + rows + [""]
|
|
94
|
+
if not any_finding:
|
|
95
|
+
lines += ["_No findings reported._", ""]
|
|
96
|
+
|
|
97
|
+
lines += ["## Reviewers", ""]
|
|
98
|
+
for name, r in result.agent_results.items():
|
|
99
|
+
if r.ok:
|
|
100
|
+
note = r.summary.strip() or "no summary"
|
|
101
|
+
lines.append(f"- **{name}** ({r.finding_count} finding(s)): {note}")
|
|
102
|
+
else:
|
|
103
|
+
lines.append(f"- **{name}**: _Agent failed: {r.error}_")
|
|
104
|
+
lines.append("")
|
|
105
|
+
agents_run = len(result.agent_results) + (1 if summary else 0)
|
|
106
|
+
lines.append(
|
|
107
|
+
f"_Tokens: {result.total_tokens:,} (provider-reported) · "
|
|
108
|
+
f"Duration: {result.duration_s:.1f}s · Agents: {agents_run}_"
|
|
109
|
+
)
|
|
110
|
+
return "\n".join(lines) + "\n"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _uri(path: Path) -> str:
|
|
114
|
+
"""Artifact URI: relative paths stay relative (GitHub resolves them from the repo root)."""
|
|
115
|
+
return path.as_uri() if path.is_absolute() else path.as_posix()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _int_line(value: Any) -> int | None:
|
|
119
|
+
if isinstance(value, bool):
|
|
120
|
+
return None
|
|
121
|
+
if isinstance(value, int):
|
|
122
|
+
return value if value > 0 else None
|
|
123
|
+
if isinstance(value, str) and value.strip().isdigit():
|
|
124
|
+
return int(value) or None
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def report_sarif(result: ReviewResult, sources: Sources, target: str | Path) -> dict[str, Any]:
|
|
129
|
+
"""SARIF 2.1.0 log for GitHub code scanning: one ``result`` per finding.
|
|
130
|
+
|
|
131
|
+
A finding's line refers to the text the reviewers saw. For a directory that
|
|
132
|
+
is every file joined under ``# File:`` headers, so the line is mapped back to
|
|
133
|
+
its file; a line that cannot be mapped keeps the reviewed path, without a region.
|
|
134
|
+
"""
|
|
135
|
+
results: list[dict[str, Any]] = []
|
|
136
|
+
rules: dict[str, dict[str, Any]] = {}
|
|
137
|
+
for agent, r in result.agent_results.items():
|
|
138
|
+
for f in r.findings:
|
|
139
|
+
rule_id = str(f.get("cwe") or agent)
|
|
140
|
+
rules.setdefault(rule_id, {"id": rule_id})
|
|
141
|
+
title = str(f.get("title") or "Untitled")
|
|
142
|
+
message = f"{title}: {f['description']}" if f.get("description") else title
|
|
143
|
+
if f.get("suggestion"):
|
|
144
|
+
message += f"\nFix: {f['suggestion']}"
|
|
145
|
+
physical: dict[str, Any] = {"artifactLocation": {"uri": _uri(Path(target))}}
|
|
146
|
+
line = _int_line(f.get("line"))
|
|
147
|
+
spot = sources.locate(line) if line is not None else None
|
|
148
|
+
if spot is not None:
|
|
149
|
+
physical = {
|
|
150
|
+
"artifactLocation": {"uri": _uri(spot[0])},
|
|
151
|
+
"region": {"startLine": spot[1]},
|
|
152
|
+
}
|
|
153
|
+
results.append(
|
|
154
|
+
{
|
|
155
|
+
"ruleId": rule_id,
|
|
156
|
+
"level": SARIF_LEVELS.get(str(f.get("severity")), "note"),
|
|
157
|
+
"message": {"text": message},
|
|
158
|
+
"locations": [{"physicalLocation": physical}],
|
|
159
|
+
"properties": {"agent": agent, "severity": f.get("severity", "info")},
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
return {
|
|
163
|
+
"$schema": SARIF_SCHEMA,
|
|
164
|
+
"version": "2.1.0",
|
|
165
|
+
"runs": [
|
|
166
|
+
{
|
|
167
|
+
"tool": {
|
|
168
|
+
"driver": {
|
|
169
|
+
"name": "crewforge",
|
|
170
|
+
"version": __version__,
|
|
171
|
+
"informationUri": "https://github.com/aimanmalib/crewforge",
|
|
172
|
+
"rules": list(rules.values()),
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
"invocations": [
|
|
176
|
+
{
|
|
177
|
+
"executionSuccessful": not result.failed_agents,
|
|
178
|
+
"toolExecutionNotifications": [
|
|
179
|
+
{
|
|
180
|
+
"level": "error",
|
|
181
|
+
"message": {"text": f"agent '{name}' failed: {r.error}"},
|
|
182
|
+
}
|
|
183
|
+
for name, r in result.agent_results.items()
|
|
184
|
+
if not r.ok
|
|
185
|
+
],
|
|
186
|
+
}
|
|
187
|
+
],
|
|
188
|
+
"results": results,
|
|
189
|
+
}
|
|
190
|
+
],
|
|
191
|
+
}
|
|
@@ -52,6 +52,19 @@ class Sources:
|
|
|
52
52
|
language: str
|
|
53
53
|
files: list[Path] = field(default_factory=list)
|
|
54
54
|
skipped: list[str] = field(default_factory=list)
|
|
55
|
+
#: ``(file, first, last)``: lines ``first..last`` of :attr:`code` hold line 1.. of ``file``.
|
|
56
|
+
spans: list[tuple[Path, int, int]] = field(default_factory=list)
|
|
57
|
+
|
|
58
|
+
def locate(self, line: int) -> tuple[Path, int] | None:
|
|
59
|
+
"""Map a line of :attr:`code` to ``(file, line in that file)``.
|
|
60
|
+
|
|
61
|
+
``None`` when the line is outside the code or falls on a ``# File:``
|
|
62
|
+
header or the blank lines between two files.
|
|
63
|
+
"""
|
|
64
|
+
for path, first, last in self.spans:
|
|
65
|
+
if first <= line <= last:
|
|
66
|
+
return path, line - first + 1
|
|
67
|
+
return None
|
|
55
68
|
|
|
56
69
|
|
|
57
70
|
def collect(path: str | Path, *, max_file_bytes: int = DEFAULT_MAX_FILE_BYTES) -> Sources:
|
|
@@ -60,13 +73,20 @@ def collect(path: str | Path, *, max_file_bytes: int = DEFAULT_MAX_FILE_BYTES) -
|
|
|
60
73
|
code = target.read_text(encoding="utf-8", errors="replace")
|
|
61
74
|
if not code.strip():
|
|
62
75
|
raise SourceError(f"{target} is empty")
|
|
63
|
-
return Sources(
|
|
76
|
+
return Sources(
|
|
77
|
+
code=code,
|
|
78
|
+
language=detect_language(target.suffix),
|
|
79
|
+
files=[target],
|
|
80
|
+
spans=[(target, 1, _line_count(code))],
|
|
81
|
+
)
|
|
64
82
|
if not target.is_dir():
|
|
65
83
|
raise SourceError(f"path not found: {target}")
|
|
66
84
|
|
|
67
85
|
parts: list[str] = []
|
|
68
86
|
files: list[Path] = []
|
|
69
87
|
skipped: list[str] = []
|
|
88
|
+
spans: list[tuple[Path, int, int]] = []
|
|
89
|
+
start = 1 # line of ``code`` where the next "# File:" header goes
|
|
70
90
|
languages: set[str] = set()
|
|
71
91
|
for f in sorted(target.rglob("*")):
|
|
72
92
|
rel = f.relative_to(target)
|
|
@@ -80,10 +100,20 @@ def collect(path: str | Path, *, max_file_bytes: int = DEFAULT_MAX_FILE_BYTES) -
|
|
|
80
100
|
text = f.read_text(encoding="utf-8", errors="replace")
|
|
81
101
|
if not text.strip():
|
|
82
102
|
continue
|
|
83
|
-
|
|
103
|
+
part = f"# File: {rel}\n{text}"
|
|
104
|
+
parts.append(part)
|
|
84
105
|
files.append(f)
|
|
106
|
+
spans.append((f, start + 1, start + _line_count(text)))
|
|
107
|
+
start += part.count("\n") + 2 # the part, then the blank line joining the next one
|
|
85
108
|
languages.add(detect_language(f.suffix))
|
|
86
109
|
if not parts:
|
|
87
110
|
raise SourceError(f"no supported source files found under {target}")
|
|
88
111
|
language = languages.pop() if len(languages) == 1 else ""
|
|
89
|
-
return Sources(
|
|
112
|
+
return Sources(
|
|
113
|
+
code="\n\n".join(parts), language=language, files=files, skipped=skipped, spans=spans
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _line_count(text: str) -> int:
|
|
118
|
+
"""Lines in ``text`` as an editor numbers them (a final newline adds none)."""
|
|
119
|
+
return text.count("\n") + (0 if text.endswith("\n") else 1)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crewforge
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: One crew of LLM agents, five recipes: code review, document Q&A, API scaffolding, content and honest academic-writing help — on any OpenAI-compatible provider.
|
|
5
5
|
Author: Muhammad Aiman
|
|
6
6
|
License-Expression: MIT
|
|
@@ -145,7 +145,7 @@ tables are trimmed to their totals.
|
|
|
145
145
|
|
|
146
146
|
| Command | Agents | What you get |
|
|
147
147
|
|---|---|---|
|
|
148
|
-
| `review PATH` | security, style, complexity, logic, testing, documentation (parallel) → summary | Findings by severity, a decision and a score; `-f json` for CI |
|
|
148
|
+
| `review PATH` | security, style, complexity, logic, testing, documentation (parallel) → summary | Findings by severity, a decision and a score; `-f json` for CI, `-f sarif` for GitHub code scanning |
|
|
149
149
|
| `ingest FILES` | ingester, indexer (no LLM) | How each file was parsed and chunked |
|
|
150
150
|
| `ask FILES -q Q [--verify] [--rerank]` | retriever → answerer → fact-checker | An answer with `[Source N]` citations and a claim-by-claim verdict |
|
|
151
151
|
| `summarize FILES` | summarizer | Summary and key points per document |
|
|
@@ -177,6 +177,30 @@ $ crewforge review app.py -f markdown -o review.md # app.py builds SQL with
|
|
|
177
177
|
total 7 calls 22,366 tokens (provider-reported)
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
+
`-f sarif` writes a [Static Analysis Results Interchange Format 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html)
|
|
181
|
+
log (`sarif`), the format GitHub code scanning reads, so findings can appear in a repository's Security tab. Each finding becomes one
|
|
182
|
+
result: `critical` and `error` map to level `error`, `warning` to `warning`, `info` to `note`; the rule is the
|
|
183
|
+
finding's CWE when it has one, otherwise the reviewer's name. When you review a directory, the line a reviewer
|
|
184
|
+
reports is mapped back to the file it came from. A finding whose line is missing or outside the code points at the
|
|
185
|
+
reviewed path without a line. Reviewers that fail are listed in `invocations`, and the exit code is the same as for
|
|
186
|
+
the other formats.
|
|
187
|
+
|
|
188
|
+
```yaml
|
|
189
|
+
# .github/workflows/review.yml (needs an API key secret, e.g. OPENAI_API_KEY)
|
|
190
|
+
permissions:
|
|
191
|
+
contents: read
|
|
192
|
+
security-events: write
|
|
193
|
+
steps:
|
|
194
|
+
- uses: actions/checkout@v4
|
|
195
|
+
- run: pipx run crewforge review src/ -f sarif -o crewforge.sarif
|
|
196
|
+
env:
|
|
197
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
198
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
199
|
+
if: always()
|
|
200
|
+
with:
|
|
201
|
+
sarif_file: crewforge.sarif
|
|
202
|
+
```
|
|
203
|
+
|
|
180
204
|
### `ask` and `summarize`
|
|
181
205
|
|
|
182
206
|
<p align="center">
|
|
@@ -473,9 +497,13 @@ pip install crewforge # Python 3.10 hingga 3.14
|
|
|
473
497
|
pip install "crewforge[docs,api]" # pilihan: PDF/DOCX dan semakan aplikasi FastAPI
|
|
474
498
|
export OPENAI_API_KEY=sk-... # atau penyedia lain, lihat `crewforge providers`
|
|
475
499
|
crewforge review src/
|
|
500
|
+
crewforge review src/ -f sarif -o crewforge.sarif # untuk GitHub code scanning
|
|
476
501
|
crewforge academic feedback draf.md --brief taklimat.md --language ms
|
|
477
502
|
```
|
|
478
503
|
|
|
504
|
+
`review -f sarif` menulis laporan `SARIF` 2.1.0 yang dibaca GitHub code scanning: setiap dapatan menjadi satu
|
|
505
|
+
keputusan dalam tab Security, dan baris dalam semakan folder dipetakan kembali ke fail asalnya.
|
|
506
|
+
|
|
479
507
|
Senarai penyedia, contoh setiap resipi dan seni bina ada dalam bahagian bahasa Inggeris di atas
|
|
480
508
|
([Providers](https://github.com/aimanmalib/crewforge#providers), [Recipes](https://github.com/aimanmalib/crewforge#recipes)).
|
|
481
509
|
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
"""Render a :class:`ReviewResult` as JSON or Markdown."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
from .agents import SEVERITIES
|
|
8
|
-
from .pipeline import ReviewResult
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def report_dict(result: ReviewResult) -> dict[str, Any]:
|
|
12
|
-
"""Machine-readable report: decision, per-agent findings/status, usage."""
|
|
13
|
-
summary = result.summary_result
|
|
14
|
-
return {
|
|
15
|
-
"status": result.status,
|
|
16
|
-
"language": result.language or "auto",
|
|
17
|
-
"decision": (summary.data.get("decision") if summary else None),
|
|
18
|
-
"quality_score": (summary.data.get("quality_score") if summary else None),
|
|
19
|
-
"total_findings": result.total_findings,
|
|
20
|
-
"total_tokens": result.total_tokens,
|
|
21
|
-
"duration_s": round(result.duration_s, 2),
|
|
22
|
-
"agents": {
|
|
23
|
-
name: {
|
|
24
|
-
"status": r.status,
|
|
25
|
-
"summary": r.summary,
|
|
26
|
-
"findings": r.findings,
|
|
27
|
-
"tokens": r.tokens_used,
|
|
28
|
-
"error": r.error,
|
|
29
|
-
}
|
|
30
|
-
for name, r in result.agent_results.items()
|
|
31
|
-
},
|
|
32
|
-
"summary": (
|
|
33
|
-
{
|
|
34
|
-
"status": summary.status,
|
|
35
|
-
"report": summary.summary,
|
|
36
|
-
"top_issues": summary.data.get("top_issues", []),
|
|
37
|
-
"error": summary.error,
|
|
38
|
-
}
|
|
39
|
-
if summary
|
|
40
|
-
else None
|
|
41
|
-
),
|
|
42
|
-
"usage": result.usage,
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _finding_line(agent: str, f: dict[str, Any]) -> str:
|
|
47
|
-
where = f" (line {f['line']})" if f.get("line") not in (None, "") else ""
|
|
48
|
-
cwe = f" [{f['cwe']}]" if f.get("cwe") else ""
|
|
49
|
-
text = f"- **{f.get('title') or 'Untitled'}**{where}{cwe} — _{agent}_"
|
|
50
|
-
if f.get("description"):
|
|
51
|
-
text += f": {f['description']}"
|
|
52
|
-
if f.get("suggestion"):
|
|
53
|
-
text += f"\n - Fix: {f['suggestion']}"
|
|
54
|
-
return text
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def report_markdown(result: ReviewResult) -> str:
|
|
58
|
-
"""Human-readable Markdown report."""
|
|
59
|
-
lines = ["# crewforge review", ""]
|
|
60
|
-
summary = result.summary_result
|
|
61
|
-
if summary and summary.ok:
|
|
62
|
-
lines += [
|
|
63
|
-
f"**Decision:** {summary.data.get('decision')} · "
|
|
64
|
-
f"**Quality score:** {summary.data.get('quality_score')}/100",
|
|
65
|
-
"",
|
|
66
|
-
]
|
|
67
|
-
if summary.summary.strip():
|
|
68
|
-
lines += ["## Summary", "", summary.summary.strip(), ""]
|
|
69
|
-
top = summary.data.get("top_issues") or []
|
|
70
|
-
if top:
|
|
71
|
-
lines += ["### Top issues", ""] + [f"{i}. {t}" for i, t in enumerate(top, 1)] + [""]
|
|
72
|
-
elif summary:
|
|
73
|
-
lines += ["## Summary", "", f"_Summary agent failed: {summary.error}_", ""]
|
|
74
|
-
|
|
75
|
-
lines += ["## Findings", ""]
|
|
76
|
-
any_finding = False
|
|
77
|
-
for severity in SEVERITIES:
|
|
78
|
-
rows = [
|
|
79
|
-
_finding_line(name, f)
|
|
80
|
-
for name, r in result.agent_results.items()
|
|
81
|
-
for f in r.findings
|
|
82
|
-
if f.get("severity") == severity
|
|
83
|
-
]
|
|
84
|
-
if rows:
|
|
85
|
-
any_finding = True
|
|
86
|
-
lines += [f"### {severity.capitalize()} ({len(rows)})", ""] + rows + [""]
|
|
87
|
-
if not any_finding:
|
|
88
|
-
lines += ["_No findings reported._", ""]
|
|
89
|
-
|
|
90
|
-
lines += ["## Reviewers", ""]
|
|
91
|
-
for name, r in result.agent_results.items():
|
|
92
|
-
if r.ok:
|
|
93
|
-
note = r.summary.strip() or "no summary"
|
|
94
|
-
lines.append(f"- **{name}** ({r.finding_count} finding(s)): {note}")
|
|
95
|
-
else:
|
|
96
|
-
lines.append(f"- **{name}**: _Agent failed: {r.error}_")
|
|
97
|
-
lines.append("")
|
|
98
|
-
agents_run = len(result.agent_results) + (1 if summary else 0)
|
|
99
|
-
lines.append(
|
|
100
|
-
f"_Tokens: {result.total_tokens:,} (provider-reported) · "
|
|
101
|
-
f"Duration: {result.duration_s:.1f}s · Agents: {agents_run}_"
|
|
102
|
-
)
|
|
103
|
-
return "\n".join(lines) + "\n"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|