aegisure 0.1.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.
- aegisure-0.1.0/.gitignore +20 -0
- aegisure-0.1.0/PKG-INFO +36 -0
- aegisure-0.1.0/README.md +12 -0
- aegisure-0.1.0/pyproject.toml +39 -0
- aegisure-0.1.0/src/aegisure/__init__.py +1 -0
- aegisure-0.1.0/src/aegisure/agent_failure_memory.py +51 -0
- aegisure-0.1.0/src/aegisure/agent_memory_export.py +140 -0
- aegisure-0.1.0/src/aegisure/attribution.py +83 -0
- aegisure-0.1.0/src/aegisure/cli.py +182 -0
- aegisure-0.1.0/src/aegisure/constitution.py +268 -0
- aegisure-0.1.0/src/aegisure/cost_router.py +89 -0
- aegisure-0.1.0/src/aegisure/crypto_identity.py +241 -0
- aegisure-0.1.0/src/aegisure/diff_parser.py +184 -0
- aegisure-0.1.0/src/aegisure/diff_risk.py +130 -0
- aegisure-0.1.0/src/aegisure/finding_dismissals.py +36 -0
- aegisure-0.1.0/src/aegisure/llm_provider.py +189 -0
- aegisure-0.1.0/src/aegisure/memory_timeline.py +57 -0
- aegisure-0.1.0/src/aegisure/policy_engine.py +112 -0
- aegisure-0.1.0/src/aegisure/privacy.py +89 -0
- aegisure-0.1.0/src/aegisure/provenance.py +93 -0
- aegisure-0.1.0/src/aegisure/repair_prompt.py +74 -0
- aegisure-0.1.0/src/aegisure/safety.py +86 -0
- aegisure-0.1.0/src/aegisure/second_opinion.py +57 -0
- aegisure-0.1.0/src/aegisure/state.py +75 -0
- aegisure-0.1.0/src/aegisure/storage/__init__.py +0 -0
- aegisure-0.1.0/src/aegisure/storage/db.py +77 -0
- aegisure-0.1.0/src/aegisure/storage/profile_paths.py +17 -0
- aegisure-0.1.0/src/aegisure_github/__init__.py +0 -0
- aegisure-0.1.0/src/aegisure_github/client.py +119 -0
- aegisure-0.1.0/src/aegisure_github/models.py +114 -0
- aegisure-0.1.0/src/aegisure_github/pr_flow.py +112 -0
- aegisure-0.1.0/src/aegisure_github/webhooks.py +61 -0
- aegisure-0.1.0/tests/test_core.py +64 -0
- aegisure-0.1.0/tests/test_github_flow.py +70 -0
- aegisure-0.1.0/tests/test_pivot_m1_core.py +84 -0
- aegisure-0.1.0/tests/test_pivot_m2_memory_provenance.py +87 -0
- aegisure-0.1.0/tests/test_pivot_m3_github_app.py +70 -0
- aegisure-0.1.0/tests/test_pivot_m4_intelligence.py +61 -0
- aegisure-0.1.0/tests/test_pivot_n2_llm_cost_memory.py +54 -0
- aegisure-0.1.0/tests/test_pivot_n4_github_pr_flow.py +63 -0
- aegisure-0.1.0/tests/test_pivot_n6_false_positive.py +20 -0
aegisure-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aegisure
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Aegisure CLI and core engine for governing AI coding agents.
|
|
5
|
+
Project-URL: Homepage, https://aegisure.dev
|
|
6
|
+
Project-URL: Repository, https://github.com/Hetul803/Aegisure
|
|
7
|
+
Project-URL: Documentation, https://aegisure.dev/docs
|
|
8
|
+
Author: Aegisure
|
|
9
|
+
License: Proprietary
|
|
10
|
+
Keywords: ai-agents,developer-tools,github,provenance,security
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: cryptography>=42.0.0
|
|
20
|
+
Requires-Dist: httpx>=0.27.0
|
|
21
|
+
Requires-Dist: pyyaml>=6.0.1
|
|
22
|
+
Requires-Dist: typer>=0.12.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Aegisure CLI
|
|
26
|
+
|
|
27
|
+
Aegisure is the control and audit plane for AI coding agents. The CLI gives a local-first path for generating a repo Constitution, scanning diffs, exporting cross-agent memory files, capturing provenance, and preparing repair prompts.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install aegisure
|
|
31
|
+
aegisure init
|
|
32
|
+
aegisure scan --staged
|
|
33
|
+
aegisure export
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The static scanner is fully offline and does not call an LLM. Optional review features can use Anthropic, OpenAI, or Ollama when configured.
|
aegisure-0.1.0/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Aegisure CLI
|
|
2
|
+
|
|
3
|
+
Aegisure is the control and audit plane for AI coding agents. The CLI gives a local-first path for generating a repo Constitution, scanning diffs, exporting cross-agent memory files, capturing provenance, and preparing repair prompts.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install aegisure
|
|
7
|
+
aegisure init
|
|
8
|
+
aegisure scan --staged
|
|
9
|
+
aegisure export
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The static scanner is fully offline and does not call an LLM. Optional review features can use Anthropic, OpenAI, or Ollama when configured.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.24"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aegisure"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Aegisure CLI and core engine for governing AI coding agents."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "Proprietary" }
|
|
12
|
+
authors = [{ name = "Aegisure" }]
|
|
13
|
+
keywords = ["ai-agents", "github", "security", "provenance", "developer-tools"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"cryptography>=42.0.0",
|
|
25
|
+
"httpx>=0.27.0",
|
|
26
|
+
"pyyaml>=6.0.1",
|
|
27
|
+
"typer>=0.12.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://aegisure.dev"
|
|
32
|
+
Repository = "https://github.com/Hetul803/Aegisure"
|
|
33
|
+
Documentation = "https://aegisure.dev/docs"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
aegisure = "aegisure.cli:main"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["src/aegisure", "src/aegisure_github"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class AgentFailureRecord:
|
|
11
|
+
repo: str
|
|
12
|
+
agent: str
|
|
13
|
+
failure_class: str
|
|
14
|
+
summary: str
|
|
15
|
+
repair_worked: bool | None = None
|
|
16
|
+
created_at: str = ""
|
|
17
|
+
|
|
18
|
+
def to_dict(self) -> dict:
|
|
19
|
+
return {
|
|
20
|
+
"repo": self.repo,
|
|
21
|
+
"agent": self.agent,
|
|
22
|
+
"failure_class": self.failure_class,
|
|
23
|
+
"summary": self.summary,
|
|
24
|
+
"repair_worked": self.repair_worked,
|
|
25
|
+
"created_at": self.created_at or datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def record_agent_failure(repo_path: str | Path, record: AgentFailureRecord) -> Path:
|
|
30
|
+
target_dir = Path(repo_path).resolve() / ".aegisure"
|
|
31
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
target = target_dir / "agent-failure-memory.jsonl"
|
|
33
|
+
with target.open("a", encoding="utf-8") as handle:
|
|
34
|
+
handle.write(json.dumps(record.to_dict(), sort_keys=True) + "\n")
|
|
35
|
+
return target
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def list_agent_failures(repo_path: str | Path, *, agent: str | None = None) -> list[dict]:
|
|
39
|
+
target = Path(repo_path).resolve() / ".aegisure" / "agent-failure-memory.jsonl"
|
|
40
|
+
if not target.exists():
|
|
41
|
+
return []
|
|
42
|
+
rows = []
|
|
43
|
+
for line in target.read_text(encoding="utf-8").splitlines():
|
|
44
|
+
try:
|
|
45
|
+
row = json.loads(line)
|
|
46
|
+
except Exception:
|
|
47
|
+
continue
|
|
48
|
+
if agent and row.get("agent") != agent:
|
|
49
|
+
continue
|
|
50
|
+
rows.append(row)
|
|
51
|
+
return rows
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .constitution import Constitution, constitution_for_repo, render_constitution
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
EXPORT_TARGETS = (
|
|
9
|
+
"AEGIS.md",
|
|
10
|
+
"AGENTS.md",
|
|
11
|
+
"CLAUDE.md",
|
|
12
|
+
".cursorrules",
|
|
13
|
+
".clinerules",
|
|
14
|
+
".github/copilot-instructions.md",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _rules_block(constitution: Constitution) -> str:
|
|
19
|
+
return "\n".join(f"- {rule}" for rule in constitution.agent_rules)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _approval_block(constitution: Constitution) -> str:
|
|
23
|
+
return "\n".join(f"- {rule}" for rule in constitution.approval_rules)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _test_block(constitution: Constitution) -> str:
|
|
27
|
+
return "\n".join(f"- `{command}`" for command in constitution.test_commands) or "- No test command detected yet."
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _protected_block(constitution: Constitution) -> str:
|
|
31
|
+
return "\n".join(f"- `{path}`" for path in constitution.protected_paths) or "- No protected paths detected yet."
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def build_memory_exports(constitution: Constitution) -> dict[str, str]:
|
|
35
|
+
shared = {
|
|
36
|
+
"repo": constitution.repo_name,
|
|
37
|
+
"summary": constitution.summary,
|
|
38
|
+
"rules": _rules_block(constitution),
|
|
39
|
+
"approval": _approval_block(constitution),
|
|
40
|
+
"tests": _test_block(constitution),
|
|
41
|
+
"protected": _protected_block(constitution),
|
|
42
|
+
}
|
|
43
|
+
aura_md = render_constitution(constitution)
|
|
44
|
+
agent_md = f"""# Agent Instructions for {shared['repo']}
|
|
45
|
+
|
|
46
|
+
{shared['summary']}
|
|
47
|
+
|
|
48
|
+
## Required Behavior
|
|
49
|
+
{shared['rules']}
|
|
50
|
+
|
|
51
|
+
## Human Approval Required
|
|
52
|
+
{shared['approval']}
|
|
53
|
+
|
|
54
|
+
## Protected Paths
|
|
55
|
+
{shared['protected']}
|
|
56
|
+
|
|
57
|
+
## Verification
|
|
58
|
+
{shared['tests']}
|
|
59
|
+
"""
|
|
60
|
+
claude_md = f"""# Claude Code Memory
|
|
61
|
+
|
|
62
|
+
You are working in `{shared['repo']}`.
|
|
63
|
+
|
|
64
|
+
Project summary: {shared['summary']}
|
|
65
|
+
|
|
66
|
+
Follow these non-negotiable rules:
|
|
67
|
+
{shared['rules']}
|
|
68
|
+
|
|
69
|
+
Before editing protected areas, ask for human review:
|
|
70
|
+
{shared['protected']}
|
|
71
|
+
|
|
72
|
+
Run or recommend these checks:
|
|
73
|
+
{shared['tests']}
|
|
74
|
+
"""
|
|
75
|
+
cursor_rules = f"""You are an AI coding agent in {shared['repo']}.
|
|
76
|
+
|
|
77
|
+
{shared['summary']}
|
|
78
|
+
|
|
79
|
+
Rules:
|
|
80
|
+
{shared['rules']}
|
|
81
|
+
|
|
82
|
+
Protected paths:
|
|
83
|
+
{shared['protected']}
|
|
84
|
+
|
|
85
|
+
Tests:
|
|
86
|
+
{shared['tests']}
|
|
87
|
+
"""
|
|
88
|
+
cline_rules = f"""# Cline/Roo Rules
|
|
89
|
+
|
|
90
|
+
Repository: {shared['repo']}
|
|
91
|
+
|
|
92
|
+
{shared['summary']}
|
|
93
|
+
|
|
94
|
+
Do:
|
|
95
|
+
{shared['rules']}
|
|
96
|
+
|
|
97
|
+
Require approval:
|
|
98
|
+
{shared['approval']}
|
|
99
|
+
|
|
100
|
+
Verify with:
|
|
101
|
+
{shared['tests']}
|
|
102
|
+
"""
|
|
103
|
+
copilot = f"""# GitHub Copilot Instructions
|
|
104
|
+
|
|
105
|
+
This repository uses Aegisure as its project Constitution.
|
|
106
|
+
|
|
107
|
+
Summary: {shared['summary']}
|
|
108
|
+
|
|
109
|
+
Coding rules:
|
|
110
|
+
{shared['rules']}
|
|
111
|
+
|
|
112
|
+
Protected paths:
|
|
113
|
+
{shared['protected']}
|
|
114
|
+
|
|
115
|
+
Verification:
|
|
116
|
+
{shared['tests']}
|
|
117
|
+
"""
|
|
118
|
+
return {
|
|
119
|
+
"AEGIS.md": aura_md,
|
|
120
|
+
"AGENTS.md": agent_md,
|
|
121
|
+
"CLAUDE.md": claude_md,
|
|
122
|
+
".cursorrules": cursor_rules,
|
|
123
|
+
".clinerules": cline_rules,
|
|
124
|
+
".github/copilot-instructions.md": copilot,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def write_memory_exports(repo_path: str | Path, *, overwrite: bool = True) -> list[dict[str, str | bool]]:
|
|
129
|
+
repo = Path(repo_path).resolve()
|
|
130
|
+
exports = build_memory_exports(constitution_for_repo(repo))
|
|
131
|
+
results: list[dict[str, str | bool]] = []
|
|
132
|
+
for target, content in exports.items():
|
|
133
|
+
path = repo / target
|
|
134
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
previous = path.read_text(encoding="utf-8") if path.exists() else None
|
|
136
|
+
changed = previous != content
|
|
137
|
+
if changed and (overwrite or previous is None):
|
|
138
|
+
path.write_text(content, encoding="utf-8")
|
|
139
|
+
results.append({"path": str(path), "target": target, "changed": changed})
|
|
140
|
+
return results
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .diff_parser import ParsedDiff
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
KNOWN_AGENTS = {
|
|
13
|
+
"codex": ("codex", "openai codex"),
|
|
14
|
+
"claude-code": ("claude", "claude code"),
|
|
15
|
+
"cursor": ("cursor",),
|
|
16
|
+
"copilot": ("copilot", "github copilot"),
|
|
17
|
+
"cline": ("cline",),
|
|
18
|
+
"roo": ("roo", "roo code"),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class AttributionRecord:
|
|
24
|
+
repo: str
|
|
25
|
+
change_id: str
|
|
26
|
+
path: str
|
|
27
|
+
agent: str
|
|
28
|
+
source: str
|
|
29
|
+
created_at: str
|
|
30
|
+
|
|
31
|
+
def to_dict(self) -> dict:
|
|
32
|
+
return {
|
|
33
|
+
"repo": self.repo,
|
|
34
|
+
"change_id": self.change_id,
|
|
35
|
+
"path": self.path,
|
|
36
|
+
"agent": self.agent,
|
|
37
|
+
"source": self.source,
|
|
38
|
+
"created_at": self.created_at,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def infer_agent(*, commit_message: str = "", pr_body: str = "", explicit_agent: str | None = None) -> str:
|
|
43
|
+
if explicit_agent:
|
|
44
|
+
return explicit_agent
|
|
45
|
+
text = f"{commit_message}\n{pr_body}".lower()
|
|
46
|
+
trailer = re.search(r"aura-agent:\s*([a-z0-9_.-]+)", text)
|
|
47
|
+
if trailer:
|
|
48
|
+
return trailer.group(1)
|
|
49
|
+
for agent, needles in KNOWN_AGENTS.items():
|
|
50
|
+
if any(needle in text for needle in needles):
|
|
51
|
+
return agent
|
|
52
|
+
return "unknown"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def attribution_records(parsed: ParsedDiff, *, repo: str, change_id: str, agent: str, source: str = "analysis") -> list[AttributionRecord]:
|
|
56
|
+
now = datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
57
|
+
return [AttributionRecord(repo=repo, change_id=change_id, path=file.path, agent=agent, source=source, created_at=now) for file in parsed.files]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def append_attribution_ledger(repo_path: str | Path, records: list[AttributionRecord]) -> Path:
|
|
61
|
+
aegisure_dir = Path(repo_path).resolve() / ".aegisure"
|
|
62
|
+
aegisure_dir.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
target = aegisure_dir / "attribution-ledger.jsonl"
|
|
64
|
+
with target.open("a", encoding="utf-8") as handle:
|
|
65
|
+
for record in records:
|
|
66
|
+
handle.write(json.dumps(record.to_dict(), sort_keys=True) + "\n")
|
|
67
|
+
return target
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def query_attribution_ledger(repo_path: str | Path, *, agent: str | None = None) -> list[dict]:
|
|
71
|
+
target = Path(repo_path).resolve() / ".aegisure" / "attribution-ledger.jsonl"
|
|
72
|
+
if not target.exists():
|
|
73
|
+
return []
|
|
74
|
+
rows = []
|
|
75
|
+
for line in target.read_text(encoding="utf-8").splitlines():
|
|
76
|
+
try:
|
|
77
|
+
row = json.loads(line)
|
|
78
|
+
except Exception:
|
|
79
|
+
continue
|
|
80
|
+
if agent and row.get("agent") != agent:
|
|
81
|
+
continue
|
|
82
|
+
rows.append(row)
|
|
83
|
+
return rows
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from .agent_memory_export import write_memory_exports
|
|
12
|
+
from .attribution import append_attribution_ledger, attribution_records, infer_agent
|
|
13
|
+
from .constitution import write_constitution, constitution_for_repo
|
|
14
|
+
from .diff_parser import parse_unified_diff
|
|
15
|
+
from .diff_risk import analyze_diff
|
|
16
|
+
from .policy_engine import default_policy_yaml, evaluate_policy
|
|
17
|
+
from .provenance import build_commit_message, prompt_hash, read_commit_provenance, record_git_note
|
|
18
|
+
from .repair_prompt import generate_repair_prompt
|
|
19
|
+
from .second_opinion import cross_model_second_opinion, heuristic_second_opinion
|
|
20
|
+
|
|
21
|
+
app = typer.Typer(help="Aegisure: control and audit plane for AI coding agents.")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _repo(path: str | Path = ".") -> Path:
|
|
25
|
+
return Path(path).resolve()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _selected_repo(repo: Path | None, path: Path | None) -> Path:
|
|
29
|
+
return _repo(path or repo or ".")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _run_git(repo: Path, args: list[str], *, check: bool = True) -> subprocess.CompletedProcess[str]:
|
|
33
|
+
proc = subprocess.run(["git", *args], cwd=repo, capture_output=True, text=True)
|
|
34
|
+
if check and proc.returncode != 0:
|
|
35
|
+
raise typer.BadParameter(proc.stderr.strip() or proc.stdout.strip() or f"git {' '.join(args)} failed")
|
|
36
|
+
return proc
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _diff_text(repo: Path, *, staged: bool = False, base: Optional[str] = None) -> str:
|
|
40
|
+
if base:
|
|
41
|
+
return _run_git(repo, ["diff", base]).stdout
|
|
42
|
+
if staged:
|
|
43
|
+
return _run_git(repo, ["diff", "--cached"]).stdout
|
|
44
|
+
text = _run_git(repo, ["diff"]).stdout
|
|
45
|
+
return text or _run_git(repo, ["diff", "--cached"]).stdout
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@app.command()
|
|
49
|
+
def init(
|
|
50
|
+
repo: Path | None = typer.Argument(None, help="Repository to scan."),
|
|
51
|
+
path: Path | None = typer.Option(None, "--path", help="Repository to scan."),
|
|
52
|
+
overwrite: bool = typer.Option(False, help="Overwrite existing AEGIS.md."),
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Scan a repository and generate the canonical AEGIS.md Constitution."""
|
|
55
|
+
|
|
56
|
+
target = write_constitution(_selected_repo(repo, path), overwrite=overwrite)
|
|
57
|
+
typer.echo(f"Generated {target}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@app.command()
|
|
61
|
+
def export(
|
|
62
|
+
repo: Path | None = typer.Argument(None, help="Repository to export memory files into."),
|
|
63
|
+
path: Path | None = typer.Option(None, "--path", help="Repository to export memory files into."),
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Export AEGIS.md into standard agent memory files."""
|
|
66
|
+
|
|
67
|
+
results = write_memory_exports(_selected_repo(repo, path), overwrite=True)
|
|
68
|
+
for result in results:
|
|
69
|
+
marker = "updated" if result["changed"] else "unchanged"
|
|
70
|
+
typer.echo(f"{marker}: {result['target']}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@app.command()
|
|
74
|
+
def scan(
|
|
75
|
+
repo: Path | None = typer.Argument(None, help="Repository to scan."),
|
|
76
|
+
path: Path | None = typer.Option(None, "--path", help="Repository to scan."),
|
|
77
|
+
staged: bool = typer.Option(False, "--staged", help="Analyze staged changes."),
|
|
78
|
+
base: Optional[str] = typer.Option(None, "--base", help="Git ref to diff against."),
|
|
79
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON."),
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Analyze a local diff using the LLM-free static core."""
|
|
82
|
+
|
|
83
|
+
repo_path = _selected_repo(repo, path)
|
|
84
|
+
diff = _diff_text(repo_path, staged=staged, base=base)
|
|
85
|
+
constitution = constitution_for_repo(repo_path)
|
|
86
|
+
report = analyze_diff(diff, constitution=constitution)
|
|
87
|
+
policy = evaluate_policy(diff, policy_text=default_policy_yaml(), risk_report=report)
|
|
88
|
+
payload = {**report.to_dict(), "policy_evaluation": policy.to_dict()}
|
|
89
|
+
if json_output:
|
|
90
|
+
typer.echo(json.dumps(payload, indent=2, sort_keys=True))
|
|
91
|
+
if report.verdict == "block":
|
|
92
|
+
raise typer.Exit(1)
|
|
93
|
+
return
|
|
94
|
+
typer.echo(f"Aegisure verdict: {report.verdict} ({report.score}/100)")
|
|
95
|
+
typer.echo(report.summary)
|
|
96
|
+
for finding in report.findings:
|
|
97
|
+
location = f"{finding.path}:{finding.line}" if finding.line else finding.path
|
|
98
|
+
typer.echo(f"- {finding.severity.upper()} {finding.category} at {location}: {finding.explanation}")
|
|
99
|
+
if not policy.passed:
|
|
100
|
+
typer.echo("Policy violations:")
|
|
101
|
+
for violation in policy.violations:
|
|
102
|
+
typer.echo(f"- {violation.severity.upper()} {violation.rule_id}: {violation.explanation}")
|
|
103
|
+
if report.verdict == "block":
|
|
104
|
+
raise typer.Exit(1)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@app.command()
|
|
108
|
+
def repair(
|
|
109
|
+
repo: Path | None = typer.Argument(None, help="Repository to inspect."),
|
|
110
|
+
path: Path | None = typer.Option(None, "--path", help="Repository to inspect."),
|
|
111
|
+
staged: bool = typer.Option(False, "--staged", help="Use staged diff."),
|
|
112
|
+
agent: str = typer.Option("codex", "--agent", help="Target agent for the repair prompt."),
|
|
113
|
+
) -> None:
|
|
114
|
+
"""Generate a constrained repair prompt for the current risky diff."""
|
|
115
|
+
|
|
116
|
+
repo_path = _selected_repo(repo, path)
|
|
117
|
+
diff = _diff_text(repo_path, staged=staged)
|
|
118
|
+
constitution = constitution_for_repo(repo_path)
|
|
119
|
+
report = analyze_diff(diff, constitution=constitution)
|
|
120
|
+
prompt = generate_repair_prompt(risk_report=report, constitution=constitution, agent=agent)
|
|
121
|
+
typer.echo(prompt.prompt)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@app.command()
|
|
125
|
+
def review(
|
|
126
|
+
repo: Path | None = typer.Argument(None, help="Repository to inspect."),
|
|
127
|
+
path: Path | None = typer.Option(None, "--path", help="Repository to inspect."),
|
|
128
|
+
staged: bool = typer.Option(False, "--staged", help="Use staged diff."),
|
|
129
|
+
provider: str = typer.Option("static", "--provider", help="static, anthropic, openai, or ollama."),
|
|
130
|
+
) -> None:
|
|
131
|
+
"""Run a second opinion on the current diff."""
|
|
132
|
+
|
|
133
|
+
diff = _diff_text(_selected_repo(repo, path), staged=staged)
|
|
134
|
+
if provider == "static":
|
|
135
|
+
opinion = heuristic_second_opinion(diff)
|
|
136
|
+
else:
|
|
137
|
+
opinion = asyncio.run(cross_model_second_opinion(diff, author_agent="unknown", reviewer=provider))
|
|
138
|
+
typer.echo(json.dumps(opinion.to_dict(), indent=2, sort_keys=True))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@app.command()
|
|
142
|
+
def commit(
|
|
143
|
+
message: str = typer.Option(..., "-m", "--message", help="Commit message."),
|
|
144
|
+
agent: str = typer.Option(..., "--agent", help="Agent name: codex, claude-code, cursor, copilot, cline, roo, human."),
|
|
145
|
+
prompt: str = typer.Option(..., "--prompt", help="Prompt that produced the change."),
|
|
146
|
+
repo: Path | None = typer.Option(None, "--repo", help="Repository to commit."),
|
|
147
|
+
path: Path | None = typer.Option(None, "--path", help="Repository to commit."),
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Create a git commit and capture provenance + attribution."""
|
|
150
|
+
|
|
151
|
+
repo_path = _selected_repo(repo, path)
|
|
152
|
+
final_message = build_commit_message(message, agent=agent, prompt=prompt)
|
|
153
|
+
_run_git(repo_path, ["commit", "-m", final_message])
|
|
154
|
+
sha = _run_git(repo_path, ["rev-parse", "HEAD"]).stdout.strip()
|
|
155
|
+
record = read_commit_provenance(repo_path, sha)
|
|
156
|
+
if record:
|
|
157
|
+
record_git_note(repo_path, sha, record)
|
|
158
|
+
diff = _run_git(repo_path, ["show", "--format=", "--find-renames", sha]).stdout
|
|
159
|
+
parsed = parse_unified_diff(diff)
|
|
160
|
+
ledger = attribution_records(parsed, repo=repo_path.name, change_id=prompt_hash(prompt), agent=infer_agent(explicit_agent=agent), source="cli_commit")
|
|
161
|
+
append_attribution_ledger(repo_path, ledger)
|
|
162
|
+
typer.echo(f"Committed {sha} and recorded {len(ledger)} attribution entries.")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@app.command()
|
|
166
|
+
def login(workspace: str = typer.Option("local", "--workspace", help="Workspace id or slug."), token: str = typer.Option("", "--token", help="Optional dashboard API token.")) -> None:
|
|
167
|
+
"""Store local workspace login metadata for CLI use."""
|
|
168
|
+
|
|
169
|
+
from .state import record_audit_event
|
|
170
|
+
from .storage.db import init_db
|
|
171
|
+
|
|
172
|
+
init_db()
|
|
173
|
+
record_audit_event({"workspace_id": workspace, "event_type": "cli_login", "message": "CLI workspace login configured.", "payload": {"token_configured": bool(token)}})
|
|
174
|
+
typer.echo(f"CLI configured for workspace `{workspace}`. Static scans remain local-first.")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def main() -> None:
|
|
178
|
+
app()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
main()
|