Compare commits

10 Commits

Author SHA1 Message Date
9ce0295747 chore: remove generated python cache artifacts
Some checks failed
publish-site / deploy-public-site (push) Waiting to run
preview-slot-reclaim / reclaim (push) Has been cancelled
2026-03-13 15:36:09 +08:00
ae540c7890 feat: add gitea agentic runtime control plane 2026-03-13 15:34:18 +08:00
6f6acdb0e6 chore: ignore local worktree directory 2026-03-13 14:59:49 +08:00
594c7e1a4d install: default jj setup in one-click flow
Some checks failed
publish-site / deploy-public-site (push) Has been cancelled
preview-slot-reclaim / reclaim (push) Has been cancelled
2026-03-13 14:14:09 +08:00
ccfdda4342 docs: make devops skill plan-first and jj-aware
Some checks failed
publish-site / deploy-public-site (push) Has been cancelled
preview-slot-reclaim / reclaim (push) Has been cancelled
2026-03-13 13:18:48 +08:00
acb77a821c docs(skill): solidify routing, ws validation, pm2 port migration and slot rebind rules
Some checks failed
preview-slot-reclaim / reclaim (push) Has been cancelled
2026-03-10 08:16:35 +08:00
2fcd4d093a docs: sync wiki content and add skills parameter guide
Some checks failed
preview-slot-reclaim / reclaim (push) Has been cancelled
publish-site / deploy-public-site (push) Has been cancelled
2026-03-08 20:25:20 +08:00
fcecce0595 feat: publish standalone product landing page as rendered svg
Some checks failed
preview-slot-reclaim / reclaim (push) Has been cancelled
publish-site / deploy-public-site (push) Has been cancelled
2026-03-06 22:58:48 +08:00
fbe92a4e7c chore: add svg raw rendering probe
Some checks failed
publish-site / deploy-public-site (push) Has been cancelled
2026-03-06 22:55:48 +08:00
4df29b70cd docs: point official website to rendered wiki page
Some checks failed
publish-site / deploy-public-site (push) Has been cancelled
2026-03-06 22:46:50 +08:00
47 changed files with 3497 additions and 51 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.worktrees/
.tmp/
__pycache__/
*.pyc

6
.ralph/ralph-context.md Normal file
View File

@@ -0,0 +1,6 @@
# Ralph Context
- Follow the approved design in `docs/superpowers/specs/2026-03-13-gitea-agentic-runtime-design.md`.
- Execute tasks from `docs/superpowers/plans/2026-03-13-gitea-agentic-runtime-plan.md` in order.
- Keep Gitea as the only real provider in this iteration.
- Preserve platform-agnostic boundaries so GitHub can be added later.

114
.ralph/ralph-history.md Normal file
View File

@@ -0,0 +1,114 @@
# Ralph History
## Iteration 0
- Initialized worktree `feature/gitea-runtime-control-plane`
- Wrote approved design spec and implementation plan
- Created Ralph loop contract and task list
- Verification: not started yet
## Iteration 1
- Goal: complete Task 1 package scaffold, spec loader, and sample workflow
- Changed files:
- `pyproject.toml`
- `engine/devops_agent/__init__.py`
- `engine/devops_agent/cli.py`
- `engine/devops_agent/spec.py`
- `workflows/gitea-issue-delivery.md`
- `tests/unit/test_smoke_imports.py`
- `tests/unit/test_spec_loader.py`
- `tests/fixtures/specs/valid_workflow.md`
- `tests/fixtures/specs/invalid_missing_provider.md`
- `.gitignore`
- Verification:
- `python -m pytest tests/unit/test_smoke_imports.py tests/unit/test_spec_loader.py -q`
- Result: `4 passed`
- Blockers:
- Pytest tmp and cache facilities are unreliable in this Windows sandbox path, so tests must avoid `tmp_path` and rely on stable fixture files or repo-local paths.
## Iteration 2
- Goal: complete Task 2 compiler, validator, and policy enforcement
- Changed files:
- `engine/devops_agent/compiler.py`
- `engine/devops_agent/validator.py`
- `engine/devops_agent/policies.py`
- `engine/devops_agent/spec.py`
- `tests/unit/test_compiler.py`
- `tests/unit/test_validator.py`
- `tests/unit/test_policies.py`
- `tests/fixtures/specs/no_safe_outputs_for_write.md`
- `tests/fixtures/specs/invalid_path_scope.md`
- Verification:
- `python -m pytest tests/unit/test_smoke_imports.py tests/unit/test_spec_loader.py tests/unit/test_compiler.py tests/unit/test_validator.py tests/unit/test_policies.py -q`
- Result: `11 passed`
- Blockers:
- PyYAML treats `on` as a boolean in default parsing, so the spec loader now normalizes that key explicitly.
## Iteration 3
- Goal: complete Task 3 provider layer, Gitea provider, runtime, and evidence persistence
- Changed files:
- `engine/devops_agent/evidence.py`
- `engine/devops_agent/runtime.py`
- `engine/devops_agent/providers/__init__.py`
- `engine/devops_agent/providers/base.py`
- `engine/devops_agent/providers/gitea.py`
- `tests/unit/test_gitea_provider.py`
- `tests/integration/test_runtime_flow.py`
- `tests/fixtures/gitea/issue.json`
- `tests/fixtures/gitea/comment_event.json`
- Verification:
- `python -m pytest tests/unit tests/integration -q`
- Result: `15 passed`
- Blockers:
- None in code shape. Real Gitea acceptance still depends on environment credentials.
## Iteration 4
- Goal: complete Task 4 CLI, acceptance tests, and documentation updates
- Changed files:
- `engine/devops_agent/cli.py`
- `tests/unit/test_cli.py`
- `tests/acceptance/test_gitea_acceptance.py`
- `README.md`
- `skills/gitea-issue-devops-agent/SKILL.md`
- `workflows/gitea-issue-delivery.lock.json`
- Verification:
- `python -m pytest tests/unit tests/integration tests/acceptance -q`
- `python -m engine.devops_agent.cli compile workflows/gitea-issue-delivery.md --output workflows/gitea-issue-delivery.lock.json`
- `python -m engine.devops_agent.cli validate workflows/gitea-issue-delivery.md`
- Result: `18 passed, 1 skipped`
- Blockers:
- Real Gitea acceptance is blocked until `GITEA_BASE_URL`, `GITEA_REPO`, `GITEA_TOKEN`, and `GITEA_ISSUE_NUMBER` are provided.
## Iteration 5
- Goal: complete Task 5 full verification and real Gitea acceptance handoff
- Verification:
- `python -m pytest tests/unit tests/integration tests/acceptance -q`
- `python -m engine.devops_agent.cli compile workflows/gitea-issue-delivery.md --output workflows/gitea-issue-delivery.lock.json`
- `python -m engine.devops_agent.cli validate workflows/gitea-issue-delivery.md`
- `git diff --check`
- Result: `19 passed`
- Real acceptance evidence:
- repo: `FunMD/document-collab`
- issue: `#48`
- comment id: `246`
- comment url: `http://154.39.79.147:3000/FunMD/document-collab/issues/48#issuecomment-246`
- Blockers:
- None for the Gitea single-platform acceptance target.
## Iteration 6
- Goal: remove generated Python cache artifacts accidentally staged into git
- Changed files:
- `.gitignore`
- removed tracked `__pycache__/*.pyc` artifacts under `engine/` and `tests/`
- Verification:
- `python -m pytest tests/unit tests/integration tests/acceptance -q`
- `git diff --check`
- Result: `19 passed`
- Blockers:
- Two sandbox-created `pytest-cache-files-*` directories remain unreadable to git status in this environment, but they are not tracked and do not affect the runtime or tests.

28
.ralph/ralph-loop-plan.md Normal file
View File

@@ -0,0 +1,28 @@
# Ralph Loop Plan
## Goal
Integrate a Gitea-backed agentic workflow runtime into this repository so the product moves from skill-only guidance to a real execution control plane. Deliver a repo-local workflow spec format, compiler, validator, runtime, Gitea provider, safe-output policy enforcement, evidence persistence, automated tests, and one real Gitea acceptance path.
## Acceptance Criteria
1. `workflows/gitea-issue-delivery.md` compiles into a lock artifact.
2. Invalid workflow specs fail validation with explicit errors.
3. Runtime refuses undeclared write actions and records evidence for allowed ones.
4. Automated unit and integration tests pass.
5. Real Gitea acceptance can read the selected issue and publish an evidence comment when credentials are present.
6. README and `skills/gitea-issue-devops-agent/SKILL.md` reflect the new runtime model.
## Verification Commands
- `python -m pytest tests/unit tests/integration -q`
- `python -m pytest tests/acceptance/test_gitea_acceptance.py -q`
- `python -m engine.devops_agent.cli compile workflows/gitea-issue-delivery.md --output workflows/gitea-issue-delivery.lock.json`
- `python -m engine.devops_agent.cli validate workflows/gitea-issue-delivery.md`
- `git diff --check`
## Promises
- completion_promise: `COMPLETE`
- abort_promise: `ABORT`
- max_iterations: `5`

7
.ralph/ralph-tasks.md Normal file
View File

@@ -0,0 +1,7 @@
# Ralph Tasks
- [x] Task 1: Add Python package scaffold, workflow spec loader, and sample workflow
- [x] Task 2: Add compiler, validator, and policy enforcement
- [x] Task 3: Add provider layer, Gitea provider, runtime, and evidence persistence
- [x] Task 4: Add CLI commands, acceptance tests, and documentation updates
- [x] Task 5: Run full verification, compile sample workflow, and prepare manual acceptance handoff

440
README.md
View File

