Skill: agistack/planner (version 1.0.0)

--- name: planner description: Local-first planning engine for trips, weeks, launches, projects, schedules, and structured decision-making. Use whenever the user wants to plan something, organize future steps, create a roadmap, map constraints, compare options, or turn a vague goal into a phased plan. Also use when the user says they need a plan, itinerary, schedule, rollout, timeline, or structured next steps. Local-only storage. --- # Planner: Turn uncertainty into a workable plan. ## Core Philosophy 1. Plans should reduce uncertainty, not create overhead. 2. Start with constraints, then shape the plan. 3. Large plans should become phases, milestones, and next steps. 4. Good planning creates clarity before execution begins. ## Runtime Requirements - Python 3 must be available as `python3` - No external packages required ## Storage All data is stored locally only under: - `~/.openclaw/workspace/memory/planner/plans.json` - `~/.openclaw/workspace/memory/planner/archive.json` No external sync. No cloud storage. No third-party planning APIs. ## Plan Types - `trip`: Travel, itinerary, booking sequence, budget-aware planning. - `week`: Weekly structure, focus themes, time blocks, priorities. - `project`: Multi-phase roadmap with milestones. - `launch`: Rollout, preparation, dependencies, timeline. - `decision`: Option comparison with constraints and tradeoffs. ## Core Fields - `goal`: What the user wants to achieve - `constraints`: Budget, time, energy, deadlines, dependencies - `phases`: High-level stages - `milestones`: Concrete checkpoints - `next_steps`: Immediate actionable moves - `notes`: Additional planning context ## Key Workflows - **Capture a plan**: `add_plan.py` - **See best structure**: `suggest_plan.py` - **Update a plan**: `update_plan.py` - **Summarize today's planning view**: `daily_plan.py` - **Review all active plans**: `weekly_map.py` ## Scripts | Script | Purpose | |---|---| | `init_storage.py` | Initialize local storage files | | `add_plan.py` | Capture a new plan | | `suggest_plan.py` | Show the best current plan or next phase | | `update_plan.py` | Update plan status, dates, notes, or phases | | `daily_plan.py` | Show active plans and immediate next steps | | `weekly_map.py` | Review all plans across horizons |

path: references/philosophy.md
content: # Planner Philosophy

1. Start from the goal.
2. Make constraints explicit.
3. Break plans into phases.
4. Keep next steps visible.
5. Planning should create confidence, not clutter.


path: scripts/add_plan.py
content: #!/usr/bin/env python3
import argparse
import os
import sys
import uuid
from datetime import datetime

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, SCRIPT_DIR)

from lib.storage import load_plans, save_plans

VALID_TYPES = ["trip", "week", "project", "launch", "decision"]

def parse_csv(value):
    if not value:
        return []
    return [v.strip() for v in value.split(",") if v.strip()]

def parse_pipe(value):
    if not value:
        return []
    return [v.strip() for v in value.split("|") if v.strip()]

def main():
    parser = argparse.ArgumentParser(description="Capture a new plan")
    parser.add_argument("--goal", required=True, help="Main planning goal")
    parser.add_argument("--type", choices=VALID_TYPES, default="project")
    parser.add_argument("--deadline", help="Target date YYYY-MM-DD")
    parser.add_argument("--constraints", help="Comma-separated constraints")
    parser.add_argument("--phases", help="Pipe-separated phases")
    parser.add_argument("--milestones", help="Pipe-separated milestones")
    parser.add_argument("--next_steps", help="Pipe-separated immediate next steps")
    parser.add_argument("--notes", default="", help="Additional notes")
    args = parser.parse_args()

    prefix_map = {
        "trip": "TRP",
        "week": "WKP",
        "project": "PLN",
        "launch": "LCH",
        "decision": "DSN"
    }
    plan_id = f"{prefix_map[args.type]}-{str(uuid.uuid4())[:4].upper()}"
    now = datetime.now().isoformat()

    plan = {
        "id": plan_id,
        "goal": args.goal,
        "type": args.type,
        "status": "active",
        "created_at": now,
        "updated_at": now,
        "deadline": args.deadline,
        "constraints": parse_csv(args.constraints),
        "phases": parse_pipe(args.phases),
        "milestones": parse_pipe(args.milestones),
        "next_steps": parse_pipe(args.next_steps),
        "notes": args.notes
    }

    data = load_plans()
    data["plans"][plan_id] = plan
    save_plans(data)

    print(f"✓ Plan captured: {plan_id}")
    print(f"  Goal: {args.goal}")
    print(f"  Type: {args.type}")
    if args.deadline:
        print(f"  Deadline: {args.deadline}")

