svarog-forge 2.2.0__tar.gz → 2.2.2__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.
- {svarog_forge-2.2.0 → svarog_forge-2.2.2}/PKG-INFO +1 -1
- svarog_forge-2.2.2/forge/__init__.py +6 -0
- svarog_forge-2.2.0/forge.py → svarog_forge-2.2.2/forge/cli.py +223 -224
- svarog_forge-2.2.2/forge.py +20 -0
- {svarog_forge-2.2.0 → svarog_forge-2.2.2}/pyproject.toml +1 -1
- svarog_forge-2.2.0/forge/__init__.py +0 -3
- svarog_forge-2.2.0/forge/cli.py +0 -12
- {svarog_forge-2.2.0 → svarog_forge-2.2.2}/.gitignore +0 -0
- {svarog_forge-2.2.0 → svarog_forge-2.2.2}/CHANGELOG.md +0 -0
- {svarog_forge-2.2.0 → svarog_forge-2.2.2}/CONTRIBUTING.md +0 -0
- {svarog_forge-2.2.0 → svarog_forge-2.2.2}/LICENSE +0 -0
- {svarog_forge-2.2.0 → svarog_forge-2.2.2}/README.md +0 -0
- {svarog_forge-2.2.0 → svarog_forge-2.2.2}/README.ru.md +0 -0
- {svarog_forge-2.2.0 → svarog_forge-2.2.2}/SECURITY.md +0 -0
- {svarog_forge-2.2.0 → svarog_forge-2.2.2}/SKILL.md +0 -0
- {svarog_forge-2.2.0 → svarog_forge-2.2.2}/forge-verify.py +0 -0
- {svarog_forge-2.2.0 → svarog_forge-2.2.2}/forge.yaml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: svarog-forge
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: Forge. The smithy itself — AI-powered project forge from idea to deployment.
|
|
5
5
|
Project-URL: Homepage, https://github.com/SvarogForge/forge
|
|
6
6
|
Project-URL: Repository, https://github.com/SvarogForge/forge
|
|
@@ -1,224 +1,223 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
""
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
except
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
commit_data = {"findings": [], "total_messages": 0, "total_findings": 0}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
cf["
|
|
87
|
-
cf
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
scan_results["
|
|
91
|
-
scan_results["
|
|
92
|
-
scan_results
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
Examples:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
parser.add_argument("--
|
|
163
|
-
parser.add_argument("--
|
|
164
|
-
parser.add_argument("--
|
|
165
|
-
parser.add_argument("--
|
|
166
|
-
parser.add_argument("--
|
|
167
|
-
parser.add_argument("--
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if args.audit and not args.plan and not args.resume:
|
|
177
|
-
log("AUDIT", "Running security audit...")
|
|
178
|
-
results = run_security_audit()
|
|
179
|
-
if args.json:
|
|
180
|
-
print(json.dumps(results, indent=2, ensure_ascii=False))
|
|
181
|
-
else:
|
|
182
|
-
passed = display_audit_results(results)
|
|
183
|
-
sys.exit(0 if passed else 1)
|
|
184
|
-
|
|
185
|
-
if args.status or not any([args.plan, args.resume, args.audit, args.version]):
|
|
186
|
-
status()
|
|
187
|
-
sys.exit(0)
|
|
188
|
-
|
|
189
|
-
idea = args.plan or "Unknown"
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
"
|
|
197
|
-
"
|
|
198
|
-
"
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
"
|
|
207
|
-
"
|
|
208
|
-
"
|
|
209
|
-
"
|
|
210
|
-
"
|
|
211
|
-
"
|
|
212
|
-
"
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
"
|
|
217
|
-
"
|
|
218
|
-
"
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
main()
|
|
1
|
+
"""CLI entry point for Forge."""
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
VERSION = "2.2.2"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def log(phase: str, message: str, level: str = "INFO"):
|
|
13
|
+
"""Structured logging for forge pipeline."""
|
|
14
|
+
ts = datetime.now().strftime("%H:%M:%S")
|
|
15
|
+
print(f"[{ts}] [{level}] [{phase}] {message}")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def status():
|
|
19
|
+
"""Show current forge pipeline status."""
|
|
20
|
+
print(f"⚒️ Forge v{VERSION}")
|
|
21
|
+
print(" Pipeline: ready")
|
|
22
|
+
print(" Phases: scout → gate → assemble → 🛡️audit → crucible → 🛡️audit → deliver → 🛡️audit")
|
|
23
|
+
print(" Use: python forge.py --plan 'your idea'")
|
|
24
|
+
print(" Security: python forge.py --audit")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run_security_audit() -> dict:
|
|
28
|
+
"""Run full security audit: files + commit messages."""
|
|
29
|
+
# Try forge-verify.py next to package, or on PATH
|
|
30
|
+
verify_script = find_verify_script()
|
|
31
|
+
|
|
32
|
+
if verify_script:
|
|
33
|
+
try:
|
|
34
|
+
result = subprocess.run(
|
|
35
|
+
[sys.executable, verify_script, "--security", "--json"],
|
|
36
|
+
capture_output=True, text=True, timeout=60
|
|
37
|
+
)
|
|
38
|
+
if result.returncode == 0:
|
|
39
|
+
scan_results = json.loads(result.stdout)
|
|
40
|
+
else:
|
|
41
|
+
try:
|
|
42
|
+
scan_results = json.loads(result.stdout)
|
|
43
|
+
except json.JSONDecodeError:
|
|
44
|
+
scan_results = {
|
|
45
|
+
"findings": [],
|
|
46
|
+
"error": f"Security audit failed: {result.stderr[:200]}",
|
|
47
|
+
"total_findings": 0,
|
|
48
|
+
}
|
|
49
|
+
except subprocess.TimeoutExpired:
|
|
50
|
+
scan_results = {"findings": [{"risk": "🟡", "label": "⏱️ Security audit timed out", "file": "N/A", "line": 0, "match": "Timeout >60s"}], "total_findings": 1}
|
|
51
|
+
else:
|
|
52
|
+
scan_results = {"findings": [], "total_findings": 0}
|
|
53
|
+
|
|
54
|
+
# Commit message scan
|
|
55
|
+
try:
|
|
56
|
+
result = subprocess.run(
|
|
57
|
+
["git", "log", "--max-count=10", "--format=%s"],
|
|
58
|
+
capture_output=True, text=True, timeout=10
|
|
59
|
+
)
|
|
60
|
+
commits = result.stdout.strip().splitlines()
|
|
61
|
+
except Exception:
|
|
62
|
+
commits = []
|
|
63
|
+
|
|
64
|
+
if verify_script:
|
|
65
|
+
try:
|
|
66
|
+
commit_result = subprocess.run(
|
|
67
|
+
[sys.executable, verify_script, "--commits", "--json"],
|
|
68
|
+
input="\n".join(commits),
|
|
69
|
+
capture_output=True, text=True, timeout=30
|
|
70
|
+
)
|
|
71
|
+
if commit_result.returncode == 0:
|
|
72
|
+
commit_data = json.loads(commit_result.stdout)
|
|
73
|
+
else:
|
|
74
|
+
try:
|
|
75
|
+
commit_data = json.loads(commit_result.stdout)
|
|
76
|
+
except json.JSONDecodeError:
|
|
77
|
+
commit_data = {"findings": [], "total_messages": 0, "total_findings": 0}
|
|
78
|
+
except Exception:
|
|
79
|
+
commit_data = {"findings": [], "total_messages": 0, "total_findings": 0}
|
|
80
|
+
else:
|
|
81
|
+
commit_data = {"findings": [], "total_messages": 0, "total_findings": 0}
|
|
82
|
+
|
|
83
|
+
all_findings = scan_results.get("findings", [])
|
|
84
|
+
for cf in commit_data.get("findings", []):
|
|
85
|
+
cf["file"] = "commit_message"
|
|
86
|
+
cf["line"] = 0
|
|
87
|
+
all_findings.append(cf)
|
|
88
|
+
|
|
89
|
+
scan_results["findings"] = all_findings
|
|
90
|
+
scan_results["total_findings"] = len(all_findings)
|
|
91
|
+
scan_results["commit_messages_scanned"] = commit_data.get("total_messages", 0)
|
|
92
|
+
return scan_results
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def find_verify_script():
|
|
96
|
+
"""Locate forge-verify.py."""
|
|
97
|
+
# Check next to the installed package
|
|
98
|
+
import os
|
|
99
|
+
pkg_dir = os.path.dirname(__file__)
|
|
100
|
+
for candidate in [
|
|
101
|
+
os.path.join(os.path.dirname(pkg_dir), "forge-verify.py"),
|
|
102
|
+
os.path.join(os.getcwd(), "forge-verify.py"),
|
|
103
|
+
]:
|
|
104
|
+
if os.path.exists(candidate):
|
|
105
|
+
return candidate
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def display_audit_results(results: dict):
|
|
110
|
+
"""Display security audit results."""
|
|
111
|
+
findings = results.get("findings", [])
|
|
112
|
+
commit_count = results.get("commit_messages_scanned", 0)
|
|
113
|
+
|
|
114
|
+
if not findings:
|
|
115
|
+
msg = f" 🛡️ Security Audit: ✅ PASSED — clean"
|
|
116
|
+
if commit_count:
|
|
117
|
+
msg += f" (scanned {commit_count} commits)"
|
|
118
|
+
print(f"\n{msg}")
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
red = [f for f in findings if f.get("risk") == "🔴"]
|
|
122
|
+
yellow = [f for f in findings if f.get("risk") == "🟡"]
|
|
123
|
+
|
|
124
|
+
if red:
|
|
125
|
+
print(f"\n 🛡️ Security Audit: ❌ FAILED — {len(red)} blocking issue(s)")
|
|
126
|
+
for f in red:
|
|
127
|
+
label = "commit" if f.get("file") == "commit_message" else f"{f['file']}:{f['line']}"
|
|
128
|
+
print(f" 🔴 {label} — {f['match']}")
|
|
129
|
+
return False
|
|
130
|
+
elif yellow:
|
|
131
|
+
print(f"\n 🛡️ Security Audit: ⚠️ WARNINGS — {len(yellow)} medium issue(s)")
|
|
132
|
+
for f in yellow:
|
|
133
|
+
label = "commit" if f.get("file") == "commit_message" else f"{f['file']}:{f['line']}"
|
|
134
|
+
print(f" 🟡 {label} — {f['match']}")
|
|
135
|
+
if f.get("commit_msg"):
|
|
136
|
+
print(f" ↳ {f['commit_msg']}")
|
|
137
|
+
if commit_count:
|
|
138
|
+
print(f" (scanned {commit_count} commits)")
|
|
139
|
+
return True
|
|
140
|
+
else:
|
|
141
|
+
msg = f" 🛡️ Security Audit: ✅ PASSED — clean"
|
|
142
|
+
if commit_count:
|
|
143
|
+
msg += f" (scanned {commit_count} commits)"
|
|
144
|
+
print(f"\n{msg}")
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def main():
|
|
149
|
+
parser = argparse.ArgumentParser(
|
|
150
|
+
description="⚒️ Forge — AI smithy orchestrator",
|
|
151
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
152
|
+
epilog="""Examples:
|
|
153
|
+
forge --plan "Telegram bot for tracking GitHub stars"
|
|
154
|
+
forge --plan "..." --scout-only
|
|
155
|
+
forge --resume crucible
|
|
156
|
+
forge --status
|
|
157
|
+
forge --version
|
|
158
|
+
forge --audit
|
|
159
|
+
forge --audit --json
|
|
160
|
+
""",
|
|
161
|
+
)
|
|
162
|
+
parser.add_argument("--plan", type=str, help="Project idea to forge")
|
|
163
|
+
parser.add_argument("--scout-only", action="store_true", help="Only run market research")
|
|
164
|
+
parser.add_argument("--resume", type=str, help="Resume from phase: gate, assemble, crucible, deliver")
|
|
165
|
+
parser.add_argument("--status", action="store_true", help="Show pipeline status")
|
|
166
|
+
parser.add_argument("--version", action="store_true", help="Show version")
|
|
167
|
+
parser.add_argument("--audit", action="store_true", help="Run security audit on current directory")
|
|
168
|
+
parser.add_argument("--json", action="store_true", help="JSON output (with --audit)")
|
|
169
|
+
|
|
170
|
+
args = parser.parse_args()
|
|
171
|
+
|
|
172
|
+
if args.version:
|
|
173
|
+
print(f"Forge v{VERSION}")
|
|
174
|
+
sys.exit(0)
|
|
175
|
+
|
|
176
|
+
if args.audit and not args.plan and not args.resume:
|
|
177
|
+
log("AUDIT", "Running security audit...")
|
|
178
|
+
results = run_security_audit()
|
|
179
|
+
if args.json:
|
|
180
|
+
print(json.dumps(results, indent=2, ensure_ascii=False))
|
|
181
|
+
else:
|
|
182
|
+
passed = display_audit_results(results)
|
|
183
|
+
sys.exit(0 if passed else 1)
|
|
184
|
+
|
|
185
|
+
if args.status or not any([args.plan, args.resume, args.audit, args.version]):
|
|
186
|
+
status()
|
|
187
|
+
sys.exit(0)
|
|
188
|
+
|
|
189
|
+
idea = args.plan or "Unknown"
|
|
190
|
+
log("START", f"⚒️ Forge starting for: {idea}")
|
|
191
|
+
|
|
192
|
+
if args.scout_only:
|
|
193
|
+
log("SCOUT", f"Market research for: {idea}")
|
|
194
|
+
print(json.dumps({
|
|
195
|
+
"phase": "scout",
|
|
196
|
+
"idea": idea,
|
|
197
|
+
"message": "Run delegate_task for 3 parallel scouts",
|
|
198
|
+
"next": "review reports → gate"
|
|
199
|
+
}, indent=2))
|
|
200
|
+
elif args.resume:
|
|
201
|
+
log("RESUME", f"Resuming from phase: {args.resume}")
|
|
202
|
+
else:
|
|
203
|
+
log("SCOUT", "Phase 1: Dispatching Talaria scouts...")
|
|
204
|
+
pipeline = {
|
|
205
|
+
"scout": "3 parallel scouts (competitors, demand, architecture)",
|
|
206
|
+
"gate": "User approval required",
|
|
207
|
+
"assemble": "5 sub-agents + hands-on fixes",
|
|
208
|
+
"security_audit_1": "🛡️ After assembly — secrets, personal data, NSFW, Discord",
|
|
209
|
+
"crucible": "Verification checklist + pre-release security audit",
|
|
210
|
+
"security_audit_2": "🛡️ Before release — repo contents, CVE, README-reality",
|
|
211
|
+
"deliver": "Release + documentation",
|
|
212
|
+
"security_audit_3": "🛡️ After release — release notes, PyPI, CI logs",
|
|
213
|
+
}
|
|
214
|
+
print(json.dumps({
|
|
215
|
+
"pipeline": pipeline,
|
|
216
|
+
"idea": idea,
|
|
217
|
+
"version": VERSION,
|
|
218
|
+
"next_step": "Run 'forge --audit' for security scan"
|
|
219
|
+
}, indent=2, ensure_ascii=False))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
if __name__ == "__main__":
|
|
223
|
+
main()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
⚒️ Forge — AI smithy orchestrator.
|
|
4
|
+
|
|
5
|
+
Part of SvarogForge (github.com/SvarogForge/forge).
|
|
6
|
+
For Hermes Agent (hermes-agent.nousresearch.com).
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python forge.py --plan "Telegram bot for tracking GitHub stars"
|
|
10
|
+
python forge.py --plan "..." --scout-only
|
|
11
|
+
python forge.py --resume crucible
|
|
12
|
+
python forge.py --status
|
|
13
|
+
python forge.py --audit # Security audit on current directory
|
|
14
|
+
python forge.py --audit --plan "..." # Full cycle with security audit
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from forge.cli import main
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
main()
|
svarog_forge-2.2.0/forge/cli.py
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
"""CLI entry point for Forge — imported via pip-installed svarog-forge package."""
|
|
2
|
-
import sys
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
|
-
# Allow importing forge.py from the package root
|
|
6
|
-
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
|
7
|
-
|
|
8
|
-
from forge import main # noqa: E402
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if __name__ == "__main__":
|
|
12
|
-
main()
|
|
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
|