Skip to content
Open
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
6 changes: 3 additions & 3 deletions .agent/commands/bootstrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ Read the project’s existing AGENTS.md and `.agent/config.toml` if present; fol
Input: project repository and its architecture, README and backlog.

1. Inspect existing instructions, files, Git status and verification tools. Preserve user changes.
2. Copy or merge `.agent/commands/`, `.agent/config.example.toml`, `scripts/round.py`, `templates/`
and `SPEC.md`, plus `examples/one-round.md` for its linked walkthrough. Optionally install `.agents/skills/reconcile-loop/`, the `.claude/commands/`
wrappers and `.claude/agents/` subagents. They reference the same procedures. Preserve these relative paths and retain the
2. Run the pack's `scripts/install.py` against the project root, adding `--claude`/`--codex` for
the runner entry points, or copy the same paths manually. Merge every `*.pack-new` file it
reports into the project's version, then delete it. Preserve relative paths and retain the
pack’s MIT notice with copied materials; do not replace an existing project’s license.
3. Merge [the project contract](../../templates/AGENTS.md) into the target AGENTS.md; do not copy
the pattern library's contributor contract. Adapt `.agent/config.example.toml` into
Expand Down
37 changes: 35 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,28 @@ The architecture defines the target; one backlog records current status and depe
bound execution, PRs carry evidence, and a fresh planner reconstructs state without previous chats.
Only the next executable frontier needs detailed planning.

## Quick start

For people, from a clean checkout of your project:

```sh
git clone https://github.com/TKontu/Reconcile-loop ../Reconcile-loop
python3 ../Reconcile-loop/scripts/install.py . --claude --codex
```

Then ask your agent to follow [bootstrap](.agent/commands/bootstrap.md) (`/bootstrap` in Claude Code)
and commit the result. Each round runs `takeoff → plan-round → execute-task (one per executor) →
review-round → merge-round → reconcile`. To update, pull the pack, rerun the installer and merge any
`*.pack-new` files.

For agents, paste this prompt:

> Adopt Reconcile-loop in this repository. Clone https://github.com/TKontu/Reconcile-loop into a
> temporary directory, run its `scripts/install.py` against this repository root from a clean
> checkout (add `--claude` and/or `--codex` for the runners in use), then follow
> `.agent/commands/bootstrap.md`. Stop with a ready first item or named setup gaps; do not dispatch,
> publish or merge.

## Adopt it

Bring your own architecture, README and backlog. Ask an agent to follow
Expand All @@ -32,8 +54,19 @@ The pack contains:
- A standard-library [round CLI](scripts/round.py) for local records and packet/result checks.
- One [worked round](examples/one-round.md).

Copy/merge `.agent/`, `scripts/round.py`, `templates/`, `SPEC.md` and `examples/one-round.md`,
preserving their relative paths.
Install from a clone or release of this repository into a clean checkout of your project:

```sh
python3 path/to/Reconcile-loop/scripts/install.py path/to/project --claude --codex
```

It copies `.agent/`, `scripts/round.py`, `templates/`, `SPEC.md`, `examples/one-round.md` and the
MIT notice (`.agent/LICENSE`), preserving relative paths, and ignores `.agent-runs/` and HANDOFF.md.
The runner flags are optional and remembered. `.agent/pack.lock` records the pack version and
installed file hashes. Rerun the same command from a newer pack to update: unmodified files are
replaced, while locally edited or pre-existing files are kept with the new version beside them as
`*.pack-new` to merge and delete. It never writes your README, AGENTS.md or `.agent/config.toml`.
Use `--dry-run` to preview. Copying the same paths manually remains supported.
Keep your project's README and merge its operating contract from the template. Optional entry points
are one `.agents/skills/reconcile-loop/` skill, ten `.claude/commands/` wrappers and three
`.claude/agents/` subagents (review lens, CI watcher, quick fixer). All use the same canonical
Expand Down
160 changes: 160 additions & 0 deletions scripts/install.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
"""Install or update the Reconcile-loop pack in a project, preserving local edits."""

import argparse
import hashlib
import json
import subprocess
import sys
from pathlib import Path