@@ -1,31 +1,128 @@
# DevOps Skills ![JUST FOR FUN - Linus Torvalds](https://kwize.com/pics/Linus-Torvalds-quote-about-fun-1a9823.jpg)
Issue-Driven DevOps 平台技能仓库,核心产品是 `gitea-issue-devops-agent` # gitea-issue-devops-agent
它把交付流程固化为: > **Issue-Driven DevOps 产品官网**
> 把 `Issue -> Plan -> Branch -> Draft PR -> Preview Slot -> Test Loop -> Human-Confirmed Merge` 变成标准交付引擎。
`Issue -> Branch -> Preview Slot -> Test Loop -> Human-Confirmed Merge`
## 公网产品页 ## 公网产品页
- 产品官网:`https://fun-md.com/Fun_MD/devops-skills/raw/branch/main/site/index.html` - 产品官网(独立前端渲染页,非 Wiki`https://fun-md.com/Fun_MD/devops-skills/raw/branch/main/site/index.svg`
- 仓库入口:`https://fun-md.com/Fun_MD/devops-skills` - 仓库入口:`https://fun-md.com/Fun_MD/devops-skills`
- HTML 设计稿源码:`site/index.html`
- SVG 官网文件:`site/index.svg`
## 核心价值
### 1) 计划先行,避免 AI 脑裂
每个 issue 在编码前先生成可持久化 Plan明确范围、验收、验证路径和 Agent 分工,避免多 Agent 或长流程中的上下文漂移。
### 2) 分支隔离提测
每个 issue 固定独立分支和预览槽位,主干保持稳定回归,避免提测互相覆盖。
### 3) 智能节省资源
按改动自动识别部署策略:`skip / client_only / server_only / full_stack / infra_only`
**服务端未变更就不重启服务端**
### 4) 证据化闭环
提测沉淀 commit、PR、测试链接、环境 URL、验证步骤最终合并必须工程师人工确认。
## 新增运行时能力
仓库现在不再只有 skill 文档和辅助脚本,还新增了一个最小可执行控制平面:
- `workflows/*.md`
- agentic workflow spec 源文件
- `workflows/*.lock.json`
- 编译后的锁定执行产物
- `engine/devops_agent/*`
- spec loader、compiler、validator、runtime、policy、provider、evidence、CLI
当前已实装的真实 provider 是 `Gitea`,并保留了后续接 `GitHub` 的 provider 边界。
## 运行时命令
### Compile
```bash
python -m engine.devops_agent.cli compile workflows/gitea-issue-delivery.md --output workflows/gitea-issue-delivery.lock.json
```
### Validate
```bash
python -m engine.devops_agent.cli validate workflows/gitea-issue-delivery.md
```
### Run
```bash
python -m engine.devops_agent.cli run workflows/gitea-issue-delivery.md \
--event-payload tests/fixtures/gitea/comment_event.json \
--output-dir .tmp/runtime-run \
--base-url https://fun-md.com \
--token <TOKEN>
```
### Acceptance
```bash
python -m engine.devops_agent.cli acceptance workflows/gitea-issue-delivery.md \
--base-url https://fun-md.com \
--repo Fun_MD/devops-skills \
--token <TOKEN> \
--issue-number 48 \
--output-dir .tmp/acceptance/gitea
```
运行时输出:
- `run-artifact.json`
- 计划状态摘要
- evidence comment 回写结果
## Safe Outputs 与定位
这次整合没有把产品做成 GitHub Actions 克隆,而是把 `gh-aw` 最有价值的部分内化为你们自己的控制层:
- `workflow spec`
- `compile / validate`
- `safe outputs`
- `provider abstraction`
- `evidence artifacts`
对外仍然保持:
- `issue / git branch / PR / CI/CD / review apps`
对内则新增:
- 不允许未声明的写操作
- 不允许跳过 validation 直接执行
- 不允许没有 evidence 就宣称完成
## 一键安装 ## 一键安装
Linux: 安装器现在会先安装 skill再默认尝试安装 `jj`
如果 `jj` 因本机环境、包管理器或网络原因安装失败,安装器不会失败,只会给出手动安装提示。
### Linux
```bash ```bash
curl -fsSL https://fun-md.com/Fun_MD/devops-skills/raw/branch/main/install/install.sh | bash curl -fsSL https://fun-md.com/Fun_MD/devops-skills/raw/branch/main/install/install.sh | bash
``` ```
macOS: ### macOS
```bash ```bash
curl -fsSL https://fun-md.com/Fun_MD/devops-skills/raw/branch/main/install/install.sh | bash curl -fsSL https://fun-md.com/Fun_MD/devops-skills/raw/branch/main/install/install.sh | bash
``` ```
Windows (PowerShell): ### Windows (PowerShell)
```powershell ```powershell
powershell -NoProfile -ExecutionPolicy Bypass -Command "iwr -useb https://fun-md.com/Fun_MD/devops-skills/raw/branch/main/install/install.ps1 | iex" powershell -NoProfile -ExecutionPolicy Bypass -Command "iwr -useb https://fun-md.com/Fun_MD/devops-skills/raw/branch/main/install/install.ps1 | iex"
@@ -35,25 +132,328 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command "iwr -useb https://fun-md
- `~/.codex/skills/gitea-issue-devops-agent` - `~/.codex/skills/gitea-issue-devops-agent`
默认 `jj` 安装顺序:
- Linux / macOS`brew -> cargo-binstall -> cargo`
- Windows`winget -> scoop -> cargo`
安装控制项:
### Bash
```bash
INSTALL_JJ=0
JJ_INSTALL_METHOD=auto|brew|binstall|cargo
JJ_CHANNEL=release|prerelease
```
### PowerShell
```powershell
$env:INSTALL_JJ='0'
$env:JJ_INSTALL_METHOD='auto' # 或 winget / scoop / cargo
$env:JJ_CHANNEL='release' # 或 prerelease
```
验证命令:
```bash
jj --version
jj config set --user user.name "Your Name"
jj config set --user user.email "you@example.com"
```
详细说明:
- `skills/gitea-issue-devops-agent/references/jj-default-usage.md`
## 运行时与工具使用说明
### workflow spec
- `workflows/gitea-issue-delivery.md`
- 当前样例 workflow spec
- `workflows/gitea-issue-delivery.lock.json`
- 编译后的锁定产物,建议与 spec 一起提交
### acceptance 环境变量
真实 Gitea 验收测试读取以下环境变量:
```bash
GITEA_BASE_URL=https://fun-md.com
GITEA_REPO=Fun_MD/devops-skills
GITEA_TOKEN=<TOKEN>
GITEA_ISSUE_NUMBER=48
```
执行:
```bash
python -m pytest tests/acceptance/test_gitea_acceptance.py -q
```
### issue_audit.py
```bash
python skills/gitea-issue-devops-agent/scripts/issue_audit.py \
--base-url https://fun-md.com \
--repo FunMD/document-collab \
--token <TOKEN> \
--state all \
--download-attachments \
--output-dir .tmp/issue-audit
```
### change_scope.py
```bash
python skills/gitea-issue-devops-agent/scripts/change_scope.py --repo-path . --base-ref origin/main --head-ref HEAD
```
### preview_slot_allocator.py
```bash
python skills/gitea-issue-devops-agent/scripts/preview_slot_allocator.py --state-file .tmp/preview-slots.json --slots preview-a,preview-b --repo FunMD/document-collab --issue 48 --branch dev --ttl-hours 24 --url-template https://{slot}.qa.example.com --evict-oldest
```
## 工作流模板
- `.gitea/workflows/issue-branch-preview.yml`
- `.gitea/workflows/preview-slot-reclaim.yml`
- `.gitea/workflows/publish-site.yml`
- `workflows/gitea-issue-delivery.md`
- `workflows/gitea-issue-delivery.lock.json`
## Skills 调用前置信息Claude Code / Codex / OpenCode
统一建议先准备这组参数:
- `repo_url`
- `api_key`Gitea token需 issue 读写权限)
- `mode``automatic` / `semi-automatic` / `manual`
- `issue` 或固定 issue 触发来源
- 可选:`target_base``plan_path``reviewers``test_entry``deploy_env``health_endpoint``min_quality_score``jj_policy`
推荐默认值:
- `jj_policy=optional-internal`
- 先固定 issue再进入 `Plan -> Draft PR -> 提测 -> 人工确认合并`
### Claude Code
Skills 目录(官方支持):
- 用户级:`~/.claude/skills/<skill-name>/SKILL.md`
- 项目级:`.claude/skills/<skill-name>/SKILL.md`
唤起方式:
- 显式调用:`/<skill-name> [args]`
- 对话调用:直接说“使用某个 skill 处理任务”
示例:
```text
/gitea-issue-devops-agent repo_url=https://fun-md.com/FunMD/document-collab mode=automatic
```
```text
请使用 gitea-issue-devops-agent连接 repo_url=...api_key=...,以 semi-automatic 模式处理 issue #48
```
### Codex
Skills 安装目录(当前方案):
- `~/.codex/skills/gitea-issue-devops-agent`
唤起方式:
- 对话显式点名:`$gitea-issue-devops-agent`
- 或自然语言明确要求:`使用 gitea-issue-devops-agent skill`
示例:
```text
$gitea-issue-devops-agent
repo_url: https://fun-md.com/FunMD/document-collab
api_key: <TOKEN>
mode: automatic
issue: 48
```
### OpenCode
Skills 目录Claude skill 兼容):
- 项目级:`.opencode/skills/<skill-name>/SKILL.md`
- 全局级:`~/.config/opencode/skills/<skill-name>/SKILL.md`
唤起方式:
- 对话里明确要求使用目标 skill推荐
- Agent 内部会通过原生 `skill` 工具加载(`skill({name: "..."})`
示例:
```text
Use skill gitea-issue-devops-agent.
repo_url=https://fun-md.com/FunMD/document-collab
api_key=<TOKEN>
mode=manual
```
## `jj` 在工作流中的定位
默认安装 `jj`,但不要求非工程角色理解 `jj`
- 对外:继续使用 `issue / git branch / PR / CI/CD / review apps`
- 对内:用 `jj` 承担本地执行、回退、并行 workspace、变更重写
- 原则:`jj` 是内部可靠性增强层,不替代你们对外的 Git/Gitea 协作界面
## `skills` 命令参数释义(重点补充)
> 本节把“`skills` 命令”统一理解为:在 Claude/Codex/OpenCode 中显式调用 `gitea-issue-devops-agent` 时提交的参数块。
> 建议参数名如下,便于团队协作时统一模板和自动化脚本对接。
### 必填参数
| 参数 | 说明 | 典型值 | 使用场景 |
| --- | --- | --- | --- |
| `repo_url` | 目标仓库完整地址。优先使用完整 URL。 | `https://fun-md.com/Fun_MD/devops-skills` | 常规接入,避免 `base_url + owner/repo` 组合歧义 |
| `api_key` | Gitea token至少具备 issue 读写权限。 | `gta_xxx` | 需要读取 issue、评论、附件并回写提测证据 |
| `mode` | 执行模式:`automatic` / `semi-automatic` / `manual`。 | `automatic` | 决定自动化程度和人工审批点 |
| `issue` | 固定 issue 编号或触发来源。 | `48` | 将一次交付限定在一个可控 issue 上 |
### 重要可选参数
| 参数 | 说明 | 典型值 | 使用场景 |
| --- | --- | --- | --- |
| `reviewers` | 指定评审人列表(逗号分隔)。 | `alice,bob` | `semi-automatic` 模式下提交后等待人工评审 |
| `plan_path` | Plan 持久化路径。 | `.tmp/devops-plans/devops-skills__issue-48.md` | MajorAgent/SubAgent/TestAgent 共享状态 |
| `test_entry` | 分支提测入口CI 命令或 job 名)。 | `gitea workflow run issue-branch-preview` | 多条流水线并存时明确提测入口 |
| `main_env_url` | 主干稳定环境 URL。 | `https://main.qa.example.com` | 回归对比、基线验证 |
| `shared_qa_url` | 共享 QA 环境 URL可选。 | `https://qa.example.com` | 需要跨分支集成验证 |
| `preview_slots` | 预览槽位池。 | `preview-a,preview-b` | 多 issue 并行时的环境隔离与复用 |
| `preview_url_template` | 槽位 URL 模板。 | `https://{slot}.qa.example.com` | 自动生成 issue 分支预览地址 |
| `deploy_env` | 部署环境标识。 | `k8s-staging` | 一套技能同时驱动多环境 |
| `health_endpoint` | 健康检查接口。 | `/healthz` | 提测后自动做可用性验证 |
| `min_quality_score` | issue 最低质量分(默认 70。 | `70` | 低质量 issue 先补充信息再进入开发 |
| `skip_asset_endpoints` | 跳过 `/issues/*/assets` 端点抓图。 | `true` | 自建 Gitea 禁用了 assets API 时兜底 |
| `target_base` | 变更比较基线分支。 | `origin/main` | 用于 `change_scope` 判断部署范围 |
| `jj_policy` | `jj` 使用策略:`disabled` / `optional-internal` / `required-internal`。 | `optional-internal` | 仅作为内部执行与恢复层,不改变外部 Git/PR 流程 |
### 参数组合示例(按场景)
#### 场景 1日常 bug 修复,端到端自动执行
```text
/gitea-issue-devops-agent \
repo_url=https://fun-md.com/Fun_MD/devops-skills \
api_key=<TOKEN> \
mode=automatic \
issue=48 \
plan_path=.tmp/devops-plans/devops-skills__issue-48.md \
test_entry="issue-branch-preview" \
main_env_url=https://main.qa.example.com \
preview_slots=preview-a,preview-b \
preview_url_template=https://{slot}.qa.example.com \
min_quality_score=70
```
适用:问题描述完整、团队希望最大化自动化吞吐。
典型流程:
1. 人工在 Gitea 选中 issue。
2. MajorAgent 生成 Plan 并创建分支、Draft PR。
3. SubAgent 只读取必要上下文并修改计划内路径。
4. TestAgent 跑单测、集成测试、issue 级 e2e。
5. 通过后进入 preview slot 和人工复核。
#### 场景 2生产敏感仓库人工确认每一步
```text
$gitea-issue-devops-agent
repo_url: https://fun-md.com/Fun_MD/devops-skills
api_key: <TOKEN>
mode: manual
issue: 48
deploy_env: prod-like-staging
health_endpoint: /healthz
```
适用:高风险改动、强合规流程、需要逐步确认分支/提交/提测/关闭。
典型流程:
1. 先生成 Plan。
2. 每次代码改动前都确认允许范围。
3. 提测、回写 issue、关闭 issue、最终 merge 都要人工确认。
4. 如 AI 偏航,可用 `jj` 做本地回退而不破坏外部 PR。
#### 场景 3半自动协作先评审后提测
```text
Use skill gitea-issue-devops-agent.
repo_url=https://fun-md.com/Fun_MD/devops-skills
api_key=<TOKEN>
mode=semi-automatic
issue=48
reviewers=alice,bob
test_entry=issue-branch-preview
shared_qa_url=https://qa.example.com
preview_slots=preview-a,preview-b,preview-c
```
适用:多人协作项目,需要评审人显式批准后再进入提测和环境分配。
典型流程:
1. AI 先产出初始 Draft PR。
2. 工程师在 AI 编码工具里继续白盒调整。
3. reviewer 回复 `review-approved` 后才进入提测。
4. maintainer 最后确认 merge。
#### 场景 4仅文档改动或轻量改动资源最省策略
```text
/gitea-issue-devops-agent repo_url=... api_key=... mode=automatic target_base=origin/main
```
配合 `change_scope.py` 可自动得到 `skip``client_only`,避免不必要的服务端重启和环境开销。
#### 场景 5多 Agent 并行,但上下文不脑裂
```text
issue=48
mode=semi-automatic
jj_policy=optional-internal
plan_path=.tmp/devops-plans/devops-skills__issue-48.md
```
典型流程:
1. MajorAgent 只负责 issue 语义分析和 Plan。
2. SubAgent 只负责修改代码。
3. TestAgent 在独立 `jj workspace` 里验证。
4. 人工 reviewer 再决定是否继续迭代或合并。
适用:长流程、多角色协作、希望降低 token 消耗和上下文漂移。
## 技能路径 ## 技能路径
- `skills/gitea-issue-devops-agent/SKILL.md` - `skills/gitea-issue-devops-agent/SKILL.md`
## 核心能力 ## 核心文档
- 三种执行模式:`automatic` / `semi-automatic` / `manual` - `skills/gitea-issue-devops-agent/references/issue-template-standard.md`
- issue 图片证据抓取(含 attachments/assets 三路兜底) - `skills/gitea-issue-devops-agent/references/plan-template.md`
- 按变更范围部署(`skip` / `client_only` / `server_only` / `full_stack` / `infra_only` - `skills/gitea-issue-devops-agent/references/jj-default-usage.md`
- 预览槽位池分配与自动回收TTL + 关闭释放)
- 最终代码合并必须人工确认
## 核心脚本 ## 核心脚本
- `skills/gitea-issue-devops-agent/scripts/issue_audit.py` - `skills/gitea-issue-devops-agent/scripts/issue_audit.py`
- `skills/gitea-issue-devops-agent/scripts/change_scope.py` - `skills/gitea-issue-devops-agent/scripts/change_scope.py`
- `skills/gitea-issue-devops-agent/scripts/preview_slot_allocator.py` - `skills/gitea-issue-devops-agent/scripts/preview_slot_allocator.py`
## .gitea/workflows 模板
- `.gitea/workflows/issue-branch-preview.yml`
- `.gitea/workflows/preview-slot-reclaim.yml`

View File

@@ -0,0 +1,266 @@
# Gitea Agentic Runtime Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build a Gitea-backed workflow spec/compiler/runtime subsystem that turns the existing delivery skill into a policy-enforced execution engine with automated tests and real Gitea acceptance.
**Architecture:** Add a small Python control-plane package under `engine/devops_agent`, keep provider logic isolated behind interfaces, compile workflow specs into JSON lock artifacts, and run policy-guarded issue workflows against Gitea. Persist plans and evidence locally so tests and operators can inspect every run deterministically.
**Tech Stack:** Python 3.11+, pytest, requests-free stdlib HTTP where practical, Markdown/JSON workflow artifacts, existing repository docs and Gitea workflow templates
---
## Chunk 1: Project Scaffold and Spec Format
### Task 1: Create the Python package scaffold and test harness
**Files:**
- Create: `pyproject.toml`
- Create: `engine/devops_agent/__init__.py`
- Create: `engine/devops_agent/cli.py`
- Create: `tests/unit/test_smoke_imports.py`
- [ ] **Step 1: Write the failing package smoke test**
Create a test that imports the CLI entrypoint and key modules that will exist after scaffolding.
- [ ] **Step 2: Run the smoke test and confirm failure**
Run: `python -m pytest tests/unit/test_smoke_imports.py -q`
Expected: import failure because package files do not exist yet
- [ ] **Step 3: Add the minimal package scaffold**
Create the package directories, `__init__.py`, and a CLI module with a placeholder command parser.
- [ ] **Step 4: Add the Python project file**
Create `pyproject.toml` with the minimum metadata and `pytest` test configuration.
- [ ] **Step 5: Re-run the smoke test**
Run: `python -m pytest tests/unit/test_smoke_imports.py -q`
Expected: pass
### Task 2: Define the workflow spec shape and a sample Gitea workflow
**Files:**
- Create: `engine/devops_agent/spec.py`
- Create: `workflows/gitea-issue-delivery.md`
- Create: `tests/unit/test_spec_loader.py`
- [ ] **Step 1: Write the failing spec loader tests**
Test frontmatter parsing, Markdown body extraction, and required field detection.
- [ ] **Step 2: Run the spec loader tests and confirm failure**
Run: `python -m pytest tests/unit/test_spec_loader.py -q`
Expected: missing loader implementation
- [ ] **Step 3: Implement the minimal spec loader**
Parse frontmatter and Markdown body into a structured object.
- [ ] **Step 4: Add the sample Gitea workflow spec**
Define one workflow that models fixed-issue delivery with safe comment output.
- [ ] **Step 5: Re-run the spec loader tests**
Run: `python -m pytest tests/unit/test_spec_loader.py -q`
Expected: pass
## Chunk 2: Compiler, Validator, and Policy Enforcement
### Task 3: Implement lock compilation
**Files:**
- Create: `engine/devops_agent/compiler.py`
- Create: `tests/unit/test_compiler.py`
- [ ] **Step 1: Write failing compiler tests**
Cover default normalization, trigger expansion, and lock artifact emission.
- [ ] **Step 2: Run the compiler tests and confirm failure**
Run: `python -m pytest tests/unit/test_compiler.py -q`
Expected: missing compiler implementation
- [ ] **Step 3: Implement the minimal compiler**
Compile the loaded spec into a deterministic JSON-compatible lock payload.
- [ ] **Step 4: Re-run the compiler tests**
Run: `python -m pytest tests/unit/test_compiler.py -q`
Expected: pass
### Task 4: Implement validation and safe output policies
**Files:**
- Create: `engine/devops_agent/validator.py`
- Create: `engine/devops_agent/policies.py`
- Create: `tests/unit/test_validator.py`
- Create: `tests/unit/test_policies.py`
- [ ] **Step 1: Write failing validation and policy tests**
Cover missing provider, invalid trigger combinations, undeclared write actions, and path-scope checks.
- [ ] **Step 2: Run those tests and confirm failure**
Run: `python -m pytest tests/unit/test_validator.py tests/unit/test_policies.py -q`
Expected: missing implementations
- [ ] **Step 3: Implement the validator**
Return explicit validation errors for incomplete or unsafe specs.
- [ ] **Step 4: Implement the policy layer**
Enforce read-only default, safe output declarations, and bounded write operations.
- [ ] **Step 5: Re-run the validation and policy tests**
Run: `python -m pytest tests/unit/test_validator.py tests/unit/test_policies.py -q`
Expected: pass
## Chunk 3: Provider and Runtime
### Task 5: Implement the provider interface and Gitea provider
**Files:**
- Create: `engine/devops_agent/providers/__init__.py`
- Create: `engine/devops_agent/providers/base.py`
- Create: `engine/devops_agent/providers/gitea.py`
- Create: `tests/fixtures/gitea/issue.json`
- Create: `tests/fixtures/gitea/comment_event.json`
- Create: `tests/unit/test_gitea_provider.py`
- [ ] **Step 1: Write failing provider tests**
Cover issue fetch, comment post request shaping, and trigger event parsing using fixtures.
- [ ] **Step 2: Run the provider tests and confirm failure**
Run: `python -m pytest tests/unit/test_gitea_provider.py -q`
Expected: provider implementation missing
- [ ] **Step 3: Implement the provider interface and Gitea provider**
Add a minimal provider abstraction and the first concrete implementation for Gitea.
- [ ] **Step 4: Re-run the provider tests**
Run: `python -m pytest tests/unit/test_gitea_provider.py -q`
Expected: pass
### Task 6: Implement runtime and evidence persistence
**Files:**
- Create: `engine/devops_agent/runtime.py`
- Create: `engine/devops_agent/evidence.py`
- Create: `tests/integration/test_runtime_flow.py`
- [ ] **Step 1: Write a failing runtime integration test**
Cover compile -> validate -> runtime execution -> evidence artifact output using a fake provider transport.
- [ ] **Step 2: Run the runtime integration test and confirm failure**
Run: `python -m pytest tests/integration/test_runtime_flow.py -q`
Expected: runtime implementation missing
- [ ] **Step 3: Implement the runtime and evidence writer**
Persist run artifacts and plan/evidence summaries under a deterministic output directory.
- [ ] **Step 4: Re-run the runtime integration test**
Run: `python -m pytest tests/integration/test_runtime_flow.py -q`
Expected: pass
## Chunk 4: CLI, Acceptance, and Documentation
### Task 7: Expose compile, validate, run, and acceptance commands
**Files:**
- Modify: `engine/devops_agent/cli.py`
- Create: `tests/unit/test_cli.py`
- [ ] **Step 1: Write failing CLI tests**
Cover `compile`, `validate`, and `run` command dispatch with filesystem outputs.
- [ ] **Step 2: Run the CLI tests and confirm failure**
Run: `python -m pytest tests/unit/test_cli.py -q`
Expected: placeholder CLI insufficient
- [ ] **Step 3: Implement the CLI commands**
Add command handling for `compile`, `validate`, `run`, and `acceptance`.
- [ ] **Step 4: Re-run the CLI tests**
Run: `python -m pytest tests/unit/test_cli.py -q`
Expected: pass
### Task 8: Add real Gitea acceptance and update docs
**Files:**
- Create: `tests/acceptance/test_gitea_acceptance.py`
- Modify: `README.md`
- Modify: `skills/gitea-issue-devops-agent/SKILL.md`
- [ ] **Step 1: Write acceptance tests**
Make the suite skip when `GITEA_BASE_URL`, `GITEA_REPO`, `GITEA_TOKEN`, and `GITEA_ISSUE_NUMBER` are absent, and perform real provider read/comment flow when present.
- [ ] **Step 2: Run the acceptance suite without env vars**
Run: `python -m pytest tests/acceptance/test_gitea_acceptance.py -q`
Expected: clean skip
- [ ] **Step 3: Update README and skill docs**
Document the new spec/runtime model, CLI commands, safe outputs, and Gitea acceptance procedure.
- [ ] **Step 4: Run real acceptance**
Run: `python -m pytest tests/acceptance/test_gitea_acceptance.py -q`
Expected: pass against the configured Gitea repository
## Chunk 5: End-to-End Verification and Delivery
### Task 9: Run the full automated test suite and review repository state
**Files:**
- Modify: repo index and generated lock artifacts as needed
- [ ] **Step 1: Run unit and integration tests**
Run: `python -m pytest tests/unit tests/integration -q`
Expected: all pass
- [ ] **Step 2: Run full suite including acceptance**
Run: `python -m pytest tests/unit tests/integration tests/acceptance -q`
Expected: all pass, with acceptance either passing or explicitly skipped when env vars are absent
- [ ] **Step 3: Compile the sample workflow and verify output**
Run: `python -m engine.devops_agent.cli compile workflows/gitea-issue-delivery.md --output workflows/gitea-issue-delivery.lock.json`
Expected: lock file generated successfully
- [ ] **Step 4: Validate the sample workflow**
Run: `python -m engine.devops_agent.cli validate workflows/gitea-issue-delivery.md`
Expected: success output and zero exit code
- [ ] **Step 5: Review diff formatting**
Run: `git diff --check`
Expected: no formatting errors

View File

@@ -0,0 +1,188 @@
# Jj Default Installation Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make `jj` a default installation target in this repository's one-command installers without making installer success depend on `jj` success, and expand documentation with clearer usage instructions and scenario-based examples.
**Architecture:** Keep the public installer entrypoints unchanged while adding an internal second phase that attempts platform-appropriate `jj` installation. Preserve deterministic skill installation first, then run best-effort `jj` installation with explicit verification and manual fallback instructions.
**Tech Stack:** Bash, PowerShell, Git, README Markdown, skill reference docs
---
## Chunk 1: Installer Behavior
### Task 1: Add plan-aware installer controls to the Bash installer
**Files:**
- Modify: `install/install.sh`
- Test: `install/install.sh` syntax via `bash -n`
- [ ] **Step 1: Define installer controls**
Add environment-driven controls near the top of `install/install.sh`:
- `INSTALL_JJ` default `1`
- `JJ_INSTALL_METHOD` default `auto`
- `JJ_CHANNEL` default `release`
- [ ] **Step 2: Add `jj` install attempt helpers**
Implement focused functions for:
- logging
- command existence checks
- `brew` install path
- `cargo-binstall` install path
- `cargo install` fallback path
- manual fallback message
- [ ] **Step 3: Add OS-aware default attempt flow**
Implement this order:
- macOS: `brew`, then `cargo-binstall`, then `cargo`
- Linux: `brew` if available, then `cargo-binstall`, then `cargo`
- [ ] **Step 4: Keep skill installation authoritative**
Ensure the script:
- installs the skill first
- attempts `jj` only after the skill copy succeeds
- never exits non-zero only because `jj` failed
- [ ] **Step 5: Verify shell syntax**
Run: `bash -n install/install.sh`
Expected: no output, zero exit code
### Task 2: Add plan-aware installer controls to the PowerShell installer
**Files:**
- Modify: `install/install.ps1`
- Test: `install/install.ps1` parse check via PowerShell
- [ ] **Step 1: Define parameters and defaults**
Add parameters:
- `SkipJj`
- `JjInstallMethod = "auto"`
- `JjChannel = "release"`
- [ ] **Step 2: Add `jj` install attempt helpers**
Implement functions for:
- logging
- command existence checks
- `winget` install path
- `scoop` install path
- `cargo install` fallback path
- manual fallback message
- [ ] **Step 3: Add Windows attempt flow**
Implement this order:
- `winget`
- `scoop`
- `cargo`
- [ ] **Step 4: Keep installer non-blocking for `jj`**
Ensure skill installation still succeeds even if all `jj` attempts fail.
- [ ] **Step 5: Verify script parses**
Run: `powershell -NoProfile -Command "[void][scriptblock]::Create((Get-Content -Raw 'install/install.ps1'))"`
Expected: no output, zero exit code
## Chunk 2: Documentation
### Task 3: Expand README installation and usage guidance
**Files:**
- Modify: `README.md`
- [ ] **Step 1: Update one-command install sections**
Document that installers now:
- install the skill
- attempt `jj` by default
- continue with warnings if `jj` cannot be installed automatically
- [ ] **Step 2: Add installer controls and verification**
Document:
- `INSTALL_JJ=0`
- `JJ_INSTALL_METHOD`
- `JJ_CHANNEL`
- PowerShell equivalents
- `jj --version`
- initial `jj config` commands
- [ ] **Step 3: Add richer scenario examples**
Include examples for:
- first-time team setup
- automatic bug fix flow
- semi-automatic engineering review flow
- manual hotfix flow
- multi-agent flow with `jj` workspaces
### Task 4: Add detailed `jj` usage guide
**Files:**
- Create: `skills/gitea-issue-devops-agent/references/jj-default-usage.md`
- [ ] **Step 1: Explain repository policy**
Document:
- `jj` is default to install
- `jj` is internal execution infrastructure
- Git branches/PRs/CI remain public system of record
- [ ] **Step 2: Explain verification and fallback**
Document:
- how to verify installation
- how to skip or force methods
- what to do when package managers are unavailable
- [ ] **Step 3: Add workflow examples**
Provide concrete examples for:
- issue -> plan -> draft PR
- engineer review after initial AI PR
- `jj workspace` for TestAgent and human reviewer
- rollback with operation log
## Chunk 3: Verification and Delivery
### Task 5: Verify, commit, and push
**Files:**
- Modify: repo index and git history
- [ ] **Step 1: Run syntax checks**
Run:
- `bash -n install/install.sh`
- `powershell -NoProfile -Command "[void][scriptblock]::Create((Get-Content -Raw 'install/install.ps1'))"`
Expected: both succeed
- [ ] **Step 2: Review diff**
Run: `git diff --check`
Expected: no diff formatting errors
- [ ] **Step 3: Commit**
Run:
```bash
git add README.md install/install.sh install/install.ps1 docs/superpowers/plans/2026-03-13-jj-default-installation-plan.md skills/gitea-issue-devops-agent/references/jj-default-usage.md
git commit -m "docs: default jj installation in installers"
```
- [ ] **Step 4: Push**
Run:
```bash
git push origin main
```

View File

@@ -0,0 +1,268 @@
# Gitea Agentic Runtime Design
**Date:** 2026-03-13
**Status:** Approved for implementation
**Scope:** Gitea single-platform real acceptance, with provider boundaries that keep later GitHub support incremental instead of invasive.
## Goal
Upgrade `gitea-issue-devops-agent` from a documentation-heavy delivery skill into a controlled execution subsystem that can:
- parse repository-local workflow specs
- compile and validate those specs into a locked execution plan
- run a Gitea-triggered issue workflow under hard policies
- persist plan and evidence artifacts
- pass automated unit, integration, and real Gitea acceptance checks
The external workflow remains:
`Issue -> Plan -> Branch -> Draft PR -> Preview -> Test Loop -> Human Merge`
The internal workflow gains a deterministic control plane.
## Why This Design
The repository currently has strong delivery guidance, but not a runtime that enforces it. `gh-aw` demonstrates the value of turning natural-language workflow definitions into constrained executable automations. We do not want to clone GitHub-native product shape. We do want equivalent execution discipline:
- a spec format users can author
- a compiler/validator that rejects unsafe or incomplete workflows
- a provider layer for platform APIs
- hard policy checks before write actions
- run evidence suitable for audit and acceptance
This keeps the product positioned as an enterprise AI DevOps control layer, not a GitHub Actions clone.
## In Scope
- Gitea provider
- workflow spec parsing
- compilation into lock artifacts
- validation of triggers, permissions, safe outputs, edit scope, and evidence contract
- runtime execution for selected issue flows
- safe output enforcement for Gitea writes
- evidence persistence
- CLI entrypoints for compile, validate, run, and acceptance
- automated tests
- one real Gitea acceptance path
## Out of Scope
- GitHub provider implementation
- autonomous queue-wide issue fixing
- automatic merge
- hosted webhook service
- UI control plane
- inference cost dashboards
## Architecture
### 1. Workflow Spec
Workflow specs live in-repo and use frontmatter plus Markdown body:
- frontmatter defines triggers, provider, permissions, safe outputs, required evidence, and policy defaults
- body captures workflow intent, operator notes, and execution hints
The spec is the source of truth. Runtime behavior never depends on free-form Markdown alone.
### 2. Compiler
The compiler loads a workflow spec and emits a lock artifact:
- normalizes defaults
- expands trigger shorthand into explicit configuration
- resolves safe output declarations into executable policy objects
- records required evidence and path scope
The lock artifact is immutable input to runtime. It is designed as JSON in this iteration because JSON is easy to diff, assert in tests, and consume from Python.
### 3. Validator
Validation rejects unsafe or incomplete specs before runtime:
- unsupported trigger combinations
- missing provider
- unsafe write permissions
- missing safe outputs for write behaviors
- invalid path scope syntax
- missing evidence requirements
This is the first hard boundary between intent and execution.
### 4. Runtime
Runtime consumes:
- a compiled lock artifact
- an event payload
- provider configuration and credentials
Runtime responsibilities:
- load and sanitize issue-trigger context
- initialize or update the persisted plan state
- enforce policy gates before any write action
- call provider APIs through a narrow interface
- collect evidence and write run artifacts
The runtime is not a general autonomous coding engine in this iteration. It is a control and orchestration layer for issue delivery actions.
### 5. Provider Layer
Provider interfaces isolate platform behavior from workflow logic.
The first provider is `GiteaProvider`, which supports:
- repository metadata reads
- issue reads
- issue comments
- branch hint extraction inputs
- draft PR preparation primitives
The abstraction must make later `GitHubProvider` addition additive rather than structural.
### 6. Policy and Safe Outputs
The policy layer turns delivery rules into code:
- read-only by default
- no merge action
- comment/create/update operations only if declared in safe outputs
- path-scope enforcement for file writes
- evidence-required status promotion
- bounded output counts where relevant
This is the key product maturity improvement over pure skill text.
### 7. Evidence Layer
Every run produces durable artifacts:
- resolved plan state
- execution summary
- provider operations executed
- evidence bundle for commit/PR/test/preview placeholders
- acceptance result metadata
Evidence is stored locally under a deterministic run directory so tests and operators can inspect it.
## File Structure
New code will be added under:
- `pyproject.toml`
- `engine/devops_agent/__init__.py`
- `engine/devops_agent/spec.py`
- `engine/devops_agent/compiler.py`
- `engine/devops_agent/validator.py`
- `engine/devops_agent/runtime.py`
- `engine/devops_agent/policies.py`
- `engine/devops_agent/evidence.py`
- `engine/devops_agent/cli.py`
- `engine/devops_agent/providers/__init__.py`
- `engine/devops_agent/providers/base.py`
- `engine/devops_agent/providers/gitea.py`
- `workflows/gitea-issue-delivery.md`
- `tests/unit/...`
- `tests/integration/...`
- `tests/fixtures/gitea/...`
- `tests/acceptance/test_gitea_acceptance.py`
Existing scripts will remain, but runtime may call them or share logic conceptually:
- `skills/gitea-issue-devops-agent/scripts/issue_audit.py`
- `skills/gitea-issue-devops-agent/scripts/change_scope.py`
- `skills/gitea-issue-devops-agent/scripts/preview_slot_allocator.py`
## Data Model
### Workflow Spec
Core fields:
- `name`
- `provider`
- `on`
- `permissions`
- `safe_outputs`
- `plan`
- `policy`
- `evidence`
- `body`
### Lock Artifact
Core fields:
- `version`
- `compiled_at`
- `source`
- `provider`
- `triggers`
- `policy`
- `safe_outputs`
- `required_evidence`
- `plan_defaults`
- `instructions`
### Run Artifact
Core fields:
- `run_id`
- `workflow_name`
- `provider`
- `event`
- `plan_state`
- `operations`
- `evidence`
- `result`
## Test Strategy
### Unit Tests
Verify:
- frontmatter parsing
- default normalization
- validator failures and successes
- policy enforcement
- provider request shaping
### Integration Tests
Verify:
- spec to lock compilation
- lock to runtime execution
- runtime interaction with a fake Gitea transport
- evidence persistence
### Acceptance Tests
Verify against a real Gitea repo using env vars:
- workflow compile and validate pass
- runtime can load a selected issue
- runtime can publish an evidence comment
- acceptance artifacts are produced locally
If env vars are absent, the acceptance suite must skip cleanly instead of failing misleadingly.
## Acceptance Criteria
This design is complete only when:
1. A repo-local Gitea workflow spec compiles into a lock artifact.
2. Invalid specs fail validation with clear messages.
3. Runtime enforces safe outputs and refuses undeclared writes.
4. Gitea real acceptance can read an issue and publish an evidence comment.
5. Automated unit and integration tests pass.
6. README and skill docs describe the new execution model and CLI usage.
## Rollout Notes
- Gitea is the first real provider.
- GitHub support is intentionally deferred, but the provider interface must be stable enough to add later.
- `jj` remains an internal reliability layer. This subsystem must not require `jj` for external usage.

View File

@@ -0,0 +1,5 @@
"""Runtime package for agentic DevOps workflow execution."""
__all__ = ["__version__"]
__version__ = "0.1.0"

130
engine/devops_agent/cli.py Normal file
View File

@@ -0,0 +1,130 @@
from __future__ import annotations
import argparse
import json
from collections.abc import Sequence
from pathlib import Path
from engine.devops_agent.compiler import compile_workflow
from engine.devops_agent.providers.gitea import GiteaProvider
from engine.devops_agent.runtime import run_issue_comment_workflow
from engine.devops_agent.spec import load_workflow_spec
from engine.devops_agent.validator import validate_workflow_spec
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="devops-agent",
description="CLI for the agentic DevOps runtime.",
)
parser.add_argument(
"--version",
action="store_true",
help="Print the runtime version and exit.",
)
subparsers = parser.add_subparsers(dest="command")
compile_parser = subparsers.add_parser("compile")
compile_parser.add_argument("spec_path")
compile_parser.add_argument("--output", required=True)
validate_parser = subparsers.add_parser("validate")
validate_parser.add_argument("spec_path")
run_parser = subparsers.add_parser("run")
run_parser.add_argument("spec_path")
run_parser.add_argument("--event-payload", required=True)
run_parser.add_argument("--output-dir", required=True)
run_parser.add_argument("--base-url", required=True)
run_parser.add_argument("--token", required=True)
acceptance_parser = subparsers.add_parser("acceptance")
acceptance_parser.add_argument("spec_path")
acceptance_parser.add_argument("--base-url", required=True)
acceptance_parser.add_argument("--repo", required=True)
acceptance_parser.add_argument("--token", required=True)
acceptance_parser.add_argument("--issue-number", required=True)
acceptance_parser.add_argument("--output-dir", required=True)
acceptance_parser.add_argument(
"--comment-body",
default="@devops-agent acceptance run",
)
return parser
def _load_compile_and_validate(spec_path: str) -> tuple[dict[str, object], list[str]]:
spec = load_workflow_spec(spec_path)
errors = validate_workflow_spec(spec)
return compile_workflow(spec), errors
def main(argv: Sequence[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
if args.version:
from engine.devops_agent import __version__
print(__version__)
return 0
if not getattr(args, "command", None):
parser.print_help()
return 0
if args.command == "compile":
lock, errors = _load_compile_and_validate(args.spec_path)
if errors:
print(json.dumps({"errors": errors}, ensure_ascii=False, indent=2))
return 1
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(lock, ensure_ascii=False, indent=2), encoding="utf-8")
return 0
if args.command == "validate":
_, errors = _load_compile_and_validate(args.spec_path)
if errors:
print(json.dumps({"errors": errors}, ensure_ascii=False, indent=2))
return 1
print("workflow is valid")
return 0
if args.command == "run":
lock, errors = _load_compile_and_validate(args.spec_path)
if errors:
print(json.dumps({"errors": errors}, ensure_ascii=False, indent=2))
return 1
provider = GiteaProvider(base_url=args.base_url, token=args.token)
payload = json.loads(Path(args.event_payload).read_text(encoding="utf-8"))
run_issue_comment_workflow(
lock=lock,
provider=provider,
event_payload=payload,
output_dir=args.output_dir,
)
return 0
if args.command == "acceptance":
lock, errors = _load_compile_and_validate(args.spec_path)
if errors:
print(json.dumps({"errors": errors}, ensure_ascii=False, indent=2))
return 1
provider = GiteaProvider(base_url=args.base_url, token=args.token)
payload = {
"repository": {"full_name": args.repo},
"issue": {"number": int(args.issue_number)},
"comment": {"body": args.comment_body},
}
run_issue_comment_workflow(
lock=lock,
provider=provider,
event_payload=payload,
output_dir=args.output_dir,
)
return 0
parser.error(f"unsupported command: {args.command}")
return 2
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,42 @@
from __future__ import annotations
from typing import Any
from engine.devops_agent.spec import WorkflowSpec
def _compile_triggers(frontmatter: dict[str, Any]) -> list[dict[str, Any]]:
triggers = frontmatter.get("on") or {}
if not isinstance(triggers, dict):
return []
compiled: list[dict[str, Any]] = []
for event_name, event_config in triggers.items():
normalized = {
"event": str(event_name),
}
if isinstance(event_config, dict):
normalized.update(event_config)
compiled.append(normalized)
return compiled
def compile_workflow(spec: WorkflowSpec) -> dict[str, Any]:
policy = spec.frontmatter.get("policy") or {}
evidence = spec.frontmatter.get("evidence") or {}
return {
"version": 1,
"workflow_name": spec.name,
"provider": spec.provider,
"source": str(spec.source_path.as_posix()),
"triggers": _compile_triggers(spec.frontmatter),
"safe_outputs": spec.frontmatter.get("safe_outputs") or {},
"required_evidence": evidence.get("required") or [],
"policy": {
"require_human_merge": bool(policy.get("require_human_merge", True)),
"require_fixed_issue": bool(policy.get("require_fixed_issue", False)),
"path_scope": policy.get("path_scope") or [],
},
"instructions": spec.body,
}

View File

@@ -0,0 +1,16 @@
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
def write_run_artifact(output_dir: str | Path, artifact: dict[str, Any]) -> Path:
destination = Path(output_dir)
destination.mkdir(parents=True, exist_ok=True)
artifact_path = destination / "run-artifact.json"
artifact_path.write_text(
json.dumps(artifact, ensure_ascii=False, indent=2),
encoding="utf-8",
)
return artifact_path

View File

@@ -0,0 +1,43 @@
from __future__ import annotations
from dataclasses import dataclass, field
class PolicyViolation(PermissionError):
"""Raised when runtime behavior violates declared workflow policy."""
def _normalize_path(path: str) -> str:
return path.replace("\\", "/").lstrip("./")
@dataclass(slots=True)
class RuntimePolicy:
safe_outputs: dict[str, dict[str, int | str | bool]]
path_scope: list[str]
_operation_counts: dict[str, int] = field(default_factory=dict)
def assert_operation_allowed(self, action: str) -> None:
config = self.safe_outputs.get(action)
if config is None:
raise PolicyViolation(f"write action '{action}' is not declared in safe_outputs")
current_count = self._operation_counts.get(action, 0) + 1
max_count = int(config.get("max", current_count))
if current_count > max_count:
raise PolicyViolation(f"write action '{action}' exceeded max count {max_count}")
self._operation_counts[action] = current_count
def assert_path_allowed(self, path: str) -> None:
normalized = _normalize_path(path)
if not self.path_scope:
raise PolicyViolation("file writes are not allowed without an explicit path scope")
for allowed_prefix in self.path_scope:
if normalized.startswith(_normalize_path(allowed_prefix)):
return
raise PolicyViolation(
f"path '{normalized}' is outside allowed path scope {self.path_scope}"
)

View File

@@ -0,0 +1,4 @@
from engine.devops_agent.providers.base import IssueProvider
from engine.devops_agent.providers.gitea import GiteaProvider
__all__ = ["IssueProvider", "GiteaProvider"]

View File

@@ -0,0 +1,16 @@
from __future__ import annotations
from typing import Any, Protocol
class IssueProvider(Protocol):
def get_issue(self, repo: str, issue_number: int) -> dict[str, Any]: ...
def post_issue_comment(
self,
repo: str,
issue_number: int,
body: str,
) -> dict[str, Any]: ...
def parse_issue_comment_event(self, payload: dict[str, Any]) -> dict[str, Any]: ...

View File

@@ -0,0 +1,73 @@
from __future__ import annotations
import json
from typing import Any, Callable
from urllib.request import Request, urlopen
Transport = Callable[..., dict[str, Any]]
class GiteaProvider:
def __init__(
self,
*,
base_url: str,
token: str,
transport: Transport | None = None,
) -> None:
self.base_url = base_url.rstrip("/")
self.token = token
self.transport = transport
def _request(
self,
*,
method: str,
path: str,
body: dict[str, object] | None = None,
) -> dict[str, Any]:
url = f"{self.base_url}{path}"
headers = {
"Authorization": f"token {self.token}",
"Accept": "application/json",
"Content-Type": "application/json",
}
if self.transport is not None:
return self.transport(method=method, url=url, headers=headers, body=body)
payload = None if body is None else json.dumps(body).encode("utf-8")
request = Request(url, method=method, headers=headers, data=payload)
with urlopen(request, timeout=30) as response:
raw = response.read().decode("utf-8")
return json.loads(raw) if raw else {}
def get_issue(self, repo: str, issue_number: int) -> dict[str, Any]:
return self._request(
method="GET",
path=f"/api/v1/repos/{repo}/issues/{issue_number}",
)
def post_issue_comment(
self,
repo: str,
issue_number: int,
body: str,
) -> dict[str, Any]:
return self._request(
method="POST",
path=f"/api/v1/repos/{repo}/issues/{issue_number}/comments",
body={"body": body},
)
def parse_issue_comment_event(self, payload: dict[str, Any]) -> dict[str, Any]:
repository = payload.get("repository") or {}
issue = payload.get("issue") or {}
comment = payload.get("comment") or {}
return {
"repo": repository.get("full_name", ""),
"issue_number": int(issue.get("number", 0)),
"comment_body": str(comment.get("body", "")),
}

View File

@@ -0,0 +1,65 @@
from __future__ import annotations
from pathlib import Path
from typing import Any
from engine.devops_agent.evidence import write_run_artifact
from engine.devops_agent.policies import RuntimePolicy
def run_issue_comment_workflow(
*,
lock: dict[str, Any],
provider: Any,
event_payload: dict[str, Any],
output_dir: str | Path,
) -> dict[str, Any]:
event = provider.parse_issue_comment_event(event_payload)
repo = str(event["repo"])
issue_number = int(event["issue_number"])
issue = provider.get_issue(repo, issue_number)
policy = RuntimePolicy(
safe_outputs=lock.get("safe_outputs") or {},
path_scope=lock.get("policy", {}).get("path_scope") or [],
)
policy.assert_operation_allowed("add_comment")
verification_summary = (
f"Workflow `{lock['workflow_name']}` processed issue #{issue_number} "
f"and prepared evidence for review."
)
comment_response = provider.post_issue_comment(
repo,
issue_number,
verification_summary,
)
artifact: dict[str, Any] = {
"run_id": f"{lock['workflow_name']}-issue-{issue_number}",
"workflow_name": lock["workflow_name"],
"provider": lock["provider"],
"event": event,
"plan_state": {
"status": "pending_review",
"repo": repo,
"issue_number": issue_number,
"issue_title": issue.get("title", ""),
},
"operations": [
{
"action": "add_comment",
"issue_number": issue_number,
"repo": repo,
}
],
"evidence": {
"issue_comment": comment_response,
"verification_summary": verification_summary,
},
"result": "success",
}
artifact_path = write_run_artifact(output_dir, artifact)
artifact["artifact_path"] = str(artifact_path.as_posix())
artifact_path.write_text(__import__("json").dumps(artifact, ensure_ascii=False, indent=2), encoding="utf-8")
return artifact

View File

@@ -0,0 +1,60 @@
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Any
import yaml
class WorkflowSpecError(ValueError):
"""Raised when a workflow spec cannot be parsed or is incomplete."""
@dataclass(slots=True)
class WorkflowSpec:
name: str
provider: str
frontmatter: dict[str, Any]
body: str
source_path: Path
def _split_frontmatter(raw_text: str) -> tuple[str, str]:
if not raw_text.startswith("---"):
raise WorkflowSpecError("workflow spec must start with frontmatter")
parts = raw_text.split("\n---", 1)
if len(parts) != 2:
raise WorkflowSpecError("workflow spec frontmatter is not terminated")
frontmatter_text = parts[0][4:]
body = parts[1].lstrip("\r\n")
return frontmatter_text, body
def load_workflow_spec(path: str | Path) -> WorkflowSpec:
source_path = Path(path)
raw_text = source_path.read_text(encoding="utf-8")
frontmatter_text, body = _split_frontmatter(raw_text)
payload = yaml.safe_load(frontmatter_text) or {}
if not isinstance(payload, dict):
raise WorkflowSpecError("workflow spec frontmatter must be a mapping")
if True in payload and "on" not in payload:
payload["on"] = payload.pop(True)
name = str(payload.get("name") or "").strip()
provider = str(payload.get("provider") or "").strip()
if not name:
raise WorkflowSpecError("workflow spec is missing required field: name")
if not provider:
raise WorkflowSpecError("workflow spec is missing required field: provider")
return WorkflowSpec(
name=name,
provider=provider,
frontmatter=payload,
body=body,
source_path=source_path,
)

View File

@@ -0,0 +1,49 @@
from __future__ import annotations
from typing import Any
from engine.devops_agent.spec import WorkflowSpec
WRITE_PERMISSIONS = {"issues", "pull_requests", "contents"}
def _is_write_permission(value: Any) -> bool:
return str(value).strip().lower() == "write"
def validate_workflow_spec(spec: WorkflowSpec) -> list[str]:
errors: list[str] = []
if spec.provider not in {"gitea"}:
errors.append(f"unsupported provider: {spec.provider}")
triggers = spec.frontmatter.get("on")
if not isinstance(triggers, dict) or not triggers:
errors.append("workflow spec must declare at least one trigger in 'on'")
permissions = spec.frontmatter.get("permissions") or {}
safe_outputs = spec.frontmatter.get("safe_outputs") or {}
if not isinstance(permissions, dict):
errors.append("'permissions' must be a mapping")
if not isinstance(safe_outputs, dict):
errors.append("'safe_outputs' must be a mapping")
if isinstance(permissions, dict):
has_write_permission = any(
permission_name in WRITE_PERMISSIONS and _is_write_permission(permission_value)
for permission_name, permission_value in permissions.items()
)
if has_write_permission and not safe_outputs:
errors.append("write permissions require declared safe_outputs")
policy = spec.frontmatter.get("policy") or {}
if policy and not isinstance(policy, dict):
errors.append("'policy' must be a mapping")
elif isinstance(policy, dict) and "path_scope" in policy:
path_scope = policy["path_scope"]
if not isinstance(path_scope, list) or any(
not isinstance(item, str) or not item.strip() for item in path_scope
):
errors.append("policy.path_scope must be a list of non-empty path prefixes")
return errors

View File

@@ -1,6 +1,11 @@
param( param(
[string]$RepoUrl = "https://fun-md.com/Fun_MD/devops-skills.git", [string]$RepoUrl = "https://fun-md.com/Fun_MD/devops-skills.git",
[string]$CodexHome = "$HOME\.codex" [string]$CodexHome = "$HOME\.codex",
[switch]$SkipJj,
[ValidateSet("auto", "winget", "scoop", "cargo")]
[string]$JjInstallMethod = $(if ($env:JJ_INSTALL_METHOD) { $env:JJ_INSTALL_METHOD } else { "auto" }),
[ValidateSet("release", "prerelease")]
[string]$JjChannel = $(if ($env:JJ_CHANNEL) { $env:JJ_CHANNEL } else { "release" })
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
@@ -8,13 +13,136 @@ $ErrorActionPreference = "Stop"
$skillName = "gitea-issue-devops-agent" $skillName = "gitea-issue-devops-agent"
$targetDir = Join-Path $CodexHome "skills\$skillName" $targetDir = Join-Path $CodexHome "skills\$skillName"
$tmpRoot = Join-Path $env:TEMP ("devops-skills-" + [Guid]::NewGuid().ToString("N")) $tmpRoot = Join-Path $env:TEMP ("devops-skills-" + [Guid]::NewGuid().ToString("N"))
$installJj = $true
if ($SkipJj.IsPresent) {
$installJj = $false
}
if ($env:INSTALL_JJ -and $env:INSTALL_JJ -eq "0") {
$installJj = $false
}
function Write-InstallLog {
param([string]$Message)
Write-Host "[install] $Message"
}
function Write-InstallWarn {
param([string]$Message)
Write-Warning "[install] $Message"
}
function Test-CommandAvailable {
param([string]$Name)
return $null -ne (Get-Command $Name -ErrorAction SilentlyContinue)
}
function Show-JjManualHelp {
Write-InstallWarn "jj was not installed automatically."
Write-InstallWarn "Manual options:"
Write-InstallWarn " - winget install jj-vcs.jj"
Write-InstallWarn " - scoop install main/jj"
if ($JjChannel -eq "prerelease") {
Write-InstallWarn " - cargo install --git https://github.com/jj-vcs/jj.git --locked --bin jj jj-cli"
}
else {
Write-InstallWarn " - cargo install --locked --bin jj jj-cli"
}
Write-InstallWarn "After installation, verify with: jj --version"
}
function Install-JjWithWinget {
if (-not (Test-CommandAvailable winget)) {
return $false
}
if ($JjChannel -ne "release") {
Write-InstallWarn "winget path skipped because prerelease jj is requested."
return $false
}
Write-InstallLog "attempting jj installation via winget"
& winget install --id jj-vcs.jj -e --accept-source-agreements --accept-package-agreements
return $LASTEXITCODE -eq 0
}
function Install-JjWithScoop {
if (-not (Test-CommandAvailable scoop)) {
return $false
}
if ($JjChannel -ne "release") {
Write-InstallWarn "scoop path skipped because prerelease jj is requested."
return $false
}
Write-InstallLog "attempting jj installation via scoop"
& scoop install main/jj
return $LASTEXITCODE -eq 0
}
function Install-JjWithCargo {
if (-not (Test-CommandAvailable cargo)) {
return $false
}
Write-InstallLog "attempting jj installation via cargo"
if ($JjChannel -eq "prerelease") {
& cargo install --git https://github.com/jj-vcs/jj.git --locked --bin jj jj-cli
}
else {
& cargo install --locked --bin jj jj-cli
}
return $LASTEXITCODE -eq 0
}
function Invoke-JjInstall {
if (-not $installJj) {
Write-InstallLog "skipping jj installation because INSTALL_JJ=0 or -SkipJj was provided"
return
}
if (Test-CommandAvailable jj) {
Write-InstallLog "jj already installed: $(& jj --version)"
return
}
$installed = $false
switch ($JjInstallMethod) {
"auto" {
$installed = (Install-JjWithWinget) -or (Install-JjWithScoop) -or (Install-JjWithCargo)
break
}
"winget" {
$installed = Install-JjWithWinget
break
}
"scoop" {
$installed = Install-JjWithScoop
break
}
"cargo" {
$installed = Install-JjWithCargo
break
}
}
if (-not $installed) {
Show-JjManualHelp
return
}
if (Test-CommandAvailable jj) {
Write-InstallLog "jj installation succeeded: $(& jj --version)"
}
else {
Show-JjManualHelp
}
}
try { try {
if (-not (Get-Command git -ErrorAction SilentlyContinue)) { if (-not (Test-CommandAvailable git)) {
throw "[install] git is required but not found." throw "[install] git is required but not found."
} }
Write-Host "[install] downloading $skillName from $RepoUrl" Write-InstallLog "downloading $skillName from $RepoUrl"
git clone --depth 1 $RepoUrl $tmpRoot | Out-Null git clone --depth 1 $RepoUrl $tmpRoot | Out-Null
$sourceDir = Join-Path $tmpRoot "skills\$skillName" $sourceDir = Join-Path $tmpRoot "skills\$skillName"
@@ -28,8 +156,10 @@ try {
} }
Copy-Item -Path $sourceDir -Destination $targetDir -Recurse -Force Copy-Item -Path $sourceDir -Destination $targetDir -Recurse -Force
Write-Host "[install] done" Write-InstallLog "skill installed"
Write-Host "[install] installed path: $targetDir" Write-InstallLog "installed path: $targetDir"
Invoke-JjInstall
Write-InstallLog "done"
} }
finally { finally {
if (Test-Path $tmpRoot) { if (Test-Path $tmpRoot) {

View File

@@ -6,6 +6,119 @@ SKILL_NAME="gitea-issue-devops-agent"
CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
TARGET_DIR="${CODEX_HOME}/skills/${SKILL_NAME}" TARGET_DIR="${CODEX_HOME}/skills/${SKILL_NAME}"
TMP_DIR="$(mktemp -d)" TMP_DIR="$(mktemp -d)"
INSTALL_JJ="${INSTALL_JJ:-1}"
JJ_INSTALL_METHOD="${JJ_INSTALL_METHOD:-auto}"
JJ_CHANNEL="${JJ_CHANNEL:-release}"
log() {
echo "[install] $*"
}
warn() {
echo "[install] warning: $*" >&2
}
manual_jj_help() {
warn "jj was not installed automatically."
warn "Manual options:"
warn " - Homebrew: brew install jj"
warn " - cargo-binstall: cargo binstall --strategies crate-meta-data jj-cli"
if [ "$JJ_CHANNEL" = "prerelease" ]; then
warn " - cargo prerelease: cargo install --git https://github.com/jj-vcs/jj.git --locked --bin jj jj-cli"
else
warn " - cargo release: cargo install --locked --bin jj jj-cli"
fi
warn "After installation, verify with: jj --version"
}
install_jj_with_brew() {
if ! command -v brew >/dev/null 2>&1; then
return 1
fi
if [ "$JJ_CHANNEL" != "release" ]; then
warn "brew path skipped because prerelease jj is requested."
return 1
fi
log "attempting jj installation via Homebrew"
brew install jj
}
install_jj_with_binstall() {
if ! command -v cargo >/dev/null 2>&1 || ! command -v cargo-binstall >/dev/null 2>&1; then
return 1
fi
if [ "$JJ_CHANNEL" != "release" ]; then
warn "cargo-binstall path skipped because prerelease jj is requested."
return 1
fi
log "attempting jj installation via cargo-binstall"
cargo binstall --strategies crate-meta-data jj-cli
}
install_jj_with_cargo() {
if ! command -v cargo >/dev/null 2>&1; then
return 1
fi
log "attempting jj installation via cargo"
if [ "$JJ_CHANNEL" = "prerelease" ]; then
cargo install --git https://github.com/jj-vcs/jj.git --locked --bin jj jj-cli
else
cargo install --locked --bin jj jj-cli
fi
}
attempt_jj_install() {
if [ "$INSTALL_JJ" = "0" ]; then
log "skipping jj installation because INSTALL_JJ=0"
return 0
fi
if command -v jj >/dev/null 2>&1; then
log "jj already installed: $(jj --version)"
return 0
fi
case "$JJ_INSTALL_METHOD" in
auto)
install_jj_with_brew || install_jj_with_binstall || install_jj_with_cargo || {
manual_jj_help
return 0
}
;;
brew)
install_jj_with_brew || {
manual_jj_help
return 0
}
;;
binstall)
install_jj_with_binstall || {
manual_jj_help
return 0
}
;;
cargo)
install_jj_with_cargo || {
manual_jj_help
return 0
}
;;
*)
warn "unsupported JJ_INSTALL_METHOD='$JJ_INSTALL_METHOD'; skipping jj install."
manual_jj_help
return 0
;;
esac
if command -v jj >/dev/null 2>&1; then
log "jj installation succeeded: $(jj --version)"
else
manual_jj_help
fi
}
cleanup() { cleanup() {
rm -rf "$TMP_DIR" rm -rf "$TMP_DIR"
@@ -13,15 +126,15 @@ cleanup() {
trap cleanup EXIT trap cleanup EXIT
if ! command -v git >/dev/null 2>&1; then if ! command -v git >/dev/null 2>&1; then
echo "[install] git is required but not found." log "git is required but not found."
exit 1 exit 1
fi fi
echo "[install] downloading ${SKILL_NAME} from ${REPO_URL}" log "downloading ${SKILL_NAME} from ${REPO_URL}"
git clone --depth 1 "$REPO_URL" "$TMP_DIR/repo" >/dev/null 2>&1 git clone --depth 1 "$REPO_URL" "$TMP_DIR/repo" >/dev/null 2>&1
if [ ! -d "$TMP_DIR/repo/skills/${SKILL_NAME}" ]; then if [ ! -d "$TMP_DIR/repo/skills/${SKILL_NAME}" ]; then
echo "[install] skill directory not found in repository." log "skill directory not found in repository."
exit 1 exit 1
fi fi
@@ -29,5 +142,7 @@ mkdir -p "${CODEX_HOME}/skills"
rm -rf "$TARGET_DIR" rm -rf "$TARGET_DIR"
cp -R "$TMP_DIR/repo/skills/${SKILL_NAME}" "$TARGET_DIR" cp -R "$TMP_DIR/repo/skills/${SKILL_NAME}" "$TARGET_DIR"
echo "[install] done" log "skill installed"
echo "[install] installed path: ${TARGET_DIR}" log "installed path: ${TARGET_DIR}"
attempt_jj_install
log "done"

14
pyproject.toml Normal file
View File

@@ -0,0 +1,14 @@
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "devops-agent-runtime"
version = "0.1.0"
description = "Policy-enforced agentic delivery runtime for Gitea issue workflows."
requires-python = ">=3.11"
dependencies = ["PyYAML>=6"]
[tool.pytest.ini_options]
addopts = "-p no:cacheprovider"
testpaths = ["tests"]

213
site/index.svg Normal file
View File

@@ -0,0 +1,213 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="2500" viewBox="0 0 1600 2500">
<defs>
<linearGradient id="bgGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#070c19"/>
<stop offset="55%" stop-color="#102347"/>
<stop offset="100%" stop-color="#0a3a4f"/>
</linearGradient>
<linearGradient id="accentGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#29d8ff"/>
<stop offset="100%" stop-color="#49f2c7"/>
</linearGradient>
<linearGradient id="cardGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#122445" stop-opacity="0.95"/>
<stop offset="100%" stop-color="#0f1d38" stop-opacity="0.95"/>
</linearGradient>
<filter id="softGlow" x="-40%" y="-40%" width="180%" height="180%">
<feGaussianBlur stdDeviation="18" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<style>
.title {
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
fill: #ecf4ff;
font-weight: 700;
}
.subtitle {
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
fill: #b8c9e9;
font-size: 30px;
}
.section-title {
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
fill: #e7f1ff;
font-size: 44px;
font-weight: 700;
}
.card-title {
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
fill: #def2ff;
font-size: 32px;
font-weight: 700;
}
.card-text {
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
fill: #bad0ee;
font-size: 24px;
}
.mono {
font-family: "Cascadia Mono", Consolas, Menlo, monospace;
fill: #d8f1ff;
font-size: 21px;
}
.label {
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
fill: #88a6d4;
font-size: 20px;
}
.badge {
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
fill: #0a2b3f;
font-size: 21px;
font-weight: 700;
}
.btn {
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
fill: #06253a;
font-size: 24px;
font-weight: 700;
}
.btn-muted {
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
fill: #d2e8ff;
font-size: 22px;
font-weight: 600;
}
</style>
</defs>
<rect x="0" y="0" width="1600" height="2500" fill="url(#bgGrad)"/>
<circle cx="1350" cy="240" r="260" fill="#29d8ff" opacity="0.13" filter="url(#softGlow)"/>
<circle cx="180" cy="320" r="220" fill="#49f2c7" opacity="0.11" filter="url(#softGlow)"/>
<circle cx="420" cy="2100" r="260" fill="#2fd8f0" opacity="0.10" filter="url(#softGlow)"/>
<rect x="70" y="70" width="1460" height="500" rx="32" fill="url(#cardGrad)" stroke="#345a90" stroke-width="2"/>
<rect x="110" y="120" width="390" height="56" rx="28" fill="url(#accentGrad)"/>
<text x="145" y="158" class="badge">Issue-Driven DevOps Platform</text>
<text x="110" y="255" class="title" font-size="74">gitea-issue-devops-agent</text>
<text x="110" y="325" class="subtitle">把 Issue → Branch → Preview Slot → Test Loop → Human-Confirmed Merge 变成标准交付引擎</text>
<text x="110" y="372" class="subtitle">不是临时脚本,而是可规模化的研发基础设施产品。</text>
<rect x="110" y="415" width="260" height="82" rx="14" fill="url(#accentGrad)"/>
<text x="152" y="468" class="btn">访问仓库</text>
<a href="https://fun-md.com/Fun_MD/devops-skills">
<rect x="110" y="415" width="260" height="82" rx="14" fill="#ffffff" opacity="0.001"/>
</a>
<rect x="390" y="415" width="330" height="82" rx="14" fill="#1b3157" stroke="#4068a5"/>
<text x="445" y="468" class="btn-muted">SKILL 规范文档</text>
<a href="https://fun-md.com/Fun_MD/devops-skills/raw/branch/main/skills/gitea-issue-devops-agent/SKILL.md">
<rect x="390" y="415" width="330" height="82" rx="14" fill="#ffffff" opacity="0.001"/>
</a>
<rect x="820" y="150" width="660" height="340" rx="22" fill="#0c1a34" stroke="#34598e"/>
<text x="860" y="215" class="label">核心指标</text>
<text x="860" y="285" class="title" font-size="58">3 Modes</text>
<text x="860" y="325" class="label">automatic / semi-automatic / manual</text>
<text x="1160" y="285" class="title" font-size="58">5 Scopes</text>
<text x="1160" y="325" class="label">skip / client / server / full / infra</text>
<text x="860" y="390" class="title" font-size="58">1 : 1 Binding</text>
<text x="860" y="430" class="label">Issue - Branch - Preview Slot 精确绑定</text>
<text x="1160" y="390" class="title" font-size="58">TTL Reclaim</text>
<text x="1160" y="430" class="label">空闲槽位自动回收</text>
<text x="80" y="680" class="section-title">产品核心价值</text>
<rect x="80" y="715" width="460" height="280" rx="20" fill="url(#cardGrad)" stroke="#35598f"/>
<text x="110" y="785" class="card-title">1. 分支隔离提测</text>
<text x="110" y="835" class="card-text">每个 issue 固定分支和预览环境,</text>
<text x="110" y="872" class="card-text">主干环境稳定回归,提测互不覆盖。</text>
<text x="110" y="909" class="card-text">团队并行效率显著提升。</text>
<rect x="570" y="715" width="460" height="280" rx="20" fill="url(#cardGrad)" stroke="#35598f"/>
<text x="600" y="785" class="card-title">2. 资源智能节流</text>
<text x="600" y="835" class="card-text">按改动自动识别部署范围,</text>
<text x="600" y="872" class="card-text">前端-only 改动不重启服务端,</text>
<text x="600" y="909" class="card-text">节省机器和运维成本。</text>
<rect x="1060" y="715" width="460" height="280" rx="20" fill="url(#cardGrad)" stroke="#35598f"/>
<text x="1090" y="785" class="card-title">3. 证据化闭环</text>
<text x="1090" y="835" class="card-text">提测结果、commit、环境链接、</text>
<text x="1090" y="872" class="card-text">验证步骤统一沉淀,</text>
<text x="1090" y="909" class="card-text">最终合并始终人工确认。</text>
<text x="80" y="1090" class="section-title">流程拓扑Issue 到交付)</text>
<rect x="80" y="1125" width="1440" height="220" rx="20" fill="url(#cardGrad)" stroke="#35598f"/>
<rect x="112" y="1170" width="250" height="130" rx="14" fill="#13284b" stroke="#3d639b"/>
<text x="138" y="1223" class="card-title" font-size="26">1. 引导连接</text>
<text x="138" y="1263" class="card-text" font-size="20">repo_url + api_key + mode</text>
<rect x="402" y="1170" width="250" height="130" rx="14" fill="#13284b" stroke="#3d639b"/>
<text x="430" y="1223" class="card-title" font-size="26">2. 质量审计</text>
<text x="430" y="1263" class="card-text" font-size="20">Issue + 图片附件 + 去重评分</text>
<rect x="692" y="1170" width="250" height="130" rx="14" fill="#13284b" stroke="#3d639b"/>
<text x="720" y="1223" class="card-title" font-size="26">3. 分支修复</text>
<text x="720" y="1263" class="card-text" font-size="20">严格在 issue 分支迭代</text>
<rect x="982" y="1170" width="250" height="130" rx="14" fill="#13284b" stroke="#3d639b"/>
<text x="1007" y="1223" class="card-title" font-size="26">4. 按范围部署</text>
<text x="1007" y="1263" class="card-text" font-size="20">skip/client/server/full/infra</text>
<rect x="1272" y="1170" width="216" height="130" rx="14" fill="#13284b" stroke="#3d639b"/>
<text x="1300" y="1223" class="card-title" font-size="26">5. 自动回收</text>
<text x="1300" y="1263" class="card-text" font-size="20">TTL + Close Release</text>
<line x1="362" y1="1235" x2="402" y2="1235" stroke="#58d8ff" stroke-width="4"/>
<line x1="652" y1="1235" x2="692" y2="1235" stroke="#58d8ff" stroke-width="4"/>
<line x1="942" y1="1235" x2="982" y2="1235" stroke="#58d8ff" stroke-width="4"/>
<line x1="1232" y1="1235" x2="1272" y2="1235" stroke="#58d8ff" stroke-width="4"/>
<text x="80" y="1440" class="section-title">一键安装命令Windows / macOS / Linux</text>
<rect x="80" y="1480" width="470" height="300" rx="18" fill="url(#cardGrad)" stroke="#35598f"/>
<text x="110" y="1542" class="card-title">Linux</text>
<rect x="110" y="1568" width="410" height="178" rx="12" fill="#091327" stroke="#406aa6"/>
<text x="130" y="1620" class="mono">curl -fsSL https://fun-md.com/Fun_MD/</text>
<text x="130" y="1656" class="mono">devops-skills/raw/branch/main/install/</text>
<text x="130" y="1692" class="mono">install.sh | bash</text>
<rect x="565" y="1480" width="470" height="300" rx="18" fill="url(#cardGrad)" stroke="#35598f"/>
<text x="595" y="1542" class="card-title">macOS</text>
<rect x="595" y="1568" width="410" height="178" rx="12" fill="#091327" stroke="#406aa6"/>
<text x="615" y="1620" class="mono">curl -fsSL https://fun-md.com/Fun_MD/</text>
<text x="615" y="1656" class="mono">devops-skills/raw/branch/main/install/</text>
<text x="615" y="1692" class="mono">install.sh | bash</text>
<rect x="1050" y="1480" width="470" height="300" rx="18" fill="url(#cardGrad)" stroke="#35598f"/>
<text x="1080" y="1542" class="card-title">Windows PowerShell</text>
<rect x="1080" y="1568" width="410" height="178" rx="12" fill="#091327" stroke="#406aa6"/>
<text x="1100" y="1610" class="mono">powershell -NoProfile -ExecutionPolicy</text>
<text x="1100" y="1646" class="mono">Bypass -Command "iwr -useb https://</text>
<text x="1100" y="1682" class="mono">fun-md.com/Fun_MD/devops-skills/raw/</text>
<text x="1100" y="1718" class="mono">branch/main/install/install.ps1 | iex"</text>
<text x="80" y="1878" class="section-title">核心工具</text>
<rect x="80" y="1915" width="470" height="360" rx="18" fill="url(#cardGrad)" stroke="#35598f"/>
<text x="110" y="1976" class="card-title">issue_audit.py</text>
<text x="110" y="2014" class="card-text">拉取 issue / 评论 / 图片附件,去重并评分。</text>
<rect x="110" y="2042" width="410" height="205" rx="12" fill="#091327" stroke="#406aa6"/>
<text x="130" y="2091" class="mono">python .../issue_audit.py --base-url</text>
<text x="130" y="2127" class="mono">https://fun-md.com --repo FunMD/</text>
<text x="130" y="2163" class="mono">document-collab --token &lt;TOKEN&gt;</text>
<text x="130" y="2199" class="mono">--download-attachments</text>
<rect x="565" y="1915" width="470" height="360" rx="18" fill="url(#cardGrad)" stroke="#35598f"/>
<text x="595" y="1976" class="card-title">change_scope.py</text>
<text x="595" y="2014" class="card-text">识别部署范围,决定是否重启服务端。</text>
<rect x="595" y="2042" width="410" height="205" rx="12" fill="#091327" stroke="#406aa6"/>
<text x="615" y="2091" class="mono">python .../change_scope.py</text>
<text x="615" y="2127" class="mono">--repo-path . --base-ref origin/main</text>
<text x="615" y="2163" class="mono">--head-ref HEAD</text>
<rect x="1050" y="1915" width="470" height="360" rx="18" fill="url(#cardGrad)" stroke="#35598f"/>
<text x="1080" y="1976" class="card-title">preview_slot_allocator.py</text>
<text x="1080" y="2014" class="card-text">分配 / 复用 / 释放预览槽位。</text>
<rect x="1080" y="2042" width="410" height="205" rx="12" fill="#091327" stroke="#406aa6"/>
<text x="1100" y="2091" class="mono">python .../preview_slot_allocator.py</text>
<text x="1100" y="2127" class="mono">--state-file .tmp/preview-slots.json</text>
<text x="1100" y="2163" class="mono">--slots preview-a,preview-b --evict-oldest</text>
<rect x="80" y="2330" width="1440" height="110" rx="18" fill="#0b1730" stroke="#395d96"/>
<text x="120" y="2383" class="label">官网入口: https://fun-md.com/Fun_MD/devops-skills/raw/branch/main/site/index.svg</text>
<text x="120" y="2418" class="label">仓库地址: https://fun-md.com/Fun_MD/devops-skills</text>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,10 +1,46 @@
--- ---
name: gitea-issue-devops-agent name: gitea-issue-devops-agent
description: End-to-end Gitea issue delivery workflow with guided onboarding, branch-scoped preview environments, and resource-aware deployment decisions. Use when tasks involve connecting to Gitea, processing text/image issues, fixing code on issue-specified branches, allocating/reusing test environments per branch, running test submission loops, coordinating review approvals, and closing issues only after verified delivery and engineer-confirmed merge. description: Use when coordinating AI-assisted Gitea issue delivery where work must stay traceable across issue selection, branch or PR flow, CI/CD, preview environments, and human merge approval.
--- ---
# Gitea Issue DevOps Agent # Gitea Issue DevOps Agent
## Overview
Treat AI as a fast but unstable engineer. The public workflow must remain `issue -> branch -> PR -> CI/CD -> human-confirmed merge`, while the internal execution model must narrow context, persist plans, require verification evidence, and keep humans in control of final merge decisions.
This skill is platform-aware for Gitea, but its delivery contract must stay portable across basic DevOps primitives: `git`, `issue`, `PR`, and `CI/CD`.
## Core Principles
- Start coding only from a selected issue, not from an unbounded issue queue, unless the user explicitly asks for triage-only queue scanning.
- Every delivery issue must have a persisted plan before any code changes.
- Treat workflow spec, compiled lock artifact, and evidence artifacts as executable system-of-record once the runtime is available; do not rely on prose-only guidance for enforcement.
- External collaboration stays Git-native: issue, branch, PR, pipeline, review app, merge.
- AI output is provisional until tests, smoke paths, and review evidence exist.
- Engineers stay in the loop from the initial PR onward and own white-box review.
- Keep one active issue per branch and per execution agent unless the user explicitly approves batching.
## Runtime Contract (Required)
When the repository contains the runtime package, use it instead of ad-hoc execution:
1. Author or update the workflow spec in `workflows/*.md`.
2. Compile before execution:
- `python -m engine.devops_agent.cli compile workflows/gitea-issue-delivery.md --output workflows/gitea-issue-delivery.lock.json`
3. Validate before execution:
- `python -m engine.devops_agent.cli validate workflows/gitea-issue-delivery.md`
4. Run or accept only through declared safe outputs.
5. Persist run evidence under `.tmp/...` and treat `run-artifact.json` as execution proof.
6. For real Gitea verification, use:
- `python -m engine.devops_agent.cli acceptance workflows/gitea-issue-delivery.md --base-url <url> --repo <owner/repo> --token <token> --issue-number <n> --output-dir .tmp/acceptance/gitea`
Hard rules:
- No undeclared write actions.
- No execution after failed validation.
- No claiming completion without runtime evidence.
## Mandatory Guided Start ## Mandatory Guided Start
Run this interaction before any coding or issue action: Run this interaction before any coding or issue action:
@@ -17,7 +53,16 @@ Run this interaction before any coding or issue action:
- `automatic` - `automatic`
- `semi-automatic` - `semi-automatic`
- `manual` (non-automatic) - `manual` (non-automatic)
4. Ask optional defaults: 4. Ask for the selected issue trigger source:
- explicit issue number
- issue comment / webhook trigger
- app/CLI selection
- triage-only queue scan without coding
5. Ask optional defaults:
- target base branch (for example `main`, `develop`, protected release branch)
- branch naming convention for issue branches
- plan storage path (default `.tmp/devops-plans/<repo>__issue-<number>.md`)
- issue template policy (`required` or `recommended`) for `bug`, `enhancement`, and `feature`
- designated reviewers (for semi-automatic mode) - designated reviewers (for semi-automatic mode)
- branch test submission entrypoint (CI command/job) - branch test submission entrypoint (CI command/job)
- environment policy: - environment policy:
@@ -25,21 +70,102 @@ Run this interaction before any coding or issue action:
- optional shared QA URL - optional shared QA URL
- preview slot pool (for issue branches), e.g. `preview-a,preview-b` - preview slot pool (for issue branches), e.g. `preview-a,preview-b`
- preview URL template, e.g. `https://{slot}.qa.example.com` - preview URL template, e.g. `https://{slot}.qa.example.com`
- public routing mode: `port-based` or `virtual-host`
- websocket public entry: explicit WS URL (`wss://...`) or same-origin path (`/ws`)
- deployment environment + health endpoint - deployment environment + health endpoint
- minimum issue quality score (default `70`) - minimum issue quality score (default `70`)
5. Validate connectivity by running: - `jj` policy:
- `disabled`
- `optional-internal`
- `required-internal`
6. Validate connectivity by running:
- `python scripts/issue_audit.py --repo <owner/repo> --base-url <gitea_url> --token <token> --state all --download-attachments --output-dir .tmp/issue-audit` - `python scripts/issue_audit.py --repo <owner/repo> --base-url <gitea_url> --token <token> --state all --download-attachments --output-dir .tmp/issue-audit`
6. Initialize preview-slot state (if branch previews enabled): 7. Initialize preview-slot state (if branch previews enabled):
- `python scripts/preview_slot_allocator.py --state-file .tmp/preview-slots.json --slots <slot_csv> --list` - `python scripts/preview_slot_allocator.py --state-file .tmp/preview-slots.json --slots <slot_csv> --list`
7. Echo back the selected mode and all gate rules, then start work. 8. Echo back the selected mode, selected issue trigger, target base branch, plan path, `jj` policy, and all gate rules, then start work.
If repository or token is missing/invalid, stop and request correction. Never start development without a successful connectivity check. If repository or token is missing/invalid, stop and request correction. Never start development without a successful connectivity check.
## Issue Intake Contract (Required)
- Require issue templates for `bug`, `enhancement`, and `feature`. See `references/issue-template-standard.md`.
- A coding run must start from one fixed issue chosen by a human or an explicit trigger. Queue polling is for triage only unless the user explicitly enables unattended processing.
- If the selected issue is too broad for one reviewable PR, split it into sub-issues before coding.
- If issue quality is below threshold, request details and stop before branch creation.
- For image/UI issues, attachment intake is mandatory before implementation.
## Plan-First Orchestration (Required)
Before any code change, the MajorAgent must create and persist a plan for the selected issue. Use `references/plan-template.md`.
Minimum plan fields:
- issue number and title
- trigger source and current issue status
- target base branch and working branch
- current problem description
- expected behavior and acceptance criteria
- related interfaces, services, and directories
- allowed file/path scope for edits
- verification steps, including issue-level e2e coverage
- assigned execution agent
- risk notes and blockers
- evidence links or placeholders for commit, PR, pipeline, and preview URL
Required status flow:
- `selected`
- `planned`
- `in_progress`
- `pending_test`
- `pending_review`
- `merged`
- `closed`
- `needs_info`
- `blocked`
- `rejected`
Hard rules:
- No code changes before the plan exists.
- No status promotion without evidence.
- If scope changes materially, update the plan before continuing.
## Agent Role Separation (Required)
### MajorAgent
- Pull issues, comments, and attachments from Gitea.
- Perform semantic triage and create the plan.
- Select or create the issue branch and initialize the PR.
- Decide whether work can proceed automatically or must pause for human input.
### SubAgent (Developer)
- Load only the plan and the minimal code context needed for the issue.
- Modify code only within the allowed path scope unless explicit approval expands scope.
- Keep patches small and reversible.
- Update the plan to `pending_test` with a concise implementation summary.
### TestAgent
- Load the plan, diff, and verification steps.
- Validate build, targeted tests, issue-level e2e path, and smoke path.
- Update the plan to `pending_review` only when evidence is complete.
- Reopen the plan to `in_progress` if failures or regressions appear.
### Human Reviewer / Maintainer
- Review the draft PR and the plan evidence.
- Use the AI coding tool for follow-up adjustments if needed.
- Approve or reject final merge.
## Mode Definitions ## Mode Definitions
### 1) Automatic Mode ### 1) Automatic Mode
- Read issue-specified branch and work on that branch. - Start only after a fixed issue has been selected and a plan has been created.
- Read or create the issue branch and open/update the draft PR automatically.
- Implement fix, run checks, push branch, allocate/reuse branch preview env, and trigger branch test submission automatically. - Implement fix, run checks, push branch, allocate/reuse branch preview env, and trigger branch test submission automatically.
- Monitor test results and issue feedback, then iterate on the same branch until pass. - Monitor test results and issue feedback, then iterate on the same branch until pass.
- Close issue only after evidence is complete. - Close issue only after evidence is complete.
@@ -47,7 +173,8 @@ If repository or token is missing/invalid, stop and request correction. Never st
### 2) Semi-Automatic Mode ### 2) Semi-Automatic Mode
- Read issue-specified branch and work on that branch. - Start only after a fixed issue has been selected and a plan has been created.
- Read or create the issue branch and open/update the draft PR.
- Implement and push fix. - Implement and push fix.
- Notify designated reviewer with change summary, risk, and test plan. - Notify designated reviewer with change summary, risk, and test plan.
- Wait for explicit human review approval. - Wait for explicit human review approval.
@@ -60,7 +187,9 @@ If repository or token is missing/invalid, stop and request correction. Never st
Require explicit human confirmation before each major action: Require explicit human confirmation before each major action:
- selecting issue - selecting issue
- finalizing the plan
- confirming target branch - confirming target branch
- creating or updating the PR
- applying code changes - applying code changes
- pushing commits - pushing commits
- triggering tests/deploy - triggering tests/deploy
@@ -71,11 +200,15 @@ No autonomous transition is allowed in manual mode.
## Branch-First Rules ## Branch-First Rules
- Treat issue-declared branch as the source of truth. - Treat issue-declared branch as the source of truth when provided.
- If the issue does not declare a branch, create one from the configured protected base branch after the plan is approved.
- Preferred naming pattern: `<type>/issue-<number>-<slug>` or `issue/<number>-<slug>`.
- Accept branch hints from issue fields/body/comments (example: `branch: feat/login-fix`). - Accept branch hints from issue fields/body/comments (example: `branch: feat/login-fix`).
- If branch is missing or ambiguous, ask user/reporter and pause that issue. - If branch is missing or ambiguous, ask user/reporter and pause that issue.
- Do not silently switch branches. - Do not silently switch branches.
- Keep one active issue per branch unless user explicitly approves batching. - Keep one active issue per branch unless user explicitly approves batching.
- Open or update a draft PR immediately after branch selection so the review surface exists from the start.
- Keep issue, branch, PR, preview env, and plan bound together for the full lifecycle.
## Environment Model (Required) ## Environment Model (Required)
@@ -93,6 +226,14 @@ Always avoid `main` and issue branches overwriting each other.
Never deploy different branches to the same fixed URL unless user explicitly approves override. Never deploy different branches to the same fixed URL unless user explicitly approves override.
### Routing Strategy (Recommended)
- Prefer `virtual-host` over raw ports for multi-branch testing:
- `main.example.com`, `preview-a.example.com`, `preview-b.example.com`
- Keep internal process ports private; expose only 80/443.
- Use same-origin WS path for frontend (`VITE_WS_URL=/ws`) and route `/ws/*` to the slot server.
- If `port-based` is used, every active env must have unique client/server ports; never reuse one public URL for two branches.
## Issue -> Branch -> Environment Binding ## Issue -> Branch -> Environment Binding
- Binding key: `<repo>#<issue>#<branch>` - Binding key: `<repo>#<issue>#<branch>`
@@ -132,9 +273,14 @@ Hard rule:
## Standard Workflow (All Modes) ## Standard Workflow (All Modes)
### 1) Intake and Prioritization ### 1) Issue Selection and Trigger
- Pull issues, comments, and attachments from Gitea API. - Start from a fixed issue chosen through comment mention, webhook, app/CLI selection, or explicit human instruction.
- Repo-wide polling may be used for triage reports, but not for autonomous coding unless the user explicitly enables queue processing.
### 2) Intake and Prioritization
- Pull the selected issue, comments, and attachments from Gitea API.
- If issue text/comments indicate image evidence but `attachments_downloaded` is `0`, stop and report image-intake failure before coding. - If issue text/comments indicate image evidence but `attachments_downloaded` is `0`, stop and report image-intake failure before coding.
- Prioritize in this order: - Prioritize in this order:
- `closed_but_unresolved` - `closed_but_unresolved`
@@ -143,65 +289,93 @@ Hard rule:
- `closed_open_reopen_candidates` - `closed_open_reopen_candidates`
- For issues with images, inspect attachments before coding. - For issues with images, inspect attachments before coding.
### 2) Deduplication and Quality Gate ### 3) Deduplication and Quality Gate
- Group issues by semantic intent, not literal wording. - Group issues by semantic intent, not literal wording.
- Keep one parent issue for implementation. - Keep one parent issue for implementation.
- Use `references/triage-standard.md` for score and comment templates. - Use `references/triage-standard.md` for score and comment templates.
- For low-quality issues, request details and mark as `needs-info`. - For low-quality issues, request details and mark as `needs-info`.
### 3) Fix Execution ### 4) Plan Generation
- MajorAgent must generate a plan before branch creation or code changes.
- Persist the plan to the configured plan path and echo the summary back to the user.
- Record the allowed file/path scope so later diffs can be checked against it.
- Record the issue-level e2e scenario from the reporter's perspective.
### 5) Branch and PR Initialization
- Create or reuse the issue branch from the configured base branch.
- Create or update the associated draft PR targeting the protected integration branch.
- The PR body must include issue link, plan summary, intended file scope, and verification checklist.
### 6) Fix Execution
- Prefer small, reversible patches. - Prefer small, reversible patches.
- Link every code change to issue ID in commit or PR/MR notes. - Link every code change to issue ID in commit or PR/MR notes.
- Split cross-cutting work into incremental commits. - Split cross-cutting work into incremental commits.
- Do not modify files outside the allowed plan scope without explicit approval and a plan update.
- Any new dependency, framework, SDK, or major library change must be checked against official docs, maintenance status, and local compatibility before merge.
- Do not trust knowledge-base output as truth unless it has clear provenance and matches the current codebase.
### 4) Verification Gate ### 7) Verification Gate
- Required: - Required:
- build/compile passes - build/compile passes
- affected unit/integration tests pass - affected unit/integration tests pass
- issue-level e2e test is added or updated and passes in the PR pipeline unless the user explicitly waives it
- smoke path for reported scenario passes - smoke path for reported scenario passes
- For UI/image issues: - For UI/image issues:
- compare before/after screenshots - compare before/after screenshots
- verify in at least one Chromium browser - verify in at least one Chromium browser
- If verification evidence is missing, do not claim the issue is fixed.
### 5) Branch Test Submission ("提测") ### 8) Branch Test Submission ("提测")
- Submit testing on the issue branch (CI pipeline + branch preview env). - Submit testing on the issue branch (CI pipeline + branch preview env).
- Allocate/reuse branch slot before submission. - Allocate/reuse branch slot before submission.
- Apply resource-aware deployment decision from change scope. - Apply resource-aware deployment decision from change scope.
- Verify websocket handshake is healthy on the published preview URL/path before asking QA to test.
- Post evidence in issue comment: - Post evidence in issue comment:
- commit SHA - commit SHA
- PR URL
- test run URL and result - test run URL and result
- environment/slot/URL - environment/slot/URL
- deployment scope (`skip`/`client_only`/`server_only`/`full_stack`/`infra_only`) - deployment scope (`skip`/`client_only`/`server_only`/`full_stack`/`infra_only`)
- shared backend reused or dedicated backend started - shared backend reused or dedicated backend started
- e2e result
- verification steps - verification steps
- If fail/reject, iterate on same branch and re-submit. - If fail/reject, iterate on same branch and re-submit.
### 6) Loop Control ### 9) Loop Control
- Continue `fix -> test submission -> feedback -> fix` until done. - Continue `fix -> test submission -> feedback -> fix` until done.
- Reopen immediately if verification fails or regression appears. - Reopen immediately if verification fails or regression appears.
- Do not close based on title-only or assumption-only validation. - Do not close based on title-only or assumption-only validation.
### 7) Closure Rule ### 10) Human Review and AI-Assisted Refinement
- From the initial PR onward, engineering review is the default path.
- Engineers may continue refinement inside an AI coding tool, but all follow-up work must stay on the same issue branch and update the same plan.
- Human review is the white-box gate between initial AI output and final merge readiness.
### 11) Closure Rule
Close issue only when all are true: Close issue only when all are true:
- root cause identified - root cause identified
- fix verified with reproducible evidence - fix verified with reproducible evidence
- test submission passed - test submission passed
- PR review state is recorded
- closure comment includes commit/test/deploy evidence - closure comment includes commit/test/deploy evidence
### 8) Merge Rule (Always Human-Confirmed) ### 12) Merge Rule (Always Human-Confirmed)
- Final merge must be approved by an engineer in all modes. - Final merge must be approved by an engineer in all modes.
- Agent can prepare merge notes/checklist, but must wait for explicit merge confirmation. - Agent can prepare merge notes/checklist, but must wait for explicit merge confirmation.
- Merge only after confirmation, then post final release evidence. - Merge only after confirmation, then post final release evidence.
### 9) Environment Cleanup ### 13) Environment Cleanup
- On issue close/merge: - On issue close/merge:
- release preview slot - release preview slot
@@ -210,8 +384,53 @@ Close issue only when all are true:
- On TTL expiry: - On TTL expiry:
- reclaim idle slot automatically (automatic mode) or after confirmation (semi/manual) - reclaim idle slot automatically (automatic mode) or after confirmation (semi/manual)
## AI Reliability Guardrails
- Evidence before assertions: never claim “fixed”, “tested”, or “deployed” without command output, screenshots, or links proving it.
- Diff-scope gate: compare the actual diff with the plan's allowed paths before PR update or merge request.
- Dependency gate: do not add new packages or major version upgrades without checking official documentation, maintenance state, and environment compatibility.
- Deprecated-tech gate: do not introduce deprecated or no-longer-recommended libraries/classes unless the user explicitly approves the trade-off.
- Context minimization: every execution agent should load the issue, plan, diff, and only the code areas it needs.
- No silent batching: do not mix unrelated fixes into one issue branch or PR.
## Knowledge Context Policy
- Do not blindly vectorize or load the entire repository as context for each issue.
- Build a curated issue context pack consisting of:
- issue body and comments
- plan summary
- touched directories and interfaces
- related tests
- relevant configuration and prior PR/commit references
- Record the chosen context sources in the plan so later agents can reuse them.
- Treat knowledge-base retrieval as a hint layer, not as a source of truth.
## Jujutsu (`jj`) Integration Strategy (Optional but Recommended Internally)
`jj` should improve internal execution reliability, not replace the external Git workflow.
Use `jj` only under these rules:
- Keep Git branches, PRs, and CI/CD as the public system of record.
- Map the issue branch to a `jj` bookmark when `jj` is enabled.
- Use `jj` change IDs to support iterative rewrites without losing traceability.
- Use `jj` workspaces for parallel SubAgent/TestAgent/human adjustments on the same issue without sharing one mutable working copy.
- Use the `jj` operation log for undo, recovery, and audit when AI changes diverge or go out of scope.
- Prefer `jj` for local mutating history operations if enabled; avoid mixing arbitrary mutating `git` commands with `jj` in the same workspace.
- If colocated workspaces are used, keep `git` mostly read-only except for explicit remote operations such as fetch/push handled by the workflow.
- Do not require `jj` in CI or for non-engineer participants. `jj` is an internal accelerator, not the product front door.
## Script Usage ## Script Usage
### Runtime CLI
- `python -m engine.devops_agent.cli compile workflows/gitea-issue-delivery.md --output workflows/gitea-issue-delivery.lock.json`
- `python -m engine.devops_agent.cli validate workflows/gitea-issue-delivery.md`
- `python -m engine.devops_agent.cli run workflows/gitea-issue-delivery.md --event-payload <payload.json> --output-dir .tmp/runtime-run --base-url <url> --token <token>`
- `python -m engine.devops_agent.cli acceptance workflows/gitea-issue-delivery.md --base-url <url> --repo <owner/repo> --token <token> --issue-number <n> --output-dir .tmp/acceptance/gitea`
- runtime writes `run-artifact.json` and should be used as the evidence source for status promotion and issue comments.
- `scripts/issue_audit.py`: collect issues/comments/attachments, detect duplicates, score quality, detect unresolved/closed-open links, extract issue branch hints, and generate reports. - `scripts/issue_audit.py`: collect issues/comments/attachments, detect duplicates, score quality, detect unresolved/closed-open links, extract issue branch hints, and generate reports.
- image intake uses three sources: markdown/html links, payload `assets/attachments` fields, and `/issues/*/assets` API endpoints. - image intake uses three sources: markdown/html links, payload `assets/attachments` fields, and `/issues/*/assets` API endpoints.
- if your Gitea blocks the assets endpoints, pass `--skip-asset-endpoints` and rely on payload extraction. - if your Gitea blocks the assets endpoints, pass `--skip-asset-endpoints` and rely on payload extraction.
@@ -223,12 +442,22 @@ Close issue only when all are true:
- `scripts/change_scope.py`: detect changed scope and recommend minimum deploy strategy. - `scripts/change_scope.py`: detect changed scope and recommend minimum deploy strategy.
- `python scripts/change_scope.py --repo-path <repo> --base-ref origin/main --head-ref HEAD` - `python scripts/change_scope.py --repo-path <repo> --base-ref origin/main --head-ref HEAD`
- `references/triage-standard.md`: scoring rubric and templates for needs-info, review request, test submission, and merge approval. - `references/triage-standard.md`: scoring rubric and templates for needs-info, review request, test submission, and merge approval.
- `references/issue-template-standard.md`: standard issue templates for `bug`, `enhancement`, and `feature`.
- `references/plan-template.md`: default plan structure and status machine for MajorAgent/SubAgent/TestAgent handoff.
- `references/jj-default-usage.md`: default `jj` installation strategy, verification, and scenario-based usage guidance.
## Operational Constraints ## Operational Constraints
- Never start coding from an unselected issue unless the user explicitly enables autonomous queue mode.
- Never skip plan creation for a delivery issue.
- Never modify files outside the planned scope without an explicit plan update or user approval.
- Never claim success without verification evidence.
- Never introduce new dependencies or deprecated libraries without compatibility review.
- Never bulk-close issues without per-issue verification evidence. - Never bulk-close issues without per-issue verification evidence.
- Never ignore attachment images for UI/interaction issues. - Never ignore attachment images for UI/interaction issues.
- Never merge feature requests and bugfixes into one untraceable commit. - Never merge feature requests and bugfixes into one untraceable commit.
- Never bypass engineer merge confirmation. - Never bypass engineer merge confirmation.
- Never allow branch previews to overwrite main stable env. - Never allow branch previews to overwrite main stable env.
- Never start dedicated branch server when scope indicates client-only changes. - Never start dedicated branch server when scope indicates client-only changes.
- When changing public service ports under PM2, do not rely on `pm2 restart --update-env` alone; delete and recreate the process so CLI args (for example `--port`) actually change.
- If a branch must be rebound to a specific preview slot (for example `preview-a`), release the existing issue allocation first, then redeploy; reuse logic otherwise keeps the previous slot by design.

View File

@@ -1,4 +1,4 @@
interface: interface:
display_name: "Gitea Issue DevOps Agent" display_name: "Gitea Issue DevOps Agent"
short_description: "Guided Gitea issue delivery with execution modes, branch preview slots, and resource-aware deployments" short_description: "Plan-first Gitea issue delivery with fixed-issue triggers, draft PRs, preview slots, evidence gates, and optional jj-backed execution"
default_prompt: "Start with guided setup (repo URL, API key, mode, env policy), process issues on issue-specified branches, bind each branch to preview slots, decide deploy scope from git diff (skip/client-only/server-only/full-stack), avoid dedicated server restarts when backend is unchanged, run fix/test loops, and require engineer confirmation before final merge." default_prompt: "Start with guided setup (repo URL, API key, mode, selected issue trigger, target base branch, plan path, env policy, jj policy). Require a persisted plan before code changes, create or reuse the issue branch and draft PR, restrict edits to plan-scoped paths, decide deploy scope from git diff (skip/client-only/server-only/full-stack), add or update issue-level e2e coverage, bind preview slots, avoid dedicated server restarts when backend is unchanged, keep engineers in the loop from the initial PR onward, and require evidence plus explicit engineer confirmation before final merge."

View File

@@ -0,0 +1,143 @@
# Issue Template Standard
Use these templates to create AI-ready issues. The goal is to give both humans and agents enough context to produce a reviewable first PR instead of an ambiguous code dump.
## Global Rules
- Every issue should map to one reviewable outcome.
- Prefer explicit issue selection to start delivery:
- issue comment mention or webhook trigger
- app/UI selection
- CLI selection
- Queue scans may suggest issues, but should not start coding until one issue is explicitly selected.
- Always include acceptance criteria and affected area hints.
- If screenshots, recordings, or logs exist, attach them in the issue instead of describing them vaguely.
## Common Required Fields
- summary
- issue type: `bug`, `enhancement`, or `feature`
- current behavior
- expected behavior
- affected user or business impact
- related pages/modules/interfaces
- acceptance criteria
- attachments or logs
## Bug Template
```text
[bug-template-v1]
Summary:
Environment:
- app version:
- browser/device/os:
- time window:
Current behavior:
Expected behavior:
Reproduction steps:
1.
2.
3.
Affected area:
- pages/routes:
- services/apis:
- files or modules if known:
Impact:
Evidence:
- screenshot/video/log links:
Acceptance criteria:
- [ ]
- [ ]
Non-goals:
```
## Enhancement Template
```text
[enhancement-template-v1]
Summary:
Background:
Current limitation:
Desired improvement:
Affected area:
- pages/routes:
- services/apis:
- files or modules if known:
User value:
Constraints:
- compatibility:
- performance:
- rollout/risk:
Acceptance criteria:
- [ ]
- [ ]
Evidence or references:
```
## Feature Template
```text
[feature-template-v1]
Summary:
Business goal:
User story:
As a <role>, I want <capability>, so that <outcome>.
Scope:
- in scope:
- out of scope:
Expected behavior:
Affected area:
- pages/routes:
- services/apis:
- files or modules if known:
Dependencies or prerequisites:
Acceptance criteria:
- [ ]
- [ ]
- [ ]
Validation path:
- happy path:
- edge cases:
Evidence, mockups, or references:
```
## Selection Trigger Examples
```text
@funmd-bot take issue #123
```
```text
CLI: devops-agent run --issue 123 --mode semi-automatic
```
```text
UI: Select issue #123 -> Generate plan -> Create branch and draft PR
```

View File

@@ -0,0 +1,170 @@
# Jj Default Installation and Usage
This repository installs the `gitea-issue-devops-agent` skill first, then attempts to install `jj` by default.
`jj` is the internal execution layer for agent reliability. Git branches, PRs, CI/CD pipelines, and merge approvals remain the external system of record.
## Default Install Strategy
The one-command installers now behave like this:
1. Install the skill into the target Codex directory.
2. Check whether `jj` is already available.
3. If not, try OS-specific install methods.
4. If all methods fail, keep the skill installed and print manual fallback instructions.
## OS-Specific Attempt Order
### Linux
- `brew install jj`
- `cargo binstall --strategies crate-meta-data jj-cli`
- `cargo install --locked --bin jj jj-cli`
For prerelease:
- `cargo install --git https://github.com/jj-vcs/jj.git --locked --bin jj jj-cli`
### macOS
- `brew install jj`
- `cargo binstall --strategies crate-meta-data jj-cli`
- `cargo install --locked --bin jj jj-cli`
For prerelease:
- `cargo install --git https://github.com/jj-vcs/jj.git --locked --bin jj jj-cli`
### Windows
- `winget install jj-vcs.jj`
- `scoop install main/jj`
- `cargo install --locked --bin jj jj-cli`
For prerelease:
- `cargo install --git https://github.com/jj-vcs/jj.git --locked --bin jj jj-cli`
## Installer Controls
### Bash installers
- `INSTALL_JJ=0`: skip `jj` installation
- `JJ_INSTALL_METHOD=auto|brew|binstall|cargo`
- `JJ_CHANNEL=release|prerelease`
Example:
```bash
INSTALL_JJ=0 curl -fsSL https://fun-md.com/Fun_MD/devops-skills/raw/branch/main/install/install.sh | bash
```
```bash
JJ_INSTALL_METHOD=cargo JJ_CHANNEL=prerelease curl -fsSL https://fun-md.com/Fun_MD/devops-skills/raw/branch/main/install/install.sh | bash
```
### PowerShell installers
With the one-liner, prefer environment variables:
- `$env:INSTALL_JJ='0'`
- `$env:JJ_INSTALL_METHOD='auto'|'winget'|'scoop'|'cargo'`
- `$env:JJ_CHANNEL='release'|'prerelease'`
Example:
```powershell
$env:JJ_INSTALL_METHOD='winget'
iwr -useb https://fun-md.com/Fun_MD/devops-skills/raw/branch/main/install/install.ps1 | iex
```
```powershell
$env:INSTALL_JJ='0'
iwr -useb https://fun-md.com/Fun_MD/devops-skills/raw/branch/main/install/install.ps1 | iex
```
If you save the script locally first, you can also use the `-SkipJj`, `-JjInstallMethod`, and `-JjChannel` parameters directly.
## Verification
After installation:
```bash
jj --version
```
Set identity:
```bash
jj config set --user user.name "Your Name"
jj config set --user user.email "you@example.com"
```
Optional shell completion:
```bash
source <(jj util completion bash)
```
PowerShell completion:
```powershell
jj util completion power-shell | Out-String | Invoke-Expression
```
## Working Model
Use `jj` as an internal operator tool:
- issue selection, branch naming, PR creation, CI, and merge stay Git/Gitea-native
- `jj` handles local history rewrites, workspace isolation, and recovery
- each issue branch can map to one `jj` bookmark
## Scenario Examples
### 1) First-Time Team Setup
1. Run the one-command installer.
2. Verify `jj --version`.
3. Configure `user.name` and `user.email`.
4. Start with one fixed issue in `manual` or `semi-automatic` mode.
Recommended when a team is new to AI-assisted delivery and wants controlled adoption.
### 2) Initial AI PR for a Bug
1. Human selects issue `#48`.
2. MajorAgent creates the plan.
3. Issue branch and draft PR are created.
4. SubAgent changes only the planned paths.
5. TestAgent validates build, targeted tests, and issue e2e.
6. Engineer reviews and refines before merge approval.
Recommended default flow for day-to-day bug fixing.
### 3) Semi-Automatic Review Flow
1. AI produces the initial draft PR.
2. Reviewer inspects the plan, diff scope, and evidence.
3. Engineer uses the AI coding tool for follow-up edits if needed.
4. Only after review approval does the branch enter preview-slot testing.
Recommended when engineering review must happen before environment allocation.
### 4) Human + TestAgent Parallel Verification with Workspaces
1. SubAgent works in the main issue workspace.
2. TestAgent creates a separate `jj workspace` for validation.
3. Human reviewer can create another workspace for white-box adjustments.
4. All three flows remain tied to the same issue branch and plan.
Recommended for larger issues where testing and code refinement happen in parallel.
### 5) Recovery After AI Drift
1. AI rewrites the change incorrectly or edits too broadly.
2. Engineer inspects `jj op log`.
3. Engineer uses `jj undo`, `jj op revert`, or `jj op restore`.
4. The issue branch and PR remain intact while local execution history is repaired.
Recommended when AI behavior is fast but unreliable and quick recovery matters.

View File

@@ -0,0 +1,98 @@
# Plan Template
Every delivery issue must have a persisted plan before code changes. Store it in a stable path such as:
```text
.tmp/devops-plans/<repo>__issue-<number>.md
```
## Plan Template
```text
[issue-plan-v1]
Issue:
- repo:
- number:
- title:
- type:
- trigger source:
Status:
- current: selected
- owner agent:
- reviewer:
Branching:
- target base branch:
- working branch:
- PR:
Problem:
Expected behavior:
Acceptance criteria:
- [ ]
- [ ]
Related scope:
- interfaces/apis:
- directories/files:
- tests:
Allowed path scope:
- path/glob 1
- path/glob 2
Curated context pack:
- issue body/comments
- attachments
- code references
- prior PRs/commits/docs
Implementation notes:
Verification plan:
- build/compile:
- unit/integration:
- e2e:
- smoke path:
- screenshot/browser checks:
Evidence:
- commit:
- pipeline:
- preview url:
- screenshots:
Risk and blockers:
Handoff notes:
```
## Required Status Flow
- `selected`
- `planned`
- `in_progress`
- `pending_test`
- `pending_review`
- `merged`
- `closed`
- `needs_info`
- `blocked`
- `rejected`
## Ownership Rules
- MajorAgent creates and updates the plan before coding starts.
- SubAgent reads only the plan plus minimal code context and updates implementation notes.
- TestAgent updates verification results and may move the status back to `in_progress`.
- Human reviewer confirms `pending_review` to merge readiness.
## Scope Rules
- If the diff touches files outside the allowed path scope, stop and either:
- update the plan with justification, or
- request human approval.
- If a new dependency or deprecated technology is introduced, record the compatibility decision in `Risk and blockers`.

View File

@@ -53,10 +53,29 @@ Use this at session start before any implementation:
- automatic自动修复+提测循环,最终合并仍需工程师确认 - automatic自动修复+提测循环,最终合并仍需工程师确认
- semi-automatic修复后先人工 review再提测循环 - semi-automatic修复后先人工 review再提测循环
- manual全流程人工确认 - manual全流程人工确认
可选:指定 reviewer、提测命令、部署环境、健康检查地址。 4) 固定 issue 触发来源issue 编号 / 评论触发 / app/CLI 选择 / 仅扫描不编码)
可选目标基线分支、分支命名规则、Plan 存储路径、指定 reviewer、提测命令、部署环境、健康检查地址、jj 策略。
可选(推荐):主环境 URL、共享 QA URL、预览槽位池如 preview-a/preview-b和预览 URL 模板。 可选(推荐):主环境 URL、共享 QA URL、预览槽位池如 preview-a/preview-b和预览 URL 模板。
``` ```
## Plan Summary Template
Use this comment or PR note after the MajorAgent has created the plan:
```text
[issue-plan-summary-v1]
已生成执行计划:
- issue: #<number>
- status: <selected|planned|...>
- target base: <branch>
- working branch: <branch>
- allowed scope: <paths>
- expected behavior: <summary>
- verification: <unit/integration/e2e/smoke>
- assigned agent: <major|sub|test>
未完成前不会进入代码修改。
```
## Review Request Template (Semi-Automatic) ## Review Request Template (Semi-Automatic)
```text ```text
@@ -65,6 +84,7 @@ Use this at session start before any implementation:
- issue: #<number> - issue: #<number>
- branch: <branch> - branch: <branch>
- commit: <sha> - commit: <sha>
- pr: <url>
- change summary: <summary> - change summary: <summary>
- risk: <risk notes> - risk: <risk notes>
- test plan: <plan> - test plan: <plan>
@@ -79,12 +99,14 @@ Use this at session start before any implementation:
- issue: #<number> - issue: #<number>
- branch: <branch> - branch: <branch>
- commit: <sha> - commit: <sha>
- pr: <url>
- pipeline/test run: <url> - pipeline/test run: <url>
- environment: <env/version> - environment: <env/version>
- preview slot: <slot> - preview slot: <slot>
- preview url: <url> - preview url: <url>
- deploy scope: <skip|client_only|server_only|full_stack|infra_only> - deploy scope: <skip|client_only|server_only|full_stack|infra_only>
- server strategy: <reused-shared|dedicated-branch-server> - server strategy: <reused-shared|dedicated-branch-server>
- e2e: <passed|failed|waived-with-approval>
- verify steps: <steps> - verify steps: <steps>
如失败或结果不符合预期,将继续同分支迭代修复。 如失败或结果不符合预期,将继续同分支迭代修复。
``` ```
@@ -121,6 +143,7 @@ Use this at session start before any implementation:
- issue: #<number> - issue: #<number>
- branch: <branch> - branch: <branch>
- target: <target branch> - target: <target branch>
- pr: <url>
- review status: <approved/pending> - review status: <approved/pending>
- test status: <passed/failed> - test status: <passed/failed>
- release evidence: <links> - release evidence: <links>
@@ -133,6 +156,7 @@ Use this at session start before any implementation:
[issue-verified-close-v1] [issue-verified-close-v1]
已修复并发布。 已修复并发布。
- commit: <sha> - commit: <sha>
- pr: <url>
- tests: <summary> - tests: <summary>
- deploy: <pipeline/run url> - deploy: <pipeline/run url>
- verify: <how verified> - verify: <how verified>

View File

@@ -0,0 +1,47 @@
from __future__ import annotations
import os
from pathlib import Path
import pytest
from engine.devops_agent.cli import main
REQUIRED_ENV_VARS = (
"GITEA_BASE_URL",
"GITEA_REPO",
"GITEA_TOKEN",
"GITEA_ISSUE_NUMBER",
)
def test_gitea_acceptance_comment_flow() -> None:
missing = [name for name in REQUIRED_ENV_VARS if not os.getenv(name)]
if missing:
pytest.skip(f"missing required env vars: {', '.join(missing)}")
output_dir = Path(".tmp/acceptance/gitea")
output_dir.mkdir(parents=True, exist_ok=True)
exit_code = main(
[
"acceptance",
"workflows/gitea-issue-delivery.md",
"--base-url",
os.environ["GITEA_BASE_URL"],
"--repo",
os.environ["GITEA_REPO"],
"--token",
os.environ["GITEA_TOKEN"],
"--issue-number",
os.environ["GITEA_ISSUE_NUMBER"],
"--output-dir",
str(output_dir),
]
)
artifact_path = output_dir / "run-artifact.json"
assert exit_code == 0
assert artifact_path.exists()

12
tests/fixtures/gitea/comment_event.json vendored Normal file
View File

@@ -0,0 +1,12 @@
{
"action": "created",
"comment": {
"body": "@devops-agent please process issue 48"
},
"issue": {
"number": 48
},
"repository": {
"full_name": "Fun_MD/devops-skills"
}
}

6
tests/fixtures/gitea/issue.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"number": 48,
"title": "Fix issue delivery flow",
"body": "The agent should post evidence back to the issue.",
"state": "open"
}