if __name__ == "__main__":
    main()


path: scripts/daily_plan.py
content: #!/usr/bin/env python3
import os
import sys

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, SCRIPT_DIR)

from lib.storage import load_plans

def main():
    data = load_plans()
    plans = [p for p in data.get("plans", {}).values() if p.get("status") == "active"]

    if not plans:
        print("No active plans right now.")
        return

    print("Active planning view:")
    for plan in plans[:5]:
        print(f"- {plan['id']} | {plan['goal']} | type={plan['type']}")
        if plan.get("next_steps"):
            print(f"  next: {plan['next_steps'][0]}")
        elif plan.get("phases"):
            print(f"  phase: {plan['phases'][0]}")

if __name__ == "__main__":
    main()


path: scripts/init_storage.py
content: #!/usr/bin/env python3
import json
import os
from datetime import datetime

PLANNER_DIR = os.path.expanduser("~/.openclaw/workspace/memory/planner")
PLANS_FILE = os.path.join(PLANNER_DIR, "plans.json")
ARCHIVE_FILE = os.path.join(PLANNER_DIR, "archive.json")

def write_if_missing(path, payload):
    if not os.path.exists(path):
        with open(path, "w", encoding="utf-8") as f:
            json.dump(payload, f, indent=2, ensure_ascii=False)

def main():
    os.makedirs(PLANNER_DIR, exist_ok=True)

    write_if_missing(PLANS_FILE, {
        "metadata": {
            "version": "1.0.0",
            "created_at": datetime.now().isoformat(),
            "last_updated": datetime.now().isoformat()
        },
        "plans": {}
    })

    write_if_missing(ARCHIVE_FILE, {
        "metadata": {
            "version": "1.0.0",
            "created_at": datetime.now().isoformat(),
            "last_updated": datetime.now().isoformat()
        },
        "plans": {}
    })

    print("✓ Planner storage initialized")
    print(f"  {PLANS_FILE}")
    print(f"  {ARCHIVE_FILE}")

if __name__ == "__main__":
    main()


path: scripts/lib/storage.py
content: #!/usr/bin/env python3
import json
import os
from datetime import datetime

PLANNER_DIR = os.path.expanduser("~/.openclaw/workspace/memory/planner")
PLANS_FILE = os.path.join(PLANNER_DIR, "plans.json")
ARCHIVE_FILE = os.path.join(PLANNER_DIR, "archive.json")

def ensure_dir():
    os.makedirs(PLANNER_DIR, exist_ok=True)

def _safe_load(path, default):
    ensure_dir()
    if not os.path.exists(path):
        return default
    try:
        with open(path, "r", encoding="utf-8") as f:
            return json.load(f)
    except (json.JSONDecodeError, OSError):
        return default

def _atomic_save(path, data):
    ensure_dir()
    tmp = path + ".tmp"
    with open(tmp, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2, ensure_ascii=False)
    os.replace(tmp, path)

def load_plans():
    return _safe_load(PLANS_FILE, {
        "metadata": {
            "version": "1.0.0",
            "created_at": datetime.now().isoformat(),
            "last_updated": datetime.now().isoformat()
        },
        "plans": {}
    })

def save_plans(data):
    data.setdefault("metadata", {})
    data["metadata"]["last_updated"] = datetime.now().isoformat()
    _atomic_save(PLANS_FILE, data)

def load_archive():
    return _safe_load(ARCHIVE_FILE, {
        "metadata": {
            "version": "1.0.0",
            "created_at": datetime.now().isoformat(),
            "last_updated": datetime.now().isoformat()
        },
        "plans": {}
    })

def save_archive(data):
    data.setdefault("metadata", {})
    data["metadata"]["last_updated"] = datetime.now().isoformat()
    _atomic_save(ARCHIVE_FILE, data)


path: scripts/suggest_plan.py
content: #!/usr/bin/env python3
import argparse
import os
import sys
from datetime import datetime

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, SCRIPT_DIR)

from lib.storage import load_plans

