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
142
sssf/templates/adws/adw_modules/runner.py
Normal file
142
sssf/templates/adws/adw_modules/runner.py
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
"""The Run object: config + adw_id + agent_map + tracer + console, bound once.
|
||||
|
||||
`run.phase(PhaseParams(...))` is the ONE phase primitive — a context manager
|
||||
for all three kinds (engineer, agent, code). Success must be earned: every
|
||||
phase defaults to fail; only a clean exit flips it (agent phases additionally
|
||||
require a parsed envelope + green gates, enforced inside ph.call).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
|
||||
from . import agents, git_helper
|
||||
from .console import Console
|
||||
from .data_types import AgentCall, EnvelopeBase, EventRecord, Phase, PhaseParams
|
||||
from .utils import ensure_dir, now_iso
|
||||
|
||||
|
||||
class PhaseHandle:
|
||||
def __init__(self, run: "Run", phase: Phase):
|
||||
self.run = run
|
||||
self.phase = phase
|
||||
|
||||
def log(self, **payload) -> None:
|
||||
self.run.tracer.event(EventRecord(adw_id=self.run.adw_id,
|
||||
phase_id=self.phase.phase_id,
|
||||
type="log", name=self.phase.params.name,
|
||||
payload=payload))
|
||||
self.run.console.note(", ".join(f"{k}: {v}" for k, v in payload.items()))
|
||||
if self.phase.params.kind == "engineer" and "input" in payload:
|
||||
self.run.tracer.session_request(self.run.adw_id, str(payload["input"]))
|
||||
|
||||
def call(self, call: AgentCall) -> EnvelopeBase:
|
||||
if self.phase.params.kind != "agent":
|
||||
raise RuntimeError("ph.call() is only valid inside an agent phase")
|
||||
return agents.execute(self.run, self.phase, call)
|
||||
|
||||
|
||||
class Run:
|
||||
def __init__(self, cfg, adw_id: str, tracer, engineer: str):
|
||||
self.cfg = cfg
|
||||
self.adw_id = adw_id
|
||||
self.tracer = tracer
|
||||
self.console = Console(tracer, adw_id)
|
||||
self.engineer = engineer
|
||||
self.phases: list[Phase] = []
|
||||
self.tokens = 0
|
||||
self.cost = 0.0
|
||||
self._seq = tracer.max_phase_seq(adw_id) # a joined run continues the sequence
|
||||
self.repo_root = git_helper.repo_root() # where every agent is spawned to work
|
||||
self.session_dir = ensure_dir(Path(cfg.defaults.data_dir) / "sessions" / adw_id)
|
||||
self.context_handoff_dir = ensure_dir(self.session_dir / "context_handoff")
|
||||
self._agent_map_path = self.session_dir / "agent_map.json"
|
||||
self.agent_map: dict = (json.loads(self._agent_map_path.read_text())
|
||||
if self._agent_map_path.exists() else {})
|
||||
|
||||
# ── agent map (adw_id -> per-agent coding-agent session ids) ────────────
|
||||
def save_agent_map(self, agent: str, entry: dict) -> None:
|
||||
self.agent_map[agent] = entry
|
||||
self._agent_map_path.write_text(json.dumps(self.agent_map, indent=2))
|
||||
|
||||
# ── usage (run totals mirror what the tracer accumulates in sqlite) ─────
|
||||
def add_usage(self, tokens: int, cost: float) -> None:
|
||||
self.tokens += tokens
|
||||
self.cost += cost
|
||||
self.tracer.session_add_usage(self.adw_id, tokens, cost)
|
||||
|
||||
# ── the phase primitive ─────────────────────────────────────────────────
|
||||
@contextmanager
|
||||
def phase(self, params: PhaseParams):
|
||||
self._seq += 1
|
||||
phase = Phase(phase_id=f"{self.adw_id}_{self._seq:02d}_{params.name}",
|
||||
adw_id=self.adw_id, seq=self._seq, params=params,
|
||||
status="running", started_at=now_iso())
|
||||
self.phases.append(phase)
|
||||
self.tracer.phase_upsert(phase)
|
||||
self.tracer.event(EventRecord(adw_id=self.adw_id, phase_id=phase.phase_id,
|
||||
type="phase_start", name=params.name,
|
||||
payload={"kind": params.kind, "owner": params.owner,
|
||||
"description": params.description}))
|
||||
self.console.phase_started(phase)
|
||||
clock = time.monotonic()
|
||||
try:
|
||||
yield PhaseHandle(self, phase)
|
||||
except BaseException as error:
|
||||
phase.status = "fail" # success must be earned
|
||||
phase.error = str(error)[:1000]
|
||||
phase.ended_at = now_iso()
|
||||
self.tracer.event(EventRecord(adw_id=self.adw_id, phase_id=phase.phase_id,
|
||||
type="error", name=params.name,
|
||||
payload={"error": phase.error}))
|
||||
self.tracer.event(EventRecord(adw_id=self.adw_id, phase_id=phase.phase_id,
|
||||
type="phase_end", name=params.name,
|
||||
payload={"status": "fail"}))
|
||||
self.tracer.phase_upsert(phase)
|
||||
self.tracer.session_finish(self.adw_id, ok=False)
|
||||
self.console.phase_ended(phase, time.monotonic() - clock)
|
||||
self.console.session_finished(False, self.tokens, self.cost,
|
||||
self.cfg.observability.db)
|
||||
raise
|
||||
else:
|
||||
phase.status = "success"
|
||||
phase.ended_at = now_iso()
|
||||
self.tracer.event(EventRecord(adw_id=self.adw_id, phase_id=phase.phase_id,
|
||||
type="phase_end", name=params.name,
|
||||
payload={"status": "success"}))
|
||||
self.tracer.phase_upsert(phase)
|
||||
self.console.phase_ended(phase, time.monotonic() - clock)
|
||||
|
||||
# ── run outcome ─────────────────────────────────────────────────────────
|
||||
def finish(self, accepted: bool = True, reason: str = "") -> int:
|
||||
"""Finalize the run and return its exit code. Call this exactly once.
|
||||
|
||||
Two criteria, not one. Every phase must have passed, AND the ADW's own
|
||||
acceptance test must hold. They are different questions on purpose: a
|
||||
test phase that ran the suite did its job even when the suite came back
|
||||
red, so the PHASE succeeds while the RUN must not.
|
||||
|
||||
This replaces a `succeeded` property that answered only the first
|
||||
question — and, being a property with side effects, wrote the session
|
||||
status and printed the banner before the caller's `and test.passed` was
|
||||
ever evaluated. A run whose suite never passed was recorded green in the
|
||||
db, on the terminal, and in the UI while exiting 1. Anyone reading the
|
||||
trace saw success; only a CI job checking `$?` saw the truth. One call
|
||||
now settles the db, the banner, and the exit code together, so the three
|
||||
cannot disagree.
|
||||
"""
|
||||
phases_ok = bool(self.phases) and all(p.status == "success" for p in self.phases)
|
||||
ok = phases_ok and accepted
|
||||
if phases_ok and not accepted:
|
||||
note = reason or "the run's acceptance criterion was not met"
|
||||
self.tracer.event(EventRecord(
|
||||
adw_id=self.adw_id,
|
||||
phase_id=self.phases[-1].phase_id if self.phases else "",
|
||||
type="error", name="not_accepted", payload={"reason": note}))
|
||||
self.console.note(f"not accepted: {note}")
|
||||
self.tracer.session_finish(self.adw_id, ok=ok)
|
||||
self.console.session_finished(ok, self.tokens, self.cost, self.cfg.observability.db)
|
||||
return 0 if ok else 1
|
||||
Loading…
Add table
Add a link
Reference in a new issue