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
131
sssf/templates/adws/adw_modules/console.py
Normal file
131
sssf/templates/adws/adw_modules/console.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
"""Console reporter: one narrative, two destinations.
|
||||
|
||||
Every line an ADW prints ALSO lands in the db as a `log` event, so the swim-lane
|
||||
UI reads the same story the terminal does. Both go through `_emit` — print and
|
||||
trace cannot drift. Plain sequential lines only: no spinners, no live displays,
|
||||
so a CI log reads exactly like a terminal.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from rich.console import Console as RichConsole
|
||||
from rich.markup import escape
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
|
||||
from .data_types import EnvelopeBase, EventRecord, Phase
|
||||
|
||||
KIND_COLOR = {"engineer": "cyan", "agent": "magenta", "code": "yellow"}
|
||||
MAX_LINE = 160 # dynamic text (summaries, violations, errors) is clipped
|
||||
|
||||
|
||||
def _clip(text: str, limit: int = MAX_LINE) -> str:
|
||||
text = " ".join(str(text).split())
|
||||
return text if len(text) <= limit else text[: limit - 1] + "…"
|
||||
|
||||
|
||||
class Console:
|
||||
"""Bound to one run's tracer. Reachable as `run.console` everywhere."""
|
||||
|
||||
def __init__(self, tracer, adw_id: str):
|
||||
self.tracer = tracer
|
||||
self.adw_id = adw_id
|
||||
self.phase_id = "" # current lane — log events attach to it
|
||||
self.phase_name = ""
|
||||
self.results: list[str] = [] # phase statuses, for the summary
|
||||
self._finished = False # the summary panel prints once
|
||||
self._out = RichConsole(highlight=False, soft_wrap=True)
|
||||
|
||||
# ── the one helper: print AND trace, always together ────────────────────
|
||||
def _emit(self, markup: str, level: str = "info", renderable=None) -> None:
|
||||
text = Text.from_markup(markup)
|
||||
self._out.print(renderable if renderable is not None else text)
|
||||
self.tracer.event(EventRecord(
|
||||
adw_id=self.adw_id, phase_id=self.phase_id, type="log",
|
||||
name=self.phase_name or "console",
|
||||
payload={"message": text.plain, "level": level}))
|
||||
|
||||
# ── session ─────────────────────────────────────────────────────────────
|
||||
def session_started(self, adw_id: str, engineer: str) -> None:
|
||||
self._emit(f"[bold cyan]adw_id:[/bold cyan] [bold]{escape(adw_id)}[/bold]"
|
||||
f" [dim]engineer[/dim] {escape(engineer)}")
|
||||
|
||||
def session_finished(self, ok: bool, tokens: int, cost: float, db_path: str) -> None:
|
||||
if self._finished:
|
||||
return
|
||||
self._finished = True
|
||||
passed = sum(1 for r in self.results if r == "success")
|
||||
status = "[green]✓ success[/green]" if ok else "[red]✗ fail[/red]"
|
||||
rows = [f" [dim]status[/dim] {status}",
|
||||
f" [dim]phases[/dim] {passed}/{len(self.results)} passed",
|
||||
f" [dim]tokens[/dim] {tokens:,}",
|
||||
f" [dim]cost[/dim] ${cost:.4f}",
|
||||
f" [dim]adw_id[/dim] {escape(self.adw_id)}",
|
||||
f" [dim]db[/dim] {escape(str(db_path))}",
|
||||
f" [dim]next[/dim] [bold]just phases {escape(self.adw_id)}[/bold]"]
|
||||
panel = Panel(Text.from_markup("\n".join(rows)),
|
||||
title="[bold]ADW complete[/bold]",
|
||||
border_style="green" if ok else "red", expand=False)
|
||||
plain = (f"session {self.adw_id} {'success' if ok else 'fail'} · "
|
||||
f"{passed}/{len(self.results)} phases · {tokens:,} tokens · ${cost:.4f}")
|
||||
self._emit(escape(plain), level="info" if ok else "error", renderable=panel)
|
||||
|
||||
# ── phases ──────────────────────────────────────────────────────────────
|
||||
def phase_started(self, phase: Phase) -> None:
|
||||
self.phase_id, self.phase_name = phase.phase_id, phase.params.name
|
||||
p = phase.params
|
||||
color = KIND_COLOR.get(p.kind, "white")
|
||||
line = (f"[bold {color}]▶ {phase.seq:02d} {escape(p.name)}[/bold {color}]"
|
||||
f" [{color}]{p.kind}[/{color}] [dim]· {escape(p.owner)}[/dim]")
|
||||
if p.description:
|
||||
line += f" [dim]{escape(_clip(p.description))}[/dim]"
|
||||
self._emit(line)
|
||||
|
||||
def phase_ended(self, phase: Phase, seconds: float) -> None:
|
||||
ok = phase.status == "success"
|
||||
self.results.append(phase.status)
|
||||
line = (f" {'[green]✓[/green]' if ok else '[red]✗[/red]'} "
|
||||
f"{escape(phase.params.name)} [dim]{seconds:.1f}s[/dim]")
|
||||
if not ok and phase.error:
|
||||
line += f" [red]{escape(_clip(phase.error))}[/red]"
|
||||
self._emit(line, level="info" if ok else "error")
|
||||
self.phase_id, self.phase_name = "", ""
|
||||
|
||||
def note(self, message: str) -> None:
|
||||
"""Free-form detail inside the current phase — what `ph.log()` recorded."""
|
||||
self._emit(f" [dim]· {escape(_clip(message))}[/dim]")
|
||||
|
||||
# ── agents ──────────────────────────────────────────────────────────────
|
||||
def agent_started(self, name: str, model: str, session_id: str) -> None:
|
||||
self._emit(f" [magenta]▸[/magenta] {escape(name)} [dim]{escape(model)}[/dim]"
|
||||
f" [dim]session {escape(session_id)}[/dim]")
|
||||
|
||||
def agent_finished(self, name: str, tokens: int, cost: float) -> None:
|
||||
self._emit(f" [dim]└ {escape(name)} used {tokens:,} tokens · ${cost:.4f}[/dim]")
|
||||
|
||||
def retry(self, name: str, attempt: int, limit: int, reason: str) -> None:
|
||||
self._emit(f" [yellow]⟳[/yellow] {escape(name)} retry {attempt}/{limit} "
|
||||
f"[dim]— same session · {escape(_clip(reason))}[/dim]", level="warn")
|
||||
|
||||
# ── verification ────────────────────────────────────────────────────────
|
||||
def gate_result(self, name: str, report) -> None:
|
||||
"""A gate reports WHAT it checked, not just whether it passed."""
|
||||
ok = report.passed
|
||||
mark = "[green]✓[/green]" if ok else "[red]✗[/red]"
|
||||
summary = (f"{len(report.checks)} checked" if ok
|
||||
else f"[red]{len(report.violations)} of {len(report.checks)} failed[/red]")
|
||||
self._emit(f" {mark} gate [dim]{escape(name)}[/dim] [dim]{summary}[/dim]",
|
||||
level="info" if ok else "error")
|
||||
for check in report.checks:
|
||||
style = "dim" if check.ok else "dim red"
|
||||
detail = f" — {_clip(check.note)}" if check.note else ""
|
||||
self._emit(f" [{style}]{'·' if check.ok else '✗'} {escape(_clip(check.item))}"
|
||||
f"{escape(detail)}[/{style}]", level="info" if check.ok else "error")
|
||||
|
||||
def envelope_summary(self, envelope: EnvelopeBase) -> None:
|
||||
ok = envelope.status == "success"
|
||||
line = (f" {'[green]✓[/green]' if ok else '[red]✗[/red]'} "
|
||||
f"{type(envelope).__name__} [dim]{escape(_clip(envelope.summary))}[/dim]")
|
||||
self._emit(line, level="info" if ok else "error")
|
||||
if envelope.artifacts:
|
||||
self._emit(f" [dim]artifacts: {escape(_clip(', '.join(envelope.artifacts)))}[/dim]")
|
||||
Loading…
Add table
Add a link
Reference in a new issue