Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions RESEARCH.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Last reviewed: 2026-08-04.
- PR #54 (Apply consolidated template-rule backlog, #44/#46/#47): shipped 2026-06-14. Observed effect: pending — needs N cycles of data. Sharpens the external-anchor definition (#44: UNVERIFIED handling when the anchor can't run; #46: green CI is not verification when CI's scope excludes the changed files) and grounds the Tests-fix rubric item empirically (#47: stash-and-run FAIL→PASS, not scored from reasoning alone) — directly targeting the self-preference failure mode this finding's evidence documents for judgment-based rubric scoring.
- PR #57 (Fix documentation drift across the three sources of truth): shipped 2026-07-25. Observed effect: pending — needs N cycles of data. Restores `{{EXTERNAL_SIGNAL_LABEL}}` in the Phase 1 carried-over-PR bullet, where PR #54 had reintroduced a hardcoded "CI must be green first" after PR #28 generalized the anchor. An anchor named `CI` in a repo that has no CI is an anchor the loop cannot check, which degrades the self-review to exactly the unanchored critique this finding shows is neutral-to-harmful.
- PR #67 (Add Tests-fix CI-revert fallback ladder): shipped 2026-07-26. Observed effect: pending — needs N cycles of data. Closes a gap left by PR #54: the Tests-fix rubric item required a local `git stash` experiment with no substitute when the local anchor can't run, even though the same PR's UNVERIFIED handling fully anticipates that case. Without a defined fallback, Tests-fix silently degraded back to unanchored judgment on exactly the repos this finding is about — confirmed twice, in two separate PRs opened by a generated child skill running against a repo whose local test anchor could not be run.
- PR #121 (Define the structurally-red-anchor path and require the anchor to have executed tests): shipped 2026-08-16. Observed effect: pending — needs N cycles of data. Extends the same argument as PR #54's "green CI is not verification when CI's scope excludes the changed files" to two further ways an anchor can carry no information: a required job that has *never* succeeded on the base branch (observed on a repo whose CI jobs have failed on empty credentials since February 2026, so every cycle terminated in a hand-off regardless of quality), and a build tool that reports success having executed zero tests (`:test NO-SOURCE` still prints `BUILD SUCCESSFUL`). Both were previously read as ordinary signals — the first as a blocking red, the second as a green — which is unanchored critique wearing an anchor's clothes.

---

Expand Down
26 changes: 22 additions & 4 deletions create-dev-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,14 @@ git log --oneline -10

Do not open a new PR while one is already open against the same repo.

