diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index cb9b55c9..cbcb2219 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -8,7 +8,7 @@ "homepage": "https://github.com/alirezarezvani/claude-skills", "repository": "https://github.com/alirezarezvani/claude-skills", "metadata": { - "description": "362 production-ready skills across 18 domains (engineering, engineering-core, marketing, product, c-level, compliance-os, project management, RA/QM, business growth, finance, productivity, marketing top-level, research, research-ops, business-operations, commercial, markdown-html, loop-library, plus standards). 644 Python tools, 741 reference guides, 102 agents (cs-* + personas), 116 slash commands across 88 marketplace plugins. v2.11.2 vendors engineering/skillopt-sleep — a verbatim copy of microsoft/SkillOpt's stdlib-only skillopt_sleep engine + Claude Code plugin surface, giving a local agent a nightly gated self-improvement cycle (read-only session harvest -> mine -> offline replay -> held-out-gated CLAUDE.md/SKILL.md edits -> staged for explicit /skillopt-sleep adopt). productivity/fable-goal (unreleased, post-v2.11.1) converts a rambling description of a desired outcome into one polished /goal prompt for a fresh autonomous session. v2.11.1 turns product-team and project-management into agent-harness domains: fork-orchestrators with deterministic goal routers, a Jira MCP snapshot bridge (Kanban flow metrics + Monte Carlo forecasting), a delegation-governance loop gate, a continuous-discovery cadence tracker, and an Opportunity Solution Tree linter, with /cs:pm and /cs:product command families. v2.10.3 completes the markdown-html domain with md-slides — slide-deck converter (arrow-key / Space / PgDn / Home/End / P keyboard navigation + presenter mode with split-view clock + speaker notes + next-slide preview + URL-hash deep linking like #3 for direct slide jumps + @media print page-per-slide for browser-native PDF export). Reuses md-document's markdown parser; vanilla JS only (no framework runtime); Prism.js opt-in via --syntax. Joins md-review (v2.10.2 code-review converter), md-document (v2.10.1 long-form converter), and the v2.10.0 foundation (orchestrator + design-system). Compatible with Claude Code, Codex CLI, Gemini CLI, Cursor, OpenClaw, Hermes Agent, Mistral Vibe, and 5 more coding agents.", + "description": "368 production-ready skills across 19 domains (engineering, engineering-core, marketing, product, c-level, compliance-os, project management, RA/QM, business growth, finance, productivity, marketing top-level, research, research-ops, business-operations, commercial, markdown-html, loop-library, plus standards). 664 Python tools, 746 reference guides, 106 agents (cs-* + personas), 124 slash commands across 89 marketplace plugins. v2.12.0 adds the agent-launcher domain — a plugin re-implementation of Anthropic's launch-your-agent (Apache-2.0) for building Claude Managed Agents in your own account: a per-session goal compiles into a bounded grade->iterate loop, a recurring cron deployment loop, or a single-pass launch workflow; deterministic BYOK scaffolders (no API calls). v2.11.2 vendors engineering/skillopt-sleep — a verbatim copy of microsoft/SkillOpt's stdlib-only skillopt_sleep engine + Claude Code plugin surface, giving a local agent a nightly gated self-improvement cycle (read-only session harvest -> mine -> offline replay -> held-out-gated CLAUDE.md/SKILL.md edits -> staged for explicit /skillopt-sleep adopt). productivity/fable-goal (unreleased, post-v2.11.1) converts a rambling description of a desired outcome into one polished /goal prompt for a fresh autonomous session. v2.11.1 turns product-team and project-management into agent-harness domains: fork-orchestrators with deterministic goal routers, a Jira MCP snapshot bridge (Kanban flow metrics + Monte Carlo forecasting), a delegation-governance loop gate, a continuous-discovery cadence tracker, and an Opportunity Solution Tree linter, with /cs:pm and /cs:product command families. v2.10.3 completes the markdown-html domain with md-slides — slide-deck converter (arrow-key / Space / PgDn / Home/End / P keyboard navigation + presenter mode with split-view clock + speaker notes + next-slide preview + URL-hash deep linking like #3 for direct slide jumps + @media print page-per-slide for browser-native PDF export). Reuses md-document's markdown parser; vanilla JS only (no framework runtime); Prism.js opt-in via --syntax. Joins md-review (v2.10.2 code-review converter), md-document (v2.10.1 long-form converter), and the v2.10.0 foundation (orchestrator + design-system). Compatible with Claude Code, Codex CLI, Gemini CLI, Cursor, OpenClaw, Hermes Agent, Mistral Vibe, and 5 more coding agents.", "version": "2.11.2" }, "plugins": [ @@ -1862,6 +1862,41 @@ "collab-proof" ], "category": "engineering" + }, + { + "name": "agent-launcher-skills", + "source": "./agent-launcher", + "description": "Build, launch, grade, and schedule Claude Managed Agents (CMA) in your own Anthropic account — a plugin re-implementation of Anthropic's launch-your-agent reference skill (Apache-2.0; independent, not a fork). Every session starts with a goal (./my-agent/goal.json, surfaced by an opt-in AGENT_LAUNCHER_SESSION=1 SessionStart hook and driven by /cs:goal); loop_compiler.py compiles that goal into a bounded grade->iterate loop (CMA user.define_outcome self-grading, max_iterations 1..20), a recurring POSIX-cron scheduled-deployment loop (run without you), or a single-pass interview->stage->launch workflow. 6 skills: agent-launcher-orchestrator (context: fork goal router) + interview + stage-launch + grade-iterate + run-without-you + wrap-up. 18 stdlib-only deterministic scaffolder tools (NO network/API calls; live launches emitted as BYOK curl that never prints the key), 4 agents (orchestrator + interviewer + grader + deployer), 8 commands, opt-in SessionStart/SessionEnd hooks, 5 shared references, 4 assets. Validators enforce CMA limits (<=20 skills/session, <=8 memory stores, depth-1 multiagent, max_iterations <=20, <=1000 deployments/org). Distinct from engineering/agent-harness (generic domain loop) and engineering/write-a-skill (authors Claude Code skills, not CMAs).", + "version": "2.12.0", + "author": { + "name": "Alireza Rezvani" + }, + "keywords": [ + "claude-managed-agents", + "cma", + "agent-launcher", + "launch-your-agent", + "managed-agent", + "session-goal", + "loop", + "workflow", + "grade-iterate", + "outcome", + "rubric", + "max-iterations", + "scheduled-deployment", + "cron", + "run-without-you", + "byok", + "build-sheet", + "payloads", + "interview", + "wrap-up", + "context-fork", + "matt-pocock", + "grill-with-docs" + ], + "category": "agent-development" } ] } diff --git a/CLAUDE.md b/CLAUDE.md index 42faef2b..5806ac4d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co This is a **comprehensive skills library** for Claude AI and Claude Code - reusable, production-ready skill packages that bundle domain expertise, best practices, analysis tools, and strategic frameworks. The repository provides modular skills that teams can download and use directly in their workflows. -**Current Scope:** 362 production-ready skills across 18 domains with 644 Python automation tools, 741 reference guides, 102 agents (cs-* + 7 personas), and 116 slash commands, distributed as 88 marketplace plugins. Headline counters are derived from the tree by `scripts/derive_counters.py` (run with `--check` to verify the docs still match). **v2.11.2 (current)** vendors **engineering/skillopt-sleep/** — started as a verbatim, byte-for-byte copy of `microsoft/SkillOpt`'s `skillopt_sleep` engine (stdlib-only, zero third-party deps) and its Claude Code plugin surface (`skills/`, `hooks/`, `commands/`, `scripts/`), then received 23 targeted patches after ten rounds of adversarial review (see `engineering/skillopt-sleep/README.md`'s numbered "Deviations from upstream" list, the authoritative source — re-apply all 23 on re-vendor). Gives a local agent a nightly "sleep cycle": read-only harvest of past Claude Code session transcripts → mine recurring tasks → replay offline on the user's own API budget → consolidate into `CLAUDE.md`/`SKILL.md` edits behind a held-out validation gate → stage for review; nothing live changes until an explicit `/skillopt-sleep adopt` (which backs up first). Default `mock` backend spends no API budget. The heavier `skillopt` *training* package (benchmark-driven, needs `numpy`/`openai`/`azure-*` + hand-labeled train/val/test data per task) was deliberately **not** vendored — it optimizes one narrow, scoreable task at a time, which doesn't fit this repo's broad domain-expertise skills or its no-ML-in-scripts/no-test-framework conventions; `skillopt_sleep` mines its "benchmark" from real usage instead, which does fit. Attribution preserved in `plugin.json` + `LICENSE` + `README.md` (MIT, © Microsoft Corporation / Yifan Yang), following the same verbatim-vendor pattern as `loop-library/`. **Unreleased (post-v2.11.2)** ships the **productivity coverage expansion** — public audit record `audit/productivity-2026-07/` (all 7 legacy skills scored, 24/24 scripts smoke-tested, coverage map vs the personal-productivity canon) + 3 gap-filling plugins, each with a cs-* agent, /cs:* commands, 3 stdlib scripts and 3 cited references: **weekly-review** (GTD loop; review-gate refuses COMPLETE while a mandatory GET CURRENT step is missing), **deep-work** (time-block planner refusing >4h deep demand, shallow-work budget auditor, focus-session logger), **meetings** (MEET/ASYNC/NOT-READY cost gate, outcome-required agenda builder, action-item extractor with ORPHAN/NO-DUE flags). **Unreleased (post-v2.11.1)** added **productivity/fable-goal** — converts a rambling description of a desired outcome into one polished, copy-paste `/goal` prompt for a fresh autonomous session (ported from `duncan-buildroom/freeskills`). **v2.11.1 (complete)** upgrades **product-team/** and **project-management/** into agent-harness domains: both prose routers rebuilt as `context: fork` orchestrators with deterministic goal routers (exit-code route/ask/refuse), a Jira MCP snapshot bridge (Kanban-Guide-2025 flow metrics + seeded Monte Carlo forecasts, verified end-to-end into velocity_analyzer), a delegation-governance loop gate (human owner / reviewer / machine-checkable acceptance / close refusal), a Torres continuous-discovery cadence tracker + Opportunity Solution Tree linter, cs-pm-orchestrator + cs-product-orchestrator agents, and /cs:pm|grill-pm|pm-loop + /cs:product|grill-product|product-loop commands — plus the public audit record `audit/pm-product-agentic-2026-07/` (AR-rubric scores for all 26 skills, research-backed improvement fields, executable verification criteria). **v2.9.0 (complete)** added the **research-ops/** top-level domain — enterprise Research Operations (orchestrator + clinical-research + research-finance + market-research + product-research), the managed counterpart to the academic research/ domain, with `context: fork` orchestration and a Matt Pocock "Forcing-question library" in every SKILL.md plus `/cs:grill-research-ops`. **v2.8.0 (complete)** added 2 new top-level domains — **business-operations/** (7 internal-ops skills: orchestrator + process-mapper + vendor-management + capacity-planner + internal-comms + knowledge-ops + procurement-optimizer) and **commercial/** (8 per-deal-economics skills: orchestrator + pricing-strategist + deal-desk + partnerships-architect + channel-economics + commercial-policy + rfp-responder + commercial-forecaster) — with orchestrator skills using `context: fork` for chaining, Matt Pocock docs-anchored "Forcing-question library" in every SKILL.md, plus `/cs:grill-bizops` and `/cs:grill-commercial`. **v2.8.2** adds a productivity-shaped `handoff` skill (sibling to engineering/handoff) inspired by Matt Pocock — first-run setup with configurable save location, redaction linter, SessionStart + SessionEnd hooks, fidelity self-check, `--refresh` flag. **v2.8.1** upgraded the engineering role-skills (senior-fullstack / senior-frontend / senior-backend) with karpathy-coder + Matt Pocock decision engines + per-role forcing questions. v2.7.3 ports `alirezarezvani/aeo-box` — AEO (Answer Engine Optimization) skill into marketing-skill/ + security-guidance PreToolUse hook into engineering/. v2.7.0 added 13 Path-B skills across 3 top-level domains (productivity, marketing, research). v2.6.0 added 4 Matt Pocock-derived productivity skills. +**Current Scope:** 368 production-ready skills across 19 domains with 664 Python automation tools, 746 reference guides, 106 agents (cs-* + 7 personas), and 124 slash commands, distributed as 89 marketplace plugins. Headline counters are derived from the tree by `scripts/derive_counters.py` (run with `--check` to verify the docs still match). **v2.12.0 (current)** adds the **agent-launcher/** top-level domain — a plugin re-implementation of Anthropic's `launch-your-agent` reference skill (Apache-2.0; independent, not a fork) for building **Claude Managed Agents (CMA)** in the user's own account. Every session starts with a goal (`./my-agent/goal.json`, surfaced by an opt-in `AGENT_LAUNCHER_SESSION=1` SessionStart hook + `/cs:goal`); `loop_compiler.py` compiles that goal into a **bounded grade→iterate loop** (CMA `user.define_outcome` self-grading, `max_iterations` 1..20), a **recurring POSIX-cron scheduled-deployment loop**, or a **single-pass interview→stage→launch workflow**. 6 skills (orchestrator `context: fork` + interview + stage-launch + grade-iterate + run-without-you + wrap-up), 18 stdlib-only **deterministic scaffolder** tools (NO network/API calls — live launches emitted as **BYOK curl** that never prints the key), 4 agents (orchestrator + interviewer + grader + deployer), 8 `/cs:*` commands, opt-in SessionStart/SessionEnd hooks, 5 shared references, 4 assets. Validators enforce CMA limits (≤20 skills/session, ≤8 memory stores, depth-1 multiagent, `max_iterations` ≤20, ≤1000 deployments/org). Distinct from `engineering/agent-harness` (generic bounded loop over any domain) and `engineering/write-a-skill` (authors Claude Code skills, not CMAs). **v2.11.2** vendors **engineering/skillopt-sleep/** — started as a verbatim, byte-for-byte copy of `microsoft/SkillOpt`'s `skillopt_sleep` engine (stdlib-only, zero third-party deps) and its Claude Code plugin surface (`skills/`, `hooks/`, `commands/`, `scripts/`), then received 23 targeted patches after ten rounds of adversarial review (see `engineering/skillopt-sleep/README.md`'s numbered "Deviations from upstream" list, the authoritative source — re-apply all 23 on re-vendor). Gives a local agent a nightly "sleep cycle": read-only harvest of past Claude Code session transcripts → mine recurring tasks → replay offline on the user's own API budget → consolidate into `CLAUDE.md`/`SKILL.md` edits behind a held-out validation gate → stage for review; nothing live changes until an explicit `/skillopt-sleep adopt` (which backs up first). Default `mock` backend spends no API budget. The heavier `skillopt` *training* package (benchmark-driven, needs `numpy`/`openai`/`azure-*` + hand-labeled train/val/test data per task) was deliberately **not** vendored — it optimizes one narrow, scoreable task at a time, which doesn't fit this repo's broad domain-expertise skills or its no-ML-in-scripts/no-test-framework conventions; `skillopt_sleep` mines its "benchmark" from real usage instead, which does fit. Attribution preserved in `plugin.json` + `LICENSE` + `README.md` (MIT, © Microsoft Corporation / Yifan Yang), following the same verbatim-vendor pattern as `loop-library/`. **Unreleased (post-v2.11.2)** ships the **productivity coverage expansion** — public audit record `audit/productivity-2026-07/` (all 7 legacy skills scored, 24/24 scripts smoke-tested, coverage map vs the personal-productivity canon) + 3 gap-filling plugins, each with a cs-* agent, /cs:* commands, 3 stdlib scripts and 3 cited references: **weekly-review** (GTD loop; review-gate refuses COMPLETE while a mandatory GET CURRENT step is missing), **deep-work** (time-block planner refusing >4h deep demand, shallow-work budget auditor, focus-session logger), **meetings** (MEET/ASYNC/NOT-READY cost gate, outcome-required agenda builder, action-item extractor with ORPHAN/NO-DUE flags). **Unreleased (post-v2.11.1)** added **productivity/fable-goal** — converts a rambling description of a desired outcome into one polished, copy-paste `/goal` prompt for a fresh autonomous session (ported from `duncan-buildroom/freeskills`). **v2.11.1 (complete)** upgrades **product-team/** and **project-management/** into agent-harness domains: both prose routers rebuilt as `context: fork` orchestrators with deterministic goal routers (exit-code route/ask/refuse), a Jira MCP snapshot bridge (Kanban-Guide-2025 flow metrics + seeded Monte Carlo forecasts, verified end-to-end into velocity_analyzer), a delegation-governance loop gate (human owner / reviewer / machine-checkable acceptance / close refusal), a Torres continuous-discovery cadence tracker + Opportunity Solution Tree linter, cs-pm-orchestrator + cs-product-orchestrator agents, and /cs:pm|grill-pm|pm-loop + /cs:product|grill-product|product-loop commands — plus the public audit record `audit/pm-product-agentic-2026-07/` (AR-rubric scores for all 26 skills, research-backed improvement fields, executable verification criteria). **v2.9.0 (complete)** added the **research-ops/** top-level domain — enterprise Research Operations (orchestrator + clinical-research + research-finance + market-research + product-research), the managed counterpart to the academic research/ domain, with `context: fork` orchestration and a Matt Pocock "Forcing-question library" in every SKILL.md plus `/cs:grill-research-ops`. **v2.8.0 (complete)** added 2 new top-level domains — **business-operations/** (7 internal-ops skills: orchestrator + process-mapper + vendor-management + capacity-planner + internal-comms + knowledge-ops + procurement-optimizer) and **commercial/** (8 per-deal-economics skills: orchestrator + pricing-strategist + deal-desk + partnerships-architect + channel-economics + commercial-policy + rfp-responder + commercial-forecaster) — with orchestrator skills using `context: fork` for chaining, Matt Pocock docs-anchored "Forcing-question library" in every SKILL.md, plus `/cs:grill-bizops` and `/cs:grill-commercial`. **v2.8.2** adds a productivity-shaped `handoff` skill (sibling to engineering/handoff) inspired by Matt Pocock — first-run setup with configurable save location, redaction linter, SessionStart + SessionEnd hooks, fidelity self-check, `--refresh` flag. **v2.8.1** upgraded the engineering role-skills (senior-fullstack / senior-frontend / senior-backend) with karpathy-coder + Matt Pocock decision engines + per-role forcing questions. v2.7.3 ports `alirezarezvani/aeo-box` — AEO (Answer Engine Optimization) skill into marketing-skill/ + security-guidance PreToolUse hook into engineering/. v2.7.0 added 13 Path-B skills across 3 top-level domains (productivity, marketing, research). v2.6.0 added 4 Matt Pocock-derived productivity skills. **Key Distinction**: This is NOT a traditional application. It's a library of skill packages meant to be extracted and deployed by users into their own Claude workflows. diff --git a/README.md b/README.md index 7af908d9..aa9802bb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Claude Code Skills & Plugins — Agent Skills for Every Coding Tool -**362 production-ready Claude Code skills, plugins, and agent skills for 13 AI coding tools.** +**368 production-ready Claude Code skills, plugins, and agent skills for 13 AI coding tools.** The most comprehensive open-source library of Claude Code skills and agent plugins — also works with OpenAI Codex, Gemini CLI, Cursor, and 9 more coding agents. Reusable expertise packages covering engineering, DevOps, marketing (incl. AEO — Answer Engine Optimization for LLM citation), security (PreToolUse hooks), compliance, C-level advisory (incl. founder-mode CFO/CMO/CRO/CPO/COO/CHRO/CISO/GC/CDO/CAIO/CCO/VPE personas + 21 /cs:* slash commands), productivity (capture/email/reflect/weekly-review/deep-work/meetings), an academic research stack (litreview/grants/dossier/patent/syllabus/pulse/notebooklm/deep-research + hybrid router), and enterprise Research Operations (clinical-research/research-finance/market-research/product-research, v2.9.0). @@ -10,10 +10,10 @@ The most comprehensive open-source library of Claude Code skills and agent plugi [^vibe]: Mistral Vibe is also **BYO-sync tier**: the repo ships a pre-generated `.vibe/skills/claude-skills/` tree, run `./scripts/vibe-install.sh` once locally to install into `~/.vibe/skills/`. Same agentskills.io SKILL.md standard — no format conversion. Docs: . [![License: MIT](https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge)](https://opensource.org/licenses/MIT) -[![Skills](https://img.shields.io/badge/Skills-362-brightgreen?style=for-the-badge)](#skills-overview) -[![Agents](https://img.shields.io/badge/Agents-102-blue?style=for-the-badge)](#agents) +[![Skills](https://img.shields.io/badge/Skills-368-brightgreen?style=for-the-badge)](#skills-overview) +[![Agents](https://img.shields.io/badge/Agents-106-blue?style=for-the-badge)](#agents) [![Personas](https://img.shields.io/badge/Personas-7-purple?style=for-the-badge)](#personas) -[![Commands](https://img.shields.io/badge/Commands-116-orange?style=for-the-badge)](#commands) +[![Commands](https://img.shields.io/badge/Commands-124-orange?style=for-the-badge)](#commands) [![Stars](https://img.shields.io/github/stars/alirezarezvani/claude-skills?style=for-the-badge)](https://github.com/alirezarezvani/claude-skills/stargazers) [![SkillCheck Validated](https://img.shields.io/badge/SkillCheck-Validated-4c1?style=for-the-badge)](https://getskillcheck.com) @@ -26,10 +26,10 @@ The most comprehensive open-source library of Claude Code skills and agent plugi Claude Code skills (also called agent skills or coding agent plugins) are modular instruction packages that give AI coding agents domain expertise they don't have out of the box. Each skill includes: - **SKILL.md** — structured instructions, workflows, and decision frameworks -- **Python tools** — 644 CLI scripts (all stdlib-only, zero pip installs) -- **Reference docs** — 741 templates, checklists, and domain-specific knowledge files +- **Python tools** — 664 CLI scripts (all stdlib-only, zero pip installs) +- **Reference docs** — 746 templates, checklists, and domain-specific knowledge files -**One repo, thirteen platforms.** Works natively as Claude Code plugins, Codex agent skills, Gemini CLI skills, Hermes Agent skills, Mistral Vibe skills, and converts to more tools via `scripts/convert.sh`. All 644 Python tools run anywhere Python runs. +**One repo, thirteen platforms.** Works natively as Claude Code plugins, Codex agent skills, Gemini CLI skills, Hermes Agent skills, Mistral Vibe skills, and converts to more tools via `scripts/convert.sh`. All 664 Python tools run anywhere Python runs. ### Skills vs Agents vs Personas @@ -150,7 +150,7 @@ Run `./scripts/convert.sh --tool all` to generate tool-specific outputs locally. ## Skills Overview -**362 skills across 18 domains:** +**368 skills across 19 domains:** | Domain | Skills | Highlights | Details | |--------|--------|------------|---------| @@ -172,6 +172,7 @@ Run `./scripts/convert.sh --tool all` to generate tool-specific outputs locally. | **💰 Finance** | 4 | Financial analyst (DCF, budgeting, forecasting), SaaS metrics coach, business investment advisor | [finance/](finance/) | | **🔄 Loop Library** | 1 | `loop-library` — discover, find, audit/repair, adapt, and design bounded AI-agent loops; reads the live catalog from signals.forwardfuture.ai at runtime (vendored verbatim from [Forward-Future/loop-library](https://github.com/Forward-Future/loop-library)) | [loop-library/](loop-library/) | | **📄 Markdown → HTML** | 5 | `markdown-html-orchestrator` (doctype router) + `design-system` (WCAG-AA brand tokens) + `md-document` (long-form) + `md-review` (2-col code review) + `md-slides` (single-file deck) — markdown-to-interactive-HTML converter | [markdown-html/](markdown-html/) | +| **🚀 Agent Launcher** ✨v2.12.0 | 6 | Build/launch/grade/schedule **Claude Managed Agents** in your own account — orchestrator (`context: fork` goal router) + `interview` + `stage-launch` + `grade-iterate` (bounded outcome loop) + `run-without-you` (cron deployment loop) + `wrap-up`. Every session starts with a goal; deterministic BYOK scaffolders (no API calls). Re-implements Anthropic's launch-your-agent (Apache-2.0) | [agent-launcher/](agent-launcher/) | --- @@ -354,7 +355,7 @@ Yes. Skills work natively with 13 tools: Claude Code, OpenAI Codex, Gemini CLI, No. We follow semantic versioning and maintain backward compatibility within patch releases. Existing script arguments, plugin source paths, and SKILL.md structures are never changed in patch versions. See the [CHANGELOG](CHANGELOG.md) for details on each release. **Are the Python tools dependency-free?** -Yes. All 644 Python tools use the standard library only — zero pip installs required. Every skill's CLI entry point is verified to run with `--help` (most skills ship one script per tool; a few, like the vendored `engineering/skillopt-sleep` engine, ship a multi-module package behind a single `python -m` entry point). +Yes. All 664 Python tools use the standard library only — zero pip installs required. Every skill's CLI entry point is verified to run with `--help` (most skills ship one script per tool; a few, like the vendored `engineering/skillopt-sleep` engine, ship a multi-module package behind a single `python -m` entry point). **How do I create my own Claude Code skill?** Each skill is a folder with a `SKILL.md` (frontmatter + instructions), optional `scripts/`, `references/`, and `assets/`. See the [Skills & Agents Factory](https://github.com/alirezarezvani/claude-code-skills-agents-factory) for a step-by-step guide. diff --git a/agent-launcher/.claude-plugin/plugin.json b/agent-launcher/.claude-plugin/plugin.json new file mode 100644 index 00000000..0a1e9604 --- /dev/null +++ b/agent-launcher/.claude-plugin/plugin.json @@ -0,0 +1,30 @@ +{ + "name": "agent-launcher-skills", + "description": "Turn Anthropic's launch-your-agent reference skill into a full domain plugin for building Claude Managed Agents (CMA). Every session starts with a goal (./my-agent/goal.json, surfaced by an opt-in AGENT_LAUNCHER_SESSION=1 SessionStart hook and driven by /cs:goal); a deterministic loop_compiler.py compiles that goal into a grade->iterate loop (CMA user.define_outcome self-grading, bounded by max_iterations), a recurring cron scheduled-deployment loop (run without you), or a single-pass interview->stage->launch workflow. 6 skills: agent-launcher-orchestrator (context: fork goal router) + interview (build sheet: primitives table + v1/v2 deferrals + eval plan) + stage-launch (validated env/agent/session/kickoff payloads + resumable BYOK curl launch script that never prints the key) + grade-iterate (outcome/rubric + verdict reader + held-back eval scaffold) + run-without-you (POSIX-cron deployment + NEXT-DIRECTIONS) + wrap-up (primitive inventory + regenerated overview HTML + next upgrades). 18 stdlib-only deterministic scaffolder tools (no network/API calls; live launches emitted as BYOK curl), 4 agents (orchestrator + interviewer + grader + deployer), 8 commands, opt-in SessionStart/SessionEnd hooks, 5 shared references, 4 assets. Validators enforce CMA limits (<=20 skills/session, <=8 memory stores, <=20 roster / 25 threads / depth-1 multiagent, max_iterations 1..20, <=20 creds/vault, <=1000 deployments/org). Inspired by anthropics/launch-your-agent (Apache-2.0); independent re-implementation, not a fork.", + "version": "2.12.0", + "author": { + "name": "Alireza Rezvani", + "url": "https://alirezarezvani.com" + }, + "homepage": "https://github.com/alirezarezvani/claude-skills/tree/main/agent-launcher", + "repository": "https://github.com/alirezarezvani/claude-skills", + "license": "MIT", + "skills": [ + "./skills/agent-launcher-orchestrator", + "./skills/interview", + "./skills/stage-launch", + "./skills/grade-iterate", + "./skills/run-without-you", + "./skills/wrap-up" + ], + "source": { + "spec": "agent-launcher/SPEC.md", + "build_pattern": "Domain plugin (research-ops / markdown-html shape). Orchestrator skill uses context: fork and a deterministic exit-code goal router. Every session carries a goal (./my-agent/goal.json); loop_compiler.py compiles goal+phase into a grade->iterate loop, a cron deployment loop, or a single-pass workflow. All tools are stdlib-only deterministic scaffolders (no API calls); live CMA launches are emitted as BYOK curl scripts. Opt-in SessionStart hook (AGENT_LAUNCHER_SESSION=1) surfaces the goal. Every SKILL.md ships a Forcing-question library per Matt Pocock grill-with-docs discipline.", + "distinct_from": "Inspired by anthropics/launch-your-agent (Apache-2.0) but an independent re-implementation for this marketplace, not a fork. Distinct from engineering/agent-harness (generic bounded self-verifying loop over any of the 18 repo domains) — agent-launcher is specifically about scaffolding and launching Claude Managed Agents in the user's own Anthropic account. Distinct from engineering/write-a-skill (authors Claude Code skills, not CMAs) and engineering/autoresearch-agent (Karpathy file-optimization loop)." + }, + "attribution": { + "inspired_by": "anthropics/launch-your-agent", + "upstream_license": "Apache-2.0", + "note": "Independent plugin re-implementation of the launch-your-agent four-phase CMA workflow. CMA primitive semantics drawn from the public Claude Managed Agents documentation. Not a fork; no upstream code copied verbatim." + } +} diff --git a/agent-launcher/CLAUDE.md b/agent-launcher/CLAUDE.md new file mode 100644 index 00000000..2bb3de5b --- /dev/null +++ b/agent-launcher/CLAUDE.md @@ -0,0 +1,61 @@ +# CLAUDE.md — agent-launcher domain + +Guidance for working inside `agent-launcher/`. See the root `CLAUDE.md` for +repo-wide rules. + +## What this domain is + +A plugin that scaffolds and launches **Claude Managed Agents (CMA)** in the user's +own Anthropic account, organized around a **per-session goal** that compiles into a +**loop or workflow**. Inspired by `anthropics/launch-your-agent` (Apache-2.0); +independent re-implementation, not a fork. + +## Non-negotiable rules (enforced by SPEC.md + validators) + +1. **Deterministic scaffolders only.** Every script under `skills/*/scripts/` is + stdlib-only and makes **no** network/API calls. Live launches are emitted as + BYOK curl. Do not add `requests`, `anthropic`, or any network client. +2. **Never surface the API key.** Launch scripts read `$ANTHROPIC_API_KEY`; no + tool echoes/logs/writes a key. `payload_validator.py` and reviewers check this. +3. **CMA limits are law.** Keep `references/cma-primitives.md` as the source of + truth for ceilings; validators must match it. +4. **Bounded loops only.** `loop_compiler.py` never emits a grade→iterate loop + without a `max_iterations` cap (1..20). +5. **The hook is opt-in and crash-proof.** Gated by `AGENT_LAUNCHER_SESSION=1`; + exits 0 on any error. Never make it fire unconditionally. +6. **The folder is the user's.** All artifacts go under `./my-agent/`; scripts + accept `--out-dir` and default there. Never write into the plugin folder. + +## Structure + +- `skills/agent-launcher-orchestrator/` — `context: fork` goal router + (`goal_router.py`, `goal_state.py`, `loop_compiler.py`). +- `skills/{interview,stage-launch,grade-iterate,run-without-you,wrap-up}/` — one + phase each, 3 tools each. +- `agents/` — `cs-agent-launcher-orchestrator` + 3 phase specialists. +- `commands/` — 8 `/cs:*` commands. +- `hooks/` — opt-in `session_start.py` / `session_end.py` + `hooks.json`. +- `references/` — 5 shared docs. `assets/` — schema + templates + example. + +## Tool conventions + +- Every tool: `argparse` with real `--help`, a `--sample` that runs a deterministic + demo and exits 0, and JSON output via `--json` where a machine reads it. +- Default output dir `./my-agent/`; never assume network access. +- Import shared logic via relative `sys.path` insert (see how the orchestrator + tools import `goal_state`). + +## Forcing-question discipline + +Every SKILL.md ships a "Forcing-question library" (Matt Pocock grill-with-docs): +walk one question at a time, recommend an answer, cite the reference. The +`/cs:grill-agent-launcher` command surfaces them. + +## When editing + +- Changing a CMA limit → update `references/cma-primitives.md` **and** every + validator in lockstep. +- Adding a loop shape → update `loop_compiler.py` **and** + `references/loops-and-workflows.md`. +- Keep `SPEC.md` authoritative; if you ship something different, record it in the + delivery report. diff --git a/agent-launcher/README.md b/agent-launcher/README.md new file mode 100644 index 00000000..416cc7c4 --- /dev/null +++ b/agent-launcher/README.md @@ -0,0 +1,83 @@ +# agent-launcher + +Build, launch, grade, and schedule **Claude Managed Agents (CMA)** in your own +Anthropic account — as a Claude Code plugin where **every session starts with a +goal** and that goal compiles into a **loop or a workflow**. + +Inspired by Anthropic's reference skill +[`anthropics/launch-your-agent`](https://github.com/anthropics/launch-your-agent) +(Apache-2.0). This is an independent re-implementation for the claude-skills +marketplace — not a fork — that adds agents/sub-agents, an opt-in session-start +goal, deterministic scaffolders, and explicit loop/workflow compilation. + +## The four phases + +| Phase | Skill | Command | Loop/workflow | +|---|---|---|---| +| 1 · Interview → Plan | `interview` | `/cs:interview` | single-pass workflow | +| 2 · Stage → Launch | `stage-launch` | `/cs:stage-launch` | single-pass workflow | +| 3 · Grade → Iterate | `grade-iterate` | `/cs:grade` | **grade→iterate loop** (bounded by `max_iterations`) | +| 4 · Run Without You | `run-without-you` | `/cs:run-without-you` | **recurring cron deployment loop** | +| — · Close out | `wrap-up` | `/cs:wrap-up` | — | + +`agent-launcher-orchestrator` (`context: fork`) reads the session goal, routes to +the right phase, and compiles the loop. + +## Every session starts with a goal + +- State lives in `./my-agent/goal.json` (your folder — it keeps working after the + session ends). +- **Set it:** `/cs:goal set "Launch an agent that triages my inbox every morning"`. +- **Resume it automatically:** enable the opt-in hook with + `export AGENT_LAUNCHER_SESSION=1`; the `SessionStart` hook surfaces the current + goal + phase so you pick up exactly where you left off. Disabled by default — no + ambient behavior in unrelated repos. +- **Advance it:** `/cs:goal advance` moves to the next phase. + +## Loops vs workflows + +`loop_compiler.py` compiles the goal + phase into exactly one shape: + +- **single-pass workflow** — interview → plan → stage → launch (Phases 1–2). +- **grade→iterate loop** — CMA `user.define_outcome` self-grading, **bounded** by + `max_iterations` (1..20); never unbounded (Phase 3). +- **recurring deployment loop** — POSIX-cron scheduled deployment that re-runs the + goal "without you", optionally self-grading each firing (Phase 4). + +See [`references/loops-and-workflows.md`](references/loops-and-workflows.md). + +## Safety & hard rules + +- **Deterministic scaffolders only** — every tool is stdlib-only and makes no API + calls. Live launches are emitted as **BYOK curl scripts** you run with your own + `$ANTHROPIC_API_KEY`. The key is never printed, logged, or written. +- Validators enforce CMA limits (≤20 skills/session, ≤8 memory stores, depth-1 + multiagent, `max_iterations` ≤20, …). +- The opt-in hook can never break a session (exits 0 on any error). + +## Quick start + +```bash +export AGENT_LAUNCHER_SESSION=1 # optional: auto-surface the goal each session +/cs:goal set "Nightly repo dependency auditor that writes report.md" +/cs:launch # runs the orchestrator from the current phase +``` + +## Layout + +``` +agent-launcher/ +├── SPEC.md # the build goal (verification target) +├── skills/ # 6 skills, 3 stdlib tools each +├── agents/ # 4 agents (orchestrator + interviewer + grader + deployer) +├── commands/ # 8 /cs:* commands +├── hooks/ # opt-in SessionStart / SessionEnd +├── references/ # 5 shared reference docs +└── assets/ # build-sheet schema, overview + NEXT-DIRECTIONS templates, example +``` + +## Attribution + +Inspired by `anthropics/launch-your-agent` (Apache-2.0). CMA primitive semantics +are drawn from the public [Claude Managed Agents docs](https://platform.claude.com/docs/en/managed-agents/overview). +No upstream code is copied verbatim. License: MIT (this plugin). diff --git a/agent-launcher/SPEC.md b/agent-launcher/SPEC.md new file mode 100644 index 00000000..0dcb7be8 --- /dev/null +++ b/agent-launcher/SPEC.md @@ -0,0 +1,121 @@ +# agent-launcher — Build Spec (the goal) + +**Status:** authoritative build target. The verification workflow scores every +shipped file against this document. Anything shipped that differs from this spec +must be recorded in the delivery report with a reason. + +## What this is + +A Claude Code **plugin** that turns Anthropic's reference skill +[`anthropics/launch-your-agent`](https://github.com/anthropics/launch-your-agent) +(Apache-2.0) into a full domain plugin with **agents, sub-agents, skills, +commands, an opt-in session-start goal, and loops/workflows**. + +The upstream skill walks a technical founder through building a **Claude Managed +Agent (CMA)** in four phases. This plugin keeps that four-phase spine and adds a +repo-native shape: + +- **Every session starts with a goal.** A `./my-agent/goal.json` state file holds + the current goal + phase. An opt-in `SessionStart` hook (env-gated) surfaces it; + the `/cs:goal` command sets/advances it manually. The goal is the single source + of "what are we launching, and where are we in launching it". +- **Goals become loops or workflows.** A deterministic `loop_compiler.py` turns a + goal + phase into one of: + - a **grade→iterate loop** (Phase 3) — CMA `user.define_outcome` self-grading, + bounded by `max_iterations` (1..20); + - a **recurring deployment loop** (Phase 4) — a POSIX-cron scheduled deployment + that re-runs the goal "without you"; + - a **single-pass workflow** (Phases 1–2) — interview → plan → stage → launch. + +## Hard rules (non-negotiable) + +1. **Deterministic scaffolders only.** Every Python tool is stdlib-only and makes + **no** network/API calls. Live launches are emitted as runnable **BYOK curl + scripts** the user executes with their own `$ANTHROPIC_API_KEY`. Complies with + the repo's "no LLM calls in scripts" + ClawHub "no paid dependencies" rules. +2. **Never print the API key.** Launch scripts read `$ANTHROPIC_API_KEY` from the + environment; no tool echoes, logs, or writes a key. +3. **The folder is theirs.** All artifacts land in `./my-agent/` (build sheet, + payloads, launch script, eval scaffold, deployment, overview page, + `NEXT-DIRECTIONS.md`, `goal.json`) and keep working after the session ends. +4. **Everything versioned.** v0 is the core job; v1/v2 capture every deferred item + with its reason and exact mechanism. +5. **Respect CMA limits.** Validators enforce the documented ceilings (≤20 skills/ + session, ≤8 memory stores, ≤20 roster / 25 threads / depth-1 multiagent, + `max_iterations` 1..20, ≤20 creds/vault, ≤1000 deployments/org). +6. **The hook is opt-in and can never break a session.** Gated by + `AGENT_LAUNCHER_SESSION=1`; any error exits 0. + +## Deliverables + +### Skills (6) — orchestrator + 5 phase skills + +| Skill | Phase | Role | +|---|---|---| +| `agent-launcher-orchestrator` | — | `context: fork` session-goal router; classifies the goal, routes to a phase skill, compiles the loop/workflow | +| `interview` | 1 | Interview → Plan: build sheet (primitives table, v1/v2 deferrals, eval plan) | +| `stage-launch` | 2 | Stage → Launch: validated payloads + resumable BYOK curl launch script | +| `grade-iterate` | 3 | Grade → Iterate loop: outcome/rubric, verdict reading, held-back eval scaffold | +| `run-without-you` | 4 | Recurring loop: cron scheduled deployment + NEXT-DIRECTIONS | +| `wrap-up` | — | Close-out: primitive inventory, overview page, next 1–2 upgrades | + +### Tools (18) — 3 deterministic stdlib scaffolders per skill + +- **orchestrator:** `goal_router.py` (goal → lane, exit-code route/ask/refuse), + `goal_state.py` (init/set/status/advance goal.json), `loop_compiler.py` + (goal+phase → plan.v1: grade-loop / cron-loop / single-pass). +- **interview:** `interview_planner.py` (answers → primitives table + deferrals), + `build_sheet_builder.py` (assemble build-sheet.json), `primitives_validator.py` + (validate vs CMA limits → PASS/WARN/FAIL). +- **stage-launch:** `payload_generator.py` (build sheet → env/agent/session/kickoff + JSON payloads), `launch_script_writer.py` (resumable BYOK curl launcher), + `payload_validator.py` (required-field + limit check pre-launch). +- **grade-iterate:** `outcome_builder.py` (`user.define_outcome` payload, clamps + max_iterations), `verdict_reader.py` (grader result → table + next move), + `eval_scaffold.py` (held-back cases + parallel-run plan). +- **run-without-you:** `deployment_builder.py` (`POST /v1/deployments` payload), + `cron_validator.py` (5-field POSIX cron + IANA tz + DST note), + `next_directions_writer.py` (write/refresh NEXT-DIRECTIONS.md). +- **wrap-up:** `primitives_inventory.py` (recap owned primitives), + `overview_page.py` (regenerate agent-overview.html), `upgrade_suggester.py` + (next 1–2 upgrades from deferrals). + +Every tool passes `--help` and `--sample` (exit 0), stdlib-only. + +### Agents (4) + +- `cs-agent-launcher-orchestrator` — session-goal router persona (forks context). +- `cs-agent-interviewer` — Phase-1 interview specialist sub-agent. +- `cs-agent-grader` — Phase-3 grade→iterate loop specialist sub-agent. +- `cs-agent-deployer` — Phase-4 scheduling / run-without-you specialist sub-agent. + +### Commands (8) + +`/cs:launch` (main entry / resume), `/cs:goal` (set/show/advance goal), +`/cs:interview`, `/cs:stage-launch`, `/cs:grade`, `/cs:run-without-you`, +`/cs:wrap-up`, `/cs:grill-agent-launcher` (Matt Pocock docs-anchored grill). + +### Hooks (opt-in, env-gated) + +`hooks/hooks.json` + `hooks/session_start.py` (surface current goal as +`` data; gated by `AGENT_LAUNCHER_SESSION=1`) + +`hooks/session_end.py` (remind to checkpoint goal state; gated by +`AGENT_LAUNCHER_SESSIONEND` default on when session flag set). + +### References (shared, domain-level) + +`cma-primitives.md`, `interview-to-config.md`, `examples-bank.md`, +`loops-and-workflows.md`, `session-goal-model.md` — each citing authoritative +sources incl. the CMA docs and the upstream repo. + +### Assets + +`build-sheet.schema.json`, `agent-overview.template.html`, +`NEXT-DIRECTIONS.template.md`, `example-build-sheet.json`. + +## Attribution + +Inspired by `anthropics/launch-your-agent` (Apache-2.0). This is an independent +plugin re-implementation for the claude-skills marketplace, not a fork; the CMA +primitive semantics are drawn from the public CMA docs. Recorded in +`plugin.json` `source` + this SPEC + README. diff --git a/agent-launcher/agents/cs-agent-deployer.md b/agent-launcher/agents/cs-agent-deployer.md new file mode 100644 index 00000000..901f7ff1 --- /dev/null +++ b/agent-launcher/agents/cs-agent-deployer.md @@ -0,0 +1,36 @@ +--- +name: cs-agent-deployer +description: Phase-4 specialist for making a Claude Managed Agent run without you. Turns a graded agent into a recurring POSIX-cron scheduled deployment (optionally self-grading each firing), an event-driven curl trigger, or confirmed on-demand use, then finalizes NEXT-DIRECTIONS. Invoke for phase=run-without-you. Uses deployment_builder.py, cron_validator.py, next_directions_writer.py. Always tests with a manual run before trusting the schedule; surfaces wall-clock DST caveats. Signature question — "What cadence, and did you fire one manual run first?" +tools: Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion +model: sonnet +--- + +# cs-agent-deployer — Phase 4 specialist (the recurring loop) + +You make the agent run without the founder. A scheduled deployment fires a fresh +session on a cron cadence; each firing can nest an outcome so it self-grades. + +## Voice + +Allergic to: +- Committing a schedule that was never fired once (test with a manual `run` first) +- A cron time that lands in the DST fold (02:00–03:00 in DST zones) +- A recurring loop with no safety rails (always_ask MCP, limited networking, read_only untrusted memory, per-firing max_iterations, workspace spend limit) +- A schedule with no self-grading when the job has a rubric + +Signature opener: **"What cadence should this run on — and did you fire one manual +run to confirm before I leave the cron in place?"** + +## Operating loop + +1. `cron_validator.py --cron … --timezone …` → valid shape + DST note. +2. `deployment_builder.py --sheet … --nest-outcome --out …` → deployment payload + + BYOK curl (create + manual test-run). Fire one manual run, read the verdict. +3. `next_directions_writer.py` → refresh `NEXT-DIRECTIONS.md`. +4. `goal_state.py set --phase wrap-up`, hand to `cs-agent-launcher-orchestrator` / + the `wrap-up` skill. + +## Hard rules + +- Test before you trust. Safety rails on by default. DST is wall-clock — pick safe + times. ≤1,000 deployments/org. Emit BYOK curl; never make API calls or print keys. diff --git a/agent-launcher/agents/cs-agent-grader.md b/agent-launcher/agents/cs-agent-grader.md new file mode 100644 index 00000000..0ac6ec22 --- /dev/null +++ b/agent-launcher/agents/cs-agent-grader.md @@ -0,0 +1,39 @@ +--- +name: cs-agent-grader +description: Phase-3 specialist for the bounded grade→iterate loop when building a Claude Managed Agent. Defines a CMA outcome (required rubric, max_iterations clamped 1..20), reads each grader verdict, decides the next move (sharpen / re-run / escalate / promote), and runs held-back eval cases in parallel once a version passes. Invoke for phase=grade-iterate. Uses outcome_builder.py, verdict_reader.py, eval_scaffold.py. Never emits an unbounded loop. Signature question — "What are the 3–5 rubric lines a good run must satisfy?" +tools: Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion +model: sonnet +--- + +# cs-agent-grader — Phase 3 specialist (the loop) + +You own the grade→iterate loop. CMA's outcome primitive self-grades the agent's +work in an isolated context; you read the verdict, decide the next move, and keep +the loop **bounded**. + +## Voice + +Allergic to: +- An outcome with no rubric (the rubric is the whole point) +- "Just keep improving" (every loop has a `max_iterations` cap) +- Grading generalization on cases the agent already iterated against (hold cases back) +- Acting before reading the grader's explanation + +Signature opener: **"What are the 3–5 rubric lines a good run must satisfy — each +one checkable against the output?"** + +## Operating loop + +1. `outcome_builder.py --sheet … --max-iterations N` → rubric-backed outcome + (clamped 1..20). Send it as a `user.define_outcome` event. +2. On each verdict: `verdict_reader.py --result …` → SHIP / SHARPEN / ESCALATE / + RESUME. Make the single highest-value fix per iteration; each iteration must move + ≥1 rubric line fail→pass. +3. Once a version passes: `eval_scaffold.py` → run held-back cases in parallel + (≤25 threads), graded against the same rubric. +4. Decide: ship v0, or `goal_state.py set --phase run-without-you`. + +## Hard rules + +- Rubric required; loop bounded; held-back cases stay held back. Read the verdict + before acting. diff --git a/agent-launcher/agents/cs-agent-interviewer.md b/agent-launcher/agents/cs-agent-interviewer.md new file mode 100644 index 00000000..e1748eac --- /dev/null +++ b/agent-launcher/agents/cs-agent-interviewer.md @@ -0,0 +1,36 @@ +--- +name: cs-agent-interviewer +description: Phase-1 specialist for building a Claude Managed Agent — interviews the founder through the six intake slots (job, trigger, inputs, actions, definition-of-done, recurrence) and produces a validated build sheet (primitives table + v1/v2 deferrals + eval plan) without needing an API key. Invoke for phase=interview. Uses interview_planner.py, build_sheet_builder.py, primitives_validator.py. Mocks connectors in v0 (schema-true custom tools); real MCP servers become v1 deferrals. Signature question — "What one job — singular — should this agent do end-to-end?" +tools: Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion +model: sonnet +--- + +# cs-agent-interviewer — Phase 1 specialist + +You interview a founder into a build sheet. No API key needed — your output is a +plan. You capture the founder's own words and never invent specifics they didn't +claim. + +## Voice + +Allergic to: +- A vague "an AI that helps with stuff" (force one job, one sentence) +- Deferring the definition of done (the rubric is where the value hides) +- Wiring a real integration before it's needed (mock it in v0; defer the MCP server to v1) + +Signature opener: **"What one job — singular — should this agent do end-to-end?"** + +## Operating loop + +1. Walk the six slots with AskUserQuestion, one at a time, recommending an answer + and citing `references/interview-to-config.md`. +2. `interview_planner.py` → primitives skeleton + deferrals. +3. `build_sheet_builder.py` → `./my-agent/build-sheet.json`. +4. `primitives_validator.py` → fix any FAIL, surface WARN. +5. Record: `goal_state.py set --phase stage-launch --artifact build_sheet=./my-agent/build-sheet.json`. + +## Hard rules + +- v0 is the core job only; everything else is a versioned deferral with a reason + and an exact mechanism. +- Their problem, their words. Mock connectors in v0. diff --git a/agent-launcher/agents/cs-agent-launcher-orchestrator.md b/agent-launcher/agents/cs-agent-launcher-orchestrator.md new file mode 100644 index 00000000..3e1d8332 --- /dev/null +++ b/agent-launcher/agents/cs-agent-launcher-orchestrator.md @@ -0,0 +1,42 @@ +--- +name: cs-agent-launcher-orchestrator +description: Session-goal router for building Claude Managed Agents. Reads ./my-agent/goal.json, routes deterministically to a phase skill (interview → stage-launch → grade-iterate → run-without-you → wrap-up) via goal_router.py, and compiles the goal+phase into a single-pass workflow, a bounded grade→iterate loop, or a recurring cron deployment loop via loop_compiler.py. Forks context so build sheets, payloads, and eval cases stay out of the parent thread. Never makes API calls — emits BYOK curl. Signature forcing question — "What one job should this agent do end-to-end, and what would a good run look like?" +tools: Read, Write, Edit, Glob, Grep, Bash, Skill, AskUserQuestion +model: sonnet +--- + +# cs-agent-launcher-orchestrator — the session-goal router + +You turn a founder's one-sentence goal into a launched Claude Managed Agent (CMA), +one phase at a time. Every session carries a **goal** (`./my-agent/goal.json`); you +read it, route to the right phase, and compile it into a loop or a workflow. Heavy +intake stays in your forked context — the parent gets a digest. + +## Voice + +Allergic to: +- A goal that's two jobs wearing one coat (split it into two `./my-agent-*/` folders) +- Routing on a three-word goal (refuse; get one sentence first) +- Any tool touching the network or the API key (you emit BYOK curl; the founder runs it) +- An "improve forever" loop (every grade loop has a `max_iterations` cap) + +Signature opener: **"What one job should this agent do end-to-end, and what would a +good run look like? That tells me the phase and the loop."** + +## Operating loop + +1. Ensure a goal exists: `goal_state.py status` (else `init`). +2. Route: `goal_router.py --out-dir ./my-agent` → act on exit 0 (route) / 3 (ask the + one printed question) / 4 (refuse; get one sentence). +3. Compile: `loop_compiler.py` → `plan.v1` (single-pass / grade-iterate / cron-loop). +4. Fork to the phase skill with {goal, agent_name, out_dir, plan}. On return, + `goal_state.py advance` and hand the parent a ≤100-word digest. + +## Hard rules + +- Refuse without a goal or on an under-3-word goal. +- Never make API calls; never print the key. +- Bounded loops only. The folder is the founder's (`./my-agent/`). + +Delegate to the phase specialists (`cs-agent-interviewer`, `cs-agent-grader`, +`cs-agent-deployer`) when a phase needs its own focused sub-agent. diff --git a/agent-launcher/assets/NEXT-DIRECTIONS.template.md b/agent-launcher/assets/NEXT-DIRECTIONS.template.md new file mode 100644 index 00000000..753fa52e --- /dev/null +++ b/agent-launcher/assets/NEXT-DIRECTIONS.template.md @@ -0,0 +1,32 @@ +# NEXT-DIRECTIONS — {agent_name} + +Generated by `next_directions_writer.py`. Lives in `./my-agent/`. This is the +versioned roadmap: v0 is live; each row below is a deferred upgrade with its +reason and exact mechanism. + +## v0 (live) + +> {goal} + +- **Loop shape:** {loop_shape} +- **Live primitives:** {live_primitives} +- **Last graded verdict:** {last_verdict} + +## Deferred upgrades + +| Version | Item | Why deferred | Exact mechanism to ship it | +|---|---|---|---| +{deferral_rows} + +## Suggested next 1–2 moves + +{next_moves} + +## Operating notes + +- Test any schedule change with a manual deployment `run` before committing. +- Keep MCP toolsets on `always_ask`; keep untrusted-input memory `read_only`. +- Each config change to the agent mints a new version — pin the version in the + deployment once a run passes. + +_Last updated: {updated_at}_ diff --git a/agent-launcher/assets/agent-overview.template.html b/agent-launcher/assets/agent-overview.template.html new file mode 100644 index 00000000..35753633 --- /dev/null +++ b/agent-launcher/assets/agent-overview.template.html @@ -0,0 +1,40 @@ + +
+ +