def score_plan(plan):
    score = 0
    if plan.get("status") != "active":
        return -9999
    if plan.get("deadline"):
        score += 20
        try:
            days_left = (datetime.fromisoformat(plan["deadline"]) - datetime.now()).days
            if days_left <= 7:
                score += 25
            elif days_left <= 30:
                score += 10
        except ValueError:
            pass
    if plan.get("next_steps"):
        score += 20
    if plan.get("milestones"):
        score += 10
    if plan.get("phases"):
        score += 10
    return score

def main():
    parser = argparse.ArgumentParser(description="Suggest current planning focus")
    parser.add_argument("--type", choices=["trip", "week", "project", "launch", "decision"])
    args = parser.parse_args()

    data = load_plans()
    plans = list(data.get("plans", {}).values())

    if args.type:
        plans = [p for p in plans if p.get("type") == args.type]

    ranked = sorted(plans, key=score_plan, reverse=True)
    ranked = [p for p in ranked if score_plan(p) > 0][:3]

    if not ranked:
        print("No strong active plan stands out right now.")
        print("Capture a new plan or add next steps to an existing one.")
        return

    labels = ["Top Plan", "Backup", "Backup"]
    for idx, plan in enumerate(ranked):
        print(f"{labels[idx]} — {plan['goal']}")
        print(f"  Type: {plan['type']}")
        if plan.get("deadline"):
            print(f"  Deadline: {plan['deadline']}")
        if plan.get("next_steps"):
            print(f"  Next step: {plan['next_steps'][0]}")
        elif plan.get("phases"):
            print(f"  Current phase candidate: {plan['phases'][0]}")
        print()

if __name__ == "__main__":
    main()


path: scripts/update_plan.py
content: #!/usr/bin/env python3
import argparse
import os
import sys
from datetime import datetime

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, SCRIPT_DIR)

from lib.storage import load_plans, save_plans

VALID_STATUS = ["active", "paused", "done", "archived"]

def parse_pipe(value):
    if not value:
        return None
    return [v.strip() for v in value.split("|") if v.strip()]

def main():
    parser = argparse.ArgumentParser(description="Update a plan")
    parser.add_argument("--id", required=True, help="Plan ID")
    parser.add_argument("--status", choices=VALID_STATUS)
    parser.add_argument("--deadline")
    parser.add_argument("--phases")
    parser.add_argument("--milestones")
    parser.add_argument("--next_steps")
    parser.add_argument("--notes")
    args = parser.parse_args()

    data = load_plans()
    plans = data.get("plans", {})

    if args.id not in plans:
        print(f"Plan not found: {args.id}")
        sys.exit(1)

    plan = plans[args.id]

    if args.status:
        plan["status"] = args.status
    if args.deadline is not None:
        plan["deadline"] = args.deadline
    if args.phases is not None:
        plan["phases"] = parse_pipe(args.phases)
    if args.milestones is not None:
        plan["milestones"] = parse_pipe(args.milestones)
    if args.next_steps is not None:
        plan["next_steps"] = parse_pipe(args.next_steps)
    if args.notes is not None:
        plan["notes"] = args.notes

    plan["updated_at"] = datetime.now().isoformat()
    save_plans(data)

    print(f"✓ Updated {args.id}")
    print(f"  Status: {plan['status']}")

if __name__ == "__main__":
    main()


path: scripts/weekly_map.py
content: #!/usr/bin/env python3
import os
import sys

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, SCRIPT_DIR)

from lib.storage import load_plans

def main():
    data = load_plans()
    plans = list(data.get("plans", {}).values())

    if not plans:
        print("No plans captured yet.")
        return

    print("Weekly planning map:")
    grouped = {}
    for plan in plans:
        grouped.setdefault(plan["type"], []).append(plan)

    for plan_type, items in grouped.items():
        print(f"\n[{plan_type}]")
        for plan in items[:10]:
            print(f"- {plan['id']} | {plan['goal']} | status={plan['status']}")

if __name__ == "__main__":
    main()


path: skill.json
content: {
  "name": "planner",
  "version": "1.0.0",
  "description": "Local-first planning engine for trips, weeks, launches, projects, schedules, and structured decision-making.",
  "entrypoints": [],
  "files": [
    "SKILL.md",
    "skill.json",
    "scripts/init_storage.py",
    "scripts/add_plan.py",
    "scripts/suggest_plan.py",
    "scripts/update_plan.py",
    "scripts/daily_plan.py",
    "scripts/weekly_map.py",
    "scripts/lib/storage.py",
    "references/philosophy.md"
  ]
}


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