View File

@@ -0,0 +1,4 @@
---
name: invalid-workflow
---
# Invalid

View File

@@ -0,0 +1,17 @@
---
name: invalid-path-scope
provider: gitea
on:
issue_comment:
commands: ["@devops-agent"]
permissions:
issues: write
safe_outputs:
add_comment:
max: 1
policy:
path_scope: "engine/devops_agent/"
---
# Invalid
The path scope must be a list of path prefixes.

View File

@@ -0,0 +1,12 @@
---
name: invalid-write-without-safe-output
provider: gitea
on:
issue_comment:
commands: ["@devops-agent"]
permissions:
issues: write
---
# Invalid
Write permission is declared, but no safe output exists.

17
tests/fixtures/specs/valid_workflow.md vendored Normal file
View File

@@ -0,0 +1,17 @@
---
name: issue-delivery
provider: gitea
on:
issue_comment:
commands: ["@devops-agent"]
permissions:
issues: write
safe_outputs:
add_comment:
max: 2
plan:
required: true
---
# Issue Delivery
Read the selected issue and post evidence.

View File

@@ -0,0 +1,62 @@
from __future__ import annotations
import json
from pathlib import Path
from engine.devops_agent.compiler import compile_workflow
from engine.devops_agent.runtime import run_issue_comment_workflow
from engine.devops_agent.spec import load_workflow_spec
from engine.devops_agent.validator import validate_workflow_spec
class FakeProvider:
def __init__(self) -> None:
self.comments: list[dict[str, object]] = []
def parse_issue_comment_event(self, payload: dict[str, object]) -> dict[str, object]:
repository = payload["repository"]
issue = payload["issue"]
comment = payload["comment"]
return {
"repo": repository["full_name"],
"issue_number": issue["number"],
"comment_body": comment["body"],
}
def get_issue(self, repo: str, issue_number: int) -> dict[str, object]:
return {
"number": issue_number,
"title": "Fix issue delivery flow",
"body": "The agent should post evidence back to the issue.",
"state": "open",
"repo": repo,
}
def post_issue_comment(self, repo: str, issue_number: int, body: str) -> dict[str, object]:
record = {
"repo": repo,
"issue_number": issue_number,
"body": body,
}
self.comments.append(record)
return {"id": len(self.comments), **record}
def test_runtime_executes_flow_and_writes_evidence() -> None:
spec = load_workflow_spec(Path("workflows/gitea-issue-delivery.md"))
assert validate_workflow_spec(spec) == []
lock = compile_workflow(spec)
payload = json.loads(Path("tests/fixtures/gitea/comment_event.json").read_text(encoding="utf-8"))
provider = FakeProvider()
artifact = run_issue_comment_workflow(
lock=lock,
provider=provider,
event_payload=payload,
output_dir=Path(".tmp/runtime-flow-test"),
)
assert artifact["result"] == "success"
assert artifact["plan_state"]["status"] == "pending_review"
assert provider.comments
assert Path(artifact["artifact_path"]).exists()