PACK = Path(__file__).resolve().parents[1]
LOCK = ".agent/pack.lock"
CORE = [
"SPEC.md",
".agent/commands/*.md",
".agent/config.example.toml",
"scripts/round.py",
"templates/*",
"examples/one-round.md",
]
RUNNERS = {
"claude": [".claude/commands/*.md", ".claude/agents/*.md"],
"codex": [".agents/skills/reconcile-loop/SKILL.md"],
}
RENAMED = {".agent/LICENSE": "LICENSE"}
IGNORED = [".agent-runs/", "HANDOFF.md"]


def digest(path):
return hashlib.sha256(path.read_bytes()).hexdigest()


def git(repo, *args):
try:
return subprocess.check_output(
["git", "-C", str(repo), *args], text=True, stderr=subprocess.PIPE
).strip()
except subprocess.CalledProcessError as error:
raise ValueError(error.stderr.strip() or f"git {args[0]} failed") from None


def version():
try:
return git(PACK, "describe", "--tags", "--always", "--dirty")
except (OSError, ValueError):
return "unknown"


def sources(runners):
"""Map installed relative path to pack source for the selected runners."""
files = {target: PACK / source for target, source in RENAMED.items()}
for pattern in CORE + [p for runner in runners for p in RUNNERS[runner]]:
for path in sorted(PACK.glob(pattern)):
if path.is_file():
files[path.relative_to(PACK).as_posix()] = path
return files


def plan(repo, files, locked):
"""Return (action, path) pairs; local edits and foreign files are never replaced."""
actions = []
for name, source in files.items():
target = repo / name
new = digest(source)
if not target.exists():
actions.append(("skip-deleted" if name in locked else "add", name))
elif digest(target) == new:
actions.append(("same", name))
elif locked.get(name) == digest(target):
actions.append(("update", name))
else:
actions.append(("conflict", name))
for name in sorted(set(locked) - set(files)):
target = repo / name
if target.exists() and digest(target) == locked[name]:
actions.append(("remove", name))
elif target.exists():
actions.append(("keep-removed", name))
return actions


def apply(repo, files, locked, actions):
recorded = {}
for action, name in actions:
target = repo / name
if action in {"add", "update"}:
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(files[name].read_bytes())
if action in {"add", "update", "same"}:
recorded[name] = digest(target)
elif action == "conflict":
target.with_name(target.name + ".pack-new").write_bytes(files[name].read_bytes())
if name in locked:
recorded[name] = locked[name]
elif action == "skip-deleted":
recorded[name] = locked[name]
elif action == "remove":
target.unlink()
return recorded


def ignore(repo):
path = repo / ".gitignore"
lines = path.read_text().splitlines() if path.exists() else []
missing = [entry for entry in IGNORED if entry not in lines]
if missing:
path.write_text("".join(f"{line}\n" for line in lines + missing))


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("target", type=Path, help="project repository root")
parser.add_argument("--claude", action="store_true", help="also install Claude Code files")
parser.add_argument("--codex", action="store_true", help="also install the .agents skill")
parser.add_argument("--dry-run", action="store_true", help="report actions only")
args = parser.parse_args()
try:
repo = args.target.resolve()
if repo == PACK:
raise ValueError("Target is the pack itself")
if Path(git(repo, "rev-parse", "--show-toplevel")).resolve() != repo:
raise ValueError("Target must be a repository root")
if not args.dry_run and git(repo, "status", "--porcelain"):
raise ValueError("Start from a clean checkout")
lock_path = repo / LOCK
previous = json.loads(lock_path.read_text()) if lock_path.exists() else {}
locked = previous.get("files", {})
runners = sorted(
set(previous.get("runners", []))
| {name for name in RUNNERS if getattr(args, name)}
)
files = sources(runners)
actions = plan(repo, files, locked)
for action, name in actions:
if action != "same":
print(f"{action}: {name}")
if args.dry_run:
return 0
recorded = apply(repo, files, locked, actions)
lock_path.parent.mkdir(parents=True, exist_ok=True)
lock_path.write_text(
json.dumps(
{"version": 1, "pack": version(), "runners": runners, "files": recorded},
indent=2,
sort_keys=True,
)
+ "\n"
)
ignore(repo)
conflicts = [name for action, name in actions if action == "conflict"]
if conflicts:
print(f"Merge {len(conflicts)} *.pack-new file(s), then delete them.")
print("Next: follow .agent/commands/bootstrap.md to configure the project.")
return 0
except (OSError, ValueError, json.JSONDecodeError) as error:
print(f"Cannot install: {error}", file=sys.stderr)
return 1


if __name__ == "__main__":
sys.exit(main())
Loading
Loading