commit 6f02ef607f691e3664ea2240cbf18c1242dbc020 Author: ROOG Date: Wed Feb 25 01:18:49 2026 +0800 Add `.loop-build` harness and IntelliJ project configuration files diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b6b1ecf --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 已忽略包含查询文件的默认文件夹 +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03d9549 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..90dee70 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + {} + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..2cc12fe --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 0000000..f324872 --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/skills.iml b/.idea/skills.iml new file mode 100644 index 0000000..c956989 --- /dev/null +++ b/.idea/skills.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.loop-build/HELP.md b/.loop-build/HELP.md new file mode 100644 index 0000000..3f443db --- /dev/null +++ b/.loop-build/HELP.md @@ -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 计划,目标是: + +约束是: + +仓库上下文: + +当前状态: + +要求 3~7 步,每步包含 step_id/action/verification/rollback/risk_level/expected_output。 +``` + +生成后先写入 `state/current_task.json`,再走审批与 step 循环。 diff --git a/.loop-build/README.md b/.loop-build/README.md new file mode 100644 index 0000000..be37efe --- /dev/null +++ b/.loop-build/README.md @@ -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` diff --git a/.loop-build/TASKS.md b/.loop-build/TASKS.md new file mode 100644 index 0000000..8a83e03 --- /dev/null +++ b/.loop-build/TASKS.md @@ -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: + diff --git a/.loop-build/config/allowlist.txt b/.loop-build/config/allowlist.txt new file mode 100644 index 0000000..c20d39b --- /dev/null +++ b/.loop-build/config/allowlist.txt @@ -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 diff --git a/.loop-build/config/denylist.txt b/.loop-build/config/denylist.txt new file mode 100644 index 0000000..c2d9701 --- /dev/null +++ b/.loop-build/config/denylist.txt @@ -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 diff --git a/.loop-build/config/policy.env b/.loop-build/config/policy.env new file mode 100644 index 0000000..9108f42 --- /dev/null +++ b/.loop-build/config/policy.env @@ -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 diff --git a/.loop-build/prompts/executor.md b/.loop-build/prompts/executor.md new file mode 100644 index 0000000..4b3392a --- /dev/null +++ b/.loop-build/prompts/executor.md @@ -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 diff --git a/.loop-build/prompts/planner.md b/.loop-build/prompts/planner.md new file mode 100644 index 0000000..d5a0a0b --- /dev/null +++ b/.loop-build/prompts/planner.md @@ -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. diff --git a/.loop-build/prompts/reviewer.md b/.loop-build/prompts/reviewer.md new file mode 100644 index 0000000..176cf54 --- /dev/null +++ b/.loop-build/prompts/reviewer.md @@ -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" +} +``` diff --git a/.loop-build/scripts/apply_patch.sh b/.loop-build/scripts/apply_patch.sh new file mode 100755 index 0000000..d043c5e --- /dev/null +++ b/.loop-build/scripts/apply_patch.sh @@ -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" diff --git a/.loop-build/scripts/init.sh b/.loop-build/scripts/init.sh new file mode 100755 index 0000000..74169fe --- /dev/null +++ b/.loop-build/scripts/init.sh @@ -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" < "$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 "$@" diff --git a/.loop-build/scripts/run_task.sh b/.loop-build/scripts/run_task.sh new file mode 100755 index 0000000..990e9b3 --- /dev/null +++ b/.loop-build/scripts/run_task.sh @@ -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 diff --git a/.loop-build/scripts/snapshot.sh b/.loop-build/scripts/snapshot.sh new file mode 100755 index 0000000..cbb56b3 --- /dev/null +++ b/.loop-build/scripts/snapshot.sh @@ -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 diff --git a/.loop-build/scripts/verify.sh b/.loop-build/scripts/verify.sh new file mode 100755 index 0000000..1574ed0 --- /dev/null +++ b/.loop-build/scripts/verify.sh @@ -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 " + exit 2 +fi + +show_help() { + cat <<'EOF_HELP' +usage: ./verify.sh + +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 diff --git a/.loop-build/state/current_task.json b/.loop-build/state/current_task.json new file mode 100644 index 0000000..25d30a9 --- /dev/null +++ b/.loop-build/state/current_task.json @@ -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" +} diff --git a/.loop-build/state/history.ndjson b/.loop-build/state/history.ndjson new file mode 100644 index 0000000..433f7dd --- /dev/null +++ b/.loop-build/state/history.ndjson @@ -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" +} + diff --git a/.loop-build/state/snapshots/snapshot-20260224T165344Z-test.json b/.loop-build/state/snapshots/snapshot-20260224T165344Z-test.json new file mode 100644 index 0000000..cf05ec2 --- /dev/null +++ b/.loop-build/state/snapshots/snapshot-20260224T165344Z-test.json @@ -0,0 +1,8 @@ +{ + "timestamp": "2026-02-24T16:53:44Z", + "label": "test", + "repo": "/home/roog/test/skills", + "head": "UNKNOWN", + "changed_files": [ + ] +} diff --git a/.loop-build/state/snapshots/snapshot-20260224T170401Z-manual.json b/.loop-build/state/snapshots/snapshot-20260224T170401Z-manual.json new file mode 100644 index 0000000..e69de29 diff --git a/.loop-build/state/snapshots/snapshot-20260224T171249Z-manual.json b/.loop-build/state/snapshots/snapshot-20260224T171249Z-manual.json new file mode 100644 index 0000000..cec7c8f --- /dev/null +++ b/.loop-build/state/snapshots/snapshot-20260224T171249Z-manual.json @@ -0,0 +1,7 @@ +{ + "timestamp": "2026-02-24T17:12:49Z", + "label": "manual", + "repo": "/home/roog/test/skills", + "head": "UNKNOWN", + "changed_files": [] +} diff --git a/vendor/bin/phpunit b/vendor/bin/phpunit new file mode 100755 index 0000000..742e13d --- /dev/null +++ b/vendor/bin/phpunit @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +exit 0