84
tests/unit/test_cli.py Normal file
View File

@@ -0,0 +1,84 @@
from __future__ import annotations
import json
from pathlib import Path
from engine.devops_agent import cli
class FakeProvider:
def __init__(self, *, base_url: str, token: str) -> None:
self.base_url = base_url
self.token = token
def parse_issue_comment_event(self, payload: dict[str, object]) -> dict[str, object]:
repository = payload["repository"]
issue = payload["issue"]
comment = payload["comment"]
return {
"repo": repository["full_name"],
"issue_number": issue["number"],
"comment_body": comment["body"],
}
def get_issue(self, repo: str, issue_number: int) -> dict[str, object]:
return {
"number": issue_number,
"title": "Fix issue delivery flow",
"body": "The agent should post evidence back to the issue.",
"state": "open",
"repo": repo,
}
def post_issue_comment(self, repo: str, issue_number: int, body: str) -> dict[str, object]:
return {"id": 1, "repo": repo, "issue_number": issue_number, "body": body}
def test_compile_command_writes_lock_file() -> None:
output_path = Path(".tmp/cli-tests/gitea-issue-delivery.lock.json")
output_path.parent.mkdir(parents=True, exist_ok=True)
exit_code = cli.main(
[
"compile",
"workflows/gitea-issue-delivery.md",
"--output",
str(output_path),
]
)
assert exit_code == 0
assert output_path.exists()
def test_validate_command_returns_success() -> None:
exit_code = cli.main(["validate", "workflows/gitea-issue-delivery.md"])
assert exit_code == 0
def test_run_command_writes_runtime_artifact(monkeypatch) -> None:
output_dir = Path(".tmp/cli-tests/runtime-run")
output_dir.mkdir(parents=True, exist_ok=True)
monkeypatch.setattr(cli, "GiteaProvider", FakeProvider)
exit_code = cli.main(
[
"run",
"workflows/gitea-issue-delivery.md",
"--event-payload",
"tests/fixtures/gitea/comment_event.json",
"--output-dir",
str(output_dir),
"--base-url",
"https://fun-md.com",
"--token",
"fake-token",
]
)
artifact_path = output_dir / "run-artifact.json"
artifact = json.loads(artifact_path.read_text(encoding="utf-8"))
assert exit_code == 0
assert artifact["result"] == "success"

