@@ -7983,5 +7983,294 @@ def rules_list_cmd(project_dir: str, as_json: bool) -> None:
79837983 pass # graceful degradation if commands module has issues
79847984
79857985
7986+ # ---------------------------------------------------------------------------
7987+ # specsmith skills — AI Skills Builder (Phase A)
7988+ # ---------------------------------------------------------------------------
7989+
7990+
7991+ @main .group (name = "skills" )
7992+ def skills_group () -> None :
7993+ """Build, list, test, and activate AI agent skills."""
7994+
7995+
7996+ @skills_group .command (name = "build" )
7997+ @click .argument ("description" )
7998+ @click .option ("--project-dir" , type = click .Path (exists = True ), default = "." )
7999+ @click .option ("--tag" , "tags" , multiple = True , help = "Tags for the skill." )
8000+ def skills_build_cmd (description : str , project_dir : str , tags : tuple [str , ...]) -> None :
8001+ """Generate a new skill from a natural-language description."""
8002+ from specsmith .skills_builder import build_skill
8003+
8004+ spec = build_skill (description , project_dir = project_dir , tags = list (tags ))
8005+ console .print (f"[green]\u2713 [/green] Skill created: [bold]{ spec .name } [/bold] ({ spec .id } )" )
8006+ console .print (f" [dim]{ spec .purpose } [/dim]" )
8007+
8008+
8009+ @skills_group .command (name = "list" )
8010+ @click .option ("--project-dir" , type = click .Path (exists = True ), default = "." )
8011+ @click .option ("--json" , "as_json" , is_flag = True , default = False )
8012+ def skills_list_cmd (project_dir : str , as_json : bool ) -> None :
8013+ """List available skills."""
8014+ import json as _json
8015+
8016+ from specsmith .skills_builder import list_skills
8017+
8018+ skills = list_skills (project_dir )
8019+ if as_json :
8020+ click .echo (_json .dumps ({"skills" : [s .to_dict () for s in skills ]}, indent = 2 ))
8021+ return
8022+ if not skills :
8023+ console .print ("[dim]No skills found. Use `specsmith skills build` to create one.[/dim]" )
8024+ return
8025+ console .print (f"[bold]Skills[/bold] ({ len (skills )} )\n " )
8026+ for s in skills :
8027+ badge = "[green]\u2714 [/green]" if s .active else "[dim]\u25cb [/dim]"
8028+ console .print (f" { badge } [bold]{ s .id } [/bold] { s .name } " )
8029+
8030+
8031+ @skills_group .command (name = "test" )
8032+ @click .argument ("skill_id" )
8033+ @click .option ("--project-dir" , type = click .Path (exists = True ), default = "." )
8034+ def skills_test_cmd (skill_id : str , project_dir : str ) -> None :
8035+ """Dry-run a skill to verify its spec."""
8036+ from specsmith .skills_builder import list_skills
8037+
8038+ skills = {s .id : s for s in list_skills (project_dir )}
8039+ if skill_id not in skills :
8040+ console .print (f"[red]Skill not found:[/red] { skill_id } " )
8041+ raise SystemExit (1 )
8042+ spec = skills [skill_id ]
8043+ console .print (f"[bold]Testing:[/bold] { spec .name } " )
8044+ console .print (f" Purpose: { spec .purpose } " )
8045+ console .print (f" Tools: { ', ' .join (spec .tools_used ) or 'none' } " )
8046+ console .print (f" Stop conditions: { len (spec .stop_conditions )} " )
8047+ console .print ("[green]\u2713 [/green] Skill spec is valid (dry-run)." )
8048+
8049+
8050+ @skills_group .command (name = "activate" )
8051+ @click .argument ("skill_id" )
8052+ @click .option ("--project-dir" , type = click .Path (exists = True ), default = "." )
8053+ def skills_activate_cmd (skill_id : str , project_dir : str ) -> None :
8054+ """Activate a skill for agent use."""
8055+ from specsmith .skills_builder import activate_skill
8056+
8057+ if activate_skill (skill_id , project_dir ):
8058+ console .print (f"[green]\u2713 [/green] Skill [bold]{ skill_id } [/bold] activated." )
8059+ else :
8060+ console .print (f"[red]Skill not found:[/red] { skill_id } " )
8061+ raise SystemExit (1 )
8062+
8063+
8064+ main .add_command (skills_group )
8065+
8066+
8067+ # ---------------------------------------------------------------------------
8068+ # specsmith eval — Eval-Driven Development framework (Phase P3)
8069+ # ---------------------------------------------------------------------------
8070+
8071+
8072+ @main .group (name = "eval" )
8073+ def eval_group () -> None :
8074+ """Run eval suites to benchmark AI model capabilities."""
8075+
8076+
8077+ @eval_group .command (name = "list" )
8078+ @click .option ("--json" , "as_json" , is_flag = True , default = False )
8079+ def eval_list_cmd (as_json : bool ) -> None :
8080+ """List available eval suites."""
8081+ import json as _json
8082+
8083+ from specsmith .eval .builtins import list_suites
8084+
8085+ suites = list_suites ()
8086+ if as_json :
8087+ click .echo (_json .dumps ({"suites" : [s .to_dict () for s in suites ]}, indent = 2 ))
8088+ return
8089+ if not suites :
8090+ console .print ("[dim]No eval suites available.[/dim]" )
8091+ return
8092+ console .print (f"[bold]Eval Suites[/bold] ({ len (suites )} )\n " )
8093+ for s in suites :
8094+ console .print (f" [bold]{ s .id } [/bold] { s .name } ({ len (s .cases )} cases)" )
8095+ console .print (f" [dim]{ s .description } [/dim]" )
8096+
8097+
8098+ @eval_group .command (name = "run" )
8099+ @click .argument ("suite_id" , default = "core" )
8100+ @click .option ("--json" , "as_json" , is_flag = True , default = False )
8101+ def eval_run_cmd (suite_id : str , as_json : bool ) -> None :
8102+ """Run an eval suite (stub mode — no real LLM calls)."""
8103+ import json as _json
8104+
8105+ from specsmith .eval .builtins import get_suite
8106+ from specsmith .eval .runner import run_suite
8107+
8108+ suite = get_suite (suite_id )
8109+ if suite is None :
8110+ console .print (f"[red]Suite not found:[/red] { suite_id } " )
8111+ raise SystemExit (1 )
8112+ report = run_suite (suite , stub = True )
8113+ if as_json :
8114+ click .echo (_json .dumps (report .to_dict (), indent = 2 ))
8115+ return
8116+ icon = "[green]\u2714 [/green]" if report .failed == 0 else "[red]\u2717 [/red]"
8117+ console .print (
8118+ f"{ icon } [bold]{ suite_id } [/bold] "
8119+ f"{ report .passed } /{ report .total } passed "
8120+ f"avg score { report .avg_score :.0%} "
8121+ f"avg latency { report .avg_latency_ms :.0f} ms"
8122+ )
8123+ for r in report .results :
8124+ ri = "[green]\u2713 [/green]" if r .passed else "[red]\u2717 [/red]"
8125+ console .print (f" { ri } { r .case_id } score={ r .score :.0%} { r .latency_ms :.0f} ms" )
8126+
8127+
8128+ @eval_group .command (name = "report" )
8129+ @click .argument ("suite_id" , default = "core" )
8130+ @click .option ("--output" , type = click .Path (), default = None , help = "Write markdown report to file." )
8131+ def eval_report_cmd (suite_id : str , output : str | None ) -> None :
8132+ """Generate a markdown eval report."""
8133+ from specsmith .eval .builtins import get_suite
8134+ from specsmith .eval .runner import generate_markdown_report , run_suite
8135+
8136+ suite = get_suite (suite_id )
8137+ if suite is None :
8138+ console .print (f"[red]Suite not found:[/red] { suite_id } " )
8139+ raise SystemExit (1 )
8140+ report = run_suite (suite , stub = True )
8141+ md = generate_markdown_report (report )
8142+ if output :
8143+ Path (output ).write_text (md , encoding = "utf-8" )
8144+ console .print (f"[green]\u2713 [/green] Report written to { output } " )
8145+ else :
8146+ click .echo (md )
8147+
8148+
8149+ main .add_command (eval_group )
8150+
8151+
8152+ # ---------------------------------------------------------------------------
8153+ # specsmith teams — Multi-agent team coordination (Phase P4)
8154+ # ---------------------------------------------------------------------------
8155+
8156+
8157+ @main .group (name = "teams" )
8158+ def teams_group () -> None :
8159+ """List and run multi-agent teams."""
8160+
8161+
8162+ @teams_group .command (name = "list" )
8163+ @click .option ("--json" , "as_json" , is_flag = True , default = False )
8164+ def teams_list_cmd (as_json : bool ) -> None :
8165+ """List predefined agent teams."""
8166+ import json as _json
8167+
8168+ from specsmith .agent .teams import list_teams
8169+
8170+ teams = list_teams ()
8171+ if as_json :
8172+ click .echo (_json .dumps ({"teams" : [t .to_dict () for t in teams ]}, indent = 2 ))
8173+ return
8174+ console .print (f"[bold]Agent Teams[/bold] ({ len (teams )} )\n " )
8175+ for t in teams :
8176+ roles = ", " .join (m .role for m in t .members )
8177+ console .print (f" [bold]{ t .id } [/bold] { t .name } [{ roles } ]" )
8178+ console .print (f" [dim]{ t .description } [/dim]" )
8179+
8180+
8181+ @teams_group .command (name = "run" )
8182+ @click .argument ("team_id" )
8183+ @click .argument ("task" )
8184+ def teams_run_cmd (team_id : str , task : str ) -> None :
8185+ """Spawn a team to execute a task (stub — prints team plan)."""
8186+ from specsmith .agent .teams import get_team
8187+
8188+ team = get_team (team_id )
8189+ if team is None :
8190+ console .print (f"[red]Team not found:[/red] { team_id } " )
8191+ raise SystemExit (1 )
8192+ console .print (f"[bold]Spawning team:[/bold] { team .name } " )
8193+ for m in team .members :
8194+ console .print (f" \u2192 { m .role } ({ 'required' if m .required else 'optional' } )" )
8195+ console .print (f"[dim]Task: { task } [/dim]" )
8196+ console .print (
8197+ "[yellow]\u26a0 [/yellow] Team execution is in stub mode (no real agents spawned)."
8198+ )
8199+
8200+
8201+ main .add_command (teams_group )
8202+
8203+
8204+ # ---------------------------------------------------------------------------
8205+ # specsmith esdb — ChronoMemory ESDB management (Phase ESDB)
8206+ # ---------------------------------------------------------------------------
8207+
8208+
8209+ @main .group (name = "esdb" )
8210+ def esdb_group () -> None :
8211+ """Manage the ChronoMemory Epistemic State Database."""
8212+
8213+
8214+ @esdb_group .command (name = "status" )
8215+ @click .option ("--project-dir" , type = click .Path (exists = True ), default = "." )
8216+ @click .option ("--json" , "as_json" , is_flag = True , default = False )
8217+ def esdb_status_cmd (project_dir : str , as_json : bool ) -> None :
8218+ """Show ESDB status and record counts."""
8219+ import json as _json
8220+
8221+ from specsmith .esdb .bridge import EsdbBridge
8222+
8223+ bridge = EsdbBridge (project_dir )
8224+ st = bridge .status ()
8225+ counts = bridge .record_counts ()
8226+ if as_json :
8227+ click .echo (_json .dumps ({"status" : st .to_dict (), "counts" : counts }, indent = 2 ))
8228+ return
8229+ icon = "[green]\u25cf [/green]" if st .available else "[red]\u25cf [/red]"
8230+ console .print (f"{ icon } ESDB — { st .backend } " )
8231+ console .print (f" Records: { st .record_count } " )
8232+ for kind , count in counts .items ():
8233+ console .print (f" { kind } : { count } " )
8234+ if st .chain_valid :
8235+ console .print (" [green]\u2714 [/green] WAL chain integrity OK" )
8236+
8237+
8238+ @esdb_group .command (name = "migrate" )
8239+ @click .option ("--project-dir" , type = click .Path (exists = True ), default = "." )
8240+ def esdb_migrate_cmd (project_dir : str ) -> None :
8241+ """Migrate .specsmith/ flat JSON to ESDB (stub — validates data)."""
8242+ from specsmith .esdb .bridge import EsdbBridge
8243+
8244+ bridge = EsdbBridge (project_dir )
8245+ reqs = bridge .requirements ()
8246+ tests = bridge .testcases ()
8247+ console .print ("[bold]Migration scan:[/bold]" )
8248+ console .print (f" Requirements: { len (reqs )} " )
8249+ console .print (f" Test cases: { len (tests )} " )
8250+ console .print ("[green]\u2713 [/green] Data validated. Full Rust ESDB migration not yet active." )
8251+ console .print (
8252+ "[dim]When ChronoMemory native engine is linked, run this again to migrate.[/dim]"
8253+ )
8254+
8255+
8256+ @esdb_group .command (name = "replay" )
8257+ @click .option ("--project-dir" , type = click .Path (exists = True ), default = "." )
8258+ def esdb_replay_cmd (project_dir : str ) -> None :
8259+ """Replay ESDB WAL to verify state integrity (stub)."""
8260+ from specsmith .esdb .bridge import EsdbBridge
8261+
8262+ bridge = EsdbBridge (project_dir )
8263+ st = bridge .status ()
8264+ console .print (f"[bold]Replay check:[/bold] { st .backend } " )
8265+ console .print (f" Records: { st .record_count } " )
8266+ if st .chain_valid :
8267+ console .print ("[green]\u2714 [/green] WAL chain valid — state consistent." )
8268+ else :
8269+ console .print ("[red]\u2717 [/red] WAL chain integrity failure detected." )
8270+
8271+
8272+ main .add_command (esdb_group )
8273+
8274+
79868275if __name__ == "__main__" :
79878276 main ()
0 commit comments