feat(agent-launcher): new domain plugin for building Claude Managed Agents

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: 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); all pass --help/--sample
- 4 agents (orchestrator + interviewer + grader + deployer), 8 /cs:* commands
- opt-in SessionStart/SessionEnd hooks (exit 0 on any error), 5 shared
  references, 4 assets (build-sheet schema + overview/next-directions templates
  + example)
- validators enforce CMA limits (<=20 skills/session, <=8 memory stores,
  depth-1 multiagent, max_iterations <=20, <=1000 deployments/org)
- registered in marketplace.json; headline counters trued up via
  derive_counters.py --check (skills 362->368, domains 18->19, tools 644->664,
  refs 741->746, agents 102->106, commands 116->124, plugins 88->89)

Distinct from engineering/agent-harness (generic bounded loop over any domain)
and engineering/write-a-skill (authors Claude Code skills, not CMAs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012FwXG6TqCXKZQvF4iD69cv
This commit is contained in:
Claude 2026-08-17 02:39:34 +00:00
parent aa8d778811
commit d1f2396c6f
No known key found for this signature in database
55 changed files with 4356 additions and 11 deletions

View file

@ -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"
}
]
}

File diff suppressed because one or more lines are too long

View file

@ -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: <https://docs.mistral.ai/mistral-vibe/agents-skills>.
[![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.

View file

@ -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."
}
}

61
agent-launcher/CLAUDE.md Normal file
View file

@ -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.

83
agent-launcher/README.md Normal file
View file

@ -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 12).
- **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).

121
agent-launcher/SPEC.md Normal file
View file

@ -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 12) — 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 12 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 12 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
`<agent_launcher_goal>` 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.

View file

@ -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:0003: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.

View file

@ -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 35 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 35 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.

View file

@ -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.

View file

@ -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.

View file

@ -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 12 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}_

View file

@ -0,0 +1,40 @@
<!-- agent-overview template. Rendered by overview_page.py into a single, self-contained ./my-agent/agent-overview.html. Placeholders {{like_this}} are string-substituted; no external assets. -->
<main class="al-overview">
<style>
.al-overview{font:16px/1.6 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;max-width:760px;margin:2rem auto;padding:0 1.25rem;color:#1a1a1a}
.al-overview h1{font-size:1.6rem;margin:.2rem 0}
.al-overview .goal{background:#f5f7fb;border-left:4px solid #4b6bfb;padding:.9rem 1.1rem;border-radius:6px;margin:1rem 0}
.al-overview table{width:100%;border-collapse:collapse;margin:1rem 0;font-size:.95rem}
.al-overview th,.al-overview td{text-align:left;padding:.5rem .6rem;border-bottom:1px solid #e6e8ee}
.al-overview th{color:#555;font-weight:600}
.al-overview .badge{display:inline-block;padding:.15rem .55rem;border-radius:999px;font-size:.8rem;background:#e8f0e8;color:#1c6b2e}
.al-overview .badge.loop{background:#eef0ff;color:#3448c5}
.al-overview footer{color:#888;font-size:.85rem;margin-top:2rem}
@media(prefers-color-scheme:dark){.al-overview{color:#e8e8e8}.al-overview .goal{background:#1b2030;border-left-color:#7d93ff}.al-overview th,.al-overview td{border-color:#2a2f3d}.al-overview th{color:#aaa}}
</style>
<h1>{{agent_name}}</h1>
<p><span class="badge">{{status}}</span> <span class="badge loop">{{loop_shape}}</span></p>
<div class="goal">{{goal}}</div>
<h2>Primitives owned</h2>
<table>
<thead><tr><th>Primitive</th><th>Value</th></tr></thead>
<tbody>
{{primitives_rows}}
</tbody>
</table>
<h2>Latest run</h2>
<table>
<tbody>
<tr><th>Verdict</th><td>{{last_verdict}}</td></tr>
<tr><th>Iterations</th><td>{{iterations}}</td></tr>
<tr><th>Schedule</th><td>{{schedule}}</td></tr>
</tbody>
</table>
<h2>Next 12 upgrades</h2>
<ol>{{next_moves}}</ol>
<footer>Generated by agent-launcher · {{updated_at}} · Console: {{console_link}}</footer>
</main>

View file

@ -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"}}
}
}
}
}

View file

@ -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"}
]
}
}

View file

@ -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 \"<goal>\" | status | advance | phase <name>"
---
# /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>"``goal_state.py init --goal "<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 <name>``goal_state.py set --phase <name>` (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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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 12 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.

View file

@ -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 <agent_launcher_goal> 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"
}
]
}
]
}
}

View file

@ -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())

View file

@ -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 <agent_launcher_goal> 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("<agent_launcher_goal>")
print(body.rstrip())
print("</agent_launcher_goal>")
except Exception:
# Never break a session.
return 0
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -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).

View file

@ -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).

View file

@ -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".

View file

@ -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 12)
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.

View file

@ -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 `<agent_launcher_goal>` 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.

View file

@ -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:* 35 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.

View file

@ -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())

View file

@ -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())

View file

@ -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())

View file

@ -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 35 rubric lines?" *Recommend:* grounded, checkable criteria.
*Cite:* cma-primitives.md (rubric required).
2. "How many iterations before you'd rather look yourself?" *Recommend:* 35.
*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).

View file

@ -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())

View file

@ -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())

View file

@ -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())

View file

@ -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:* 35 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).

View file

@ -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())

View file

@ -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())

View file

@ -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())

View file

@ -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:0003: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:0003: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.

View file

@ -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())

View file

@ -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())

View file

@ -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())

View file

@ -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.

View file

@ -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())

View file

@ -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())

View file

@ -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())

View file

@ -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 12 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 12 upgrades with mechanisms.

View file

@ -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"<tr><td>{_esc(k)}</td><td>{_esc(v)}</td></tr>")
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"<li>{_esc(d.get('version', 'v?'))}: {_esc(d.get('item', ''))}{_esc(d.get('mechanism', ''))}</li>")
if not moves:
moves.append("<li>Monitor the first scheduled runs; tighten the rubric if verdicts drift.</li>")
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())

View file

@ -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())

View file

@ -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())