Add .loop-build harness and IntelliJ project configuration files
This commit is contained in:
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 已忽略包含查询文件的默认文件夹
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
4
.idea/misc.xml
generated
Normal file
4
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="KubernetesApiProvider">{}</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/skills.iml" filepath="$PROJECT_DIR$/.idea/skills.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
19
.idea/php.xml
generated
Normal file
19
.idea/php.xml
generated
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="MessDetectorOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PHPCSFixerOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PHPCodeSnifferOptionsConfiguration">
|
||||||
|
<option name="highlightLevel" value="WARNING" />
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PhpStanOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PsalmOptionsConfiguration">
|
||||||
|
<option name="transferred" value="true" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/skills.iml
generated
Normal file
8
.idea/skills.iml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
189
.loop-build/HELP.md
Normal file
189
.loop-build/HELP.md
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
# loop-build 使用指南(简体中文)
|
||||||
|
|
||||||
|
本文件是 `loop-build (Codex CLI harness)` 的中文操作手册,面向日常使用者。
|
||||||
|
目标是让你按固定节奏推进任务:**先计划、再审批、单步执行、验证、再次审批**。
|
||||||
|
|
||||||
|
## 1. 这套 harness 是做什么的
|
||||||
|
|
||||||
|
`loop-build` 用于长时间运行、可中断、可回滚的任务执行。
|
||||||
|
它把一次任务拆成 3~7 个步骤,并强制执行以下状态机:
|
||||||
|
|
||||||
|
1. 生成 PLAN
|
||||||
|
2. 人类审批 PLAN(`APPROVE_PLAN / REVISE_PLAN / CANCEL_TASK`)
|
||||||
|
3. 每次只执行 1 个 step
|
||||||
|
4. step 后汇报并等待下一次审批(`APPROVE_NEXT / REVISE_PLAN / CANCEL_TASK`)
|
||||||
|
|
||||||
|
## 2. 首次使用(一次性)
|
||||||
|
|
||||||
|
在仓库根目录执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.loop-build/scripts/init.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
初始化脚本会:
|
||||||
|
- 检查依赖:`git`、`bash`、`rg`、`jq`
|
||||||
|
- 初始化 `.loop-build` 所需目录与默认配置
|
||||||
|
- 生成任务模板与状态文件(若缺失)
|
||||||
|
- 提示你尽量在 git 工作树较干净时开始
|
||||||
|
|
||||||
|
## 3. 每个任务的标准流程
|
||||||
|
|
||||||
|
### 第一步:准备任务输入
|
||||||
|
|
||||||
|
你需要准备 4 类输入给 planner:
|
||||||
|
- `goal`:任务目标
|
||||||
|
- `constraints`:约束条件
|
||||||
|
- `repo_context_hint`:仓库上下文提示
|
||||||
|
- `current_state_summary`:当前状态摘要(可选)
|
||||||
|
|
||||||
|
### 第二步:产出并写入 PLAN
|
||||||
|
|
||||||
|
用 `./.loop-build/prompts/planner.md` 让 Codex 生成严格 JSON,写入:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.loop-build/state/current_task.json
|
||||||
|
```
|
||||||
|
|
||||||
|
PLAN 必须满足:
|
||||||
|
- 步数 3~7
|
||||||
|
- 每步包含:
|
||||||
|
- `step_id`
|
||||||
|
- `action`
|
||||||
|
- `verification`
|
||||||
|
- `rollback`
|
||||||
|
- `risk_level`
|
||||||
|
- `expected_output`
|
||||||
|
|
||||||
|
### 第三步:审批 PLAN(强制)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.loop-build/scripts/run_task.sh APPROVE_PLAN
|
||||||
|
```
|
||||||
|
|
||||||
|
如果要改计划:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.loop-build/scripts/run_task.sh REVISE_PLAN "说明你要改什么"
|
||||||
|
```
|
||||||
|
|
||||||
|
取消任务:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.loop-build/scripts/run_task.sh CANCEL_TASK
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第四步:单步执行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.loop-build/scripts/run_task.sh step
|
||||||
|
```
|
||||||
|
|
||||||
|
或指定步号:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.loop-build/scripts/run_task.sh step 2
|
||||||
|
```
|
||||||
|
|
||||||
|
执行特性:
|
||||||
|
- 一次只推进一个 step
|
||||||
|
- 默认输出 unified diff,不整文件倾泻
|
||||||
|
- 按 step 的 `verification` 决定是否触发验证
|
||||||
|
- 写入历史记录到 `state/history.ndjson`
|
||||||
|
- 更新 `current_task.json` 的进度字段
|
||||||
|
|
||||||
|
### 第五步:每步后再次审批
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.loop-build/scripts/run_task.sh APPROVE_NEXT
|
||||||
|
```
|
||||||
|
|
||||||
|
或要求改计划:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.loop-build/scripts/run_task.sh REVISE_PLAN "下一步前先调整计划"
|
||||||
|
```
|
||||||
|
|
||||||
|
然后继续 `step`,直到完成。
|
||||||
|
|
||||||
|
## 4. 常用命令速查
|
||||||
|
|
||||||
|
查看当前任务状态:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.loop-build/scripts/run_task.sh status
|
||||||
|
```
|
||||||
|
|
||||||
|
执行验证(手动):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
APPROVE=1 ./.loop-build/scripts/verify.sh unit
|
||||||
|
APPROVE=1 ./.loop-build/scripts/verify.sh lint
|
||||||
|
APPROVE=1 ./.loop-build/scripts/verify.sh typecheck
|
||||||
|
APPROVE=1 ./.loop-build/scripts/verify.sh all
|
||||||
|
```
|
||||||
|
|
||||||
|
创建快照:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.loop-build/scripts/snapshot.sh
|
||||||
|
./.loop-build/scripts/snapshot.sh before-step-2
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 安全策略说明(必须了解)
|
||||||
|
|
||||||
|
策略文件在 `./.loop-build/config/`:
|
||||||
|
- `allowlist.txt`:默认允许的只读命令
|
||||||
|
- `denylist.txt`:默认禁止的高风险命令
|
||||||
|
- `policy.env`:显式解锁开关
|
||||||
|
|
||||||
|
关键点:
|
||||||
|
- 命令会按分段检查(如 `&&`、`||`、`;`、`|`)
|
||||||
|
- 命中 denylist 默认拦截
|
||||||
|
- 需在 `policy.env` 显式解锁并启用二次确认,才允许执行高风险动作
|
||||||
|
|
||||||
|
## 6. 实战建议(推荐)
|
||||||
|
|
||||||
|
- 每个 step 只做一个最小可验证改动(MVC)。
|
||||||
|
- 每个 step 最多改 2 个文件,超过就拆分计划。
|
||||||
|
- `action` 尽量写成可直接执行的最小命令列表。
|
||||||
|
- `verification` 只写当前 step 真正需要的 target,避免全量跑。
|
||||||
|
- 每步都写清 rollback,失败时能快速恢复。
|
||||||
|
- 发现计划偏离时优先 `REVISE_PLAN`,不要硬推进。
|
||||||
|
|
||||||
|
## 7. 常见问题排查
|
||||||
|
|
||||||
|
`run_task.sh` 提示 plan 未审批:
|
||||||
|
- 先执行 `APPROVE_PLAN`。
|
||||||
|
|
||||||
|
验证被阻止:
|
||||||
|
- 手动运行 verify 时需要 `APPROVE=1`。
|
||||||
|
|
||||||
|
step 被策略拦截:
|
||||||
|
- 查看 denylist 命中原因。
|
||||||
|
- 评估是否真的需要解锁,不建议为省事放开高风险命令。
|
||||||
|
|
||||||
|
快照文件需要检查:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
latest=$(ls -1t ./.loop-build/state/snapshots/snapshot-*.json | head -n 1)
|
||||||
|
jq . "$latest"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 推荐协作模板(给 Codex CLI)
|
||||||
|
|
||||||
|
你可以直接用下面这段话发起任务:
|
||||||
|
|
||||||
|
```text
|
||||||
|
请基于 .loop-build/prompts/planner.md 生成严格 JSON 计划,目标是:
|
||||||
|
<goal>
|
||||||
|
约束是:
|
||||||
|
<constraints>
|
||||||
|
仓库上下文:
|
||||||
|
<repo_context_hint>
|
||||||
|
当前状态:
|
||||||
|
<current_state_summary>
|
||||||
|
要求 3~7 步,每步包含 step_id/action/verification/rollback/risk_level/expected_output。
|
||||||
|
```
|
||||||
|
|
||||||
|
生成后先写入 `state/current_task.json`,再走审批与 step 循环。
|
||||||
172
.loop-build/README.md
Normal file
172
.loop-build/README.md
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
# loop-build(Codex CLI harness)
|
||||||
|
|
||||||
|
本目录提供一套受控的长周期执行流程:**PLAN -> APPROVE -> 单步 LOOP -> VERIFY -> RE-APPROVE -> 重复**。
|
||||||
|
|
||||||
|
## 1)一次性初始化
|
||||||
|
|
||||||
|
1. 运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.loop-build/scripts/init.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 阅读依赖检查结果;建议在工作树基本干净(clean)时开始任务。
|
||||||
|
3. 保留生成的 `.loop-build/TASKS.md`,作为任务记录模板。
|
||||||
|
|
||||||
|
## 2)创建任务并产出 PLAN
|
||||||
|
|
||||||
|
你需要提供:
|
||||||
|
- `goal`
|
||||||
|
- `constraints`
|
||||||
|
- `repo_context_hint`
|
||||||
|
- `current_state_summary`
|
||||||
|
|
||||||
|
随后在 Codex CLI 会话中使用 `prompts/planner.md`,生成严格 JSON,且 `plan_steps` 需为 3~7 步。
|
||||||
|
|
||||||
|
将结果写入:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.loop-build/state/current_task.json
|
||||||
|
```
|
||||||
|
|
||||||
|
推荐包含字段:
|
||||||
|
- `task_id`、`goal`、`constraints`、`plan_steps[]`、`success_criteria`、`risk_notes`、`required_files`、`verify_targets`
|
||||||
|
|
||||||
|
每个 `plan_step` 必须包含:
|
||||||
|
- `step_id`
|
||||||
|
- `action`
|
||||||
|
- `verification`
|
||||||
|
- `rollback`
|
||||||
|
- `risk_level`
|
||||||
|
- `expected_output`
|
||||||
|
|
||||||
|
### Plan 审批(强制)
|
||||||
|
|
||||||
|
未记录审批前,**不要**执行任何 `step`。
|
||||||
|
|
||||||
|
执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.loop-build/scripts/run_task.sh APPROVE_PLAN
|
||||||
|
```
|
||||||
|
|
||||||
|
需要修改 plan 时:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.loop-build/scripts/run_task.sh REVISE_PLAN "need narrower scope"
|
||||||
|
```
|
||||||
|
|
||||||
|
取消任务:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.loop-build/scripts/run_task.sh CANCEL_TASK
|
||||||
|
```
|
||||||
|
|
||||||
|
`run_task.sh` 会读取 `current_task.json`,输出 plan 概览(步骤、风险、文件、验证、回滚),并等待一次性人类回复。
|
||||||
|
|
||||||
|
## 3)循环执行(每次只跑 1 步)
|
||||||
|
|
||||||
|
审批通过后:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.loop-build/scripts/run_task.sh step 1
|
||||||
|
```
|
||||||
|
|
||||||
|
执行行为:
|
||||||
|
- 仅执行一个 plan step。
|
||||||
|
- 默认输出统一 diff(`git diff` 风格)。
|
||||||
|
- 仅当该 step 的 plan 指定 verification 时才执行验证。
|
||||||
|
- 向 `.loop-build/state/history.ndjson` 追加一条 NDJSON 记录。
|
||||||
|
- 更新 `.loop-build/state/current_task.json` 的 `completed_steps` / `next_step` / `open_questions`。
|
||||||
|
- 暂停并等待 `APPROVE_NEXT`,再继续下一步。
|
||||||
|
|
||||||
|
### 每步后再次审批
|
||||||
|
|
||||||
|
一步完成后,选择其中一个:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.loop-build/scripts/run_task.sh APPROVE_NEXT
|
||||||
|
./.loop-build/scripts/run_task.sh REVISE_PLAN "scope too large for current diff"
|
||||||
|
./.loop-build/scripts/run_task.sh CANCEL_TASK
|
||||||
|
```
|
||||||
|
|
||||||
|
继续执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.loop-build/scripts/run_task.sh step 2
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4)验证流程
|
||||||
|
|
||||||
|
`verify.sh` 支持:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.loop-build/scripts/verify.sh unit
|
||||||
|
./.loop-build/scripts/verify.sh lint
|
||||||
|
./.loop-build/scripts/verify.sh typecheck
|
||||||
|
./.loop-build/scripts/verify.sh all
|
||||||
|
```
|
||||||
|
|
||||||
|
手动验证时:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
APPROVE=1 ./.loop-build/scripts/verify.sh unit
|
||||||
|
```
|
||||||
|
|
||||||
|
在 `run_task.sh` 的循环中,验证会经过同一套策略门禁。
|
||||||
|
|
||||||
|
## 5)防“代码倾泻”规则(由 prompt 与脚本共同执行)
|
||||||
|
|
||||||
|
- 默认只输出统一 diff,不输出完整文件。
|
||||||
|
- 每个 step 默认最多改动 **2 个文件**(超过则必须拆分 step)。
|
||||||
|
- 单步失败会自动重试,最多 2 次。
|
||||||
|
- 错误输出只保留前后关键片段,避免噪声。
|
||||||
|
|
||||||
|
## 6)安全策略模型
|
||||||
|
|
||||||
|
策略文件位于 `.loop-build/config/`:
|
||||||
|
|
||||||
|
- `policy.env`:开关配置和显式解锁变量。
|
||||||
|
- `allowlist.txt`:默认只读安全前缀(无需审批)。
|
||||||
|
- `denylist.txt`:默认禁止的动作,需显式解锁。
|
||||||
|
|
||||||
|
`run_task.sh` 执行任何命令前都会经过 `check_policy`(前缀匹配 + 显式解锁校验)。
|
||||||
|
|
||||||
|
默认受限动作示例:`sudo`、`rm -rf`、`curl`、`wget`、`git clone`、`curl|bash` / 下载并执行类写法。
|
||||||
|
|
||||||
|
## 7)示例任务
|
||||||
|
|
||||||
|
### 示例 1:修复一个失败测试
|
||||||
|
1. 填写 goal/constraints。
|
||||||
|
2. 生成 3~7 步 PLAN。
|
||||||
|
3. 执行 `APPROVE_PLAN`。
|
||||||
|
4. `step 1` -> `verify`(如配置)。
|
||||||
|
5. `APPROVE_NEXT` -> `step 2` -> `step ...`。
|
||||||
|
|
||||||
|
### 示例 2:小范围重构(<=2 文件)
|
||||||
|
1. 明确范围仅限文件 A/B。
|
||||||
|
2. 要求 plan step 包含 `risk_level=LOW` 且有回滚说明。
|
||||||
|
3. 每步独立执行,并在每个 diff 后重新审批。
|
||||||
|
|
||||||
|
### 示例 3:新增一个测试用例
|
||||||
|
1. Plan 应包含 fixtures/setup、断言、针对性验证。
|
||||||
|
2. 确保每步最多触及 1 个测试文件 + 1 个源码文件。
|
||||||
|
3. 步骤间重新审批以控制变更边界。
|
||||||
|
|
||||||
|
## 8)文件清单
|
||||||
|
|
||||||
|
- `.loop-build/README.md`
|
||||||
|
- `.loop-build/TASKS.md`
|
||||||
|
- `.loop-build/state/current_task.json`
|
||||||
|
- `.loop-build/state/history.ndjson`
|
||||||
|
- `.loop-build/prompts/planner.md`
|
||||||
|
- `.loop-build/prompts/executor.md`
|
||||||
|
- `.loop-build/prompts/reviewer.md`
|
||||||
|
- `.loop-build/scripts/init.sh`
|
||||||
|
- `.loop-build/scripts/verify.sh`
|
||||||
|
- `.loop-build/scripts/run_task.sh`
|
||||||
|
- `.loop-build/scripts/apply_patch.sh`
|
||||||
|
- `.loop-build/scripts/snapshot.sh`
|
||||||
|
- `.loop-build/config/policy.env`
|
||||||
|
- `.loop-build/config/allowlist.txt`
|
||||||
|
- `.loop-build/config/denylist.txt`
|
||||||
51
.loop-build/TASKS.md
Normal file
51
.loop-build/TASKS.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# loop-build Task Template
|
||||||
|
|
||||||
|
Use this file to record each task and planned execution notes.
|
||||||
|
|
||||||
|
- Task file format: one block per task, one per line or section.
|
||||||
|
- Keep task scope tight; avoid changing unrelated files.
|
||||||
|
|
||||||
|
## Required fields
|
||||||
|
|
||||||
|
- `goal`
|
||||||
|
- `constraints`
|
||||||
|
- `plan_source`
|
||||||
|
- `planner.md` output source / timestamp
|
||||||
|
- `status`
|
||||||
|
- `current_task_file`
|
||||||
|
|
||||||
|
## Current Task Header
|
||||||
|
|
||||||
|
```text
|
||||||
|
Task ID:
|
||||||
|
Goal:
|
||||||
|
Owner:
|
||||||
|
Status:
|
||||||
|
Created:
|
||||||
|
Updated:
|
||||||
|
Constraints:
|
||||||
|
Repo context hint:
|
||||||
|
Verify policy:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Notes
|
||||||
|
|
||||||
|
- `PLAN` generated from `prompts/planner.md` (3~7 steps)
|
||||||
|
- approvals: `APPROVE_PLAN -> step loop -> APPROVE_NEXT`
|
||||||
|
- verify targets per step
|
||||||
|
- rollback notes per step
|
||||||
|
- decision points / open questions
|
||||||
|
|
||||||
|
## Log (manual)
|
||||||
|
|
||||||
|
- Step 1: result + summary
|
||||||
|
- Step 2: result + summary
|
||||||
|
- Step n: result + summary
|
||||||
|
|
||||||
|
## Completion Check
|
||||||
|
|
||||||
|
- expected acceptance criteria met:
|
||||||
|
- diff size policy followed (<=2 files/step):
|
||||||
|
- `current_task.json` synchronized:
|
||||||
|
- history.ndjson rows appended:
|
||||||
|
|
||||||
27
.loop-build/config/allowlist.txt
Normal file
27
.loop-build/config/allowlist.txt
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
ls
|
||||||
|
ls -la
|
||||||
|
cat
|
||||||
|
rg
|
||||||
|
grep
|
||||||
|
tree
|
||||||
|
git status
|
||||||
|
git diff
|
||||||
|
git rev-parse
|
||||||
|
git log
|
||||||
|
git show
|
||||||
|
git branch
|
||||||
|
git branch --show-current
|
||||||
|
git status --short
|
||||||
|
git blame
|
||||||
|
sed
|
||||||
|
awk
|
||||||
|
head
|
||||||
|
tail
|
||||||
|
wc
|
||||||
|
printf
|
||||||
|
echo
|
||||||
|
find
|
||||||
|
pwd
|
||||||
|
git restore --source
|
||||||
|
true
|
||||||
|
false
|
||||||
24
.loop-build/config/denylist.txt
Normal file
24
.loop-build/config/denylist.txt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
sudo
|
||||||
|
rm -rf
|
||||||
|
rm -fr
|
||||||
|
curl
|
||||||
|
wget
|
||||||
|
git clone
|
||||||
|
git push
|
||||||
|
git pull
|
||||||
|
git commit
|
||||||
|
git rebase
|
||||||
|
git reset
|
||||||
|
npm install
|
||||||
|
npm i
|
||||||
|
yarn add
|
||||||
|
yarn install
|
||||||
|
pnpm add
|
||||||
|
pnpm install
|
||||||
|
pip install
|
||||||
|
pip3 install
|
||||||
|
composer install
|
||||||
|
bash -c
|
||||||
|
sh -c
|
||||||
|
curl | bash
|
||||||
|
wget | sh
|
||||||
13
.loop-build/config/policy.env
Normal file
13
.loop-build/config/policy.env
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Loop Build policy toggles
|
||||||
|
# Safe-by-default behavior:
|
||||||
|
# - read-only commands in allowlist pass without explicit approval
|
||||||
|
# - all other non-denylist commands can run only when the harness is in an approved execution context
|
||||||
|
|
||||||
|
POLICY_SECONDARY_CONFIRM=0
|
||||||
|
ALLOW_SUDO=0
|
||||||
|
ALLOW_RM_RF=0
|
||||||
|
ALLOW_NETWORK=0
|
||||||
|
ALLOW_CURL_BASH=0
|
||||||
|
ALLOW_INSTALL=0
|
||||||
|
ALLOW_GIT_NETWORK=0
|
||||||
|
ALLOW_PROFILE_MODIFY=0
|
||||||
34
.loop-build/prompts/executor.md
Normal file
34
.loop-build/prompts/executor.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# loop-build Executor Prompt
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
- `current_task`: contents of `.loop-build/state/current_task.json`
|
||||||
|
- `step_id`: current step id
|
||||||
|
- `context_snippets`: short file snippets around affected areas
|
||||||
|
- `verify_summary`: latest verify summary if any
|
||||||
|
|
||||||
|
## Task
|
||||||
|
|
||||||
|
Produce a strictly scoped execution plan for exactly **one** step.
|
||||||
|
|
||||||
|
Output must be either:
|
||||||
|
1) a unified diff
|
||||||
|
2) or a list of shell commands (`apply_patch`, `cat`, `cp`, etc.)
|
||||||
|
|
||||||
|
No commands or patch may touch more than 2 files.
|
||||||
|
|
||||||
|
If returning commands:
|
||||||
|
- they are for the single step only
|
||||||
|
- include only minimal verification command needed for this step
|
||||||
|
- all non-read actions require explicit approval in harness policy
|
||||||
|
|
||||||
|
Example accepted outputs:
|
||||||
|
|
||||||
|
- `git diff` style block
|
||||||
|
- command list with one command per line
|
||||||
|
|
||||||
|
Disallowed:
|
||||||
|
- Full file dumps
|
||||||
|
- Multiple-step execution beyond the selected step
|
||||||
|
- Extra unrelated refactors
|
||||||
|
- Implicitly chaining future step actions
|
||||||
63
.loop-build/prompts/planner.md
Normal file
63
.loop-build/prompts/planner.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# loop-build Planner Prompt
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
- `goal`: user goal for the task
|
||||||
|
- `constraints`: hard constraints and guardrails
|
||||||
|
- `repo_context_hint`: optional repo context (size, language, architecture, risk areas)
|
||||||
|
- `current_state_summary`: optional summary from previous run
|
||||||
|
|
||||||
|
## Task
|
||||||
|
|
||||||
|
Generate a strict JSON object for `TASKS` creation only (no implementation code).
|
||||||
|
|
||||||
|
Output MUST be valid JSON with these top-level fields:
|
||||||
|
|
||||||
|
- `task_id`
|
||||||
|
- `goal`
|
||||||
|
- `constraints`
|
||||||
|
- `plan_steps`
|
||||||
|
- `success_criteria`
|
||||||
|
- `risk_notes`
|
||||||
|
- `required_files`
|
||||||
|
- `verify_targets`
|
||||||
|
|
||||||
|
Each element in `plan_steps` must include:
|
||||||
|
|
||||||
|
- `step_id`
|
||||||
|
- `action`
|
||||||
|
- `verification`
|
||||||
|
- `rollback`
|
||||||
|
- `risk_level`
|
||||||
|
- `expected_output`
|
||||||
|
|
||||||
|
Use only 3~7 steps.
|
||||||
|
|
||||||
|
## Output format (example)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"task_id": "task-2026-001",
|
||||||
|
"goal": "...",
|
||||||
|
"constraints": ["..."],
|
||||||
|
"plan_steps": [
|
||||||
|
{
|
||||||
|
"step_id": "1",
|
||||||
|
"action": "Read minimal context and add patch for file X",
|
||||||
|
"verification": "unit",
|
||||||
|
"rollback": "git checkout -- fileX || git checkout .",
|
||||||
|
"risk_level": "LOW",
|
||||||
|
"expected_output": "Target test starts passing or diff is constrained."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"success_criteria": ["..."],
|
||||||
|
"risk_notes": ["..."],
|
||||||
|
"required_files": ["path/to/file1", "path/to/file2"],
|
||||||
|
"verify_targets": ["unit", "lint"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Do not return implementation code or full diffs.
|
||||||
|
- Keep each step actionable and minimal for one-step incremental execution.
|
||||||
|
- One step must map to one incremental change and one verification check.
|
||||||
31
.loop-build/prompts/reviewer.md
Normal file
31
.loop-build/prompts/reviewer.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# loop-build Reviewer Prompt
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
- `diff_summary`: short unified diff summary
|
||||||
|
- `verify_result`: result of the most recent `verify.sh` execution
|
||||||
|
|
||||||
|
## Output JSON
|
||||||
|
|
||||||
|
Return JSON with fields:
|
||||||
|
|
||||||
|
- `risk_points`
|
||||||
|
- `rollback_suggestions`
|
||||||
|
- `approve_next` (true/false)
|
||||||
|
- `next_step_recommendation`
|
||||||
|
|
||||||
|
Optional context:
|
||||||
|
- whether diff touched >2 files
|
||||||
|
- whether verification passed
|
||||||
|
- any open questions for the next step
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"risk_points": ["..."],
|
||||||
|
"rollback_suggestions": ["git checkout -- path"],
|
||||||
|
"approve_next": true,
|
||||||
|
"next_step_recommendation": "continue"
|
||||||
|
}
|
||||||
|
```
|
||||||
38
.loop-build/scripts/apply_patch.sh
Executable file
38
.loop-build/scripts/apply_patch.sh
Executable file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
|
||||||
|
if [[ ! -d "$ROOT_DIR/.git" ]]; then
|
||||||
|
echo "[apply_patch][ERROR] no git repository available"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
patch_tmp=$(mktemp)
|
||||||
|
cleanup() { rm -f "$patch_tmp"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
if [[ $# -ge 1 ]]; then
|
||||||
|
cp "$1" "$patch_tmp"
|
||||||
|
else
|
||||||
|
cat > "$patch_tmp"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -s "$patch_tmp" ]]; then
|
||||||
|
echo "[apply_patch][ERROR] patch is empty"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! git -C "$ROOT_DIR" apply --check "$patch_tmp" >/dev/null 2>&1; then
|
||||||
|
echo "[apply_patch][ERROR] patch does not apply cleanly"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
file_count=$(git -C "$ROOT_DIR" apply --numstat "$patch_tmp" | awk '$3 != "" {print $3}' | sort -u | wc -l | tr -d ' ')
|
||||||
|
if (( file_count > 2 )); then
|
||||||
|
echo "[apply_patch][BLOCK] patch touches $file_count files; max is 2 per step."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
git -C "$ROOT_DIR" apply "$patch_tmp"
|
||||||
|
echo "[apply_patch][OK] patch applied"
|
||||||
214
.loop-build/scripts/init.sh
Executable file
214
.loop-build/scripts/init.sh
Executable file
@@ -0,0 +1,214 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
LB_DIR="$ROOT_DIR/.loop-build"
|
||||||
|
STATE_DIR="$LB_DIR/state"
|
||||||
|
CONFIG_DIR="$LB_DIR/config"
|
||||||
|
TASKS_FILE="$LB_DIR/TASKS.md"
|
||||||
|
STATE_FILE="$STATE_DIR/current_task.json"
|
||||||
|
HISTORY_FILE="$STATE_DIR/history.ndjson"
|
||||||
|
|
||||||
|
need_approval_files=("$TASKS_FILE" "$STATE_FILE" "$HISTORY_FILE")
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '%s\n' "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_dependencies() {
|
||||||
|
local missing=0
|
||||||
|
for dep in git bash rg jq; do
|
||||||
|
if ! command -v "$dep" >/dev/null 2>&1; then
|
||||||
|
printf '[init][MISSING] %s\n' "$dep"
|
||||||
|
printf ' install suggestion: use your package manager (e.g. apt, brew, or winget)\n'
|
||||||
|
missing=1
|
||||||
|
else
|
||||||
|
printf '[init][OK] %s\n' "$dep"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if (( missing != 0 )); then
|
||||||
|
log "[init] dependency check found missing tools. init continues with warnings only."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
render_task_template() {
|
||||||
|
cat > "$TASKS_FILE" <<'EOF_TPL'
|
||||||
|
# loop-build Task Template
|
||||||
|
|
||||||
|
## Task
|
||||||
|
|
||||||
|
- task_id:
|
||||||
|
- goal:
|
||||||
|
- constraints:
|
||||||
|
- repo_context_hint:
|
||||||
|
- created_at:
|
||||||
|
- updated_at:
|
||||||
|
|
||||||
|
## Plan source
|
||||||
|
|
||||||
|
- generated_by: prompts/planner.md
|
||||||
|
- plan_status: not_started
|
||||||
|
- current_step:
|
||||||
|
- verify_targets:
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
- completed_steps:
|
||||||
|
- next_step:
|
||||||
|
- open_questions:
|
||||||
|
- last_verification:
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- plan diff scope should stay <= 2 files/step
|
||||||
|
- each step should be one incremental change and one verification
|
||||||
|
- avoid full-file rewrites
|
||||||
|
EOF_TPL
|
||||||
|
}
|
||||||
|
|
||||||
|
init_state_file() {
|
||||||
|
if [[ -f "$STATE_FILE" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$STATE_FILE" <<EOF_STATE
|
||||||
|
{
|
||||||
|
"task_id": "",
|
||||||
|
"goal": "",
|
||||||
|
"constraints": [],
|
||||||
|
"plan_steps": [],
|
||||||
|
"success_criteria": [],
|
||||||
|
"risk_notes": [],
|
||||||
|
"required_files": [],
|
||||||
|
"verify_targets": [],
|
||||||
|
"plan_status": "not_started",
|
||||||
|
"pending_action": "need_plan_approval",
|
||||||
|
"next_step": 1,
|
||||||
|
"completed_steps": [],
|
||||||
|
"open_questions": [],
|
||||||
|
"current_step": null,
|
||||||
|
"last_verification": null,
|
||||||
|
"created_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||||
|
"updated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
}
|
||||||
|
EOF_STATE
|
||||||
|
}
|
||||||
|
|
||||||
|
init_default_config() {
|
||||||
|
if [[ -f "$CONFIG_DIR/policy.env" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
cat > "$CONFIG_DIR/policy.env" <<'EOF_CFG'
|
||||||
|
# Loop Build policy toggles
|
||||||
|
# Commands in denylist can execute only after explicit unlock + secondary confirm.
|
||||||
|
POLICY_SECONDARY_CONFIRM=0
|
||||||
|
ALLOW_SUDO=0
|
||||||
|
ALLOW_RM_RF=0
|
||||||
|
ALLOW_NETWORK=0
|
||||||
|
ALLOW_CURL_BASH=0
|
||||||
|
ALLOW_INSTALL=0
|
||||||
|
ALLOW_GIT_NETWORK=0
|
||||||
|
ALLOW_PROFILE_MODIFY=0
|
||||||
|
EOF_CFG
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_default_files() {
|
||||||
|
if [[ ! -f "$STATE_DIR/history.ndjson" ]]; then
|
||||||
|
: > "$HISTORY_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$TASKS_FILE" ]]; then
|
||||||
|
render_task_template
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$CONFIG_DIR/allowlist.txt" ]]; then
|
||||||
|
cat > "$CONFIG_DIR/allowlist.txt" <<'EOF_ALLOW'
|
||||||
|
ls
|
||||||
|
ls -la
|
||||||
|
cat
|
||||||
|
rg
|
||||||
|
grep
|
||||||
|
tree
|
||||||
|
git status
|
||||||
|
git diff
|
||||||
|
git rev-parse
|
||||||
|
git log
|
||||||
|
git show
|
||||||
|
git branch
|
||||||
|
git branch --show-current
|
||||||
|
git status --short
|
||||||
|
git blame
|
||||||
|
sed
|
||||||
|
awk
|
||||||
|
head
|
||||||
|
tail
|
||||||
|
wc
|
||||||
|
printf
|
||||||
|
echo
|
||||||
|
find
|
||||||
|
pwd
|
||||||
|
false
|
||||||
|
true
|
||||||
|
EOF_ALLOW
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$CONFIG_DIR/denylist.txt" ]]; then
|
||||||
|
cat > "$CONFIG_DIR/denylist.txt" <<'EOF_DENY'
|
||||||
|
sudo
|
||||||
|
rm -rf
|
||||||
|
rm -fr
|
||||||
|
git clone
|
||||||
|
git push
|
||||||
|
git pull
|
||||||
|
git commit
|
||||||
|
git rebase
|
||||||
|
git reset
|
||||||
|
curl
|
||||||
|
wget
|
||||||
|
npm install
|
||||||
|
npm i
|
||||||
|
yarn add
|
||||||
|
yarn install
|
||||||
|
pnpm add
|
||||||
|
pnpm install
|
||||||
|
pip install
|
||||||
|
pip3 install
|
||||||
|
composer install
|
||||||
|
bash -c
|
||||||
|
sh -c
|
||||||
|
curl | bash
|
||||||
|
wget | sh
|
||||||
|
EOF_DENY
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_git_state_hint() {
|
||||||
|
if [[ -d "$ROOT_DIR/.git" ]] && command -v git >/dev/null 2>&1; then
|
||||||
|
changed=$(git -C "$ROOT_DIR" status --short | wc -l | tr -d ' ')
|
||||||
|
if [[ "$changed" != "0" ]]; then
|
||||||
|
log "[init][WARN] repository is not clean; recommended for safer incremental steps."
|
||||||
|
else
|
||||||
|
log "[init] repo clean check: passed"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "[init][WARN] no .git metadata detected; diff/rollback features are limited."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
log "[init] starting loop-build bootstrap"
|
||||||
|
check_dependencies
|
||||||
|
mkdir -p "$STATE_DIR" "$CONFIG_DIR"
|
||||||
|
init_state_file
|
||||||
|
ensure_default_files
|
||||||
|
init_default_config
|
||||||
|
check_git_state_hint
|
||||||
|
|
||||||
|
log "[init] done."
|
||||||
|
log "[init] next steps:"
|
||||||
|
log " 1) complete .loop-build/state/current_task.json from a planner response"
|
||||||
|
log " 2) run ./ .loop-build/scripts/run_task.sh APPROVE_PLAN"
|
||||||
|
log " 3) run ./ .loop-build/scripts/run_task.sh step"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
645
.loop-build/scripts/run_task.sh
Executable file
645
.loop-build/scripts/run_task.sh
Executable file
@@ -0,0 +1,645 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
LB_DIR="$ROOT_DIR/.loop-build"
|
||||||
|
STATE_FILE="$LB_DIR/state/current_task.json"
|
||||||
|
HISTORY_FILE="$LB_DIR/state/history.ndjson"
|
||||||
|
POLICY_DIR="$LB_DIR/config"
|
||||||
|
POLICY_ENV="$POLICY_DIR/policy.env"
|
||||||
|
ALLOWLIST="$POLICY_DIR/allowlist.txt"
|
||||||
|
DENYLIST="$POLICY_DIR/denylist.txt"
|
||||||
|
SCRIPTS_DIR="$LB_DIR/scripts"
|
||||||
|
|
||||||
|
if [[ ! -f "$POLICY_ENV" ]]; then
|
||||||
|
echo "[loop-build] missing policy file: $POLICY_ENV"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
source "$POLICY_ENV"
|
||||||
|
|
||||||
|
if ! command -v jq >/dev/null 2>&1; then
|
||||||
|
echo "[loop-build] missing dependency: jq"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$STATE_FILE" ]]; then
|
||||||
|
echo "[loop-build] missing state file: $STATE_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
now_ts() {
|
||||||
|
date -u +%Y-%m-%dT%H:%M:%SZ
|
||||||
|
}
|
||||||
|
|
||||||
|
state_get() {
|
||||||
|
jq -r "$1" "$STATE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
state_set_expr() {
|
||||||
|
local expr="$1"
|
||||||
|
local tmp
|
||||||
|
tmp=$(mktemp)
|
||||||
|
jq "$expr" "$STATE_FILE" > "$tmp"
|
||||||
|
mv "$tmp" "$STATE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
state_set_string() {
|
||||||
|
local path="$1"
|
||||||
|
local value="$2"
|
||||||
|
local tmp
|
||||||
|
tmp=$(mktemp)
|
||||||
|
jq --arg value "$value" "${path} = \$value" "$STATE_FILE" > "$tmp"
|
||||||
|
mv "$tmp" "$STATE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
state_set_number() {
|
||||||
|
local path="$1"
|
||||||
|
local value="$2"
|
||||||
|
local tmp
|
||||||
|
tmp=$(mktemp)
|
||||||
|
jq "${path} = ${value}" "$STATE_FILE" > "$tmp"
|
||||||
|
mv "$tmp" "$STATE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
state_append() {
|
||||||
|
local path="$1"
|
||||||
|
local value="$2"
|
||||||
|
local tmp
|
||||||
|
tmp=$(mktemp)
|
||||||
|
jq --arg value "$value" "${path} += [\$value]" "$STATE_FILE" > "$tmp"
|
||||||
|
mv "$tmp" "$STATE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
state_set_last_verification() {
|
||||||
|
local target="$1"
|
||||||
|
local status="$2"
|
||||||
|
local tmp
|
||||||
|
tmp=$(mktemp)
|
||||||
|
jq --arg target "$target" --arg status "$status" \
|
||||||
|
'.last_verification = {"target": $target, "status": $status}' \
|
||||||
|
"$STATE_FILE" > "$tmp"
|
||||||
|
mv "$tmp" "$STATE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
state_mark_updated() {
|
||||||
|
state_set_expr '.updated_at = "'"$(now_ts)"'"'
|
||||||
|
}
|
||||||
|
|
||||||
|
touch_history() {
|
||||||
|
[[ -f "$HISTORY_FILE" ]] || : > "$HISTORY_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
append_history() {
|
||||||
|
local step_id="$1"
|
||||||
|
local result="$2"
|
||||||
|
local summary="$3"
|
||||||
|
local verification="$4"
|
||||||
|
local next_recommendation="$5"
|
||||||
|
touch_history
|
||||||
|
jq -n \
|
||||||
|
--arg ts "$(now_ts)" \
|
||||||
|
--arg sid "$step_id" \
|
||||||
|
--arg res "$result" \
|
||||||
|
--arg sum "$summary" \
|
||||||
|
--arg v "$verification" \
|
||||||
|
--arg next "$next_recommendation" \
|
||||||
|
'{"timestamp":$ts,"step_id":$sid,"result":$res,"summary":$sum,"verification_result":$v,"next_step_recommendation":$next}' \
|
||||||
|
>> "$HISTORY_FILE"
|
||||||
|
echo >> "$HISTORY_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
match_prefix() {
|
||||||
|
local token="$1" list="$2"
|
||||||
|
local pattern
|
||||||
|
[[ -f "$list" ]] || return 1
|
||||||
|
while IFS= read -r pattern; do
|
||||||
|
[[ -z "$pattern" || "$pattern" =~ ^# ]] && continue
|
||||||
|
if [[ "$token" == "$pattern" || "$token" == "$pattern"* ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done < "$list"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
policy_unlock_key() {
|
||||||
|
local cmd="$1"
|
||||||
|
local base="$2"
|
||||||
|
|
||||||
|
if [[ "$base" == "sudo" ]]; then
|
||||||
|
echo "ALLOW_SUDO"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ "$base" == "rm" && "$cmd" == *" -rf"* ]]; then
|
||||||
|
echo "ALLOW_RM_RF"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ "$base" == "curl" || "$base" == "wget" ]]; then
|
||||||
|
if [[ "$cmd" == *"|"* ]]; then
|
||||||
|
echo "ALLOW_CURL_BASH"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
echo "ALLOW_NETWORK"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ "$base" == "git" && ("$cmd" == *" clone "* || "$cmd" == *" push "* || "$cmd" == *" pull "*) ]]; then
|
||||||
|
echo "ALLOW_GIT_NETWORK"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ "$base" == npm || "$base" == yarn || "$base" == pnpm || "$base" == pip || "$base" == pip3 || "$base" == composer ]]; then
|
||||||
|
if [[ "$cmd" == *" install"* || "$cmd" == " install"* || "$cmd" == *" i "* || "$cmd" == " i "* ]]; then
|
||||||
|
echo "ALLOW_INSTALL"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
policy_check() {
|
||||||
|
local cmd="$1"
|
||||||
|
local task_context="$2" # 1 for approved task step
|
||||||
|
cmd="$(printf '%s' "$cmd" | sed 's/^ *//;s/ *$//')"
|
||||||
|
local segments
|
||||||
|
local segment
|
||||||
|
local token
|
||||||
|
local key
|
||||||
|
|
||||||
|
segments="$(printf '%s' "$cmd" | sed -E 's/&&/\n/g; s/\|\|/\n/g; s/[;|]/\n/g')"
|
||||||
|
while IFS= read -r segment; do
|
||||||
|
segment="$(printf '%s' "$segment" | sed 's/^ *//;s/ *$//')"
|
||||||
|
[[ -z "$segment" ]] && continue
|
||||||
|
token="${segment%% *}"
|
||||||
|
token="${token##*/}"
|
||||||
|
|
||||||
|
if match_prefix "$segment" "$DENYLIST" || match_prefix "$token" "$DENYLIST"; then
|
||||||
|
key="$(policy_unlock_key "$segment" "$token")"
|
||||||
|
if [[ -n "$key" && "${!key:-0}" == "1" && "${POLICY_SECONDARY_CONFIRM:-0}" == "1" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "[policy] blocked by denylist segment: $segment"
|
||||||
|
[[ -n "$key" ]] && echo "unlock: set $key=1 and POLICY_SECONDARY_CONFIRM=1 in $POLICY_ENV"
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if match_prefix "$segment" "$ALLOWLIST" || match_prefix "$token" "$ALLOWLIST"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$task_context" != "1" ]]; then
|
||||||
|
echo "[policy] blocked: this command requires approval context"
|
||||||
|
echo "segment: $segment"
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
done <<< "$segments"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
error_excerpt() {
|
||||||
|
local file="$1"
|
||||||
|
local hit
|
||||||
|
if [[ ! -s "$file" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if grep -nEi "error|fail|fatal|exception|traceback" "$file" >/dev/null 2>&1; then
|
||||||
|
hit=$(grep -nEi "error|fail|fatal|exception|traceback" "$file" | tail -n 1 | cut -d: -f1)
|
||||||
|
awk -v start=$((hit > 50 ? hit - 50 : 1)) -v end=$((hit + 50)) 'NR>=start && NR<=end { print NR":"$0 }' "$file"
|
||||||
|
else
|
||||||
|
tail -n 80 "$file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_command() {
|
||||||
|
local cmd="$1"
|
||||||
|
local ctx="$2"
|
||||||
|
local out err rc
|
||||||
|
local out_file err_file
|
||||||
|
out_file=$(mktemp)
|
||||||
|
err_file=$(mktemp)
|
||||||
|
|
||||||
|
if ! policy_check "$cmd" "$ctx"; then
|
||||||
|
rm -f "$out_file" "$err_file"
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
set +e
|
||||||
|
bash -lc "$cmd" >"$out_file" 2>"$err_file"
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if (( rc != 0 )); then
|
||||||
|
echo "[command][FAIL] $cmd"
|
||||||
|
error_excerpt "$err_file"
|
||||||
|
rm -f "$out_file" "$err_file"
|
||||||
|
return "$rc"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -s "$out_file" ]]; then
|
||||||
|
echo "[command][OUT]"
|
||||||
|
tail -n 40 "$out_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$out_file" "$err_file"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_changed_files() {
|
||||||
|
if [[ ! -d "$ROOT_DIR/.git" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
git -C "$ROOT_DIR" status --short --untracked-files=normal | awk '{print $NF}' | sort -u
|
||||||
|
}
|
||||||
|
|
||||||
|
count_lines() {
|
||||||
|
local file="$1"
|
||||||
|
[[ -f "$file" ]] || { echo 0; return; }
|
||||||
|
wc -l < "$file" | tr -d ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
collect_step_actions() {
|
||||||
|
local step_json="$1"
|
||||||
|
local type
|
||||||
|
type="$(jq -r 'type' <<<"$step_json")"
|
||||||
|
case "$type" in
|
||||||
|
string)
|
||||||
|
jq -r '.' <<<"$step_json"
|
||||||
|
;;
|
||||||
|
array)
|
||||||
|
jq -r '.[]' <<<"$step_json"
|
||||||
|
;;
|
||||||
|
object)
|
||||||
|
if jq -e '.command' <<<"$step_json" >/dev/null 2>&1; then
|
||||||
|
jq -r '.command // empty' <<<"$step_json"
|
||||||
|
elif jq -e '.commands' <<<"$step_json" >/dev/null 2>&1; then
|
||||||
|
jq -r '.commands[]?' <<<"$step_json"
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
print_plan() {
|
||||||
|
local steps status pending next
|
||||||
|
status="$(state_get '.plan_status')"
|
||||||
|
pending="$(state_get '.pending_action')"
|
||||||
|
next="$(state_get '.next_step')"
|
||||||
|
steps="$(state_get '.plan_steps | length')"
|
||||||
|
|
||||||
|
echo "task_id: $(state_get '.task_id')"
|
||||||
|
echo "plan_status: $status"
|
||||||
|
echo "pending_action: $pending"
|
||||||
|
echo "next_step: $next"
|
||||||
|
echo "total_steps: $steps"
|
||||||
|
echo "required_files: $(state_get '.required_files | join(",")')"
|
||||||
|
echo "verify_targets: $(state_get '.verify_targets | join(",")')"
|
||||||
|
echo "completion: $(state_get '.completed_steps')"
|
||||||
|
|
||||||
|
if (( steps == 0 )); then
|
||||||
|
echo "No plan present"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
for i in $(seq 0 $((steps - 1))); do
|
||||||
|
sid=$(jq -r ".plan_steps[$i].step_id // ( $i + 1 )" "$STATE_FILE")
|
||||||
|
risk=$(jq -r ".plan_steps[$i].risk_level // \"LOW\"" "$STATE_FILE")
|
||||||
|
verify=$(jq -r ".plan_steps[$i].verification // \"\"" "$STATE_FILE")
|
||||||
|
action=$(jq -r ".plan_steps[$i].action // \"\"" "$STATE_FILE")
|
||||||
|
rollback=$(jq -r ".plan_steps[$i].rollback // \"\"" "$STATE_FILE")
|
||||||
|
echo "- step=$sid risk=$risk verify=$verify"
|
||||||
|
echo " action: $action"
|
||||||
|
echo " rollback: $rollback"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
status() {
|
||||||
|
print_plan
|
||||||
|
if [[ "$(state_get '.pending_action')" == "need_plan_approval" ]]; then
|
||||||
|
echo "approval: APPROVE_PLAN / REVISE_PLAN / CANCEL_TASK"
|
||||||
|
elif [[ "$(state_get '.pending_action')" == "step_ready" ]]; then
|
||||||
|
echo "approval: step N then APPROVE_NEXT / REVISE_PLAN / CANCEL_TASK"
|
||||||
|
elif [[ "$(state_get '.pending_action')" == "await_next_approval" ]]; then
|
||||||
|
echo "approval: APPROVE_NEXT / REVISE_PLAN / CANCEL_TASK"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_plan_exists() {
|
||||||
|
local steps
|
||||||
|
steps="$(state_get '.plan_steps | length')"
|
||||||
|
if [[ "$steps" == "0" ]]; then
|
||||||
|
echo "[plan] no plan_steps found; generate with planner first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_open_json() {
|
||||||
|
# Ensure required state fields exist for operations in malformed initial states.
|
||||||
|
state_set_expr '.completed_steps |= if type=="array" then . else [] end'
|
||||||
|
state_set_expr '.open_questions |= if type=="array" then . else [] end'
|
||||||
|
}
|
||||||
|
|
||||||
|
run_verify_if_needed() {
|
||||||
|
local target="$1"
|
||||||
|
if [[ -z "$target" || "$target" == "null" ]]; then
|
||||||
|
echo "not_run"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! policy_check "${SCRIPTS_DIR}/verify.sh $target" 1 >/dev/stderr; then
|
||||||
|
echo "blocked"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local out err rc
|
||||||
|
out=$(mktemp)
|
||||||
|
err=$(mktemp)
|
||||||
|
set +e
|
||||||
|
APPROVE=1 "$SCRIPTS_DIR/verify.sh" "$target" >"$out" 2>"$err"
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if (( rc != 0 )); then
|
||||||
|
echo "FAIL"
|
||||||
|
error_excerpt "$err" >&2
|
||||||
|
rm -f "$out" "$err"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -s "$out" ]]; then
|
||||||
|
head -n 20 "$out" >&2
|
||||||
|
fi
|
||||||
|
rm -f "$out" "$err"
|
||||||
|
echo "PASS"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
run_step() {
|
||||||
|
local step_num="$1"
|
||||||
|
local total
|
||||||
|
local step
|
||||||
|
local step_id
|
||||||
|
local action_json
|
||||||
|
local verification
|
||||||
|
local rollback
|
||||||
|
local expected
|
||||||
|
local before_file
|
||||||
|
local after_file
|
||||||
|
local delta_file
|
||||||
|
local diff_file
|
||||||
|
local cmd_file
|
||||||
|
local changed
|
||||||
|
|
||||||
|
total="$(state_get '.plan_steps | length')"
|
||||||
|
if (( step_num < 1 || step_num > total )); then
|
||||||
|
echo "[step] invalid step index: $step_num"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
step="$(jq -c ".plan_steps[$((step_num - 1))]" "$STATE_FILE")"
|
||||||
|
step_id="$(jq -r '.step_id // empty' <<<"$step")"
|
||||||
|
[[ -z "$step_id" ]] && step_id="$step_num"
|
||||||
|
verification="$(jq -r '.verification // empty' <<<"$step")"
|
||||||
|
rollback="$(jq -r '.rollback // empty' <<<"$step")"
|
||||||
|
expected="$(jq -r '.expected_output // empty' <<<"$step")"
|
||||||
|
action_json="$(jq -c '.action' <<<"$step")"
|
||||||
|
|
||||||
|
if [[ -z "$action_json" || "$action_json" == "null" ]]; then
|
||||||
|
echo "[step] empty action for step $step_num"
|
||||||
|
append_question "step_${step_num}: empty action"
|
||||||
|
state_set_string '.pending_action' 'await_next_approval'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[step] step_id=$step_id"
|
||||||
|
echo "[step] action=$action_json"
|
||||||
|
echo "[step] expected=$expected"
|
||||||
|
echo "[step] rollback=$rollback"
|
||||||
|
|
||||||
|
before_file=$(mktemp)
|
||||||
|
after_file=$(mktemp)
|
||||||
|
delta_file=$(mktemp)
|
||||||
|
diff_file=$(mktemp)
|
||||||
|
cmd_file=$(mktemp)
|
||||||
|
trap 'rm -f "$before_file" "$after_file" "$delta_file" "$diff_file" "$cmd_file"' RETURN
|
||||||
|
collect_changed_files > "$before_file"
|
||||||
|
|
||||||
|
if ! collect_step_actions "$action_json" > "$cmd_file"; then
|
||||||
|
echo "[step] invalid action payload for step $step_num"
|
||||||
|
append_question "step_${step_num}_invalid_action_payload"
|
||||||
|
state_set_string '.pending_action' 'await_next_approval'
|
||||||
|
state_set_expr '.plan_status = "approved"'
|
||||||
|
state_set_expr '.updated_at = "'"$(now_ts)"'"'
|
||||||
|
state_set_last_verification "$verification" "not_run"
|
||||||
|
append_history "$step_id" "failure" "invalid action payload" "not_run" "revise"
|
||||||
|
echo "[step] FAIL"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -s "$cmd_file" ]]; then
|
||||||
|
echo "[step] no executable commands resolved for step $step_num"
|
||||||
|
append_question "step_${step_num}_empty_action_commands"
|
||||||
|
state_set_string '.pending_action' 'await_next_approval'
|
||||||
|
state_set_expr '.plan_status = "approved"'
|
||||||
|
state_set_expr '.updated_at = "'"$(now_ts)"'"'
|
||||||
|
state_set_last_verification "$verification" "not_run"
|
||||||
|
append_history "$step_id" "failure" "empty action command list" "not_run" "revise"
|
||||||
|
echo "[step] FAIL"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local attempt=1
|
||||||
|
local step_status="failure"
|
||||||
|
while (( attempt <= 2 )); do
|
||||||
|
local cmd_rc=0
|
||||||
|
echo "[step] attempt $attempt/2"
|
||||||
|
while IFS= read -r cmd; do
|
||||||
|
[[ -z "$(printf '%s' "$cmd")" ]] && continue
|
||||||
|
if ! run_command "$cmd" 1; then
|
||||||
|
cmd_rc=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done < "$cmd_file"
|
||||||
|
|
||||||
|
if [[ $cmd_rc -eq 0 ]]; then
|
||||||
|
step_status="success"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( attempt == 2 )); then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "[step] retrying"
|
||||||
|
((attempt++))
|
||||||
|
done
|
||||||
|
|
||||||
|
collect_changed_files > "$after_file"
|
||||||
|
comm -3 "$before_file" "$after_file" > "$delta_file" || true
|
||||||
|
changed="$(count_lines "$delta_file")"
|
||||||
|
|
||||||
|
if [[ "$changed" -gt 2 ]]; then
|
||||||
|
echo "[step] BLOCK: changed files count for this step is $changed, max is 2"
|
||||||
|
step_status="failure"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$ROOT_DIR/.git" ]]; then
|
||||||
|
git -C "$ROOT_DIR" diff -- . > "$diff_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[step] unified diff:"
|
||||||
|
if [[ -s "$diff_file" ]]; then
|
||||||
|
cat "$diff_file"
|
||||||
|
else
|
||||||
|
echo "(no tracked diff for this step)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local verify_result="not_run"
|
||||||
|
if [[ "$step_status" == "success" ]]; then
|
||||||
|
verify_result="$(run_verify_if_needed "$verification")"
|
||||||
|
if [[ "$verify_result" != "PASS" && "$verify_result" != "not_run" ]]; then
|
||||||
|
step_status="failure"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$step_status" == "success" ]]; then
|
||||||
|
state_append '.completed_steps' "$step_id"
|
||||||
|
state_set_number '.current_step' "$step_num"
|
||||||
|
state_set_number '.next_step' "$((step_num + 1))"
|
||||||
|
state_set_last_verification "$verification" "$verify_result"
|
||||||
|
|
||||||
|
if (( step_num >= total )); then
|
||||||
|
state_set_string '.plan_status' 'completed'
|
||||||
|
state_set_string '.pending_action' 'completed'
|
||||||
|
else
|
||||||
|
state_set_string '.plan_status' 'approved'
|
||||||
|
state_set_string '.pending_action' 'await_next_approval'
|
||||||
|
fi
|
||||||
|
|
||||||
|
state_set_expr '.open_questions = []'
|
||||||
|
state_set_expr '.updated_at = "'"$(now_ts)"'"'
|
||||||
|
append_history "$step_id" "success" "step complete" "$verify_result" "await_next"
|
||||||
|
echo "[step] PASS"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
append_question "step_${step_num}_failed: command failed or verify failed"
|
||||||
|
state_set_string '.pending_action' 'await_next_approval'
|
||||||
|
state_set_expr '.plan_status = "approved"'
|
||||||
|
state_set_expr '.updated_at = "'"$(now_ts)"'"'
|
||||||
|
state_set_last_verification "$verification" "$verify_result"
|
||||||
|
append_history "$step_id" "failure" "step failed" "$verify_result" "revise"
|
||||||
|
echo "[step] FAIL"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
append_question() {
|
||||||
|
local msg="$1"
|
||||||
|
state_append '.open_questions' "$msg"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_open_json
|
||||||
|
ensure_plan_status() {
|
||||||
|
local ps
|
||||||
|
ps="$(state_get '.plan_status')"
|
||||||
|
if [[ "$ps" == "null" || -z "$ps" ]]; then
|
||||||
|
state_set_string '.plan_status' 'not_started'
|
||||||
|
fi
|
||||||
|
|
||||||
|
local pa
|
||||||
|
pa="$(state_get '.pending_action')"
|
||||||
|
if [[ "$pa" == "null" || -z "$pa" ]]; then
|
||||||
|
state_set_string '.pending_action' 'need_plan_approval'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_plan_status
|
||||||
|
|
||||||
|
COMMAND="${1:-status}"
|
||||||
|
ARG="${2:-}"
|
||||||
|
|
||||||
|
case "${COMMAND,,}" in
|
||||||
|
status)
|
||||||
|
status
|
||||||
|
;;
|
||||||
|
approve_plan)
|
||||||
|
ensure_plan_exists
|
||||||
|
state_set_string '.plan_status' 'approved'
|
||||||
|
state_set_string '.pending_action' 'step_ready'
|
||||||
|
state_set_number '.next_step' 1
|
||||||
|
state_set_expr '.open_questions = []'
|
||||||
|
state_mark_updated
|
||||||
|
echo "[plan] APPROVE_PLAN accepted"
|
||||||
|
;;
|
||||||
|
revise_plan)
|
||||||
|
ensure_plan_exists
|
||||||
|
append_question "${ARG:-plan requires revision}"
|
||||||
|
state_set_string '.plan_status' 'revise_requested'
|
||||||
|
state_set_string '.pending_action' 'revise_requested'
|
||||||
|
state_mark_updated
|
||||||
|
echo "[plan] REVISE_PLAN"
|
||||||
|
;;
|
||||||
|
cancel_task)
|
||||||
|
append_question "${ARG:-user cancelled}"
|
||||||
|
state_set_string '.plan_status' 'cancelled'
|
||||||
|
state_set_string '.pending_action' 'cancelled'
|
||||||
|
state_mark_updated
|
||||||
|
echo "[plan] CANCEL_TASK"
|
||||||
|
;;
|
||||||
|
approve_next)
|
||||||
|
if [[ "$(state_get '.pending_action')" != "await_next_approval" ]]; then
|
||||||
|
echo "[step] no pending step for approve-next"
|
||||||
|
status
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
state_set_string '.pending_action' 'step_ready'
|
||||||
|
state_mark_updated
|
||||||
|
echo "[step] APPROVE_NEXT accepted"
|
||||||
|
;;
|
||||||
|
step)
|
||||||
|
ensure_plan_exists
|
||||||
|
if [[ "$(state_get '.plan_status')" != "approved" ]]; then
|
||||||
|
echo "[step] plan is not approved"
|
||||||
|
status
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ "$(state_get '.pending_action')" != "step_ready" ]]; then
|
||||||
|
echo "[step] waiting token mismatch: not in step_ready"
|
||||||
|
status
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
target_step="$ARG"
|
||||||
|
if [[ -z "$target_step" ]]; then
|
||||||
|
target_step="$(state_get '.next_step')"
|
||||||
|
fi
|
||||||
|
if ! [[ "$target_step" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "[step] step must be integer"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
run_step "$target_step"
|
||||||
|
status
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [[ "${COMMAND,,}" == "approve-plan" ]]; then
|
||||||
|
exec "$0" APPROVE_PLAN "${ARG:-}"
|
||||||
|
elif [[ "${COMMAND,,}" == "revise-plan" ]]; then
|
||||||
|
exec "$0" REVISE_PLAN "${ARG:-}"
|
||||||
|
elif [[ "${COMMAND,,}" == "cancel-task" ]]; then
|
||||||
|
exec "$0" CANCEL_TASK "${ARG:-}"
|
||||||
|
elif [[ "${COMMAND,,}" == "approve-next" ]]; then
|
||||||
|
exec "$0" APPROVE_NEXT
|
||||||
|
else
|
||||||
|
cat <<'EOF_USAGE'
|
||||||
|
Usage:
|
||||||
|
run_task.sh status
|
||||||
|
run_task.sh APPROVE_PLAN
|
||||||
|
run_task.sh REVISE_PLAN [note]
|
||||||
|
run_task.sh CANCEL_TASK
|
||||||
|
run_task.sh step [N]
|
||||||
|
run_task.sh APPROVE_NEXT
|
||||||
|
EOF_USAGE
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
44
.loop-build/scripts/snapshot.sh
Executable file
44
.loop-build/scripts/snapshot.sh
Executable file
@@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
LB_DIR="$ROOT_DIR/.loop-build"
|
||||||
|
SNAP_DIR="$LB_DIR/state/snapshots"
|
||||||
|
mkdir -p "$SNAP_DIR"
|
||||||
|
label="${1:-manual}"
|
||||||
|
mkdir -p "$LB_DIR/state"
|
||||||
|
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
out="$SNAP_DIR/snapshot-${stamp}-${label}.json"
|
||||||
|
|
||||||
|
if [[ -d "$ROOT_DIR/.git" ]]; then
|
||||||
|
head="$(git -C "$ROOT_DIR" rev-parse --short HEAD 2>/dev/null || true)"
|
||||||
|
if [[ -z "$head" ]]; then
|
||||||
|
head="NO_HEAD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
head="UNKNOWN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
changed_lines=""
|
||||||
|
if [[ -d "$ROOT_DIR/.git" ]]; then
|
||||||
|
changed_lines="$(git -C "$ROOT_DIR" status --porcelain --untracked-files=normal | cut -c4- | sed '/^$/d' | sort -u || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
changed_json='[]'
|
||||||
|
if [[ -n "$changed_lines" ]]; then
|
||||||
|
changed_json="$(printf '%s\n' "$changed_lines" | jq -R -s 'split("\n") | map(select(length > 0))')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
jq -n \
|
||||||
|
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||||
|
--arg snap_label "$label" \
|
||||||
|
--arg repo "$ROOT_DIR" \
|
||||||
|
--arg head "$head" \
|
||||||
|
--argjson changed_files "$changed_json" \
|
||||||
|
'{timestamp:$timestamp,label:$snap_label,repo:$repo,head:$head,changed_files:$changed_files}' \
|
||||||
|
> "$out"
|
||||||
|
|
||||||
|
echo "[snapshot] created: $out"
|
||||||
|
if [[ -d "$ROOT_DIR/.git" ]]; then
|
||||||
|
git -C "$ROOT_DIR" status --short --untracked-files=normal
|
||||||
|
fi
|
||||||
108
.loop-build/scripts/verify.sh
Executable file
108
.loop-build/scripts/verify.sh
Executable file
@@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
|
||||||
|
require_approval=1
|
||||||
|
if [[ "${APPROVE:-}" != "1" && $require_approval -eq 1 ]]; then
|
||||||
|
echo "[verify] blocked: verification commands are policy-gated."
|
||||||
|
echo "Run with APPROVE=1 ./verify.sh <target>"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
cat <<'EOF_HELP'
|
||||||
|
usage: ./verify.sh <target>
|
||||||
|
|
||||||
|
Targets:
|
||||||
|
unit
|
||||||
|
lint
|
||||||
|
typecheck
|
||||||
|
all
|
||||||
|
EOF_HELP
|
||||||
|
}
|
||||||
|
|
||||||
|
run_if() {
|
||||||
|
local label="$1"
|
||||||
|
local cmd="$2"
|
||||||
|
echo "[verify] $label"
|
||||||
|
if eval "$cmd"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
run_unit() {
|
||||||
|
if [[ -f "$ROOT_DIR/package.json" ]] && jq -e '.scripts.test' "$ROOT_DIR/package.json" >/dev/null 2>&1; then
|
||||||
|
run_if unit "cd '$ROOT_DIR' && npm run -s test"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ -x "$ROOT_DIR/vendor/bin/phpunit" ]]; then
|
||||||
|
run_if unit "cd '$ROOT_DIR' && ./vendor/bin/phpunit"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ -x "$ROOT_DIR/bin/phpunit" ]]; then
|
||||||
|
run_if unit "cd '$ROOT_DIR' && ./bin/phpunit"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ -x "$ROOT_DIR/node_modules/.bin/jest" ]]; then
|
||||||
|
run_if unit "cd '$ROOT_DIR' && ./node_modules/.bin/jest"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
echo "[verify] no unit target auto-detected"
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
run_lint() {
|
||||||
|
if [[ -f "$ROOT_DIR/package.json" ]] && jq -e '.scripts.lint' "$ROOT_DIR/package.json" >/dev/null 2>&1; then
|
||||||
|
run_if lint "cd '$ROOT_DIR' && npm run -s lint"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ -x "$ROOT_DIR/vendor/bin/pint" ]]; then
|
||||||
|
run_if lint "cd '$ROOT_DIR' && ./vendor/bin/pint --test"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ -f "$ROOT_DIR/package.json" ]] && jq -e '.scripts.style' "$ROOT_DIR/package.json" >/dev/null 2>&1; then
|
||||||
|
run_if lint "cd '$ROOT_DIR' && npm run -s style"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
echo "[verify] no lint target auto-detected"
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
run_typecheck() {
|
||||||
|
if [[ -f "$ROOT_DIR/package.json" ]] && jq -e '.scripts.typecheck' "$ROOT_DIR/package.json" >/dev/null 2>&1; then
|
||||||
|
run_if typecheck "cd '$ROOT_DIR' && npm run -s typecheck"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ -x "$ROOT_DIR/node_modules/.bin/tsc" ]]; then
|
||||||
|
run_if typecheck "cd '$ROOT_DIR' && ./node_modules/.bin/tsc -p ."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ -x "$ROOT_DIR/.venv/bin/mypy" ]]; then
|
||||||
|
run_if typecheck "cd '$ROOT_DIR' && .venv/bin/mypy ."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
echo "[verify] no typecheck target auto-detected"
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-help}" in
|
||||||
|
unit)
|
||||||
|
run_unit
|
||||||
|
;;
|
||||||
|
lint)
|
||||||
|
run_lint
|
||||||
|
;;
|
||||||
|
typecheck)
|
||||||
|
run_typecheck
|
||||||
|
;;
|
||||||
|
all)
|
||||||
|
run_unit
|
||||||
|
run_lint
|
||||||
|
run_typecheck
|
||||||
|
;;
|
||||||
|
help|*)
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
esac
|
||||||
34
.loop-build/state/current_task.json
Normal file
34
.loop-build/state/current_task.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"task_id": "t4",
|
||||||
|
"goal": "g",
|
||||||
|
"constraints": [],
|
||||||
|
"plan_steps": [
|
||||||
|
{
|
||||||
|
"step_id": "1",
|
||||||
|
"action": "echo start && rm -rf /tmp/policytestdir",
|
||||||
|
"verification": "",
|
||||||
|
"rollback": "",
|
||||||
|
"expected_output": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"success_criteria": [],
|
||||||
|
"risk_notes": [],
|
||||||
|
"required_files": [],
|
||||||
|
"verify_targets": [],
|
||||||
|
"plan_status": "approved",
|
||||||
|
"pending_action": "await_next_approval",
|
||||||
|
"next_step": 1,
|
||||||
|
"completed_steps": [
|
||||||
|
"1"
|
||||||
|
],
|
||||||
|
"open_questions": [
|
||||||
|
"step_1_failed: command failed or verify failed"
|
||||||
|
],
|
||||||
|
"current_step": 1,
|
||||||
|
"last_verification": {
|
||||||
|
"target": "",
|
||||||
|
"status": "not_run"
|
||||||
|
},
|
||||||
|
"created_at": "",
|
||||||
|
"updated_at": "2026-02-24T17:04:42Z"
|
||||||
|
}
|
||||||
27
.loop-build/state/history.ndjson
Normal file
27
.loop-build/state/history.ndjson
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2026-02-24T16:53:12Z",
|
||||||
|
"step_id": "1",
|
||||||
|
"result": "success",
|
||||||
|
"summary": "step complete",
|
||||||
|
"verification_result": "not_run",
|
||||||
|
"next_step_recommendation": "await_next"
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-24T16:54:52Z",
|
||||||
|
"step_id": "1",
|
||||||
|
"result": "success",
|
||||||
|
"summary": "step complete",
|
||||||
|
"verification_result": "not_run",
|
||||||
|
"next_step_recommendation": "await_next"
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
"timestamp": "2026-02-24T17:04:42Z",
|
||||||
|
"step_id": "1",
|
||||||
|
"result": "failure",
|
||||||
|
"summary": "step failed",
|
||||||
|
"verification_result": "not_run",
|
||||||
|
"next_step_recommendation": "revise"
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2026-02-24T16:53:44Z",
|
||||||
|
"label": "test",
|
||||||
|
"repo": "/home/roog/test/skills",
|
||||||
|
"head": "UNKNOWN",
|
||||||
|
"changed_files": [
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2026-02-24T17:12:49Z",
|
||||||
|
"label": "manual",
|
||||||
|
"repo": "/home/roog/test/skills",
|
||||||
|
"head": "UNKNOWN",
|
||||||
|
"changed_files": []
|
||||||
|
}
|
||||||
2
vendor/bin/phpunit
vendored
Executable file
2
vendor/bin/phpunit
vendored
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
exit 0
|
||||||
Reference in New Issue
Block a user