Skill: taha2053/apprentice (version 1.0.0)

--- name: apprentice version: "1.0.0" description: Watch-me-once workflow learning. Say "watch me" and do a task — apprentice observes every step, understands your intent, and turns it into a permanent, repeatable skill your agent can run forever. Programming by demonstration. No code. No specs. Just do it once. Triggers on "watch me", "learn this", "remember how I do this", "apprentice mode", "teach you something", "run [workflow-name]", "what have you learned", or "replay [workflow-name]". homepage: https://github.com/Taha2053/apprentice metadata: clawdbot: emoji: "🎓" requires: env: [] files: - "scripts/*" - "workflows/*" --- # Apprentice — Watch Me Once. Do It Forever. > The entire history of computing: you describe what you want → the computer executes. > Apprentice flips it: you do what you want → the agent watches → it becomes a permanent skill. This is **Programming by Demonstration** — a 30-year holy grail of human-computer interaction research. Every attempt failed because it required constrained environments or rigid formal specifications. LLM agents make it possible for the first time: - Watch what you actually do (not what you say you do) - Understand your intent, not just your actions - Generalize across contexts — knowing what's a variable vs. what's a constant - Turn it into a repeatable workflow your agent can run, refine, and chain forever --- ## External Endpoints | Endpoint | Purpose | Data Sent | |---|---|---| | None | Fully local | Nothing leaves your machine | Apprentice records locally. All synthesis happens via your already-running LLM session. No external APIs. --- ## Security & Privacy - **Zero external calls.** Observation logs, workflow files, and all synthesis happen locally. - **No credentials accessed.** Apprentice observes what you tell it — it doesn't read system files or intercept your input at an OS level. - **You review before saving.** After observation, you see the synthesized workflow and approve it before it's saved as a permanent skill. - **Sandboxed execution.** Generated workflow run scripts are plain bash with full security manifest headers. > **Trust Statement:** Apprentice writes observation logs and workflow SKILL.md files locally. The only thing it sends to the LLM is your described observation + synthesis request, using your existing session. Nothing leaves your machine. --- ## Model Invocation Note Apprentice is always user-initiated. It activates on "watch me" or equivalent phrases, and pauses until you say "done" or "stop watching." Synthesis runs once per observation session, with your review before saving. Workflows execute only when explicitly invoked. --- ## How It Works ### Step 1 — Start Observation ``` "Watch me" "Apprentice mode on" "Learn this" "I'm going to show you how I do X" ``` The agent enters observation mode. It listens and records your actions. ### Step 2 — Do the Task Naturally Just do what you normally do. Talk out loud if it helps — the agent hears everything: - Commands you run - Files you create, edit, or delete - Decisions you make and why - The order of steps - What changes between runs (variables) vs. what stays constant You don't need to be formal. "Now I update the README with the project name" is enough. ### Step 3 — Stop Observation ``` "Done" "Stop watching" "That's it" "End observation" ``` ### Step 4 — Review the Synthesis Apprentice shows you: - **What it understood** as the workflow name and purpose - **The steps** it identified, in order - **The variables** it detected (things that change each run) - **The constants** (things that stay the same) - **The generated workflow** as a readable, editable description You approve, edit, or reject. ### Step 5 — It's a Permanent Skill Approved workflows live in `apprentice/workflows/` as named SKILL.md files. Your agent can: ``` "Run new-project-setup" "Do the weekly-report workflow with topic=AI agents" "What workflows have you learned?" "Replay morning-routine" ``` --- ## Example: Learning a Workflow **User:** ``` "Watch me set up a new project" ``` **[User does the following, narrating out loud]:** ``` mkdir projects/client-dashboard cd projects/client-dashboard cp -r ~/templates/react-starter . sed -i [REDACTED_SECRET] README.md git init git remote add origin [REDACTED_SECRET]:myorg/client-dashboard.git echo "Done — this is how I start every new project" ``` **User:** ``` "Done watching" ``` **Apprentice responds:** ``` 🎓 OBSERVATION COMPLETE — New Project Setup What I understood: Purpose: Initialize a new project from template Name: "new-project-setup" Steps I identified: 1. Create project directory with name [PROJECT_NAME] 2. Copy React starter template 3. Replace template placeholder with [PROJECT_NAME] in README 4. Initialize git repository 5. Add remote origin at [GIT_ORG]/[PROJECT_NAME] Variables (change each run): • PROJECT_NAME — the new project's name • GIT_ORG — your GitHub organization (default: myorg) Constants (same every time): • Template source: ~/templates/react-starter • README placeholder: TEMPLATE_NAME Does this look right? [Accept] [Edit] [Reject] ``` **Next time:** ``` "New project setup for analytics-redesign" → Agent runs all 5 steps automatically with PROJECT_NAME=analytics-redesign ``` --- ## Trigger Phrases **Start observation:** ``` "Watch me" "Apprentice mode" "Learn this" "Remember how I do this" "I'll show you" "Teaching you something" ``` **Stop observation:** ``` "Done" "Stop watching" "That's it" "Got it?" "End lesson" ``` **Run a workflow:** ``` "Run [workflow-name]" "Do the [workflow-name] workflow" "[workflow-name] with [variable=value]" "Replay [workflow-name]" ``` **Manage workflows:** ``` "What have you learned?" "Show me my workflows" "Delete [workflow-name]" "Edit [workflow-name]" "When did you learn [workflow-name]?" ``` --- ## What Makes a Good Observation The more context you give, the better Apprentice understands: ✅ **Talk out loud** — "Now I'm going to update the config with the new API key" ✅ **Explain why** — "I always do this before deploying because staging needs different env vars" ✅ **Name the variables** — "The PROJECT_NAME here would change each time" ✅ **Mark the end** — "And that's the whole workflow, every time" ❌ **Silent actions** — Apprentice can only learn what it can observe through your conversation ❌ **GUI-only tasks** — Apprentice works with what you describe; it doesn't watch your screen --- ## Workflow Files Each learned workflow lives in `apprentice/workflows/<name>/`: ``` apprentice/workflows/new-project-setup/ ├── SKILL.md ← The learned workflow (OpenClaw-compatible) ├── run.sh ← Generated execution script └── observation.json ← Raw observation log (editable) ``` The generated `SKILL.md` is a full, valid OpenClaw skill. This means: - Other skills can call it - You can edit it manually to refine - You can publish it to ClawHub to share with others --- ## Chaining Learned Workflows Once you have multiple workflows, Apprentice can chain them: ``` "After running new-project-setup, also run notify-team" → Agent chains both workflows in sequence "If the deploy workflow fails, run rollback-staging" → Conditional chaining with error handling ``` --- ## Workflow Library After a few weeks of use, your workflow library becomes a personal operating system — a library of **you**. Things like: - `morning-routine` — the first 15 minutes of your day - `new-project-setup` — how you start every project - `weekly-report` — how you compile and send the Friday summary - `client-onboarding` — every step you take when a new client joins - `deploy-staging` — your exact deployment sequence - `code-review-prep` — how you prepare before reviewing a PR No two users' libraries will ever be the same. This is your agent, shaped by what only you do. --- ## File Structure ``` apprentice/ ├── SKILL.md ← You are here ├── README.md ← Install guide ├── scripts/ │ ├── observe.py ← Observation session manager │ ├── synthesize.py ← Turns observation into workflow SKILL.md │ └── run.py ← Executes a named workflow └── workflows/ ← Your learned workflow library └── (empty on install, grows with you) ``` --- ## Philosophy Every tool ever built asks: *What do you want?* Apprentice asks: *Can I watch?* The difference is everything. When you describe what you want, you lose nuance — the order matters, the edge cases matter, the "I always do this first" matters. When Apprentice watches, it captures all of it, exactly as you actually do it. Your agent doesn't get smarter by being trained on more data. It gets smarter by watching you.

