genesis-cli 0.1.0__py3-none-any.whl
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.
- genesis_cli/README.md +172 -0
- genesis_cli/__init__.py +0 -0
- genesis_cli/adopt.py +75 -0
- genesis_cli/agents_md.py +76 -0
- genesis_cli/baseline.py +114 -0
- genesis_cli/gates.py +50 -0
- genesis_cli/init.py +158 -0
- genesis_cli/main.py +228 -0
- genesis_cli/report.py +251 -0
- genesis_cli/review.py +151 -0
- genesis_cli/sarif.py +137 -0
- genesis_cli-0.1.0.dist-info/METADATA +198 -0
- genesis_cli-0.1.0.dist-info/RECORD +16 -0
- genesis_cli-0.1.0.dist-info/WHEEL +4 -0
- genesis_cli-0.1.0.dist-info/entry_points.txt +2 -0
- genesis_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
genesis_cli/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# genesis-cli — the Genesis Agent Workbench CLI
|
|
2
|
+
|
|
3
|
+
Make any repository agent-ready, and keep AI-generated changes governed.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install genesis-cli # or: uv tool install genesis-cli
|
|
9
|
+
genesis --version
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Python 3.12+. `genesis-gates` (the engine) comes along as a dependency; there is
|
|
13
|
+
nothing else to install and no API key to configure.
|
|
14
|
+
|
|
15
|
+
> **Status:** PyPI publication happens at the first tagged release (`v0.1.0`),
|
|
16
|
+
> driven by `.github/workflows/release.yml` in the
|
|
17
|
+
> [genesis-framework](https://github.com/Inceptium-ai/genesis-framework)
|
|
18
|
+
> repository. Until that tag lands, install from source:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# from a clone of the monorepo / framework repo
|
|
22
|
+
uv sync && uv run genesis adopt .
|
|
23
|
+
# or, without cloning:
|
|
24
|
+
pip install "git+https://github.com/Inceptium-ai/genesis-framework#subdirectory=packages/genesis-cli"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
genesis --version # installed version
|
|
31
|
+
genesis adopt [path] # audit a repo: score, gaps, AGENTS.md draft
|
|
32
|
+
genesis adopt . --write # also install AGENTS.md into the repo root
|
|
33
|
+
genesis adopt . --review # add the AI second-reader (see below)
|
|
34
|
+
genesis adopt . --min-score 70 # CI mode: exit 1 below the threshold
|
|
35
|
+
genesis adopt . --json # machine-readable report on stdout
|
|
36
|
+
genesis adopt . --baseline prev/report.json --fail-on-regression
|
|
37
|
+
# CI mode: diff against a previous report,
|
|
38
|
+
# exit 1 on regression (see below)
|
|
39
|
+
genesis gates [path] # run just the delivery gates, per-gate table
|
|
40
|
+
genesis init [path] # scaffold a new/bare repo agent-ready (see below)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## What `adopt` does
|
|
44
|
+
|
|
45
|
+
Deterministic, zero API keys, zero infrastructure:
|
|
46
|
+
|
|
47
|
+
1. **Delivery gates** — secret-scan, dependency-scan, unit-tests (in an isolated
|
|
48
|
+
venv with the app's own requirements), SAST, SBOM. Gates skip cleanly when a
|
|
49
|
+
scanner isn't installed.
|
|
50
|
+
2. **System completeness** — detects systems (auth, payments, file uploads)
|
|
51
|
+
and checks their *mandatory flows* against the implementation surface
|
|
52
|
+
(routes, files), not prose. This catches the classic AI-agent failure:
|
|
53
|
+
an app that ships signup with no sign-in — or Stripe checkout with no
|
|
54
|
+
webhook signature verification, uploads with no size limit.
|
|
55
|
+
3. **Env reconciliation** — the compose `${VAR}` contract and direct env reads,
|
|
56
|
+
diffed for unset/undocumented variables.
|
|
57
|
+
4. **Structure** — tests, CI, lockfiles, `.env.example`, agent instructions.
|
|
58
|
+
|
|
59
|
+
Output: a 0–100 **readiness score** with grade, `report.html` (shareable),
|
|
60
|
+
`report.json` (machine-readable), `report.sarif` (SARIF 2.1.0 — feed it to
|
|
61
|
+
GitHub code scanning or any SARIF viewer), and an `AGENTS.md` draft that turns
|
|
62
|
+
the findings into instructions for AI coding agents.
|
|
63
|
+
|
|
64
|
+
## Baseline comparison (`--baseline`, `--fail-on-regression`)
|
|
65
|
+
|
|
66
|
+
Point `--baseline` at a previous run's `report.json` and `adopt` diffs the two
|
|
67
|
+
reports: score delta, grade change, new/resolved gate findings (identity:
|
|
68
|
+
gate + title + location) and new/resolved mandatory-flow gaps (identity:
|
|
69
|
+
profile + flow). The diff prints as a **CHANGES SINCE BASELINE** section and is
|
|
70
|
+
embedded in `report.json` as `baseline_comparison`.
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
CHANGES SINCE BASELINE
|
|
74
|
+
score ▲ +9.2 grade F→D
|
|
75
|
+
findings: 0 new, 0 resolved · gaps: 0 new, 1 resolved
|
|
76
|
+
− closed gap: Implement returning-user sign-in (login)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`--fail-on-regression` (requires `--baseline`) exits 1 when the run
|
|
80
|
+
**regressed**: the score dropped by more than 0.5 points, a new mandatory-flow
|
|
81
|
+
gap appeared, or a new critical/high finding appeared. It composes with
|
|
82
|
+
`--min-score` — either condition fails the run; when both flags pass, the exit
|
|
83
|
+
code is 0 regardless of grade.
|
|
84
|
+
|
|
85
|
+
### Score history (`history.jsonl`)
|
|
86
|
+
|
|
87
|
+
Every `adopt` run appends one line to `<out-dir>/history.jsonl` —
|
|
88
|
+
`{generated_at, score, grade, findings_count, gaps_count}` — append-only, never
|
|
89
|
+
rewritten. With two or more runs the terminal report ends with a trend line:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
history: 44.8 → 49.0 → 75.5 (3 runs)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## `genesis gates` — just the delivery gates
|
|
96
|
+
|
|
97
|
+
`genesis gates [path]` runs the same deterministic gate set as `adopt`
|
|
98
|
+
(secret-scan, unit-tests, dependency-scan, sast, sbom) *without* the full
|
|
99
|
+
analysis — a fast check for "did my fix turn secret-scan green?". It prints a
|
|
100
|
+
per-gate table (name, status, summary) plus the top 5 findings — with location
|
|
101
|
+
and remediation — for each failed gate. Exit code: `0` when every selected
|
|
102
|
+
gate passed or skipped, `1` otherwise.
|
|
103
|
+
|
|
104
|
+
- `--gate NAME` (repeatable) limits the run to specific gates; names are
|
|
105
|
+
validated against the gate registry, and a typo lists the valid names.
|
|
106
|
+
- `--json` prints the full evidence package instead of the table.
|
|
107
|
+
|
|
108
|
+
## `genesis init` — start agent-ready
|
|
109
|
+
|
|
110
|
+
For a new or bare repository, `genesis init [path]` scaffolds the files agents
|
|
111
|
+
need on day one: `AGENTS.md` (from a fresh audit of whatever exists — on an
|
|
112
|
+
empty directory its first task is "establish a test command"), `.gitignore`
|
|
113
|
+
(covers `.env*` and `.genesis-report/`), a `.env.example` stub, a
|
|
114
|
+
`.github/workflows/genesis-audit.yml` self-audit workflow, and a commented
|
|
115
|
+
`.genesis/config.yaml` gate config. It is idempotent and **never overwrites**
|
|
116
|
+
— existing files are reported as "exists, skipped".
|
|
117
|
+
|
|
118
|
+
## The AI second-reader (`--review`)
|
|
119
|
+
|
|
120
|
+
Optional pass that runs through the local `claude` CLI using your existing
|
|
121
|
+
Claude subscription login — still **no API key**. It is **additive-only**: it
|
|
122
|
+
may add findings, annotate (including flagging likely false positives),
|
|
123
|
+
prioritize, and write the executive summary. It can never remove or downgrade a
|
|
124
|
+
deterministic finding, and it never changes the score. If the CLI is missing or
|
|
125
|
+
the call fails, the audit completes without it.
|
|
126
|
+
|
|
127
|
+
## CI usage
|
|
128
|
+
|
|
129
|
+
```yaml
|
|
130
|
+
# .github/workflows/genesis-audit.yml
|
|
131
|
+
name: genesis-audit
|
|
132
|
+
on:
|
|
133
|
+
pull_request:
|
|
134
|
+
push: {branches: [main]} # keeps the baseline cache fresh
|
|
135
|
+
permissions:
|
|
136
|
+
contents: read
|
|
137
|
+
security-events: write # for the SARIF upload
|
|
138
|
+
jobs:
|
|
139
|
+
audit:
|
|
140
|
+
runs-on: ubuntu-latest
|
|
141
|
+
steps:
|
|
142
|
+
- uses: actions/checkout@v4
|
|
143
|
+
- uses: astral-sh/setup-uv@v4
|
|
144
|
+
- run: uv sync
|
|
145
|
+
# Restore the last report so PRs are gated on regressions, not just the
|
|
146
|
+
# absolute score. The cache is (re)saved after every run on main.
|
|
147
|
+
- uses: actions/cache@v4
|
|
148
|
+
with:
|
|
149
|
+
path: .genesis-baseline/
|
|
150
|
+
key: genesis-baseline-${{ github.sha }}
|
|
151
|
+
restore-keys: genesis-baseline-
|
|
152
|
+
- run: |
|
|
153
|
+
if [ -f .genesis-baseline/report.json ]; then
|
|
154
|
+
uv run genesis adopt . --min-score 70 \
|
|
155
|
+
--baseline .genesis-baseline/report.json --fail-on-regression
|
|
156
|
+
else
|
|
157
|
+
uv run genesis adopt . --min-score 70
|
|
158
|
+
fi
|
|
159
|
+
- run: mkdir -p .genesis-baseline &&
|
|
160
|
+
cp .genesis-report/report.json .genesis-baseline/report.json
|
|
161
|
+
if: github.ref == 'refs/heads/main'
|
|
162
|
+
- uses: actions/upload-artifact@v4
|
|
163
|
+
if: always()
|
|
164
|
+
with: {name: genesis-report, path: .genesis-report/}
|
|
165
|
+
# Findings land in the GitHub Security tab + inline PR annotations.
|
|
166
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
167
|
+
if: always()
|
|
168
|
+
with: {sarif_file: .genesis-report/report.sarif}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Exit codes: `0` pass, `1` below threshold / regressed (or grade D/F when
|
|
172
|
+
neither `--min-score` nor `--fail-on-regression` is given), `2` usage error.
|
genesis_cli/__init__.py
ADDED
|
File without changes
|
genesis_cli/adopt.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""`genesis adopt` — audit any repo for agent-readiness. Deterministic, no LLM.
|
|
2
|
+
|
|
3
|
+
Pipeline: gates (secrets/deps/tests/sast/sbom, skipping cleanly when tools are
|
|
4
|
+
absent) + system-completeness + env reconciliation + structure facts → a scored
|
|
5
|
+
readiness report and an AGENTS.md draft.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from datetime import UTC, datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
# Importing these registers gates/providers via side effects.
|
|
15
|
+
import genesis_gates.gates # noqa: F401
|
|
16
|
+
import genesis_gates.providers # noqa: F401
|
|
17
|
+
from genesis_gates import GateContext, GateRunner, get_gate, load_gate_config
|
|
18
|
+
from genesis_gates.analysis import (
|
|
19
|
+
analyze,
|
|
20
|
+
reconcile,
|
|
21
|
+
repo_input,
|
|
22
|
+
score_readiness,
|
|
23
|
+
structure_facts,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Deterministic gate set. risk-classify is excluded on purpose: it consults an
|
|
27
|
+
# LLM; `adopt` must run with zero keys and zero infrastructure.
|
|
28
|
+
ADOPT_GATES = ("secret-scan", "unit-tests", "dependency-scan", "sast", "sbom")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def run_gates(repo: Path, gate_names: Sequence[str] = ADOPT_GATES) -> dict:
|
|
32
|
+
"""Run the named gates over ``repo`` and return the EvidencePackage dict.
|
|
33
|
+
|
|
34
|
+
Shared by ``genesis adopt`` (which folds ``gate_results`` into the full
|
|
35
|
+
readiness report) and ``genesis gates`` (which presents the package as-is).
|
|
36
|
+
"""
|
|
37
|
+
gates = [get_gate(name)() for name in gate_names]
|
|
38
|
+
manifest = {
|
|
39
|
+
"apiVersion": "genesis.dev/v0",
|
|
40
|
+
"kind": "DeliveryManifest",
|
|
41
|
+
"change": {"id": "ADOPT-0001-001", "title": f"Agent-readiness audit of {repo.name}",
|
|
42
|
+
"source": "adopted"},
|
|
43
|
+
"service": {"name": repo.name, "owner": "genesis-adopt",
|
|
44
|
+
"criticality": "medium", "data_classification": "internal"},
|
|
45
|
+
"validation": {"required": [], "coverage_threshold": 0},
|
|
46
|
+
"release": {"risk_level": "medium", "strategy": "direct",
|
|
47
|
+
"rollback_required": False},
|
|
48
|
+
}
|
|
49
|
+
ctx = GateContext(repo_path=str(repo), manifest=manifest,
|
|
50
|
+
config=load_gate_config(repo))
|
|
51
|
+
return GateRunner(gates).run(ctx).to_dict()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def run_audit(repo: Path) -> dict:
|
|
55
|
+
"""Full audit. Returns the report dict (also consumed by the renderers)."""
|
|
56
|
+
repo = repo.resolve()
|
|
57
|
+
gate_results = run_gates(repo).get("gate_results", [])
|
|
58
|
+
completeness = analyze(repo_input(repo))
|
|
59
|
+
reconciliation = reconcile(repo, stored_deps={})
|
|
60
|
+
facts = structure_facts(repo)
|
|
61
|
+
readiness = score_readiness(
|
|
62
|
+
gate_results=gate_results, facts=facts,
|
|
63
|
+
completeness=completeness, reconciliation=reconciliation,
|
|
64
|
+
)
|
|
65
|
+
return {
|
|
66
|
+
"kind": "GenesisReadinessReport",
|
|
67
|
+
"version": 1,
|
|
68
|
+
"repo": str(repo),
|
|
69
|
+
"generated_at": datetime.now(UTC).isoformat(timespec="seconds"),
|
|
70
|
+
"readiness": readiness,
|
|
71
|
+
"gates": gate_results,
|
|
72
|
+
"completeness": completeness,
|
|
73
|
+
"env": reconciliation,
|
|
74
|
+
"structure": facts,
|
|
75
|
+
}
|
genesis_cli/agents_md.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""AGENTS.md generator — agent instructions derived from the audit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def render_agents_md(report: dict) -> str:
|
|
9
|
+
repo = Path(report["repo"])
|
|
10
|
+
facts = report["structure"]
|
|
11
|
+
cmds: dict[str, str] = facts.get("commands", {})
|
|
12
|
+
gaps = report["completeness"].get("gaps", [])
|
|
13
|
+
env_missing = [d["env_var"] for d in report["env"].get("missing_required", [])]
|
|
14
|
+
|
|
15
|
+
lines: list[str] = [
|
|
16
|
+
f"# AGENTS.md — {repo.name}",
|
|
17
|
+
"",
|
|
18
|
+
"_Generated by `genesis adopt`. Instructions for AI coding agents "
|
|
19
|
+
"(Claude Code, Cursor, etc.) working in this repository._",
|
|
20
|
+
"",
|
|
21
|
+
"## Commands",
|
|
22
|
+
"",
|
|
23
|
+
]
|
|
24
|
+
if cmds:
|
|
25
|
+
lines += [f"- **{label}**: `{cmd}`" for label, cmd in sorted(cmds.items())]
|
|
26
|
+
lines += ["", "Run the test command after every change. Do not commit "
|
|
27
|
+
"with failing tests."]
|
|
28
|
+
else:
|
|
29
|
+
lines += ["- No build/test commands were detected. FIRST TASK for any "
|
|
30
|
+
"agent: establish a test command and record it here."]
|
|
31
|
+
lines += [
|
|
32
|
+
"",
|
|
33
|
+
"## Ground rules",
|
|
34
|
+
"",
|
|
35
|
+
"- Work in small, reviewable changes; one concern per branch/PR.",
|
|
36
|
+
"- Never hardcode secrets, API keys, ports, or model IDs — read them "
|
|
37
|
+
"from environment variables and document new ones in `.env.example`.",
|
|
38
|
+
"- Match the existing code style, structure, and naming of the "
|
|
39
|
+
"surrounding module.",
|
|
40
|
+
"- Fail closed: unknown states and validation errors must surface, "
|
|
41
|
+
"never be swallowed.",
|
|
42
|
+
]
|
|
43
|
+
if env_missing:
|
|
44
|
+
lines += [
|
|
45
|
+
"",
|
|
46
|
+
"## Environment contract (currently incomplete)",
|
|
47
|
+
"",
|
|
48
|
+
"These variables are referenced with no default and no documented "
|
|
49
|
+
"value — supply them before deploying, and keep `.env.example` "
|
|
50
|
+
"current:",
|
|
51
|
+
"",
|
|
52
|
+
]
|
|
53
|
+
lines += [f"- `{v}`" for v in env_missing]
|
|
54
|
+
if gaps:
|
|
55
|
+
lines += [
|
|
56
|
+
"",
|
|
57
|
+
"## Known completeness gaps (from the readiness audit)",
|
|
58
|
+
"",
|
|
59
|
+
"Mandatory system flows that appear to be missing. Treat these as "
|
|
60
|
+
"high-priority tasks:",
|
|
61
|
+
"",
|
|
62
|
+
]
|
|
63
|
+
for g in gaps:
|
|
64
|
+
lines += [f"- **{g['title']}** ({g['profile']}:{g['flow']})"]
|
|
65
|
+
lines += [
|
|
66
|
+
"",
|
|
67
|
+
"## Validation",
|
|
68
|
+
"",
|
|
69
|
+
"Every change should pass the Genesis gates before merge:",
|
|
70
|
+
"",
|
|
71
|
+
"```bash",
|
|
72
|
+
"genesis adopt . --json # re-run the audit; the readiness score must not regress",
|
|
73
|
+
"```",
|
|
74
|
+
"",
|
|
75
|
+
]
|
|
76
|
+
return "\n".join(lines)
|
genesis_cli/baseline.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Baseline comparison + score history — the CI regression story.
|
|
2
|
+
|
|
3
|
+
`compare` diffs two `genesis adopt` reports. Identity is deliberately simple
|
|
4
|
+
and stable across runs: a gate finding is ``(gate, title, location)``, a
|
|
5
|
+
completeness gap is ``(profile, flow)``. ``regressed`` is the CI signal: the
|
|
6
|
+
score dropped by more than half a point, a mandatory-flow gap appeared, or a
|
|
7
|
+
new critical/high finding appeared.
|
|
8
|
+
|
|
9
|
+
`append_history` keeps one JSON line per audit in ``<out_dir>/history.jsonl``
|
|
10
|
+
(append-only — never rewritten), so the readiness score becomes a trackable
|
|
11
|
+
time series without any infrastructure.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
# A dip within this tolerance is jitter (e.g. a gate flipping skipped→passed),
|
|
20
|
+
# not a regression.
|
|
21
|
+
SCORE_TOLERANCE = 0.5
|
|
22
|
+
|
|
23
|
+
_REGRESSION_SEVERITIES = frozenset({"critical", "high"})
|
|
24
|
+
|
|
25
|
+
HISTORY_FILE = "history.jsonl"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _findings_by_identity(report: dict) -> dict[tuple, dict]:
|
|
29
|
+
"""Gate findings keyed by (gate, title, location); first occurrence wins."""
|
|
30
|
+
out: dict[tuple, dict] = {}
|
|
31
|
+
for gate in report.get("gates", []):
|
|
32
|
+
for f in gate.get("findings", []):
|
|
33
|
+
key = (f.get("gate") or gate.get("gate"), f.get("title"),
|
|
34
|
+
f.get("location"))
|
|
35
|
+
out.setdefault(key, f)
|
|
36
|
+
return out
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _gaps_by_identity(report: dict) -> dict[tuple, dict]:
|
|
40
|
+
"""Completeness gaps keyed by (profile, flow)."""
|
|
41
|
+
return {(g.get("profile"), g.get("flow")): g
|
|
42
|
+
for g in report.get("completeness", {}).get("gaps", [])}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def compare(current: dict, baseline: dict) -> dict:
|
|
46
|
+
"""Diff ``current`` against ``baseline`` (two adopt report dicts)."""
|
|
47
|
+
cur_f, base_f = _findings_by_identity(current), _findings_by_identity(baseline)
|
|
48
|
+
cur_g, base_g = _gaps_by_identity(current), _gaps_by_identity(baseline)
|
|
49
|
+
|
|
50
|
+
score_delta = round(
|
|
51
|
+
current["readiness"]["score"] - baseline["readiness"]["score"], 1)
|
|
52
|
+
cur_grade = current["readiness"]["grade"]
|
|
53
|
+
base_grade = baseline["readiness"]["grade"]
|
|
54
|
+
grade_change = f"{base_grade}→{cur_grade}" if cur_grade != base_grade else None
|
|
55
|
+
|
|
56
|
+
new_findings = [f for k, f in cur_f.items() if k not in base_f]
|
|
57
|
+
resolved_findings = [f for k, f in base_f.items() if k not in cur_f]
|
|
58
|
+
new_gaps = [g for k, g in cur_g.items() if k not in base_g]
|
|
59
|
+
resolved_gaps = [g for k, g in base_g.items() if k not in cur_g]
|
|
60
|
+
|
|
61
|
+
regressed = (
|
|
62
|
+
score_delta < -SCORE_TOLERANCE
|
|
63
|
+
or bool(new_gaps)
|
|
64
|
+
or any(f.get("severity") in _REGRESSION_SEVERITIES for f in new_findings)
|
|
65
|
+
)
|
|
66
|
+
return {
|
|
67
|
+
"score_delta": score_delta,
|
|
68
|
+
"grade_change": grade_change,
|
|
69
|
+
"new_findings": new_findings,
|
|
70
|
+
"resolved_findings": resolved_findings,
|
|
71
|
+
"new_gaps": new_gaps,
|
|
72
|
+
"resolved_gaps": resolved_gaps,
|
|
73
|
+
"regressed": regressed,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def append_history(out_dir: Path, report: dict) -> list[dict]:
|
|
78
|
+
"""Append this audit to ``<out_dir>/history.jsonl``; return all entries.
|
|
79
|
+
|
|
80
|
+
Append-only by design: the file is the artifact directory's score ledger.
|
|
81
|
+
Unparseable lines (e.g. from a truncated write) are skipped, not fatal.
|
|
82
|
+
"""
|
|
83
|
+
entry = {
|
|
84
|
+
"generated_at": report["generated_at"],
|
|
85
|
+
"score": report["readiness"]["score"],
|
|
86
|
+
"grade": report["readiness"]["grade"],
|
|
87
|
+
"findings_count": sum(len(g.get("findings", []))
|
|
88
|
+
for g in report.get("gates", [])),
|
|
89
|
+
"gaps_count": len(report.get("completeness", {}).get("gaps", [])),
|
|
90
|
+
}
|
|
91
|
+
path = out_dir / HISTORY_FILE
|
|
92
|
+
with path.open("a", encoding="utf-8") as fh:
|
|
93
|
+
fh.write(json.dumps(entry) + "\n")
|
|
94
|
+
|
|
95
|
+
entries = []
|
|
96
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
97
|
+
try:
|
|
98
|
+
entries.append(json.loads(line))
|
|
99
|
+
except json.JSONDecodeError:
|
|
100
|
+
continue
|
|
101
|
+
return entries
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def render_trend(entries: list[dict], max_shown: int = 8) -> str | None:
|
|
105
|
+
"""One-line score trend ("history: 44.8 → 49.0 → 75.5 (3 runs)").
|
|
106
|
+
|
|
107
|
+
None when there is nothing to compare yet (fewer than two runs).
|
|
108
|
+
"""
|
|
109
|
+
if len(entries) < 2:
|
|
110
|
+
return None
|
|
111
|
+
shown = entries[-max_shown:]
|
|
112
|
+
scores = " → ".join(str(e["score"]) for e in shown)
|
|
113
|
+
prefix = "… → " if len(entries) > len(shown) else ""
|
|
114
|
+
return f"history: {prefix}{scores} ({len(entries)} runs)"
|
genesis_cli/gates.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""`genesis gates` — run just the deterministic delivery gates, no full audit.
|
|
2
|
+
|
|
3
|
+
Same gate set and configuration as `genesis adopt` (via `adopt.run_gates`),
|
|
4
|
+
but without the completeness/env/structure analysis: a fast per-gate table for
|
|
5
|
+
local iteration ("did my fix turn secret-scan green?") and scripting.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
_STATUS_MARKS = {"passed": "✓", "failed": "✗", "skipped": "–", "error": "!"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def render_gates_terminal(package: dict) -> str:
|
|
14
|
+
"""Per-gate table (name, status, summary) + findings for failed gates."""
|
|
15
|
+
results = package.get("gate_results", [])
|
|
16
|
+
if not results:
|
|
17
|
+
return "no gates were run"
|
|
18
|
+
name_w = max(len("GATE"), *(len(str(r.get("gate", ""))) for r in results))
|
|
19
|
+
|
|
20
|
+
lines = ["", f" {'GATE':<{name_w}} {'STATUS':<8} SUMMARY"]
|
|
21
|
+
for r in results:
|
|
22
|
+
mark = _STATUS_MARKS.get(r.get("status"), "?")
|
|
23
|
+
summary = r.get("summary") or r.get("error") or ""
|
|
24
|
+
lines.append(f"{mark} {r.get('gate', ''):<{name_w}} "
|
|
25
|
+
f"{r.get('status', ''):<8} {summary[:80]}")
|
|
26
|
+
|
|
27
|
+
for r in results:
|
|
28
|
+
findings = r.get("findings", [])
|
|
29
|
+
if r.get("status") != "failed" or not findings:
|
|
30
|
+
continue
|
|
31
|
+
lines += ["", f"── {r['gate']}: top findings " +
|
|
32
|
+
"─" * max(0, 50 - len(str(r["gate"])))]
|
|
33
|
+
for f in findings[:5]:
|
|
34
|
+
lines.append(f" [{f.get('severity')}] {f.get('title')}")
|
|
35
|
+
if f.get("location"):
|
|
36
|
+
lines.append(f" at {f['location']}")
|
|
37
|
+
if f.get("remediation"):
|
|
38
|
+
lines.append(f" fix: {f['remediation']}")
|
|
39
|
+
if len(findings) > 5:
|
|
40
|
+
lines.append(f" … {len(findings) - 5} more (use --json for all)")
|
|
41
|
+
|
|
42
|
+
lines.append("")
|
|
43
|
+
return "\n".join(lines)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def gates_exit_code(package: dict) -> int:
|
|
47
|
+
"""0 when every selected gate passed or skipped, 1 otherwise."""
|
|
48
|
+
ok = all(r.get("status") in ("passed", "skipped")
|
|
49
|
+
for r in package.get("gate_results", []))
|
|
50
|
+
return 0 if ok else 1
|
genesis_cli/init.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""`genesis init` — scaffold a new (or bare) repo so agents start agent-ready.
|
|
2
|
+
|
|
3
|
+
Idempotent: every file is created only if absent; existing files are reported
|
|
4
|
+
as skipped and never modified. AGENTS.md comes from a fresh audit of whatever
|
|
5
|
+
already exists, so on an empty directory it carries the "establish a test
|
|
6
|
+
command" first task, and on a partial repo it reflects real commands and gaps.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from .adopt import run_audit
|
|
14
|
+
from .agents_md import render_agents_md
|
|
15
|
+
|
|
16
|
+
GITIGNORE = """\
|
|
17
|
+
# Environment — never commit secrets (.env.example is the documented template).
|
|
18
|
+
.env
|
|
19
|
+
.env.*
|
|
20
|
+
!.env.example
|
|
21
|
+
|
|
22
|
+
# Genesis audit artifacts
|
|
23
|
+
.genesis-report/
|
|
24
|
+
|
|
25
|
+
# Dependencies & build output
|
|
26
|
+
node_modules/
|
|
27
|
+
dist/
|
|
28
|
+
build/
|
|
29
|
+
coverage/
|
|
30
|
+
|
|
31
|
+
# Python
|
|
32
|
+
__pycache__/
|
|
33
|
+
*.py[cod]
|
|
34
|
+
.venv/
|
|
35
|
+
venv/
|
|
36
|
+
.pytest_cache/
|
|
37
|
+
.mypy_cache/
|
|
38
|
+
.ruff_cache/
|
|
39
|
+
|
|
40
|
+
# OS / editor noise
|
|
41
|
+
.DS_Store
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
ENV_EXAMPLE = """\
|
|
45
|
+
# Environment contract — every variable the app reads, with safe example
|
|
46
|
+
# values. Copy to .env and fill in real values (never commit .env).
|
|
47
|
+
#
|
|
48
|
+
# EXAMPLE_API_KEY=changeme
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
AUDIT_WORKFLOW = """\
|
|
52
|
+
# Agent-readiness audit: every PR runs `genesis adopt` and fails when the
|
|
53
|
+
# readiness score drops below the bar. Raise --min-score as remediation lands.
|
|
54
|
+
name: genesis-audit
|
|
55
|
+
on:
|
|
56
|
+
pull_request:
|
|
57
|
+
workflow_dispatch:
|
|
58
|
+
|
|
59
|
+
permissions:
|
|
60
|
+
contents: read
|
|
61
|
+
security-events: write # for the SARIF upload
|
|
62
|
+
|
|
63
|
+
jobs:
|
|
64
|
+
audit:
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
steps:
|
|
67
|
+
- uses: actions/checkout@v4
|
|
68
|
+
- uses: actions/setup-python@v5
|
|
69
|
+
with:
|
|
70
|
+
python-version: "3.12"
|
|
71
|
+
# TODO: genesis-cli is not on PyPI yet. Until it is published, replace
|
|
72
|
+
# this with an install from your checkout of the Genesis repo
|
|
73
|
+
# (e.g. `pip install path/to/genesis/packages/genesis-cli`).
|
|
74
|
+
- run: pip install genesis-cli
|
|
75
|
+
- name: Agent-readiness audit
|
|
76
|
+
run: genesis adopt . --min-score 40
|
|
77
|
+
- uses: actions/upload-artifact@v4
|
|
78
|
+
if: always()
|
|
79
|
+
with:
|
|
80
|
+
name: genesis-readiness-report
|
|
81
|
+
path: .genesis-report/
|
|
82
|
+
# Surface findings in the GitHub Security tab / PR annotations.
|
|
83
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
84
|
+
if: always()
|
|
85
|
+
with:
|
|
86
|
+
sarif_file: .genesis-report/report.sarif
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
GATE_CONFIG = """\
|
|
90
|
+
# Genesis gate config — the "how" of validation (provider selection,
|
|
91
|
+
# thresholds, BYO tooling). All optional: with everything commented out, each
|
|
92
|
+
# gate auto-picks its default provider and skips cleanly when the scanner is
|
|
93
|
+
# not installed. Schema: genesis-gates contracts/gateconfig.schema.json.
|
|
94
|
+
apiVersion: genesis.dev/v0
|
|
95
|
+
kind: GateConfig
|
|
96
|
+
|
|
97
|
+
# defaults: # applied to every gate, overridable per gate
|
|
98
|
+
# fail_on_severity: high # findings at/above this severity fail the gate
|
|
99
|
+
# timeout: 300
|
|
100
|
+
|
|
101
|
+
# gates:
|
|
102
|
+
# secret-scan:
|
|
103
|
+
# provider: gitleaks
|
|
104
|
+
# dependency-scan:
|
|
105
|
+
# provider: trivy
|
|
106
|
+
# fail_on_severity: critical
|
|
107
|
+
# unit-tests:
|
|
108
|
+
# provider: command
|
|
109
|
+
# command: "npm test"
|
|
110
|
+
# fail_on_nonzero: true
|
|
111
|
+
# sast:
|
|
112
|
+
# provider: report # import a report your CI already produced
|
|
113
|
+
# report: semgrep.sarif
|
|
114
|
+
# parser: sarif
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def run_init(repo: Path) -> tuple[list[str], list[str]]:
|
|
119
|
+
"""Scaffold `repo`. Returns (created, skipped) as repo-relative paths."""
|
|
120
|
+
repo.mkdir(parents=True, exist_ok=True)
|
|
121
|
+
|
|
122
|
+
# Audit what exists BEFORE writing anything, so AGENTS.md reflects the
|
|
123
|
+
# repo as found (empty dir -> "establish a test command" first task).
|
|
124
|
+
agents_md = render_agents_md(run_audit(repo))
|
|
125
|
+
|
|
126
|
+
created: list[str] = []
|
|
127
|
+
skipped: list[str] = []
|
|
128
|
+
for rel, content in (
|
|
129
|
+
("AGENTS.md", agents_md),
|
|
130
|
+
(".gitignore", GITIGNORE),
|
|
131
|
+
(".env.example", ENV_EXAMPLE),
|
|
132
|
+
(".github/workflows/genesis-audit.yml", AUDIT_WORKFLOW),
|
|
133
|
+
(".genesis/config.yaml", GATE_CONFIG),
|
|
134
|
+
):
|
|
135
|
+
path = repo / rel
|
|
136
|
+
if path.exists():
|
|
137
|
+
skipped.append(rel)
|
|
138
|
+
else:
|
|
139
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
path.write_text(content)
|
|
141
|
+
created.append(rel)
|
|
142
|
+
return created, skipped
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def render_summary(repo: Path, created: list[str], skipped: list[str]) -> str:
|
|
146
|
+
lines = [f"genesis init — {repo}", ""]
|
|
147
|
+
lines += [f" created {rel}" for rel in created]
|
|
148
|
+
lines += [f" exists, skipped {rel}" for rel in skipped]
|
|
149
|
+
lines += [
|
|
150
|
+
"",
|
|
151
|
+
"Next steps:",
|
|
152
|
+
" 1. Fill in .env.example as you introduce configuration (never commit .env).",
|
|
153
|
+
" 2. Establish a test command and record it in AGENTS.md — agents run it "
|
|
154
|
+
"after every change.",
|
|
155
|
+
" 3. Run `genesis adopt .` after your first commits to get a readiness "
|
|
156
|
+
"score and gap list.",
|
|
157
|
+
]
|
|
158
|
+
return "\n".join(lines)
|