Remake skillu project

This commit is contained in:
lachtan
2026-07-22 12:31:40 +02:00
parent a8c9770d7d
commit 19014ed3d9
9 changed files with 234 additions and 256 deletions

View File

@@ -1,88 +1,128 @@
---
name: project
aliases: [proj]
description: >
Project management — long-running things with notes, next-steps, and status.
Use when user mentions "project".
Switch between named ongoing projects, each with its own persistent
context, carried forward across turns until the user switches or ends it.
Triggers on: "project X", "switch to project X", "we're working on X",
"end project" / "no project". For a long-lived, named work context — not a
single task to plan and execute, and not a short or global fact to
remember.
---
# Project
File-backed project store in `projects/`. Each project is one markdown file
with YAML frontmatter (`status`, `priority`, `created`, `slug`) and a free-form
body for notes and next-steps.
Named, long-lived work contexts. Each project keeps its own instructions, its
own history, its own current-state summary, and its own generated files —
separate from every other project and from the agent's general memory.
## Backend
## Layout
`skills/project/scripts/project.py` — deterministic CRUD for frontmatter and
basic operations. Agent handles all body edits via `edit_file` / `apply_patch`.
`workspace/projects/<slug>/`:
## Commands
- `prompt.md` — project-specific context/instructions, read in full whenever
the project becomes active
- `memory.md` — append-only chronological log of decisions and history
- `state.md` — living document: the current state/synthesis of the project,
edited in place, not appended to
- `artifacts/` — generated files (documents, code, data exports, reports)
### `project add <název>` — create
**Which project is active is tracked purely through conversation memory —
nothing is persisted to disk for that.** Once a project is chosen, keep
treating it as active for the rest of this conversation; don't re-read its
files on every subsequent turn once they're already in context.
1. Run: `uv run skills/project/scripts/project.py add "<název>" [--priority high|medium|low]`
2. Default priority is `medium`.
3. Echo: `Created project '<slug>' (priority: <priority>)`
## Activation
### `project list` — list active
Triggered by "project X" / "switch to project X" / "we're working on X":
1. Run: `uv run skills/project/scripts/project.py list`
2. Echo JSON output. Format as:
```
Active projects:
- <slug> (priority: high) — <first line of body / project name>
```
3. If empty → "No active projects."
1. Turn `X` into a kebab-case directory name (like picking a slug for a plan
file — no need to spell out an algorithm, just pick something sensible).
2. **`workspace/projects/<slug>/` already exists** → read `prompt.md`,
`memory.md`, and `state.md` in full, then briefly confirm what's active
and, if `prompt.md` or `state.md` has content, what context you loaded.
3. **It doesn't exist, but the name is a close match to one or more existing
projects** (case-insensitive) → ask which one was meant. Don't guess, and
don't create a near-duplicate of an existing project.
4. **It doesn't exist and there's no close match** → this skill only works
with projects that already exist without asking. Ask the user whether to
start a new project with that name. Only on explicit yes, create
`prompt.md`, `memory.md`, `state.md` (all empty) and `artifacts/`. Never
create a project just because a trigger phrase was said.
### `project show <slug>` — display
**Already active:** if the same project is already active in this
conversation, don't re-create or re-read anything — just continue.
1. Run: `uv run skills/project/scripts/project.py show <slug>`
2. Echo the full markdown file.
## Staying active
### `project status <slug> <active|paused|done>` — change status
Once a project is active, keep applying its `prompt.md` instructions and
`state.md` context for the rest of the conversation, until the user switches
or ends it.
1. Run: `uv run skills/project/scripts/project.py status <slug> <status>`
2. Echo: `Project '<slug>' is now <status>.`
If a long gap or an ambiguous reference makes it unclear whether the project
is still the right context (e.g. the conversation has clearly moved to an
unrelated topic), ask rather than silently carrying it forward or silently
dropping it.
### `project next <slug> <text>` — set next step
## Switching and ending
1. Read the project file.
2. Use `edit_file` to replace the content under `## Další krok` with the new text.
3. If the section does not exist, add it before the end of the file.
4. Echo: `Next step for '<slug>' updated.`
- **"switch to Y"** → run the Activation flow for Y; Y becomes active
instead.
- **"end project" / "no project" / "stop working on X"** → stop treating any
project as active. Say so. Don't delete anything.
- With no active project, behave normally — never force a project onto an
unrelated request.
### `project note <slug> <text>` — add a note
## Writing to memory.md
1. Read the project file.
2. Use `edit_file` to append a bullet under `## Poznámky`:
`- <today>: <text>`
3. If `## Poznámky` does not exist, add it after the first heading.
4. Echo: `Note added to '<slug>'.`
Append only — never rewrite or reorder existing entries. One entry per
decision, dead end, or noteworthy piece of history:
### `project switch <slug>` — session context
```
- YYYY-MM-DD: <terse entry, reformulated, not verbatim>
```
1. Run `my(action="set", key="project_context", value="<slug>")`.
2. Echo: `Switched to project '<slug>'. Next project commands without slug will use this context.`
3. If a command is missing a slug and `project_context` is set, use it automatically.
Write an entry when a decision is made, a dead end is found, or a fact
central to the project's ongoing context emerges — not for routine
back-and-forth. When in doubt whether something is memory-worthy, prefer not
writing it; `memory.md` is for what a future conversation needs to pick up
the thread, not a transcript.
## Rules
## Maintaining state.md
- **Slug** = kebab-case from first 4 words of the name. Used as filename (`<slug>.md`).
- **Frontmatter** is read-only for the agent — never edit it directly in the file.
Use `project.py status` to change status.
- **Body edits** (notes, next-step, structure changes) are always done by the agent
via `edit_file` / `apply_patch`.
- **No database** — pure markdown files. Git-friendly, one commit per change.
- **Session context** (`project switch`) lives only in `my` scratchpad and is lost
on restart. Re-run `project switch` after restart if needed.
- **Priority** = `high` | `medium` | `low`. `list` sorts by priority (high first).
- **Status** = `active` | `paused` | `done`. `list` shows only `active`.
Unlike `memory.md`, `state.md` is edited in place: surgically update the
relevant section when the project's current state or understanding has
moved on enough that the old text would mislead a reader. It answers "where
is this now", not "what happened" — old content gets replaced, not appended
to. If it's still empty and the project has accumulated enough context to
synthesize, offer to draft it.
## Artifacts
Files generated while working on the project (documents, code, data exports,
reports) go in `workspace/projects/<slug>/artifacts/` instead of scattered
elsewhere. Name them descriptively; no numbering or index file needed at
this scale.
## Listing
**"list projects" / "which projects exist":** list the directory names under
`workspace/projects/`. If none exist, say so.
## Edge cases
- `project add` with existing slug → error, do not overwrite.
- `project show` / `project status` / `project next` / `project note` with
missing slug → "Project '<slug>' not found."
- Missing `## Poznámky` or `## Další krok` → agent creates the section.
- Empty `projects/` → `list` returns empty array.
- "project" with no name → ask which project.
- The project directory exists but one or more of `prompt.md` / `memory.md`
/ `state.md` is missing (e.g. created by hand) → create the missing
file(s) empty, don't error.
- Deleting or renaming a project is out of scope for this skill — point the
user at `workspace/projects/<slug>/` to do it by hand.
## Rules
- This skill's body is English; reply to the user in their own language.
- Never fabricate project content — `prompt.md`, `memory.md`, and `state.md`
only grow from what the user actually said or what actually happened.
- Never create a new project without the user's explicit confirmation.
- Don't force a project context onto an unrelated request.
- The Dream memory processor must not touch `workspace/projects/` — it is
outside the memory and skills Dream curates.

