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 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.
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
+ }
@@ -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)
@@ -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)