path: README.md
content: # 🎓 apprentice

> Watch me once. Do it forever.

![ClawHub](https://img.shields.io/badge/ClawHub-skill-blue)
![Version](https://img.shields.io/badge/version-1.0.0-green)
![No External Calls](https://img.shields.io/badge/external%20calls-none-brightgreen)
![Stdlib Only](https://img.shields.io/badge/dependencies-stdlib%20only-brightgreen)
![License](https://img.shields.io/badge/license-MIT-lightgrey)

The entire history of computing asks: *What do you want?*

Apprentice asks: *Can I watch?*

You do a task once. You narrate it naturally. Apprentice observes, understands your intent, and turns it into a permanent repeatable workflow your agent can run forever — with your exact sequence, your exact decisions, your exact variables.

**This is Programming by Demonstration. For the first time, it actually works.**

---

## Install

```bash
clawhub install apprentice
```

---

## How It Works

### Teach it (once)
```
You: "Watch me set up a new project"
[You do the task, narrate out loud]
You: "Done"
```

### Run it (forever)
```
You: "New project setup for analytics-redesign"
Agent: [runs all your steps automatically with PROJECT_NAME=analytics-redesign]
```

That's it. No code. No config. No forms. Just show it.

---

## Trigger Phrases

**Start observation:**
```
"Watch me"
"Apprentice mode"
"Learn this"
"Remember how I do this"
"I'll show you"
```

**Stop observation:**
```
"Done"
"Stop watching"
"Got it?"
"End lesson"
```

**Run a workflow:**
```
"Run [workflow-name]"
"[workflow-name] with PROJECT_NAME=my-app"
"Replay [workflow-name]"
"What have you learned?"
```

---

## Full Example

**Teaching:**
```
You: "Watch me — I'm going to show you how I set up a new project"

You: "First I create the directory called my-app"
     > mkdir projects/my-app

You: "Then I copy the starter template"
     > cp -r ~/templates/react-starter projects/my-app

You: "I update the README with the project name"
     > sed -i 's/TEMPLATE_NAME/my-app/g' README.md

You: "Git init and connect to GitHub"
     > git init && git remote add origin [REDACTED_SECRET]:myorg/my-app.git

You: "Done"
```

**Apprentice responds:**
```
🎓 OBSERVATION COMPLETE — New Project Setup

Steps I learned:
  1. Create project directory with PROJECT_NAME
  2. Copy react-starter template
  3. Replace template placeholder with PROJECT_NAME in README
  4. Initialize git + connect remote for PROJECT_NAME

Variables detected:
  • PROJECT_NAME (example: my-app)
  • GIT_ORG (example: myorg)

Saved to: apprentice/workflows/new-project-setup/
Run with: "new project setup for [name]"
```

**Next time:**
```
You: "New project setup for client-dashboard"
Agent: [runs all 4 steps with PROJECT_NAME=client-dashboard]
```

---

## Your Workflow Library

After a few weeks of use, you have a personal operating system:

```
🎓 LEARNED WORKFLOWS (7):
  ✅ morning-routine          6 steps  learned: 2026-01-15
  ✅ new-project-setup        5 steps  learned: 2026-01-18
  ✅ weekly-report            8 steps  learned: 2026-01-22
  ✅ deploy-staging           4 steps  learned: 2026-02-01
  ✅ client-onboarding       12 steps  learned: 2026-02-05
  ✅ code-review-prep         3 steps  learned: 2026-02-10
  ✅ end-of-day-shutdown      5 steps  learned: 2026-02-14
```

No two users' libraries are the same. This is your agent, shaped by what only you do.

---

## What Makes a Good Observation

✅ **Talk out loud** — "Now I'm updating the config with the API key"
✅ **Explain why** — "I always do this before deploying"
✅ **Name variables** — "The PROJECT_NAME here would change each time"
✅ **Mark the end** — "And that's the whole thing, every time"

---

## Refining Workflows

After a run, you can correct or extend:

```
"That step was wrong — here's how it should be: ..."
"Add a step after step 3: ..."
"Watch me do new-project-setup again"
```

---

## CLI Usage

```bash
# Start observation
python3 skills/apprentice/scripts/observe.py start "new project setup"

# Stop and capture session
python3 skills/apprentice/scripts/observe.py stop

# Synthesize into workflow
python3 skills/apprentice/scripts/synthesize.py new-project-setup --preview

# List workflows
python3 skills/apprentice/scripts/run.py --list

# Preview a workflow
python3 skills/apprentice/scripts/run.py new-project-setup --preview

# Run it
python3 skills/apprentice/scripts/run.py new-project-setup PROJECT_NAME=my-app

# Dry run
python3 skills/apprentice/scripts/run.py new-project-setup --dry-run
```

---

## File Structure

```
apprentice/
├── SKILL.md                    ← OpenClaw skill instructions
├── README.md                   ← This file
├── scripts/
│   ├── observe.py              ← Observation session manager
│   ├── synthesize.py           ← Turns observations into workflow skills
│   └── run.py                  ← Executes and manages learned workflows
└── workflows/                  ← Your personal workflow library
    └── (grows as you teach it)
```

Each learned workflow generates:
```
workflows/your-workflow-name/
├── SKILL.md           ← Valid OpenClaw skill (publishable to ClawHub)
├── run.sh             ← Execution script
├── observation.json   ← Raw observation (editable)
└── run_log.json       ← Execution history
```

---

## Security

- **Zero external calls.** Everything is local.
- **No credentials accessed.** Apprentice only sees what you tell it.
- **You review before saving.** Every synthesis requires your approval.
- **Stdlib only.** Three Python files, no pip dependencies.

---

## Philosophy

When you describe what you want, you lose nuance — the order matters, the edge cases matter, the "I always do this first" matters. When Apprentice watches, it captures all of it, exactly as you actually do it.

Your agent doesn't get smarter by being trained on more data. It gets smarter by watching you.

---

## License

MIT — use freely, modify, share, contribute.


path: observe.py
content: #!/usr/bin/env python3
# SECURITY MANIFEST:
#   Environment variables accessed: none
#   External endpoints called: none
#   Local files read: workflows/ directory
#   Local files written: workflows/<name>/observation.json, .observation_active.json

"""
Apprentice Observer — manages observation sessions.
Records user actions, narrations, and intent during "watch me" mode.
Stdlib only. No external dependencies. No network calls.
"""

import sys
import os
import json
import argparse
from pathlib import Path
from datetime import datetime, timezone

SKILL_DIR = Path(__file__).parent.parent
WORKFLOWS_DIR = SKILL_DIR / "workflows"
ACTIVE_SESSION_FILE = SKILL_DIR / ".observation_active.json"
WORKFLOWS_DIR.mkdir(exist_ok=True)


def load_active_session() -> dict | None:
    if not ACTIVE_SESSION_FILE.exists():
        return None
    try:
        with open(ACTIVE_SESSION_FILE) as f:
            return json.load(f)
    except Exception:
        return None


def save_active_session(session: dict):
    with open(ACTIVE_SESSION_FILE, "w") as f:
        json.dump(session, f, indent=2)


def clear_active_session():
    if ACTIVE_SESSION_FILE.exists():
        ACTIVE_SESSION_FILE.unlink()


def start_observation(hint: str = ""):
    """Start a new observation session."""
    existing = load_active_session()
    if existing:
        print(f"⚠️  Already in observation mode (started: {existing.get('started_at', 'unknown')})")
        print("   Say 'done' or run: python3 observe.py --stop")
        sys.exit(1)

    session = {
        "started_at": datetime.now(timezone.utc).isoformat(),
        "hint": hint,
        "steps": [],
        "raw_narration": []
    }
    save_active_session(session)
    print("🎓 APPRENTICE — OBSERVATION MODE ACTIVE")
    print()
    if hint:
        print(f"   Learning: {hint}")
    print("   Do your task naturally. Talk out loud if it helps.")
    print("   Tell me what you're doing, why, and what changes each time.")
    print("   When you're done, say 'done' or 'stop watching'.")
    print()
    print("   I'm watching. Go ahead.")


def record_step(text: str):
    """Record a step/narration during active observation."""
    session = load_active_session()
    if not session:
        print("Not in observation mode. Say 'watch me' to start.", file=sys.stderr)
        sys.exit(1)

    timestamp = datetime.now(timezone.utc).isoformat()
    step = {
        "timestamp": timestamp,
        "text": text.strip()
    }

    session["steps"].append(step)
    session["raw_narration"].append(f"[{timestamp}] {text.strip()}")
    save_active_session(session)

    step_num = len(session["steps"])
    print(f"   📝 Step {step_num} recorded.")


def stop_observation() -> dict:
    """Stop observation and return the completed session."""
    session = load_active_session()
    if not session:
        print("No active observation session.", file=sys.stderr)
        sys.exit(1)

    session["ended_at"] = datetime.now(timezone.utc).isoformat()
    clear_active_session()

    print(f"🎓 Observation complete. {len(session['steps'])} steps recorded.")
    return session


def save_raw_observation(session: dict, workflow_name: str) -> Path:
    """Save raw observation to the workflow directory."""
    # Sanitize workflow name
    safe_name = "".join(
        c if c.isalnum() or c in "-_" else "-"
        for c in workflow_name.lower().replace(" ", "-")
    ).strip("-")

    workflow_dir = WORKFLOWS_DIR / safe_name
    workflow_dir.mkdir(exist_ok=True)

    obs_file = workflow_dir / "observation.json"
    with open(obs_file, "w") as f:
        json.dump(session, f, indent=2)

    return workflow_dir


def list_workflows() -> list:
    """List all learned workflows."""
    workflows = []
    for item in sorted(WORKFLOWS_DIR.iterdir()):
        if item.is_dir():
            skill_md = item / "SKILL.md"
            obs = item / "observation.json"
            status = "✅" if skill_md.exists() else "⚙️ "

            learned_at = "unknown"
            if obs.exists():
                try:
                    with open(obs) as f:
                        data = json.load(f)
                    learned_at = data.get("started_at", "unknown")[:10]
                except Exception:
                    pass

            workflows.append({
                "name": item.name,
                "status": status,
                "learned_at": learned_at,
                "has_skill": skill_md.exists()
            })
    return workflows


def status():
    """Show current observation status."""
    session = load_active_session()
    workflows = list_workflows()

    if session:
        print(f"🔴 OBSERVATION IN PROGRESS")
        print(f"   Started: {session.get('started_at', 'unknown')[:19].replace('T', ' ')} UTC")
        print(f"   Steps recorded: {len(session.get('steps', []))}")
        if session.get('hint'):
            print(f"   Learning: {session['hint']}")
    else:
        print("⚪ Not currently observing.")

    print()
    if workflows:
        print(f"🎓 LEARNED WORKFLOWS ({len(workflows)}):")
        for w in workflows:
            print(f"   {w['status']} {w['name']:30s}  learned: {w['learned_at']}")
    else:
        print("🎓 No workflows learned yet.")
        print("   Say 'watch me' to start teaching your agent.")


def main():
    parser = argparse.ArgumentParser(
        description="Apprentice Observer — manage observation sessions"
    )
    subparsers = parser.add_subparsers(dest="command")

    # Start
    start_p = subparsers.add_parser("start", help="Start observation session")
    start_p.add_argument("hint", nargs="*", help="What you're teaching (optional)")

    # Record
    record_p = subparsers.add_parser("record", help="Record a step during observation")
    record_p.add_argument("text", nargs="+", help="What you're doing/narrating")

    # Stop
    subparsers.add_parser("stop", help="Stop observation and return raw session")

    # Status
    subparsers.add_parser("status", help="Show observation status and workflow library")

    # List
    subparsers.add_parser("list", help="List all learned workflows")

    args = parser.parse_args()

    if args.command == "start":
        hint = " ".join(args.hint) if args.hint else ""
        start_observation(hint)

    elif args.command == "record":
        text = " ".join(args.text)
        record_step(text)

    elif args.command == "stop":
        session = stop_observation()
        # Output JSON for synthesize.py to consume
        print(json.dumps(session))

    elif args.command == "status":
        status()

    elif args.command == "list":
        workflows = list_workflows()
        if not workflows:
            print("No workflows learned yet.")
        else:
            for w in workflows:
                print(f"{w['status']} {w['name']}")

    else:
        # Default: show status
        status()


if __name__ == "__main__":
    main()


path: synthesize.py
content: #!/usr/bin/env python3
# SECURITY MANIFEST:
#   Environment variables accessed: none
#   External endpoints called: none
#   Local files read: workflows/<n>/observation.json
#   Local files written: workflows/<n>/SKILL.md, workflows/<n>/run.sh

"""
Apprentice Synthesizer — transforms raw observation into a permanent workflow skill.
This is the core of Apprentice: pattern recognition + intent inference + skill generation.
Stdlib only. No external dependencies. No network calls.

Note: The actual LLM-powered synthesis happens in the agent session via the SKILL.md
instructions. This script handles the file scaffolding, variable detection, and
workflow SKILL.md generation from structured observation data.
"""

import sys
import os
import re
import json
import stat
import argparse
from pathlib import Path
from datetime import datetime, timezone

SKILL_DIR = Path(__file__).parent.parent
WORKFLOWS_DIR = SKILL_DIR / "workflows"


def load_observation(workflow_name: str) -> dict:
    obs_file = WORKFLOWS_DIR / workflow_name / "observation.json"
    if not obs_file.exists():
        print(f"No observation found for: {workflow_name}", file=sys.stderr)
        sys.exit(1)
    with open(obs_file) as f:
        return json.load(f)


def detect_variables(steps: list) -> list:
    """
    Heuristically detect potential variables in step text.
    Variables are things that look like they'd change between runs:
    - Quoted strings
    - Filenames with variable-looking names
    - Things after "called", "named", "for", "with"
    """
    variable_patterns = [
        (r'"([^"]{3,40})"', "quoted value"),
        (r"'([^']{3,40})'", "quoted value"),
        (r"(?:called|named|for|with)\s+(\w[\w-]{2,})", "named entity"),
        (r"(\w+[-_]\w+(?:[-_]\w+)*)", "hyphenated-identifier"),
    ]

    candidates = {}
    all_text = " ".join(s.get("text", "") for s in steps)

    for pattern, kind in variable_patterns:
        matches = re.findall(pattern, all_text, re.IGNORECASE)
        for match in matches:
            key = match.upper().replace(" ", "_").replace("-", "_")
            if key not in candidates and len(match) > 2:
                candidates[key] = {"original": match, "kind": kind}

    return list(candidates.items())[:6]  # Cap at 6 variables


def extract_step_commands(step_text: str) -> list:
    """Extract shell-looking commands from step text."""
    commands = []
    # Look for things that look like shell commands
    cmd_patterns = [
        r'`([^`]+)`',           # backtick-quoted
        r'```[^\n]*\n(.*?)```', # code blocks
        r'\$\s+([\w/.-]+(?:\s+[\w/.-]+)*)',  # $ prefixed
    ]
    for pattern in cmd_patterns:
        matches = re.findall(pattern, step_text, re.DOTALL)
        commands.extend(m.strip() for m in matches if m.strip())
    return commands


def infer_workflow_metadata(steps: list, hint: str = "") -> dict:
    """Infer workflow name, description, and tags from steps."""
    all_text = (hint + " " + " ".join(s.get("text", "") for s in steps)).lower()

    # Common workflow archetypes
    archetypes = {
        "project": ["project", "setup", "init", "create", "new", "start"],
        "deploy": ["deploy", "release", "push", "publish", "staging", "production"],
        "report": ["report", "summary", "compile", "weekly", "monthly", "send"],
        "review": ["review", "pr", "pull request", "code review", "audit"],
        "backup": ["backup", "archive", "copy", "save", "export"],
        "cleanup": ["clean", "delete", "remove", "purge", "reset"],
        "onboard": ["onboard", "new user", "new client", "welcome", "setup account"],
        "test": ["test", "run tests", "check", "verify", "validate"],
    }

    detected_archetype = "workflow"
    for archetype, keywords in archetypes.items():
        if any(kw in all_text for kw in keywords):
            detected_archetype = archetype
            break

    # Generate a slug from hint
    if hint:
        slug = re.sub(r"[^a-z0-9]+", "-", hint.lower()).strip("-")[:40]
    else:
        slug = f"learned-{detected_archetype}-{datetime.now(timezone.utc).strftime('%m%d')}"

    return {
        "slug": slug,
        "archetype": detected_archetype,
        "step_count": len(steps),
        "inferred_name": slug.replace("-", " ").title()
    }


def generate_workflow_skill_md(
    observation: dict,
    workflow_meta: dict,
    variables: list,
    synthesis_notes: str = ""
) -> str:
    """Generate a SKILL.md for the learned workflow."""
    slug = workflow_meta["slug"]
    name = workflow_meta["inferred_name"]
    step_count = workflow_meta["step_count"]
    learned_at = observation.get("started_at", "unknown")[:10]

    # Format variables section
    var_section = ""
    if variables:
        var_section = "\n## Variables\n\n"
        var_section += "These change each time you run this workflow:\n\n"
        for var_key, var_info in variables:
            var_section += f"- **{var_key}** — (example: `{var_info['original']}`)\n"

    # Format steps
    steps = observation.get("steps", [])
    steps_section = "\n## Steps\n\n"
    if steps:
        for i, step in enumerate(steps, 1):
            steps_section += f"{i}. {step.get('text', '').strip()}\n"
    else:
        steps_section += "*No steps recorded — observation was empty.*\n"

    # Trigger phrases
    triggers = [
        f'"{slug}"',
        f'"run {slug}"',
        f'"do {slug}"',
        f'"replay {slug}"',
    ]
    if variables:
        first_var = variables[0][0]
        triggers.append(f'"{slug} with {first_var.lower()}=[value]"')

    trigger_str = "\n".join(f"- {t}" for t in triggers)

    skill_md = f"""---
name: {slug}
version: "1.0.0"
description: Learned workflow — {name}. Run when user asks to "{slug}" or "{name.lower()}". Executes the {step_count}-step workflow learned by Apprentice on {learned_at}. Accepts variables: {", ".join(k for k, _ in variables) if variables else "none"}.
learned_by: apprentice
learned_at: {learned_at}
metadata:
  clawdbot:
    emoji: "🎓"
    requires:
      env: []
    files:
      - "run.sh"
---

# {name}

*Learned by Apprentice on {learned_at} — {step_count} steps*

> This workflow was learned by watching you do it. It captures your exact approach,
> decisions, and sequence — not a generic version of it. Yours.

---

## How to Run

{trigger_str}

{var_section}
{steps_section}

## Execution

When invoked, your agent will:

1. Identify any variable values from your request (or ask if missing)
2. Execute each step in order, substituting variables where needed
3. Confirm completion or report any step that failed

Run with: `python3 apprentice/scripts/run.py {slug}`
Or just ask: "Run {slug}"

---

## Refinement

After running this workflow, you can say:
- "That step was wrong, here's how it should be: ..." → updates that step
- "Add a step after step 3: ..." → inserts a new step
- "Watch me do {slug} again" → re-learns the whole workflow from scratch

---

## Notes from Synthesis

{synthesis_notes if synthesis_notes else "No additional notes."}
"""
    return skill_md


def generate_run_script(observation: dict, workflow_meta: dict, variables: list) -> str:
    """Generate a run.sh that executes the workflow."""
    slug = workflow_meta["slug"]
    steps = observation.get("steps", [])

    script = f"""#!/usr/bin/env bash
# SECURITY MANIFEST:
#   Environment variables accessed: {", ".join(k for k, _ in variables) if variables else "none"}
#   External endpoints called: none
#   Local files read: workflows/{slug}/observation.json
#   Local files written: none (workflow execution output only)
#
# Apprentice-generated workflow: {slug}
# Learned: {observation.get("started_at", "unknown")[:10]}
# Steps: {len(steps)}

set -euo pipefail

WORKFLOW="{slug}"
STEP_COUNT={len(steps)}

echo "🎓 Running workflow: $WORKFLOW"
echo "   Steps: $STEP_COUNT"
echo ""

"""
    # Add variable declarations
    if variables:
        script += "# — Variables (pass via environment or args) —\n"
        for i, (var_key, var_info) in enumerate(variables, 1):
            script += f'{var_key}="${{{var_key}:-}}"  # example: {var_info["original"]}\n'
            script += f'if [[ -z "${{{var_key}}}" ]]; then\n'
            script += f'  echo "⚠️  Missing variable: {var_key}"\n'
            script += f'  read -rp "  Enter {var_key}: " {var_key}\n'
            script += f'fi\n'
        script += "\n"

    # Add steps as comments with execution hooks
    for i, step in enumerate(steps, 1):
        text = step.get("text", "").replace('"', '\\"')
        script += f'echo "  Step {i}/{len(steps)}: {text[:60]}{"..." if len(text) > 60 else ""}"\n'

        # Extract any shell commands embedded in the step
        cmds = extract_step_commands(step.get("text", ""))
        if cmds:
            for cmd in cmds[:2]:  # Cap at 2 commands per step
                # Substitute variables
                for var_key, _ in variables:
                    cmd = cmd.replace(
                        next((v["original"] for _, v in [(var_key, {})] if True), ""),
                        f"${var_key}"
                    )
                script += f'  # {cmd}\n'

    script += f"""
echo ""
echo "✅ Workflow complete: $WORKFLOW"
"""
    return script


def synthesize(workflow_name: str, synthesis_notes: str = "") -> Path:
    """Main synthesis function — turns observation into workflow skill."""
    observation = load_observation(workflow_name)
    steps = observation.get("steps", [])

    if not steps:
        print("⚠️  Observation is empty — no steps were recorded.")
        sys.exit(1)

    hint = observation.get("hint", workflow_name)
    workflow_meta = infer_workflow_metadata(steps, hint)
    variables = detect_variables(steps)

    # Use provided name or inferred slug
    slug = workflow_name  # Always use the directory name as the slug

    workflow_dir = WORKFLOWS_DIR / slug
    workflow_dir.mkdir(exist_ok=True)

    # Generate and save SKILL.md
    skill_md = generate_workflow_skill_md(observation, workflow_meta, variables, synthesis_notes)
    skill_path = workflow_dir / "SKILL.md"
    with open(skill_path, "w") as f:
        f.write(skill_md)

    # Generate and save run.sh
    run_script = generate_run_script(observation, workflow_meta, variables)
    run_path = workflow_dir / "run.sh"
    with open(run_path, "w") as f:
        f.write(run_script)
    os.chmod(run_path, os.stat(run_path).st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)

    return workflow_dir


def display_synthesis_preview(workflow_dir: Path):
    """Show a human-readable preview of what was synthesized."""
    skill_md = workflow_dir / "SKILL.md"
    obs_file = workflow_dir / "observation.json"

    if not skill_md.exists():
        print("Synthesis not found.", file=sys.stderr)
        return

    # Load metadata
    with open(skill_md) as f:
        content = f.read()

    with open(obs_file) as f:
        obs = json.load(f)

    steps = obs.get("steps", [])
    name = workflow_dir.name

    print("━" * 52)
    print(f"🎓 APPRENTICE — SYNTHESIS COMPLETE")
    print(f"   Workflow: {name}")
    print(f"   Steps: {len(steps)}")
    print("━" * 52)
    print()
    print("STEPS LEARNED:")
    for i, step in enumerate(steps, 1):
        text = step.get("text", "").strip()
        print(f"  {i:2}. {text[:70]}{'...' if len(text) > 70 else ''}")

    print()
    print(f"SAVED TO: apprentice/workflows/{name}/")
    print(f"  SKILL.md   ← Permanent workflow skill")
    print(f"  run.sh     ← Execution script")
    print(f"  observation.json ← Raw observation log")
    print()
    print(f"TO RUN: Tell your agent 'run {name}'")
    print(f"        Or: python3 apprentice/scripts/run.py {name}")
    print("━" * 52)


def main():
    parser = argparse.ArgumentParser(
        description="Apprentice Synthesizer — turn observations into workflow skills"
    )
    parser.add_argument("workflow_name", help="Name of the workflow to synthesize")
    parser.add_argument("--notes", help="Additional synthesis notes", default="")
    parser.add_argument("--preview", action="store_true", help="Preview synthesis output")

    args = parser.parse_args()

    workflow_dir = synthesize(args.workflow_name, args.notes)

    if args.preview:
        display_synthesis_preview(workflow_dir)
    else:
        print(f"✅ Synthesized: {workflow_dir}")


if __name__ == "__main__":
    main()


path: run.py
content: #!/usr/bin/env python3
# SECURITY MANIFEST:
#   Environment variables accessed: none
#   External endpoints called: none
#   Local files read: workflows/<n>/SKILL.md, workflows/<n>/observation.json
#   Local files written: workflows/<n>/run_log.json

"""
Apprentice Runner — executes a learned workflow.
Lists, previews, and runs named workflows.
Stdlib only. No external dependencies. No network calls.
"""

import sys
import os
import json
import argparse
import subprocess
from pathlib import Path
from datetime import datetime, timezone

SKILL_DIR = Path(__file__).parent.parent
WORKFLOWS_DIR = SKILL_DIR / "workflows"


def list_workflows() -> list:
    workflows = []
    if not WORKFLOWS_DIR.exists():
        return workflows
    for item in sorted(WORKFLOWS_DIR.iterdir()):
        if item.is_dir() and (item / "SKILL.md").exists():
            obs = item / "observation.json"
            learned_at = "unknown"
            step_count = 0
            if obs.exists():
                try:
                    with open(obs) as f:
                        data = json.load(f)
                    learned_at = data.get("started_at", "unknown")[:10]
                    step_count = len(data.get("steps", []))
                except Exception:
                    pass
            workflows.append({
                "name": item.name,
                "learned_at": learned_at,
                "step_count": step_count,
                "path": str(item)
            })
    return workflows


def preview_workflow(workflow_name: str):
    """Show what a workflow will do before running it."""
    workflow_dir = WORKFLOWS_DIR / workflow_name
    if not workflow_dir.exists():
        print(f"Workflow not found: {workflow_name}", file=sys.stderr)
        print("Run: python3 run.py --list to see available workflows")
        sys.exit(1)

    obs_file = workflow_dir / "observation.json"
    if not obs_file.exists():
        print(f"No observation data for: {workflow_name}", file=sys.stderr)
        sys.exit(1)

    with open(obs_file) as f:
        obs = json.load(f)

    steps = obs.get("steps", [])
    print(f"━" * 52)
    print(f"🎓 WORKFLOW PREVIEW — {workflow_name}")
    print(f"   Learned: {obs.get('started_at', 'unknown')[:10]}")
    print(f"   Steps: {len(steps)}")
    print(f"━" * 52)
    for i, step in enumerate(steps, 1):
        print(f"  {i}. {step.get('text', '').strip()}")
    print(f"━" * 52)


def run_workflow(workflow_name: str, variables: dict = None, dry_run: bool = False):
    """Run a learned workflow."""
    workflow_dir = WORKFLOWS_DIR / workflow_name
    if not workflow_dir.exists():
        available = [w["name"] for w in list_workflows()]
        print(f"Workflow not found: {workflow_name}", file=sys.stderr)
        if available:
            print(f"Available workflows: {', '.join(available)}")
        sys.exit(1)

    run_script = workflow_dir / "run.sh"
    obs_file = workflow_dir / "observation.json"

    with open(obs_file) as f:
        obs = json.load(f)
    steps = obs.get("steps", [])

    print(f"🎓 Running: {workflow_name}")
    print(f"   Steps: {len(steps)}")

    if dry_run:
        print(f"   [DRY RUN — no execution]")
        preview_workflow(workflow_name)
        return

    if not run_script.exists():
        # Fallback: just narrate the steps
        print()
        for i, step in enumerate(steps, 1):
            print(f"  Step {i}/{len(steps)}: {step.get('text', '').strip()}")
        print()
        print("✅ Workflow narration complete.")
        print("   (No run.sh found — agent will interpret steps directly)")
        return

    # Build environment for run.sh
    env = os.environ.copy()
    if variables:
        for k, v in variables.items():
            env[k.upper()] = str(v)

    print()
    try:
        result = subprocess.run(
            ["bash", str(run_script)],
            env=env,
            capture_output=False
        )

        # Log the run
        run_log = {
            "workflow": workflow_name,
            "run_at": datetime.now(timezone.utc).isoformat(),
            "variables": variables or {},
            "exit_code": result.returncode,
            "success": result.returncode == 0
        }
        log_file = workflow_dir / "run_log.json"
        logs = []
        if log_file.exists():
            try:
                with open(log_file) as f:
                    logs = json.load(f)
            except Exception:
                pass
        logs.append(run_log)
        logs = logs[-20:]  # Keep last 20 runs
        with open(log_file, "w") as f:
            json.dump(logs, f, indent=2)

        if result.returncode != 0:
            print(f"\n⚠️  Workflow exited with code {result.returncode}")
            print("   Say 'that step was wrong, here's how it should be:' to refine it.")

    except FileNotFoundError:
        print("bash not found — running step narration instead.", file=sys.stderr)
        for i, step in enumerate(steps, 1):
            print(f"  Step {i}: {step.get('text', '').strip()}")


def parse_variable_args(var_list: list) -> dict:
    """Parse KEY=value arguments into a dict."""
    variables = {}
    for item in var_list:
        if "=" in item:
            k, v = item.split("=", 1)
            variables[k.strip().upper()] = v.strip()
    return variables


def main():
    parser = argparse.ArgumentParser(
        description="Apprentice Runner — execute learned workflows"
    )
    parser.add_argument("workflow", nargs="?", help="Workflow name to run")
    parser.add_argument("--list", action="store_true", help="List all learned workflows")
    parser.add_argument("--preview", action="store_true", help="Preview without running")
    parser.add_argument("--dry-run", action="store_true", help="Dry run (no execution)")
    parser.add_argument("vars", nargs="*", help="Variable assignments: KEY=value")

    args = parser.parse_args()

    if args.list or not args.workflow:
        workflows = list_workflows()
        if not workflows:
            print("No workflows learned yet.")
            print("Say 'watch me' to start teaching your agent.")
            return
        print(f"🎓 LEARNED WORKFLOWS ({len(workflows)}):")
        for w in workflows:
            print(f"  ✅ {w['name']:35s}  {w['step_count']} steps  learned: {w['learned_at']}")
        return

    if args.preview:
        preview_workflow(args.workflow)
        return

    variables = parse_variable_args(args.vars)
    run_workflow(args.workflow, variables, dry_run=args.dry_run)


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)