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.
This commit is contained in:
parent
a42608f602
commit
2cc766aabe
98 changed files with 12508 additions and 0 deletions
183
sssf/templates/adws/adw_simple_sdlc.py
Normal file
183
sssf/templates/adws/adw_simple_sdlc.py
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
#!/usr/bin/env -S uv run
|
||||
# /// script
|
||||
# dependencies = ["pydantic", "python-dotenv", "pyyaml", "rich"]
|
||||
# ///
|
||||
"""ADW Simple SDLC — plan, build, test, review, document, committing as it goes.
|
||||
|
||||
Usage:
|
||||
uv run adws/adw_simple_sdlc.py "<prompt or path/to/prompt.md>" [--config adws/adw_sssf_config/sssf.config.yaml] [--adw-id a1b2c3d4]
|
||||
|
||||
Phases: engineer(request) -> planner -> git(commit_plan)
|
||||
-> builder -> code(test) [-> builder(fix) -> code(test) ... bounded]
|
||||
-> reviewer [-> builder(revise) -> reviewer ... bounded]
|
||||
-> code(retest, only if a revision changed code)
|
||||
-> git(commit_build) -> code(changes) -> documenter -> git(commit_docs)
|
||||
|
||||
Three commits, three work products, three authors. The plan, the code, and the
|
||||
write-up each land in their own commit, and each commit message is the words of
|
||||
the agent that produced it — `commit_message` on PlanOutput describes the spec,
|
||||
on BuildOutput the code, on DocumentOutput the write-up. No agent's sentence is
|
||||
ever reused for another agent's diff.
|
||||
|
||||
Testing is CODE, not an agent. `bun test` is a command, not a judgement call:
|
||||
an agent rediscovering it every run costs a million tokens to learn what a
|
||||
subprocess already knows. Failures travel back to the builder as an envelope,
|
||||
so the repair loop is unchanged — only the runner became free and repeatable.
|
||||
|
||||
Two different questions still get asked, in order. The suite asks "does it
|
||||
run"; the reviewer asks "is this what was asked for", against `plan.md` — and
|
||||
neither can answer the other's. A revision that closes a review finding
|
||||
re-enters the suite, so the tree that gets committed is the tree that was both
|
||||
tested and approved.
|
||||
|
||||
The code commit lands after verification, not straight after the build: fixes
|
||||
and revisions are part of the same work product, and red code has no business
|
||||
on the branch. A run that fails verification therefore leaves the plan
|
||||
committed and the working tree dirty — the spec is a real artifact either way,
|
||||
and the unfinished code stays where the engineer can see it.
|
||||
|
||||
The documenter measures against the commit this run STARTED from, not against
|
||||
`main`, because by then the run has moved `main` itself. That baseline is
|
||||
pinned before the first commit phase and printed in the request phase.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from adw_modules import agents, changes, gates, git_helper, quality, session, utils
|
||||
from adw_modules.data_types import (AgentCall, BuildOutput, ChangeCapture,
|
||||
DocumentOutput, PhaseParams, PlanOutput,
|
||||
ReviewOutput)
|
||||
|
||||
REQUIRED_AGENTS = ["planner", "builder", "reviewer", "documenter"]
|
||||
MAX_FIX_LOOPS = 3
|
||||
MAX_REVISION_LOOPS = 2
|
||||
|
||||
DOCUMENT_NOTES = ("Read diff_path in full before writing. Document only what the "
|
||||
"diff shows, then copy the write-up into app_docs/ as your task "
|
||||
"describes.")
|
||||
|
||||
|
||||
def main(prompt: str, config: str = "adws/adw_sssf_config/sssf.config.yaml", adw_id: str | None = None) -> int:
|
||||
cfg = agents.load_config(config)
|
||||
agents.validate(cfg, REQUIRED_AGENTS)
|
||||
run = session.ensure(cfg, adw_id)
|
||||
baseline = git_helper.rev("HEAD") # pinned before this run commits anything
|
||||
|
||||
def commit(ph, envelope) -> None:
|
||||
"""Commit what the preceding phase produced, in that agent's own words."""
|
||||
message = envelope.commit_message or f"sssf({run.adw_id}): {envelope.summary}"
|
||||
ph.log(sha=git_helper.commit_all(message), message=message)
|
||||
|
||||
def record(ph, result) -> None:
|
||||
"""Log a deterministic block's verdict — the same shape every ADW uses."""
|
||||
passed = sum(1 for check in result.checks if check.passed)
|
||||
ph.log(passed=result.passed, checks=f"{passed}/{len(result.checks)}",
|
||||
artifacts=", ".join(result.artifacts))
|
||||
|
||||
with run.phase(PhaseParams(name="request", kind="engineer", owner=run.engineer,
|
||||
description="Capture the incoming ask")) as ph:
|
||||
ph.log(input=prompt, baseline=git_helper.short_sha(baseline))
|
||||
|
||||
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="commit_plan", kind="code", owner="git",
|
||||
description="Put the spec on record before any code exists to blur it")) as ph:
|
||||
commit(ph, plan)
|
||||
|
||||
with run.phase(PhaseParams(name="build", kind="agent", owner="builder",
|
||||
description="Implement the plan exactly")) as ph:
|
||||
build = ph.call(AgentCall(output_type=BuildOutput, prompt=prompt, previous=plan,
|
||||
gates=[gates.diff_matches_claims]))
|
||||
|
||||
test = None
|
||||
for i in range(1, MAX_FIX_LOOPS + 1):
|
||||
with run.phase(PhaseParams(name=f"test_{i}", kind="code", owner="quality",
|
||||
description="Run the suite — a known command, so code runs "
|
||||
"it and no agent has to rediscover it")) as ph:
|
||||
test = quality.run_tests(run)
|
||||
record(ph, test)
|
||||
|
||||
if test.passed:
|
||||
break
|
||||
|
||||
with run.phase(PhaseParams(name=f"fix_{i}", kind="agent", owner="builder", retries=1,
|
||||
description="Repair what the suite reported, from its "
|
||||
"verbatim output")) as ph:
|
||||
build = ph.call(AgentCall(output_type=BuildOutput, prompt=prompt,
|
||||
previous=quality.as_envelope(test, "tests"),
|
||||
gates=[gates.diff_matches_claims]))
|
||||
|
||||
review = None
|
||||
revised = False
|
||||
for i in range(1, MAX_REVISION_LOOPS + 1):
|
||||
with run.phase(PhaseParams(name=f"review_{i}", kind="agent", owner="reviewer",
|
||||
description="Confirm the build matches the plan")) as ph:
|
||||
review = ph.call(AgentCall(output_type=ReviewOutput, prompt=prompt, previous=build,
|
||||
gates=[gates.artifacts_exist, gates.verdict_consistent]))
|
||||
|
||||
if review.approved or i == MAX_REVISION_LOOPS:
|
||||
break
|
||||
|
||||
with run.phase(PhaseParams(name=f"revise_{i}", kind="agent", owner="builder", retries=1,
|
||||
description="Close the reviewer's blocking findings")) as ph:
|
||||
build = ph.call(AgentCall(output_type=BuildOutput, prompt=prompt, previous=review,
|
||||
gates=[gates.diff_matches_claims]))
|
||||
revised = True
|
||||
|
||||
# A revision edited code after the suite last ran, so the green light is
|
||||
# stale. Re-run it rather than commit on a result that predates the change.
|
||||
if revised and review is not None and review.approved:
|
||||
with run.phase(PhaseParams(name="retest", kind="code", owner="quality",
|
||||
description="Re-run the suite — the revision changed code "
|
||||
"after the last green result")) as ph:
|
||||
test = quality.run_tests(run)
|
||||
record(ph, test)
|
||||
|
||||
# Red tests or a rejected review stop the chain here: the code stays
|
||||
# uncommitted and nothing is documented, because there is nothing worth
|
||||
# describing yet. The plan commit stands — it is a record of what was asked.
|
||||
verified = (test is not None and test.passed
|
||||
and review is not None and review.approved)
|
||||
if verified:
|
||||
with run.phase(PhaseParams(name="commit_build", kind="code", owner="git",
|
||||
description="Land the code only now: green suite, approved review")) as ph:
|
||||
commit(ph, build)
|
||||
|
||||
with run.phase(PhaseParams(name="changes", kind="code", owner="git",
|
||||
description="Diff the whole run against its pinned baseline, for the documenter")) as ph:
|
||||
changeset = changes.capture(run, ChangeCapture(base=baseline))
|
||||
ph.log(base=f"{changeset.base.label} @ {changeset.base.commit[:7]}",
|
||||
reason=changeset.base.reason,
|
||||
files=len(changeset.files) + len(changeset.untracked),
|
||||
lines=f"+{changeset.insertions} -{changeset.deletions}",
|
||||
diff=changeset.diff_path)
|
||||
if changeset.empty:
|
||||
raise RuntimeError(
|
||||
f"nothing changed since {changeset.base.label} "
|
||||
f"({changeset.base.reason}) — there is nothing to document.")
|
||||
|
||||
with run.phase(PhaseParams(name="document", kind="agent", owner="documenter", retries=1,
|
||||
description="Write up the completed change")) as ph:
|
||||
document = ph.call(AgentCall(output_type=DocumentOutput, prompt=prompt,
|
||||
previous=changes.as_envelope(changeset, DOCUMENT_NOTES),
|
||||
gates=[gates.artifacts_exist, gates.files_non_empty]))
|
||||
|
||||
with run.phase(PhaseParams(name="commit_docs", kind="code", owner="git",
|
||||
description="Ship the write-up in its own commit, beside the code it describes")) as ph:
|
||||
commit(ph, document)
|
||||
|
||||
return run.finish(accepted=verified,
|
||||
reason="the suite or the review never came back clean")
|
||||
|
||||
|
||||
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))
|
||||
Loading…
Add table
Add a link
Reference in a new issue