**Establish whether a merge path exists.** Before selecting work, check whether {{EXTERNAL_SIGNAL_LABEL}} is *capable* of being green here — a required check that has never passed on `{{DEFAULT_BRANCH}}` is a different condition from one that is red on this branch, and only the first makes every cycle terminate in a hand-off regardless of the change's quality:
\`\`\`bash
gh run list --branch {{DEFAULT_BRANCH}} --limit 20
# then, for any workflow with no success above:
gh run list --workflow <workflow-file> --branch {{DEFAULT_BRANCH}} --limit 50 --json conclusion
\`\`\`
If a required workflow shows no successful run in its whole recorded history on `{{DEFAULT_BRANCH}}`, record the repo as **structurally red** for that workflow, together with the cause (absent credentials, an unset secret, an unavailable license) and any issue tracking it. This is a fact about the repository, not about the change — establishing it once at triage is what lets Phase 4 assess per-job signal and Phase 8 name the blocking condition, instead of rediscovering it at the merge gate every cycle.

**Close stale-open issues first.** Check whether any open issues were resolved by
recent PRs but not yet closed. Cross-reference `git log` against open issue titles:
\`\`\`bash
Expand Down Expand Up @@ -270,6 +278,7 @@ Universal rules:
- **Match sibling structure.** Before creating a new file in a directory, read the section headers / structure of every existing file in the same directory and conform to the established pattern. Example: `grep "^##" path/to/dir/*.md` for docs, or read 2–3 neighboring source files for code.
- **Rename siblings together.** When renaming a heading or identifier that is part of a parallel pair or series (e.g. `Required X` / `Optional X`, `loadConfig` / `saveConfig`), scan for the siblings and rename them in the same commit.
- **Scratch-file handling in a sandboxed harness.** When a step needs a scratch file for inspection or transformation (not a project source file — e.g. redirecting `git show` output for byte-level inspection, or a throwaway helper script), prefer the `Write` tool over `> file` shell redirection, and prefer `python3 -c "import os; os.remove(path)"` over `rm` to clean it up afterward. Some harness sandboxes statically block plain `>` redirection and `rm` outright — even for files the same session just created inside the working directory — while `Write` and `os.remove` are not pattern-matched the same way. The same blocking applies to scratch **directory trees** (e.g. an isolated tool-home created to work around a lock-file issue): use `python3 -c "import shutil; shutil.rmtree(path)"` instead of `rm -rf`. Creating such a tree at all may be unavailable — some sandboxes block `mkdir` (and therefore `git clone` into a fresh subdirectory) even for paths *inside* the allowed working directory — so keep scratch work to individual files written into the existing tree rather than a new directory.
- **Name every scratch file uniquely per repo and per cycle.** Use `<repo>-<branch>-pr-body.md`, never a generic `pr-body.md` — the harness scratchpad is shared between concurrently running dev loops, so a colliding name is overwritten silently, with no error to notice and another repo's content left in the file. Write the file immediately before the command that consumes it, and treat it as consumed once used rather than as durable state; where a body has to survive a wait, re-verify what was actually published (`gh pr view <number> --json body`) instead of trusting the file.
- **Avoid command substitution in Bash tool calls.** Some harnesses' command classifiers reject `$(...)` outright, so a prescribed `--body "$(cat <<'EOF' ... EOF)"` or `git commit -m "$(cat <<'EOF' ... EOF)"` can fail before ever reaching the shell. Compose long bodies (PR comments, commit messages, issue bodies) with the `Write` tool to a scratch file and pass them by file instead — `--body-file` (`gh pr comment`, `gh issue create`) or `-F` (`git commit`) — as done in Phase 4 step 5 and Phase 6. Likewise prefer separate `grep` invocations over `\|` alternation, which some classifiers flag as an expansion.

Write or update tests for every change (and see Stage B in Phase 2 when the *whole cycle* is dedicated to expanding coverage of existing functionality):
Expand All @@ -285,6 +294,8 @@ Verify the build is clean:

Fix all failures before proceeding. Never skip tests or bypass hooks.

**Confirm the anchor actually executed tests.** A zero exit status is not evidence that anything ran: `./gradlew test` prints `BUILD SUCCESSFUL` on `:test NO-SOURCE`, and the pytest/npm equivalents exit 0 on "no tests ran" or an empty match. Read the output for an executed-test count before recording a PASS. Where zero tests ran, the green verifies nothing and must never be reported as "tests pass" — say so plainly and fall back to whatever the `{{TEST_CMD}}` substitution names as the real gate for this repo.

**Formatting is scoped to changed files.** Run any formatter against **only the files you changed** (e.g. `black <changed files>` / `autoflake --in-place <changed files>`), not a tree-wide script — a whole-repo reformat pulls unrelated files into the PR and violates the Scope rule below. If you do run a tree-wide formatter, only `git add` files in this PR's scope and `git checkout --` any unrelated files it touched. Pre-existing formatting drift lands as its own formatting-only sweep, never smuggled into a feature/fix PR. (Such scripts may not be executable in the checkout — invoke as `bash format.sh`.)

**Git-staging hygiene.** Stage by name — **never** `git add -A` or `git add .`. The harness writes `.claude/` state (e.g. `scheduled_tasks.lock`) into the tree while the loop runs, and the project `.gitignore` may not cover it; a blanket add leaks harness state into the project repo (and the classifier blocks the `git rm --cached` cleanup as scope-escalation). After staging, run `git status` and confirm no `.claude/` entries are staged before committing.
Expand All @@ -298,7 +309,7 @@ Count the soft ceiling against **non-test net LOC**. If non-test changes exceed
**Implementation summary (re-read at the start of Phase 4).** Before pushing, write a compressed implementation summary:
- **Files actually modified:** path, path, ...
- **Commit summary:** one line per commit
- **Test/validation result:** PASS / FAIL — name the command that produced the verdict
- **Test/validation result:** PASS / FAIL — name the command that produced the verdict and how many tests it executed
- **Open carryovers:** anything in scope that wasn't done and why (becomes input for Phase 9)

---
Expand All @@ -309,7 +320,8 @@ Count the soft ceiling against **non-test net LOC**. If non-test changes exceed

\`\`\`bash
git push -u origin {{BRANCH_PREFIX}}/<short-description>
gh pr create --title "..." --body "..."
# compose the body with the Write tool to a uniquely-named scratch file first (Phase 3 scratch-file rule)
gh pr create --title "..." --body-file <scratch-file-path>
\`\`\`

PR body must include:
Expand Down Expand Up @@ -338,6 +350,8 @@ Perform a self-review. This step is anchored on external signals ({{EXTERNAL_SIG

**Green CI is not verification when CI's scope excludes the changed files.** If this PR changes code the CI structurally cannot execute (platform-specific scripts — `.ps1`/`.bat`/`.command`, installer configs, OS-gated shell paths), a green run does **not** verify those files. State the no-automated-coverage gap in the PR body (and CHANGELOG), lean harder on adversarial hand-review of that code, and recommend a real-platform smoke test before merge — never let a green run on the *other* language imply the script was tested.

**A structurally-red anchor is not a verdict on this PR.** Where Phase 1 recorded a required job that has never succeeded on `{{DEFAULT_BRANCH}}`, do not read the aggregate red as one blocking signal — assess each failing job separately. A job carries **no signal** for this PR when all three hold: its failure reproduces identically on `{{DEFAULT_BRANCH}}`, its cause is environmental (absent credentials, an unset secret, an unavailable license) rather than anything in the diff, and the diff touches nothing that job would have exercised. Quote the base-branch failure and the failing step to establish each. A no-signal job is not counted as blocking — but it is not verification either: score the {{EXTERNAL_SIGNAL_LABEL}} rubric item from the jobs that *do* carry signal, mark the rest **UNVERIFIED** under the same scope gate as an anchor that cannot run, and state both in the self-review and PR body. A job red for any other reason — including one whose failure the diff could plausibly have caused — is an ordinary failure and blocks as usual.

2. **Read the full diff:**
\`\`\`bash
gh pr diff <number>
Expand Down Expand Up @@ -371,7 +385,8 @@ Perform a self-review. This step is anchored on external signals ({{EXTERNAL_SIG

5. **Post the self-review as a plain PR comment** — not a formal review API call. The auto-mode classifier blocks `POST /pulls/<n>/reviews` (it misrepresents independent review); a plain comment keeps the audit trail without implying reviewer independence. Compose the body with the `Write` tool to a scratch file and pass it by file — **avoid command substitution** (`--body "$(cat <<'EOF' ... EOF)"`), which some harnesses' command classifiers reject outright:
\`\`\`bash
# write the body (below) to a scratch file with the Write tool first, e.g. <repo-root>/.self-review-scratch.md:
# write the body (below) to a scratch file with the Write tool first, named uniquely per the
# Phase 3 scratch-file rule, e.g. <repo-root>/.self-review-<branch>.md:
# Self-review rubric:
# - Scope: PASS — <justification>
# - Tests-new: PASS — <justification>
Expand Down Expand Up @@ -471,6 +486,8 @@ git checkout {{DEFAULT_BRANCH}} && git pull

**Review-ready hand-off is a valid terminal state — not a failed cycle.** On a repo with no autonomous merge path (branch-protected `{{DEFAULT_BRANCH}}` requiring an approving review, and/or repository auto-merge disabled), a clean, anchor-green PR's true terminal state is *open + self-review posted + awaiting human approval*. Expect `gh pr merge` → `the base branch policy prohibits the merge` and `--auto` → `Auto merge is not allowed for this repository`. **Never use `--admin`** to bypass a deliberately-configured human gate, and a later self-audit must not score "could not auto-merge" as a failure. A do-not-auto-merge path match blocks **autonomous** merge only: if the human codeowner explicitly authorizes the merge after being shown which protected path matched, that authorization satisfies the hold — proceed (still run the Phase 7 docs sweep + the regression gate above first), and state in the merge report which protected path was overridden and by whose authorization.

**A structurally-red anchor is a named blocking condition, not an ordinary hand-off.** Where Phase 1 recorded a required workflow that has never succeeded on `{{DEFAULT_BRANCH}}`, the merge gate cannot be satisfied by any change this loop could write, so the hand-off must say that rather than reporting "awaiting review" — the two have entirely different remedies. State which jobs were assessed as carrying no signal for this diff and on what grounds, which jobs carried signal and were green, and the issue tracking the structural failure (file one on the project repo if none exists, and link it). The point is that the owner sees the single action that would unblock every future cycle, instead of a hand-off that reads like a queue.

**Worktree note:** under a `git worktree` workflow (main checkout kept on `{{DEFAULT_BRANCH}}`), `gh pr merge --delete-branch`'s *local* branch deletion can fail with `fatal: '{{DEFAULT_BRANCH}}' is already checked out` — the merge and remote-branch deletion still succeed. Verify the real state with `gh pr view <number> --json state` and delete the remote branch separately rather than treating the local error as a failed merge.

Verify issues auto-closed. Close any that did not:
Expand Down Expand Up @@ -539,9 +556,10 @@ Return to Phase 1.
**Tests fail after addressing a comment:** same rule.
**{{EXTERNAL_SIGNAL_LABEL}} fails on the PR (Phase 4 or later):** treat the failing anchor as the highest-priority external signal — fix the underlying cause locally, push, and re-confirm before continuing the rubric or addressing other comments. Do not start or re-run the self-review rubric while {{EXTERNAL_SIGNAL_LABEL}} is failing.
**The external anchor cannot run (tool/interpreter absent or broken in the sandbox, or the sandbox restricts filesystem/tool access to only the target repo's own working directory — common in gardener-dispatched sessions, and possibly extending to blocking creation of a *new* directory inside that allowed root, which rules out cloning a fixture locally as a workaround):** do not claim it green and do not iterate on the sandbox. Flag **UNVERIFIED** and gate on scope (Phase 4): anchor-relevant files changed → mark UNVERIFIED + do-not-auto-merge + hand to CI/human (prefer the CI check on the exact head SHA); no anchor-relevant files (docs-only) → record UNVERIFIED-not-applicable and continue, stating it in the PR body.
**{{EXTERNAL_SIGNAL_LABEL}} has never been green on `{{DEFAULT_BRANCH}}` (structurally red — e.g. a required job fails on absent credentials or an unset secret):** this is not the same as red-on-this-branch, and no change the loop can write will satisfy the gate. Record it at triage (Phase 1), assess per-job signal in Phase 4 so a job carrying no signal for the diff is neither counted as blocking nor mistaken for verification, and hand off in Phase 8 naming the blocking condition and the issue tracking it rather than reporting an ordinary "awaiting review".
**An issue requires a harness-blocked operation (editing `CLAUDE.md`/agent-loaded config, registering an external submodule):** recognize it at triage (Phase 1) — surface to the user for explicit authorization rather than attempting it mid-cycle; never remove a path while `CLAUDE.md` still references it.
**Autonomous multi-cycle batch (`/<loop> until …`):** skip/cap the Phase-5 human-review wait (rubric + green anchor is the gate); still hand off do-not-auto-merge/charter PRs. Stop when only blocked, charter-gated, or too-large work remains, or a cycle yields no scoped work.
**A concurrent session holds the tree or an open PR:** adopt its PR (bring current with `{{DEFAULT_BRANCH}}`, re-run the anchor, review, merge if green) rather than doing nothing; work in a `git worktree` to avoid colliding, and treat a harmless local `--delete-branch` failure as success once `gh pr view --json state` confirms the merge. **Put the worktree inside the repository checkout** (e.g. `.worktrees/<branch>`, added to `.git/info/exclude` so an untracked directory doesn't pollute `git status`), never in `/tmp` or anywhere else outside it: a path-restricted sandbox refuses every read and edit outside the checkout, so a worktree placed there locks the session out of its own working tree one file operation at a time. **In a headless dispatch (e.g. `gardener tend`), do not create a worktree at all** — the run already owns a clone dedicated to it, so there is no concurrent session to collide with, and the checkout can be used directly.
**A concurrent session holds the tree or an open PR:** adopt its PR (bring current with `{{DEFAULT_BRANCH}}`, re-run the anchor, review, merge if green) rather than doing nothing; work in a `git worktree` to avoid colliding, and treat a harmless local `--delete-branch` failure as success once `gh pr view --json state` confirms the merge. **Put the worktree inside the repository checkout** (e.g. `.worktrees/<branch>`, added to `.git/info/exclude` so an untracked directory doesn't pollute `git status`), never in `/tmp` or anywhere else outside it: a path-restricted sandbox refuses every read and edit outside the checkout, so a worktree placed there locks the session out of its own working tree one file operation at a time. **In a headless dispatch (e.g. `gardener tend`), do not create a worktree at all** — the run already owns a clone dedicated to it, so there is no concurrent session to collide with, and the checkout can be used directly. The harness scratchpad is a third resource sibling loops contend for, alongside the shared tree and the shared `origin/{{DEFAULT_BRANCH}}`, and unlike those two it fails silently: a scratch file named generically is overwritten by the other loop with no error, so name scratch files uniquely per repo and per cycle (Phase 3) and re-verify anything published from one.
**A test fails intermittently (suspected flake):** re-run the test command once. If the same test fails again, treat it as a real failure and investigate. If it passes on the second run, note the flake in the PR body and proceed — do not suppress or `@Ignore` a test without understanding why it is flaky.
**A review comment is a false positive:** reply with evidence, do not apply the change.
**A `gh` command fails with a transient network/transport error (`http2: client conn could not be established`, `unexpected EOF`):** retry once or twice before treating it as blocked — do not treat the first failure as terminal and silently drop a self-review, commit, or comment.
Expand Down
Loading