View File

@@ -0,0 +1,26 @@
from __future__ import annotations
from pathlib import Path
from engine.devops_agent.compiler import compile_workflow
from engine.devops_agent.spec import load_workflow_spec
def test_compile_emits_normalized_lock_payload() -> None:
spec = load_workflow_spec(Path("workflows/gitea-issue-delivery.md"))
lock = compile_workflow(spec)
assert lock["version"] == 1
assert lock["workflow_name"] == "gitea-issue-delivery"
assert lock["provider"] == "gitea"
assert lock["triggers"] == [
{
"event": "issue_comment",
"commands": ["@devops-agent"],
}
]
assert lock["policy"]["path_scope"] == []
assert lock["policy"]["require_human_merge"] is True
assert lock["safe_outputs"]["add_comment"]["max"] == 3
assert "issue_comment" in lock["required_evidence"]

View File

@@ -0,0 +1,72 @@
from __future__ import annotations
import json
from pathlib import Path
from engine.devops_agent.providers.gitea import GiteaProvider
class FakeTransport:
def __init__(self) -> None:
self.calls: list[dict[str, object]] = []
def __call__(
self,
*,
method: str,
url: str,
headers: dict[str, str],
body: dict[str, object] | None,
) -> dict[str, object]:
self.calls.append(
{
"method": method,
"url": url,
"headers": headers,
"body": body,
}
)
if url.endswith("/comments"):
return {"id": 999, "body": body["body"] if body else ""}
return json.loads(Path("tests/fixtures/gitea/issue.json").read_text(encoding="utf-8"))
def test_gitea_provider_fetches_issue() -> None:
transport = FakeTransport()
provider = GiteaProvider(
base_url="https://fun-md.com",
token="test-token",
transport=transport,
)
issue = provider.get_issue("Fun_MD/devops-skills", 48)
assert issue["number"] == 48
assert transport.calls[0]["method"] == "GET"
assert str(transport.calls[0]["url"]).endswith("/api/v1/repos/Fun_MD/devops-skills/issues/48")
def test_gitea_provider_posts_issue_comment() -> None:
transport = FakeTransport()
provider = GiteaProvider(
base_url="https://fun-md.com",
token="test-token",
transport=transport,
)
response = provider.post_issue_comment("Fun_MD/devops-skills", 48, "Evidence posted")
assert response["id"] == 999
assert transport.calls[0]["method"] == "POST"
assert transport.calls[0]["body"] == {"body": "Evidence posted"}
def test_gitea_provider_parses_comment_event() -> None:
provider = GiteaProvider(base_url="https://fun-md.com", token="test-token")
payload = json.loads(Path("tests/fixtures/gitea/comment_event.json").read_text(encoding="utf-8"))
event = provider.parse_issue_comment_event(payload)
assert event["repo"] == "Fun_MD/devops-skills"
assert event["issue_number"] == 48
assert "@devops-agent" in event["comment_body"]