{{agent_name}}

+

{{status}} {{loop_shape}}

+
{{goal}}
+ +

Primitives owned

+ + + + {{primitives_rows}} + +
PrimitiveValue
+ +

Latest run

+ + + + + + +
Verdict{{last_verdict}}
Iterations{{iterations}}
Schedule{{schedule}}
+ +

Next 1–2 upgrades

+
    {{next_moves}}
+ +
Generated by agent-launcher · {{updated_at}} · Console: {{console_link}}
+
diff --git a/agent-launcher/assets/build-sheet.schema.json b/agent-launcher/assets/build-sheet.schema.json new file mode 100644 index 00000000..684c893f --- /dev/null +++ b/agent-launcher/assets/build-sheet.schema.json @@ -0,0 +1,91 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://github.com/alirezarezvani/claude-skills/agent-launcher/build-sheet.schema.json", + "title": "agent-launcher build sheet", + "description": "The Phase-1 plan for a Claude Managed Agent. Produced by build_sheet_builder.py, validated by primitives_validator.py, consumed by payload_generator.py.", + "type": "object", + "required": ["agent_name", "goal", "primitives"], + "additionalProperties": false, + "properties": { + "agent_name": {"type": "string", "minLength": 1, "maxLength": 128}, + "goal": {"type": "string", "minLength": 1, "description": "One-sentence job the agent does end-to-end."}, + "primitives": { + "type": "object", + "required": ["agent", "environment"], + "additionalProperties": false, + "properties": { + "agent": { + "type": "object", + "required": ["model"], + "properties": { + "model": {"type": "string"}, + "system": {"type": "string"}, + "tools": {"type": "array", "items": {"type": "object"}}, + "mcp_servers": {"type": "array", "items": {"type": "object"}}, + "skills": {"type": "array", "items": {"type": "string"}, "maxItems": 20}, + "multiagent": {"type": "object"} + } + }, + "environment": { + "type": "object", + "required": ["type"], + "properties": { + "type": {"enum": ["cloud", "self_hosted"]}, + "networking": {"enum": ["unrestricted", "limited"]}, + "allowed_hosts": {"type": "array", "items": {"type": "string"}}, + "packages": {"type": "object"} + } + }, + "session": { + "type": "object", + "properties": { + "resources": {"type": "array", "items": {"type": "object"}}, + "vault_ids": {"type": "array", "items": {"type": "string"}}, + "memory_stores": {"type": "array", "maxItems": 8, "items": {"type": "object"}} + } + }, + "outcome": { + "type": "object", + "properties": { + "description": {"type": "string"}, + "rubric": {"type": "string", "description": "Required markdown criteria for the grader."}, + "max_iterations": {"type": "integer", "minimum": 1, "maximum": 20} + } + }, + "deployment": { + "type": "object", + "properties": { + "schedule": { + "type": "object", + "required": ["expression", "timezone"], + "properties": { + "expression": {"type": "string", "description": "5-field POSIX cron."}, + "timezone": {"type": "string", "description": "IANA identifier."} + } + } + } + } + } + }, + "deferrals": { + "type": "array", + "items": { + "type": "object", + "required": ["version", "item", "reason", "mechanism"], + "properties": { + "version": {"type": "string", "pattern": "^v[1-9][0-9]*$"}, + "item": {"type": "string"}, + "reason": {"type": "string"}, + "mechanism": {"type": "string"} + } + } + }, + "eval_plan": { + "type": "object", + "properties": { + "success_criteria": {"type": "array", "items": {"type": "string"}}, + "held_back_cases": {"type": "array", "items": {"type": "object"}} + } + } + } +} diff --git a/agent-launcher/assets/example-build-sheet.json b/agent-launcher/assets/example-build-sheet.json new file mode 100644 index 00000000..d0e853b3 --- /dev/null +++ b/agent-launcher/assets/example-build-sheet.json @@ -0,0 +1,62 @@ +{ + "agent_name": "support-triage", + "goal": "Every morning, read overnight support emails and label each urgent / question / bug / spam with a one-line reason.", + "primitives": { + "agent": { + "model": "claude-opus-4-8", + "system": "You triage overnight support email. For each message assign exactly one label (urgent | question | bug | spam) and a one-line reason grounded in the email text. Never invent facts not in the email.", + "tools": [{"type": "agent_toolset_20260401"}], + "mcp_servers": [], + "skills": [] + }, + "environment": { + "type": "cloud", + "networking": "unrestricted", + "packages": {} + }, + "session": { + "resources": [], + "vault_ids": [], + "memory_stores": [ + {"access": "read_only", "instructions": "Past label decisions for consistency."} + ] + }, + "outcome": { + "description": "Label every overnight support email with one category and a grounded one-line reason.", + "rubric": "- Every email has exactly one label\n- Each label is one of: urgent, question, bug, spam\n- Each reason quotes or paraphrases the email (no invented facts)\n- Urgent is reserved for outages / paying-customer blockers", + "max_iterations": 5 + }, + "deployment": { + "schedule": { + "expression": "0 9 * * *", + "timezone": "Europe/Berlin" + } + } + }, + "deferrals": [ + { + "version": "v1", + "item": "Real Gmail read via MCP", + "reason": "OAuth vault credential not yet registered", + "mechanism": "Register mcp_oauth cred for the Gmail MCP server; replace the mock label_email custom tool with the real MCP toolset (always_ask)." + }, + { + "version": "v2", + "item": "Auto-draft replies to question-labeled emails", + "reason": "Out of v0 scope; needs send-safe review", + "mechanism": "Add a save_draft custom tool (always_ask); keep sending as a human step." + } + ], + "eval_plan": { + "success_criteria": [ + "100% of emails labeled", + "0 invented facts in reasons", + "Urgent precision >= 0.9 on held-back set" + ], + "held_back_cases": [ + {"id": "case-1", "input": "Angry paying customer: dashboard down since 3am", "expect_label": "urgent"}, + {"id": "case-2", "input": "How do I export to CSV?", "expect_label": "question"}, + {"id": "case-3", "input": "Buy cheap watches!!!", "expect_label": "spam"} + ] + } +} diff --git a/agent-launcher/commands/cs-goal.md b/agent-launcher/commands/cs-goal.md new file mode 100644 index 00000000..c8357fe4 --- /dev/null +++ b/agent-launcher/commands/cs-goal.md @@ -0,0 +1,21 @@ +--- +description: Set, show, or advance the per-session agent-launcher goal (./my-agent/goal.json) — the through-line of a CMA launch. Backs the opt-in SessionStart hook. Subcommands map to goal_state.py init/set/status/advance. +argument-hint: "set \"\" | status | advance | phase " +--- + +# /cs:goal — manage the session goal + +The goal is one sentence for one agent. It selects the phase and the loop shape. + +**$ARGUMENTS** + +Run `goal_state.py` under `agent-launcher/skills/agent-launcher-orchestrator/scripts/`: + +- `set ""` → `goal_state.py init --goal ""` (or `set --goal` if it exists). +- `status` → `goal_state.py status` (prints goal, agent_name, phase, phases_done, loop). +- `advance` → `goal_state.py advance` (moves to the next phase). +- `phase ` → `goal_state.py set --phase ` (interview | stage-launch | + grade-iterate | run-without-you | wrap-up | done). + +Enable auto-surfacing each session with `export AGENT_LAUNCHER_SESSION=1` (the +opt-in SessionStart hook). Two jobs → two goals in two `./my-agent-*/` folders. diff --git a/agent-launcher/commands/cs-grade.md b/agent-launcher/commands/cs-grade.md new file mode 100644 index 00000000..71d845cc --- /dev/null +++ b/agent-launcher/commands/cs-grade.md @@ -0,0 +1,23 @@ +--- +description: Phase 3 — the bounded grade→iterate loop. Define a CMA outcome (required rubric, max_iterations 1..20), read each grader verdict, decide the next move, and run held-back eval cases once a version passes, via the grade-iterate skill. Never unbounded. +argument-hint: "[optional: path to build-sheet.json]" +--- + +# /cs:grade — Phase 3: Grade → Iterate + +Run the `grade-iterate` skill. + +**$ARGUMENTS** + +## Steps + +1. `python3 agent-launcher/skills/grade-iterate/scripts/outcome_builder.py --sheet ./my-agent/build-sheet.json --max-iterations 5 --out ./my-agent/payloads/outcome.json` + — rubric required; send as a `user.define_outcome` event. +2. On each verdict: `python3 agent-launcher/skills/grade-iterate/scripts/verdict_reader.py --result ./my-agent/last-verdict.json` + → SHIP / SHARPEN / ESCALATE / RESUME. Each iteration must move ≥1 rubric line + fail→pass. +3. Once a version passes: `python3 agent-launcher/skills/grade-iterate/scripts/eval_scaffold.py --sheet ./my-agent/build-sheet.json --out ./my-agent/eval.json` + — held-back cases in parallel (≤25 threads). +4. Decide: ship v0, or `goal_state.py set --phase run-without-you`. + +Bounded loops only. Read the verdict before acting. Held-back cases stay held back. diff --git a/agent-launcher/commands/cs-grill-agent-launcher.md b/agent-launcher/commands/cs-grill-agent-launcher.md new file mode 100644 index 00000000..c6ec9224 --- /dev/null +++ b/agent-launcher/commands/cs-grill-agent-launcher.md @@ -0,0 +1,32 @@ +--- +description: Matt Pocock docs-anchored grill for an agent-launcher goal — walks the phase's forcing questions ONE at a time, each with a recommended answer and a citation to a reference doc, refusing to advance on a fuzzy input. Use to pressure-test a CMA plan before launching. +argument-hint: "[optional: phase name to grill — interview | grade-iterate | run-without-you]" +--- + +# /cs:grill-agent-launcher — pressure-test the plan + +Grill the current goal's phase using its SKILL.md "Forcing-question library". + +**$ARGUMENTS** + +## Discipline + +- **One question per turn.** Never batch. Wait for the answer before the next. +- **Recommend an answer.** Lead with the strongest default and why. +- **Cite the canon.** Each question names its reference doc (cma-primitives.md, + interview-to-config.md, loops-and-workflows.md, session-goal-model.md). +- **Refuse to advance on fuzz.** If the answer is vague, restate the question with a + sharper recommended option. + +## Question sources + +| Phase | Forcing questions live in | +|---|---| +| interview | `skills/interview/SKILL.md` | +| stage-launch | `skills/stage-launch/SKILL.md` | +| grade-iterate | `skills/grade-iterate/SKILL.md` | +| run-without-you | `skills/run-without-you/SKILL.md` | +| wrap-up | `skills/wrap-up/SKILL.md` | +| (whole plan) | `skills/agent-launcher-orchestrator/SKILL.md` | + +Start with the orchestrator's five questions unless `$ARGUMENTS` names a phase. diff --git a/agent-launcher/commands/cs-interview.md b/agent-launcher/commands/cs-interview.md new file mode 100644 index 00000000..d0f52a41 --- /dev/null +++ b/agent-launcher/commands/cs-interview.md @@ -0,0 +1,23 @@ +--- +description: Phase 1 — interview the founder into a validated CMA build sheet (primitives table + v1/v2 deferrals + eval plan) via the interview skill. No API key needed. Walks the six intake slots, maps to primitives, validates against CMA limits. +argument-hint: "[optional: a one-line description of the agent's job]" +--- + +# /cs:interview — Phase 1: Interview → Plan + +Run the `interview` skill. + +**$ARGUMENTS** + +## Steps + +1. Walk the six intake slots (job, trigger, inputs, actions, definition-of-done, + recurrence) with AskUserQuestion — one at a time, recommend + cite. +2. `python3 agent-launcher/skills/interview/scripts/interview_planner.py --job "..." ... --out ./my-agent/plan.json` +3. `python3 agent-launcher/skills/interview/scripts/build_sheet_builder.py --plan ./my-agent/plan.json --out-dir ./my-agent` +4. `python3 agent-launcher/skills/interview/scripts/primitives_validator.py --sheet ./my-agent/build-sheet.json` + — fix FAIL, surface WARN. +5. `goal_state.py set --phase stage-launch --artifact build_sheet=./my-agent/build-sheet.json`. + +Mock connectors in v0 (schema-true custom tools); wire real MCP servers as v1 +deferrals. v0 is the core job only. diff --git a/agent-launcher/commands/cs-launch.md b/agent-launcher/commands/cs-launch.md new file mode 100644 index 00000000..b50426c3 --- /dev/null +++ b/agent-launcher/commands/cs-launch.md @@ -0,0 +1,28 @@ +--- +description: Main entry / resume for building a Claude Managed Agent. Runs the agent-launcher-orchestrator skill from the current session goal — reads ./my-agent/goal.json, routes to the right phase (interview → stage-launch → grade-iterate → run-without-you → wrap-up), compiles the loop/workflow, and forks to the phase skill. Emits BYOK curl; never makes API calls or prints the key. +argument-hint: "[optional: a one-sentence goal to set first]" +--- + +# /cs:launch — build/resume a Claude Managed Agent + +Route through the `agent-launcher-orchestrator` skill. + +**$ARGUMENTS** + +## Steps + +1. If `$ARGUMENTS` is a goal and no `./my-agent/goal.json` exists, set it: + `python3 agent-launcher/skills/agent-launcher-orchestrator/scripts/goal_state.py init --goal "$ARGUMENTS"`. +2. Route from the current phase: + `python3 agent-launcher/skills/agent-launcher-orchestrator/scripts/goal_router.py --out-dir ./my-agent` + — act on exit 0 (route) / 3 (ask the printed question) / 4 (refuse; get one sentence). +3. Compile the loop: + `python3 agent-launcher/skills/agent-launcher-orchestrator/scripts/loop_compiler.py --out-dir ./my-agent`. +4. Invoke the routed phase skill; on completion, `goal_state.py advance` and print a + ≤100-word digest (phase done, artifact paths, loop shape, one next step). + +## Refusals + +- No goal set → run `/cs:goal set "..."` first. +- Under-3-word goal → get one sentence naming the one job. +- Never touch the network or the API key. diff --git a/agent-launcher/commands/cs-run-without-you.md b/agent-launcher/commands/cs-run-without-you.md new file mode 100644 index 00000000..bad6d61a --- /dev/null +++ b/agent-launcher/commands/cs-run-without-you.md @@ -0,0 +1,24 @@ +--- +description: Phase 4 — make the agent run without you. Turn a graded agent into a recurring POSIX-cron scheduled deployment (optionally self-grading each firing), an event-driven curl trigger, or on-demand use, then finalize NEXT-DIRECTIONS, via the run-without-you skill. Tests with a manual run before trusting the schedule. +argument-hint: "[optional: cron expression, e.g. \"0 9 * * *\"]" +--- + +# /cs:run-without-you — Phase 4: Run Without You + +Run the `run-without-you` skill. + +**$ARGUMENTS** + +## Steps + +1. `python3 agent-launcher/skills/run-without-you/scripts/cron_validator.py --cron "0 9 * * *" --timezone Europe/Berlin` + — invalid → exit 1; read the wall-clock DST note. +2. `python3 agent-launcher/skills/run-without-you/scripts/deployment_builder.py --sheet ./my-agent/build-sheet.json --agent-id agent_… --env-id env_… --nest-outcome --out ./my-agent/payloads/deployment.json` + — prints BYOK curl to create + manually test the deployment. +3. Fire ONE manual `run`, read the verdict, then leave the cron in place; pin the + agent version. +4. `python3 agent-launcher/skills/run-without-you/scripts/next_directions_writer.py --sheet ./my-agent/build-sheet.json --loop-shape cron-loop --out-dir ./my-agent` +5. `goal_state.py set --phase wrap-up`. + +Test before you trust. Safety rails on by default. DST is wall-clock. ≤1,000 +deployments/org. diff --git a/agent-launcher/commands/cs-stage-launch.md b/agent-launcher/commands/cs-stage-launch.md new file mode 100644 index 00000000..28201fbc --- /dev/null +++ b/agent-launcher/commands/cs-stage-launch.md @@ -0,0 +1,26 @@ +--- +description: Phase 2 — turn a build sheet into exact API payloads and a resumable BYOK curl launch script, then launch (environment → agent → session → kickoff) with the founder's own key via the stage-launch skill. No tool makes API calls; the key never enters chat. +argument-hint: "[optional: path to build-sheet.json, default ./my-agent/build-sheet.json]" +--- + +# /cs:stage-launch — Phase 2: Stage → Launch + +Run the `stage-launch` skill. + +**$ARGUMENTS** + +## Steps + +1. `python3 agent-launcher/skills/stage-launch/scripts/payload_generator.py --sheet ./my-agent/build-sheet.json --out-dir ./my-agent` +2. `python3 agent-launcher/skills/stage-launch/scripts/launch_script_writer.py --out-dir ./my-agent` +3. `python3 agent-launcher/skills/stage-launch/scripts/payload_validator.py --dir ./my-agent` + — FAIL blocks (especially a key_leak finding). +4. Minimal key step (in the founder's shell, never chat): + `[ -n "$ANTHROPIC_API_KEY" ] && echo present || echo "export ANTHROPIC_API_KEY=... first"`. +5. `export ANTHROPIC_API_KEY=... && ./my-agent/launch.sh` — watch the first poll, + mark checkpoints with Console links, then `goal_state.py set --phase grade-iterate`. + +## Hard rules + +- The key never enters chat, a file, a payload, or a log. +- Sequential launch; watch the first poll foreground. Re-running launch.sh resumes. diff --git a/agent-launcher/commands/cs-wrap-up.md b/agent-launcher/commands/cs-wrap-up.md new file mode 100644 index 00000000..97ed4065 --- /dev/null +++ b/agent-launcher/commands/cs-wrap-up.md @@ -0,0 +1,20 @@ +--- +description: Close out a launched Claude Managed Agent — recap every primitive owned, regenerate the single-file overview page, and suggest the next 1–2 upgrades, via the wrap-up skill. The last stop before phase=done. +argument-hint: "[optional: path to build-sheet.json]" +--- + +# /cs:wrap-up — close it out + +Run the `wrap-up` skill. + +**$ARGUMENTS** + +## Steps + +1. `python3 agent-launcher/skills/wrap-up/scripts/primitives_inventory.py --sheet ./my-agent/build-sheet.json --goal ./my-agent/goal.json` +2. `python3 agent-launcher/skills/wrap-up/scripts/overview_page.py --sheet ./my-agent/build-sheet.json --out-dir ./my-agent --status live` +3. `python3 agent-launcher/skills/wrap-up/scripts/upgrade_suggester.py --sheet ./my-agent/build-sheet.json --top 2` +4. Ensure `NEXT-DIRECTIONS.md` is current, then `goal_state.py advance` → phase=done. + +Recap what's actually live (read from the sheet + goal state). The overview page is +single-file and shareable. Every next move names its exact mechanism. diff --git a/agent-launcher/hooks/hooks.json b/agent-launcher/hooks/hooks.json new file mode 100644 index 00000000..955a6210 --- /dev/null +++ b/agent-launcher/hooks/hooks.json @@ -0,0 +1,25 @@ +{ + "description": "Opt-in SessionStart/SessionEnd hooks for agent-launcher. BOTH are disabled unless AGENT_LAUNCHER_SESSION=1 is set, so they never fire in unrelated repos. SessionStart surfaces the current CMA launch goal (./my-agent/goal.json) as data so a multi-session launch resumes where it stopped. SessionEnd reminds you to checkpoint/advance the goal. Any error exits 0 — a hook can never break a session. Additionally disable SessionEnd with AGENT_LAUNCHER_SESSIONEND=0.", + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session_start.py" + } + ] + } + ], + "SessionEnd": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session_end.py" + } + ] + } + ] + } +} diff --git a/agent-launcher/hooks/session_end.py b/agent-launcher/hooks/session_end.py new file mode 100644 index 00000000..17722371 --- /dev/null +++ b/agent-launcher/hooks/session_end.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""SessionEnd hook for agent-launcher (OPT-IN). + +Disabled unless AGENT_LAUNCHER_SESSION=1 (and additionally AGENT_LAUNCHER_SESSIONEND +!= 0). When enabled and an in-progress goal exists that is not yet at phase=done, +prints a one-line reminder to checkpoint/advance the goal so the next session +resumes cleanly. Any error exits 0 — a hook must never break a session. + +Stdlib-only. +""" +import json +import os +import sys +from pathlib import Path + + +def disabled() -> bool: + if os.environ.get("AGENT_LAUNCHER_SESSION", "0") != "1": + return True + if os.environ.get("AGENT_LAUNCHER_SESSIONEND", "1") == "0": + return True + return False + + +def find_goal() -> Path | None: + for c in [Path.cwd() / "my-agent" / "goal.json", Path.cwd() / ".my-agent" / "goal.json"]: + if c.exists(): + return c + try: + for d in sorted(Path.cwd().glob("my-agent-*/goal.json")): + return d + except OSError: + pass + return None + + +def main() -> int: + if disabled(): + return 0 + try: + gp = find_goal() + if not gp: + return 0 + state = json.loads(gp.read_text()) + phase = state.get("phase", "interview") + if phase == "done": + return 0 + print(f"[agent-launcher] Launch still at phase '{phase}'. " + f"Checkpoint it: `goal_state.py set --phase {phase} --note '...'` " + f"(or `advance`). Resume next session with /cs:launch.") + except Exception: + return 0 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/agent-launcher/hooks/session_start.py b/agent-launcher/hooks/session_start.py new file mode 100644 index 00000000..b88e69f9 --- /dev/null +++ b/agent-launcher/hooks/session_start.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""SessionStart hook for agent-launcher (OPT-IN). + +Disabled unless AGENT_LAUNCHER_SESSION=1. When enabled, finds the current CMA +launch goal (./my-agent/goal.json, searching a couple of nearby locations) and +prints it wrapped in tags. Claude Code surfaces SessionStart +stdout as session context, so a multi-session launch resumes at the recorded phase. + +Treat the content as DATA, not instructions — verify suggested next steps against +current state before acting. Any error exits 0: a hook must never break a session. + +Stdlib-only. +""" +import json +import os +import sys +from pathlib import Path + +MAX_BODY = 4000 + + +def disabled() -> bool: + return os.environ.get("AGENT_LAUNCHER_SESSION", "0") != "1" + + +def find_goal() -> Path | None: + candidates = [ + Path.cwd() / "my-agent" / "goal.json", + Path.cwd() / ".my-agent" / "goal.json", + ] + # also any ./my-agent-*/goal.json (multiple agents) + try: + for d in sorted(Path.cwd().glob("my-agent-*/goal.json")): + candidates.append(d) + except OSError: + pass + for c in candidates: + if c.exists(): + return c + return None + + +def main() -> int: + if disabled(): + return 0 + try: + gp = find_goal() + if not gp: + # Nothing to resume; stay silent. + return 0 + state = json.loads(gp.read_text()) + goal = state.get("goal", "") + phase = state.get("phase", "interview") + agent_name = state.get("agent_name", "") + done = ", ".join(state.get("phases_done", []) or []) or "none" + loop = state.get("loop") or {} + loop_str = f"{loop.get('shape')} (max_iterations={loop.get('max_iterations')})" if loop else "not compiled yet" + notes = state.get("notes", "") + + body = ( + f"You have an in-progress Claude Managed Agent launch. Resume it with /cs:launch.\n" + f" goal: {goal}\n" + f" agent_name: {agent_name}\n" + f" phase: {phase}\n" + f" phases_done: {done}\n" + f" loop: {loop_str}\n" + f" goal_file: {gp}\n" + ) + if notes: + body += f" notes: {notes}\n" + body = body[:MAX_BODY] + + print("") + print(body.rstrip()) + print("") + except Exception: + # Never break a session. + return 0 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/agent-launcher/references/cma-primitives.md b/agent-launcher/references/cma-primitives.md new file mode 100644 index 00000000..50f6ab72 --- /dev/null +++ b/agent-launcher/references/cma-primitives.md @@ -0,0 +1,130 @@ +# Claude Managed Agents — Core Primitives & Limits + +Reference for every tool and skill in this plugin. Semantics are drawn from the +public Claude Managed Agents (CMA) documentation +(https://platform.claude.com/docs/en/managed-agents/overview). Payload shapes the +plugin emits target this contract; always confirm against the live docs before a +production launch, since the API evolves. + +## The four core primitives + +| Primitive | ID prefix | What it is | +|---|---|---| +| **Agent** | `agent_…` | Reusable, **versioned** config: model, system prompt, tools, MCP servers, skills, optional multiagent roster. Each config-changing update mints a **new version**; immutable once created. | +| **Environment** | `env_…` | Execution context (Anthropic `cloud` sandbox or `self_hosted`). **Unversioned**; pre-installs + caches packages across sessions. | +| **Session** | `sesn_…` | One agent instance: isolated container, conversation history, sandbox state. Two-step: **create** (provisions sandbox, starts `idle`) → **send event** to start work. | +| **Event** | — | Bidirectional `{domain}.{action}` messages (`user.message`, `agent.tool_use`, `session.status_idle`). Streamed over SSE; each carries `processed_at`. | + +## Agent configuration + +- **Required:** `name`, `model` (Claude 4.5-family or later). +- **Optional:** `system`, `tools`, `mcp_servers`, `skills`, `multiagent`, + `description`, `metadata`. +- **Versioning:** array fields (`tools`, `mcp_servers`, `skills`) are + full-replacement — omit to preserve, `[]`/`null` to clear. `metadata` merges + per-key. Sessions pin a version `{"type":"agent","id":"…","version":N}` or + default to latest by string ID. + +## Environment configuration + +- `config.type`: `cloud` (Anthropic-managed) or `self_hosted`. +- Package managers run alphabetically: `apt`, `cargo`, `gem`, `go`, `npm`, `pip`; + pinning supported (`pandas==2.2.0`, `express@4.18.0`). +- Networking: `unrestricted` (default, full outbound minus safety blocklist) or + `limited` (only `allowed_hosts` bare hostnames / `*.wildcard`, plus MCP + + package-manager toggles). + +## Session lifecycle + +- Statuses: `idle` (awaiting input) → `running` → `rescheduling` (transient) or + `terminated` (unrecoverable). +- Checkpoints kept **30 days** after last activity; resume by sending a + `user.message` (resets the timer). +- Resources at creation: `memory_store`, `file`, `repository`. File/repo + updatable mid-session; memory stores attach **only at creation**. +- Token tracking: cumulative `input_tokens`, `output_tokens`, + `cache_creation_input_tokens`, `cache_read_input_tokens` (5-min cache TTL). + +## Tools & permissions + +- **Prebuilt agent toolset:** `{"type":"agent_toolset_20260401"}` — `bash`, + `read`, `write`, `edit`, `glob`, `grep`, `web_fetch`, `web_search`. +- **Custom tools:** client-executed `{"type":"custom",…}` with `input_schema`. + Flow: `agent.custom_tool_use` → session idles `requires_action` → return + `user.custom_tool_result`. +- **Permission policies:** `always_allow` (auto) / `always_ask` (pause for + `user.tool_confirmation`). Convention: **agent toolset → `always_allow`, MCP + toolset → `always_ask`** so new MCP tools can't run unapproved. + +## Outcomes — the self-grading loop (Phase 3 grade→iterate) + +- Send `user.define_outcome` with `description` (task), **required** `rubric` + (markdown criteria), optional `max_iterations` (default 3, **max 20**). +- Grader is auto-provisioned in a **separate context window** (isolated from the + agent's choices); returns pass/fail explanation fed back for the next iteration. +- Results: `satisfied`, `needs_revision`, `max_iterations_reached`, `failed`, + `interrupted`. One outcome per session, but chainable after a terminal event. + +## Memory stores (cross-session persistence) + +- Workspace-scoped text collections surviving across sessions. +- Limits: **≤100 kB (~25k tokens) per store; ≤2,000 memories per store; ≤8 stores + per session.** +- Attach at session creation: `{type:"memory_store", memory_store_id, access, + instructions}`; access `read_write` (default) or `read_only`. +- Mounted at `/mnt/memory/`; writes sync back and mint immutable `memver_…` + versions (30-day retention, no restore — re-write instead). +- ⚠️ `read_write` + untrusted input = prompt-injection can poison future sessions. + +## Vaults & credentials + +- Register third-party creds once, reference by `vault_ids` at session creation. +- Categories: `mcp_oauth` (Anthropic auto-refresh), `static_bearer`, + `environment_variable` (substituted at egress; not visible to the agent). +- Limits: unique key per vault, keys immutable (archive+recreate), **≤20 + creds/vault.** + +## Multi-agent sessions + +- One **coordinator** delegates to a roster, each agent in its own context-isolated + session **thread**; all threads share sandbox, filesystem, vault creds. +- Declare `multiagent:{type:"coordinator", agents:[…]}`. +- Limits: **depth 1 only; ≤20 unique agents; ≤25 concurrent threads.** + +## Scheduled deployments — native cron (Phase 4 recurring loop) + +- A **deployment** (`depl_…`) kicks off sessions on a recurring schedule. +- Create: `POST /v1/deployments` with `name`, `agent` (id or pinned version), + `environment_id`, **`initial_events`** (must include `user.message`; can carry + `user.define_outcome`), `schedule:{type:"cron", expression, timezone}`. +- Schedule: standard **5-field POSIX cron** (minute granularity); `timezone` = + IANA id; **wall-clock DST** (literal local time; spring-forward nonexistent + times skipped, fall-back times fire twice). +- Each firing → a session, tracked as a **deployment run** (`drun_…`). +- Lifecycle: pause / unpause / archive / manual `run` (immediate test). +- Limit: **1,000 deployments/org.** + +## Rate limits & structural constraints + +- API: 300 req/min (create), 600 req/min (read) per org. +- Outcome `max_iterations`: default 3, max 20. +- Skills: **20 per session.** +- Memory: 100 kB / memory, 2,000 / store, 8 stores / session. +- Multiagent: 20 roster, 25 threads, depth 1. +- Vault credentials: 20 per vault. +- Checkpoints: 30-day retention. + +## Not eligible / notable + +- Not eligible for Zero Data Retention or HIPAA BAA (stateful by design). +- `system.message` mid-session: Opus-4.8-only. +- No spend cap inside CMA — limits are workspace-level. +- MCP tunnels for private servers: limited research preview. + +## Sources + +1. Claude Managed Agents — Overview. https://platform.claude.com/docs/en/managed-agents/overview +2. anthropics/launch-your-agent — `cma-primitives.md`. https://github.com/anthropics/launch-your-agent +3. anthropics/launch-your-agent — `interview-to-config.md`. +4. Anthropic API reference — messages, tool use, streaming (SSE) conventions. +5. Anthropic engineering — "Building effective agents" (workflow vs autonomous loop framing). diff --git a/agent-launcher/references/examples-bank.md b/agent-launcher/references/examples-bank.md new file mode 100644 index 00000000..cbceba05 --- /dev/null +++ b/agent-launcher/references/examples-bank.md @@ -0,0 +1,60 @@ +# Examples bank — CMA agents worth launching + +Concrete v0 scopes the interview can anchor on. Each shows the one job, the loop +shape, and the first deferral. Use these to open the interview warmly, then build +from the founder's own words — never substitute an example for what they stated. + +## 1. Support-inbox triage (recurring loop) +- **Job:** every morning, read overnight support emails and label each + urgent / question / bug / spam with a one-line reason. +- **Primitives:** cloud env (unrestricted) · agent toolset · Gmail MCP + (`always_ask`) · `read_only` memory of past labels · outcome rubric on label + accuracy · cron `0 9 * * *`. +- **Loop:** recurring deployment, each firing self-grades (nested outcome). +- **v0 mock:** custom `label_email` tool returns a mock result; **v1** wires the + real Gmail MCP when the OAuth vault cred lands. + +## 2. Nightly repo dependency auditor (recurring loop) +- **Job:** scan the repo for outdated / vulnerable dependencies and write a + `report.md`. +- **Primitives:** cloud env with `npm` + `pip` · `repository` resource · agent + toolset · outcome rubric ("names every advisory with a fix") · cron `0 2 * * *`. +- **Loop:** recurring deployment. +- **v1:** open a PR with the bumps (needs GitHub MCP). + +## 3. Weekly competitor pulse (recurring loop) +- **Job:** each Monday, fetch three competitor changelogs and summarize what + changed for our roadmap. +- **Primitives:** `limited` networking (`allowed_hosts` = the three domains) · + `web_fetch` · `read_write` memory of prior weeks · outcome rubric · cron + `0 8 * * 1`. +- **Loop:** recurring deployment; memory makes week 10 sharper than week 1. + +## 4. One-shot data cleaner (single-pass → grade loop) +- **Job:** normalize a messy CSV to a target schema and validate every row. +- **Primitives:** cloud env with `pandas==2.2.0` · `file` resource · agent + toolset · outcome rubric on schema conformance · `max_iterations: 5`. +- **Loop:** grade→iterate until `satisfied`; no schedule (runs on request). + +## 5. Draft-only sales-email responder (grade loop, human-in-loop) +- **Job:** draft (never send) a reply to an inbound sales email in our voice. +- **Primitives:** agent toolset · `read_only` memory of our voice guide · outcome + rubric on tone + accuracy · custom `save_draft` tool (`always_ask`). +- **Loop:** grade→iterate; sending stays a human step (v1 = wire real send behind + `always_ask`). + +## Pattern notes + +- **Mock connectors in v0.** Every example that needs a real integration ships a + schema-true custom-tool mock first; the real MCP server is the first v1 deferral. +- **Memory is opt-in.** Only examples that benefit from cross-run learning attach a + store; the rest skip it to reduce surface. +- **Every recurring loop nests an outcome** so each firing self-grades. + +## Sources + +1. anthropics/launch-your-agent — `examples-bank.md`. +2. Claude Managed Agents — Overview (use-case framing). +3. Anthropic — "Building effective agents" (agent use-case taxonomy). +4. Anthropic customer stories — internal-worker / product-feature agent patterns. +5. This repo — engineering/dependency-auditor, marketing pulse, research/pulse (analogous jobs). diff --git a/agent-launcher/references/interview-to-config.md b/agent-launcher/references/interview-to-config.md new file mode 100644 index 00000000..bd17df95 --- /dev/null +++ b/agent-launcher/references/interview-to-config.md @@ -0,0 +1,61 @@ +# Interview → CMA config mapping + +How Phase-1 interview answers map to CMA primitives. `interview_planner.py` +implements this table deterministically. + +## The six intake slots + +| Slot | Question | Maps to | +|---|---|---| +| **Job** | "What one job should this agent do end-to-end?" | agent `system` + outcome `description` | +| **Trigger** | "What kicks it off — you ask it, an event, or a schedule?" | on-demand session vs event `user.message` vs `schedule.cron` deployment | +| **Inputs** | "What does it read? (files, repo, memory, third-party systems)" | `file` / `repository` / `memory_store` resources; `mcp_servers` + `vault_ids` | +| **Actions** | "What does it do? (draft, write files, call APIs, run code)" | agent toolset vs custom tools vs MCP tools + permission policy | +| **Definition of done** | "How would you grade a good run?" | outcome `rubric` (required, markdown) | +| **Recurrence** | "Does it run once, on request, or on a cadence?" | single-pass vs grade→iterate loop vs cron deployment loop | + +## Mapping rules + +1. **Connectors are mockable in v0.** If a real MCP server / credential isn't ready, + default to a **custom tool** with a schema-true `input_schema` (the agent calls + it; the founder returns a mock `user.custom_tool_result`), or to a **draft** + action. Wire the real MCP server as **v1** when credentials arrive. Record the + deferral with its reason and exact mechanism. +2. **Their problem, their words.** Populate `name`, `system`, and `rubric` from what + the founder actually stated. Never invent specifics they didn't claim. +3. **Permission defaults.** Agent toolset → `always_allow`; every MCP toolset → + `always_ask`. +4. **Networking defaults.** Start `unrestricted` for v0 cloud; tighten to `limited` + with an explicit `allowed_hosts` list as a v1 hardening step. +5. **Memory only if run #10 should beat run #1.** Attach a `read_write` memory store + only when the job benefits from cross-session learning; otherwise skip it (a + store attaches only at creation and adds prompt-injection surface). +6. **Recurrence decides the loop.** once → single-pass workflow; "grade until good" + → grade→iterate loop (`max_iterations`); "every morning / weekly" → cron + deployment loop. + +## Build-sheet shape (produced by `build_sheet_builder.py`) + +``` +{ + "agent_name": "...", + "goal": "one-sentence job", + "primitives": { + "agent": {"model": "...", "system": "...", "tools": [...], "mcp_servers": [...], "skills": [...]}, + "environment": {"type": "cloud", "networking": "unrestricted", "packages": {...}}, + "session": {"resources": [...], "vault_ids": [...]}, + "outcome": {"description": "...", "rubric": "markdown", "max_iterations": 3}, + "deployment": {"schedule": {"expression": "0 9 * * *", "timezone": "..."}} // optional + }, + "deferrals": [{"version": "v1", "item": "...", "reason": "...", "mechanism": "..."}], + "eval_plan": {"success_criteria": [...], "held_back_cases": [...]} +} +``` + +## Sources + +1. anthropics/launch-your-agent — `interview-to-config.md`. +2. Claude Managed Agents — Overview (primitive semantics). +3. Anthropic — "Building effective agents": workflows (predefined paths) vs agents (dynamic). +4. Teresa Torres — *Continuous Discovery Habits* (interview → opportunity mapping discipline). +5. Amy Hoy / Jobs-to-be-Done — "job the customer hires the product to do". diff --git a/agent-launcher/references/loops-and-workflows.md b/agent-launcher/references/loops-and-workflows.md new file mode 100644 index 00000000..16cb5df2 --- /dev/null +++ b/agent-launcher/references/loops-and-workflows.md @@ -0,0 +1,72 @@ +# Loops and workflows — how a goal compiles + +`loop_compiler.py` turns a session **goal + phase** into exactly one execution +shape. This file is the decision table it implements. + +## The three shapes + +### 1. Single-pass workflow (Phases 1–2) +A fixed, ordered path with no repeat: **interview → plan → validate → stage → +launch**. Deterministic, no self-grading. Used while the agent is still being +scoped and first launched. Anthropic's "Building effective agents" calls this a +*workflow*: predefined code paths orchestrate the steps. + +Terminal state: a live session exists and produced its first output. + +### 2. Grade→iterate loop (Phase 3) — bounded +The CMA **outcome** primitive. Send `user.define_outcome` with a required +`rubric`; an isolated grader returns pass/fail; failing verdicts feed the next +attempt. The loop is **bounded by `max_iterations`** (default 3, max 20) — never +unbounded. This is the plugin's answer to "make it good", not "run it forever". + +Loop invariant: each iteration must move a rubric line from fail→pass or the run +halts at `max_iterations_reached` and escalates to the founder. Verdict-reading +(`verdict_reader.py`) decides the next move: **sharpen** the prompt/tools, +**re-run** as-is, or **promote to schedule**. + +Terminal states: `satisfied` (ship it), `max_iterations_reached` / `failed` +(escalate), `interrupted` (resume). + +### 3. Recurring deployment loop (Phase 4) — cron +A **scheduled deployment** (`depl_…`) fires a fresh session on a POSIX-cron +cadence — "run without you". Each firing is a `drun_…`. Optionally each firing +carries its own `user.define_outcome`, nesting a bounded grade→iterate loop +*inside* each recurring run. + +Terminal state: none by design — it runs until paused/archived. Safety comes from +`always_ask` MCP permissions, `limited` networking, `read_only` memory where +possible, `max_iterations` per firing, and workspace spend limits. Always test +with a manual `run` before committing the schedule. + +## Decision table (goal.phase → shape) + +| Phase | Recurrence answer | Shape | +|---|---|---| +| interview / stage-launch | any | single-pass workflow | +| grade-iterate | "make it good", "grade it" | grade→iterate loop (bounded) | +| run-without-you | "every morning", "weekly", cron given | recurring deployment loop | +| run-without-you | "when X happens" (event) | event-driven curl (documented, not scheduled) | +| run-without-you | "only when I ask" | on-demand (no deployment) | + +## Nesting rule + +The most valuable production shape is a **cron loop whose `initial_events` +include a `user.define_outcome`** — every scheduled firing self-grades before it +finishes. `deployment_builder.py` supports this by accepting the outcome payload +from `outcome_builder.py`. + +## Why bounded beats unbounded + +An unbounded "keep improving" loop has no terminal state and burns budget with no +guarantee of convergence. CMA's `max_iterations` is a hard cap; this plugin never +emits a loop without one. Mirrors the repo's own loop-discipline canon +(engineering/agent-harness AR5, loop-library stop-states, tc-tracker). + +## Sources + +1. Anthropic — "Building effective agents" (workflows vs agents; prompt chaining, evaluator-optimizer). +2. Claude Managed Agents — Overview (outcomes, scheduled deployments). +3. anthropics/launch-your-agent — Phase 3 / Phase 4 design. +4. Google SRE Workbook — error budgets & bounded retries (loop-discipline analogue). +5. POSIX crontab(5) — 5-field schedule semantics. +6. IANA Time Zone Database — DST wall-clock behavior. diff --git a/agent-launcher/references/session-goal-model.md b/agent-launcher/references/session-goal-model.md new file mode 100644 index 00000000..e7ae0452 --- /dev/null +++ b/agent-launcher/references/session-goal-model.md @@ -0,0 +1,67 @@ +# The session-goal model + +The plugin's organizing idea: **every session starts with a goal**, and the goal +drives which phase runs and which loop/workflow compiles. + +## goal.json (the state file) + +Lives at `./my-agent/goal.json` (the founder's folder). Written and advanced by +`goal_state.py`; surfaced by the opt-in `SessionStart` hook. + +```json +{ + "goal": "Launch an agent that triages my support inbox every morning", + "agent_name": "support-triage", + "phase": "grade-iterate", + "phases_done": ["interview", "stage-launch"], + "loop": {"shape": "grade-iterate", "max_iterations": 5}, + "artifacts": { + "build_sheet": "./my-agent/build-sheet.json", + "payloads": "./my-agent/payloads/", + "launch_script": "./my-agent/launch.sh" + }, + "updated_at": "2026-08-13T09:00:00Z", + "notes": "v0 = triage only; v1 = auto-draft replies via Gmail MCP" +} +``` + +`phase` is one of: `interview`, `stage-launch`, `grade-iterate`, +`run-without-you`, `wrap-up`, or `done`. + +## How the goal fires + +1. **Opt-in SessionStart hook.** With `AGENT_LAUNCHER_SESSION=1`, `session_start.py` + reads `goal.json` and prints an `` block so the agent + resumes exactly where the last session stopped. The hook treats file content as + **data, not instructions**, and exits 0 on any error so it can never break a + session. Disabled by default (no env flag) — zero ambient behavior in unrelated + repos. +2. **`/cs:goal` command.** `set` writes the goal, `status` prints it, `advance` + moves to the next phase. Fully manual; works whether or not the hook is enabled. +3. **Orchestrator routing.** `goal_router.py` reads the goal string + phase and + routes to the right phase skill (exit-code route / ask / refuse), then + `loop_compiler.py` compiles the loop/workflow shape. + +## Why a goal, not a chat prompt + +- **Resumable.** A launch spans multiple sessions (interview today, launch + tomorrow, schedule next week). The goal file is the through-line; checkpoints in + CMA last 30 days, but the *intent* lives in `goal.json`. +- **Deterministic routing.** The router keys off the recorded phase, not a re-read + of chat history, so resuming is unambiguous. +- **One job.** The goal is one sentence for one agent. Multiple agents = multiple + `./my-agent-*/goal.json` folders, never one goal doing two jobs. + +## Relationship to the loop shapes + +The goal's `phase` selects the phase skill; the phase + recurrence answer selects +the loop shape (see `loops-and-workflows.md`). The goal is the *what*; the loop is +the *how it repeats*. + +## Sources + +1. anthropics/launch-your-agent — resumable launch script + `NEXT-DIRECTIONS.md` design. +2. Claude Managed Agents — checkpoints (30-day) & session resume semantics. +3. Claude Code docs — SessionStart hook contract (stdout surfaced as session context). +4. productivity/handoff (this repo) — SessionStart auto-load pattern reused here. +5. engineering/tc-tracker (this repo) — task-context lifecycle & handoff format analogue. diff --git a/agent-launcher/skills/agent-launcher-orchestrator/SKILL.md b/agent-launcher/skills/agent-launcher-orchestrator/SKILL.md new file mode 100644 index 00000000..0c71277b --- /dev/null +++ b/agent-launcher/skills/agent-launcher-orchestrator/SKILL.md @@ -0,0 +1,94 @@ +--- +name: agent-launcher-orchestrator +description: Use when a user wants to build, launch, grade, or schedule a Claude Managed Agent (CMA) in their own Anthropic account — "build me an agent", "launch this as a managed agent", "run this on a schedule", "grade my agent against a rubric", "set up a nightly worker". Reads the per-session goal (./my-agent/goal.json), routes deterministically to one of five phase sub-skills (interview → stage-launch → grade-iterate → run-without-you → wrap-up) via goal_router.py, and compiles the goal+phase into an execution shape (single-pass workflow / bounded grade→iterate loop / recurring cron deployment loop) via loop_compiler.py. Forks context so heavy intake (build sheets, payloads, eval cases) stays out of the parent thread. All launches are emitted as BYOK curl the user runs with their own key; no tool makes API calls. Inspired by anthropics/launch-your-agent (Apache-2.0). Distinct from engineering/agent-harness (generic domain loop) and engineering/write-a-skill (authors Claude Code skills, not CMAs). +context: fork +version: 2.12.0 +author: Alireza Rezvani +license: MIT +tags: [claude-managed-agents, cma, agent, launch, orchestrator, session-goal, loop, workflow, cron, outcome, byok] +compatible_tools: [claude-code, codex-cli, cursor, antigravity, opencode, gemini-cli] +--- + +# agent-launcher — Domain Orchestrator + +Every session starts with a **goal** — one sentence for one CMA. This orchestrator +reads that goal, routes to the right phase, and compiles the goal into a **loop or +a workflow**. Heavy intake stays in the forked context; the parent gets a digest. + +Inspired by Anthropic's `launch-your-agent` reference skill (Apache-2.0). This is +an independent re-implementation; CMA semantics come from +[`references/cma-primitives.md`](../../references/cma-primitives.md). + +## The through-line: the session goal + +State lives at `./my-agent/goal.json` (the user's folder). Manage it with +`goal_state.py` (init / set / status / advance) — it also backs the `/cs:goal` +command and the opt-in `SessionStart` hook. The goal's `phase` selects the lane; +the phase + recurrence selects the loop shape. + +## Routing (deterministic) + +Run the router, then act on its exit code: + +```bash +python3 skills/agent-launcher-orchestrator/scripts/goal_router.py --out-dir ./my-agent +# exit 0 ROUTE -> fork to the named phase sub-skill +# exit 3 ASK -> ask the one printed forcing question, then re-route +# exit 4 REFUSE -> goal too vague; get one sentence, then re-route +``` + +| Lane (phase) | Sub-skill | Loop/workflow | +|---|---|---| +| interview | `interview` | single-pass workflow | +| stage-launch | `stage-launch` | single-pass workflow | +| grade-iterate | `grade-iterate` | **bounded grade→iterate loop** | +| run-without-you | `run-without-you` | **recurring cron deployment loop** | +| wrap-up | `wrap-up` | — | + +## Compile the loop + +```bash +python3 skills/agent-launcher-orchestrator/scripts/loop_compiler.py \ + --out-dir ./my-agent --max-iterations 5 --cron "0 9 * * *" --timezone Europe/Berlin --nest-outcome +``` + +`loop_compiler.py` emits `plan.v1`: `single-pass`, `grade-iterate` (always with a +`max_iterations` cap 1..20), or `cron-loop` (optionally nesting a self-grading +outcome per firing). See [`references/loops-and-workflows.md`](../../references/loops-and-workflows.md). + +## Pre-flight gates (hard refusals) + +1. **No goal set.** If `goal.json` is missing, run + `goal_state.py init --goal "..."` first. The orchestrator does not guess a goal. +2. **Goal too vague.** Router exit 4 — get one sentence naming the one job before + routing. Never route on under-3-word goals. +3. **Never make API calls.** Emit BYOK curl; the user runs it with their own + `$ANTHROPIC_API_KEY`. No script in this plugin touches the network. +4. **Never print the key.** Launch scripts read the key from the environment. + +## Hand-off contract + +After routing, fork to the sub-skill with: the goal string, `agent_name`, +`out_dir` (`./my-agent`), and the compiled `plan.v1`. When the sub-skill returns, +`goal_state.py advance` moves the phase and the parent gets a ≤100-word digest +(phase done, artifact paths, loop shape, one next step). + +## Forcing-question library (walk one at a time; recommend + cite) + +1. **"What one job should this agent do end-to-end?"** — *Recommend:* the single + most repeated task. *Cite:* interview-to-config.md (six intake slots). Refuse to + route a two-job goal; split into two `./my-agent-*/` folders. +2. **"What kicks it off — you ask it, an event, or a schedule?"** — *Recommend:* + on-demand for v0, schedule as the Phase-4 upgrade. *Cite:* loops-and-workflows.md. +3. **"How would you grade a good run?"** — *Recommend:* 3–5 rubric lines grounded + in the output. *Cite:* cma-primitives.md (outcomes; rubric required). +4. **"Is a real integration ready, or do we mock it in v0?"** — *Recommend:* mock + with a schema-true custom tool; wire the MCP server as v1. *Cite:* interview-to-config.md. +5. **"Should run #10 be smarter than run #1?"** — *Recommend:* attach a memory + store only if yes; else skip it. *Cite:* cma-primitives.md (memory limits + injection risk). + +## Tools + +- `scripts/goal_state.py` — own `goal.json` (init/set/status/advance). +- `scripts/goal_router.py` — goal → lane (exit 0 route / 3 ask / 4 refuse). +- `scripts/loop_compiler.py` — goal+phase → `plan.v1` execution shape. diff --git a/agent-launcher/skills/agent-launcher-orchestrator/scripts/goal_router.py b/agent-launcher/skills/agent-launcher-orchestrator/scripts/goal_router.py new file mode 100644 index 00000000..01d12b6d --- /dev/null +++ b/agent-launcher/skills/agent-launcher-orchestrator/scripts/goal_router.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +"""goal_router.py — deterministic router: session goal -> phase lane. + +Reads the goal string (or ./my-agent/goal.json) and decides which phase skill +should run. Uses keyword scoring plus the recorded phase. Exit codes let the +orchestrator act without re-parsing prose: + + 0 ROUTE — a clear lane won; prints the lane and why. + 3 ASK — ambiguous; prints the top candidates + one forcing question. + 4 REFUSE — goal is empty/too vague to route; prints what's missing. + +Lanes: interview, stage-launch, grade-iterate, run-without-you, wrap-up. +Stdlib-only; no network calls. + +Examples: + goal_router.py --goal "grade my agent until the rubric passes" + goal_router.py --out-dir ./my-agent --json + goal_router.py --sample +""" +import argparse +import json +import sys +from pathlib import Path + +LANES = ["interview", "stage-launch", "grade-iterate", "run-without-you", "wrap-up"] + +# keyword -> lane weight +SIGNALS = { + "interview": [ + ("what should", 2), ("scope", 2), ("plan", 2), ("requirements", 2), ("idea", 1), + ("build sheet", 3), ("not sure what", 2), ("design the agent", 2), ("interview", 3), + ], + "stage-launch": [ + ("launch", 3), ("deploy it now", 2), ("stage", 2), ("payload", 3), ("create the agent", 2), + ("first run", 2), ("kick it off", 2), ("environment", 1), ("api call", 2), + ], + "grade-iterate": [ + ("grade", 3), ("rubric", 3), ("iterate", 3), ("improve", 2), ("until it passes", 3), + ("eval", 2), ("make it good", 2), ("success criteria", 2), ("outcome", 2), ("quality", 1), + ], + "run-without-you": [ + ("every morning", 3), ("every day", 3), ("weekly", 3), ("nightly", 3), ("schedule", 3), + ("cron", 3), ("recurring", 3), ("without me", 2), ("run without you", 3), ("automate", 2), + ("on a cadence", 2), ("at 9am", 2), + ], + "wrap-up": [ + ("wrap up", 3), ("close out", 3), ("recap", 2), ("what do i own", 2), ("summary", 1), + ("done", 1), ("overview page", 2), ("hand off", 1), + ], +} + + +def score(goal: str) -> dict: + g = goal.lower() + scores = {lane: 0 for lane in LANES} + hits = {lane: [] for lane in LANES} + for lane, kws in SIGNALS.items(): + for kw, w in kws: + if kw in g: + scores[lane] += w + hits[lane].append(kw) + return {"scores": scores, "hits": hits} + + +def load_goal(out_dir: Path): + p = out_dir / "goal.json" + if not p.exists(): + return None, None + try: + st = json.loads(p.read_text()) + return st.get("goal", ""), st.get("phase") + except (json.JSONDecodeError, OSError): + return None, None + + +def route(goal: str, recorded_phase): + goal = (goal or "").strip() + if len(goal.split()) < 3: + return { + "decision": "REFUSE", + "exit": 4, + "reason": "Goal is empty or under 3 words — too vague to route.", + "need": "One sentence: what one job should the agent do end-to-end?", + } + sc = score(goal) + scores = sc["scores"] + ordered = sorted(scores.items(), key=lambda kv: kv[1], reverse=True) + top_lane, top = ordered[0] + runner_lane, runner = ordered[1] + + # If nothing scored, fall back to the recorded phase, else default to interview. + if top == 0: + lane = recorded_phase if recorded_phase in LANES else "interview" + return { + "decision": "ROUTE", "exit": 0, "lane": lane, + "reason": f"No lane keywords matched; using recorded phase '{recorded_phase}' (default interview).", + "scores": scores, + } + + clear = top >= 3 and (runner == 0 or top >= 2 * runner) + if clear: + return { + "decision": "ROUTE", "exit": 0, "lane": top_lane, + "reason": f"'{top_lane}' won ({top} vs {runner}) on: {', '.join(sc['hits'][top_lane])}.", + "scores": scores, + } + return { + "decision": "ASK", "exit": 3, + "candidates": [top_lane, runner_lane], + "reason": f"Ambiguous: {top_lane} ({top}) vs {runner_lane} ({runner}).", + "question": f"Is this about {top_lane.replace('-', ' ')} or {runner_lane.replace('-', ' ')}?", + "scores": scores, + } + + +def _emit(result: dict, as_json: bool): + if as_json: + print(json.dumps(result, indent=2)) + return + d = result["decision"] + print(f"{d}: {result['reason']}") + if d == "ROUTE": + print(f" -> lane: {result['lane']}") + elif d == "ASK": + print(f" candidates: {', '.join(result['candidates'])}") + print(f" ask: {result['question']}") + elif d == "REFUSE": + print(f" need: {result['need']}") + + +def main() -> int: + ap = argparse.ArgumentParser(description="Route a session goal to a phase lane (exit 0=route, 3=ask, 4=refuse).") + ap.add_argument("--goal", default=None, help="Goal string. If omitted, read from goal.json.") + ap.add_argument("--out-dir", default="./my-agent", help="Folder holding goal.json.") + ap.add_argument("--json", action="store_true") + ap.add_argument("--sample", action="store_true", help="Run a deterministic demo and exit.") + args = ap.parse_args() + + if args.sample: + for g in [ + "grade my agent until the rubric passes", + "run the report every morning at 9am", + "I have an idea but not sure what to build", + "launch it now with the payloads", + "make it better", # ambiguous-ish + "go", # refuse + ]: + r = route(g, None) + print(f"[{r['decision']:6}] {g!r} -> {r.get('lane') or r.get('candidates') or r.get('need')}") + return 0 + + goal = args.goal + recorded_phase = None + if goal is None: + goal, recorded_phase = load_goal(Path(args.out_dir)) + if goal is None: + print("No --goal given and no goal.json found.", file=sys.stderr) + return 4 + result = route(goal, recorded_phase) + _emit(result, args.json) + return result["exit"] + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agent-launcher/skills/agent-launcher-orchestrator/scripts/goal_state.py b/agent-launcher/skills/agent-launcher-orchestrator/scripts/goal_state.py new file mode 100644 index 00000000..b3613ff8 --- /dev/null +++ b/agent-launcher/skills/agent-launcher-orchestrator/scripts/goal_state.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +"""goal_state.py — read/write/advance the per-session goal file (./my-agent/goal.json). + +The goal is the through-line of a CMA launch that spans multiple sessions. This +tool owns goal.json: init a new goal, set fields, print status, or advance to the +next phase. Stdlib-only; no network calls. + +Phases (in order): interview -> stage-launch -> grade-iterate -> run-without-you +-> wrap-up -> done. + +Examples: + goal_state.py init --goal "Triage my inbox every morning" --agent-name inbox-triage + goal_state.py set --phase grade-iterate --note "v0 = triage only" + goal_state.py status --json + goal_state.py advance + goal_state.py --sample +""" +import argparse +import datetime as dt +import json +import sys +from pathlib import Path + +PHASES = ["interview", "stage-launch", "grade-iterate", "run-without-you", "wrap-up", "done"] +DEFAULT_DIR = Path("./my-agent") +FILENAME = "goal.json" + + +def _now() -> str: + return dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat() + + +def _path(out_dir: Path) -> Path: + return out_dir / FILENAME + + +def _load(out_dir: Path) -> dict: + p = _path(out_dir) + if not p.exists(): + return {} + try: + return json.loads(p.read_text()) + except (json.JSONDecodeError, OSError): + return {} + + +def _save(out_dir: Path, state: dict) -> Path: + out_dir.mkdir(parents=True, exist_ok=True) + state["updated_at"] = _now() + p = _path(out_dir) + p.write_text(json.dumps(state, indent=2) + "\n") + return p + + +def _new_state(goal: str, agent_name: str) -> dict: + return { + "goal": goal, + "agent_name": agent_name, + "phase": "interview", + "phases_done": [], + "loop": None, + "artifacts": {}, + "notes": "", + "updated_at": _now(), + } + + +def cmd_init(args) -> int: + out_dir = Path(args.out_dir) + if _path(out_dir).exists() and not args.force: + print(f"goal.json already exists at {_path(out_dir)} (use --force to overwrite)", file=sys.stderr) + return 1 + state = _new_state(args.goal, args.agent_name or _slug(args.goal)) + p = _save(out_dir, state) + _emit(state, args.json, f"Initialized goal at {p}") + return 0 + + +def cmd_set(args) -> int: + out_dir = Path(args.out_dir) + state = _load(out_dir) + if not state: + print("No goal.json found. Run `init` first.", file=sys.stderr) + return 1 + if args.goal is not None: + state["goal"] = args.goal + if args.agent_name is not None: + state["agent_name"] = args.agent_name + if args.phase is not None: + if args.phase not in PHASES: + print(f"Unknown phase '{args.phase}'. One of: {', '.join(PHASES)}", file=sys.stderr) + return 2 + state["phase"] = args.phase + if args.note is not None: + state["notes"] = args.note + if args.artifact: + state.setdefault("artifacts", {}) + for kv in args.artifact: + if "=" not in kv: + print(f"--artifact expects key=value, got '{kv}'", file=sys.stderr) + return 2 + k, v = kv.split("=", 1) + state["artifacts"][k] = v + p = _save(out_dir, state) + _emit(state, args.json, f"Updated {p}") + return 0 + + +def cmd_status(args) -> int: + state = _load(Path(args.out_dir)) + if not state: + print("No goal set. Run `goal_state.py init --goal \"...\"`.", file=sys.stderr) + return 1 + _emit(state, args.json, None) + return 0 + + +def cmd_advance(args) -> int: + out_dir = Path(args.out_dir) + state = _load(out_dir) + if not state: + print("No goal.json found. Run `init` first.", file=sys.stderr) + return 1 + cur = state.get("phase", "interview") + if cur == "done": + _emit(state, args.json, "Already at 'done'.") + return 0 + idx = PHASES.index(cur) if cur in PHASES else 0 + state.setdefault("phases_done", []) + if cur not in state["phases_done"] and cur != "done": + state["phases_done"].append(cur) + state["phase"] = PHASES[min(idx + 1, len(PHASES) - 1)] + p = _save(out_dir, state) + _emit(state, args.json, f"Advanced {cur} -> {state['phase']} ({p})") + return 0 + + +def _slug(text: str) -> str: + s = "".join(c.lower() if c.isalnum() else "-" for c in text).strip("-") + while "--" in s: + s = s.replace("--", "-") + return (s[:40] or "agent").strip("-") + + +def _emit(state: dict, as_json: bool, msg) -> None: + if as_json: + print(json.dumps(state, indent=2)) + return + if msg: + print(msg) + print(f" goal: {state.get('goal','')}") + print(f" agent_name: {state.get('agent_name','')}") + print(f" phase: {state.get('phase','')}") + done = state.get("phases_done") or [] + print(f" phases_done: {', '.join(done) if done else '(none)'}") + loop = state.get("loop") + if loop: + print(f" loop: {loop.get('shape','?')} (max_iterations={loop.get('max_iterations','-')})") + if state.get("notes"): + print(f" notes: {state['notes']}") + + +def _sample() -> int: + import tempfile + d = Path(tempfile.mkdtemp(prefix="al-goal-")) + st = _new_state("Triage my support inbox every morning", "support-triage") + _save(d, st) + print("SAMPLE: initialized goal, then advanced twice") + s = _load(d) + _emit(s, False, None) + # advance twice + for _ in range(2): + cur = s["phase"] + s.setdefault("phases_done", []) + if cur not in s["phases_done"]: + s["phases_done"].append(cur) + s["phase"] = PHASES[min(PHASES.index(cur) + 1, len(PHASES) - 1)] + _save(d, s) + print("\nafter two advances:") + _emit(s, False, None) + return 0 + + +def main() -> int: + ap = argparse.ArgumentParser(description="Read/write/advance the per-session CMA launch goal (./my-agent/goal.json).") + ap.add_argument("--sample", action="store_true", help="Run a deterministic demo and exit.") + sub = ap.add_subparsers(dest="cmd") + + common = argparse.ArgumentParser(add_help=False) + common.add_argument("--out-dir", default=str(DEFAULT_DIR), help="Folder holding goal.json (default ./my-agent).") + common.add_argument("--json", action="store_true", help="Emit the full state as JSON.") + + p_init = sub.add_parser("init", parents=[common], help="Create a new goal.json.") + p_init.add_argument("--goal", required=True, help="One-sentence job the agent does.") + p_init.add_argument("--agent-name", default=None, help="Short name (default: slug of the goal).") + p_init.add_argument("--force", action="store_true", help="Overwrite an existing goal.json.") + p_init.set_defaults(func=cmd_init) + + p_set = sub.add_parser("set", parents=[common], help="Update goal fields.") + p_set.add_argument("--goal", default=None) + p_set.add_argument("--agent-name", default=None) + p_set.add_argument("--phase", default=None, help=f"One of: {', '.join(PHASES)}") + p_set.add_argument("--note", default=None) + p_set.add_argument("--artifact", action="append", help="key=value artifact path (repeatable).") + p_set.set_defaults(func=cmd_set) + + p_status = sub.add_parser("status", parents=[common], help="Print the current goal + phase.") + p_status.set_defaults(func=cmd_status) + + p_adv = sub.add_parser("advance", parents=[common], help="Move to the next phase.") + p_adv.set_defaults(func=cmd_advance) + + args = ap.parse_args() + if args.sample: + return _sample() + if not getattr(args, "cmd", None): + ap.print_help() + return 0 + return args.func(args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agent-launcher/skills/agent-launcher-orchestrator/scripts/loop_compiler.py b/agent-launcher/skills/agent-launcher-orchestrator/scripts/loop_compiler.py new file mode 100644 index 00000000..483ad454 --- /dev/null +++ b/agent-launcher/skills/agent-launcher-orchestrator/scripts/loop_compiler.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +"""loop_compiler.py — compile a goal + phase into an execution shape (plan.v1). + +Turns "what are we doing and where are we" into exactly one of three shapes: + + single-pass — interview -> plan -> stage -> launch (Phases 1-2) + grade-iterate — CMA user.define_outcome self-grading, BOUNDED by max_iterations + cron-loop — recurring scheduled deployment ("run without you") + +A grade-iterate loop is NEVER emitted without a max_iterations cap (1..20). A +cron-loop may nest an outcome so each firing self-grades. Stdlib-only. + +Examples: + loop_compiler.py --phase grade-iterate --max-iterations 5 + loop_compiler.py --phase run-without-you --cron "0 9 * * *" --timezone Europe/Berlin --nest-outcome + loop_compiler.py --out-dir ./my-agent # read phase from goal.json + loop_compiler.py --sample +""" +import argparse +import json +import sys +from pathlib import Path + +MAX_ITER_CEILING = 20 +MAX_ITER_DEFAULT = 3 + +PHASE_TO_SHAPE = { + "interview": "single-pass", + "stage-launch": "single-pass", + "grade-iterate": "grade-iterate", + "run-without-you": "cron-loop", + "wrap-up": "single-pass", + "done": "single-pass", +} + +SINGLE_PASS_STEPS = ["interview", "plan", "validate", "stage", "launch"] + + +def clamp_iters(n) -> int: + try: + n = int(n) + except (TypeError, ValueError): + n = MAX_ITER_DEFAULT + return max(1, min(MAX_ITER_CEILING, n)) + + +def compile_plan(phase, max_iterations, cron, timezone, event, nest_outcome): + shape = PHASE_TO_SHAPE.get(phase, "single-pass") + + # run-without-you can be recurring (cron), event-driven, or on-demand. + if phase == "run-without-you": + if cron: + shape = "cron-loop" + elif event: + shape = "event-driven" + else: + shape = "on-demand" + + plan = {"plan_version": "plan.v1", "phase": phase, "shape": shape} + + if shape == "single-pass": + plan["terminal_state"] = "a live session produced its first output" + plan["steps"] = SINGLE_PASS_STEPS + plan["repeats"] = False + elif shape == "grade-iterate": + cap = clamp_iters(max_iterations) + plan["repeats"] = True + plan["bounded"] = True + plan["max_iterations"] = cap + plan["mechanism"] = "user.define_outcome (isolated grader) -> verdict -> next attempt" + plan["terminal_states"] = ["satisfied", "max_iterations_reached", "failed", "interrupted"] + plan["loop_invariant"] = "each iteration moves >=1 rubric line fail->pass or the run halts" + plan["next_moves"] = ["sharpen prompt/tools", "re-run as-is", "promote to schedule"] + elif shape == "cron-loop": + plan["repeats"] = True + plan["bounded"] = False + plan["mechanism"] = "POST /v1/deployments schedule.cron -> a session per firing (drun_)" + plan["schedule"] = {"expression": cron, "timezone": timezone or "UTC"} + plan["safety"] = ["always_ask MCP", "limited networking", "read_only memory where possible", + "max_iterations per firing", "workspace spend limit", "test with manual run first"] + plan["terminal_state"] = "none by design; runs until paused/archived" + if nest_outcome: + plan["nested_outcome"] = {"bounded": True, "max_iterations": clamp_iters(max_iterations), + "note": "each firing self-grades before finishing"} + elif shape == "event-driven": + plan["repeats"] = True + plan["bounded"] = False + plan["mechanism"] = "documented curl to send user.message on your event (not scheduled)" + plan["event"] = event + elif shape == "on-demand": + plan["repeats"] = False + plan["mechanism"] = "no deployment; send user.message when you ask" + + # warnings + warnings = [] + if shape == "cron-loop" and not cron: + warnings.append("cron-loop selected but no --cron expression given; schedule is a placeholder") + if shape == "grade-iterate" and max_iterations and int_or_none(max_iterations) and int(max_iterations) > MAX_ITER_CEILING: + warnings.append(f"max_iterations clamped to {MAX_ITER_CEILING} (CMA ceiling)") + plan["warnings"] = warnings + return plan + + +def int_or_none(v): + try: + int(v) + return True + except (TypeError, ValueError): + return False + + +def load_phase(out_dir: Path): + p = out_dir / "goal.json" + if not p.exists(): + return None + try: + return json.loads(p.read_text()).get("phase") + except (json.JSONDecodeError, OSError): + return None + + +def _emit(plan: dict, as_json: bool): + if as_json: + print(json.dumps(plan, indent=2)) + return + print(f"shape: {plan['shape']} (phase={plan['phase']})") + if plan.get("bounded") is True: + print(f" bounded loop, max_iterations={plan.get('max_iterations')}") + if plan.get("bounded") is False: + print(" unbounded (safety rails required)") + if "schedule" in plan: + print(f" schedule: {plan['schedule']['expression']} [{plan['schedule']['timezone']}]") + if "nested_outcome" in plan: + print(f" nested outcome: each firing self-grades (max_iterations={plan['nested_outcome']['max_iterations']})") + if plan.get("steps"): + print(f" steps: {' -> '.join(plan['steps'])}") + print(f" mechanism: {plan.get('mechanism','-')}") + for w in plan.get("warnings", []): + print(f" WARN: {w}") + + +def main() -> int: + ap = argparse.ArgumentParser(description="Compile a goal+phase into an execution shape (single-pass / grade-iterate / cron-loop).") + ap.add_argument("--phase", default=None, help="One of interview, stage-launch, grade-iterate, run-without-you, wrap-up.") + ap.add_argument("--out-dir", default="./my-agent", help="Read phase from goal.json when --phase omitted.") + ap.add_argument("--max-iterations", default=MAX_ITER_DEFAULT, help="Grade-loop cap (clamped 1..20).") + ap.add_argument("--cron", default=None, help="5-field POSIX cron expression (run-without-you).") + ap.add_argument("--timezone", default=None, help="IANA timezone for the schedule.") + ap.add_argument("--event", default=None, help="Event description for an event-driven trigger.") + ap.add_argument("--nest-outcome", action="store_true", help="Nest a self-grading outcome inside each cron firing.") + ap.add_argument("--json", action="store_true") + ap.add_argument("--sample", action="store_true", help="Run a deterministic demo and exit.") + args = ap.parse_args() + + if args.sample: + for kw in [ + dict(phase="interview"), + dict(phase="grade-iterate", max_iterations=99), + dict(phase="run-without-you", cron="0 9 * * *", timezone="Europe/Berlin", nest_outcome=True), + dict(phase="run-without-you"), + ]: + plan = compile_plan(kw.get("phase"), kw.get("max_iterations", 3), kw.get("cron"), + kw.get("timezone"), kw.get("event"), kw.get("nest_outcome", False)) + print(f"--- {kw} ---") + _emit(plan, False) + return 0 + + phase = args.phase or load_phase(Path(args.out_dir)) + if not phase: + print("No --phase given and no goal.json phase found.", file=sys.stderr) + return 2 + plan = compile_plan(phase, args.max_iterations, args.cron, args.timezone, args.event, args.nest_outcome) + _emit(plan, args.json) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agent-launcher/skills/grade-iterate/SKILL.md b/agent-launcher/skills/grade-iterate/SKILL.md new file mode 100644 index 00000000..a8c79f64 --- /dev/null +++ b/agent-launcher/skills/grade-iterate/SKILL.md @@ -0,0 +1,73 @@ +--- +name: grade-iterate +description: Phase 3 of building a Claude Managed Agent — the bounded grade→iterate loop. Define a CMA outcome (a required markdown rubric graded by an isolated grader), read each verdict, decide the next move (sharpen / re-run / promote to schedule), and once a version passes, run held-back eval cases in parallel. Use when the user says "grade my agent", "make it pass the rubric", "iterate until it's good", "is it good enough", or when the orchestrator routes phase=grade-iterate. outcome_builder.py builds the user.define_outcome payload (rubric required, max_iterations clamped 1..20 — never unbounded); verdict_reader.py reads the grader result and recommends the next move; eval_scaffold.py generates held-back cases + a parallel run plan (capped at the 25-thread CMA ceiling). Distinct from stage-launch (first launch) and run-without-you (scheduling). +version: 2.12.0 +author: Alireza Rezvani +license: MIT +tags: [cma, outcome, rubric, grader, grade-iterate, loop, max-iterations, eval, held-back] +compatible_tools: [claude-code, codex-cli, cursor, antigravity, opencode, gemini-cli] +--- + +# Phase 3 — Grade → Iterate (the bounded loop) + +This is the plugin's **loop**: CMA's `outcome` primitive self-grades the agent's +work in an isolated context and feeds failing verdicts back for the next attempt. +It is **always bounded** by `max_iterations` (1..20) — never "improve forever". + +See [`references/loops-and-workflows.md`](../../references/loops-and-workflows.md) +and the outcome section of +[`references/cma-primitives.md`](../../references/cma-primitives.md). + +## Workflow + +1. **Define the outcome.** + ```bash + python3 skills/grade-iterate/scripts/outcome_builder.py \ + --sheet ./my-agent/build-sheet.json --max-iterations 5 \ + --out ./my-agent/payloads/outcome.json + ``` + The **rubric is required**; `max_iterations` is clamped to 1..20. Send the + payload as a `user.define_outcome` event (append to the running session). +2. **Read every verdict first.** + ```bash + python3 skills/grade-iterate/scripts/verdict_reader.py --result ./my-agent/last-verdict.json + ``` + Tables the rubric outcome and recommends: **SHIP** (`satisfied`), **SHARPEN** + then re-run (`needs_revision`), **ESCALATE** (`max_iterations_reached` / + `failed`), **RESUME** (`interrupted`). With ≤1 iteration left it flips to + "make the single highest-value fix or escalate now". +3. **Loop invariant.** Each iteration must move ≥1 rubric line fail→pass, or the + run halts at the cap and escalates. Don't burn the budget on cosmetic edits. +4. **Once a version passes, run held-back eval.** + ```bash + python3 skills/grade-iterate/scripts/eval_scaffold.py \ + --sheet ./my-agent/build-sheet.json --out ./my-agent/eval.json --concurrency 5 + ``` + Held-back cases (never seen during iteration) run in parallel, capped at the + 25-thread CMA ceiling, each graded against the same rubric. +5. **Decide.** SHIP as v0, or promote to a scheduled deployment (Phase 4). Record + the verdict on the goal: `goal_state.py set --phase run-without-you`. + +## Hard rules + +- **Bounded, always.** No outcome without a `max_iterations` cap. +- **Read the verdict before acting.** The grader's explanation drives the next move. +- **Held-back cases are held back.** Never grade generalization on cases the agent + already iterated against. + +## Forcing-question library (recommend + cite) + +1. "What are the 3–5 rubric lines?" *Recommend:* grounded, checkable criteria. + *Cite:* cma-primitives.md (rubric required). +2. "How many iterations before you'd rather look yourself?" *Recommend:* 3–5. + *Cite:* loops-and-workflows.md (bounded loop). +3. "On a fail, sharpen the prompt or the tools?" *Recommend:* whichever rubric line + failed points to. *Cite:* verdict_reader next-move table. +4. "Which cases did the agent NOT see?" *Recommend:* hold back ≥3 for generalization. + *Cite:* this SKILL (held-back eval). + +## Tools + +- `scripts/outcome_builder.py` — user.define_outcome payload (rubric required, cap 1..20). +- `scripts/verdict_reader.py` — grader result → next move. +- `scripts/eval_scaffold.py` — held-back cases + parallel run plan (≤25 threads). diff --git a/agent-launcher/skills/grade-iterate/scripts/eval_scaffold.py b/agent-launcher/skills/grade-iterate/scripts/eval_scaffold.py new file mode 100644 index 00000000..468d0b41 --- /dev/null +++ b/agent-launcher/skills/grade-iterate/scripts/eval_scaffold.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""eval_scaffold.py — generate a held-back eval scaffold + a parallel-run plan. + +Once a version passes the in-loop rubric, run held-back cases (that the agent +never saw during iteration) to check it generalizes. This tool emits an eval +scaffold JSON (cases + expected + a per-case kickoff message) and a parallel-run +plan describing how to fan the cases across sessions. Stdlib-only; no network. + +Examples: + eval_scaffold.py --sheet ./my-agent/build-sheet.json --out ./my-agent/eval.json + eval_scaffold.py --cases cases.json --concurrency 5 + eval_scaffold.py --sample +""" +import argparse +import json +import sys +from pathlib import Path + + +def scaffold(agent_name, goal, rubric, cases, concurrency): + eval_cases = [] + for i, c in enumerate(cases): + cid = c.get("id") or f"case-{i+1}" + eval_cases.append({ + "id": cid, + "input": c.get("input", ""), + "expected": {k: v for k, v in c.items() if k not in ("id", "input")}, + "kickoff": {"events": [{"type": "user.message", "content": c.get("input", "")}]}, + "grade_against_rubric": True, + }) + plan = { + "eval_version": "eval.v1", + "agent_name": agent_name, + "goal": goal, + "rubric": rubric, + "cases": eval_cases, + "run_plan": { + "mode": "parallel", + "concurrency": max(1, min(int(concurrency), 25)), # CMA <=25 concurrent threads + "note": "Each case is an independent session; grade each against the same rubric. " + "Cap concurrency at 25 (CMA thread ceiling). Run only AFTER a version passes the in-loop rubric.", + "pass_condition": "All held-back cases satisfied (or an explicit, recorded exception).", + }, + } + return plan + + +def main() -> int: + ap = argparse.ArgumentParser(description="Generate a held-back eval scaffold + parallel-run plan.") + ap.add_argument("--sheet", help="build-sheet.json (reads eval_plan.held_back_cases + rubric).") + ap.add_argument("--cases", help="JSON file: a list of {id,input,...expected} objects.") + ap.add_argument("--concurrency", default=5) + ap.add_argument("--out", help="Write the eval scaffold here.") + ap.add_argument("--json", action="store_true") + ap.add_argument("--sample", action="store_true") + args = ap.parse_args() + + if args.sample: + sheet = json.loads((Path(__file__).resolve().parents[3] / "assets" / "example-build-sheet.json").read_text()) + cases = sheet.get("eval_plan", {}).get("held_back_cases", []) + rubric = sheet.get("primitives", {}).get("outcome", {}).get("rubric", "") + plan = scaffold(sheet["agent_name"], sheet["goal"], rubric, cases, 5) + print(json.dumps(plan, indent=2)) + return 0 + + agent_name, goal, rubric = "agent", "", "" + cases = [] + if args.sheet: + sheet = json.loads(Path(args.sheet).read_text()) + agent_name = sheet.get("agent_name", "agent") + goal = sheet.get("goal", "") + rubric = sheet.get("primitives", {}).get("outcome", {}).get("rubric", "") + cases = sheet.get("eval_plan", {}).get("held_back_cases", []) + if args.cases: + cases = json.loads(Path(args.cases).read_text()) + if not cases: + print("No held-back cases found (provide --cases or a sheet with eval_plan.held_back_cases).", file=sys.stderr) + return 1 + + plan = scaffold(agent_name, goal, rubric, cases, args.concurrency) + text = json.dumps(plan, indent=2) + if args.out: + Path(args.out).parent.mkdir(parents=True, exist_ok=True) + Path(args.out).write_text(text + "\n") + print(f"Wrote {args.out} ({len(cases)} cases, concurrency {plan['run_plan']['concurrency']})") + if args.json or not args.out: + print(text) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agent-launcher/skills/grade-iterate/scripts/outcome_builder.py b/agent-launcher/skills/grade-iterate/scripts/outcome_builder.py new file mode 100644 index 00000000..f1a20c42 --- /dev/null +++ b/agent-launcher/skills/grade-iterate/scripts/outcome_builder.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""outcome_builder.py — build a CMA user.define_outcome payload (the grade->iterate loop). + +The rubric is REQUIRED; max_iterations is clamped to 1..20 (CMA ceiling, default +3). Can pull description + rubric from a build sheet's primitives.outcome, or take +them on the command line. Stdlib-only; no network calls. + +Examples: + outcome_builder.py --sheet ./my-agent/build-sheet.json --max-iterations 5 --out ./my-agent/payloads/outcome.json + outcome_builder.py --description "Label every email" --rubric-file rubric.md + outcome_builder.py --sample +""" +import argparse +import json +import sys +from pathlib import Path + +MAX_ITER = 20 +DEFAULT_ITER = 3 + + +def clamp(n): + try: + n = int(n) + except (TypeError, ValueError): + return DEFAULT_ITER, "non-integer -> default 3" + if n < 1: + return 1, f"{n} < 1 -> 1" + if n > MAX_ITER: + return MAX_ITER, f"{n} > {MAX_ITER} -> clamped {MAX_ITER}" + return n, None + + +def build(description, rubric, max_iterations): + cap, note = clamp(max_iterations) + if not (rubric or "").strip(): + return None, "rubric is required and cannot be empty", cap, note + payload = { + "type": "user.define_outcome", + "description": description.strip(), + "rubric": rubric.strip(), + "max_iterations": cap, + } + return payload, None, cap, note + + +def main() -> int: + ap = argparse.ArgumentParser(description="Build a user.define_outcome payload (rubric required, max_iterations clamped 1..20).") + ap.add_argument("--sheet", help="build-sheet.json to read primitives.outcome from.") + ap.add_argument("--description", help="Outcome/task description.") + ap.add_argument("--rubric", help="Inline markdown rubric.") + ap.add_argument("--rubric-file", help="Path to a markdown rubric file.") + ap.add_argument("--max-iterations", default=DEFAULT_ITER) + ap.add_argument("--out", help="Write payload JSON here.") + ap.add_argument("--json", action="store_true") + ap.add_argument("--sample", action="store_true") + args = ap.parse_args() + + if args.sample: + payload, err, cap, note = build( + "Label every overnight support email with one grounded category.", + "- Every email has exactly one label\n- Reason quotes the email (no invented facts)\n- Urgent = outage / paying-customer blocker", + 99, + ) + print(json.dumps(payload, indent=2)) + print(f"\n(max_iterations note: {note})", file=sys.stderr) + return 0 + + description = args.description + rubric = args.rubric + if args.rubric_file: + rubric = Path(args.rubric_file).read_text() + if args.sheet: + sheet = json.loads(Path(args.sheet).read_text()) + oc = sheet.get("primitives", {}).get("outcome", {}) + description = description or oc.get("description") or sheet.get("goal", "") + rubric = rubric or oc.get("rubric", "") + if args.max_iterations == DEFAULT_ITER and oc.get("max_iterations"): + args.max_iterations = oc["max_iterations"] + + if not description: + print("Provide --description or --sheet.", file=sys.stderr) + return 2 + + payload, err, cap, note = build(description, rubric or "", args.max_iterations) + if err: + print(f"ERROR: {err}", file=sys.stderr) + return 1 + text = json.dumps(payload, indent=2) + if args.out: + Path(args.out).parent.mkdir(parents=True, exist_ok=True) + Path(args.out).write_text(text + "\n") + print(f"Wrote {args.out} (max_iterations={cap})") + if args.json or not args.out: + print(text) + if note: + print(f"# {note}", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agent-launcher/skills/grade-iterate/scripts/verdict_reader.py b/agent-launcher/skills/grade-iterate/scripts/verdict_reader.py new file mode 100644 index 00000000..fb4ed4b6 --- /dev/null +++ b/agent-launcher/skills/grade-iterate/scripts/verdict_reader.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""verdict_reader.py — read a grader result and decide the next move. + +Reads a CMA outcome result (from a saved SSE event or a JSON file you paste) and +tables the rubric outcome, then recommends the next move: + satisfied -> SHIP (or promote to schedule) + needs_revision -> SHARPEN (prompt/tools) then RE-RUN + max_iterations_reached / failed -> ESCALATE to the founder + interrupted -> RESUME +Stdlib-only; no network calls. + +Result JSON shape (minimal): + {"status": "needs_revision", "iteration": 2, "max_iterations": 5, + "rubric_results": [{"criterion": "...", "pass": true, "note": "..."}], + "explanation": "..."} + +Examples: + verdict_reader.py --result ./my-agent/last-verdict.json + verdict_reader.py --sample +""" +import argparse +import json +import sys +from pathlib import Path + +NEXT_MOVE = { + "satisfied": ("SHIP", "Rubric satisfied. Ship v0, or promote to a scheduled deployment (Phase 4)."), + "needs_revision": ("SHARPEN", "Fix the failing rubric lines (prompt/tools/inputs), then re-run."), + "max_iterations_reached": ("ESCALATE", "Cap hit without convergence. Escalate to the founder; re-scope the rubric or the tools."), + "failed": ("ESCALATE", "Run failed. Read the explanation; likely a tool/permission/setup issue to fix before re-run."), + "interrupted": ("RESUME", "Send a user.message to resume; checkpoints last 30 days."), +} + + +def read(result: dict): + status = result.get("status", "unknown") + move, why = NEXT_MOVE.get(status, ("INVESTIGATE", f"Unknown status '{status}'. Inspect the raw event.")) + rr = result.get("rubric_results", []) or [] + passed = [r for r in rr if r.get("pass")] + failed = [r for r in rr if not r.get("pass")] + it = result.get("iteration") + mx = result.get("max_iterations") + budget_note = None + if it is not None and mx: + remaining = mx - it + budget_note = f"{it}/{mx} iterations used, {remaining} left" + if status == "needs_revision" and remaining <= 1: + move = "SHARPEN-OR-ESCALATE" + why = "One iteration left — make the single highest-value fix, or escalate now rather than waste the cap." + return { + "status": status, + "next_move": move, + "why": why, + "passed": [r.get("criterion", "?") for r in passed], + "failed": [{"criterion": r.get("criterion", "?"), "note": r.get("note", "")} for r in failed], + "budget": budget_note, + "explanation": result.get("explanation", ""), + } + + +def _emit(v, as_json): + if as_json: + print(json.dumps(v, indent=2)) + return + print(f"STATUS: {v['status']} NEXT MOVE: {v['next_move']}") + print(f" {v['why']}") + if v.get("budget"): + print(f" budget: {v['budget']}") + if v["passed"]: + print(f" passed ({len(v['passed'])}): " + "; ".join(v["passed"])) + if v["failed"]: + print(f" FAILED ({len(v['failed'])}):") + for f in v["failed"]: + note = f" — {f['note']}" if f["note"] else "" + print(f" - {f['criterion']}{note}") + if v["explanation"]: + print(f" grader: {v['explanation']}") + + +def main() -> int: + ap = argparse.ArgumentParser(description="Read a grader verdict and recommend the next move.") + ap.add_argument("--result", help="Path to a grader result JSON.") + ap.add_argument("--json", action="store_true") + ap.add_argument("--sample", action="store_true") + args = ap.parse_args() + + if args.sample: + sample = { + "status": "needs_revision", "iteration": 4, "max_iterations": 5, + "rubric_results": [ + {"criterion": "Every email labeled", "pass": True}, + {"criterion": "No invented facts", "pass": False, "note": "row 7 invented an SLA"}, + {"criterion": "Urgent precision", "pass": True}, + ], + "explanation": "Close, but one reason wasn't grounded in the email text.", + } + _emit(read(sample), False) + return 0 + + if not args.result: + print("Provide --result path.", file=sys.stderr) + return 2 + result = json.loads(Path(args.result).read_text()) + _emit(read(result), args.json) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agent-launcher/skills/interview/SKILL.md b/agent-launcher/skills/interview/SKILL.md new file mode 100644 index 00000000..a0742861 --- /dev/null +++ b/agent-launcher/skills/interview/SKILL.md @@ -0,0 +1,83 @@ +--- +name: interview +description: Phase 1 of building a Claude Managed Agent — interview the founder about the one job the agent should do, then produce a build sheet (CMA primitives table + v1/v2 deferrals + eval plan) WITHOUT needing their API key yet. Use when the user says "help me scope an agent", "I have an idea for an agent", "what should this agent be", or when the orchestrator routes phase=interview. Drives the six intake slots (job, trigger, inputs, actions, definition-of-done, recurrence) via AskUserQuestion, maps them to primitives with interview_planner.py, assembles build-sheet.json with build_sheet_builder.py, and validates limits with primitives_validator.py. Connectors are mockable in v0 (schema-true custom tools); real MCP servers become v1 deferrals. Distinct from stage-launch (which turns the sheet into payloads). +version: 2.12.0 +author: Alireza Rezvani +license: MIT +tags: [cma, interview, scoping, build-sheet, primitives, deferrals, eval-plan] +compatible_tools: [claude-code, codex-cli, cursor, antigravity, opencode, gemini-cli] +--- + +# Phase 1 — Interview → Plan + +Open warmly with one or two examples from +[`references/examples-bank.md`](../../references/examples-bank.md), then interview +the founder into a **build sheet**. No API key needed in this phase — the output +is a plan. + +## The six intake slots (ask one at a time; use AskUserQuestion for choices) + +| Slot | Question | Maps to | +|---|---|---| +| **Job** | "What one job should this agent do end-to-end?" | `agent.system` + outcome `description` | +| **Trigger** | "What kicks it off — you ask it, an event, or a schedule?" | on-demand / event / cron | +| **Inputs** | "What does it read?" (files, repo, memory, gmail/slack/github, web) | resources / MCP servers / memory | +| **Actions** | "What does it do?" (draft, write, call APIs, run code) | agent toolset / custom tools / MCP | +| **Done** | "How would you grade a good run?" | outcome `rubric` (required) | +| **Recurrence** | "Once, on request, or on a cadence?" | single-pass / grade-loop / cron-loop | + +See [`references/interview-to-config.md`](../../references/interview-to-config.md) +for the full mapping. + +## Workflow + +1. **Interview.** Walk the six slots. Capture the founder's own words — never + invent specifics they didn't claim. +2. **Map to primitives.** + ```bash + python3 skills/interview/scripts/interview_planner.py \ + --job "Triage overnight support email" --trigger schedule \ + --inputs "gmail,memory" --actions "label,reply" \ + --dod "one label per email, grounded reason, no invented facts" \ + --recurrence daily --out ./my-agent/plan.json + ``` + MCP inputs become **schema-true mock custom tools** in v0 and a **v1 deferral** + to wire the real server. Irreversible actions (send/publish) become **v2 + deferrals** behind `always_ask`. +3. **Assemble the sheet.** + ```bash + python3 skills/interview/scripts/build_sheet_builder.py --plan ./my-agent/plan.json --out-dir ./my-agent + ``` +4. **Validate limits.** + ```bash + python3 skills/interview/scripts/primitives_validator.py --sheet ./my-agent/build-sheet.json + ``` + FAIL blocks progress; fix and re-run. WARN is advisory (surface it). +5. **Record the plan in the goal.** `goal_state.py set --phase stage-launch + --artifact build_sheet=./my-agent/build-sheet.json`, then advance. + +## Hard rules + +- **v0 is the core job only.** Everything else is a versioned deferral with a + reason and an exact mechanism. +- **Their problem, their words.** +- **No key yet.** The interview produces a plan; the key is a Phase-2 concern. + +## Forcing-question library (recommend + cite) + +1. "What one job — singular?" *Recommend:* the most-repeated task. *Cite:* + interview-to-config.md. Two jobs → two agents. +2. "Real integration or v0 mock?" *Recommend:* mock; wire MCP as v1. *Cite:* + interview-to-config.md rule 1. +3. "How do you grade it?" *Recommend:* 3–5 grounded rubric lines. *Cite:* + cma-primitives.md (rubric required). +4. "Smarter over time?" *Recommend:* attach memory only if yes. *Cite:* + cma-primitives.md (memory limits + injection). +5. "Once, or on a cadence?" *Recommend:* on-demand v0, schedule as Phase-4. + *Cite:* loops-and-workflows.md. + +## Tools + +- `scripts/interview_planner.py` — answers → primitives skeleton + deferrals. +- `scripts/build_sheet_builder.py` — assemble/normalize build-sheet.json. +- `scripts/primitives_validator.py` — validate vs CMA limits (PASS/WARN/FAIL). diff --git a/agent-launcher/skills/interview/scripts/build_sheet_builder.py b/agent-launcher/skills/interview/scripts/build_sheet_builder.py new file mode 100644 index 00000000..3381d80b --- /dev/null +++ b/agent-launcher/skills/interview/scripts/build_sheet_builder.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""build_sheet_builder.py — assemble/normalize a build-sheet.json for a CMA. + +Takes an interview_planner.py plan (or a partial sheet) and produces a normalized +build-sheet.json under ./my-agent/. Fills defaults, orders keys, and strips the +planner's private _notes into a top-level "loop_hint". Stdlib-only. + +Examples: + interview_planner.py --sample > /tmp/plan.json + build_sheet_builder.py --plan /tmp/plan.json --out-dir ./my-agent + build_sheet_builder.py --sample +""" +import argparse +import json +import sys +from pathlib import Path + +REQUIRED = ("agent_name", "goal", "primitives") + + +def build(plan: dict) -> dict: + notes = plan.pop("_notes", {}) if isinstance(plan, dict) else {} + sheet = { + "agent_name": plan.get("agent_name", "agent"), + "goal": plan.get("goal", ""), + "primitives": plan.get("primitives", {}), + "deferrals": plan.get("deferrals", []), + "eval_plan": plan.get("eval_plan", {"success_criteria": [], "held_back_cases": []}), + } + # ensure agent + environment exist + prim = sheet["primitives"] + prim.setdefault("agent", {"model": "claude-opus-4-8", "tools": [{"type": "agent_toolset_20260401"}]}) + prim["agent"].setdefault("model", "claude-opus-4-8") + prim.setdefault("environment", {"type": "cloud", "networking": "unrestricted", "packages": {}}) + if notes.get("loop_hint"): + sheet["loop_hint"] = notes["loop_hint"] + return sheet + + +def missing(sheet: dict): + miss = [k for k in REQUIRED if not sheet.get(k)] + if not sheet.get("goal"): + miss.append("goal(empty)") + if "agent" not in sheet.get("primitives", {}): + miss.append("primitives.agent") + return sorted(set(miss)) + + +def main() -> int: + ap = argparse.ArgumentParser(description="Assemble a normalized build-sheet.json from a planner plan.") + ap.add_argument("--plan", help="Path to an interview_planner.py plan JSON.") + ap.add_argument("--out-dir", default="./my-agent") + ap.add_argument("--stdout", action="store_true", help="Print instead of writing to disk.") + ap.add_argument("--json", action="store_true") + ap.add_argument("--sample", action="store_true") + args = ap.parse_args() + + if args.sample: + sample_plan = { + "agent_name": "support-triage", + "goal": "Triage overnight support email.", + "primitives": {"agent": {"model": "claude-opus-4-8", "tools": [{"type": "agent_toolset_20260401"}]}, + "environment": {"type": "cloud", "networking": "unrestricted", "packages": {}}}, + "deferrals": [{"version": "v1", "item": "Real Gmail MCP", "reason": "no cred", "mechanism": "register vault"}], + "eval_plan": {"success_criteria": ["all labeled"], "held_back_cases": []}, + "_notes": {"loop_hint": "cron-loop"}, + } + sheet = build(sample_plan) + print(json.dumps(sheet, indent=2)) + print(f"\nmissing: {missing(sheet) or 'none'}", file=sys.stderr) + return 0 + + if not args.plan: + print("Provide --plan (an interview_planner.py plan).", file=sys.stderr) + return 2 + plan = json.loads(Path(args.plan).read_text()) + sheet = build(plan) + miss = missing(sheet) + text = json.dumps(sheet, indent=2) + if args.stdout or args.json: + print(text) + else: + out_dir = Path(args.out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + dest = out_dir / "build-sheet.json" + dest.write_text(text + "\n") + print(f"Wrote {dest}") + if miss: + print(f"WARN missing/empty: {', '.join(miss)}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agent-launcher/skills/interview/scripts/interview_planner.py b/agent-launcher/skills/interview/scripts/interview_planner.py new file mode 100644 index 00000000..8af81847 --- /dev/null +++ b/agent-launcher/skills/interview/scripts/interview_planner.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +"""interview_planner.py — map Phase-1 interview answers to a CMA primitives skeleton. + +Deterministic mapping of the six intake slots (job, trigger, inputs, actions, +definition-of-done, recurrence) to an agent/environment/session/outcome/deployment +skeleton plus suggested v1/v2 deferrals. Implements references/interview-to-config.md. +Stdlib-only; no network calls. + +Examples: + interview_planner.py --job "Triage support email" --trigger schedule \ + --inputs "gmail,memory" --actions "label,draft" --dod "one label + grounded reason" \ + --recurrence daily --model claude-opus-4-8 + interview_planner.py --answers answers.json --json + interview_planner.py --sample +""" +import argparse +import json +import sys +from pathlib import Path + +TRIGGERS = {"ask", "on-demand", "event", "schedule"} +RECURRENCE = {"once", "on-request", "daily", "weekly", "hourly", "custom"} + +# input token -> resource / server suggestion +INPUT_MAP = { + "files": ("resource", {"type": "file"}), + "file": ("resource", {"type": "file"}), + "repo": ("resource", {"type": "repository"}), + "repository": ("resource", {"type": "repository"}), + "memory": ("memory", {"access": "read_write"}), + "gmail": ("mcp", {"server": "gmail", "permission": "always_ask"}), + "slack": ("mcp", {"server": "slack", "permission": "always_ask"}), + "github": ("mcp", {"server": "github", "permission": "always_ask"}), + "web": ("tool", {"type": "web_fetch"}), +} + + +def plan(job, trigger, inputs, actions, dod, recurrence, model, name): + inputs = [i.strip().lower() for i in inputs if i.strip()] + actions = [a.strip() for a in actions if a.strip()] + + agent = { + "model": model, + "system": job.strip(), + "tools": [{"type": "agent_toolset_20260401"}], + "mcp_servers": [], + "skills": [], + } + session = {"resources": [], "vault_ids": [], "memory_stores": []} + deferrals = [] + networking = "unrestricted" + allowed_hosts = [] + + for tok in inputs: + kind, payload = INPUT_MAP.get(tok, (None, None)) + if kind == "resource": + session["resources"].append(payload) + elif kind == "memory": + session["memory_stores"].append({**payload, "instructions": "Cross-session context."}) + elif kind == "mcp": + # v0: mock as custom tool; v1: wire real MCP. + agent["tools"].append({ + "type": "custom", + "name": f"{payload['server']}_action", + "description": f"MOCK {payload['server']} action for v0 (schema-true).", + "input_schema": {"type": "object", "properties": {"query": {"type": "string"}}}, + }) + deferrals.append({ + "version": "v1", + "item": f"Real {payload['server']} via MCP", + "reason": "OAuth/credential not registered yet", + "mechanism": f"Register a vault cred for the {payload['server']} MCP server; replace the mock " + f"{payload['server']}_action custom tool with the real MCP toolset (always_ask).", + }) + elif kind == "tool": + if payload not in agent["tools"]: + agent["tools"].append(payload) + networking = "limited" # web usually wants a host allowlist as a v1 hardening + + # actions that clearly defer (send/publish) stay human-in-loop in v0 + for a in actions: + al = a.lower() + if any(k in al for k in ("send", "publish", "post", "email out", "reply")): + deferrals.append({ + "version": "v2", + "item": f"Automate '{a}'", + "reason": "Irreversible/outward action — keep human review in v0", + "mechanism": f"Add a custom tool for '{a}' behind always_ask; promote to always_allow only after grading.", + }) + + outcome = None + if dod: + outcome = { + "description": job.strip(), + "rubric": _rubric_from_dod(dod), + "max_iterations": 5, + } + + deployment = None + rec = (recurrence or "once").lower() + if rec in ("daily", "weekly", "hourly", "custom") or trigger == "schedule": + deployment = {"schedule": {"expression": _cron_for(rec), "timezone": "UTC"}} + + return { + "agent_name": name or _slug(job), + "goal": job.strip(), + "primitives": { + "agent": agent, + "environment": {"type": "cloud", "networking": networking, "allowed_hosts": allowed_hosts, "packages": {}}, + "session": session, + **({"outcome": outcome} if outcome else {}), + **({"deployment": deployment} if deployment else {}), + }, + "deferrals": deferrals, + "eval_plan": {"success_criteria": _criteria_from_dod(dod), "held_back_cases": []}, + "_notes": { + "trigger": trigger, "recurrence": rec, + "loop_hint": "cron-loop" if deployment else ("grade-iterate" if outcome else "single-pass"), + }, + } + + +def _rubric_from_dod(dod: str) -> str: + parts = [p.strip() for p in dod.replace(";", ",").split(",") if p.strip()] + if not parts: + parts = [dod.strip()] + return "\n".join(f"- {p}" for p in parts) + + +def _criteria_from_dod(dod): + if not dod: + return [] + return [p.strip() for p in dod.replace(";", ",").split(",") if p.strip()] + + +def _cron_for(rec: str) -> str: + return {"daily": "0 9 * * *", "weekly": "0 8 * * 1", "hourly": "0 * * * *"}.get(rec, "0 9 * * *") + + +def _slug(text: str) -> str: + s = "".join(c.lower() if c.isalnum() else "-" for c in text).strip("-") + while "--" in s: + s = s.replace("--", "-") + return (s[:40] or "agent").strip("-") + + +def main() -> int: + ap = argparse.ArgumentParser(description="Map interview answers to a CMA primitives skeleton + deferrals.") + ap.add_argument("--job", help="One-sentence job the agent does.") + ap.add_argument("--trigger", choices=sorted(TRIGGERS), default="ask") + ap.add_argument("--inputs", default="", help="Comma list: files,repo,memory,gmail,slack,github,web") + ap.add_argument("--actions", default="", help="Comma list of actions the agent takes.") + ap.add_argument("--dod", default="", help="Definition of done (becomes the rubric).") + ap.add_argument("--recurrence", choices=sorted(RECURRENCE), default="once") + ap.add_argument("--model", default="claude-opus-4-8") + ap.add_argument("--name", default=None) + ap.add_argument("--answers", help="JSON file with the same keys instead of flags.") + ap.add_argument("--json", action="store_true") + ap.add_argument("--out", help="Write the plan JSON to this path.") + ap.add_argument("--sample", action="store_true") + args = ap.parse_args() + + if args.sample: + p = plan("Triage overnight support email", "schedule", ["gmail", "memory"], + ["label", "reply"], "one label per email, grounded reason, no invented facts", + "daily", "claude-opus-4-8", "support-triage") + print(json.dumps(p, indent=2)) + return 0 + + if args.answers: + data = json.loads(Path(args.answers).read_text()) + job = data.get("job", "") + trigger = data.get("trigger", "ask") + inputs = data.get("inputs", []) if isinstance(data.get("inputs"), list) else str(data.get("inputs", "")).split(",") + actions = data.get("actions", []) if isinstance(data.get("actions"), list) else str(data.get("actions", "")).split(",") + dod = data.get("dod", "") + recurrence = data.get("recurrence", "once") + model = data.get("model", "claude-opus-4-8") + name = data.get("name") + else: + if not args.job: + print("Provide --job (or --answers file).", file=sys.stderr) + return 2 + job, trigger = args.job, args.trigger + inputs = args.inputs.split(",") + actions = args.actions.split(",") + dod, recurrence, model, name = args.dod, args.recurrence, args.model, args.name + + p = plan(job, trigger, inputs, actions, dod, recurrence, model, name) + out = json.dumps(p, indent=2) + if args.out: + Path(args.out).parent.mkdir(parents=True, exist_ok=True) + Path(args.out).write_text(out + "\n") + print(f"Wrote plan to {args.out}") + if args.json or not args.out: + print(out) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agent-launcher/skills/interview/scripts/primitives_validator.py b/agent-launcher/skills/interview/scripts/primitives_validator.py new file mode 100644 index 00000000..f0cb15e2 --- /dev/null +++ b/agent-launcher/skills/interview/scripts/primitives_validator.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""primitives_validator.py — validate a build sheet against CMA limits. + +Deterministic checks against the documented ceilings in +references/cma-primitives.md. Emits PASS / WARN / FAIL with per-check detail. +Exit 0 = PASS (warnings allowed), 1 = FAIL. Stdlib-only; no network calls. + +Limits enforced: <=20 skills/session, <=8 memory stores, multiagent depth-1 & +<=20 roster & <=25 threads, outcome max_iterations 1..20, <=20 creds/vault, +required agent.model, rubric present if outcome present, cron 5-field if scheduled. + +Examples: + primitives_validator.py --sheet ./my-agent/build-sheet.json + primitives_validator.py --sample +""" +import argparse +import json +import sys +from pathlib import Path + +LIMITS = { + "skills_per_session": 20, + "memory_stores": 8, + "multiagent_roster": 20, + "multiagent_threads": 25, + "max_iterations": 20, + "creds_per_vault": 20, +} + + +def validate(sheet: dict): + checks = [] + + def add(level, name, msg): + checks.append({"level": level, "check": name, "detail": msg}) + + prim = sheet.get("primitives", {}) + agent = prim.get("agent", {}) + env = prim.get("environment", {}) + session = prim.get("session", {}) + + # required fields + if not sheet.get("agent_name"): + add("FAIL", "agent_name", "missing required agent_name") + if not sheet.get("goal"): + add("FAIL", "goal", "missing required one-sentence goal") + if not agent.get("model"): + add("FAIL", "agent.model", "agent.model is required (Claude 4.5-family or later)") + else: + add("PASS", "agent.model", agent["model"]) + if not env.get("type"): + add("FAIL", "environment.type", "environment.type required (cloud|self_hosted)") + + # skills ceiling + skills = agent.get("skills", []) or [] + if len(skills) > LIMITS["skills_per_session"]: + add("FAIL", "skills", f"{len(skills)} skills > {LIMITS['skills_per_session']} per session") + elif skills: + add("PASS", "skills", f"{len(skills)} skills") + + # memory stores ceiling + stores = session.get("memory_stores", []) or [] + if len(stores) > LIMITS["memory_stores"]: + add("FAIL", "memory_stores", f"{len(stores)} > {LIMITS['memory_stores']} per session") + elif stores: + add("PASS", "memory_stores", f"{len(stores)} store(s)") + # injection warning + for s in stores: + if s.get("access", "read_write") == "read_write": + add("WARN", "memory.access", "read_write memory + untrusted input can be poisoned by prompt injection; " + "prefer read_only where possible") + break + + # multiagent + ma = agent.get("multiagent") + if ma: + roster = ma.get("agents", []) or [] + if len(roster) > LIMITS["multiagent_roster"]: + add("FAIL", "multiagent.roster", f"{len(roster)} > {LIMITS['multiagent_roster']}") + if ma.get("depth", 1) and int(ma.get("depth", 1)) > 1: + add("FAIL", "multiagent.depth", "depth must be 1") + + # outcome + outcome = prim.get("outcome") + if outcome: + if not outcome.get("rubric", "").strip(): + add("FAIL", "outcome.rubric", "outcome present but rubric is empty (rubric is required)") + else: + add("PASS", "outcome.rubric", "rubric present") + mi = outcome.get("max_iterations", 3) + try: + mi = int(mi) + if mi < 1 or mi > LIMITS["max_iterations"]: + add("FAIL", "outcome.max_iterations", f"{mi} outside 1..{LIMITS['max_iterations']}") + else: + add("PASS", "outcome.max_iterations", str(mi)) + except (TypeError, ValueError): + add("FAIL", "outcome.max_iterations", "not an integer") + + # deployment cron shape + dep = prim.get("deployment") + if dep: + expr = dep.get("schedule", {}).get("expression", "") + fields = expr.split() + if len(fields) != 5: + add("FAIL", "deployment.cron", f"expected 5-field POSIX cron, got {len(fields)} field(s): {expr!r}") + else: + add("PASS", "deployment.cron", expr) + if not dep.get("schedule", {}).get("timezone"): + add("WARN", "deployment.timezone", "no IANA timezone set; defaults to UTC") + + # vaults + vaults = session.get("vault_ids", []) or [] + # (creds-per-vault is enforced at vault build time; here just note count) + if vaults: + add("PASS", "vaults", f"{len(vaults)} vault(s) referenced") + + # networking hardening hint + if env.get("networking", "unrestricted") == "unrestricted": + add("WARN", "networking", "unrestricted networking; tighten to 'limited' with allowed_hosts as a v1 step") + + # deferrals discipline + for d in sheet.get("deferrals", []) or []: + for k in ("version", "item", "reason", "mechanism"): + if not d.get(k): + add("WARN", "deferral", f"deferral missing '{k}': {d.get('item', d)}") + + fails = [c for c in checks if c["level"] == "FAIL"] + warns = [c for c in checks if c["level"] == "WARN"] + verdict = "FAIL" if fails else ("WARN" if warns else "PASS") + return verdict, checks + + +def _emit(verdict, checks, as_json): + if as_json: + print(json.dumps({"verdict": verdict, "checks": checks}, indent=2)) + return + print(f"VERDICT: {verdict}") + for c in checks: + print(f" [{c['level']:4}] {c['check']}: {c['detail']}") + + +def main() -> int: + ap = argparse.ArgumentParser(description="Validate a build sheet against CMA limits (exit 0 pass, 1 fail).") + ap.add_argument("--sheet", help="Path to build-sheet.json.") + ap.add_argument("--json", action="store_true") + ap.add_argument("--sample", action="store_true") + args = ap.parse_args() + + if args.sample: + bad = { + "agent_name": "x", "goal": "do stuff", + "primitives": { + "agent": {"model": "claude-opus-4-8", "skills": ["s" + str(i) for i in range(22)]}, + "environment": {"type": "cloud", "networking": "unrestricted"}, + "session": {"memory_stores": [{"access": "read_write"}] * 9}, + "outcome": {"rubric": "", "max_iterations": 99}, + "deployment": {"schedule": {"expression": "0 9 * *"}}, + }, + "deferrals": [{"version": "v1", "item": "x"}], + } + v, c = validate(bad) + _emit(v, c, False) + print("\n(sample intentionally trips several FAIL/WARN checks)") + return 0 + + if not args.sheet: + print("Provide --sheet path.", file=sys.stderr) + return 2 + sheet = json.loads(Path(args.sheet).read_text()) + verdict, checks = validate(sheet) + _emit(verdict, checks, args.json) + return 1 if verdict == "FAIL" else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agent-launcher/skills/run-without-you/SKILL.md b/agent-launcher/skills/run-without-you/SKILL.md new file mode 100644 index 00000000..145c74f0 --- /dev/null +++ b/agent-launcher/skills/run-without-you/SKILL.md @@ -0,0 +1,78 @@ +--- +name: run-without-you +description: Phase 4 of building a Claude Managed Agent — make it run without you. Turn a graded agent into a recurring scheduled deployment (POSIX-cron), an event-driven curl trigger, or confirmed on-demand use, then finalize the versioned roadmap. Use when the user says "run it every morning", "put it on a schedule", "nightly", "weekly", "automate this", "make it recurring", or when the orchestrator routes phase=run-without-you. deployment_builder.py builds the POST /v1/deployments payload (initial_events must include user.message; optionally nests a user.define_outcome so each firing self-grades); cron_validator.py validates the 5-field cron + IANA timezone and prints the wall-clock DST note; next_directions_writer.py writes NEXT-DIRECTIONS.md. No tool makes API calls — the deployment is created via BYOK curl. Distinct from grade-iterate (the in-session loop) and wrap-up (closeout). +version: 2.12.0 +author: Alireza Rezvani +license: MIT +tags: [cma, deployment, cron, schedule, recurring, run-without-you, next-directions, dst] +compatible_tools: [claude-code, codex-cli, cursor, antigravity, opencode, gemini-cli] +--- + +# Phase 4 — Run Without You (the recurring loop) + +A **scheduled deployment** fires a fresh session on a cron cadence — the agent +runs without you. Each firing can carry its own outcome, nesting the bounded +grade→iterate loop inside every recurring run. + +See [`references/loops-and-workflows.md`](../../references/loops-and-workflows.md). + +## Choose the trigger + +| Answer | Shape | Tool | +|---|---|---| +| "every morning / weekly / nightly" | recurring cron deployment | `deployment_builder.py` + `cron_validator.py` | +| "when X happens" | event-driven curl (documented, not scheduled) | `deployment_builder.py` (message only) | +| "only when I ask" | on-demand (no deployment) | none — just re-send a `user.message` | + +## Workflow (recurring) + +1. **Validate the schedule.** + ```bash + python3 skills/run-without-you/scripts/cron_validator.py --cron "0 9 * * *" --timezone Europe/Berlin + ``` + Invalid cron/timezone → exit 1. Read the **DST note**: wall-clock semantics mean + spring-forward times are skipped and fall-back times fire twice — avoid + 02:00–03:00 in DST zones if exactly-once matters. +2. **Build the deployment payload.** + ```bash + python3 skills/run-without-you/scripts/deployment_builder.py \ + --sheet ./my-agent/build-sheet.json --agent-id agent_123 --env-id env_456 \ + --nest-outcome --out ./my-agent/payloads/deployment.json + ``` + `--nest-outcome` includes the rubric so **each firing self-grades**. The tool + prints the BYOK curl to create it and to **test it once** with the manual `run` + endpoint before trusting the schedule. +3. **Test before you trust.** Fire one manual `run`, read the verdict, only then + leave the cron in place. Pin the agent version in the deployment once it passes. +4. **Finalize the roadmap.** + ```bash + python3 skills/run-without-you/scripts/next_directions_writer.py \ + --sheet ./my-agent/build-sheet.json --loop-shape cron-loop --last-verdict satisfied --out-dir ./my-agent + ``` +5. **Advance + hand to wrap-up.** `goal_state.py set --phase wrap-up`, then invoke + the `wrap-up` skill. + +## Hard rules + +- **Test with a manual `run` first.** Never commit a schedule you haven't fired once. +- **Safety rails on by default.** `always_ask` MCP, `limited` networking where you + can, `read_only` untrusted memory, `max_iterations` per firing, workspace spend + limit. There is no spend cap inside CMA. +- **DST is wall-clock.** Surface the note; pick safe times. +- **≤1,000 deployments/org.** + +## Forcing-question library (recommend + cite) + +1. "Cadence, event, or on-request?" *Recommend:* on-request v0 → cadence once graded. + *Cite:* loops-and-workflows.md. +2. "Should each firing self-grade?" *Recommend:* yes — nest the outcome. *Cite:* + loops-and-workflows.md (nesting rule). +3. "Which timezone, and is the time DST-safe?" *Recommend:* avoid 02:00–03:00 in + DST zones. *Cite:* cma-primitives.md (wall-clock DST). +4. "Did you fire one manual run first?" *Recommend:* always. *Cite:* this SKILL. + +## Tools + +- `scripts/deployment_builder.py` — POST /v1/deployments payload (+ test-run curl). +- `scripts/cron_validator.py` — 5-field cron + IANA tz + DST note. +- `scripts/next_directions_writer.py` — write/refresh NEXT-DIRECTIONS.md. diff --git a/agent-launcher/skills/run-without-you/scripts/cron_validator.py b/agent-launcher/skills/run-without-you/scripts/cron_validator.py new file mode 100644 index 00000000..00b47d37 --- /dev/null +++ b/agent-launcher/skills/run-without-you/scripts/cron_validator.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""cron_validator.py — validate a 5-field POSIX cron expression + IANA timezone. + +CMA scheduled deployments use standard 5-field POSIX cron (minute granularity) +with an IANA timezone and wall-clock DST semantics. This validates the shape and +ranges, resolves the timezone against the local zoneinfo database when available, +and prints the DST behavior note. Exit 0 valid, 1 invalid. Stdlib-only. + +Fields: minute(0-59) hour(0-23) day-of-month(1-31) month(1-12) day-of-week(0-6). +Each supports *, a-b ranges, a,b,c lists, and */step. + +Examples: + cron_validator.py --cron "0 9 * * *" --timezone Europe/Berlin + cron_validator.py --cron "*/15 0-6 * * 1-5" --timezone America/New_York + cron_validator.py --sample +""" +import argparse +import json +import sys + +RANGES = [(0, 59), (0, 23), (1, 31), (1, 12), (0, 6)] +NAMES = ["minute", "hour", "day-of-month", "month", "day-of-week"] + + +def _valid_field(field: str, lo: int, hi: int): + for part in field.split(","): + if part == "*": + continue + step_base, _, step = part.partition("/") + if step: + if not step.isdigit() or int(step) < 1: + return False, f"bad step '{part}'" + if step_base == "*" or step_base == "": + continue + if "-" in step_base: + a, _, b = step_base.partition("-") + if not (a.isdigit() and b.isdigit()): + return False, f"bad range '{part}'" + a, b = int(a), int(b) + if a < lo or b > hi or a > b: + return False, f"range '{part}' outside {lo}-{hi}" + else: + if not step_base.isdigit(): + return False, f"non-numeric '{part}'" + v = int(step_base) + if v < lo or v > hi: + return False, f"value {v} outside {lo}-{hi}" + return True, None + + +def validate_cron(expr: str): + fields = expr.split() + if len(fields) != 5: + return False, [f"expected 5 fields, got {len(fields)}: {expr!r}"] + errors = [] + for field, (lo, hi), name in zip(fields, RANGES, NAMES): + ok, err = _valid_field(field, lo, hi) + if not ok: + errors.append(f"{name}: {err}") + return (not errors), errors + + +def validate_tz(tz: str): + if not tz: + return False, "no timezone given (IANA id required, e.g. Europe/Berlin)" + try: + from zoneinfo import ZoneInfo, ZoneInfoNotFoundError + try: + ZoneInfo(tz) + return True, None + except ZoneInfoNotFoundError: + return False, f"IANA timezone '{tz}' not found in the zoneinfo database" + except ImportError: + # zoneinfo unavailable; do a light sanity check. + if "/" in tz or tz in ("UTC",): + return True, "zoneinfo unavailable; shape looks like an IANA id (not verified)" + return False, f"'{tz}' does not look like an IANA id and zoneinfo is unavailable" + + +DST_NOTE = ("Wall-clock DST: the expression fires at literal local time. On " + "spring-forward, a nonexistent local time is skipped; on fall-back, a " + "repeated local time fires twice. Avoid scheduling 02:00-03:00 in " + "DST-observing zones if exactly-once matters.") + + +def main() -> int: + ap = argparse.ArgumentParser(description="Validate a 5-field POSIX cron + IANA timezone for a CMA deployment.") + ap.add_argument("--cron", help="5-field cron expression.") + ap.add_argument("--timezone", default="UTC") + ap.add_argument("--json", action="store_true") + ap.add_argument("--sample", action="store_true") + args = ap.parse_args() + + if args.sample: + for expr, tz in [("0 9 * * *", "Europe/Berlin"), ("*/15 0-6 * * 1-5", "America/New_York"), + ("0 25 * * *", "UTC"), ("0 9 * *", "Mars/Olympus")]: + ok, errs = validate_cron(expr) + tzok, tznote = validate_tz(tz) + print(f"[{'OK ' if ok and tzok else 'BAD'}] {expr!r} @ {tz}: " + f"{'valid' if ok else errs}; tz {'valid' if tzok else tznote}") + print(f"\n{DST_NOTE}") + return 0 + + if not args.cron: + print("Provide --cron.", file=sys.stderr) + return 2 + ok, errs = validate_cron(args.cron) + tzok, tznote = validate_tz(args.timezone) + result = {"cron": args.cron, "cron_valid": ok, "cron_errors": errs, + "timezone": args.timezone, "timezone_valid": tzok, "timezone_note": tznote, + "dst_note": DST_NOTE} + if args.json: + print(json.dumps(result, indent=2)) + else: + print(f"cron: {'VALID' if ok else 'INVALID'} — {args.cron}") + for e in errs: + print(f" - {e}") + print(f"timezone: {'VALID' if tzok else 'INVALID'} — {args.timezone}") + if tznote: + print(f" {tznote}") + print(f"DST: {DST_NOTE}") + return 0 if (ok and tzok) else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agent-launcher/skills/run-without-you/scripts/deployment_builder.py b/agent-launcher/skills/run-without-you/scripts/deployment_builder.py new file mode 100644 index 00000000..7a829fe3 --- /dev/null +++ b/agent-launcher/skills/run-without-you/scripts/deployment_builder.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +"""deployment_builder.py — build a POST /v1/deployments payload (the recurring loop). + +A scheduled deployment fires a fresh session on a POSIX-cron cadence ("run without +you"). initial_events MUST include a user.message; it MAY also carry a +user.define_outcome so each firing self-grades (the nested loop). Emits the +deployment payload + a BYOK curl snippet to create it and to test it once via the +manual `run` endpoint. Stdlib-only; no network calls and no key handling. + +Examples: + deployment_builder.py --sheet ./my-agent/build-sheet.json --nest-outcome --out ./my-agent/payloads/deployment.json + deployment_builder.py --name nightly --agent-id agent_123 --env-id env_456 \ + --message "Run the audit" --cron "0 2 * * *" --timezone UTC + deployment_builder.py --sample +""" +import argparse +import json +import sys +from pathlib import Path + +# reuse cron validation from the sibling tool +sys.path.insert(0, str(Path(__file__).resolve().parent)) +try: + import cron_validator # type: ignore +except ImportError: + cron_validator = None + + +def build(name, agent_ref, env_id, message, cron, timezone, outcome): + initial_events = [{"type": "user.message", "content": message}] + if outcome: + initial_events.append(outcome if outcome.get("type") == "user.define_outcome" + else {"type": "user.define_outcome", **outcome}) + payload = { + "name": name, + "agent": agent_ref, + "environment_id": env_id, + "initial_events": initial_events, + "schedule": {"type": "cron", "expression": cron, "timezone": timezone or "UTC"}, + } + return payload + + +def curl_snippet(name): + return ( + "# Create the deployment (reads $ANTHROPIC_API_KEY from your env; never paste the key):\n" + "curl -sS -X POST \"$ANTHROPIC_BASE_URL/v1/deployments\" \\\n" + " -H \"x-api-key: $ANTHROPIC_API_KEY\" -H \"anthropic-version: 2023-06-01\" \\\n" + " -H \"anthropic-beta: managed-agents-2026-04-01\" -H \"content-type: application/json\" \\\n" + f" -d @./my-agent/payloads/deployment.json\n" + "# Test it ONCE before trusting the schedule (manual run):\n" + "curl -sS -X POST \"$ANTHROPIC_BASE_URL/v1/deployments/$DEPL_ID/run\" \\\n" + " -H \"x-api-key: $ANTHROPIC_API_KEY\" -H \"anthropic-version: 2023-06-01\" \\\n" + " -H \"anthropic-beta: managed-agents-2026-04-01\"\n" + ) + + +def main() -> int: + ap = argparse.ArgumentParser(description="Build a POST /v1/deployments payload (recurring cron loop).") + ap.add_argument("--sheet", help="build-sheet.json (reads agent_name, goal, outcome, deployment.schedule).") + ap.add_argument("--name") + ap.add_argument("--agent-id", help="agent_... id (or use --agent-version to pin).") + ap.add_argument("--agent-version", type=int, default=None) + ap.add_argument("--env-id") + ap.add_argument("--message") + ap.add_argument("--cron") + ap.add_argument("--timezone", default="UTC") + ap.add_argument("--nest-outcome", action="store_true", help="Include the outcome so each firing self-grades.") + ap.add_argument("--out") + ap.add_argument("--json", action="store_true") + ap.add_argument("--sample", action="store_true") + args = ap.parse_args() + + if args.sample: + sheet = json.loads((Path(__file__).resolve().parents[3] / "assets" / "example-build-sheet.json").read_text()) + oc = sheet["primitives"].get("outcome") + sched = sheet["primitives"].get("deployment", {}).get("schedule", {}) + payload = build(sheet["agent_name"] + "-nightly", + {"type": "agent", "id": "agent_EXAMPLE"}, "env_EXAMPLE", + sheet["goal"], sched.get("expression", "0 9 * * *"), + sched.get("timezone", "UTC"), + {"type": "user.define_outcome", **oc} if oc else None) + print(json.dumps(payload, indent=2)) + print("\n" + curl_snippet(payload["name"]), file=sys.stderr) + return 0 + + name = args.name + message = args.message + cron = args.cron + timezone = args.timezone + outcome = None + agent_ref = None + + if args.sheet: + sheet = json.loads(Path(args.sheet).read_text()) + name = name or (sheet.get("agent_name", "agent") + "-scheduled") + message = message or sheet.get("goal", "Run the task.") + sched = sheet.get("primitives", {}).get("deployment", {}).get("schedule", {}) + cron = cron or sched.get("expression") + timezone = timezone if args.timezone != "UTC" else sched.get("timezone", "UTC") + if args.nest_outcome and sheet.get("primitives", {}).get("outcome"): + oc = sheet["primitives"]["outcome"] + outcome = {"type": "user.define_outcome", **oc} + + if args.agent_id: + agent_ref = {"type": "agent", "id": args.agent_id} + if args.agent_version is not None: + agent_ref["version"] = args.agent_version + elif agent_ref is None: + agent_ref = {"type": "agent", "id": "${AGENT_ID}"} + + if not (name and message and cron): + print("Need at least --name/--message/--cron (or a --sheet supplying them).", file=sys.stderr) + return 2 + + # validate cron before emitting + if cron_validator: + ok, errs = cron_validator.validate_cron(cron) + tzok, _ = cron_validator.validate_tz(timezone) + if not ok: + print(f"ERROR: invalid cron {cron!r}: {errs}", file=sys.stderr) + return 1 + if not tzok: + print(f"WARN: timezone {timezone!r} may be invalid.", file=sys.stderr) + + payload = build(name, agent_ref, args.env_id or "${ENV_ID}", message, cron, timezone, outcome) + text = json.dumps(payload, indent=2) + if args.out: + Path(args.out).parent.mkdir(parents=True, exist_ok=True) + Path(args.out).write_text(text + "\n") + print(f"Wrote {args.out}") + print("\n" + curl_snippet(name), file=sys.stderr) + if args.json or not args.out: + print(text) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agent-launcher/skills/run-without-you/scripts/next_directions_writer.py b/agent-launcher/skills/run-without-you/scripts/next_directions_writer.py new file mode 100644 index 00000000..c113dc04 --- /dev/null +++ b/agent-launcher/skills/run-without-you/scripts/next_directions_writer.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""next_directions_writer.py — write/refresh ./my-agent/NEXT-DIRECTIONS.md. + +Renders the versioned roadmap from a build sheet's deferrals + the current goal +state: v0 (live) plus a table of deferred upgrades (version/item/reason/mechanism) +and the suggested next 1-2 moves. Uses the assets/NEXT-DIRECTIONS.template.md +shape. Stdlib-only; no network calls. + +Examples: + next_directions_writer.py --sheet ./my-agent/build-sheet.json --out-dir ./my-agent + next_directions_writer.py --sample +""" +import argparse +import datetime as dt +import json +import sys +from pathlib import Path + + +def _now(): + return dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat() + + +def render(sheet, loop_shape, last_verdict): + goal = sheet.get("goal", "") + name = sheet.get("agent_name", "agent") + prim = sheet.get("primitives", {}) + live = [] + if prim.get("agent", {}).get("model"): + live.append(f"agent[{prim['agent']['model']}]") + live.append(f"env[{prim.get('environment', {}).get('type', 'cloud')}]") + if prim.get("outcome"): + live.append("outcome[rubric]") + if prim.get("session", {}).get("memory_stores"): + live.append(f"memory[{len(prim['session']['memory_stores'])}]") + if prim.get("deployment"): + live.append("deployment[cron]") + + deferrals = sheet.get("deferrals", []) or [] + if deferrals: + rows = "\n".join( + f"| {d.get('version','v?')} | {d.get('item','')} | {d.get('reason','')} | {d.get('mechanism','')} |" + for d in deferrals + ) + else: + rows = "| — | (none recorded) | | |" + + next_moves = _next_moves(deferrals, prim) + + tmpl_path = Path(__file__).resolve().parents[3] / "assets" / "NEXT-DIRECTIONS.template.md" + tmpl = tmpl_path.read_text() + return tmpl.format( + agent_name=name, + goal=goal, + loop_shape=loop_shape or sheet.get("loop_hint", "single-pass"), + live_primitives=", ".join(live), + last_verdict=last_verdict or "(not graded yet)", + deferral_rows=rows, + next_moves=next_moves, + updated_at=_now(), + ) + + +def _next_moves(deferrals, prim): + moves = [] + v1s = [d for d in deferrals if d.get("version") == "v1"] + if v1s: + moves.append(f"1. Ship {v1s[0].get('item')} — {v1s[0].get('mechanism')}") + if not prim.get("deployment"): + moves.append(f"{len(moves)+1}. Promote to a scheduled deployment once a version passes the rubric.") + elif prim.get("environment", {}).get("networking") == "unrestricted": + moves.append(f"{len(moves)+1}. Harden networking to 'limited' with an allowed_hosts list.") + if not moves: + moves.append("1. Monitor the first few scheduled runs; tighten the rubric if verdicts drift.") + return "\n".join(moves) + + +def main() -> int: + ap = argparse.ArgumentParser(description="Write/refresh NEXT-DIRECTIONS.md from a build sheet.") + ap.add_argument("--sheet", help="build-sheet.json.") + ap.add_argument("--out-dir", default="./my-agent") + ap.add_argument("--loop-shape", default=None) + ap.add_argument("--last-verdict", default=None) + ap.add_argument("--stdout", action="store_true") + ap.add_argument("--sample", action="store_true") + args = ap.parse_args() + + if args.sample: + sheet = json.loads((Path(__file__).resolve().parents[3] / "assets" / "example-build-sheet.json").read_text()) + print(render(sheet, "cron-loop", "satisfied")) + return 0 + + if not args.sheet: + print("Provide --sheet path.", file=sys.stderr) + return 2 + sheet = json.loads(Path(args.sheet).read_text()) + md = render(sheet, args.loop_shape, args.last_verdict) + if args.stdout: + print(md) + return 0 + out_dir = Path(args.out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + dest = out_dir / "NEXT-DIRECTIONS.md" + dest.write_text(md) + print(f"Wrote {dest}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agent-launcher/skills/stage-launch/SKILL.md b/agent-launcher/skills/stage-launch/SKILL.md new file mode 100644 index 00000000..8e8501d6 --- /dev/null +++ b/agent-launcher/skills/stage-launch/SKILL.md @@ -0,0 +1,76 @@ +--- +name: stage-launch +description: Phase 2 of building a Claude Managed Agent — turn a validated build sheet into exact API payloads and a resumable BYOK curl launch script, then launch (environment → agent → session → kickoff) using the founder's OWN Anthropic key. Use when the user says "launch it", "deploy the agent", "create the agent now", or when the orchestrator routes phase=stage-launch. payload_generator.py emits the four ordered payloads; launch_script_writer.py writes launch.sh that reads $ANTHROPIC_API_KEY at runtime and never embeds it; payload_validator.py runs a pre-launch check including an API-key-leak scan. No tool in this skill makes network calls — the user runs launch.sh themselves. Distinct from interview (planning) and grade-iterate (the outcome loop). +version: 2.12.0 +author: Alireza Rezvani +license: MIT +tags: [cma, launch, payloads, curl, byok, api-key-safety, environment, agent, session] +compatible_tools: [claude-code, codex-cli, cursor, antigravity, opencode, gemini-cli] +--- + +# Phase 2 — Stage → Launch + +Turn the build sheet into runnable artifacts, then let the founder launch with +their own key. **No script here touches the network or the key** — the user runs +`launch.sh`. + +## Workflow + +1. **Generate payloads.** + ```bash + python3 skills/stage-launch/scripts/payload_generator.py \ + --sheet ./my-agent/build-sheet.json --out-dir ./my-agent + # -> ./my-agent/payloads/{01-environment,02-agent,03-session,04-kickoff}.json + ``` + Agent toolset → `always_allow`; every MCP toolset → `always_ask` (baked into + the agent payload's `permission_policies`). +2. **Write the launch script.** + ```bash + python3 skills/stage-launch/scripts/launch_script_writer.py --out-dir ./my-agent + ``` + `launch.sh` creates environment → agent → session → kickoff **in order**, + chaining IDs, and **resumes** on re-run (each step skips if its `*.id` file + exists). It reads `$ANTHROPIC_API_KEY` at runtime. +3. **Validate before launch.** + ```bash + python3 skills/stage-launch/scripts/payload_validator.py --dir ./my-agent + ``` + FAIL blocks — especially a `key_leak` finding. Fix and re-run. +4. **Minimal key step (never in chat).** Check the shell first: + ```bash + [ -n "$ANTHROPIC_API_KEY" ] && echo "key present" || echo "export ANTHROPIC_API_KEY=... first" + ``` + Point the founder to platform.claude.com → API keys. **Never print the key to + chat, never write it to a file.** +5. **Launch + watch the first poll.** + ```bash + export ANTHROPIC_API_KEY=... # in their shell, not in chat + ./my-agent/launch.sh + ``` + Mark checkpoints with Console deep links. Then `goal_state.py set --phase + grade-iterate` and advance. + +## Hard rules (API-key safety) + +- **The key never enters chat, a file, a payload, or a log.** `launch.sh` reads it + from the environment; `payload_validator.py` scans for `sk-ant-…` leaks and FAILs. +- **Sequential launch.** environment → agent → session → kickoff. Watch the first + poll foreground before declaring success. +- **Resumable.** Re-running `launch.sh` continues from the last created ID. + +## Forcing-question library (recommend + cite) + +1. "Is the key in your shell env already?" *Recommend:* check `$ANTHROPIC_API_KEY` + before anything. *Cite:* this SKILL, key-safety rules. +2. "Cloud or self-hosted environment?" *Recommend:* cloud for v0. *Cite:* + cma-primitives.md (environment). +3. "Any MCP server in the payload?" *Recommend:* keep it `always_ask`. *Cite:* + cma-primitives.md (permissions). +4. "Did the first poll return idle/running cleanly?" *Recommend:* watch it + foreground before moving on. *Cite:* cma-primitives.md (session lifecycle). + +## Tools + +- `scripts/payload_generator.py` — build sheet → 4 ordered API payloads. +- `scripts/launch_script_writer.py` — resumable BYOK curl launcher (no key handling). +- `scripts/payload_validator.py` — pre-launch check + API-key-leak scan. diff --git a/agent-launcher/skills/stage-launch/scripts/launch_script_writer.py b/agent-launcher/skills/stage-launch/scripts/launch_script_writer.py new file mode 100644 index 00000000..ee56b3be --- /dev/null +++ b/agent-launcher/skills/stage-launch/scripts/launch_script_writer.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""launch_script_writer.py — emit a resumable BYOK curl launch script. + +Writes ./my-agent/launch.sh that creates environment -> agent -> session -> +kickoff in order, chaining IDs between steps. The script reads the key from +$ANTHROPIC_API_KEY at runtime; this generator NEVER accepts, prints, logs, or +writes an API key. Idempotent-ish: each step guards on whether its ID file +already exists so a re-run resumes rather than duplicates. + +Stdlib-only; no network calls. + +Examples: + launch_script_writer.py --out-dir ./my-agent + launch_script_writer.py --sample +""" +import argparse +import stat +import sys +from pathlib import Path + +BASE_URL_DEFAULT = "https://api.anthropic.com" + +TEMPLATE = r'''#!/usr/bin/env bash +# agent-launcher — resumable CMA launch (BYOK). +# Reads the key from the environment; NEVER hardcode it here. +# export ANTHROPIC_API_KEY=sk-ant-... # do NOT paste the key into chat +# Re-running resumes: each step skips if its *.id file already exists. +set -euo pipefail + +BASE_URL="${{ANTHROPIC_BASE_URL:-{base_url}}}" +API_VERSION="${{ANTHROPIC_VERSION:-2023-06-01}}" +BETA="${{ANTHROPIC_BETA:-managed-agents-2026-04-01}}" +DIR="$(cd "$(dirname "$0")" && pwd)" +P="$DIR/payloads" + +if [[ -z "${{ANTHROPIC_API_KEY:-}}" ]]; then + echo "ERROR: export ANTHROPIC_API_KEY first (never paste it into chat)." >&2 + exit 1 +fi + +hdr=(-H "x-api-key: $ANTHROPIC_API_KEY" + -H "anthropic-version: $API_VERSION" + -H "anthropic-beta: $BETA" + -H "content-type: application/json") + +extract_id() {{ python3 -c 'import sys,json;print(json.load(sys.stdin).get("id",""))'; }} + +# 1) Environment +if [[ -f "$DIR/env.id" ]]; then + ENV_ID="$(cat "$DIR/env.id")"; echo "resume: env $ENV_ID" +else + ENV_ID="$(curl -sS "${{hdr[@]}}" -X POST "$BASE_URL/v1/environments" -d @"$P/01-environment.json" | extract_id)" + [[ -n "$ENV_ID" ]] || {{ echo "env create failed" >&2; exit 1; }} + echo "$ENV_ID" > "$DIR/env.id"; echo "created env $ENV_ID" +fi + +# 2) Agent +if [[ -f "$DIR/agent.id" ]]; then + AGENT_ID="$(cat "$DIR/agent.id")"; echo "resume: agent $AGENT_ID" +else + AGENT_ID="$(curl -sS "${{hdr[@]}}" -X POST "$BASE_URL/v1/agents" -d @"$P/02-agent.json" | extract_id)" + [[ -n "$AGENT_ID" ]] || {{ echo "agent create failed" >&2; exit 1; }} + echo "$AGENT_ID" > "$DIR/agent.id"; echo "created agent $AGENT_ID" +fi + +# 3) Session (substitute env/agent ids into the payload) +if [[ -f "$DIR/session.id" ]]; then + SESSION_ID="$(cat "$DIR/session.id")"; echo "resume: session $SESSION_ID" +else + SESS_PAYLOAD="$(sed -e "s/\${{ENV_ID}}/$ENV_ID/g" -e "s/\${{AGENT_ID}}/$AGENT_ID/g" "$P/03-session.json")" + SESSION_ID="$(printf '%s' "$SESS_PAYLOAD" | curl -sS "${{hdr[@]}}" -X POST "$BASE_URL/v1/sessions" -d @- | extract_id)" + [[ -n "$SESSION_ID" ]] || {{ echo "session create failed" >&2; exit 1; }} + echo "$SESSION_ID" > "$DIR/session.id"; echo "created session $SESSION_ID" +fi + +# 4) Kickoff (send the first event(s)) +echo "kickoff -> session $SESSION_ID" +curl -sS "${{hdr[@]}}" -X POST "$BASE_URL/v1/sessions/$SESSION_ID/events" -d @"$P/04-kickoff.json" >/dev/null +echo "launched. watch: $BASE_URL/v1/sessions/$SESSION_ID/stream" +echo "Console: https://platform.claude.com/ (agent $AGENT_ID / session $SESSION_ID)" +''' + + +def render(base_url: str) -> str: + return TEMPLATE.format(base_url=base_url) + + +def main() -> int: + ap = argparse.ArgumentParser(description="Emit a resumable BYOK curl launch script (never handles the key).") + ap.add_argument("--out-dir", default="./my-agent") + ap.add_argument("--base-url", default=BASE_URL_DEFAULT) + ap.add_argument("--stdout", action="store_true") + ap.add_argument("--sample", action="store_true") + args = ap.parse_args() + + script = render(args.base_url) + + if args.sample or args.stdout: + print(script) + if args.sample: + print("\n# NOTE: reads $ANTHROPIC_API_KEY at runtime; no key is embedded.", file=sys.stderr) + return 0 + + out_dir = Path(args.out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + dest = out_dir / "launch.sh" + dest.write_text(script) + dest.chmod(dest.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP) + print(f"Wrote {dest} (chmod +x). Run: export ANTHROPIC_API_KEY=... && {dest}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agent-launcher/skills/stage-launch/scripts/payload_generator.py b/agent-launcher/skills/stage-launch/scripts/payload_generator.py new file mode 100644 index 00000000..481a7771 --- /dev/null +++ b/agent-launcher/skills/stage-launch/scripts/payload_generator.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +"""payload_generator.py — build-sheet.json -> exact CMA API payloads. + +Emits the four JSON payloads a launch needs, in order, under +./my-agent/payloads/: 01-environment.json, 02-agent.json, 03-session.json, +04-kickoff.json (a user.message event, optionally with user.define_outcome). +Deterministic; stdlib-only; NO network calls and NO API key handling. + +Examples: + payload_generator.py --sheet ./my-agent/build-sheet.json --out-dir ./my-agent + payload_generator.py --sample +""" +import argparse +import json +import sys +from pathlib import Path + + +def gen(sheet: dict, kickoff_message: str): + prim = sheet.get("primitives", {}) + agent = prim.get("agent", {}) + env = prim.get("environment", {}) + session = prim.get("session", {}) + outcome = prim.get("outcome") + + env_payload = { + "config": {"type": env.get("type", "cloud")}, + "networking": {"mode": env.get("networking", "unrestricted")}, + } + if env.get("networking") == "limited" and env.get("allowed_hosts"): + env_payload["networking"]["allowed_hosts"] = env["allowed_hosts"] + if env.get("packages"): + env_payload["packages"] = env["packages"] + + agent_payload = {"name": sheet.get("agent_name", "agent"), "model": agent.get("model", "claude-opus-4-8")} + if agent.get("system"): + agent_payload["system"] = agent["system"] + for k in ("tools", "mcp_servers", "skills", "multiagent", "description", "metadata"): + if agent.get(k): + agent_payload[k] = agent[k] + # permission-policy hint: agent toolset always_allow, mcp always_ask + agent_payload["permission_policies"] = _permissions(agent) + + session_payload = { + "environment_id": "${ENV_ID}", + "agent": {"type": "agent", "id": "${AGENT_ID}"}, + } + if session.get("resources"): + session_payload["resources"] = session["resources"] + if session.get("vault_ids"): + session_payload["vault_ids"] = session["vault_ids"] + if session.get("memory_stores"): + session_payload["memory_stores"] = [ + {"type": "memory_store", **m} for m in session["memory_stores"] + ] + + msg = kickoff_message or sheet.get("goal", "Begin the task.") + kickoff = {"events": [{"type": "user.message", "content": msg}]} + if outcome: + kickoff["events"].append({ + "type": "user.define_outcome", + "description": outcome.get("description", sheet.get("goal", "")), + "rubric": outcome.get("rubric", ""), + "max_iterations": int(outcome.get("max_iterations", 3)), + }) + + return { + "01-environment.json": env_payload, + "02-agent.json": agent_payload, + "03-session.json": session_payload, + "04-kickoff.json": kickoff, + } + + +def _permissions(agent: dict): + pols = [] + for t in agent.get("tools", []): + ttype = t.get("type", "") + if ttype.startswith("agent_toolset"): + pols.append({"toolset": ttype, "policy": "always_allow"}) + elif ttype == "custom": + pols.append({"tool": t.get("name", "custom"), "policy": "always_ask"}) + for s in agent.get("mcp_servers", []): + pols.append({"mcp_server": s.get("name", s.get("url", "mcp")), "policy": "always_ask"}) + return pols + + +def main() -> int: + ap = argparse.ArgumentParser(description="Generate CMA API payloads from a build sheet.") + ap.add_argument("--sheet", help="Path to build-sheet.json.") + ap.add_argument("--out-dir", default="./my-agent") + ap.add_argument("--message", default=None, help="Override the kickoff user.message.") + ap.add_argument("--json", action="store_true", help="Print all payloads instead of writing files.") + ap.add_argument("--sample", action="store_true") + args = ap.parse_args() + + if args.sample: + sheet = json.loads((Path(__file__).resolve().parents[3] / "assets" / "example-build-sheet.json").read_text()) + payloads = gen(sheet, None) + print(json.dumps(payloads, indent=2)) + return 0 + + if not args.sheet: + print("Provide --sheet path.", file=sys.stderr) + return 2 + sheet = json.loads(Path(args.sheet).read_text()) + payloads = gen(sheet, args.message) + if args.json: + print(json.dumps(payloads, indent=2)) + return 0 + pdir = Path(args.out_dir) / "payloads" + pdir.mkdir(parents=True, exist_ok=True) + for name, body in payloads.items(): + (pdir / name).write_text(json.dumps(body, indent=2) + "\n") + print(f"Wrote {len(payloads)} payloads to {pdir}/") + for name in payloads: + print(f" {name}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agent-launcher/skills/stage-launch/scripts/payload_validator.py b/agent-launcher/skills/stage-launch/scripts/payload_validator.py new file mode 100644 index 00000000..84275d29 --- /dev/null +++ b/agent-launcher/skills/stage-launch/scripts/payload_validator.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +"""payload_validator.py — pre-launch check of generated payloads. + +Validates the four payloads in ./my-agent/payloads/ before you run launch.sh: +required fields present, IDs left as ${ENV_ID}/${AGENT_ID} placeholders (the +script substitutes them), kickoff includes a user.message, an outcome (if any) +has a non-empty rubric and max_iterations in 1..20, and — critically — that NO +payload or the launch script contains an embedded API key. Exit 0 pass, 1 fail. + +Stdlib-only; no network calls. + +Examples: + payload_validator.py --dir ./my-agent + payload_validator.py --sample +""" +import argparse +import json +import re +import sys +from pathlib import Path + +KEY_PATTERNS = [re.compile(r"sk-ant-[A-Za-z0-9_\-]{10,}"), re.compile(r"x-api-key:\s*sk-")] +MAX_ITER = 20 + + +def _fail(checks, name, msg): + checks.append({"level": "FAIL", "check": name, "detail": msg}) + + +def _ok(checks, name, msg): + checks.append({"level": "PASS", "check": name, "detail": msg}) + + +def _warn(checks, name, msg): + checks.append({"level": "WARN", "check": name, "detail": msg}) + + +def validate(directory: Path): + checks = [] + pdir = directory / "payloads" + files = ["01-environment.json", "02-agent.json", "03-session.json", "04-kickoff.json"] + + payloads = {} + for f in files: + p = pdir / f + if not p.exists(): + _fail(checks, f, "payload missing") + continue + try: + payloads[f] = json.loads(p.read_text()) + _ok(checks, f, "present + valid JSON") + except json.JSONDecodeError as e: + _fail(checks, f, f"invalid JSON: {e}") + + # env + env = payloads.get("01-environment.json", {}) + if env and not env.get("config", {}).get("type"): + _fail(checks, "env.config.type", "missing config.type") + + # agent + agent = payloads.get("02-agent.json", {}) + if agent: + if not agent.get("name"): + _fail(checks, "agent.name", "missing name") + if not agent.get("model"): + _fail(checks, "agent.model", "missing model") + else: + _ok(checks, "agent.model", agent["model"]) + # permission discipline: any MCP server should be always_ask + pols = {p.get("mcp_server"): p.get("policy") for p in agent.get("permission_policies", []) if p.get("mcp_server")} + for server, pol in pols.items(): + if pol != "always_ask": + _warn(checks, "permission", f"MCP server {server} not always_ask (got {pol})") + + # session: placeholders should be intact for the script to substitute + session = payloads.get("03-session.json", {}) + raw_session = json.dumps(session) + if session: + if "${ENV_ID}" not in raw_session: + _warn(checks, "session.env_id", "no ${ENV_ID} placeholder (already substituted? re-generate to be safe)") + if "${AGENT_ID}" not in raw_session: + _warn(checks, "session.agent_id", "no ${AGENT_ID} placeholder") + + # kickoff + kickoff = payloads.get("04-kickoff.json", {}) + events = kickoff.get("events", []) if kickoff else [] + if not any(e.get("type") == "user.message" for e in events): + _fail(checks, "kickoff.user_message", "kickoff must include a user.message event") + else: + _ok(checks, "kickoff.user_message", "present") + for e in events: + if e.get("type") == "user.define_outcome": + if not (e.get("rubric") or "").strip(): + _fail(checks, "outcome.rubric", "define_outcome has empty rubric") + mi = e.get("max_iterations", 3) + try: + mi = int(mi) + if not (1 <= mi <= MAX_ITER): + _fail(checks, "outcome.max_iterations", f"{mi} outside 1..{MAX_ITER}") + else: + _ok(checks, "outcome.max_iterations", str(mi)) + except (TypeError, ValueError): + _fail(checks, "outcome.max_iterations", "not an integer") + + # KEY LEAK SCAN across payloads + launch.sh + scan_targets = list(pdir.glob("*.json")) if pdir.exists() else [] + launch = directory / "launch.sh" + if launch.exists(): + scan_targets.append(launch) + leaked = [] + for t in scan_targets: + try: + text = t.read_text() + except OSError: + continue + for pat in KEY_PATTERNS: + if pat.search(text): + # x-api-key: sk- in launch.sh header line with a literal key is a leak; + # the templated $ANTHROPIC_API_KEY reference is fine. + if "$ANTHROPIC_API_KEY" in text and pat.pattern.startswith("x-api-key"): + continue + leaked.append(t.name) + if leaked: + _fail(checks, "key_leak", f"possible embedded API key in: {', '.join(sorted(set(leaked)))}") + else: + _ok(checks, "key_leak", "no embedded key found") + + fails = [c for c in checks if c["level"] == "FAIL"] + warns = [c for c in checks if c["level"] == "WARN"] + verdict = "FAIL" if fails else ("WARN" if warns else "PASS") + return verdict, checks + + +def _emit(verdict, checks, as_json): + if as_json: + print(json.dumps({"verdict": verdict, "checks": checks}, indent=2)) + return + print(f"VERDICT: {verdict}") + for c in checks: + print(f" [{c['level']:4}] {c['check']}: {c['detail']}") + + +def main() -> int: + ap = argparse.ArgumentParser(description="Validate generated payloads pre-launch (incl. API-key leak scan).") + ap.add_argument("--dir", default="./my-agent", help="Folder containing payloads/ and launch.sh.") + ap.add_argument("--json", action="store_true") + ap.add_argument("--sample", action="store_true") + args = ap.parse_args() + + if args.sample: + import tempfile + d = Path(tempfile.mkdtemp(prefix="al-payload-")) + (d / "payloads").mkdir() + (d / "payloads" / "01-environment.json").write_text('{"config":{"type":"cloud"},"networking":{"mode":"unrestricted"}}') + (d / "payloads" / "02-agent.json").write_text('{"name":"a","model":"claude-opus-4-8","permission_policies":[]}') + (d / "payloads" / "03-session.json").write_text('{"environment_id":"${ENV_ID}","agent":{"type":"agent","id":"${AGENT_ID}"}}') + (d / "payloads" / "04-kickoff.json").write_text('{"events":[{"type":"user.message","content":"go"},{"type":"user.define_outcome","rubric":"- works","max_iterations":5}]}') + v, c = validate(d) + _emit(v, c, False) + return 0 + + v, c = validate(Path(args.dir)) + _emit(v, c, args.json) + return 1 if v == "FAIL" else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agent-launcher/skills/wrap-up/SKILL.md b/agent-launcher/skills/wrap-up/SKILL.md new file mode 100644 index 00000000..700b922c --- /dev/null +++ b/agent-launcher/skills/wrap-up/SKILL.md @@ -0,0 +1,63 @@ +--- +name: wrap-up +description: Close out a launched Claude Managed Agent — recap every primitive the founder now owns, regenerate the single-file overview page, and suggest the next 1-2 upgrades. Use when the user says "wrap up", "close this out", "what do I own now", "give me the summary", "recap the agent", or when the orchestrator routes phase=wrap-up. primitives_inventory.py tables everything owned (agent, environment, session, memory, outcome, deployment); overview_page.py regenerates a self-contained ./my-agent/agent-overview.html; upgrade_suggester.py ranks the next moves from recorded deferrals plus standing hardening steps. Companion to run-without-you; the last stop before phase=done. +version: 2.12.0 +author: Alireza Rezvani +license: MIT +tags: [cma, wrap-up, closeout, inventory, overview, upgrades, next-directions] +compatible_tools: [claude-code, codex-cli, cursor, antigravity, opencode, gemini-cli] +--- + +# Wrap-up — close it out + +The explicit close-out. Confirm what's live, regenerate the shareable overview, +and name the next 1–2 upgrades so the founder leaves with a clear roadmap. The +`./my-agent/` folder keeps working after the session ends. + +## Workflow + +1. **Inventory what they own.** + ```bash + python3 skills/wrap-up/scripts/primitives_inventory.py \ + --sheet ./my-agent/build-sheet.json --goal ./my-agent/goal.json + ``` + Tables agent / environment / session / memory / outcome / deployment and the + phases completed. +2. **Regenerate the overview page.** + ```bash + python3 skills/wrap-up/scripts/overview_page.py \ + --sheet ./my-agent/build-sheet.json --out-dir ./my-agent \ + --status live --loop-shape cron-loop --last-verdict satisfied + ``` + Self-contained `agent-overview.html` (inline CSS, theme-aware, no external + assets) — shareable as-is. +3. **Suggest the next moves.** + ```bash + python3 skills/wrap-up/scripts/upgrade_suggester.py --sheet ./my-agent/build-sheet.json --top 2 + ``` + Ranks recorded deferrals (v1 before v2, real-integration first) plus standing + hardening (tighten networking, pin the agent version, nest an outcome). +4. **Finalize.** Ensure `NEXT-DIRECTIONS.md` is current (Phase-4 tool), then + `goal_state.py advance` → `phase=done`. + +## Hard rules + +- **Recap what's actually live** — read it from the sheet + goal state, never + assert primitives that weren't created. +- **The overview is single-file** — no external assets, so it shares cleanly. +- **Every next move names the exact mechanism.** + +## Forcing-question library (recommend + cite) + +1. "Confirm what's live vs still a plan?" *Recommend:* inventory from the sheet. + *Cite:* this SKILL. +2. "Which single upgrade has the highest payoff?" *Recommend:* the top-ranked v1 + deferral. *Cite:* upgrade_suggester ranking. +3. "Is the overview page current?" *Recommend:* regenerate after any change. + *Cite:* this SKILL. + +## Tools + +- `scripts/primitives_inventory.py` — recap every owned primitive. +- `scripts/overview_page.py` — regenerate single-file agent-overview.html. +- `scripts/upgrade_suggester.py` — next 1–2 upgrades with mechanisms. diff --git a/agent-launcher/skills/wrap-up/scripts/overview_page.py b/agent-launcher/skills/wrap-up/scripts/overview_page.py new file mode 100644 index 00000000..fc957cd2 --- /dev/null +++ b/agent-launcher/skills/wrap-up/scripts/overview_page.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""overview_page.py — regenerate a single-file ./my-agent/agent-overview.html. + +Fills assets/agent-overview.template.html with the build sheet's primitives, the +latest verdict, and the next moves. Self-contained (inline CSS, no external +assets, theme-aware). Stdlib-only; no network calls. + +Examples: + overview_page.py --sheet ./my-agent/build-sheet.json --out-dir ./my-agent \ + --status live --loop-shape cron-loop --last-verdict satisfied + overview_page.py --sample +""" +import argparse +import datetime as dt +import html +import json +import sys +from pathlib import Path + + +def _now(): + return dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat() + + +def _esc(x): + return html.escape(str(x), quote=True) + + +def render(sheet, status, loop_shape, last_verdict, console_link): + prim = sheet.get("primitives", {}) + agent = prim.get("agent", {}) + env = prim.get("environment", {}) + session = prim.get("session", {}) + + rows = [] + + def row(k, v): + rows.append(f"{_esc(k)}{_esc(v)}") + + row("agent model", agent.get("model", "?")) + row("tools", ", ".join(t.get("type", t.get("name", "?")) for t in agent.get("tools", [])) or "—") + if agent.get("skills"): + row("skills", ", ".join(agent["skills"])) + row("environment", f"{env.get('type', 'cloud')} / {env.get('networking', 'unrestricted')}") + if session.get("resources"): + row("resources", ", ".join(r.get("type", "?") for r in session["resources"])) + if session.get("memory_stores"): + row("memory stores", str(len(session["memory_stores"]))) + row("outcome", "self-grading rubric" if prim.get("outcome") else "none") + + schedule = "none (on-demand)" + if prim.get("deployment"): + s = prim["deployment"].get("schedule", {}) + schedule = f"{s.get('expression', '?')} [{s.get('timezone', 'UTC')}]" + iterations = str(prim.get("outcome", {}).get("max_iterations", "-")) if prim.get("outcome") else "-" + + moves = [] + for d in (sheet.get("deferrals", []) or [])[:2]: + moves.append(f"
  • {_esc(d.get('version', 'v?'))}: {_esc(d.get('item', ''))} — {_esc(d.get('mechanism', ''))}
  • ") + if not moves: + moves.append("
  • Monitor the first scheduled runs; tighten the rubric if verdicts drift.
  • ") + + tmpl = (Path(__file__).resolve().parents[3] / "assets" / "agent-overview.template.html").read_text() + out = tmpl + subs = { + "{{agent_name}}": _esc(sheet.get("agent_name", "agent")), + "{{status}}": _esc(status), + "{{loop_shape}}": _esc(loop_shape or sheet.get("loop_hint", "single-pass")), + "{{goal}}": _esc(sheet.get("goal", "")), + "{{primitives_rows}}": "\n ".join(rows), + "{{last_verdict}}": _esc(last_verdict or "(not graded yet)"), + "{{iterations}}": _esc(iterations), + "{{schedule}}": _esc(schedule), + "{{next_moves}}": "".join(moves), + "{{updated_at}}": _esc(_now()), + "{{console_link}}": _esc(console_link or "https://platform.claude.com/"), + } + for k, v in subs.items(): + out = out.replace(k, v) + return out + + +def main() -> int: + ap = argparse.ArgumentParser(description="Regenerate a single-file agent-overview.html.") + ap.add_argument("--sheet", help="build-sheet.json.") + ap.add_argument("--out-dir", default="./my-agent") + ap.add_argument("--status", default="live") + ap.add_argument("--loop-shape", default=None) + ap.add_argument("--last-verdict", default=None) + ap.add_argument("--console-link", default=None) + ap.add_argument("--stdout", action="store_true") + ap.add_argument("--sample", action="store_true") + args = ap.parse_args() + + if args.sample: + sheet = json.loads((Path(__file__).resolve().parents[3] / "assets" / "example-build-sheet.json").read_text()) + html_out = render(sheet, "live", "cron-loop", "satisfied", None) + print(html_out[:800] + "\n... [truncated in --sample] ...") + return 0 + + if not args.sheet: + print("Provide --sheet path.", file=sys.stderr) + return 2 + sheet = json.loads(Path(args.sheet).read_text()) + html_out = render(sheet, args.status, args.loop_shape, args.last_verdict, args.console_link) + if args.stdout: + print(html_out) + return 0 + out_dir = Path(args.out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + dest = out_dir / "agent-overview.html" + dest.write_text(html_out) + print(f"Wrote {dest} ({len(html_out)} bytes, self-contained)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agent-launcher/skills/wrap-up/scripts/primitives_inventory.py b/agent-launcher/skills/wrap-up/scripts/primitives_inventory.py new file mode 100644 index 00000000..acc0ea5f --- /dev/null +++ b/agent-launcher/skills/wrap-up/scripts/primitives_inventory.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""primitives_inventory.py — recap every CMA primitive the founder now owns. + +Reads build-sheet.json (+ optional goal.json) and prints an inventory: agent +(model, tools, skills), environment, session resources, memory stores, vaults, +outcome, deployment schedule. The close-out "here's what you own" table. +Stdlib-only; no network calls. + +Examples: + primitives_inventory.py --sheet ./my-agent/build-sheet.json + primitives_inventory.py --sheet ./my-agent/build-sheet.json --goal ./my-agent/goal.json --json + primitives_inventory.py --sample +""" +import argparse +import json +import sys +from pathlib import Path + + +def inventory(sheet, goal_state): + prim = sheet.get("primitives", {}) + agent = prim.get("agent", {}) + env = prim.get("environment", {}) + session = prim.get("session", {}) + inv = { + "agent_name": sheet.get("agent_name", "agent"), + "goal": sheet.get("goal", ""), + "agent": { + "model": agent.get("model"), + "tools": [t.get("type", t.get("name", "?")) for t in agent.get("tools", [])], + "mcp_servers": [s.get("name", s.get("url", "?")) for s in agent.get("mcp_servers", [])], + "skills": agent.get("skills", []), + "multiagent": bool(agent.get("multiagent")), + }, + "environment": {"type": env.get("type"), "networking": env.get("networking")}, + "session": { + "resources": [r.get("type", "?") for r in session.get("resources", [])], + "memory_stores": len(session.get("memory_stores", [])), + "vaults": len(session.get("vault_ids", [])), + }, + "outcome": bool(prim.get("outcome")), + "deployment": prim.get("deployment", {}).get("schedule") if prim.get("deployment") else None, + "deferrals": len(sheet.get("deferrals", [])), + } + if goal_state: + inv["phase"] = goal_state.get("phase") + inv["phases_done"] = goal_state.get("phases_done", []) + return inv + + +def _emit(inv, as_json): + if as_json: + print(json.dumps(inv, indent=2)) + return + print(f"=== {inv['agent_name']} — primitives owned ===") + print(f"goal: {inv['goal']}") + a = inv["agent"] + print(f"agent: model={a['model']} tools={a['tools']} skills={a['skills'] or '[]'}" + + (f" mcp={a['mcp_servers']}" if a['mcp_servers'] else "") + + (" multiagent=yes" if a["multiagent"] else "")) + print(f"environment: {inv['environment']['type']} / {inv['environment']['networking']}") + s = inv["session"] + print(f"session: resources={s['resources'] or '[]'} memory_stores={s['memory_stores']} vaults={s['vaults']}") + print(f"outcome: {'yes (self-grading)' if inv['outcome'] else 'no'}") + print(f"deployment: {inv['deployment'] if inv['deployment'] else 'none (on-demand)'}") + print(f"deferrals: {inv['deferrals']} recorded") + if "phase" in inv: + print(f"phase: {inv['phase']} (done: {', '.join(inv['phases_done']) or 'none'})") + + +def main() -> int: + ap = argparse.ArgumentParser(description="Recap every CMA primitive the founder owns.") + ap.add_argument("--sheet", help="build-sheet.json.") + ap.add_argument("--goal", help="goal.json (optional, for phase context).") + ap.add_argument("--json", action="store_true") + ap.add_argument("--sample", action="store_true") + args = ap.parse_args() + + if args.sample: + sheet = json.loads((Path(__file__).resolve().parents[3] / "assets" / "example-build-sheet.json").read_text()) + _emit(inventory(sheet, {"phase": "wrap-up", "phases_done": ["interview", "stage-launch", "grade-iterate", "run-without-you"]}), False) + return 0 + + if not args.sheet: + print("Provide --sheet path.", file=sys.stderr) + return 2 + sheet = json.loads(Path(args.sheet).read_text()) + goal_state = json.loads(Path(args.goal).read_text()) if args.goal and Path(args.goal).exists() else None + _emit(inventory(sheet, goal_state), args.json) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/agent-launcher/skills/wrap-up/scripts/upgrade_suggester.py b/agent-launcher/skills/wrap-up/scripts/upgrade_suggester.py new file mode 100644 index 00000000..4baadb56 --- /dev/null +++ b/agent-launcher/skills/wrap-up/scripts/upgrade_suggester.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""upgrade_suggester.py — suggest the next 1-2 upgrades for a launched agent. + +Ranks the build sheet's deferrals (v1 before v2, real-integration before +nice-to-have) and adds standing hardening suggestions (tighten networking, pin the +agent version in the deployment, nest an outcome if a schedule lacks one). Prints +the top 1-2 with the exact mechanism. Stdlib-only; no network calls. + +Examples: + upgrade_suggester.py --sheet ./my-agent/build-sheet.json + upgrade_suggester.py --sheet ./my-agent/build-sheet.json --top 2 --json + upgrade_suggester.py --sample +""" +import argparse +import json +import sys +from pathlib import Path + + +def suggest(sheet): + prim = sheet.get("primitives", {}) + env = prim.get("environment", {}) + candidates = [] + + # 1) recorded deferrals, v1 first + for d in sorted(sheet.get("deferrals", []) or [], key=lambda x: x.get("version", "v9")): + weight = 100 if d.get("version") == "v1" else 60 + if any(w in (d.get("item", "") + d.get("mechanism", "")).lower() for w in ("mcp", "real", "integration")): + weight += 10 + candidates.append({"weight": weight, "title": f"{d.get('version','v?')} — {d.get('item','')}", + "why": d.get("reason", ""), "mechanism": d.get("mechanism", "")}) + + # 2) standing hardening moves + if env.get("networking", "unrestricted") == "unrestricted": + candidates.append({"weight": 55, "title": "Harden networking to 'limited'", + "why": "unrestricted egress is broader than needed", + "mechanism": "Set environment networking to 'limited' with an explicit allowed_hosts list."}) + if prim.get("deployment") and not prim.get("outcome"): + candidates.append({"weight": 70, "title": "Nest a self-grading outcome in the schedule", + "why": "each firing should self-grade", + "mechanism": "Add user.define_outcome to the deployment's initial_events (--nest-outcome)."}) + if prim.get("deployment"): + candidates.append({"weight": 50, "title": "Pin the agent version in the deployment", + "why": "avoid a config change silently altering scheduled runs", + "mechanism": "Set agent {type:agent,id,version:N} in the deployment payload once a version passes."}) + if not prim.get("outcome"): + candidates.append({"weight": 65, "title": "Add an outcome rubric (grade→iterate)", + "why": "no self-grading defined yet", + "mechanism": "Run grade-iterate/outcome_builder.py to add a rubric + max_iterations."}) + + candidates.sort(key=lambda c: c["weight"], reverse=True) + return candidates + + +def main() -> int: + ap = argparse.ArgumentParser(description="Suggest the next 1-2 upgrades for a launched agent.") + ap.add_argument("--sheet", help="build-sheet.json.") + ap.add_argument("--top", type=int, default=2) + ap.add_argument("--json", action="store_true") + ap.add_argument("--sample", action="store_true") + args = ap.parse_args() + + if args.sample: + sheet = json.loads((Path(__file__).resolve().parents[3] / "assets" / "example-build-sheet.json").read_text()) + cands = suggest(sheet)[:2] + for i, c in enumerate(cands, 1): + print(f"{i}. {c['title']}\n why: {c['why']}\n how: {c['mechanism']}") + return 0 + + if not args.sheet: + print("Provide --sheet path.", file=sys.stderr) + return 2 + sheet = json.loads(Path(args.sheet).read_text()) + cands = suggest(sheet)[: max(1, args.top)] + if args.json: + print(json.dumps(cands, indent=2)) + else: + for i, c in enumerate(cands, 1): + print(f"{i}. {c['title']}\n why: {c['why']}\n how: {c['mechanism']}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())