mirror of
https://github.com/alirezarezvani/claude-skills.git
synced 2026-10-07 02:58:13 +00:00
- Conflict resolution takes dev's counter surfaces and re-applies the agent-launcher marketplace entry (description trimmed to 950 chars for the new <=1024 guard) and README domain row - plugin.json source/attribution moved verbatim to authoring-notes.json per the post-#954 schema dev now enforces; version aligned to 2.11.2 - claude-opus-4-8 (retired, G7-blocking since #938) pinned to claude-opus-5 across 5 scripts + example build sheet; all touched scripts re-smoke-tested - 4 references topped up with external sources (7-8 each) - DELIVERY-REPORT.md removed from the public tree (sprint artifact; content preserved in PR #961 body and git history) — SPEC.md stays as build target - Gates green: derive_counters --check pass, plugin-json 94 OK + marketplace guard OK, frontmatter 0 errors, model freshness 0 findings, smoke 0 failed, hooks exit 0 with and without AGENT_LAUNCHER_SESSION Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Bzm6Pafyxja6g4jUDPcei
201 lines
8.2 KiB
Python
201 lines
8.2 KiB
Python
#!/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-5
|
|
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-5")
|
|
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-5", "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-5")
|
|
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())
|