skills/sssf/cookbooks/create_adw.md
INDigitalStudio 2cc766aabe Add sssf skill, installable via the skills CLI
Port the sssf skill from ~/.agents/skills/sssf into this repo so it can be
distributed and installed with the skills CLI (skills add INDigitalStudio/skills
--skill sssf).

- Copy the skill (SKILL.md, cookbooks, references, scripts, templates, and the
  visualizer app source) into sssf/.
- Gitignore build/runtime artifacts: the visualizer's node_modules/ and dist/,
  Python bytecode, and the machine-specific repos.json.
- Make the skill location-independent: install.py now stamps the skill's real
  path into the stamped justfile's skill_dir (replacing the hardcoded
  ~/.agents/skills/sssf), so 'just obs' finds the visualizer wherever the CLI
  installed the skill.
- Update cookbooks to use <skill>/scripts/... instead of the hardcoded path,
  and document the skills CLI install command.
- Update the repo README with install instructions.
2026-08-09 21:00:28 +00:00

8.9 KiB

Create ADW

Compose a new ADW script — a thin, deterministic Python workflow over agents already in the config. Design the chain first, then generate or hand-write it.

Step 1 — Design the chain

Answer four questions, in order:

  1. What agents, in what order? Pick from the roster (adws/adw_sssf_config/sssf.config.yaml). The starter six cover most chains:
Agent Use when Output type Typical gates
scout you need to FIND something first — read-only recon ScoutOutput artifacts_exist
planner the work needs a plan before code changes PlanOutput artifacts_exist, files_non_empty
builder code must change BuildOutput diff_matches_claims
reviewer the change must be confirmed to BE what was asked for ReviewOutput artifacts_exist, verdict_consistent
(no tester) verifying that it RUNS is a kind="code" phase over quality.py, not an agent QualityResult → as_envelope the exit code is the check
documenter finished work needs a write-up (runs after a build, off the diff) DocumentOutput artifacts_exist, files_non_empty
any agent, generic ask one-off prompt, no special shape GenericOutput as needed

A new kind of agent needs a config entry + prompt pair + output type first — see update_config.md.

The suite and the reviewer answer different questions. "Does it run" is a test, and code can ask that. "Is this the thing that was asked for" is a review, and only an agent can. A green suite over a feature nobody requested is still a failed request, and neither one covers for the other.

  1. Where does code act? Git branch/commit, migrations, deploys each get their own kind="code" phase — never buried inside an agent phase.

    Running the suite is one of these — there is no tester agent. The command is written down in quality.py, so a kind="code" phase runs it (quality.run_tests(run) → quality.as_envelope(result, "tests") back into the builder) and the bounded repair loop is unchanged. An agent rediscovering bun test on every run buys nothing a subprocess does not already know. Capturing what changed is one of these: changes.capture(run, ChangeCapture(base="main")) diffs the working tree against a resolved base, writes context_handoff/changes.diff, and changes.as_envelope(...) hands it to the next agent. A diff is two git commands, not a judgement call.

  2. Does anything loop? Test-fix cycles are bounded fix loops (see update_adw.md), not phase retries.

  3. What does each call need to prove? Pick gates per call from gates.py: artifacts_exist, files_non_empty, json_parses, diff_matches_claims, tests_pass("cmd") — or an inline one-off.