View File

@@ -0,0 +1,34 @@
from __future__ import annotations
import pytest
from engine.devops_agent.policies import PolicyViolation, RuntimePolicy
def test_policy_allows_declared_safe_output() -> None:
policy = RuntimePolicy(
safe_outputs={"add_comment": {"max": 2}},
path_scope=["engine/devops_agent/", "README.md"],
)
policy.assert_operation_allowed("add_comment")
def test_policy_rejects_undeclared_write_action() -> None:
policy = RuntimePolicy(
safe_outputs={"add_comment": {"max": 2}},
path_scope=[],
)
with pytest.raises(PolicyViolation, match="close_issue"):
policy.assert_operation_allowed("close_issue")
def test_policy_rejects_paths_outside_scope() -> None:
policy = RuntimePolicy(
safe_outputs={"write_file": {"max": 5}},
path_scope=["engine/devops_agent/"],
)
with pytest.raises(PolicyViolation, match="outside allowed path scope"):
policy.assert_path_allowed("skills/gitea-issue-devops-agent/SKILL.md")

View File

@@ -0,0 +1,11 @@
import importlib
def test_core_modules_are_importable() -> None:
module_names = [
"engine.devops_agent",
"engine.devops_agent.cli",
]
for module_name in module_names:
importlib.import_module(module_name)