View File

@@ -1,184 +0,0 @@
#!/usr/bin/env -S uv run --script
# /// script
# dependencies = ["pyyaml"]
# ///
"""
project.py — backend for /project skill.
Deterministic CRUD for project markdown files with YAML frontmatter.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from datetime import date
from pathlib import Path
import yaml
WORKSPACE = Path(__file__).resolve().parent.parent.parent.parent
PROJECTS_DIR = WORKSPACE / "projects"
# Valid statuses and priorities
STATUSES = {"active", "paused", "done"}
PRIORITIES = {"high", "medium", "low"}
def _slugify(name: str) -> str:
"""Kebab-case slug from first few words of name. Max 4 words."""
words = re.sub(r"[^a-zA-Z0-9\s]", "", name).lower().split()
words = words[:4]
return "-".join(words) if words else "project"
def _list_projects() -> list[dict]:
"""Parse frontmatter from all .md files in projects/."""
if not PROJECTS_DIR.exists():
return []
projects = []
for path in sorted(PROJECTS_DIR.glob("*.md")):
text = path.read_text(encoding="utf-8")
frontmatter, _ = _split_frontmatter(text)
if frontmatter:
meta = yaml.safe_load(frontmatter) or {}
meta["_file"] = path.name
projects.append(meta)
return projects
def _split_frontmatter(text: str) -> tuple[str | None, str]:
"""Split YAML frontmatter from body. Returns (frontmatter_yaml, body)."""
if not text.startswith("---\n"):
return None, text
end = text.find("\n---\n", 4)
if end == -1:
return None, text
return text[4:end], text[end + 5 :]
def _load_file(slug: str) -> tuple[Path, str, str | None, str]:
"""Load project file. Returns (path, full_text, frontmatter_yaml, body)."""
path = PROJECTS_DIR / f"{slug}.md"
if not path.exists():
raise FileNotFoundError(f"Project '{slug}' not found ({path.name})")
text = path.read_text(encoding="utf-8")
fm, body = _split_frontmatter(text)
return path, text, fm, body
def _write_file(path: Path, frontmatter: dict, body: str) -> None:
"""Write project file with YAML frontmatter."""
fm_yaml = yaml.safe_dump(frontmatter, allow_unicode=True, sort_keys=False, default_flow_style=False)
path.write_text(f"---\n{fm_yaml}---\n{body}", encoding="utf-8")
# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------
def cmd_add(args: argparse.Namespace) -> int:
name = (args.name or "").strip()
if not name:
print(json.dumps({"error": "name must not be empty"}), file=sys.stderr)
return 1
slug = _slugify(name)
PROJECTS_DIR.mkdir(parents=True, exist_ok=True)
path = PROJECTS_DIR / f"{slug}.md"
if path.exists():
print(json.dumps({"error": f"project '{slug}' already exists"}), file=sys.stderr)
return 1
priority = (args.priority or "medium").lower()
if priority not in PRIORITIES:
print(json.dumps({"error": f"invalid priority '{priority}' — use high/medium/low"}), file=sys.stderr)
return 1
frontmatter = {
"status": "active",
"priority": priority,
"created": date.today().isoformat(),
"slug": slug,
}
body = f"# {name}\n\n## Poznámky\n\n## Další krok\n\n"
_write_file(path, frontmatter, body)
print(json.dumps({"added": {"slug": slug, "name": name, "path": str(path.relative_to(WORKSPACE))}}, ensure_ascii=False))
return 0
def cmd_list(_args: argparse.Namespace) -> int:
projects = _list_projects()
active = [p for p in projects if p.get("status") == "active"]
# Sort by priority: high > medium > low
priority_order = {"high": 0, "medium": 1, "low": 2}
active.sort(key=lambda p: priority_order.get(p.get("priority", "medium"), 1))
print(json.dumps({"projects": active}, ensure_ascii=False))
return 0
def cmd_show(args: argparse.Namespace) -> int:
slug = (args.slug or "").strip()
try:
_path, text, _fm, _body = _load_file(slug)
except FileNotFoundError as exc:
print(json.dumps({"error": str(exc)}), file=sys.stderr)
return 1
print(text)
return 0
def cmd_status(args: argparse.Namespace) -> int:
slug = (args.slug or "").strip()
new_status = (args.status or "").strip().lower()
if new_status not in STATUSES:
print(json.dumps({"error": f"invalid status '{new_status}' — use active/paused/done"}), file=sys.stderr)
return 1
try:
path, text, fm_yaml, body = _load_file(slug)
except FileNotFoundError as exc:
print(json.dumps({"error": str(exc)}), file=sys.stderr)
return 1
if not fm_yaml:
print(json.dumps({"error": "no frontmatter found"}), file=sys.stderr)
return 1
frontmatter = yaml.safe_load(fm_yaml) or {}
frontmatter["status"] = new_status
_write_file(path, frontmatter, body)
print(json.dumps({"updated": {"slug": slug, "status": new_status}}, ensure_ascii=False))
return 0
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> int:
parser = argparse.ArgumentParser(description="Project file backend")
sub = parser.add_subparsers(dest="command", required=True)
p_add = sub.add_parser("add", help="Create a new project")
p_add.add_argument("name", help="Project name")
p_add.add_argument("--priority", default="medium", help="Priority: high/medium/low")
sub.add_parser("list", help="List active projects")
p_show = sub.add_parser("show", help="Show full project file")
p_show.add_argument("slug", help="Project slug")
p_status = sub.add_parser("status", help="Change project status")
p_status.add_argument("slug", help="Project slug")
p_status.add_argument("status", help="New status: active/paused/done")
args = parser.parse_args()
dispatch = {"add": cmd_add, "list": cmd_list, "show": cmd_show, "status": cmd_status}
return dispatch[args.command](args)
if __name__ == "__main__":
sys.exit(main())