Files
2026-07-13 12:09:03 +08:00

239 lines
7.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""Scaffold the Agent Workbench pack into a target repository.
Usage:
python3 scripts/scaffold_workbench.py <target_dir> [options]
Options:
--force Overwrite existing AGENTS.md / docs / schemas / scripts.
--minimal Skip docs/ (only AGENTS.md, schemas/, scripts/, VERSION).
--dry-run Print what would happen without writing.
--no-seed Skip seeding starter task_board.json + agent_state.json.
What it installs:
AGENTS.md — root contract for the builder agent
docs/ — agent rules, reviewer rubric, handoff, reliability
schemas/ — JSON Schemas for state + task board + scope
scripts/ — init, run_with_feedback, verify, generate_handoff
task_board.json (seeded) — one todo example task
agent_state.json (seeded) — fresh state record at schema_version 1
.workbench-version — pinned pack version
The pack source is read from this repo at:
phases/14-agent-engineering/42-agent-workbench-capstone/outputs/agent-workbench-pack/
"""
from __future__ import annotations
import argparse
import json
import shutil
import sys
from dataclasses import dataclass
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
PACK_DIR = (
ROOT
/ "phases"
/ "14-agent-engineering"
/ "42-agent-workbench-capstone"
/ "outputs"
/ "agent-workbench-pack"
)
REQUIRED_PACK_ENTRIES = ("AGENTS.md", "VERSION", "docs", "schemas", "scripts")
TOP_LEVEL_FILES = ("AGENTS.md",)
TOP_LEVEL_DIRS = ("docs", "schemas", "scripts")
MINIMAL_SKIP_DIRS = ("docs",)
@dataclass
class Action:
kind: str
source: Path | None
target: Path
note: str = ""
def describe(self, target_root: Path) -> str:
rel = self.target.relative_to(target_root)
return f" [{self.kind}] {rel}{(' — ' + self.note) if self.note else ''}"
def validate_pack(pack_dir: Path) -> list[str]:
errors: list[str] = []
if not pack_dir.is_dir():
return [f"pack source not found: {pack_dir}"]
for entry in REQUIRED_PACK_ENTRIES:
if not (pack_dir / entry).exists():
errors.append(f"pack missing required entry: {entry}")
return errors
def plan_copies(target: Path, minimal: bool) -> list[Action]:
actions: list[Action] = []
skip_dirs = set(MINIMAL_SKIP_DIRS) if minimal else set()
for name in TOP_LEVEL_FILES:
actions.append(Action("file", PACK_DIR / name, target / name))
for name in TOP_LEVEL_DIRS:
if name in skip_dirs:
actions.append(Action("skip", None, target / name, "minimal mode"))
continue
actions.append(Action("tree", PACK_DIR / name, target / name))
actions.append(
Action(
"version",
PACK_DIR / "VERSION",
target / ".workbench-version",
)
)
return actions
def detect_collisions(target: Path, actions: list[Action]) -> list[Path]:
collisions: list[Path] = []
for action in actions:
if action.kind == "skip":
continue
if action.target.exists():
collisions.append(action.target)
return collisions
def apply_action(action: Action) -> None:
if action.kind == "skip":
return
if action.kind == "file":
action.target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(action.source, action.target)
return
if action.kind == "tree":
shutil.copytree(action.source, action.target, dirs_exist_ok=True)
return
if action.kind == "version":
action.target.parent.mkdir(parents=True, exist_ok=True)
version = action.source.read_text(encoding="utf-8").strip()
action.target.write_text(version + "\n", encoding="utf-8")
return
raise ValueError(f"unknown action kind: {action.kind}")
def seed_task_board(target: Path) -> bool:
path = target / "task_board.json"
if path.exists():
return False
seed = [
{
"id": "T-001",
"goal": "First task. Replace with the real one before the agent starts.",
"owner": "builder",
"acceptance": [
"code change lands",
"tests pass",
"reviewer sign-off recorded",
],
"status": "todo",
}
]
path.write_text(
json.dumps(seed, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
return True
def seed_agent_state(target: Path) -> bool:
path = target / "agent_state.json"
if path.exists():
return False
seed = {
"schema_version": 1,
"active_task_id": None,
"touched_files": [],
"assumptions": [],
"blockers": [],
"next_action": "read AGENTS.md, pick a task from task_board.json, run scripts/init_agent.py",
}
path.write_text(
json.dumps(seed, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
return True
def render_next_steps(target: Path, pack_version: str) -> str:
rel = target.resolve()
lines = [
"",
f"Workbench pack v{pack_version} scaffolded into {rel}",
"",
"Next steps:",
" 1. Edit task_board.json. Replace T-001 with the real task.",
" 2. Edit AGENTS.md. Set project-specific build cmd, test cmd, deny rules.",
" 3. Run scripts/init_agent.py to capture environment probes.",
" 4. Hand AGENTS.md + task_board.json to the agent. Iterate.",
"",
]
return "\n".join(lines)
def main(argv: list[str]) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("target_dir", type=Path, help="directory to scaffold into")
parser.add_argument("--force", action="store_true", help="overwrite existing files")
parser.add_argument("--minimal", action="store_true", help="skip docs/")
parser.add_argument("--dry-run", action="store_true", help="preview without writing")
parser.add_argument(
"--no-seed",
action="store_true",
help="do not seed task_board.json / agent_state.json",
)
args = parser.parse_args(argv)
errors = validate_pack(PACK_DIR)
if errors:
for e in errors:
sys.stderr.write(f"error: {e}\n")
return 2
target = args.target_dir
if not target.exists():
if args.dry_run:
sys.stdout.write(f"would create target dir: {target}\n")
else:
target.mkdir(parents=True, exist_ok=True)
actions = plan_copies(target, args.minimal)
collisions = detect_collisions(target, actions)
if collisions and not args.force and not args.dry_run:
sys.stderr.write("error: target already contains:\n")
for c in collisions:
sys.stderr.write(f" {c}\n")
sys.stderr.write("pass --force to overwrite\n")
return 1
pack_version = (PACK_DIR / "VERSION").read_text(encoding="utf-8").strip()
if args.dry_run:
sys.stdout.write(f"dry run — pack v{pack_version}\n")
for action in actions:
sys.stdout.write(action.describe(target) + "\n")
if not args.no_seed:
sys.stdout.write(" [seed] task_board.json (if absent)\n")
sys.stdout.write(" [seed] agent_state.json (if absent)\n")
return 0
for action in actions:
apply_action(action)
if not args.no_seed:
seed_task_board(target)
seed_agent_state(target)
sys.stdout.write(render_next_steps(target, pack_version))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))