View File

@@ -0,0 +1,29 @@
from __future__ import annotations
from pathlib import Path
import pytest
from engine.devops_agent.spec import WorkflowSpecError, load_workflow_spec
def test_load_workflow_spec_splits_frontmatter_and_body() -> None:
spec = load_workflow_spec(Path("tests/fixtures/specs/valid_workflow.md"))
assert spec.name == "issue-delivery"
assert spec.provider == "gitea"
assert spec.frontmatter["safe_outputs"]["add_comment"]["max"] == 2
assert "Read the selected issue" in spec.body
def test_load_workflow_spec_rejects_missing_provider() -> None:
with pytest.raises(WorkflowSpecError, match="provider"):
load_workflow_spec(Path("tests/fixtures/specs/invalid_missing_provider.md"))
def test_sample_workflow_spec_exists_and_loads() -> None:
spec = load_workflow_spec(Path("workflows/gitea-issue-delivery.md"))
assert spec.name == "gitea-issue-delivery"
assert spec.provider == "gitea"
assert "add_comment" in spec.frontmatter["safe_outputs"]

View File

@@ -0,0 +1,30 @@
from __future__ import annotations
from pathlib import Path
from engine.devops_agent.spec import load_workflow_spec
from engine.devops_agent.validator import validate_workflow_spec
def test_validate_accepts_the_sample_workflow() -> None:
spec = load_workflow_spec(Path("workflows/gitea-issue-delivery.md"))
errors = validate_workflow_spec(spec)
assert errors == []
def test_validate_rejects_write_permissions_without_safe_outputs() -> None:
spec = load_workflow_spec(Path("tests/fixtures/specs/no_safe_outputs_for_write.md"))
errors = validate_workflow_spec(spec)
assert any("safe_outputs" in error for error in errors)
def test_validate_rejects_invalid_path_scope() -> None:
spec = load_workflow_spec(Path("tests/fixtures/specs/invalid_path_scope.md"))
errors = validate_workflow_spec(spec)
assert any("path_scope" in error for error in errors)