Step 2 — Ownership rules (the swim lanes depend on these)

  • kind="agent" → owner MUST be an agent name from the config — it selects the harness (model, thinking, tools, prompts) AND the lane. ph.call() runs whoever owns the phase.
  • kind="engineer" → owner=run.engineer. Every ADW opens with the engineer request phase — it is the system input record.
  • kind="code" → owner is a short actor label ("git", "db"); all code phases share the code lane.
  • Phase name must be unique within the run (plan, build, test_1, fix_1, …) — the UI keys blocks on it.
  • description is required and must earn its place. The name identifies the phase; the description explains it — what this phase does and why, in one sentence. It rides the phase_start event and is the only line of intent the trace, the console, and the phase block ever show. PhaseParams raises at construction on a blank description or one that merely restates the name (commit_plan: "Commit the plan"), so the rule fails before the phase opens rather than leaving an unreadable run in the db. Write "Put the spec on record before any code exists to blur it" instead.
  • retries=N on an agent phase = extra gate-correction rounds re-sent into the same session (pi's --session-id creates-or-continues, so context stays intact). Code-phase re-execution is not implemented in v1.

Step 3 — Generate or write it

uv run <skill>/scripts/make_adw.py --name review_docs --agents scout,builder

<skill> is the directory this skill was installed into (e.g. ~/.agents/skills/sssf or a repo's .claude/skills/sssf).

Writes adws/adw_review_docs.py: one agent phase per name, chained by previous=, starter agents mapped to their output types, unknown agents to GenericOutput. It does NOT create config entries or prompt files — do that first (update_config.md), or agents.validate() will stop the run and tell you what's missing.

The canonical skeleton

Every adw_*.py, generated or hand-written, is a uv single-file script with this shape:

#!/usr/bin/env -S uv run
# /// script
# dependencies = ["pydantic", "python-dotenv", "pyyaml", "rich"]
# ///
"""ADW Plan Build — plan the request, then implement the plan."""

import argparse
import sys

from adw_modules import agents, gates, git_helper, session, utils
from adw_modules.data_types import AgentCall, BuildOutput, PhaseParams, PlanOutput

REQUIRED_AGENTS = ["planner", "builder"]        # names, never models


def main(prompt: str, config: str = "adws/adw_sssf_config/sssf.config.yaml", adw_id: str | None = None) -> int:
    cfg = agents.load_config(config)            # 1. point to config
    agents.validate(cfg, REQUIRED_AGENTS)       # 2. fail fast — nothing spawns on a half-valid config
    run = session.ensure(cfg, adw_id)           # 3. pin-or-create the session → the Run object

    with run.phase(PhaseParams(name="request", kind="engineer", owner=run.engineer,
                               description="Capture the incoming ask")) as ph:
        ph.log(input=prompt)

    with run.phase(PhaseParams(name="plan", kind="agent", owner="planner",
                               description="Turn the request into an implementable plan")) as ph:
        plan = ph.call(AgentCall(output_type=PlanOutput, prompt=prompt,
                                 gates=[gates.artifacts_exist, gates.files_non_empty]))

    with run.phase(PhaseParams(name="build", kind="agent", owner="builder", retries=1,
                               description="Implement the plan exactly")) as ph:
        build = ph.call(AgentCall(output_type=BuildOutput, prompt=prompt, previous=plan,
                                  gates=[gates.diff_matches_claims]))

    with run.phase(PhaseParams(name="commit", kind="code", owner="git",
                               description="Commit the working tree")) as ph:
        message = build.commit_message or f"sssf({run.adw_id}): {build.summary}"
        ph.log(sha=git_helper.commit_all(message), message=message)

    return run.finish()


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("prompt", help="inline text or a path to a prompt file")
    parser.add_argument("--config", default="adws/adw_sssf_config/sssf.config.yaml")
    parser.add_argument("--adw-id", default=None, help="join or pin an existing session")
    args = parser.parse_args()
    sys.exit(main(utils.resolve_prompt(args.prompt), args.config, args.adw_id))

Non-negotiables

  • REQUIRED_AGENTS + agents.validate() — declare every agent name the script uses and validate before the first phase.
  • Every agent call declares a concrete output type from data_types.py. No untyped handoffs.
  • previous= carries the chain — the upstream envelope lands in the next agent's user.md as {{previous_envelope}}; bulky context moves through context_handoff/ files the envelope references.
  • The engineer request phase comes first, always.
  • Four-param rule — run.phase() and ph.call() each take exactly one object; new helpers with >4 params get a data type.
  • Stay thin — sequencing and acceptance only; real logic goes in adw_modules/ (update_modules.md).
  • Committing is a code phase, and it needs a fallback. PlanOutput, BuildOutput, and DocumentOutput each carry a commit_message the agent writes for its own work product — the spec, the code, the write-up. It defaults to empty, so always envelope.commit_message or <fallback>, and commit each product with the message of the agent that made it (adw_simple_sdlc.py commits three times and never crosses them). git_helper.commit_all(message) stages everything, commits, and returns the short sha; it raises a clear error when the cwd isn't a git repo or nothing changed, and that raise fails the phase.

Before you ship it

  1. uv run adws/adw_<name>.py "a tiny real request" — watch it go green end to end.
  2. Check the trace: sqlite3 adws/adw_data/sssf.db "select seq,name,kind,owner,status from phases where adw_id='<id>' order by seq;"
  3. Read the final envelope.json — is the output type earning its fields, or should it be sharper?