chore: import upstream snapshot with attribution
This commit is contained in:
Executable
+238
@@ -0,0 +1,238 @@
|
||||
#!/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:]))
|
||||
Reference in New Issue
Block a user