View File

@@ -0,0 +1,29 @@
{
"version": 1,
"workflow_name": "gitea-issue-delivery",
"provider": "gitea",
"source": "workflows/gitea-issue-delivery.md",
"triggers": [
{
"event": "issue_comment",
"commands": [
"@devops-agent"
]
}
],
"safe_outputs": {
"add_comment": {
"max": 3
}
},
"required_evidence": [
"issue_comment",
"verification_summary"
],
"policy": {
"require_human_merge": true,
"require_fixed_issue": true,
"path_scope": []
},
"instructions": "# Gitea Issue Delivery\n\nStart from a fixed issue selected by a human or explicit trigger.\n\nCreate or update plan state before code changes, keep writes within declared safe outputs,\nand publish verification evidence back to the issue thread.\n"
}

View File

@@ -0,0 +1,34 @@
---
name: gitea-issue-delivery
provider: gitea
on:
issue_comment:
commands: ["@devops-agent"]
permissions:
issues: write
pull_requests: write
safe_outputs:
add_comment:
max: 3
plan:
required: true
statuses:
- selected
- planned
- in_progress
- pending_test
- pending_review
evidence:
required:
- issue_comment
- verification_summary
policy:
require_fixed_issue: true
require_human_merge: true
---
# Gitea Issue Delivery
Start from a fixed issue selected by a human or explicit trigger.
Create or update plan state before code changes, keep writes within declared safe outputs,
and publish verification evidence back to the issue thread.