cilydian 0.1.3__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.
- cilydian-0.1.3/LICENSE +21 -0
- cilydian-0.1.3/PKG-INFO +271 -0
- cilydian-0.1.3/README.md +255 -0
- cilydian-0.1.3/cilydian.egg-info/PKG-INFO +271 -0
- cilydian-0.1.3/cilydian.egg-info/SOURCES.txt +10 -0
- cilydian-0.1.3/cilydian.egg-info/dependency_links.txt +1 -0
- cilydian-0.1.3/cilydian.egg-info/entry_points.txt +2 -0
- cilydian-0.1.3/cilydian.egg-info/requires.txt +3 -0
- cilydian-0.1.3/cilydian.egg-info/top_level.txt +1 -0
- cilydian-0.1.3/pipeline.py +379 -0
- cilydian-0.1.3/pyproject.toml +30 -0
- cilydian-0.1.3/setup.cfg +4 -0
cilydian-0.1.3/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sleepti3ht
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
cilydian-0.1.3/PKG-INFO
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cilydian
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Self-hosted agentic CI pipeline with spec-driven codegen
|
|
5
|
+
Author-email: sleepti3ht <sleepti3ht@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sleepti3ht/CiLydian
|
|
8
|
+
Project-URL: Repository, https://github.com/sleepti3ht/CiLydian
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: langgraph>=1.0
|
|
13
|
+
Requires-Dist: langchain-ollama>=1.0
|
|
14
|
+
Requires-Dist: langchain-openai>=1.0
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
<div align="center">
|
|
18
|
+
|
|
19
|
+
# CiLydian
|
|
20
|
+
|
|
21
|
+
[](https://python.org)
|
|
22
|
+
[](https://langchain-ai.github.io/langgraph/)
|
|
23
|
+
[](https://github.com)
|
|
24
|
+
[](LICENSE)
|
|
25
|
+
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
> Self-hosted agentic CI pipeline on LangGraph: **spec → coder → LLM judge (advisory) → deterministic gates (ruff + pytest)**, with a bounded repair loop and human escalation. Models are pluggable per role — local Ollama or any OpenAI-compatible endpoint.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Why CiLydian?
|
|
33
|
+
|
|
34
|
+
Most multi-agent coding tools stop at "generate and hope." CiLydian treats LLM output as untrusted input: mechanical gates (ruff, pytest) are the source of truth, the LLM judge is advisory feedback, and the loop is bounded. If the pipeline can't converge in `MAX_ITER`, it exits with a status code and the full log — never hangs, never burns tokens indefinitely.
|
|
35
|
+
|
|
36
|
+
Compared to AutoGen Studio or CrewAI: those are chat-orchestration frameworks. CiLydian is a **CI primitive** — it runs, exits with a status code, and can be wired into git hooks, GitHub Actions, or cron.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Clone and setup
|
|
44
|
+
git clone https://github.com/sleepti3ht/cilydian.git
|
|
45
|
+
cd cilydian
|
|
46
|
+
|
|
47
|
+
# Unix / macOS
|
|
48
|
+
python -m venv .venv && source .venv/bin/activate
|
|
49
|
+
# Windows (PowerShell)
|
|
50
|
+
python -m venv .venv; .\.venv\Scripts\Activate.ps1
|
|
51
|
+
|
|
52
|
+
pip install -r requirements.txt
|
|
53
|
+
|
|
54
|
+
# Run with local Ollama (default)
|
|
55
|
+
python pipeline.py "Your task description"
|
|
56
|
+
|
|
57
|
+
# Or use cloud profile (free tiers)
|
|
58
|
+
cp config.cloud.toml.example config.toml
|
|
59
|
+
cp .env.example .env # fill with OpenRouter/Groq keys
|
|
60
|
+
python pipeline.py "Your task description"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Stack: `langgraph`, `langchain-ollama`, `langchain-openai`, `ruff`, `pytest`. See `requirements.txt`.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Features
|
|
68
|
+
|
|
69
|
+
### Core
|
|
70
|
+
- **Spec-driven generation**: turn one-line ideas into a `SPEC.md` and a `test_candidate.py` skeleton before any code is written
|
|
71
|
+
- **Bounded repair loop**: coder iterates on judge feedback up to `MAX_ITER` times, then escalates to a human
|
|
72
|
+
- **Advisory LLM judge**: semantic review that never blocks on its own verdict (see [Design notes](#design-notes))
|
|
73
|
+
- **Deterministic gates**: `ruff` (auto-fix + check) and `pytest`; these are what actually block the pipeline
|
|
74
|
+
- **Short-circuit**: pytest is skipped if ruff fails, saves 10–20s per failed cycle
|
|
75
|
+
- **Config-driven models**: mix Ollama local and OpenAI-compatible cloud per role
|
|
76
|
+
- **Rate-limit resilience**: exponential backoff for 429s on free tiers
|
|
77
|
+
- **UTF-8 on Windows**: subprocess encoding prevents `cp1251` mojibake in logs
|
|
78
|
+
|
|
79
|
+
### Reliability
|
|
80
|
+
- **Defensive JSON parsing**: judge verdicts normalized via `try: int(float(raw))`
|
|
81
|
+
- **Programmatic guardrail**: `score < 4` with `verdict="pass"` is auto-downgraded to `fail` (see [Design notes](#design-notes))
|
|
82
|
+
- **Markdown fence stripping**: handles LLM code wrapped in \`\`\` fences
|
|
83
|
+
- **Log rotation**: 5 MB cap with atomic `os.replace()`; single writer, no race
|
|
84
|
+
- **Isolated gates**: `logs/ruff.toml` and `logs/pytest.ini` are auto-created on first run to prevent parent-config bleed
|
|
85
|
+
- **Preflight checks**: verifies `ruff` / `pytest` are on PATH and the coder endpoint is reachable before spending a single token
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Design notes
|
|
90
|
+
|
|
91
|
+
**The judge is advisory, the guardrails are not.** Local 7B–14B judges drift beyond SPEC — they request absent features, over- or under-rate code, and sometimes contradict themselves. So the LLM's verdict alone never blocks the pipeline. A separate programmatic rule handles one specific pathological case: if the LLM outputs `verdict="pass"` while scoring below 4, the guardrail downgrades it to `fail`. That's not the judge blocking — it's a deterministic check that doesn't trust a self-contradicting model. Mechanical gates (ruff, pytest) remain the ultimate source of truth.
|
|
92
|
+
|
|
93
|
+
**The repair loop is bounded on purpose.** `MAX_ITER` (default 3) caps attempts. Exhaustion means human escalation, not a hang. On complex tasks this may give up earlier than a human reviewer would prefer — acceptable for CI, where the cost of a false-negative is a re-run, not a lost afternoon.
|
|
94
|
+
|
|
95
|
+
**Mechanical fixes belong to the tool.** `ruff --fix` runs before `ruff check` and auto-corrects imports and formatting. LLM tokens are too expensive for style fixes a Rust binary does in 40 milliseconds.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Architecture
|
|
100
|
+
|
|
101
|
+
```mermaid
|
|
102
|
+
flowchart LR
|
|
103
|
+
classDef solarizedBase3 fill:#fdf6e3,stroke:#93a1a1,color:#657b83,stroke-width:1px
|
|
104
|
+
classDef solarizedBase02 fill:#eee8d5,stroke:#93a1a1,color:#586e75,stroke-width:1px
|
|
105
|
+
classDef solarizedYellow fill:#b58900,stroke:#b58900,color:#fdf6e3,stroke-width:2px
|
|
106
|
+
classDef solarizedOrange fill:#cb4b16,stroke:#cb4b16,color:#fdf6e3,stroke-width:2px
|
|
107
|
+
classDef solarizedGreen fill:#859900,stroke:#859900,color:#fdf6e3,stroke-width:2px
|
|
108
|
+
classDef solarizedBlue fill:#268bd2,stroke:#268bd2,color:#fdf6e3,stroke-width:2px
|
|
109
|
+
classDef solarizedCyan fill:#2aa198,stroke:#2aa198,color:#fdf6e3,stroke-width:2px
|
|
110
|
+
|
|
111
|
+
A[idea]:::solarizedYellow
|
|
112
|
+
S[spec_writer<br>role: spec]:::solarizedBlue
|
|
113
|
+
C[coder<br>role: coder]:::solarizedGreen
|
|
114
|
+
J[judge<br>advisory]:::solarizedOrange
|
|
115
|
+
T[tester<br>ruff + pytest]:::solarizedCyan
|
|
116
|
+
E[final verdict<br>exit code]:::solarizedBase02
|
|
117
|
+
|
|
118
|
+
A --> S
|
|
119
|
+
S --> C
|
|
120
|
+
C --> J
|
|
121
|
+
J -->|advisory| T
|
|
122
|
+
T -->|fail and iter < max_iter| C
|
|
123
|
+
T -->|pass or budget exhausted| E
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Configuration
|
|
129
|
+
|
|
130
|
+
### Local Ollama profile (default)
|
|
131
|
+
|
|
132
|
+
```toml
|
|
133
|
+
# config.toml
|
|
134
|
+
[pipeline]
|
|
135
|
+
max_iter = 3
|
|
136
|
+
judge_blocking = false
|
|
137
|
+
|
|
138
|
+
[roles.spec]
|
|
139
|
+
provider = "ollama"
|
|
140
|
+
model = "qwen2.5-coder:7b"
|
|
141
|
+
|
|
142
|
+
[roles.coder]
|
|
143
|
+
provider = "ollama"
|
|
144
|
+
model = "qwen2.5-coder:7b"
|
|
145
|
+
|
|
146
|
+
[roles.judge]
|
|
147
|
+
provider = "ollama"
|
|
148
|
+
model = "qwen2.5:14b"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Cloud hybrid profile (free tiers)
|
|
152
|
+
|
|
153
|
+
```toml
|
|
154
|
+
# config.cloud.toml.example
|
|
155
|
+
[pipeline]
|
|
156
|
+
max_iter = 3
|
|
157
|
+
judge_blocking = false
|
|
158
|
+
|
|
159
|
+
[roles.spec]
|
|
160
|
+
provider = "openai_compat"
|
|
161
|
+
base_url = "https://openrouter.ai/api/v1"
|
|
162
|
+
model = "deepseek/deepseek-chat"
|
|
163
|
+
api_key_env = "OPENROUTER_API_KEY"
|
|
164
|
+
|
|
165
|
+
[roles.coder]
|
|
166
|
+
provider = "openai_compat"
|
|
167
|
+
base_url = "https://openrouter.ai/api/v1"
|
|
168
|
+
model = "deepseek/deepseek-chat"
|
|
169
|
+
api_key_env = "OPENROUTER_API_KEY"
|
|
170
|
+
|
|
171
|
+
[roles.judge]
|
|
172
|
+
provider = "openai_compat"
|
|
173
|
+
base_url = "https://api.groq.com/openai/v1"
|
|
174
|
+
model = "llama-3.3-70b-versatile"
|
|
175
|
+
api_key_env = "GROQ_API_KEY"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# .env
|
|
180
|
+
OPENROUTER_API_KEY=your_key_here
|
|
181
|
+
GROQ_API_KEY=your_key_here
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
> ⚠️ Never commit `.env` — keep secrets private. Config references env var names, not values.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Performance
|
|
189
|
+
|
|
190
|
+
| Metric | Local (7B coder) | Cloud (frontier) |
|
|
191
|
+
|---|---|---|
|
|
192
|
+
| Coder throughput | ~15 tok/s | ~50–100 tok/s |
|
|
193
|
+
| Judge throughput | ~3–6 tok/s (partial CPU offload) | ~30–60 tok/s |
|
|
194
|
+
| Full cycle wall time | 4–15 min | 1–3 min |
|
|
195
|
+
| Exit code | 0 = gates passed, 1 = failed | 0 = gates passed, 1 = failed |
|
|
196
|
+
|
|
197
|
+
**Typical run:**
|
|
198
|
+
|
|
199
|
+
```log
|
|
200
|
+
[preflight] ruff: OK
|
|
201
|
+
[preflight] pytest: OK
|
|
202
|
+
[preflight] coder endpoint: reachable
|
|
203
|
+
[spec] Generated SPEC.md + test_candidate.py (1.2s)
|
|
204
|
+
[coder] Iteration 1/3 — wrote candidate.py (3.4s)
|
|
205
|
+
[judge] score=3, verdict=fail (advisory)
|
|
206
|
+
[tester] ruff rc=0, pytest rc=1 (fail)
|
|
207
|
+
[coder] Iteration 2/3 — fixed issues (2.8s)
|
|
208
|
+
[judge] score=4, verdict=pass (advisory)
|
|
209
|
+
[tester] ruff rc=0, pytest rc=0 (pass)
|
|
210
|
+
✓ Pipeline completed: gates passed (exit code 0)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Honest Limitations
|
|
216
|
+
|
|
217
|
+
- **Local models (7b-14b) fail convergence** on mid-complexity tasks (e.g., sliding-window rate limiter with exact boolean return) within 3 iterations.
|
|
218
|
+
- **Cloud free-tier models (Groq gpt-oss-120b)** provide better judge quality but have rate limits (200K tokens/day).
|
|
219
|
+
- **CiLydian is an experiment and portfolio artifact**, not a daily development tool. For production code, use manual multi-vendor workflow (DeepSeek/Qwen for prompting, Claude for architecture, Gemini for review, local ruff/pytest for gates).
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Screenshot
|
|
224
|
+
|
|
225
|
+

|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Roadmap
|
|
230
|
+
|
|
231
|
+
- [ ] **GitHub Actions integration** — trigger CiLydian on issue labels
|
|
232
|
+
- [ ] **Multi-language support** — extend beyond Python (JavaScript, Go)
|
|
233
|
+
- [ ] **Persistent state** — resume interrupted runs from checkpoint
|
|
234
|
+
- [ ] **Web dashboard** — visualize pipeline runs and logs
|
|
235
|
+
- [ ] **Plugin system** — custom gates beyond ruff/pytest (mypy, bandit)
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Contributing
|
|
240
|
+
|
|
241
|
+
Contributions are welcome! Please open an issue or submit a PR.
|
|
242
|
+
|
|
243
|
+
### Development setup
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
git clone https://github.com/sleepti3ht/cilydian.git
|
|
247
|
+
cd cilydian
|
|
248
|
+
|
|
249
|
+
# Unix / macOS
|
|
250
|
+
python -m venv .venv && source .venv/bin/activate
|
|
251
|
+
# Windows (PowerShell)
|
|
252
|
+
python -m venv .venv; .\.venv\Scripts\Activate.ps1
|
|
253
|
+
|
|
254
|
+
pip install -r requirements.txt
|
|
255
|
+
pip install pre-commit
|
|
256
|
+
pre-commit install # enforces ruff + pytest on commit
|
|
257
|
+
|
|
258
|
+
# Run the pipeline on a sample task
|
|
259
|
+
python pipeline.py "Test task"
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Code style
|
|
263
|
+
|
|
264
|
+
- Commit messages: Conventional Commits (`feat(pipeline): ...`, `fix(tester): ...`)
|
|
265
|
+
- Run `ruff check` and `pytest` before pushing (enforced by pre-commit)
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## License
|
|
270
|
+
|
|
271
|
+
MIT
|
cilydian-0.1.3/README.md
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# CiLydian
|
|
4
|
+
|
|
5
|
+
[](https://python.org)
|
|
6
|
+
[](https://langchain-ai.github.io/langgraph/)
|
|
7
|
+
[](https://github.com)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
> Self-hosted agentic CI pipeline on LangGraph: **spec → coder → LLM judge (advisory) → deterministic gates (ruff + pytest)**, with a bounded repair loop and human escalation. Models are pluggable per role — local Ollama or any OpenAI-compatible endpoint.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Why CiLydian?
|
|
17
|
+
|
|
18
|
+
Most multi-agent coding tools stop at "generate and hope." CiLydian treats LLM output as untrusted input: mechanical gates (ruff, pytest) are the source of truth, the LLM judge is advisory feedback, and the loop is bounded. If the pipeline can't converge in `MAX_ITER`, it exits with a status code and the full log — never hangs, never burns tokens indefinitely.
|
|
19
|
+
|
|
20
|
+
Compared to AutoGen Studio or CrewAI: those are chat-orchestration frameworks. CiLydian is a **CI primitive** — it runs, exits with a status code, and can be wired into git hooks, GitHub Actions, or cron.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Clone and setup
|
|
28
|
+
git clone https://github.com/sleepti3ht/cilydian.git
|
|
29
|
+
cd cilydian
|
|
30
|
+
|
|
31
|
+
# Unix / macOS
|
|
32
|
+
python -m venv .venv && source .venv/bin/activate
|
|
33
|
+
# Windows (PowerShell)
|
|
34
|
+
python -m venv .venv; .\.venv\Scripts\Activate.ps1
|
|
35
|
+
|
|
36
|
+
pip install -r requirements.txt
|
|
37
|
+
|
|
38
|
+
# Run with local Ollama (default)
|
|
39
|
+
python pipeline.py "Your task description"
|
|
40
|
+
|
|
41
|
+
# Or use cloud profile (free tiers)
|
|
42
|
+
cp config.cloud.toml.example config.toml
|
|
43
|
+
cp .env.example .env # fill with OpenRouter/Groq keys
|
|
44
|
+
python pipeline.py "Your task description"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Stack: `langgraph`, `langchain-ollama`, `langchain-openai`, `ruff`, `pytest`. See `requirements.txt`.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
### Core
|
|
54
|
+
- **Spec-driven generation**: turn one-line ideas into a `SPEC.md` and a `test_candidate.py` skeleton before any code is written
|
|
55
|
+
- **Bounded repair loop**: coder iterates on judge feedback up to `MAX_ITER` times, then escalates to a human
|
|
56
|
+
- **Advisory LLM judge**: semantic review that never blocks on its own verdict (see [Design notes](#design-notes))
|
|
57
|
+
- **Deterministic gates**: `ruff` (auto-fix + check) and `pytest`; these are what actually block the pipeline
|
|
58
|
+
- **Short-circuit**: pytest is skipped if ruff fails, saves 10–20s per failed cycle
|
|
59
|
+
- **Config-driven models**: mix Ollama local and OpenAI-compatible cloud per role
|
|
60
|
+
- **Rate-limit resilience**: exponential backoff for 429s on free tiers
|
|
61
|
+
- **UTF-8 on Windows**: subprocess encoding prevents `cp1251` mojibake in logs
|
|
62
|
+
|
|
63
|
+
### Reliability
|
|
64
|
+
- **Defensive JSON parsing**: judge verdicts normalized via `try: int(float(raw))`
|
|
65
|
+
- **Programmatic guardrail**: `score < 4` with `verdict="pass"` is auto-downgraded to `fail` (see [Design notes](#design-notes))
|
|
66
|
+
- **Markdown fence stripping**: handles LLM code wrapped in \`\`\` fences
|
|
67
|
+
- **Log rotation**: 5 MB cap with atomic `os.replace()`; single writer, no race
|
|
68
|
+
- **Isolated gates**: `logs/ruff.toml` and `logs/pytest.ini` are auto-created on first run to prevent parent-config bleed
|
|
69
|
+
- **Preflight checks**: verifies `ruff` / `pytest` are on PATH and the coder endpoint is reachable before spending a single token
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Design notes
|
|
74
|
+
|
|
75
|
+
**The judge is advisory, the guardrails are not.** Local 7B–14B judges drift beyond SPEC — they request absent features, over- or under-rate code, and sometimes contradict themselves. So the LLM's verdict alone never blocks the pipeline. A separate programmatic rule handles one specific pathological case: if the LLM outputs `verdict="pass"` while scoring below 4, the guardrail downgrades it to `fail`. That's not the judge blocking — it's a deterministic check that doesn't trust a self-contradicting model. Mechanical gates (ruff, pytest) remain the ultimate source of truth.
|
|
76
|
+
|
|
77
|
+
**The repair loop is bounded on purpose.** `MAX_ITER` (default 3) caps attempts. Exhaustion means human escalation, not a hang. On complex tasks this may give up earlier than a human reviewer would prefer — acceptable for CI, where the cost of a false-negative is a re-run, not a lost afternoon.
|
|
78
|
+
|
|
79
|
+
**Mechanical fixes belong to the tool.** `ruff --fix` runs before `ruff check` and auto-corrects imports and formatting. LLM tokens are too expensive for style fixes a Rust binary does in 40 milliseconds.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Architecture
|
|
84
|
+
|
|
85
|
+
```mermaid
|
|
86
|
+
flowchart LR
|
|
87
|
+
classDef solarizedBase3 fill:#fdf6e3,stroke:#93a1a1,color:#657b83,stroke-width:1px
|
|
88
|
+
classDef solarizedBase02 fill:#eee8d5,stroke:#93a1a1,color:#586e75,stroke-width:1px
|
|
89
|
+
classDef solarizedYellow fill:#b58900,stroke:#b58900,color:#fdf6e3,stroke-width:2px
|
|
90
|
+
classDef solarizedOrange fill:#cb4b16,stroke:#cb4b16,color:#fdf6e3,stroke-width:2px
|
|
91
|
+
classDef solarizedGreen fill:#859900,stroke:#859900,color:#fdf6e3,stroke-width:2px
|
|
92
|
+
classDef solarizedBlue fill:#268bd2,stroke:#268bd2,color:#fdf6e3,stroke-width:2px
|
|
93
|
+
classDef solarizedCyan fill:#2aa198,stroke:#2aa198,color:#fdf6e3,stroke-width:2px
|
|
94
|
+
|
|
95
|
+
A[idea]:::solarizedYellow
|
|
96
|
+
S[spec_writer<br>role: spec]:::solarizedBlue
|
|
97
|
+
C[coder<br>role: coder]:::solarizedGreen
|
|
98
|
+
J[judge<br>advisory]:::solarizedOrange
|
|
99
|
+
T[tester<br>ruff + pytest]:::solarizedCyan
|
|
100
|
+
E[final verdict<br>exit code]:::solarizedBase02
|
|
101
|
+
|
|
102
|
+
A --> S
|
|
103
|
+
S --> C
|
|
104
|
+
C --> J
|
|
105
|
+
J -->|advisory| T
|
|
106
|
+
T -->|fail and iter < max_iter| C
|
|
107
|
+
T -->|pass or budget exhausted| E
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Configuration
|
|
113
|
+
|
|
114
|
+
### Local Ollama profile (default)
|
|
115
|
+
|
|
116
|
+
```toml
|
|
117
|
+
# config.toml
|
|
118
|
+
[pipeline]
|
|
119
|
+
max_iter = 3
|
|
120
|
+
judge_blocking = false
|
|
121
|
+
|
|
122
|
+
[roles.spec]
|
|
123
|
+
provider = "ollama"
|
|
124
|
+
model = "qwen2.5-coder:7b"
|
|
125
|
+
|
|
126
|
+
[roles.coder]
|
|
127
|
+
provider = "ollama"
|
|
128
|
+
model = "qwen2.5-coder:7b"
|
|
129
|
+
|
|
130
|
+
[roles.judge]
|
|
131
|
+
provider = "ollama"
|
|
132
|
+
model = "qwen2.5:14b"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Cloud hybrid profile (free tiers)
|
|
136
|
+
|
|
137
|
+
```toml
|
|
138
|
+
# config.cloud.toml.example
|
|
139
|
+
[pipeline]
|
|
140
|
+
max_iter = 3
|
|
141
|
+
judge_blocking = false
|
|
142
|
+
|
|
143
|
+
[roles.spec]
|
|
144
|
+
provider = "openai_compat"
|
|
145
|
+
base_url = "https://openrouter.ai/api/v1"
|
|
146
|
+
model = "deepseek/deepseek-chat"
|
|
147
|
+
api_key_env = "OPENROUTER_API_KEY"
|
|
148
|
+
|
|
149
|
+
[roles.coder]
|
|
150
|
+
provider = "openai_compat"
|
|
151
|
+
base_url = "https://openrouter.ai/api/v1"
|
|
152
|
+
model = "deepseek/deepseek-chat"
|
|
153
|
+
api_key_env = "OPENROUTER_API_KEY"
|
|
154
|
+
|
|
155
|
+
[roles.judge]
|
|
156
|
+
provider = "openai_compat"
|
|
157
|
+
base_url = "https://api.groq.com/openai/v1"
|
|
158
|
+
model = "llama-3.3-70b-versatile"
|
|
159
|
+
api_key_env = "GROQ_API_KEY"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# .env
|
|
164
|
+
OPENROUTER_API_KEY=your_key_here
|
|
165
|
+
GROQ_API_KEY=your_key_here
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
> ⚠️ Never commit `.env` — keep secrets private. Config references env var names, not values.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Performance
|
|
173
|
+
|
|
174
|
+
| Metric | Local (7B coder) | Cloud (frontier) |
|
|
175
|
+
|---|---|---|
|
|
176
|
+
| Coder throughput | ~15 tok/s | ~50–100 tok/s |
|
|
177
|
+
| Judge throughput | ~3–6 tok/s (partial CPU offload) | ~30–60 tok/s |
|
|
178
|
+
| Full cycle wall time | 4–15 min | 1–3 min |
|
|
179
|
+
| Exit code | 0 = gates passed, 1 = failed | 0 = gates passed, 1 = failed |
|
|
180
|
+
|
|
181
|
+
**Typical run:**
|
|
182
|
+
|
|
183
|
+
```log
|
|
184
|
+
[preflight] ruff: OK
|
|
185
|
+
[preflight] pytest: OK
|
|
186
|
+
[preflight] coder endpoint: reachable
|
|
187
|
+
[spec] Generated SPEC.md + test_candidate.py (1.2s)
|
|
188
|
+
[coder] Iteration 1/3 — wrote candidate.py (3.4s)
|
|
189
|
+
[judge] score=3, verdict=fail (advisory)
|
|
190
|
+
[tester] ruff rc=0, pytest rc=1 (fail)
|
|
191
|
+
[coder] Iteration 2/3 — fixed issues (2.8s)
|
|
192
|
+
[judge] score=4, verdict=pass (advisory)
|
|
193
|
+
[tester] ruff rc=0, pytest rc=0 (pass)
|
|
194
|
+
✓ Pipeline completed: gates passed (exit code 0)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Honest Limitations
|
|
200
|
+
|
|
201
|
+
- **Local models (7b-14b) fail convergence** on mid-complexity tasks (e.g., sliding-window rate limiter with exact boolean return) within 3 iterations.
|
|
202
|
+
- **Cloud free-tier models (Groq gpt-oss-120b)** provide better judge quality but have rate limits (200K tokens/day).
|
|
203
|
+
- **CiLydian is an experiment and portfolio artifact**, not a daily development tool. For production code, use manual multi-vendor workflow (DeepSeek/Qwen for prompting, Claude for architecture, Gemini for review, local ruff/pytest for gates).
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Screenshot
|
|
208
|
+
|
|
209
|
+

|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Roadmap
|
|
214
|
+
|
|
215
|
+
- [ ] **GitHub Actions integration** — trigger CiLydian on issue labels
|
|
216
|
+
- [ ] **Multi-language support** — extend beyond Python (JavaScript, Go)
|
|
217
|
+
- [ ] **Persistent state** — resume interrupted runs from checkpoint
|
|
218
|
+
- [ ] **Web dashboard** — visualize pipeline runs and logs
|
|
219
|
+
- [ ] **Plugin system** — custom gates beyond ruff/pytest (mypy, bandit)
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Contributing
|
|
224
|
+
|
|
225
|
+
Contributions are welcome! Please open an issue or submit a PR.
|
|
226
|
+
|
|
227
|
+
### Development setup
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
git clone https://github.com/sleepti3ht/cilydian.git
|
|
231
|
+
cd cilydian
|
|
232
|
+
|
|
233
|
+
# Unix / macOS
|
|
234
|
+
python -m venv .venv && source .venv/bin/activate
|
|
235
|
+
# Windows (PowerShell)
|
|
236
|
+
python -m venv .venv; .\.venv\Scripts\Activate.ps1
|
|
237
|
+
|
|
238
|
+
pip install -r requirements.txt
|
|
239
|
+
pip install pre-commit
|
|
240
|
+
pre-commit install # enforces ruff + pytest on commit
|
|
241
|
+
|
|
242
|
+
# Run the pipeline on a sample task
|
|
243
|
+
python pipeline.py "Test task"
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Code style
|
|
247
|
+
|
|
248
|
+
- Commit messages: Conventional Commits (`feat(pipeline): ...`, `fix(tester): ...`)
|
|
249
|
+
- Run `ruff check` and `pytest` before pushing (enforced by pre-commit)
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## License
|
|
254
|
+
|
|
255
|
+
MIT
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cilydian
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Self-hosted agentic CI pipeline with spec-driven codegen
|
|
5
|
+
Author-email: sleepti3ht <sleepti3ht@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sleepti3ht/CiLydian
|
|
8
|
+
Project-URL: Repository, https://github.com/sleepti3ht/CiLydian
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: langgraph>=1.0
|
|
13
|
+
Requires-Dist: langchain-ollama>=1.0
|
|
14
|
+
Requires-Dist: langchain-openai>=1.0
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
<div align="center">
|
|
18
|
+
|
|
19
|
+
# CiLydian
|
|
20
|
+
|
|
21
|
+
[](https://python.org)
|
|
22
|
+
[](https://langchain-ai.github.io/langgraph/)
|
|
23
|
+
[](https://github.com)
|
|
24
|
+
[](LICENSE)
|
|
25
|
+
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
> Self-hosted agentic CI pipeline on LangGraph: **spec → coder → LLM judge (advisory) → deterministic gates (ruff + pytest)**, with a bounded repair loop and human escalation. Models are pluggable per role — local Ollama or any OpenAI-compatible endpoint.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Why CiLydian?
|
|
33
|
+
|
|
34
|
+
Most multi-agent coding tools stop at "generate and hope." CiLydian treats LLM output as untrusted input: mechanical gates (ruff, pytest) are the source of truth, the LLM judge is advisory feedback, and the loop is bounded. If the pipeline can't converge in `MAX_ITER`, it exits with a status code and the full log — never hangs, never burns tokens indefinitely.
|
|
35
|
+
|
|
36
|
+
Compared to AutoGen Studio or CrewAI: those are chat-orchestration frameworks. CiLydian is a **CI primitive** — it runs, exits with a status code, and can be wired into git hooks, GitHub Actions, or cron.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Clone and setup
|
|
44
|
+
git clone https://github.com/sleepti3ht/cilydian.git
|
|
45
|
+
cd cilydian
|
|
46
|
+
|
|
47
|
+
# Unix / macOS
|
|
48
|
+
python -m venv .venv && source .venv/bin/activate
|
|
49
|
+
# Windows (PowerShell)
|
|
50
|
+
python -m venv .venv; .\.venv\Scripts\Activate.ps1
|
|
51
|
+
|
|
52
|
+
pip install -r requirements.txt
|
|
53
|
+
|
|
54
|
+
# Run with local Ollama (default)
|
|
55
|
+
python pipeline.py "Your task description"
|
|
56
|
+
|
|
57
|
+
# Or use cloud profile (free tiers)
|
|
58
|
+
cp config.cloud.toml.example config.toml
|
|
59
|
+
cp .env.example .env # fill with OpenRouter/Groq keys
|
|
60
|
+
python pipeline.py "Your task description"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Stack: `langgraph`, `langchain-ollama`, `langchain-openai`, `ruff`, `pytest`. See `requirements.txt`.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Features
|
|
68
|
+
|
|
69
|
+
### Core
|
|
70
|
+
- **Spec-driven generation**: turn one-line ideas into a `SPEC.md` and a `test_candidate.py` skeleton before any code is written
|
|
71
|
+
- **Bounded repair loop**: coder iterates on judge feedback up to `MAX_ITER` times, then escalates to a human
|
|
72
|
+
- **Advisory LLM judge**: semantic review that never blocks on its own verdict (see [Design notes](#design-notes))
|
|
73
|
+
- **Deterministic gates**: `ruff` (auto-fix + check) and `pytest`; these are what actually block the pipeline
|
|
74
|
+
- **Short-circuit**: pytest is skipped if ruff fails, saves 10–20s per failed cycle
|
|
75
|
+
- **Config-driven models**: mix Ollama local and OpenAI-compatible cloud per role
|
|
76
|
+
- **Rate-limit resilience**: exponential backoff for 429s on free tiers
|
|
77
|
+
- **UTF-8 on Windows**: subprocess encoding prevents `cp1251` mojibake in logs
|
|
78
|
+
|
|
79
|
+
### Reliability
|
|
80
|
+
- **Defensive JSON parsing**: judge verdicts normalized via `try: int(float(raw))`
|
|
81
|
+
- **Programmatic guardrail**: `score < 4` with `verdict="pass"` is auto-downgraded to `fail` (see [Design notes](#design-notes))
|
|
82
|
+
- **Markdown fence stripping**: handles LLM code wrapped in \`\`\` fences
|
|
83
|
+
- **Log rotation**: 5 MB cap with atomic `os.replace()`; single writer, no race
|
|
84
|
+
- **Isolated gates**: `logs/ruff.toml` and `logs/pytest.ini` are auto-created on first run to prevent parent-config bleed
|
|
85
|
+
- **Preflight checks**: verifies `ruff` / `pytest` are on PATH and the coder endpoint is reachable before spending a single token
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Design notes
|
|
90
|
+
|
|
91
|
+
**The judge is advisory, the guardrails are not.** Local 7B–14B judges drift beyond SPEC — they request absent features, over- or under-rate code, and sometimes contradict themselves. So the LLM's verdict alone never blocks the pipeline. A separate programmatic rule handles one specific pathological case: if the LLM outputs `verdict="pass"` while scoring below 4, the guardrail downgrades it to `fail`. That's not the judge blocking — it's a deterministic check that doesn't trust a self-contradicting model. Mechanical gates (ruff, pytest) remain the ultimate source of truth.
|
|
92
|
+
|
|
93
|
+
**The repair loop is bounded on purpose.** `MAX_ITER` (default 3) caps attempts. Exhaustion means human escalation, not a hang. On complex tasks this may give up earlier than a human reviewer would prefer — acceptable for CI, where the cost of a false-negative is a re-run, not a lost afternoon.
|
|
94
|
+
|
|
95
|
+
**Mechanical fixes belong to the tool.** `ruff --fix` runs before `ruff check` and auto-corrects imports and formatting. LLM tokens are too expensive for style fixes a Rust binary does in 40 milliseconds.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Architecture
|
|
100
|
+
|
|
101
|
+
```mermaid
|
|
102
|
+
flowchart LR
|
|
103
|
+
classDef solarizedBase3 fill:#fdf6e3,stroke:#93a1a1,color:#657b83,stroke-width:1px
|
|
104
|
+
classDef solarizedBase02 fill:#eee8d5,stroke:#93a1a1,color:#586e75,stroke-width:1px
|
|
105
|
+
classDef solarizedYellow fill:#b58900,stroke:#b58900,color:#fdf6e3,stroke-width:2px
|
|
106
|
+
classDef solarizedOrange fill:#cb4b16,stroke:#cb4b16,color:#fdf6e3,stroke-width:2px
|
|
107
|
+
classDef solarizedGreen fill:#859900,stroke:#859900,color:#fdf6e3,stroke-width:2px
|
|
108
|
+
classDef solarizedBlue fill:#268bd2,stroke:#268bd2,color:#fdf6e3,stroke-width:2px
|
|
109
|
+
classDef solarizedCyan fill:#2aa198,stroke:#2aa198,color:#fdf6e3,stroke-width:2px
|
|
110
|
+
|
|
111
|
+
A[idea]:::solarizedYellow
|
|
112
|
+
S[spec_writer<br>role: spec]:::solarizedBlue
|
|
113
|
+
C[coder<br>role: coder]:::solarizedGreen
|
|
114
|
+
J[judge<br>advisory]:::solarizedOrange
|
|
115
|
+
T[tester<br>ruff + pytest]:::solarizedCyan
|
|
116
|
+
E[final verdict<br>exit code]:::solarizedBase02
|
|
117
|
+
|
|
118
|
+
A --> S
|
|
119
|
+
S --> C
|
|
120
|
+
C --> J
|
|
121
|
+
J -->|advisory| T
|
|
122
|
+
T -->|fail and iter < max_iter| C
|
|
123
|
+
T -->|pass or budget exhausted| E
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Configuration
|
|
129
|
+
|
|
130
|
+
### Local Ollama profile (default)
|
|
131
|
+
|
|
132
|
+
```toml
|
|
133
|
+
# config.toml
|
|
134
|
+
[pipeline]
|
|
135
|
+
max_iter = 3
|
|
136
|
+
judge_blocking = false
|
|
137
|
+
|
|
138
|
+
[roles.spec]
|
|
139
|
+
provider = "ollama"
|
|
140
|
+
model = "qwen2.5-coder:7b"
|
|
141
|
+
|
|
142
|
+
[roles.coder]
|
|
143
|
+
provider = "ollama"
|
|
144
|
+
model = "qwen2.5-coder:7b"
|
|
145
|
+
|
|
146
|
+
[roles.judge]
|
|
147
|
+
provider = "ollama"
|
|
148
|
+
model = "qwen2.5:14b"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Cloud hybrid profile (free tiers)
|
|
152
|
+
|
|
153
|
+
```toml
|
|
154
|
+
# config.cloud.toml.example
|
|
155
|
+
[pipeline]
|
|
156
|
+
max_iter = 3
|
|
157
|
+
judge_blocking = false
|
|
158
|
+
|
|
159
|
+
[roles.spec]
|
|
160
|
+
provider = "openai_compat"
|
|
161
|
+
base_url = "https://openrouter.ai/api/v1"
|
|
162
|
+
model = "deepseek/deepseek-chat"
|
|
163
|
+
api_key_env = "OPENROUTER_API_KEY"
|
|
164
|
+
|
|
165
|
+
[roles.coder]
|
|
166
|
+
provider = "openai_compat"
|
|
167
|
+
base_url = "https://openrouter.ai/api/v1"
|
|
168
|
+
model = "deepseek/deepseek-chat"
|
|
169
|
+
api_key_env = "OPENROUTER_API_KEY"
|
|
170
|
+
|
|
171
|
+
[roles.judge]
|
|
172
|
+
provider = "openai_compat"
|
|
173
|
+
base_url = "https://api.groq.com/openai/v1"
|
|
174
|
+
model = "llama-3.3-70b-versatile"
|
|
175
|
+
api_key_env = "GROQ_API_KEY"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# .env
|
|
180
|
+
OPENROUTER_API_KEY=your_key_here
|
|
181
|
+
GROQ_API_KEY=your_key_here
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
> ⚠️ Never commit `.env` — keep secrets private. Config references env var names, not values.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Performance
|
|
189
|
+
|
|
190
|
+
| Metric | Local (7B coder) | Cloud (frontier) |
|
|
191
|
+
|---|---|---|
|
|
192
|
+
| Coder throughput | ~15 tok/s | ~50–100 tok/s |
|
|
193
|
+
| Judge throughput | ~3–6 tok/s (partial CPU offload) | ~30–60 tok/s |
|
|
194
|
+
| Full cycle wall time | 4–15 min | 1–3 min |
|
|
195
|
+
| Exit code | 0 = gates passed, 1 = failed | 0 = gates passed, 1 = failed |
|
|
196
|
+
|
|
197
|
+
**Typical run:**
|
|
198
|
+
|
|
199
|
+
```log
|
|
200
|
+
[preflight] ruff: OK
|
|
201
|
+
[preflight] pytest: OK
|
|
202
|
+
[preflight] coder endpoint: reachable
|
|
203
|
+
[spec] Generated SPEC.md + test_candidate.py (1.2s)
|
|
204
|
+
[coder] Iteration 1/3 — wrote candidate.py (3.4s)
|
|
205
|
+
[judge] score=3, verdict=fail (advisory)
|
|
206
|
+
[tester] ruff rc=0, pytest rc=1 (fail)
|
|
207
|
+
[coder] Iteration 2/3 — fixed issues (2.8s)
|
|
208
|
+
[judge] score=4, verdict=pass (advisory)
|
|
209
|
+
[tester] ruff rc=0, pytest rc=0 (pass)
|
|
210
|
+
✓ Pipeline completed: gates passed (exit code 0)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Honest Limitations
|
|
216
|
+
|
|
217
|
+
- **Local models (7b-14b) fail convergence** on mid-complexity tasks (e.g., sliding-window rate limiter with exact boolean return) within 3 iterations.
|
|
218
|
+
- **Cloud free-tier models (Groq gpt-oss-120b)** provide better judge quality but have rate limits (200K tokens/day).
|
|
219
|
+
- **CiLydian is an experiment and portfolio artifact**, not a daily development tool. For production code, use manual multi-vendor workflow (DeepSeek/Qwen for prompting, Claude for architecture, Gemini for review, local ruff/pytest for gates).
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Screenshot
|
|
224
|
+
|
|
225
|
+

|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Roadmap
|
|
230
|
+
|
|
231
|
+
- [ ] **GitHub Actions integration** — trigger CiLydian on issue labels
|
|
232
|
+
- [ ] **Multi-language support** — extend beyond Python (JavaScript, Go)
|
|
233
|
+
- [ ] **Persistent state** — resume interrupted runs from checkpoint
|
|
234
|
+
- [ ] **Web dashboard** — visualize pipeline runs and logs
|
|
235
|
+
- [ ] **Plugin system** — custom gates beyond ruff/pytest (mypy, bandit)
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Contributing
|
|
240
|
+
|
|
241
|
+
Contributions are welcome! Please open an issue or submit a PR.
|
|
242
|
+
|
|
243
|
+
### Development setup
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
git clone https://github.com/sleepti3ht/cilydian.git
|
|
247
|
+
cd cilydian
|
|
248
|
+
|
|
249
|
+
# Unix / macOS
|
|
250
|
+
python -m venv .venv && source .venv/bin/activate
|
|
251
|
+
# Windows (PowerShell)
|
|
252
|
+
python -m venv .venv; .\.venv\Scripts\Activate.ps1
|
|
253
|
+
|
|
254
|
+
pip install -r requirements.txt
|
|
255
|
+
pip install pre-commit
|
|
256
|
+
pre-commit install # enforces ruff + pytest on commit
|
|
257
|
+
|
|
258
|
+
# Run the pipeline on a sample task
|
|
259
|
+
python pipeline.py "Test task"
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Code style
|
|
263
|
+
|
|
264
|
+
- Commit messages: Conventional Commits (`feat(pipeline): ...`, `fix(tester): ...`)
|
|
265
|
+
- Run `ruff check` and `pytest` before pushing (enforced by pre-commit)
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## License
|
|
270
|
+
|
|
271
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pipeline
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""CiLydian: self-hosted agentic CI pipeline.
|
|
2
|
+
|
|
3
|
+
Flow: spec -> coder -> LLM judge (advisory by default) -> deterministic gates
|
|
4
|
+
(ruff + pytest), with a bounded repair loop and human escalation.
|
|
5
|
+
|
|
6
|
+
Models are pluggable per role via config.toml: local Ollama or any
|
|
7
|
+
OpenAI-compatible endpoint (OpenRouter, Groq, Gemini compat, DeepSeek, vLLM).
|
|
8
|
+
API keys are never stored in config: roles reference environment variable names.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
19
|
+
import tomllib
|
|
20
|
+
from typing import Literal, TypedDict
|
|
21
|
+
|
|
22
|
+
from langchain_ollama import ChatOllama
|
|
23
|
+
from langchain_openai import ChatOpenAI
|
|
24
|
+
from langgraph.graph import END, StateGraph
|
|
25
|
+
|
|
26
|
+
LOG_DIR = "logs"
|
|
27
|
+
LOG_CAP = 5_000_000 # rotate a log once it exceeds this many bytes
|
|
28
|
+
DEFAULT_IDEA = "Rate limiter on asyncio.Semaphore for CVE scanner"
|
|
29
|
+
_FENCE_BLOCK = re.compile(r"```[\w-]*[ \t]*\n(.*?)\n[ \t]*```", re.DOTALL)
|
|
30
|
+
|
|
31
|
+
# Populated by init_models(); graph nodes read these globals at call time.
|
|
32
|
+
SPEC_LLM = None
|
|
33
|
+
CODER = None
|
|
34
|
+
JUDGE = None
|
|
35
|
+
MAX_ITER = 3
|
|
36
|
+
JUDGE_BLOCKING = False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_dotenv(path: str = ".env") -> None:
|
|
40
|
+
"""Minimal .env loader; avoids a python-dotenv dependency."""
|
|
41
|
+
if not os.path.exists(path):
|
|
42
|
+
return
|
|
43
|
+
with open(path, encoding="utf-8") as f:
|
|
44
|
+
for line in f:
|
|
45
|
+
line = line.strip()
|
|
46
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
47
|
+
continue
|
|
48
|
+
key, _, value = line.partition("=")
|
|
49
|
+
os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'"))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build_llm(cfg: dict):
|
|
53
|
+
"""Instantiate a chat model for one pipeline role.
|
|
54
|
+
|
|
55
|
+
provider = "ollama" -> local Ollama server
|
|
56
|
+
provider = "openai_compat" -> any OpenAI-compatible API (OpenRouter, Groq,
|
|
57
|
+
Gemini compat, DeepSeek, vLLM, ...)
|
|
58
|
+
"""
|
|
59
|
+
provider = cfg.get("provider", "ollama")
|
|
60
|
+
if provider == "ollama":
|
|
61
|
+
return ChatOllama(
|
|
62
|
+
model=cfg["model"],
|
|
63
|
+
base_url=cfg.get("base_url", "http://localhost:11434"),
|
|
64
|
+
num_ctx=cfg.get("num_ctx", 4096),
|
|
65
|
+
)
|
|
66
|
+
if provider == "openai_compat":
|
|
67
|
+
key_env = cfg.get("api_key_env", "")
|
|
68
|
+
api_key = os.environ.get(key_env, "")
|
|
69
|
+
if not api_key:
|
|
70
|
+
raise SystemExit(
|
|
71
|
+
f"[config] role needs env var {key_env!r} but it is not set. "
|
|
72
|
+
f"Copy .env.example to .env and fill it in."
|
|
73
|
+
)
|
|
74
|
+
return ChatOpenAI(
|
|
75
|
+
model=cfg["model"],
|
|
76
|
+
base_url=cfg["base_url"],
|
|
77
|
+
api_key=api_key,
|
|
78
|
+
temperature=cfg.get("temperature", 0.2),
|
|
79
|
+
max_retries=3, # exponential backoff on 429 (free-tier rate limits)
|
|
80
|
+
)
|
|
81
|
+
raise SystemExit(f"[config] unknown provider: {provider!r}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def load_config(path: str) -> dict:
|
|
85
|
+
try:
|
|
86
|
+
with open(path, "rb") as f:
|
|
87
|
+
return tomllib.load(f)
|
|
88
|
+
except FileNotFoundError:
|
|
89
|
+
raise SystemExit(f"[config] {path} not found; see config.toml in repo root")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def init_models(cfg: dict) -> None:
|
|
93
|
+
global SPEC_LLM, CODER, JUDGE
|
|
94
|
+
roles = cfg.get("roles", {})
|
|
95
|
+
for role in ("spec", "coder", "judge"):
|
|
96
|
+
if role not in roles:
|
|
97
|
+
raise SystemExit(f"[config] missing [roles.{role}] section")
|
|
98
|
+
SPEC_LLM = build_llm(roles["spec"])
|
|
99
|
+
CODER = build_llm(roles["coder"])
|
|
100
|
+
JUDGE = build_llm(roles["judge"])
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def strip_fences(text: str) -> str:
|
|
104
|
+
"""Extract the first fenced code block; models append prose after it."""
|
|
105
|
+
m = _FENCE_BLOCK.search(text)
|
|
106
|
+
if m:
|
|
107
|
+
return m.group(1)
|
|
108
|
+
t = text.strip()
|
|
109
|
+
if t.startswith("```"):
|
|
110
|
+
t = t.split("\n", 1)[1] if "\n" in t else t[3:]
|
|
111
|
+
if "```" in t: # closing fence with prose after it
|
|
112
|
+
t = t.split("```", 1)[0]
|
|
113
|
+
return t.rstrip()
|
|
114
|
+
return t
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def log(agent: str, msg: str) -> None:
|
|
118
|
+
path = os.path.join(LOG_DIR, f"{agent}.log")
|
|
119
|
+
if os.path.exists(path) and os.path.getsize(path) > LOG_CAP:
|
|
120
|
+
os.replace(path, path + ".old") # atomic on Windows; single writer, no race
|
|
121
|
+
with open(path, "a", encoding="utf-8") as f:
|
|
122
|
+
f.write(msg + "\n")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def reset_logs() -> None:
|
|
126
|
+
"""Truncate per run: live-tail panels stay alive, history = one cycle."""
|
|
127
|
+
os.makedirs(LOG_DIR, exist_ok=True)
|
|
128
|
+
for agent in ("spec", "coder", "judge", "tester"):
|
|
129
|
+
open(os.path.join(LOG_DIR, f"{agent}.log"), "w", encoding="utf-8").close()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def ensure_isolation() -> None:
|
|
133
|
+
"""Stop ruff/pytest from inheriting the parent project's configs."""
|
|
134
|
+
os.makedirs(LOG_DIR, exist_ok=True) # ← Добавленная строка
|
|
135
|
+
if not os.path.exists(os.path.join(LOG_DIR, "ruff.toml")):
|
|
136
|
+
open(os.path.join(LOG_DIR, "ruff.toml"), "w", encoding="utf-8").close()
|
|
137
|
+
if not os.path.exists(os.path.join(LOG_DIR, "pytest.ini")):
|
|
138
|
+
with open(os.path.join(LOG_DIR, "pytest.ini"), "w", encoding="utf-8") as f:
|
|
139
|
+
f.write("[pytest]\nasyncio_mode = auto\n")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def run(cmd: list[str], cwd: str) -> subprocess.CompletedProcess:
|
|
143
|
+
"""Run a gate tool; force UTF-8 decoding (Windows locales default to cp1251)."""
|
|
144
|
+
return subprocess.run(
|
|
145
|
+
cmd,
|
|
146
|
+
check=False,
|
|
147
|
+
capture_output=True,
|
|
148
|
+
text=True,
|
|
149
|
+
encoding="utf-8",
|
|
150
|
+
errors="replace",
|
|
151
|
+
cwd=cwd,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def preflight() -> None:
|
|
156
|
+
for tool in ("ruff", "pytest"):
|
|
157
|
+
if shutil.which(tool) is None:
|
|
158
|
+
raise SystemExit(
|
|
159
|
+
f"[preflight] {tool} not found on PATH (see requirements.txt)"
|
|
160
|
+
)
|
|
161
|
+
try:
|
|
162
|
+
CODER.invoke("ping")
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
raise SystemExit(f"[preflight] coder endpoint unreachable: {exc}") from exc
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class State(TypedDict):
|
|
168
|
+
idea: str
|
|
169
|
+
spec: str
|
|
170
|
+
code: str
|
|
171
|
+
verdict: dict
|
|
172
|
+
iteration: int
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def spec_writer(s: State):
|
|
176
|
+
"""Stage 0: turn a one-line idea into a SPEC plus a pytest skeleton."""
|
|
177
|
+
spec = SPEC_LLM.invoke(
|
|
178
|
+
"Turn this idea into a MINIMAL SPEC. Include ONLY what the idea states.\n"
|
|
179
|
+
"Sections: Role; Interface (exact PYTHON class/method signatures the "
|
|
180
|
+
"implementation MUST expose - never Java/Go/other languages); Constraints; "
|
|
181
|
+
"Measurable Acceptance Criteria (max 5, unit-testable); Test Plan (max 5 "
|
|
182
|
+
"tests, 1:1 mapping to criteria).\n"
|
|
183
|
+
"If the task involves time, windows, or delays: the Interface MUST include "
|
|
184
|
+
"an injectable clock parameter (e.g. clock: Callable[[], float] = "
|
|
185
|
+
"time.monotonic), and the Test Plan MUST drive time through a fake clock. "
|
|
186
|
+
"Real sleeps and wall-clock measurements are FORBIDDEN in tests.\n"
|
|
187
|
+
"FORBIDDEN unless the idea explicitly requests them: security compliance, "
|
|
188
|
+
"abuse prevention, authentication, monitoring, logging, UAT, post-release "
|
|
189
|
+
"monitoring, performance measurement, config files, documentation.\n"
|
|
190
|
+
f"Idea: {s['idea']}"
|
|
191
|
+
).content
|
|
192
|
+
tests = CODER.invoke(
|
|
193
|
+
f"Generate a pytest file for this spec:\n{spec}\n"
|
|
194
|
+
"Write only test functions, no implementation. "
|
|
195
|
+
"Import the module under test as `from candidate import ...`, "
|
|
196
|
+
"NOT as `from logs.candidate import ...`. "
|
|
197
|
+
"Output raw python code only, no markdown fences."
|
|
198
|
+
"Tests must call ONLY the class and methods listed in the SPEC Interface "
|
|
199
|
+
"section, with exact names and signatures. No invented API.\n"
|
|
200
|
+
"Tests must NEVER call time.sleep or measure wall-clock time. If the SPEC "
|
|
201
|
+
"defines an injectable clock, construct a fake clock and advance it "
|
|
202
|
+
"explicitly to simulate window expiry.\n"
|
|
203
|
+
).content
|
|
204
|
+
with open(os.path.join(LOG_DIR, "test_candidate.py"), "w", encoding="utf-8") as f:
|
|
205
|
+
f.write(strip_fences(tests))
|
|
206
|
+
log("spec", spec)
|
|
207
|
+
return {"spec": spec}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def coder(s: State):
|
|
211
|
+
"""Generate/repair the implementation; tests are the naming contract."""
|
|
212
|
+
feedback = s["verdict"].get("issues", [])
|
|
213
|
+
prompt = f"SPEC:\n{s['spec']}\n"
|
|
214
|
+
try:
|
|
215
|
+
with open(os.path.join(LOG_DIR, "test_candidate.py"), encoding="utf-8") as f:
|
|
216
|
+
prompt += (
|
|
217
|
+
f"TESTS that must pass (match names/signatures exactly):\n{f.read()}\n"
|
|
218
|
+
)
|
|
219
|
+
except OSError:
|
|
220
|
+
pass
|
|
221
|
+
prompt += (
|
|
222
|
+
f"Issues to fix: {feedback}\n"
|
|
223
|
+
"Write only the Python implementation for module `candidate`. "
|
|
224
|
+
"Raw code only, no markdown fences, no tests. "
|
|
225
|
+
"NO explanations, NO headers, NO prose after the code."
|
|
226
|
+
)
|
|
227
|
+
code = strip_fences(CODER.invoke(prompt).content)
|
|
228
|
+
log("coder", code)
|
|
229
|
+
return {"code": code, "iteration": s["iteration"] + 1}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def judge(s: State):
|
|
233
|
+
"""LLM review with defensive parsing: models ignore JSON schemas regularly."""
|
|
234
|
+
raw = JUDGE.invoke(
|
|
235
|
+
"Rate 1-5: correctness, security, simplicity. Judge ONLY against "
|
|
236
|
+
"the SPEC's measurable acceptance criteria; do not request features "
|
|
237
|
+
f"beyond the SPEC. Check against SPEC:\n{s['spec']}\nCODE:\n{s['code']}\n"
|
|
238
|
+
'Return JSON only: {"score": int, "verdict": "pass"|"fail", "issues": [str]}\n\n'
|
|
239
|
+
'CRITICAL: Output ONLY raw JSON. NO markdown fences.\n'
|
|
240
|
+
'CORRECT: {"score": 4, "verdict": "pass", "issues": []}\n'
|
|
241
|
+
'WRONG: ```json{"score": 4, "verdict": "pass", "issues": []}```'
|
|
242
|
+
).content
|
|
243
|
+
a, b = raw.find("{"), raw.rfind("}")
|
|
244
|
+
try:
|
|
245
|
+
verdict = json.loads(raw[a : b + 1]) if a != -1 else {}
|
|
246
|
+
except json.JSONDecodeError:
|
|
247
|
+
verdict = {}
|
|
248
|
+
if not isinstance(verdict, dict):
|
|
249
|
+
verdict = {}
|
|
250
|
+
verdict.setdefault("issues", [])
|
|
251
|
+
raw_score = verdict.get("score", 5)
|
|
252
|
+
try:
|
|
253
|
+
score = int(float(raw_score))
|
|
254
|
+
except (TypeError, ValueError):
|
|
255
|
+
score = 0
|
|
256
|
+
verdict["issues"].append(f"Unparseable score: {raw_score!r}")
|
|
257
|
+
verdict["verdict"] = str(verdict.get("verdict", "fail")).lower()
|
|
258
|
+
if verdict["verdict"] not in ("pass", "fail"):
|
|
259
|
+
verdict["verdict"] = "fail"
|
|
260
|
+
if score < 4 and verdict["verdict"] == "pass":
|
|
261
|
+
verdict["verdict"] = "fail"
|
|
262
|
+
verdict["issues"].append(f"Auto-downgraded: score={score} contradicts 'pass'.")
|
|
263
|
+
verdict["gate"] = "judge"
|
|
264
|
+
log("judge", json.dumps(verdict, ensure_ascii=False))
|
|
265
|
+
return {"verdict": verdict}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def tester(s: State):
|
|
269
|
+
"""Deterministic gates: ruff (auto-fix then check) and pytest. These block."""
|
|
270
|
+
candidate = os.path.join(LOG_DIR, "candidate.py")
|
|
271
|
+
with open(candidate, "w", encoding="utf-8") as f:
|
|
272
|
+
f.write(s["code"])
|
|
273
|
+
with open(os.path.join(LOG_DIR, "conftest.py"), "w", encoding="utf-8") as f:
|
|
274
|
+
f.write("import sys, os\nsys.path.insert(0, os.path.dirname(__file__))\n")
|
|
275
|
+
run(["ruff", "check", "--fix", "candidate.py"], LOG_DIR)
|
|
276
|
+
with open(candidate, encoding="utf-8") as f:
|
|
277
|
+
fixed = f.read() # State must match the artifact on disk after --fix
|
|
278
|
+
ruff = run(["ruff", "check", "candidate.py"], LOG_DIR)
|
|
279
|
+
if ruff.returncode != 0:
|
|
280
|
+
# Short-circuit: no point running pytest on code that fails lint/syntax.
|
|
281
|
+
log("tester", f"ruff rc={ruff.returncode}\n{ruff.stdout[:500]}")
|
|
282
|
+
return {
|
|
283
|
+
"code": fixed,
|
|
284
|
+
"verdict": {
|
|
285
|
+
**s["verdict"],
|
|
286
|
+
"gate": "ruff+pytest",
|
|
287
|
+
"verdict": "fail",
|
|
288
|
+
"issues": s["verdict"].get("issues", [])
|
|
289
|
+
+ [f"ruff:{ruff.stdout[:300]}"],
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
pytest_run = run(["pytest", "test_candidate.py", "-q", "--tb=short"], LOG_DIR)
|
|
293
|
+
ok = pytest_run.returncode == 0
|
|
294
|
+
log(
|
|
295
|
+
"tester",
|
|
296
|
+
f"ruff rc=0\npytest rc={pytest_run.returncode}\n{pytest_run.stdout[-500:]}",
|
|
297
|
+
)
|
|
298
|
+
extra_issues = [] if ok else [f"pytest:{pytest_run.stdout[:300]}"]
|
|
299
|
+
return {
|
|
300
|
+
"code": fixed,
|
|
301
|
+
"verdict": {
|
|
302
|
+
**s["verdict"],
|
|
303
|
+
"gate": "ruff+pytest",
|
|
304
|
+
"verdict": "pass" if ok else "fail",
|
|
305
|
+
"issues": s["verdict"].get("issues", []) + extra_issues,
|
|
306
|
+
},
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def route(s: State) -> Literal["coder", "tester"]:
|
|
311
|
+
"""Judge is advisory by default; flip JUDGE_BLOCKING to make it a gate."""
|
|
312
|
+
if (
|
|
313
|
+
JUDGE_BLOCKING
|
|
314
|
+
and s["verdict"]["verdict"] == "fail"
|
|
315
|
+
and s["iteration"] < MAX_ITER
|
|
316
|
+
):
|
|
317
|
+
return "coder"
|
|
318
|
+
return "tester"
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def route_tester(s: State) -> Literal["coder", "end"]:
|
|
322
|
+
"""Deterministic failures loop back to coder, bounded by MAX_ITER."""
|
|
323
|
+
if s["verdict"]["verdict"] == "fail" and s["iteration"] < MAX_ITER:
|
|
324
|
+
return "coder"
|
|
325
|
+
return "end"
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
g = StateGraph(State)
|
|
329
|
+
g.add_node("spec", spec_writer)
|
|
330
|
+
g.add_node("coder", coder)
|
|
331
|
+
g.add_node("judge", judge)
|
|
332
|
+
g.add_node("tester", tester)
|
|
333
|
+
g.set_entry_point("spec")
|
|
334
|
+
g.add_edge("spec", "coder")
|
|
335
|
+
g.add_edge("coder", "judge")
|
|
336
|
+
g.add_conditional_edges("judge", route, {"coder": "coder", "tester": "tester"})
|
|
337
|
+
g.add_conditional_edges("tester", route_tester, {"coder": "coder", "end": END})
|
|
338
|
+
app = g.compile()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def main() -> int:
|
|
342
|
+
global MAX_ITER, JUDGE_BLOCKING
|
|
343
|
+
parser = argparse.ArgumentParser(
|
|
344
|
+
description="Agentic CI pipeline: spec -> coder -> judge -> deterministic gates"
|
|
345
|
+
)
|
|
346
|
+
parser.add_argument(
|
|
347
|
+
"idea",
|
|
348
|
+
nargs="?",
|
|
349
|
+
default=DEFAULT_IDEA,
|
|
350
|
+
help="One-line task description (e.g., 'Async rate limiter with token bucket')",
|
|
351
|
+
)
|
|
352
|
+
parser.add_argument(
|
|
353
|
+
"--config",
|
|
354
|
+
default="config.toml",
|
|
355
|
+
help="path to role/model config (default: config.toml)",
|
|
356
|
+
)
|
|
357
|
+
args = parser.parse_args()
|
|
358
|
+
|
|
359
|
+
load_dotenv()
|
|
360
|
+
cfg = load_config(args.config)
|
|
361
|
+
init_models(cfg)
|
|
362
|
+
pipe = cfg.get("pipeline", {})
|
|
363
|
+
MAX_ITER = int(pipe.get("max_iter", 3))
|
|
364
|
+
JUDGE_BLOCKING = bool(pipe.get("judge_blocking", False))
|
|
365
|
+
|
|
366
|
+
preflight()
|
|
367
|
+
ensure_isolation()
|
|
368
|
+
reset_logs()
|
|
369
|
+
result = app.invoke(
|
|
370
|
+
{"idea": args.idea, "spec": "", "code": "", "verdict": {}, "iteration": 0}
|
|
371
|
+
)
|
|
372
|
+
print(json.dumps(result["verdict"], ensure_ascii=False, indent=2))
|
|
373
|
+
return (
|
|
374
|
+
0 if result["verdict"].get("verdict") == "pass" else 1
|
|
375
|
+
) # CI-friendly exit code
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
if __name__ == "__main__":
|
|
379
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cilydian"
|
|
7
|
+
version = "0.1.3"
|
|
8
|
+
description = "Self-hosted agentic CI pipeline with spec-driven codegen"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.12"
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "sleepti3ht", email = "sleepti3ht@gmail.com"}
|
|
15
|
+
]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"langgraph>=1.0",
|
|
18
|
+
"langchain-ollama>=1.0",
|
|
19
|
+
"langchain-openai>=1.0",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
cilydian = "pipeline:main"
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/sleepti3ht/CiLydian"
|
|
27
|
+
Repository = "https://github.com/sleepti3ht/CiLydian"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools]
|
|
30
|
+
py-modules = ["pipeline"]
|
cilydian-0.1.3/setup.cfg
ADDED