Compare commits

...
9 Commits
Author SHA1 Message Date
ducoterra b88fbaec62 feat(phased-execution): push each phase commit after it is made
The harness now pushes the phase commit right after committing it
(PHASE_PUSH=1 default, new push_phase in lib.sh): upstream when set,
else 'git push -u <first remote> <branch>'; no remote = skip with a
notice. A push failure keeps the commit local, prints the same loud
ERROR contract as a commit failure, and stops the run — the next
phase's push sweeps the unpushed commit in. PHASE_PUSH=0 opts out with
a loud notice. SKILL.md (Commits + config table + exit codes) and the
executor prompts document the new behavior.
2026-09-10 07:56:30 -04:00
ducoterra f6cbb2d664 feat(phased-execution): harness commits each completed phase atomically
Children never commit (their prompts forbid git add/commit, overriding
project instructions) so the commit is deterministic. At the phase commit
point (00_phase.md final pass) the harness makes ONE atomic commit: the
phase's code changes, the todo→complete file move, and the executor
reports, together.

- Scoped staging: a worktree snapshot taken at the phase's first unit
  (phase-sessions/dirty-<phase>, always kept) is subtracted, so the
  owner's pre-existing uncommitted work is left alone; runtime artifacts
  are never staged. The commit prints exactly what went in.
- Verified: the moved phase file (and report, unless gitignored) must be
  in the index before committing.
- Loud on failure: a commit failure prints the git error + hand-fix and
  stops the run; the phase stays complete, the miss is never swept into a
  later phase.
- PHASE_COMMIT_SUBJECT (default 'phase: <phase>'); the executor's final
  report becomes the commit body; --no-gpg-sign always passed.
- PHASE_COMMIT defaults to 1: completed phases are committed by default.
  Explicit PHASE_COMMIT=0 opts out with a loud warning that the phase is
  complete but uncommitted — a completed phase is never left uncommitted
  silently (that gap let four phases pile up uncommitted in
  brain_of_reese).
2026-09-07 12:42:59 -04:00
ducoterra e505df62e8 refactor(skills): use .agents/ instead of .agent/ for phased execution
Standardize on the .agents/ directory across all phased-execution
skills (phase state, reports, sessions, validate.sh, PLAN.md, and
per-story/feature/workflow trees). Legacy dot-less agent/ fallbacks
in migration scripts are untouched.
2026-09-05 10:52:37 -04:00
ducoterra e8a2106172 fix .agent commits 2026-09-01 10:20:06 -04:00
ducoterra 4c187ca0d2 feat(phased-execution): ntfy notification after each task; add new-project, upgrade-existing-app, writing-tests, security-audit skills 2026-08-25 11:35:29 -04:00
ducoterra a5c01735ca add todo to phased 2026-08-24 18:32:05 -04:00
ducoterra 973df33c62 add interactive browser 2026-08-23 22:11:20 -04:00
ducoterra b78ddbf256 add ntfy skill 2026-08-23 21:05:01 -04:00
ducoterra 85ac0d61b9 feat: phases + tasks — per-task execution with phase directories
- A phase is now a directory: 00_phase.md (overview + task index) plus
  small, quick NN_task.md task files; complete/ mirrors todo/
- Task is the unit of execution: run-task.sh (one unit), run-phase.sh
  (phase to completion incl. 00_phase.md final pass), auto-phase.sh
  (all units in order); validate.sh gate runs after every task
- PHASE_COMMIT commits at phase boundaries (00_phase.md / legacy file moves)
- Legacy flat todo/NN_name.md files still execute as a single unit;
  new migrate-phases-to-tasks.sh converts them to the directory layout
- phase-status.sh reports per-task state and the next unit
- New task-template.md; phase templates now carry a Tasks index
2026-08-23 19:28:01 -04:00
36 changed files with 3219 additions and 322 deletions
-5
View File
@@ -1,5 +0,0 @@
# pi treats any root-level .md file in a skills directory as a skill and
# warns "description is required" for non-skill markdown like this README.
# pi's skill scanner honors .gitignore files, so ignoring README.md here
# keeps it out of skill discovery (and out of git).
README.md
+66 -58
View File
@@ -1,39 +1,40 @@
---
name: convert-to-phased
description: Converts an existing project to the .agent/phases/ phased-execution strategy — audits the codebase, scaffolds .agent/PLAN.md, AGENTS.md, and .agent/phases/{todo,complete}/, decomposes existing functionality into per-feature files (workflows / features / user_stories), and writes a sequential phase roadmap where every phase is independently executable with its own test suite. Use when the user asks to adopt phased execution, convert an existing or legacy project to the phased protocol, or set up a phase roadmap for an existing codebase. This skill only writes planning files and never touches application code; the phase-authoring skill adds individual phases, and the phased-execution skill runs them.
description: Converts an existing project to the .agents/phases/ phased-execution strategy — audits the codebase, scaffolds .agents/PLAN.md, AGENTS.md, and .agents/phases/{todo,complete}/, decomposes existing functionality into per-feature files (workflows / features / user_stories), and writes a sequential phase roadmap where every phase is independently executable with its own test suite. Use when the user asks to adopt phased execution, convert an existing or legacy project to the phased protocol, or set up a phase roadmap for an existing codebase. This skill only writes planning files and never touches application code; the phase-authoring skill adds individual phases, and the phased-execution skill runs them.
---
# Convert to Phased
You are the **Conversion Architect** — a senior engineer responsible for
adopting an *existing* project into the phased-execution strategy. You audit
the codebase, scaffold the `.agent/` planning structure, and write a phase
the codebase, scaffold the `.agents/` planning structure, and write a phase
roadmap that takes the project from its current state to the agreed target
state. You **never implement code changes yourself** — every code change is
expressed as a phase file that the `phased-execution` skill executes. And you
expressed as a phase directory (a `00_phase.md` overview plus small task
files) that the `phased-execution` skill executes, task by task. And you
never add individual phases to an already-converted project — that is the
`phase-authoring` skill.
Phase state lives in files, not chat:
- `.agent/PLAN.md` — master plan; **LOCKED DECISIONS** are binding
- `.agent/phases/todo/NN_name.md` — pending phases (sort order = execution order)
- `.agent/phases/complete/` — finished phases (read-only history)
- `.agent/validate.sh` — the pass/fail gate for every phase (installed by the `phased-execution` skill on first run)
- `.agents/PLAN.md` — master plan; **LOCKED DECISIONS** are binding
- `.agents/phases/todo/NN_name/` — a pending phase: `00_phase.md` overview + `NN_task.md` task files (task sort order = execution order)
- `.agents/phases/complete/` — finished phases, mirroring the todo/ layout (read-only history)
- `.agents/validate.sh` — the pass/fail gate, run after every task (installed by the `phased-execution` skill on first run)
## Choosing the mode
- **Not phased** (no `.agent/phases/todo/`) → run the conversion protocol below.
- **Already phased** (`.agent/phases/todo/` exists) → stop; do not
- **Not phased** (no `.agents/phases/todo/`) → run the conversion protocol below.
- **Already phased** (`.agents/phases/todo/` exists) → stop; do not
re-convert. Point the user at the `phase-authoring` skill.
- **Partially phased** (some `.agent/` artifacts exist) → convert only the
- **Partially phased** (some `.agents/` artifacts exist) → convert only the
missing parts, and merge into existing files instead of overwriting them.
## Phase 1 — Audit (change nothing yet)
1. Run `bash scripts/project-audit.sh` (resolve `scripts/` against this
skill's directory) to report the version-control state, detected stack,
existing `.agent/` artifacts, test tooling, and the next free phase number.
existing `.agents/` artifacts, test tooling, and the next free phase number.
2. Read the README, manifests, configuration, entry points, and the test
suite layout. Identify the significant features that exist in code but
have no feature file or dedicated tests.
@@ -45,7 +46,7 @@ Phase state lives in files, not chat:
floor is, existing failures.
- **Feature decomposition:** features lacking a story/feature/workflow
file or a dedicated test suite.
- **Existing `.agent/` artifacts:** what exists, what is missing.
- **Existing `.agents/` artifacts:** what exists, what is missing.
4. **Wait for confirmation.** Do not write anything until the user confirms
the upgrade strategy based on the report.
@@ -68,68 +69,70 @@ matches the project's domain:
| Domain | Directory | File content |
|------------|---------------------------|--------------|
| CLI tool | `.agent/workflows/` | I/O contract: arguments/options, stdout/stderr, exit codes. Source of truth for the CliRunner contract tests. |
| Library | `.agent/features/` | Public API sketch + exception contracts. Source of truth for the capability test modules. |
| Web app | `.agent/user_stories/` | Narrative (Given/When/Then), UI Visualization & Structure (current and target), Playwright mapping rule. |
| Anything else | `.agent/features/` | Contract-level description: inputs/outputs and observable behavior. |
| CLI tool | `.agents/workflows/` | I/O contract: arguments/options, stdout/stderr, exit codes. Source of truth for the CliRunner contract tests. |
| Library | `.agents/features/` | Public API sketch + exception contracts. Source of truth for the capability test modules. |
| Web app | `.agents/user_stories/` | Narrative (Given/When/Then), UI Visualization & Structure (current and target), Playwright mapping rule. |
| Anything else | `.agents/features/` | Contract-level description: inputs/outputs and observable behavior. |
These files describe the **current** behavior precisely — they are contracts,
not aspirations. The refactor to reach a contract becomes a phase.
## Phase 4 — Scaffold the `.agent/` structure
## Phase 4 — Scaffold the `.agents/` structure
Create (or merge into what already exists) with file tools:
- **`.agent/PLAN.md`** — from `assets/plan-template.md`: assumptions & design
- **`.agents/PLAN.md`** — from `assets/plan-template.md`: assumptions & design
principles, the anchors table, high-level architecture (mirroring the
actual code), data model, validation workflow, the domain section (CLI/UX
strategy, public-API principles, or UI/UX guidelines), and the phase roadmap.
- **`AGENTS.md`** — the five base rules below, plus the domain additions.
- **`.agent/phases/todo/`** — the roadmap from Phase 5.
- **`.agent/phases/complete/`** — create the directory, leave it empty.
- **`.gitignore`** — add `.agent/` and `.agent/phase-sessions/` if missing.
- **`.agents/phases/todo/`** — the roadmap from Phase 5.
- **`.agents/phases/complete/`** — create the directory, leave it empty (it mirrors `todo/` as phases complete).
- **`.gitignore`** — add `.agents/phase-sessions/` and `.agents/pipeline.log` if missing (never `.agents/` itself — the planning tree is tracked and committed); if an existing `.gitignore` has a `.agents/` ignore line, remove it.
**AGENTS.md base rules:**
1. "Always read `.agent/PLAN.md` first to understand the project context and goals."
2. "Follow the phased execution protocol in `.agent/phases/`."
3. "Never modify `.agent/PLAN.md` or any files in `.agent/phases/complete/`."
4. "If you need to update any file in `.agent/phases/todo/`, you must ask the user for permission first."
5. "Strictly adhere to the **LOCKED DECISIONS** listed in `.agent/PLAN.md`."
1. "Always read `.agents/PLAN.md` first to understand the project context and goals."
2. "Follow the phased execution protocol in `.agents/phases/`."
3. "Never modify `.agents/PLAN.md` or any files in `.agents/phases/complete/`."
4. "If you need to update any file in `.agents/phases/todo/`, you must ask the user for permission first."
5. "Strictly adhere to the **LOCKED DECISIONS** listed in `.agents/PLAN.md`."
**Domain additions:**
- **CLI:** "One Workflow, One Phase": each `.agent/workflows/` file
- **CLI:** "One Workflow, One Phase": each `.agents/workflows/` file
corresponds to a distinct execution phase with its own dedicated CliRunner
contract test suite. "Safety Check": any command that mutates state must
implement `--dry-run` and be idempotent before it is complete. "Options
over Arguments": new parameters are typed options.
- **Library:** "One Capability, One Phase": each `.agent/features/` file
- **Library:** "One Capability, One Phase": each `.agents/features/` file
corresponds to a distinct execution phase with its own dedicated pytest
suite. "Public API Lock": once a capability phase is complete, its public
API is LOCKED; changes follow SemVer. "Tests via Public API": integration
tests exercise the public API surface, never private internals.
- **Web:** "One Story, One Phase": each `.agent/user_stories/` file
- **Web:** "One Story, One Phase": each `.agents/user_stories/` file
corresponds to a distinct execution phase with its own dedicated Playwright
E2E test suite. "UI Structure Check": before finalizing any UI component,
verify it follows the layout principles in `.agent/PLAN.md` and meets WCAG
verify it follows the layout principles in `.agents/PLAN.md` and meets WCAG
accessibility basics. "No CDN Rule": all CSS/JS/fonts/images must be served
locally, no external asset URLs.
**Do not create `.agent/validate.sh`** — the `phased-execution` skill
**Do not create `.agents/validate.sh`** — the `phased-execution` skill
installs it from its template on first run and it must be adapted to the
project's real checks (the foundation phase does that).
## Phase 5 — The Phase Roadmap
Write sequential phase files into `.agent/phases/todo/` using
`assets/phase-template.md` (mirrors the `phase-authoring` template; keep the
sections identical). Numbering starts at the audit's "next phase number",
counting `todo/` and `complete/` together. Never reuse or collide a number.
Write sequential phase **directories** into `.agents/phases/todo/` — each
`NN_name/` holds a `00_phase.md` overview (from `assets/phase-template.md`)
and its task files (from `assets/task-template.md`); both mirror the
`phase-authoring` templates, keep the sections identical. Numbering starts
at the audit's "next phase number", counting `todo/` and `complete/`
together. Never reuse or collide a number.
Roadmap shape:
1. **`01_…` foundation/rectification phase** — adapt `.agent/validate.sh` to
1. **`01_…` foundation/rectification phase** — adapt `.agents/validate.sh` to
the project's real test/lint/coverage checks; fix or baseline the existing
test failures; add missing test/coverage tooling. The full current suite
must be green and the project fully launchable when this phase completes.
@@ -138,19 +141,23 @@ Roadmap shape:
3. **One phase per feature file** (one workflow/capability/story, one
phase), in dependency order.
Every phase file must contain:
Every phase directory must contain:
- **Objective / Dependencies / Tasks** — file-level detail; tasks express the
*delta* from current state to the contract, not a from-scratch rebuild.
- **Testing & Quality (mandatory)** — unit and integration tests for all
new/modified logic; coverage **>90%** on new/modified code; plus the domain
block below.
- **Completion Criteria** — observable checks (commands to run, endpoints to
hit, artifacts to exist).
- **Feature linkage** — the header line references the corresponding feature
file (`.agent/workflows/…`, `.agent/features/…`, or `.agent/user_stories/…`).
- **`00_phase.md`** — **Objective / Dependencies / Tasks** (ordered index of
the task files) / **Testing & Quality (mandatory)** / **Completion
Criteria.** Unit and integration tests are required for all new/modified
logic, with coverage **>90%** on new/modified code, plus the domain block
below. Tasks express the *delta* from current state to the contract, not a
from-scratch rebuild. The header line references the corresponding feature
file (`.agents/workflows/…`, `.agents/features/…`, or
`.agents/user_stories/…`).
- **Task files `01_…`, `02_…`, …** — small, quick units (one focused change,
roughly ≤30 minutes of executor work; a phase typically holds 2–8):
Objective, Work (file-level steps), Testing & Quality, Completion
Criteria.
Domain blocks (include verbatim in the matching phase files):
Domain blocks (include verbatim in the matching `00_phase.md`; the phase's
final pass is where they are verified end-to-end):
- **CLI:** `## CLI Contract Execution Phase` — instruct the executing agent
to run ONLY the specific CliRunner contract test module for that workflow
@@ -175,15 +182,16 @@ Domain blocks (include verbatim in the matching phase files):
- **Independent Viability:** each phase leaves the project functional and launchable.
- **Architectural Anchors only:** no phase may use technology outside the LOCKED DECISIONS.
- **No Regressions:** a phase must not alter the behavior of completed phases.
- **Executable in isolation:** an agent that sees only the repository and this
phase file — no chat history, no follow-ups — must be able to finish it.
- **Executable in isolation:** an agent that sees only the repository, the phase
overview, the completed task files, and one task file — no chat history, no
follow-ups — must be able to finish that task.
## Phase 6 — Version Control & Hand-Off
- Git is mandatory: `git init` if the project is not a repository.
- Commit the conversion — `AGENTS.md`, `.gitignore`, and any other modified
non-`.agent/` files (the `.agent/` tree itself is git-ignored by protocol) —
with a Conventional Commits message (e.g. `chore(agent): adopt phased
- Commit the conversion — `AGENTS.md`, `.gitignore`, the whole `.agents/`
tree (it is tracked, not git-ignored), and any other modified files — with
a Conventional Commits message (e.g. `chore(agent): adopt phased
execution strategy with NN-phase roadmap`), always with `--no-gpg-sign`.
- Record in `AGENTS.md`: atomic commit at the conclusion of every completed
phase, Conventional Commits messages, and `--no-gpg-sign` on every
@@ -192,19 +200,19 @@ Domain blocks (include verbatim in the matching phase files):
Finish by summarizing the Architectural Anchors, the feature decomposition,
and the phase list (number, name, one-line objective). Then hand off:
- **Execute:** the `phased-execution` skill — `run-phase.sh` for a single
phase, `auto-phase.sh` for the full pipeline.
- **Execute:** the `phased-execution` skill — `run-task.sh` for a single
task, `run-phase.sh` for a phase, `auto-phase.sh` for the full pipeline.
- **Extend:** the `phase-authoring` skill for individual additional phases.
## Strict Operational Rules
- The conversion writes **planning files only**: `.agent/**`, `AGENTS.md`,
- The conversion writes **planning files only**: `.agents/**`, `AGENTS.md`,
and `.gitignore`. **Never modify application code in this skill** — bugs
and failing tests found during the audit become tasks in the foundation
phase, not direct edits.
- Never overwrite existing `.agent/` content — merge into it.
- Never modify anything in `.agent/phases/complete/`.
- Do not create `.agent/validate.sh` (the `phased-execution` skill installs
- Never overwrite existing `.agents/` content — merge into it.
- Never modify anything in `.agents/phases/complete/`.
- Do not create `.agents/validate.sh` (the `phased-execution` skill installs
it on first run).
- Wait for confirmation after the Gap Analysis Report, and wait for the user
to lock any `PROPOSED` anchor before writing `PLAN.md`.
+3 -3
View File
@@ -1,6 +1,6 @@
# Phase {{NN}} — {{Short Title}}
**Feature:** `{{.agent/workflows/<name>.md | .agent/features/<name>.md | .agent/user_stories/<name>.md | "n/a"}}`
**Feature:** `{{.agents/workflows/<name>.md | .agents/features/<name>.md | .agents/user_stories/<name>.md | "n/a"}}`
**Context:** `{{PLAN.md sections / files this phase builds on}}`
## Objective
@@ -11,8 +11,8 @@
{{or "— (none)"}}
## Tasks
1. `{{path/to/file}}` — {{specific change, file-level detail}}
2. `{{path/to/file}}` — {{specific change}}
1. `01_{{short_name}}.md` — {{one-line summary}}
2. `02_{{short_name}}.md` — {{one-line summary}}
## Testing & Quality
- Unit/integration: {{tests required for all new/modified logic — name the behaviors to cover}}
+1 -1
View File
@@ -31,4 +31,4 @@ accessibility, asset policy) — whichever matches the project's domain.}}
| # | Phase | Feature file | Objective (one line) |
|---|-------|--------------|----------------------|
| 01 | {{NN_name}} | — | foundation: adapt validate.sh, green test baseline |
| 02 | {{NN_name}} | `.agent/{{workflows\|features\|user_stories}}/{{name}}.md` | {{...}} |
| 02 | {{NN_name}} | `.agents/{{workflows\|features\|user_stories}}/{{name}}.md` | {{...}} |
+19
View File
@@ -0,0 +1,19 @@
# Task {{NN}} — {{Short Title}}
**Phase:** `{{NN_phase}}` · **Feature:** `{{.agents/workflows/<name>.md | .agents/features/<name>.md | .agents/user_stories/<name>.md | "n/a"}}`
## Objective
{{1–2 sentences: what this task delivers (the delta from current state to the contract)}}
## Work
1. `{{path/to/file}}` — {{specific change, file-level detail}}
2. `{{path/to/file}}` — {{specific change}}
## Testing & Quality
- Unit/integration: {{tests required for this task's logic}}
- Coverage: **>90%** on this task's new/modified code
## Completion Criteria
- [ ] {{observable check: command to run / endpoint to hit / artifact to exist}}
- [ ] full test suite green
- [ ] no behavior change in completed work
+35 -31
View File
@@ -2,8 +2,8 @@
# project-audit.sh — stack & phased-readiness probe for the Conversion Architect.
#
# Prints: project root, version-control state, detected manifests, domain
# hints, existing .agent/ artifacts, test tooling, and the next free phase
# number NN (counting .agent/phases/todo/ and complete/ together,
# hints, existing .agents/ artifacts, test tooling, and the next free phase
# number NN (counting .agents/phases/todo/ and complete/ together,
# zero-padded to 2 digits).
#
# Usage: bash project-audit.sh # from anywhere in the project tree
@@ -72,60 +72,64 @@ fi
if (( ! hint )); then echo " (none — classify from the manifests above)"; fi
echo
echo ".agent state:"
echo ".agents state:"
a=0
if [[ -f "$root/.agent/PLAN.md" ]]; then
echo " .agent/PLAN.md: present"
if [[ -f "$root/.agents/PLAN.md" ]]; then
echo " .agents/PLAN.md: present"
a=1
fi
if [[ -f "$root/AGENTS.md" ]]; then
echo " AGENTS.md: present"
a=1
fi
if [[ -f "$root/.agent/validate.sh" ]]; then
echo " .agent/validate.sh: present"
if [[ -f "$root/.agents/validate.sh" ]]; then
echo " .agents/validate.sh: present"
a=1
fi
for d in workflows features user_stories; do
if [[ -d "$root/.agent/$d" ]]; then
n="$(find "$root/.agent/$d" -maxdepth 1 -name '*.md' 2>/dev/null | wc -l | tr -d ' ')"
echo " .agent/$d/: present ($n file(s))"
if [[ -d "$root/.agents/$d" ]]; then
n="$(find "$root/.agents/$d" -maxdepth 1 -name '*.md' 2>/dev/null | wc -l | tr -d ' ')"
echo " .agents/$d/: present ($n file(s))"
a=1
fi
done
todo_dir="$root/.agent/phases/todo"
complete_dir="$root/.agent/phases/complete"
if [[ -d "$todo_dir" ]]; then
echo " .agent/phases/todo:"
any=0
for f in "$todo_dir"/*.md; do
if [[ -e "$f" ]]; then
echo " $(basename "$f")"
todo_dir="$root/.agents/phases/todo"
complete_dir="$root/.agents/phases/complete"
list_phases() {
local dir="$1" entry n t any=0
for entry in "$dir"/*; do
if [[ ! -e "$entry" ]]; then continue; fi
n="$(basename "$entry")"
if [[ "$n" =~ ^[0-9] ]]; then
if [[ -d "$entry" ]]; then
t="$(ls -1 "$entry" 2>/dev/null | grep -cE '\.md$' || true)"
echo " $n/ ($t file(s): 00_phase.md + tasks)"
else
echo " $n (legacy single-file phase)"
fi
any=1
fi
done
if (( ! any )); then echo " (empty)"; fi
}
if [[ -d "$todo_dir" ]]; then
echo " .agents/phases/todo:"
list_phases "$todo_dir"
a=1
fi
if [[ -d "$complete_dir" ]]; then
echo " .agent/phases/complete:"
any=0
for f in "$complete_dir"/*.md; do
if [[ -e "$f" ]]; then
echo " $(basename "$f")"
any=1
fi
done
if (( ! any )); then echo " (empty)"; fi
echo " .agents/phases/complete:"
list_phases "$complete_dir"
a=1
fi
if (( ! a )); then echo " (no .agent/ artifacts — full conversion needed)"; fi
if (( ! a )); then echo " (no .agents/ artifacts — full conversion needed)"; fi
max=0
for d in "$todo_dir" "$complete_dir"; do
for f in "$d"/*.md; do
if [[ ! -e "$f" ]]; then continue; fi
n="$(basename "$f" .md)"
[[ -d "$d" ]] || continue
for entry in "$d"/*; do
if [[ ! -e "$entry" ]]; then continue; fi
n="$(basename "$entry" .md)"
if [[ "$n" =~ ^([0-9]+) ]]; then
n=$((10#${BASH_REMATCH[1]}))
if (( n > max )); then max=$n; fi
+1
View File
@@ -0,0 +1 @@
node_modules/
+312
View File
@@ -0,0 +1,312 @@
---
name: interactive-browser
description: "Launch a visible Playwright browser and interact with web pages with human-in-the-loop assistance. Use when the user wants the agent to browse websites, fill forms, click buttons, or automate web tasks while the user watches and guides. The browser window is visible (not headless) and the agent takes screenshots to show the user the current page state."
---
# Interactive Browser
Launch a visible Playwright browser and interact with web pages while the user watches and provides guidance. This is a **human-in-the-loop** browser automation skill — the agent performs actions but the user sees everything and directs what happens next.
## Setup (one-time)
The skill uses Playwright with Chromium. Ensure Playwright is available:
```bash
npx playwright --version
```
If Chromium is not installed, install it:
```bash
npx playwright install chromium
```
The skill installs its own Playwright dependency on first use.
## How It Works
The skill runs a lightweight HTTP server that keeps a visible browser open. The agent communicates with the server via `curl` commands. All screenshots are saved to a temp directory and can be displayed to the user.
## Starting the Browser Server
```bash
# Kill any stale server on the port first
kill $(lsof -ti:9876) 2>/dev/null; sleep 2
# Start detached with logging so it survives shell exit
cd /var/home/ducoterra/.pi/agent/skills/interactive-browser && \
node scripts/browser-server.js 9876 /tmp/pi-browser-screenshots > /tmp/browser-server.log 2>&1 &
sleep 3
# ALWAYS verify it started
curl -s -g -X POST "http://localhost:9876/status"
```
The server runs on port **9876** by default. Screenshots are saved to the specified directory. Action logs go to `/tmp/browser-server.log` — check it whenever a request misbehaves.
> **Why the redirect?** A background process writing to a dead terminal can get SIGHUP/SIGPIPE and die silently. Always redirect output to a log file and verify with `/status` before use.
## Commands (via curl)
All commands use `POST` requests to `http://localhost:9876`.
**Always use `curl -g`** — selectors like `a[href='/user/login']` contain brackets that curl would otherwise interpret as URL globs (exit code 3: malformed URL).
Parameters can go in the query string OR the request body (JSON or url-encoded — body wins). For complex selectors/JS with special characters, prefer `--data-urlencode`:
```bash
curl -s -g -X POST "http://localhost:9876/click" --data-urlencode "selector=button:has-text('Submit')"
```
### Parameters
| Param | Applies to | Description |
|-------|-----------|-------------|
| `url` | launch, go | Target URL |
| `selector` | click, dblclick, fill, select, hover, get_text | CSS selector |
| `value` | fill, select | Text to fill / option value |
| `expr` | evaluate | JavaScript expression to run in page |
| `force` | click | `true` skips Playwright actionability checks (use when element is obscured or animating) |
### Launch & Navigate
```bash
# Launch browser and navigate to a URL
curl -s -g -X POST "http://localhost:9876/launch?url=https://example.com"
# Navigate to a different URL (browser stays open)
curl -s -g -X POST "http://localhost:9876/go?url=https://other-site.com"
# Reload current page
curl -s -g -X POST "http://localhost:9876/reload"
```
### Interact
```bash
# Click an element (add force=true if it's covered/animating)
curl -s -g -X POST "http://localhost:9876/click?selector=#login-btn"
# Double-click
curl -s -g -X POST "http://localhost:9876/dblclick?selector=.item"
# Fill a form field — use --data-urlencode for selectors with special chars
curl -s -g -X POST "http://localhost:9876/fill" --data-urlencode "selector=input[name='email']" --data-urlencode "value=user@example.com"
# Select an option from a dropdown
curl -s -g -X POST "http://localhost:9876/select" --data-urlencode "selector=select#country" --data-urlencode "value=US"
# Hover over an element
curl -s -g -X POST "http://localhost:9876/hover?selector=.dropdown-trigger"
```
### Inspect
```bash
# Take a screenshot of the current page
curl -s -g -X POST "http://localhost:9876/screenshot"
# Get text content of an element
curl -s -g -X POST "http://localhost:9876/get_text?selector=#main-heading"
# Evaluate JavaScript on the page (always via --data-urlencode for complex expressions)
curl -s -g -X POST "http://localhost:9876/evaluate" --data-urlencode "expr=document.title"
```
### Control
```bash
# Check browser status
curl -s -g -X POST "http://localhost:9876/status"
# Close the browser
curl -s -g -X POST "http://localhost:9876/close"
```
## Output Format
Each command outputs JSON to stdout:
```json
{
"status": "ok",
"command": "click",
"selector": "#login-btn",
"url": "https://example.com/page",
"title": "Example Page",
"screenshot": "/tmp/pi-browser-screenshots/click-2024-01-01T12-00-00.png"
}
```
On error:
```json
{
"status": "error",
"message": "Server error",
"details": "Target closed"
}
```
## Workflow: Human-in-the-Loop Interaction
This is the recommended pattern for interactive browser automation:
### 1. Start the Server
See [Starting the Browser Server](#starting-the-browser-server) — kill stale instances, start with log redirect, verify with `/status`.
### 2. Launch the Browser
```bash
curl -s -g -X POST "http://localhost:9876/launch?url=https://target-site.com"
```
### 3. Show the User the Page
Read the screenshot file from the JSON output and display it. Tell the user what you see and ask what they want to do.
```bash
read /tmp/pi-browser-screenshots/launch-*.png
```
### 4. Get User Instructions
The user looks at the visible browser window AND the screenshot, then tells you what to do:
> "Click the 'Sign In' button in the top right"
> "Fill the email field with my address"
> "Take a screenshot, I need to see what's on the page"
### 5. Execute the Action
Run the appropriate curl command:
```bash
curl -s -g -X POST "http://localhost:9876/click?selector=.nav-signin"
```
### 6. Show the Result
Read the new screenshot and show it to the user. Repeat from step 4.
### 7. Close When Done
```bash
curl -s -g -X POST "http://localhost:9876/close"
```
## Finding Selectors (Don't Guess)
**When a click fails with `Timeout ... waiting for locator(...)`, the selector doesn't match the page** — the site uses different markup than you assumed (e.g. Gitea's sign-in link is `a[href='/user/login']`, not `/user/sign_in`). Never guess hrefs. Enumerate elements first:
```bash
# List all links with their text and href
curl -s -g -X POST "http://localhost:9876/evaluate" \
--data-urlencode "expr=Array.from(document.querySelectorAll('a')).map(function(a){return a.textContent.trim()+' -> '+a.getAttribute('href')}).join('\n')"
# List form fields
curl -s -g -X POST "http://localhost:9876/evaluate" \
--data-urlencode "expr=Array.from(document.querySelectorAll('input,textarea,select')).map(function(e){return e.tagName+' name='+e.getAttribute('name')+' type='+e.getAttribute('type')}).join('\n')"
```
Selector tips:
- Use CSS selectors: `#id`, `.class`, `tag`, `[attr="value"]`
- For form fields: `input[type="email"]`, `textarea[name="message"]`, `select#country`
- For precise targeting: `input[name="username"]` is more stable than text-matching
- Use `force=true` when an element is found but covered/animating: `.../click?selector=X&force=true`
## Reading Page Content (DOM > Screenshots)
For extracting text (post lists, comments, tables), use `evaluate` on the DOM — it's exact, cheap, and scales to whole pages. Screenshots are for *showing the user state*, not for reading data.
```bash
# Example: extract Reddit post titles + scores
curl -s -g -X POST "http://localhost:9876/evaluate" \
--data-urlencode "expr=Array.from(document.querySelectorAll('shreddit-post')).map(function(p){return p.getAttribute('score')+' pts | '+p.getAttribute('post-title')}).join('\n')"
```
### Lazy-loaded / infinite-scroll pages
Pages like Reddit load content as you scroll. Scroll several times with pauses before extracting:
```bash
for i in 1 2 3 4 5; do
curl -s -g -X POST "http://localhost:9876/evaluate" --data-urlencode "expr=window.scrollBy(0, 1200)" > /dev/null
sleep 0.8
done
# then run your extraction evaluate
```
### Writing JS for `evaluate`
- Pass expressions via `--data-urlencode` — never hand-encode in the query string
- Prefer `function(){}` over arrow functions, and IIFEs `(function(){...})()` for multi-statement code
- Return a single string/number (the result is `String()`-ed); join multi-line output with `'\n'`
## Important Notes
- **Requires a display** — the browser is visible, so this needs X11/Wayland (or X forwarding / a virtual display). It will fail on a headless box with no display server.
- **The browser is visible** — the user should see it open on their screen
- **slowMo is set to 100ms** — actions have a slight delay so the user can follow along
- **Viewport is 1280x900** — reasonably sized for most tasks
- **Persistent session** — the browser keeps state (cookies, localStorage) between commands, so logged-in sessions persist
- **One browser, one page** — the server drives a single page; no parallel tabs
- **Secrets** — when possible, ask the user to type passwords themselves in the visible window rather than passing them through chat/commands
- **Screenshots are saved** to the temp directory with timestamps — you can review them later
- **Interaction timeouts are 8s** — failures return a JSON error fast; check the error's `details` for Playwright's call log
- **Trust the screenshot image, not a timing theory.** After `/screenshot`, always actually `read` the saved PNG before concluding anything. Never declare a page "blank" or "not rendered" based on an assumption about render timing or a navigation race — verify by opening the file. If it looks blank, re-capture once; if it still looks blank, run `evaluate` on `document.body.scrollHeight` to confirm rather than assuming. This skill's screenshots are rendered correctly; false "blank" reports come from not looking at the actual image.
- **Always close the browser** when done to free resources
- **The server stays running** until explicitly closed — you can launch, do other work, then come back and continue interacting
## Troubleshooting
| Symptom | Cause | Fix |
|---------|-------|-----|
| curl exit 3 (URL malformed) | Selector contains `[...]` interpreted as curl glob | Add `-g` to every curl call |
| curl exit 52 (empty reply) | Server dead | Restart it (see Starting the Browser Server), check `/tmp/browser-server.log` |
| curl exit 28 (timeout) | Server request stuck | Should not happen anymore (8s internal timeouts); check the log |
| `Timeout ... waiting for locator` | Selector doesn't match the page | Enumerate elements with `evaluate` (see Finding Selectors) |
| Click succeeds but nothing happens | Element is covered or JS-gated | Retry with `force=true`, then screenshot to verify |
| `Missing required parameter: X` | Param in URL but not decoded properly | Move it to the body with `--data-urlencode "X=..."` |
| Stale page content after navigation | Page still loading / lazy content | `reload`, or scroll before extracting |
| Screenshot looks blank (but page is fine) | Agent assumed a render race without reading the image | `read` the actual PNG; re-capture once; check `document.body.scrollHeight` — don't assume blank |
| Server port busy | Old server instance | `kill $(lsof -ti:9876)` before starting |
## Example Session
```
User: "Help me log into my GitHub account"
Agent: "I'll open a visible browser so you can see everything. Starting the server..."
Agent: $ kill $(lsof -ti:9876) 2>/dev/null; sleep 2
Agent: $ cd .../interactive-browser && node scripts/browser-server.js 9876 /tmp/pi-browser-screenshots > /tmp/browser-server.log 2>&1 &
Agent: $ curl -s -g -X POST "http://localhost:9876/status" # verify it started
Agent: $ curl -s -g -X POST "http://localhost:9876/launch?url=https://github.com/login"
Agent: [reads screenshot, shows it to user]
Agent: "I can see the GitHub login page. What would you like to do?"
User: "Fill in my email: ducoterra@example.com"
Agent: $ curl -s -g -X POST "http://localhost:9876/fill" --data-urlencode "selector=input[name='login']" --data-urlencode "value=ducoterra@example.com"
Agent: [shows new screenshot]
Agent: "Email filled in. What next?"
User: "Click the 'Continue' button"
Agent: $ curl -s -g -X POST "http://localhost:9876/click" --data-urlencode "selector=input[name='commit']"
Agent: [shows new screenshot]
Agent: "It's asking for a password. What should I do?"
User: "I'll type the password myself. You wait."
[User types password in the visible browser]
User: "Done, click Sign in"
Agent: $ curl -s -g -X POST "http://localhost:9876/click" --data-urlencode "selector=input[data-commit-button]"
Agent: [shows new screenshot]
Agent: "You're logged in! Welcome to your dashboard."
Agent: $ curl -s -g -X POST "http://localhost:9876/close"
```
+59
View File
@@ -0,0 +1,59 @@
{
"name": "interactive-browser",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "interactive-browser",
"version": "1.0.0",
"dependencies": {
"playwright": "^1.62.1"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.62.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
"integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.62.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=20"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.62.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=20"
}
}
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"name": "interactive-browser",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"type": "commonjs",
"dependencies": {
"playwright": "^1.62.1"
}
}
@@ -0,0 +1,472 @@
#!/usr/bin/env node
/**
* Interactive Browser Server
*
* A long-running HTTP server that keeps a visible Playwright browser open
* and accepts commands via HTTP. Designed for human-in-the-loop interaction.
*
* Usage:
* node browser-server.js [port]
*
* Default port: 9876
*
* API Endpoints:
* POST /launch?url=<url> - Launch browser and navigate
* POST /go?url=<url> - Navigate to URL
* POST /reload - Reload current page
* POST /screenshot - Take screenshot
* POST /click?selector=<sel> - Click element
* POST /dblclick?selector=<sel> - Double-click element
* POST /fill?selector=<sel>&value=<val> - Fill form field
* POST /select?selector=<sel>&value=<val> - Select dropdown option
* POST /hover?selector=<sel> - Hover over element
* POST /evaluate?expr=<js> - Evaluate JS expression
* POST /get_text?selector=<sel> - Get element text
* POST /close - Close browser
*
* All responses are JSON with screenshot paths.
* Screenshots are saved to the specified output directory.
*/
const { chromium } = require('playwright');
const http = require('http');
const { URL } = require('url');
const fs = require('fs');
const path = require('path');
const os = require('os');
const PORT = parseInt(process.argv[2]) || 9876;
const OUTPUT_DIR = process.argv[3] || path.join(os.tmpdir(), 'pi-browser-screenshots');
const STATE_FILE = path.join(os.tmpdir(), 'pi-browser-state.json');
let browser = null;
let context = null;
let page = null;
let wasClosed = false;
// Ensure output directory exists
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
// Never let an unhandled async error kill the server — log it instead.
process.on('unhandledRejection', (err) => {
console.error('[unhandledRejection]', err);
});
process.on('uncaughtException', (err) => {
console.error('[uncaughtException]', err);
});
async function ensureBrowser() {
if (!browser) {
browser = await chromium.launch({
headless: false,
slowMo: 100,
args: [
'--start-maximized',
'--disable-blink-features=AutomationControlled',
],
});
context = await browser.newContext({
viewport: { width: 1280, height: 900 },
locale: 'en-US',
timezoneId: 'America/New_York',
});
page = await context.newPage();
page.on('console', (msg) => {
console.error(`[Browser Console] ${msg.type()}: ${msg.text()}`);
});
page.on('pageerror', (err) => {
console.error(`[Browser Page Error] ${err.message}`);
});
wasClosed = false;
}
}
async function saveScreenshot(page, suffix = 'screenshot') {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `${suffix}-${timestamp}.png`;
const filepath = path.join(OUTPUT_DIR, filename);
await page.screenshot({ path: filepath, fullPage: false });
return filepath;
}
async function getPageInfo(page) {
try {
return {
url: page.url(),
title: await page.title(),
};
} catch {
return { url: 'about:blank', title: '' };
}
}
function jsonResponse(res, statusCode, data) {
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data, null, 2));
}
function errorResponse(res, statusCode, message, details) {
console.error(`[Error] ${message}`);
if (details) console.error(details);
jsonResponse(res, statusCode, { status: 'error', message, details });
}
function parseBody(bodyStr) {
const params = new URLSearchParams();
if (bodyStr) {
try {
// Try JSON first
const obj = JSON.parse(bodyStr);
for (const [k, v] of Object.entries(obj)) {
params.set(k, v);
}
} catch {
// Fall back to URL-encoded
const sp = new URLSearchParams(bodyStr);
for (const [k, v] of sp) params.set(k, v);
}
}
return params;
}
async function handleRequest(req, res) {
const parsedUrl = new URL(req.url, `http://localhost:${PORT}`);
const pathname = parsedUrl.pathname;
const urlParams = parsedUrl.searchParams;
// CORS support
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
if (req.method !== 'POST') {
errorResponse(res, 405, 'Method not allowed. Use POST.');
return;
}
let body = '';
for await (const chunk of req) {
body += chunk;
}
// Merge URL params and body params (body takes precedence)
const bodyParams = parseBody(body);
const params = new URLSearchParams();
for (const [k, v] of urlParams) params.set(k, v);
for (const [k, v] of bodyParams) params.set(k, v);
try {
switch (pathname) {
case '/launch': {
const url = params.get('url');
if (!url) {
errorResponse(res, 400, 'Missing required parameter: url');
return;
}
await ensureBrowser();
console.error(`[Server] Navigating to: ${url}`);
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(1000);
const info = await getPageInfo(page);
const screenshotPath = await saveScreenshot(page, 'launch');
jsonResponse(res, 200, {
status: 'ok',
command: 'launch',
url: info.url,
title: info.title,
screenshot: screenshotPath,
});
break;
}
case '/go': {
const url = params.get('url');
if (!url) {
errorResponse(res, 400, 'Missing required parameter: url');
return;
}
await ensureBrowser();
console.error(`[Server] Navigating to: ${url}`);
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(1000);
const info = await getPageInfo(page);
const screenshotPath = await saveScreenshot(page, 'go');
jsonResponse(res, 200, {
status: 'ok',
command: 'go',
url: info.url,
title: info.title,
screenshot: screenshotPath,
});
break;
}
case '/reload': {
await ensureBrowser();
console.error('[Server] Reloading page...');
await page.reload({ waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForTimeout(1000);
const info = await getPageInfo(page);
const screenshotPath = await saveScreenshot(page, 'reload');
jsonResponse(res, 200, {
status: 'ok',
command: 'reload',
url: info.url,
title: info.title,
screenshot: screenshotPath,
});
break;
}
case '/screenshot': {
await ensureBrowser();
const screenshotPath = await saveScreenshot(page, 'screenshot');
const info = await getPageInfo(page);
jsonResponse(res, 200, {
status: 'ok',
command: 'screenshot',
url: info.url,
title: info.title,
screenshot: screenshotPath,
});
break;
}
case '/click': {
const selector = params.get('selector');
if (!selector) {
errorResponse(res, 400, 'Missing required parameter: selector');
return;
}
await ensureBrowser();
const force = params.get('force') === 'true';
console.error(`[Server] Clicking: ${selector}${force ? ' (force)' : ''}`);
await page.click(selector, { timeout: 8000, force });
await page.waitForTimeout(500);
const screenshotPath = await saveScreenshot(page, 'click');
const info = await getPageInfo(page);
jsonResponse(res, 200, {
status: 'ok',
command: 'click',
selector,
url: info.url,
title: info.title,
screenshot: screenshotPath,
});
break;
}
case '/dblclick': {
const selector = params.get('selector');
if (!selector) {
errorResponse(res, 400, 'Missing required parameter: selector');
return;
}
await ensureBrowser();
console.error(`[Server] Double-clicking: ${selector}`);
await page.dblclick(selector, { timeout: 8000 });
await page.waitForTimeout(500);
const screenshotPath = await saveScreenshot(page, 'dblclick');
const info = await getPageInfo(page);
jsonResponse(res, 200, {
status: 'ok',
command: 'dblclick',
selector,
url: info.url,
title: info.title,
screenshot: screenshotPath,
});
break;
}
case '/fill': {
const selector = params.get('selector');
const value = params.get('value');
if (!selector || value === null) {
errorResponse(res, 400, 'Missing required parameters: selector, value');
return;
}
await ensureBrowser();
console.error(`[Server] Filling "${value}" into: ${selector}`);
await page.fill(selector, value, { timeout: 8000 });
await page.waitForTimeout(300);
const screenshotPath = await saveScreenshot(page, 'fill');
const info = await getPageInfo(page);
jsonResponse(res, 200, {
status: 'ok',
command: 'fill',
selector,
value,
url: info.url,
title: info.title,
screenshot: screenshotPath,
});
break;
}
case '/select': {
const selector = params.get('selector');
const value = params.get('value');
if (!selector || !value) {
errorResponse(res, 400, 'Missing required parameters: selector, value');
return;
}
await ensureBrowser();
console.error(`[Server] Selecting "${value}" in: ${selector}`);
await page.selectOption(selector, value, { timeout: 8000 });
await page.waitForTimeout(300);
const screenshotPath = await saveScreenshot(page, 'select');
const info = await getPageInfo(page);
jsonResponse(res, 200, {
status: 'ok',
command: 'select',
selector,
value,
url: info.url,
title: info.title,
screenshot: screenshotPath,
});
break;
}
case '/hover': {
const selector = params.get('selector');
if (!selector) {
errorResponse(res, 400, 'Missing required parameter: selector');
return;
}
await ensureBrowser();
console.error(`[Server] Hovering: ${selector}`);
await page.hover(selector, { timeout: 8000 });
await page.waitForTimeout(300);
const screenshotPath = await saveScreenshot(page, 'hover');
const info = await getPageInfo(page);
jsonResponse(res, 200, {
status: 'ok',
command: 'hover',
selector,
url: info.url,
title: info.title,
screenshot: screenshotPath,
});
break;
}
case '/evaluate': {
const expr = params.get('expr');
if (!expr) {
errorResponse(res, 400, 'Missing required parameter: expr');
return;
}
await ensureBrowser();
console.error(`[Server] Evaluating: ${expr}`);
const result = await page.evaluate(expr);
const screenshotPath = await saveScreenshot(page, 'evaluate');
const info = await getPageInfo(page);
jsonResponse(res, 200, {
status: 'ok',
command: 'evaluate',
expression: expr,
result: String(result),
url: info.url,
title: info.title,
screenshot: screenshotPath,
});
break;
}
case '/get_text': {
const selector = params.get('selector');
if (!selector) {
errorResponse(res, 400, 'Missing required parameter: selector');
return;
}
await ensureBrowser();
console.error(`[Server] Getting text from: ${selector}`);
const text = await page.textContent(selector);
const screenshotPath = await saveScreenshot(page, 'get_text');
const info = await getPageInfo(page);
jsonResponse(res, 200, {
status: 'ok',
command: 'get_text',
selector,
text: text || '',
url: info.url,
title: info.title,
screenshot: screenshotPath,
});
break;
}
case '/close': {
console.error('[Server] Closing browser...');
if (browser) {
await browser.close();
}
wasClosed = true;
browser = null;
context = null;
page = null;
jsonResponse(res, 200, {
status: 'ok',
command: 'close',
message: 'Browser closed',
});
break;
}
case '/status': {
if (wasClosed || !browser) {
jsonResponse(res, 200, {
status: 'ok',
state: 'idle',
message: 'Browser is not running',
});
} else {
const info = await getPageInfo(page);
jsonResponse(res, 200, {
status: 'ok',
state: 'running',
url: info.url,
title: info.title,
});
}
break;
}
default:
errorResponse(res, 404, `Unknown endpoint: ${pathname}`);
}
} catch (err) {
errorResponse(res, 500, `Server error`, err.message);
}
}
const server = http.createServer(handleRequest);
server.listen(PORT, () => {
console.error(`Interactive Browser Server running on http://localhost:${PORT}`);
console.error(`Screenshots saved to: ${OUTPUT_DIR}`);
console.error('Press Ctrl+C to stop');
});
// Graceful shutdown
process.on('SIGINT', async () => {
console.error('\n[Server] Shutting down...');
if (browser) await browser.close();
server.close();
process.exit(0);
});
process.on('SIGTERM', async () => {
console.error('\n[Server] Shutting down...');
if (browser) await browser.close();
server.close();
process.exit(0);
});
+146
View File
@@ -0,0 +1,146 @@
---
name: new-project
description: Initialises a new Python project and its high-rigor, phased-execution roadmap for a chosen project type. Use when the user wants to start a brand-new project and asks for a professional setup with independent, test-driven phases. Branch on --type to apply the correct stack defaults — web (FastAPI + PostgreSQL 17 + Playwright E2E per user story, no external CDNs), cli (Click), library/pip (src-layout, public API, PyPI-ready), or script (argparse/typer, cron-deployable). This skill scaffolds the environment AND writes the .agents/phases roadmap; the phase-authoring skill adds phases later and the phased-execution skill runs them.
---
# New Project
You are the **Project Architect** — a senior engineer who initializes a
professional-grade Python project and designs a high-rigor, phased
implementation roadmap where **every capability drives its own independent,
test-driven phase**. You scaffold the environment, lock the architectural
anchors, and hand off a roadmap the `phased-execution` skill can run. You
never implement the phases yourself — you write them as phase directories.
You are the single source of the stack-specific rigor that used to live in the
`new-python-web`, `new-python-cli`, `new-python-pip`, and `new-python-script`
prompts. Pick the correct branch from the user's declared project type and apply
**only** that branch's mandates.
## Choosing the type
Determine `--type` from the chat context (or ask once if genuinely absent):
- `web` → FastAPI web application (default when the user says "app", "service", "API", "web").
- `cli` → Click command-line tool.
- `library` → pip/PyPI library (also accept `pip`, `package`, `lib`).
- `script` → automation script / cron job.
## Phase state in files
- `.agents/PLAN.md` — master design; **LOCKED DECISIONS** are binding.
- `.agents/phases/todo/NN_name/` — pending phase: `00_phase.md` + `NN_task.md` files.
- `.agents/phases/complete/` — finished phases (mirrors `todo/`, read-only history).
- `AGENTS.md` — project operating rules derived from this skill's mandates.
## Protocol — New Project
### Phase 1: Discovery (context first, interview only if needed)
Derive from the chat context (see below). If nothing can be derived, ask in one
message:
1. **The Vision / Intent:** what it does and the one thing a user/operator runs on day one.
2. **The User / Operator:** end-user or operator (human at terminal, cron, CI, other services) — drives verbosity, idempotency, concurrency.
3. **The "Must-Haves":** non-negotiable features, commands, or public API capabilities for v1.
4. **Type-specific extras:**
- *web* — intended end-user (UI complexity / accessibility needs).
- *cli* — the operator and the first command to run.
- *library* — consumers (internal vs public PyPI), Python versions, PyPI name, license (default MIT), minimum Python (default 3.11).
- *script* — what it automates and why an existing tool can't.
- If the chat already settles identity, challenges, and stack, **do not interview** —
proceed and report the derived scope in the final summary.
- Only interview for items genuinely missing or ambiguous, in one message, then **stop and wait**.
### Phase 2: Professional Environment Scaffolding
Use `uv` for all package management. Create the mandatory scaffolding below,
applying **only your branch's** stack section.
#### Common (all types)
- `uv` project: `pyproject.toml`, `src/<name>/` package layout, `[project.scripts]` entry point where a CLI/library applies.
- `.gitignore` that **includes `.agents/phase-sessions/` and `.agents/pipeline.log`** — but never `.agents/` itself (the phase roadmap is tracked and committed).
- Multi-stage `Containerfile` (assumes `podman`/`docker`).
- `README.md` with `uv` + configuration instructions.
- **Debugpy (dev):** include `debugpy`; a utility module checks the `DEBUGPY` env var. Default (`DEBUGPY=0`/unset) → **not** imported, minimal overhead. `DEBUGPY=1` → import and listen (e.g. port 5678) without blocking.
#### Web (`--type web`)
- Deps: `fastapi`, `alembic`, `pydantic`, `python-dotenv`; dev adds `ruff`, `pyright`, `pytest`, `pytest-cov`, `playwright`. Prefer `httpx`.
- **Database:** PostgreSQL 17 (`docker.io/postgres:17`) in `compose.yaml`; docs instruct `podman compose up -d`.
- **Auxiliary (conditional):** Valkey 9 (`docker.io/valkey/valkey:9`) if caching/sessions exist; SeaweedFS 4 (`docker.io/chrislusf/seaweedfs:4`) if file uploads exist.
- **No CDN Policy:** all JS/CSS/fonts/images must be served statically from the FastAPI app. If a static frontend is used, compile/minify it in the Containerfile builder stage (Node.js/npm), then copy into the runtime image; serve via `StaticFiles`. Nothing loads from `https://…`.
- **Testing model:** every user story drives its own independent Playwright E2E suite.
#### CLI (`--type cli`)
- Deps: `click` (LOCKED — do not consider alternatives); dev adds `debugpy`, `ruff`, `pyright`, `pytest`, `pytest-cov`.
- `@click.group()` with `context_settings={"help_option_names": ("-h", "--help"), "max_content_width": 100}` and a `--version` from `importlib.metadata`.
- Global flags (`--verbose`, `--quiet`, `--config`) on the group; per-command flags on commands. Every option `show_default=True`; enums `click.Choice`; paths `click.Path`/`click.File`; env-sourced options use `envvar=`.
- `click.argument` only where a positional is genuinely idiomatic.
- Enable Click 8 shell completion (`_TOOL_COMPLETE` pattern) and document it in the README.
- **No database by default** unless the operator use case requires it; if so, document in `PLAN.md`.
- Standard exit codes: `0` success, `1` runtime error, `2` usage error. Destructive ops need `--dry-run` + confirmation/`--force`.
#### Library (`--type library`)
- Deps: only true runtime deps; heavier/optional features behind `[project.optional-dependencies]` extras.
- `src/` layout, full `[project]` metadata (name, SemVer, description, `long_description` from README, license, authors, classifiers, `requires-python`, deps).
- Public API via `__all__` in `__init__.py`; version via `importlib.metadata`; everything else private (`_`-prefixed).
- **Full type annotations mandatory**; `pyright --strict` passes with zero errors.
- Google-style docstrings on all public objects. Choose **one** docs generator (Sphinx-autodoc or MkDocs-Material) and LOCK it in Phase 3.
- Ship `py.typed`. Provide `LICENSE` (matching chosen license).
- **No `compose.yaml` / `Containerfile`** by default — runtime deps live in `pyproject.toml`.
- CI (`.github/workflows/ci.yml`) runs `ruff`, `pyright`, `pytest --cov` (fail <90%), and `uv build` on every push/PR.
- README sections: Installation (local + PyPI), Quickstart, Development Setup, Debugging.
#### Script (`--type script`)
- Deps: `python-dotenv` if it reads config/secrets from the environment; dev adds `debugpy`, `ruff`, `pyright`, `pytest`, `pytest-cov`.
- Single-purpose → stdlib `argparse`; multiple subcommands → `typer`. LOCK the choice in Phase 3.
- Config via `.env` (python-dotenv); optional `--config` file. Never hard-code paths/credentials/env values.
- **No database by default** unless the operator use case requires it; if so, document in `PLAN.md`.
- Standard exit codes; destructive ops need `--dry-run` + confirmation/`--force`. May deploy as a cron container.
### Phase 3: Strategic Architectural Design
Design with rigor, identifying **Architectural Anchors (LOCKED DECISIONS)** with
the user, adopting from the chat where already agreed. A decision is `LOCKED`
once agreed; it cannot change without explicit permission, and the locked
anchors are the only technologies phases may use. Cover:
1. Assumptions & design principles.
2. **Architectural Anchors table:** `[COMPONENT] | [DECISION] | [RATIONALE] | [STATUS: LOCKED/PROPOSED]`.
3. High-level architecture: component breakdown and data flow.
4. **Validation/Verification Workflow:** multi-step logic ensuring high-confidence outputs (tailor to type — CLI workflow, public API capability, or user story).
5. Data model proposal (where applicable) with schema and state transitions.
6. Where applicable: state machine & background jobs, and data-ingestion strategy (no hard-coded lists).
### Phase 4: The Hand-Off (write files, don't just describe)
Create with file tools:
- **`.agents/PLAN.md`** — the master design from Phase 3 (architecture, LOCKED DECISIONS, high-level roadmap).
- **`AGENTS.md`** — initialized with: read `.agents/PLAN.md` first; follow the phased protocol in `.agents/phases/`; never modify `PLAN.md` or anything in `.agents/phases/complete/`; ask before editing `todo/`; adhere to the LOCKED DECISIONS.
- **`.agents/phases/todo/`** — sequential phase directories (`01_…/`, `02_…/`, …), each with a `00_phase.md` overview plus task files, each leaving the project launchable on its own.
- **`.agents/phases/complete/`** — create, leave empty.
Do **not** create `.agents/validate.sh` — the `phased-execution` skill installs it
from its template on first run and adapts it to the project's real checks.
- **Version control:** `git init` if the project is not a repository, then
commit the scaffold — including the whole `.agents/` tree (tracked, never
git-ignored; only `.agents/phase-sessions/` and `.agents/pipeline.log` are
ignored) — with a Conventional Commits message, always `--no-gpg-sign`.
Finish by summarizing the Architectural Anchors and how to start execution with
the `phased-execution` skill (`auto-phase.sh`).
## Scoping from chat context
Extract before asking: what the user asked for and why, explicit decisions/constraints/preferences, technologies already agreed, work already in flight, and boundaries. Treat explicit user statements as interview answers; re-ask only genuinely missing/ambiguous items, in one message, then stop and wait. In your final summary, state the derived scope (type, intent, boundaries, any new technology with its permission source) so the user can correct it.
## Strict Operational Rules
- **You scaffold and design; you do not implement the roadmap.** Phases are written as directories for the `phased-execution` skill to run, task by task.
- **Apply only your chosen type's stack mandates** — do not bleed web requirements into a CLI project, and vice versa.
- **Never** create or edit files inside `.agents/PLAN.md`, `AGENTS.md`, or `.agents/phases/complete/` after this skill hands off (the `phase-authoring` skill owns later changes).
- **No external CDNs** anywhere in a web project — all assets served from the app.
- Introduce any technology outside the LOCKED DECISIONS only with explicit user permission, recorded in `PLAN.md`.
+206
View File
@@ -0,0 +1,206 @@
---
name: new-service
description: "Creates a new service in the deployment repo from the template. Use when the user wants to add a new service (e.g. 'add a new service', 'create a service', 'new service for X'). Asks for the service name, domain, and target group, then scaffolds the full directory structure with certbot, nginx, app (postgres 17 + valkey + app container), route53, and a README with deployment instructions. Includes Requires= directives, health checks, and proper env templates."
---
# New Service
You are the **Infrastructure Scaffolder** — a senior DevOps engineer who creates
new services in the Ansible deployment repo. You scaffold the complete directory
structure from the template, substituting the user's choices for all placeholders.
You always include Postgres 17 and Valkey by default, and you produce a README
with exact deployment commands.
## Context
The deployment repo lives at `/var/home/ducoterra/Deployments`. The template
resides at `/var/home/ducoterra/Deployments/template/` and contains skeleton
playbooks for the four-stage deploy: `init`, `certbot`, `nginx`, `app`.
### Template structure
```
template/
├── app/ # Quadlet containers + env files + app playbook
│ ├── playbook.yaml
│ ├── {foobar}.network
│ ├── {foobar}-app.container
│ ├── {foobar}-postgres.container
│ ├── {foobar}-valkey.container
│ ├── {foobar}.env.j2
│ └── {foobar}-postgres.env.j2
│
# Note: Container naming varies by service (e.g. litellm uses litellm-db,
# immich uses immich-database). The template uses {foobar}-postgres as the
# default. Adjust names consistently across all files if changing.
├── certbot/playbook.yaml
├── nginx/playbook.yaml
├── route53/records.json
└── README.md
```
### Variables
| Placeholder | Meaning | Example |
|-------------|---------|---------|
| `{foobar}` | Service name (kebab-case) | `litellm`, `openwebui`, `my-app` |
| `{domain}` | Top-level domain | `reeseapps`, `stackexpected`, `emmaleaf`, `wfc` |
### Targets
| Target | Location | Notes |
|--------|----------|-------|
| `stackexpected` | `stackexpected/{foobar}/` | Own services |
| `reeseapps` | `reeseapps/{foobar}/` | Personal/family services |
| `emmaleaf` | `emmaleaf/{foobar}/` | Third-party staging on KVM VM |
| `wfc` | `wfc/{foobar}/` | Third-party staging on KVM VM |
### Deploy order
1. `init` — system setup (dnf packages, svc user, SSH keys)
2. `certbot` — TLS certificates (DNS-01 challenge, requires AWS creds)
3. `nginx` — reverse proxy with SSL termination
4. `app` — deploy Quadlet containers (postgres, valkey, app)
### Certbot playbook pattern
The certbot playbook uses `install_certbot` role. The `domains` list contains
the service domain. For services with wildcard or multiple subdomains, add all
of them. The `ntfy_*` vars are optional — set to `""` to disable notifications.
### Nginx playbook pattern
The nginx playbook uses `install_nginx` role. The `nginx_http_conf` contains
the full server block. The `proxy_pass` port defaults to `8080` but should be
adjusted based on the app's actual listening port.
### Route53 pattern
The `records.json` contains a CNAME record pointing to `home.reeselink.com.`
This is a default — adjust if the service uses a different target (e.g. an
AWS ALB, a different VM, etc.).
## Protocol
### Phase 1: Gather Requirements
Extract from chat context first. Ask only what's genuinely missing, in one
message:
1. **Service name** (`{foobar}`) — kebab-case, unique within the target.
2. **Target** (`{domain}`) — `stackexpected`, `reeseapps`, `emmaleaf`, or `wfc`.
3. **Domain suffix** — usually matches the target (e.g. `reeseapps` → `reeseapps.com`), but ask if the user wants something different.
4. **App listening port** — what port does the app listen on internally? (default: `8080`)
5. **Extra domains** — any additional domains/subdomains that need certificates? (e.g. `api.{service}.{domain}.com`)
6. **Route53 target** — what should the CNAME point to? Default: `home.reeselink.com.`
7. **VM RAM** — how much memory? Default: 2048MB. Heavier services (immich) need 8192MB.
Rules:
- Treat explicit user statements as answers. Do not re-ask.
- If the user says "add a service called litellm for reeseapps", you already
have the name and target — only ask for port, extra domains, and route53 target.
- If nothing can be derived, ask all six items in one message, then stop.
### Phase 2: Scaffold the Directory
Create the full directory tree under the chosen target:
```
{target}/{foobar}/
├── app/
│ ├── playbook.yaml
│ ├── {foobar}.network
│ ├── {foobar}-app.container
│ ├── {foobar}-postgres.container
│ ├── {foobar}-valkey.container
│ ├── {foobar}.env.j2
│ └── {foobar}-postgres.env.j2
├── certbot/
│ └── playbook.yaml
├── nginx/
│ └── playbook.yaml
├── route53/
│ └── records.json
└── README.md
```
Use the template files as sources and substitute `{foobar}` and `{domain}`
throughout. Key customizations:
#### `app/playbook.yaml`
- Use the template's `app/playbook.yaml` (which already includes postgres, valkey, app containers).
- If the user has extra quadlet files to copy (e.g. custom service timers), add them to `copy_svc_quadlet_files`.
- If the service exposes a port directly (not via nginx), add `expose_ports` with the port number and include the `configure_firewalld` role (see immich playbook).
- **Service names** in `restart_svc_services`: use the container name without `.service` suffix (consistent with most services; litellm uses `.service` suffix but that's an anomaly).
#### `certbot/playbook.yaml`
- Set `domains` to include `{foobar}.{domain}.com` plus any extra domains.
- Set `account_name` to `{domain}`.
- Include `ntfy_url`, `ntfy_topic`, `ntfy_tags` for notifications (optional — default to empty/disabled).
#### `nginx/playbook.yaml`
- Set `server_name` to `{foobar}.{domain}.com`.
- Set `proxy_pass` port to the user's specified port (default `8080`).
- Include the full server block with SSL, proxy headers, and client_max_body_size.
- **Timeouts**: Services like litellm/immich use `send_timeout`, `proxy_read_timeout`, etc. with high values (1800s). Only add these if the user requests them.
- **client_max_body_size**: Default to `100m`. Services that handle large uploads (immich) use `1000m`.
#### `route53/records.json`
- Set `Name` to `{foobar}.{domain}.com`.
- Set `ResourceRecords.Value` to the user's route53 target (default `home.reeselink.com.`).
#### `app/{foobar}.env.j2`
- Include `DATABASE_URL` using `postgresql://` scheme (not `postgresql+psycopg://`) pointing to `{foobar}-postgres:5432/{foobar}`.
- Include `VALKEY_URL` using `redis://` scheme pointing to `{foobar}-valkey:6379/0`, OR separate `REDIS_HOST`/`REDIS_PORT` vars (litellm style).
- Include placeholder secrets using `{{ lookup('env', 'VAR_NAME') }}`.
- Keep `APP_ENVIRONMENT=development` as a default.
#### `app/{foobar}-postgres.env.j2`
- Set `POSTGRES_USER={foobar}`, `POSTGRES_DB={foobar}`.
- Set `POSTGRES_PASSWORD` from `{{ lookup('env', 'APP_DB_PASSWORD') }}`.
- **Note**: Some services (e.g. litellm) hardcode credentials inline in the container file using `Environment=` instead of an env file. The template uses the env file approach (more flexible, matches immich).
#### `README.md`
- Generate a README with:
- Service name and description (use a meaningful title, e.g. "reeseapps-litellm")
- Deployment commands (exact `ansible-playbook` invocations)
- Secret locations (pass paths — suggest `pass {target}/{foobar}/...`)
- Inventory entry example
- SSH config example
- **VM RAM**: Default to 2048MB. Heavier services (immich) need 8192MB — ask the user if unsure.
### Phase 3: Validation
Before finishing, verify:
1. All `{foobar}` placeholders are replaced in every file.
2. All `{domain}` placeholders are replaced in every file.
3. The `DATABASE_URL` in the app env uses the correct postgres container name.
4. The `VALKEY_URL` in the app env uses the correct valkey container name.
5. The certbot domains list includes the primary domain.
6. The nginx `server_name` matches the certbot domain.
7. The nginx `proxy_pass` port matches the user's specified port.
8. The route53 record name matches the certbot domain.
9. The playbook `hosts` references are consistent (`{domain}_{foobar}`).
### Phase 4: Report
Summarize what was created:
- Full directory tree
- Key configuration values (service name, domain, port, targets)
- Exact commands to deploy (init → certbot → nginx → app)
- Suggested pass secret paths
- Any decisions that need user attention (e.g. "adjust the proxy_pass port if your app listens on a different port")
## Strict Operational Rules
- **Always include Postgres 17 and Valkey** — they are part of the template.
- **Never hard-code secrets** — all secrets use `{{ lookup('env', 'VAR_NAME') }}` in `.env.j2` files.
- **Use kebab-case** for all service names.
- **Do not modify** the template files themselves — create new instances under the target.
- **Do not run** any playbooks or commands — only scaffold files.
- If the user wants a different database (e.g. MySQL), **ask before deviating** from the Postgres 17 default.
- If the user doesn't want Valkey, **still include it** but leave it unconfigured in the app env (the user can remove it later).
- The app container **must have `Requires=`** for postgres and valkey containers to ensure correct startup order.
- Use `postgresql://` (not `postgresql+psycopg://`) for DATABASE_URL — it's the universal scheme.
+217
View File
@@ -0,0 +1,217 @@
---
name: ntfy
description: "Send push notifications via a self-hosted ntfy server. Use when phases complete, tasks finish, the LLM needs to ask the user a question, or any scenario where the user needs to be notified."
---
# ntfy — Push Notifications
Send push notifications to the user's device via ntfy. Use this skill whenever the user needs to be notified — phase completions, task finishes, questions requiring user input, errors, or any other event warranting attention.
## Configuration — `~/.env/pi-ntfy.env`
All ntfy credentials are stored in `~/.env/pi-ntfy.env`. If this file doesn't exist, the ntfy skill won't work.
### Required Variables
| Variable | Description | Example |
|----------|-------------|---------|
| `NTFY_URL` | ntfy server URL | `https://ntfy.reeseapps.com` |
| `NTFY_TOKEN` | Authorization token | `tk_your_token_here` |
| `NTFY_TOPIC` | Default topic name | `pi` |
### Setting Up the Config File
Create the config file with your ntfy credentials:
```bash
mkdir -p ~/.env
cat > ~/.env/pi-ntfy.env << 'EOF'
NTFY_URL=https://ntfy.reeseapps.com
NTFY_TOKEN=tk_your_token_here
NTFY_TOPIC=pi
EOF
```
Then restrict permissions so only your user can read it:
```bash
chmod 600 ~/.env/pi-ntfy.env
```
### Loading Credentials
Before making requests, source the file:
```bash
source ~/.env/pi-ntfy.env
```
Or use it inline:
```bash
. ~/.env/pi-ntfy.env
```
### Checking Credentials
Verify the file is set up correctly:
```bash
source ~/.env/pi-ntfy.env
echo "URL: $NTFY_URL"
echo "Token: $NTFY_TOKEN"
echo "Topic: $NTFY_TOPIC"
```
## Priority Levels
Choose the priority based on urgency:
| Priority | Header Value | Use Case |
|----------|-------------|----------|
| Min | `X-Priority: 1` | Informational, low importance |
| Low | `X-Priority: 2` | Routine updates |
| Default | `X-Priority: 3` | Normal notifications |
| High | `X-Priority: 4` | Important, user should notice |
| Emergency| `X-Priority: 5` | Critical, must act immediately |
## Tags & Emojis
Use `X-Tags` to add emojis and labels. Separate multiple tags with commas:
```bash
-H "X-Tags: heavy_check_mark,done" # ✔️ done
-H "X-Tags: rotating_light,urgent" # 🚨 urgent
-H "X-Tags: warning,caution" # ⚠️ caution
-H "X-Tags: tada,celebration" # 🎉 celebration
-H "X-Tags: loudspeaker,announce" # 📢 announcement
-H "X-Tags: question,ask" # ❓ question
-H "X-Tags: skull,error" # 💀 error
-H "X-Tags: computer,dev" # 💻 development
-H "X-Tags: facepalm,issue" # 🤦 issue
```
Common emoji tags: `tada`, `heavy_check_mark`, `rotating_light`, `warning`, `loudspeaker`, `question`, `skull`, `computer`, `facepalm`, `arrow_forward`, `one`, `-1`, `partying_face`, `triangular_flag_on_post`, `no_entry`, `cd`.
## Markdown Support
Set `X-Markdown: yes` (or `Content-Type: text/markdown`) to enable rich formatting:
```bash
-d "Phase complete! Here's what was done:
- **Feature A** — implemented
- **Feature B** — tested
- **Feature C** — deployed
See [details](https://example.com) for more."
```
Supported: **bold**, *italics*, `[links](url)`, `inline code`, ``` code blocks ```, lists, blockquotes, headings, horizontal rules.
## Click Actions
Open a URL when the notification is tapped:
```bash
-H "X-Click: https://example.com/dashboard"
```
Common patterns:
- `https://...` — opens in browser
- `mailto:user@example.com` — opens mail app
- `ntfy://ntfy.reeseapps.com/pi` — opens ntfy app directly
## Action Buttons
Add interactive buttons to the notification (JSON array):
```bash
-H "X-Actions: [{'id': '1', 'label': 'View Details', 'uri': 'https://example.com'}]"
```
Format: `[{"id": "1", "label": "Button Text", "uri": "https://url"}]`
## Message Structure Examples
### Phase Complete
```bash
. ~/.env/pi-ntfy.env
curl -X POST "${NTFY_URL}/${NTFY_TOPIC}" \
-H "Authorization: Bearer ${NTFY_TOKEN}" \
-H "X-Title: Phase 3 Complete" \
-H "X-Priority: 4" \
-H "X-Tags: heavy_check_mark,phase-3" \
-H "X-Markdown: yes" \
-d "**Phase 3: Authentication** completed successfully.\n\n- 12 tests passed\n- 94% coverage\n- 0 regressions"
```
### Task Finished
```bash
curl -X POST "${NTFY_URL}/${NTFY_TOPIC}" \
-H "Authorization: Bearer ${NTFY_TOKEN}" \
-H "X-Title: Task Done" \
-H "X-Priority: 3" \
-H "X-Tags: computer,task-complete" \
-H "X-Markdown: yes" \
-d "Task completed: implemented user login flow"
```
### Question for User
```bash
curl -X POST "${NTFY_URL}/${NTFY_TOPIC}" \
-H "Authorization: Bearer ${NTFY_TOKEN}" \
-H "X-Title: Question" \
-H "X-Priority: 4" \
-H "X-Tags: question,needs-input" \
-H "X-Markdown: yes" \
-H "X-Click: ${NTFY_URL}/${NTFY_TOPIC}" \
-d "Should I proceed with the database migration? Reply 'yes' or 'no'."
```
### Error / Failure
```bash
curl -X POST "${NTFY_URL}/${NTFY_TOPIC}" \
-H "Authorization: Bearer ${NTFY_TOKEN}" \
-H "X-Title: Error" \
-H "X-Priority: 5" \
-H "X-Tags: skull,error" \
-H "X-Markdown: yes" \
-d "**Phase 2 failed!**\n\n`Exit code: 1`\n\n```\nError: connection refused\n```\n\nCheck logs for details."
```
### General Announcement
```bash
curl -X POST "${NTFY_URL}/${NTFY_TOPIC}" \
-H "Authorization: Bearer ${NTFY_TOKEN}" \
-H "X-Title: Update" \
-H "X-Priority: 2" \
-H "X-Tags: loudspeaker,update" \
-H "X-Markdown: yes" \
-d "The agent is now working on phase 4 of 7."
```
## Quick Reference
| Field | Header | Example |
|-------|--------|---------|
| URL | (in request) | `${NTFY_URL}/${NTFY_TOPIC}` |
| Token | (in Authorization) | `Bearer ${NTFY_TOKEN}` |
| Title | `X-Title` | `X-Title: Phase Complete` |
| Priority | `X-Priority` | `X-Priority: 5` (1–5) |
| Tags/Emojis | `X-Tags` | `X-Tags: tada,done` |
| Markdown | `X-Markdown` | `X-Markdown: yes` |
| Click URL | `X-Click` | `X-Click: https://...` |
| Actions | `X-Actions` | `X-Actions: [...]` |
| Body | `-d` | `**Message**` |
## Guidelines
1. **Always use markdown** (`X-Markdown: yes`) for readable, formatted messages.
2. **Choose priority wisely** — use 4–5 for questions and errors, 2–3 for routine updates.
3. **Pick appropriate tags** to convey the notification type at a glance.
4. **Keep titles concise** (1–5 words) — they appear in the notification shade.
5. **Include relevant details** in the body: phase numbers, task names, error messages.
6. **Add click actions** when the user might want to check more details.
7. **Use emergency priority (5)** only for critical failures requiring immediate attention.
+66 -45
View File
@@ -1,28 +1,36 @@
---
name: phase-authoring
description: The required entry point for ANY new work on a .agent/phases/ project. Whenever the user requests a new feature, a bug fix, a refactor, or any other code change to a project that has the .agent/phases/ structure, call this skill FIRST to capture the work as a phase file — never implement the change directly in code. Also use it when the user explicitly asks to add, write, or draft a phase, extend the phase roadmap, or start a new phased-execution project (Protocol B scaffolds fresh projects). Uses the Phase Architect protocol (the /new-phase and /new-project prompts, as a skill); scopes the phase from the user's chat context first and only interviews for information that is genuinely missing. This skill writes phase files; the phased-execution skill runs them.
description: The required entry point for ANY new work on a .agents/phases/ project. Whenever the user requests a new feature, a bug fix, a refactor, or any other code change to a project that has the .agents/phases/ structure, call this skill FIRST to capture the work as a phase directory with task files — never implement the change directly in code. Also use it when the user explicitly asks to add, write, or draft a phase, extend the phase roadmap, or start a new phased-execution project (Protocol B scaffolds fresh projects). Uses the Phase Architect protocol (the /new-phase and /new-project prompts, as a skill); scopes the phase from the user's chat context first and only interviews for information that is genuinely missing. This skill writes phase directories (a 00_phase.md overview plus task files); the phased-execution skill runs them, task by task.
---
# Phase Authoring
You are the **Phase Architect** — a senior engineer responsible for
extending a phased-execution project with new, independently-executable
phases. You design and write phase files; the `phased-execution` skill
executes them. You never implement phase code yourself, and you never
modify the master plan.
phases. You design and write phase directories — a `00_phase.md` overview
plus small task files; the `phased-execution` skill executes them, task by
task. You never implement phase code yourself, and you never modify the
master plan.
Phase state lives in files, not chat:
- `.agent/PLAN.md` — master plan; **LOCKED DECISIONS** are binding
- `.agent/phases/todo/NN_name.md` — pending phases (sort order = execution order)
- `.agent/phases/complete/` — finished phases (read-only history)
- `.agents/PLAN.md` — master plan; **LOCKED DECISIONS** are binding
- `.agents/phases/todo/NN_name/` — a pending phase: `00_phase.md` overview + `NN_task.md` task files (task sort order = execution order)
- `.agents/phases/todo/NN_name.md` — legacy single-file phase (still valid; migrate or split it)
- `.agents/phases/complete/` — finished phases, mirroring the todo/ layout (read-only history)
Legacy flat phases (`todo/NN_name.md`) are still executed as a single unit.
`bash scripts/migrate-phases-to-tasks.sh [project-root]` (resolve `scripts/`
against this skill's directory) converts them to the directory layout —
mechanical, safe mid-pipeline; splitting a wrapped phase's inline task list
into real task files is this skill's job.
## When to invoke this skill
This skill is the **first stop** for any request to do work on a phased project. If the user asks for a new feature, bug fix, improvement, refactor, or any other code change — regardless of phrasing ("add X", "fix Y", "update Z", "it's broken when…", "make it so that…") — do **not** start editing application code. Convert the request into a phase file with this skill; the `phased-execution` skill is the only path from a phase to code (it runs in a fresh subprocess behind the validation gate).
This skill is the **first stop** for any request to do work on a phased project. If the user asks for a new feature, bug fix, improvement, refactor, or any other code change — regardless of phrasing ("add X", "fix Y", "update Z", "it's broken when…", "make it so that…") — do **not** start editing application code. Convert the request into a phase directory (overview + task files) with this skill; the `phased-execution` skill is the only path from a phase to code (it runs each task in a fresh subprocess behind the validation gate).
- **Phased project** (`.agent/phases/todo/` exists) → **Protocol A**: create the phase for the requested work. This is the default path for feature, bug, and change requests — the user does not need to mention "phase" at all.
- **Fresh project** (no `.agent/` structure) and the user wants a phased project (asks for it explicitly, or the chat context makes clear the phased workflow is wanted) → **Protocol B**: scaffold the project and its phase roadmap.
- **Phased project** (`.agents/phases/todo/` exists) → **Protocol A**: create the phase for the requested work. This is the default path for feature, bug, and change requests — the user does not need to mention "phase" at all.
- **Fresh project** (no `.agents/` structure) and the user wants a phased project (asks for it explicitly, or the chat context makes clear the phased workflow is wanted) → **Protocol B**: scaffold the project and its phase roadmap.
- **Not phased and no sign of phased intent** → this skill does not apply; do the work normally, and if the work is substantial, suggest the `convert-to-phased` skill.
- Only if the user **explicitly** asks to bypass the phase workflow should code be edited directly — in that case note that the change skips the phase's test and validation gates.
@@ -51,10 +59,10 @@ Rules:
Before writing anything, you must:
1. Read `.agent/PLAN.md` — project goals, architecture, and **LOCKED DECISIONS**.
1. Read `.agents/PLAN.md` — project goals, architecture, and **LOCKED DECISIONS**.
2. Read `AGENTS.md` if present — project rules may add requirements (e.g. one user story per phase with a dedicated E2E suite per story, mandatory commit conventions, or a `validate.sh` gate).
3. Run `bash scripts/phase-status.sh` (resolve `scripts/` against this skill's directory) to list `todo/` and `complete/` and compute the next free number `NN`.
4. Read every file in `.agent/phases/complete/` to understand what has already been built, and review the remaining `todo/` files to avoid overlap. Read the most recent completed phase file(s) and match their **local formatting conventions** (section names, story-mapping lines, E2E/commit blocks) while keeping the required sections below.
3. Run `bash scripts/phase-status.sh` (resolve `scripts/` against this skill's directory) to list the phases and their per-task state, and compute the next free number `NN`.
4. Read the `00_phase.md` files in `.agents/phases/complete/*/` to understand what has already been built, and review the remaining `todo/` phase directories to avoid overlap. Read the most recent completed phase files and match their **local formatting conventions** (section names, story-mapping lines, E2E/commit blocks) while keeping the required sections below.
### Phase 2: Scope the New Phase (context first, interview only if needed)
@@ -68,43 +76,56 @@ Derive from the chat context (see "Scoping from chat context" above):
- If the chat context answers all four, **do not interview** — proceed
straight to Phase 3 and report the derived scope in the final summary.
- If some are missing or ambiguous, ask **only those** — in one message —
and **stop and wait** for the answers before writing the file. Never
and **stop and wait** for the answers before writing the phase. Never
re-ask what the chat already settled.
- New technology: if the user already proposed or approved it in chat, that
**is** explicit permission — record it in the phase file and note that
**is** explicit permission — record it in the phase overview (`00_phase.md`) and note that
`PLAN.md`'s anchor table needs their sign-off (this skill never edits
`PLAN.md`). If it was not discussed in chat, you must ask for — and
receive — explicit permission before proceeding.
### Phase 3: Design & Create the Phase File
### Phase 3: Design & Create the Phase
Create **exactly one** new file at `.agent/phases/todo/NN_name.md`, where
`NN` comes from Phase 1 (next free number, counting `todo/` and
`complete/` together) and `name` is a short `snake_case` description. Use
`assets/phase-template.md` as the skeleton. The file must contain:
Create **exactly one** new phase directory at `.agents/phases/todo/NN_name/`,
where `NN` comes from Phase 1 (next free number, counting `todo/` and
`complete/` together) and `name` is a short `snake_case` description. It
contains the phase overview and the phase's task files:
1. **Objective** — a 1–3 sentence statement of what the phase delivers.
2. **Dependencies** — the phases (by file name) that must be completed first.
3. **Tasks** — specific, granular, ordered steps with file-level detail where applicable.
4. **Testing & Quality (mandatory)** — requires unit and integration tests for all new logic, and states the success criterion: the phase is "Complete" only when the test suite runs successfully and achieves **>90% code coverage** on new/modified code.
5. **Completion Criteria** — observable checks (commands to run, endpoints to hit, artifacts to exist) that tell the next agent the phase is done.
1. **`00_phase.md`** — use `assets/phase-template.md` as the skeleton. It must contain:
- **Objective** — a 1–3 sentence statement of what the phase delivers.
- **Dependencies** — the phases (by directory name) that must be completed first.
- **Tasks** — the ordered index of this phase's task files (one line each).
- **Testing & Quality (mandatory)** — requires unit and integration tests for all new logic, and states the success criterion: the phase is "Complete" only when the test suite runs successfully and achieves **>90% code coverage** on new/modified code.
- **Completion Criteria** — observable checks (commands to run, endpoints to hit, artifacts to exist) that the phase's final pass verifies; include any phase-level verification blocks (e.g. a dedicated E2E or contract suite).
2. **Task files `01_…`, `02_…`, …** — use `assets/task-template.md` as the skeleton. Each must contain:
- **Objective** — 1–2 sentences on what the task delivers.
- **Work** — specific, ordered steps with file-level detail where applicable.
- **Testing & Quality** — the unit/integration tests this task's logic requires; coverage >90% on its new/modified code.
- **Completion Criteria** — observable checks that tell the next agent the task is done.
**Task sizing:** each task must be small and quick — one focused change, a
small file set, roughly ≤30 minutes of executor work; a phase typically holds
2–8 tasks. Tasks execute in file-name order, each in its own fresh subprocess
with the validation gate after every task, so later tasks may build on
earlier ones within the phase.
**Design Mandates:**
- **Independent Viability:** the phase must leave the project functional and launchable on its own once complete.
- **Architectural Anchors:** use only the technologies in the LOCKED DECISIONS of `.agent/PLAN.md`. Never introduce new technology without explicit permission.
- **Architectural Anchors:** use only the technologies in the LOCKED DECISIONS of `.agents/PLAN.md`. Never introduce new technology without explicit permission.
- **No Regressions:** the phase must not alter the behavior of completed phases.
- **Executable in isolation:** an agent that sees only the repository and this file — no chat history, no follow-ups — must be able to finish the phase. No hidden assumptions.
- **Executable in isolation:** an agent that sees only the repository, the phase overview, the completed task files, and one task file — no chat history, no follow-ups — must be able to finish that task. No hidden assumptions.
### Final Output
Confirm the path and number of the created file, and summarize its
objective, dependencies, and completion criteria. If you skipped the
interview, open the summary with the scope you derived from the chat
context (intent, dependencies, boundaries, and any new technology with its
permission source) so the user can correct it. Remind the user it can be
executed with the `phased-execution` skill (`run-phase.sh NN_name.md` for a
single phase, `auto-phase.sh` for the full pipeline).
Confirm the path and number of the created phase directory and its task
list, and summarize the phase's objective, dependencies, and completion
criteria. If you skipped the interview, open the summary with the scope you
derived from the chat context (intent, dependencies, boundaries, and any new
technology with its permission source) so the user can correct it. Remind the
user it can be executed with the `phased-execution` skill (`run-task.sh
NN_name/01_…` for a single task, `run-phase.sh NN_name` for the phase,
`auto-phase.sh` for the full pipeline).
## Protocol B — New project with a phased roadmap
@@ -132,7 +153,7 @@ Derive from the chat context (see "Scoping from chat context" above):
- Use `uv` for all package management.
- **Mandatory Dependencies:** `python-dotenv` (production); `debugpy`, `ruff`, `pyright`, `pytest`, `pytest-cov` (dev).
- **Web Projects:** add `fastapi`, `alembic`, `pydantic`; prefer `httpx`.
- **Scaffold Files:** a comprehensive `.gitignore` (must include `.agent/` and `.agent/phase-sessions/`), a multi-stage `Containerfile` (assuming `podman`/`docker`), and a `README.md` with `uv` and configuration instructions.
- **Scaffold Files:** a comprehensive `.gitignore` (must include `.agents/phase-sessions/` and `.agents/pipeline.log` — but **never** `.agents/` itself: the planning tree is tracked and committed), a multi-stage `Containerfile` (assuming `podman`/`docker`), and a `README.md` with `uv` and configuration instructions.
### Phase 3: Strategic Architectural Design
@@ -153,12 +174,12 @@ phases may use. The design must cover:
Use file tools to create (not just describe):
- **`.agent/PLAN.md`** — the master design from Phase 3 (architecture, locked decisions, high-level roadmap).
- **`AGENTS.md`** — initialized with: always read `.agent/PLAN.md` first; follow the phased protocol in `.agent/phases/`; never modify `.agent/PLAN.md` or anything in `.agent/phases/complete/`; ask the user before editing files in `.agent/phases/todo/`; strictly adhere to the **LOCKED DECISIONS**.
- **`.agent/phases/todo/`** — sequential, granular phase files (`01_…`, `02_…`, …) that each contain the sections and design mandates from Protocol A, Phase 3, and each leaves the project launchable on its own.
- **`.agent/phases/complete/`** — create the directory, leave it empty.
- **`.agents/PLAN.md`** — the master design from Phase 3 (architecture, locked decisions, high-level roadmap).
- **`AGENTS.md`** — initialized with: always read `.agents/PLAN.md` first; follow the phased protocol in `.agents/phases/`; never modify `.agents/PLAN.md` or anything in `.agents/phases/complete/`; ask the user before editing files in `.agents/phases/todo/`; strictly adhere to the **LOCKED DECISIONS**.
- **`.agents/phases/todo/`** — sequential phase directories (`01_…/`, `02_…/`, …), each with a `00_phase.md` overview and its task files per Protocol A, Phase 3, and each leaving the project launchable on its own.
- **`.agents/phases/complete/`** — create the directory, leave it empty.
Do **not** create `.agent/validate.sh` — the `phased-execution` skill
Do **not** create `.agents/validate.sh` — the `phased-execution` skill
installs it from its template on first run and it must be adapted to the
project's real checks.
@@ -167,8 +188,8 @@ execution with `phased-execution` (`auto-phase.sh`).
## Strict Operational Rules
- **Work requests become phase files.** When this skill was invoked because of a feature, bug, or change request, the deliverable is the phase file — not code. Do not edit application code during this invocation; the `phased-execution` skill does that from the phase file.
- **Never** modify `.agent/PLAN.md`, `AGENTS.md`, or any file in `.agent/phases/complete/`.
- **Never** modify existing files in `.agent/phases/todo/`; if one needs updating, ask the user for permission first.
- Create exactly **one** phase file per invocation in Protocol A. If the request covers multiple phases, propose the ordered split and ask the user to confirm it, then create only the first — the rest follow in later invocations (or a Protocol B roadmap pass if the project is new).
- Numbering: `NN` is the next free number after the highest existing file, counting `todo/` and `complete/` together. Never reuse or collide a number.
- **Work requests become phase directories.** When this skill was invoked because of a feature, bug, or change request, the deliverable is the phase directory (overview + task files) — not code. Do not edit application code during this invocation; the `phased-execution` skill does that from the task files.
- **Never** modify `.agents/PLAN.md`, `AGENTS.md`, or any file in `.agents/phases/complete/`.
- **Never** modify existing phase directories or their files in `.agents/phases/todo/`; if one needs updating, ask the user for permission first.
- Create exactly **one** phase directory (overview + task files) per invocation in Protocol A. If the request covers multiple phases, propose the ordered split and ask the user to confirm it, then create only the first — the rest follow in later invocations (or a Protocol B roadmap pass if the project is new).
- Numbering: phase `NN` is the next free number after the highest existing entry (directory or legacy file), counting `todo/` and `complete/` together; task `NN` is per-phase (`01`…). Never reuse or collide a number.
+4 -4
View File
@@ -1,6 +1,6 @@
# Phase {{NN}} — {{Short Title}}
**Story:** `{{.agent/user_stories/<story>.md or "n/a"}}`
**Story:** `{{.agents/user_stories/<story>.md or "n/a"}}`
**Context:** `{{PLAN.md sections / files this phase builds on}}`
## Objective
@@ -11,13 +11,13 @@
{{or "— (none)"}}
## Tasks
1. `{{path/to/file}}` — {{specific change, file-level detail}}
2. `{{path/to/file}}` — {{specific change}}
1. `01_{{short_name}}.md` — {{one-line summary}}
2. `02_{{short_name}}.md` — {{one-line summary}}
## Testing & Quality
- Unit/integration: {{tests required for all new logic — name the behaviors to cover}}
- Coverage: **>90%** on new/modified code
{{project additions, e.g. a dedicated Playwright E2E suite run in isolation}}
{{project additions, e.g. a dedicated Playwright E2E suite run in isolation by this phase's final pass}}
## Completion Criteria
- [ ] {{observable check: command to run / endpoint to hit / artifact to exist}}
+19
View File
@@ -0,0 +1,19 @@
# Task {{NN}} — {{Short Title}}
**Phase:** `{{NN_phase}}` · **Story:** `{{.agents/user_stories/<story>.md or "n/a"}}`
## Objective
{{1–2 sentences: what this task delivers}}
## Work
1. `{{path/to/file}}` — {{specific change, file-level detail}}
2. `{{path/to/file}}` — {{specific change}}
## Testing & Quality
- Unit/integration: {{tests required for this task's logic}}
- Coverage: **>90%** on this task's new/modified code
## Completion Criteria
- [ ] {{observable check: command to run / endpoint to hit / artifact to exist}}
- [ ] full test suite green
- [ ] no behavior change in completed work
@@ -0,0 +1,72 @@
#!/usr/bin/env bash
# migrate-phases-to-tasks.sh — convert the flat phase-file layout to the
# phase-directory + task-file layout.
#
# Each numeric-prefixed flat phase file in {todo,complete}/ is wrapped into a
# directory of the same name with the file renamed to 00_phase.md:
#
# todo/03_api.md → todo/03_api/00_phase.md
# complete/01_init.md → complete/01_init/00_phase.md
#
# The harness still executes wrapped phases: their inline task lists are
# picked up by the phase's 00_phase.md final pass, so this is safe
# mid-pipeline. Splitting a wrapped todo phase into real task files is the
# phase-authoring skill's job, not this script's. Already-migrated phases are
# left alone; a flat file colliding with an existing same-name directory is
# reported and skipped.
#
# Usage: bash migrate-phases-to-tasks.sh [project-root]
# project-root defaults to the nearest ancestor containing
# .agents/phases/todo or agent/phases/todo (older projects).
set -euo pipefail
root="${1:-}"
if [[ -n "$root" ]]; then
[[ -d "$root" ]] || { echo "✗ ERROR: $root is not a directory" >&2; exit 1; }
else
root="$(pwd)"
while :; do
if [[ -d "$root/.agents/phases/todo" || -d "$root/agent/phases/todo" ]]; then break; fi
[[ "$root" == "/" ]] && { echo "✗ ERROR: no phases/todo found at or above $(pwd)" >&2; exit 1; }
root="$(dirname "$root")"
done
fi
if [[ -d "$root/.agents/phases" ]]; then
phases="$root/.agents/phases"
elif [[ -d "$root/agent/phases" ]]; then
phases="$root/agent/phases"
else
echo "✗ ERROR: no phases directory under $root" >&2
exit 1
fi
echo "project root: $root"
echo "phases: $phases"
echo
moved=0 skipped=0
for d in todo complete; do
[[ -d "$phases/$d" ]] || continue
for f in "$phases/$d"/*.md; do
[[ -e "$f" ]] || continue
base="$(basename "$f" .md)"
[[ "$base" =~ ^[0-9] ]] || continue
if [[ -d "$phases/$d/$base" ]]; then
echo "⚠ skip: $d/$base.md — directory $d/$base/ already exists (conflict)" >&2
skipped=$((skipped + 1))
continue
fi
mkdir -p "$phases/$d/$base"
mv -f "$f" "$phases/$d/$base/00_phase.md"
echo " $d/$base.md → $d/$base/00_phase.md"
moved=$((moved + 1))
done
done
echo
echo "migrated: $moved phase file(s) → 00_phase.md directory layout; skipped: $skipped"
if (( moved > 0 )); then
echo "wrapped todo phases now run as a single final pass; use the phase-authoring"
echo "skill to split their inline task lists into real task files."
fi
+106 -26
View File
@@ -1,9 +1,13 @@
#!/usr/bin/env bash
# phase-status.sh — phase pipeline state for the Phase Architect.
# phase-status.sh — phase/task pipeline state for the Phase Architect.
#
# Finds the project root (nearest ancestor with .agent/phases/todo), prints
# the todo/ and complete/ phase listings, and computes the next free phase
# number NN (counting both directories together, zero-padded to 2 digits).
# Finds the project root (nearest ancestor with .agents/phases/todo), prints
# the todo/ and complete/ phase listings with per-task state, the next free
# phase number NN (counting both directories together, zero-padded to 2
# digits), and the next pending unit in pipeline order.
#
# Phase layout: todo/NN_name/ holds 00_phase.md (overview) + NN_task.md task
# files; a legacy flat todo/NN_name.md is a single-unit phase.
#
# Usage: bash phase-status.sh # from anywhere in the project tree
@@ -11,36 +15,112 @@ set -euo pipefail
root="$(pwd)"
while :; do
if [[ -d "$root/.agent/phases/todo" ]]; then break; fi
[[ "$root" == "/" ]] && { echo "✗ ERROR: no .agent/phases/todo found at or above $(pwd)" >&2; exit 1; }
if [[ -d "$root/.agents/phases/todo" ]]; then break; fi
[[ "$root" == "/" ]] && { echo "✗ ERROR: no .agents/phases/todo found at or above $(pwd)" >&2; exit 1; }
root="$(dirname "$root")"
done
list() {
local d="$root/.agent/phases/$1" f found=0
for f in "$d"/*.md; do
[[ -e "$f" ]] || continue
found=1
printf ' %s\n' "$(basename "$f")"
done
if (( ! found )); then printf ' (empty)\n'; fi
}
phases="$root/.agents/phases"
# --- todo: per-task state ------------------------------------------------------
echo "todo:"
found=0
for entry in "$phases/todo/"*; do
[[ -e "$entry" ]] || continue
name="$(basename "$entry")"
[[ "$name" =~ ^[0-9] ]] || continue
found=1
if [[ -d "$entry" ]]; then
# union of this phase's files across todo/ and complete/ (completed units
# move to complete/, so pending = present in todo/, done = in complete/)
comp_dir="$phases/complete/$name"
comp_files=""
if [[ -d "$comp_dir" ]]; then comp_files="$(ls -1 "$comp_dir" 2>/dev/null || true)"; fi
names="$( { ls -1 "$entry" 2>/dev/null || true; printf '%s\n' "$comp_files"; } | grep -E '\.md$' | sort -u || true)"
total=0 done_n=0
while IFS= read -r t; do
[[ -n "$t" ]] || continue
total=$((total + 1))
[[ -f "$phases/complete/$name/$t" ]] && done_n=$((done_n + 1))
done <<< "$names"
printf ' %s/ (%s of %s done)\n' "$name" "$done_n" "$total"
# tasks first (sorted), then 00_phase.md as the final pass
while IFS= read -r t; do
[[ -n "$t" ]] || continue
[[ "$t" == 00_phase.md ]] && continue
if [[ -f "$phases/complete/$name/$t" ]]; then
printf ' [x] %s\n' "$t"
else
printf ' [ ] %s\n' "$t"
fi
done <<< "$names"
if grep -qxF '00_phase.md' <<< "$names"; then
if [[ -f "$phases/complete/$name/00_phase.md" ]]; then
printf ' [x] 00_phase.md (final pass)\n'
else
printf ' [ ] 00_phase.md (final pass)\n'
fi
fi
else
printf ' %s (legacy single-file phase)\n' "$name"
fi
done
(( found )) || echo " (empty)"
# --- complete -------------------------------------------------------------------
echo "complete:"
found=0
for entry in "$phases/complete/"*; do
[[ -e "$entry" ]] || continue
name="$(basename "$entry")"
[[ "$name" =~ ^[0-9] ]] || continue
found=1
if [[ -d "$entry" ]]; then
n=0
for f in "$entry"/*.md; do [[ -e "$f" ]] && n=$((n + 1)); done
printf ' %s/ (%s files)\n' "$name" "$n"
else
printf ' %s (legacy single-file phase)\n' "$name"
fi
done
(( found )) || echo " (empty)"
# --- next free phase number -----------------------------------------------------
max=0
for d in todo complete; do
for f in "$root/.agent/phases/$d"/*.md; do
[[ -e "$f" ]] || continue
n="$(basename "$f" .md)"
if [[ "$n" =~ ^([0-9]+) ]]; then
for entry in "$phases/$d/"*; do
[[ -e "$entry" ]] || continue
name="$(basename "$entry" .md)"
if [[ "$name" =~ ^([0-9]+) ]]; then
n=$((10#${BASH_REMATCH[1]}))
if (( n > max )); then max=$n; fi
(( n > max )) && max=$n
fi
done
done
echo "next phase number: $(printf '%02d' $((max + 1)))"
echo "project root: $root"
echo "todo:"
list todo
echo "complete:"
list complete
echo "next number: $(printf '%02d' $((max + 1)))"
# --- next pending unit -----------------------------------------------------------
next=""
for entry in "$phases/todo/"*; do
[[ -e "$entry" ]] || continue
name="$(basename "$entry")"
[[ "$name" =~ ^[0-9] ]] || continue
if [[ -d "$entry" ]]; then
# first task present in todo/ and not yet in complete/ (mirrors the harness)
t=""
while IFS= read -r f; do
[[ -n "$f" ]] || continue
[[ "$f" =~ ^[0-9] && "$f" == *.md ]] || continue
[[ "$f" == 00_phase.md ]] && continue
[[ -f "$phases/complete/$name/$f" ]] && continue
t="$f"; break
done < <(ls -1 "$entry" 2>/dev/null || true)
if [[ -n "$t" ]]; then next="$name/$t"; break; fi
if [[ -f "$entry/00_phase.md" && ! -f "$phases/complete/$name/00_phase.md" ]]; then
next="$name/00_phase.md"; break
fi
elif [[ -f "$entry" ]]; then
next="$name"; break
fi
done
echo "next unit: ${next:-(none — pipeline complete)}"
+91 -29
View File
@@ -1,23 +1,30 @@
---
name: phased-execution
description: Runs the .agent/phases/ phased-execution pipeline (ported from opencode's next-phase/auto-phase commands). Use when the user asks to run the next phase, run all phases, run the phase pipeline, or check pipeline status. Each phase executes in a separate pi subprocess so this chat's context stays small.
description: Runs the .agents/phases/ phased-execution pipeline (ported from opencode's next-phase/auto-phase commands). Use when the user asks to run the next task, run the next phase, run all phases, run the phase pipeline, or check pipeline status. Each task executes in a separate pi subprocess so this chat's context stays small.
---
# Phased Execution
Phase state lives in files, not chat:
- `.agent/PLAN.md` — master plan; LOCKED DECISIONS are binding
- `.agent/phases/todo/NN_name.md` — pending phases (alphanumerical sort = execution order)
- `.agent/phases/complete/` — finished phases
- `.agent/reports/` — per-phase executor reports, stderr, and validation logs
- `.agent/validate.sh` — the pass/fail gate for every phase
- `.agents/PLAN.md` — master plan; LOCKED DECISIONS are binding
- `.agents/phases/todo/NN_name/` — a pending phase: `00_phase.md` (objective, dependencies, task index, testing & quality, completion criteria) plus `NN_task.md` task files (task sort order = execution order)
- `.agents/phases/todo/NN_name.md` — legacy single-file phase (still executable as one unit)
- `.agents/phases/complete/` — finished phases; mirrors the `todo/` layout (completed task files and the phase overview move here)
- `.agents/reports/<phase>/<task>.a<N>.{md,err,validate}` — per-task executor reports, stderr, and validation logs (legacy phases: `.agents/reports/<phase>.a<N>.*`)
- `.agents/validate.sh` — the pass/fail gate, run after **every task**
The scripts run each phase in a **separate pi process** (fresh context) with
bounded fixer retries. A phase only moves to `complete/` after the child exits
0, the child's stream ends with a clean final report, **and**
`.agent/validate.sh` passes. This chat only dispatches and relays results —
do not implement phase code yourself; that is what the subprocess is for.
The unit of execution is the **task**: each task runs in a **separate pi
process** (fresh context) with bounded fixer retries. A task only moves to
`complete/` after the child exits 0, the child's stream ends with a clean
final report, **and** `.agents/validate.sh` passes. When all of a phase's tasks
are done, `00_phase.md` runs as the phase's **final pass** (remaining inline
work + completion criteria + phase-level verification); moving it completes
the phase and is the `PHASE_COMMIT` commit point (one atomic commit — code +
phase files + reports — pushed right after when the repo has a remote;
children never commit; see Commits below). This chat only dispatches
and relays results — do not implement task code yourself; that is what the
subprocesses are for.
## Live display
@@ -26,22 +33,30 @@ the terminal: tool calls, assistant text, `◐ thinking…` / `◑ thought for N
indicators, yellow `⧉ compacting context` lines (these can take minutes —
not a hang), and provider auto-retry notices. If the child dies mid-flush and
the stream loses the final message, the report is recovered from the
child's session file (`.agent/phase-sessions/`), so a completed phase is
child's session file (`.agents/phase-sessions/`), so a completed phase is
never lost to a truncated stream.
## Commands
(Resolve `scripts/` against this skill's directory.)
Run the next phase, or a specific one:
Run the next task, or a specific one:
```bash
bash scripts/run-phase.sh # first pending phase
bash scripts/run-phase.sh 03_api.md # specific phase (warns if out of order)
bash scripts/run-task.sh # first pending task
bash scripts/run-task.sh 03_api/02_routes.md # specific task (warns if out of order)
```
Run the whole pipeline — every pending phase, in order, stopping at the first
phase that fails after all retries:
Run one phase to completion — all of its remaining tasks, then its
`00_phase.md` final pass:
```bash
bash scripts/run-phase.sh # next phase with pending work
bash scripts/run-phase.sh 03_api # specific phase (warns if out of order)
```
Run the whole pipeline — every pending task, in order (phase order, then task
order), stopping at the first task that fails after all retries:
```bash
bash scripts/auto-phase.sh
@@ -51,17 +66,55 @@ Re-running `auto-phase.sh` after a failure continues where it stopped.
## After a run
Exit codes: `0` = success (or nothing to run), `1` = phase failed after all
attempts / no pending phases error, `130`/`143` = interrupted (Ctrl+C / SIGTERM).
Exit codes: `0` = success (or nothing to run), `1` = task failed after all
attempts, or the phase commit/push failed, `130`/`143` = interrupted
(Ctrl+C / SIGTERM).
Failures always print a `✗ ERROR:` line with the last error output — if the
script's output looks like it ended abruptly, re-run it; the failed executor's
session is resumed automatically (retries continue the child's own session,
keeping its work).
Relay to the user: the phase name, its executor report (printed at the end of
the script output), and the validation outcome. On failure, point the user at
`.agent/reports/<phase>.a*.{md,err,validate}` — the script also prints a ready
to run `pi --session … -c “…”` command to continue the failed session manually.
Relay to the user: the task (or phase) name, its executor report (printed at
the end of the script output), and the validation outcome. On failure, point
the user at `.agents/reports/<phase>/<task>.a*.{md,err,validate}` (legacy
phases: `.agents/reports/<phase>.a*.*`) — the script also prints a ready to run
`pi --session … -c “…”` command to continue the failed session manually.
## Commits
Children never commit — their prompts forbid `git add`/`git commit`
(overriding any project “commit per phase” instruction), which is what keeps
the commit deterministic instead of intermittent. By default
(`PHASE_COMMIT=1`) the harness makes **ONE atomic commit per completed
phase** at its commit point: the phase's code changes, the
`todo/`→`complete/` file move, and the executor reports, together.
`PHASE_COMMIT=0` opts out — the phase is then left uncommitted with a loud
warning, never silently.
- **Scoped staging**: a snapshot of the worktree taken at the phase's first
unit (`.agents/phase-sessions/dirty-<phase>`, gitignored runtime storage)
is subtracted, so the owner's pre-existing uncommitted work is left alone;
pipeline runtime artifacts (`phase-sessions/`, `pipeline.log`) are never
staged. The commit output prints exactly what went in.
- **Verified**: the moved phase file (and the executor report, unless the
project gitignores reports) is checked against the index *before*
committing — the artifacts that used to go missing silently.
- **Loud on failure**: a commit failure prints a `✗ ERROR:` block with the
git error and the exact hand-fix command, and **stops the run**. The phase
stays complete (the work passed validation) — re-running will not
re-execute it, and a later phase's commit will not sweep the miss in: fix
the commit first, then re-run.
- Subject: `PHASE_COMMIT_SUBJECT` (default `phase: {{PHASE}}`); the
executor's final report becomes the commit body. `--no-gpg-sign` is always
passed.
- **Pushed**: with `PHASE_PUSH=1` (default) the commit is pushed right after
it is made — to the branch's upstream when one is set, otherwise
`git push -u <first remote> <branch>`. A repo with no remote skips the
push with a notice (the commit stays local). A push failure gets the same
`✗ ERROR:` contract as a commit failure: the commit is local and safe, the
run stops, and the next phase's push sweeps the unpushed commit in once
the push works. `PHASE_PUSH=0` opts out — each phase commit is left local
with a loud notice.
## Configuration (environment variables)
@@ -70,20 +123,29 @@ to run `pi --session … -c “…”` command to continue the failed session ma
| `MAX_FIX_ATTEMPTS` | `3` | Fixer retries per phase |
| `PHASE_MODEL` | session default | `--model` for child executors (e.g. `anthropic/claude-sonnet-4-5`) |
| `PHASE_THINKING` | session default | `--thinking` level for child executors |
| `PHASE_COMMIT` | `0` | `1` = `git commit --no-gpg-sign` after each passing phase |
| `PHASE_COMMIT` | `1` | `1` (default) = the harness commits each completed **phase** as one atomic commit (code changes + the file move + executor reports) when its `00_phase.md` final pass passes (legacy: when its file moves). `0` = opt out: the phase is left uncommitted with a loud warning. See Commits above |
| `PHASE_COMMIT_SUBJECT` | `phase: {{PHASE}}` | Commit subject template for `PHASE_COMMIT=1` (`{{PHASE}}` = the phase directory name); the executor's final report becomes the commit body |
| `PHASE_PUSH` | `1` | `1` (default) = push each phase commit right after it is made (upstream when set, else `git push -u <first remote> <branch>`); no remote = skip with a notice; a failure stops the run (the commit stays local — the next phase's push sweeps it in). `0` = leave commits local with a loud notice. See Commits above |
| `PI_TRUST` | `0` | `1` = pass `--approve` (load project `.pi/` settings/skills into children) |
| `FRESH_FIX` | `0` | `1` = fixer retries start fresh instead of resuming the failed session |
| `QUIET` | `0` | `1` = suppress live progress display (reports are still written) |
| `PHASE_NOTIFY` | `1` | `0` = disable ntfy push notifications. When on and `~/.env/pi-ntfy.env` exists, a notification is sent after every unit completes (`task` for each task file, `phase` for a phase's final pass) |
## Setup notes
- First run creates `.agent/validate.sh` from `assets/validate.sh` if missing.
- First run creates `.agents/validate.sh` from `assets/validate.sh` if missing.
It must be adapted to the project's real checks — it is the authoritative
quality gate.
- Phase files are created by the `/to-phase`, `/audit-create`, `/new-project`,
and `/new-python-*` prompt templates.
- Child executor sessions are kept in `.agent/phase-sessions/`; add it to
`.gitignore` if the project is versioned.
- Phase directories are created by the `phase-authoring` skill or the
`/to-phase`, `/audit-create`, `/new-project`, and `/new-python-*` prompt
templates.
- Legacy flat phase files (`todo/NN_name.md`) are still executed as a single
unit; `phase-authoring`'s `migrate-phases-to-tasks.sh` converts them to the
directory layout (the phase's final pass then picks up any inline task list).
- Child executor sessions are kept in `.agents/phase-sessions/` (plus
`pipeline.log` in `.agents/`); if the project is versioned, git-ignore those
runtime artifacts only — `.agents/` itself is tracked and committed.
(The per-phase worktree snapshot `dirty-<phase>` is always kept there too.)
- If you keep non-skill markdown (e.g. a `README.md`) in a skills directory
(like `~/.pi/agent/skills/`), pi warns “description is required” for it.
Add a `.gitignore` in that directory listing the file — pi's skill scanner
+5 -4
View File
@@ -1,11 +1,11 @@
You are a phase executor in a phased build pipeline. You run in a fresh, isolated context; the harness manages phase files, retries, and final validation.
Target phase file: `.agent/phases/todo/{{PHASE}}`
Target phase file: `.agents/phases/todo/{{PHASE}}`
## Steps
1. Read `.agent/PLAN.md` — project goals, architecture, and **LOCKED DECISIONS** (binding; never introduce technology outside them).
1. Read `.agents/PLAN.md` — project goals, architecture, and **LOCKED DECISIONS** (binding; never introduce technology outside them).
2. Read `AGENTS.md` if present.
3. Read every file in `.agent/phases/complete/` so your work stays architecturally consistent with what is already built.
3. Read the completed phase files so your work stays architecturally consistent with what is already built: every `00_phase.md` in `.agents/phases/complete/*/`, plus any legacy flat phase files directly in `.agents/phases/complete/`.
4. Read the target phase file and complete **every** task in it, in order.
5. Write the unit and integration tests required by the phase's Testing & Quality section. Do not omit parts of the code to inflate coverage.
6. Run the project's full test suite and linter. If anything fails — including the phase's coverage criterion — fix it and re-run until green.
@@ -13,7 +13,8 @@ Target phase file: `.agent/phases/todo/{{PHASE}}`
## Rules
- Work only on the target phase; never start work from other files in `todo/`.
- Do **not** move, rename, or edit the phase file, other files in `.agent/phases/todo/`, `.agent/PLAN.md`, or anything in `.agent/phases/complete/`. The harness moves the phase file on success.
- Do **not** move, rename, or edit the phase file, other files in `.agents/phases/todo/`, `.agents/PLAN.md`, or anything in `.agents/phases/complete/`. The harness moves the phase file on success.
- Do **not** run `git add` or `git commit` — leave every change in the working tree. The harness commits this completed phase (code, phase files, and reports) atomically after this pass passes and pushes it when the repo has a remote; this overrides any project instruction to commit per phase.
- Do not assume the code is correct; fix any errors you find while testing.
- Leave the repository functional when you finish.
@@ -0,0 +1,27 @@
You are the phase-completion executor in a phased build pipeline. You run in a fresh, isolated context; the harness manages phase and task files, retries, and final validation.
Phase overview: `.agents/phases/todo/{{PHASE}}/00_phase.md`
## Steps
1. Read `.agents/PLAN.md` — project goals, architecture, and **LOCKED DECISIONS** (binding; never introduce technology outside them).
2. Read `AGENTS.md` if present.
3. Read the phase overview `.agents/phases/todo/{{PHASE}}/00_phase.md`.
4. Check the phase's task index against `.agents/phases/complete/{{PHASE}}/`: if any task is missing from there, the overview itself still carries work (a legacy phase with an inline task list) — implement that remaining work now, in order, including its tests.
5. Otherwise every task is done: this is the final verification pass. Verify **each** completion criterion in the phase overview (commands to run, endpoints to hit, artifacts to exist), and execute any phase-level verification blocks it defines (e.g. a dedicated E2E or contract test suite).
6. Run the project's full test suite and linter. If anything fails — including the phase's coverage criterion or a completion criterion — fix it and re-run until green.
7. If you find defects in previously completed work (failing tests, lint errors, bugs), fix those as part of this pass.
## Rules
- Never start work from other files in `todo/`.
- Do **not** move, rename, or edit the phase overview, other files in `.agents/phases/todo/`, `.agents/PLAN.md`, or anything in `.agents/phases/complete/`. The harness moves the phase file on success.
- Do **not** run `git add` or `git commit` — leave every change in the working tree. The harness commits this completed phase (code, phase files, and reports) atomically after this pass passes and pushes it when the repo has a remote; this overrides any project instruction to commit per phase.
- Do not assume the code is correct; fix any errors you find while testing.
- Leave the repository functional when you finish.
## Final response
When everything is green, reply with a report of **at most 15 lines**:
- What was implemented or verified (short bullet list)
- Test / lint / coverage results (exact commands and outcomes)
- Completion criteria: each one with its outcome
- Notable decisions or deviations
- The next pending phase, if any
@@ -0,0 +1,27 @@
You are a task executor in a phased build pipeline. You run in a fresh, isolated context; the harness manages phase and task files, retries, and final validation.
Target task file: `.agents/phases/todo/{{PHASE}}/{{TASK}}`
## Steps
1. Read `.agents/PLAN.md` — project goals, architecture, and **LOCKED DECISIONS** (binding; never introduce technology outside them).
2. Read `AGENTS.md` if present.
3. Read the phase overview `.agents/phases/todo/{{PHASE}}/00_phase.md` — objective, dependencies, testing & quality mandate, and completion criteria.
4. Read this phase's completed task files (`.agents/phases/complete/{{PHASE}}/*.md`, excluding `00_phase.md`) and the `00_phase.md` overviews of other completed phases (`.agents/phases/complete/*/00_phase.md`) so your work stays architecturally consistent with what is already built.
5. Read the target task file and complete it fully, in order.
6. Write the unit and integration tests required by the task. Do not omit parts of the code to inflate coverage.
7. Run the project's full test suite and linter. If anything fails — including the task's coverage criterion — fix it and re-run until green.
8. If you find defects in previously completed work (failing tests, lint errors, bugs), fix those as part of this task.
## Rules
- Work only on the target task; never start the next task or work from other files in `todo/`.
- Do **not** move, rename, or edit the task file, `00_phase.md`, other files in `.agents/phases/todo/`, `.agents/PLAN.md`, or anything in `.agents/phases/complete/`. The harness moves the task file on success.
- Do **not** run `git add` or `git commit` — leave every change in the working tree. The harness makes ONE atomic commit per completed phase (code, phase files, and reports together) when the phase completes and pushes it when the repo has a remote; this overrides any project instruction to commit per task.
- Do not assume the code is correct; fix any errors you find while testing.
- Leave the repository functional when you finish.
## Final response
When everything is green, reply with a report of **at most 15 lines**:
- What was implemented (short bullet list)
- Test / lint / coverage results (exact commands and outcomes)
- Notable decisions or deviations
- The next pending task, if any
+2 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# .agent/validate.sh — validation gate for the phased-execution pipeline.
# .agents/validate.sh — validation gate for the phased-execution pipeline.
#
# A phase is only moved to .agent/phases/complete/ if this script exits 0.
# A phase is only moved to .agents/phases/complete/ if this script exits 0.
# Adapt the checks below to this project's real test suite, linter, and
# coverage floor, then commit the result.
set -uo pipefail
+28 -20
View File
@@ -1,39 +1,47 @@
#!/usr/bin/env bash
# auto-phase.sh — run the full phased pipeline, no LLM in the loop.
#
# Processes every file in .agent/phases/todo/ in alphanumerical order.
# Each phase runs in its own pi process (fresh context); on failure the
# executor's session is resumed for up to MAX_FIX_ATTEMPTS fixer rounds.
# A phase moves to .agent/phases/complete/ only after the child exits 0
# AND .agent/validate.sh passes. Stops at the first phase that cannot be
# completed — re-run this script to continue where it stopped.
# Processes every pending task in .agents/phases/todo/ in pipeline order
# (phase order, then task order within each phase). Each task runs in its
# own pi process (fresh context); .agents/validate.sh runs after EVERY task.
# On failure the executor's session is resumed for up to MAX_FIX_ATTEMPTS
# fixer rounds. A task moves to .agents/phases/complete/ only after the
# child exits 0 AND validation passes; a phase completes when its
# 00_phase.md final pass succeeds (legacy phases: when their file moves).
# Stops at the first task that cannot be completed — re-run this script to
# continue where it stopped.
#
# Env: see SKILL.md (MAX_FIX_ATTEMPTS, PHASE_MODEL, PHASE_THINKING,
# PHASE_COMMIT, PI_TRUST, FRESH_FIX).
# PHASE_COMMIT, PHASE_PUSH, PI_TRUST, FRESH_FIX).
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib.sh"
# Make interruptions visible: state stays in .agent/phases/todo, and the
# Make interruptions visible: state stays in .agents/phases/todo, and the
# failed executor's session is still resumable on the next run.
trap 'echo; echo "✗ ERROR: interrupted (SIGINT) — ${phase:-the pipeline} is left in $PHASE_TODO/; re-run to continue where it stopped" >&2; exit 130' INT
trap 'echo; echo "✗ ERROR: interrupted (SIGTERM) — ${phase:-the pipeline} is left in $PHASE_TODO/; re-run to continue where it stopped" >&2; exit 143' TERM
trap 'echo; echo "✗ ERROR: interrupted (SIGINT) — ${unit:-the pipeline} is left in $PHASE_TODO/; re-run to continue where it stopped" >&2; exit 130' INT
trap 'echo; echo "✗ ERROR: interrupted (SIGTERM) — ${unit:-the pipeline} is left in $PHASE_TODO/; re-run to continue where it stopped" >&2; exit 143' TERM
cd "$(find_root)" || die "no .agent/phases/todo found in this or parent directories (run /to-phase or /audit-create first)"
cd "$(find_root)" || die "no .agents/phases/todo found in this or parent directories (run /to-phase or /audit-create first)"
build_pi_args
delivered=()
while phase="$(next_phase)"; do
[[ -n "$phase" ]] || break
if ! execute_phase "$phase"; then
echo "✗ ERROR: pipeline stopped — $phase FAILED after $MAX_FIX_ATTEMPTS attempts" >&2
echo " fix the issues above, then re-run this script to continue where it stopped" >&2
while unit="$(next_unit)"; do
[[ -n "$unit" ]] || break
if ! execute_unit "$unit"; then
# execute_unit printed the failure detail (task failure: errors, logs,
# resume command — or phase commit/push failure: the hand-fix command).
echo "✗ ERROR: pipeline stopped — $unit did not complete (see the error output above)" >&2
echo " re-run this script to continue where it stopped" >&2
exit 1
fi
delivered+=("$phase")
delivered+=("$unit")
# Notify after every unit completes: a phase-end (00_phase.md / legacy file)
# announces the phase; a normal task file announces the task.
if is_phase_end "$unit"; then notify_task "$unit" phase; else notify_task "$unit" task; fi
done
echo
echo "✓ pipeline done — ${#delivered[@]} phase(s) delivered this run: ${delivered[*]:-none}"
remaining="$(ls -1 "$PHASE_TODO" 2>/dev/null | grep -cE '^[0-9]' || true)"
echo " remaining in $PHASE_TODO/: $remaining"
echo "✓ pipeline done — ${#delivered[@]} task(s) delivered this run: ${delivered[*]:-none}"
remaining="$(count_units)"
echo " remaining in $PHASE_TODO/: $remaining task(s)"
+432 -76
View File
@@ -1,45 +1,148 @@
#!/usr/bin/env bash
# lib.sh — shared logic for the phased-execution skill.
# Sourced by run-phase.sh and auto-phase.sh. Not meant to be run directly.
# Sourced by run-task.sh, run-phase.sh, and auto-phase.sh. Not meant to be run directly.
#
# Phase state lives in files, not chat context:
# .agent/PLAN.md master plan, LOCKED DECISIONS (binding)
# .agent/phases/todo/ pending phases, NN_name.md, sorted = execution order
# .agent/phases/complete/ finished phases
# .agent/reports/ per-phase executor reports, stderr, validation logs
# .agent/phase-sessions/ child pi session files (resumable fixers)
# .agents/PLAN.md master plan, LOCKED DECISIONS (binding)
# .agents/phases/todo/NN_name/ pending phase: 00_phase.md (overview) + NN_task.md task files
# .agents/phases/todo/NN_name.md legacy single-file phase (still executable)
# .agents/phases/complete/ finished phases — mirrors the todo/ layout
# .agents/reports/ per-task executor reports, stderr, validation logs
# .agents/phase-sessions/ child pi session files (resumable fixers)
#
# The pass/fail gate is .agent/validate.sh. A phase only moves to complete/
# after the child executor exits 0 AND validation passes.
# The unit of execution is the TASK: each task file runs in its own pi
# subprocess (fresh context) with bounded fixer retries, and
# .agents/validate.sh runs after EVERY task. A unit only moves to complete/
# after the child exits 0, the child's stream ends with a clean final
# report, and validation passes. When all of a phase's tasks are done,
# 00_phase.md runs as the phase's final pass (any remaining inline work +
# completion criteria + phase-level verification); moving it completes the
# phase and is the commit point. Children never commit — by default
# (PHASE_COMMIT=1) the harness makes ONE atomic commit per completed phase
# (code changes + the file move + the executor reports), scoped so the
# owner's pre-existing worktree changes are left alone, and pushes it when
# the repo has a remote (PHASE_PUSH=1; see commit_phase / push_phase).
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
EXECUTOR_PROMPT_FILE="$SKILL_DIR/assets/executor-prompt.md"
TASK_EXECUTOR_PROMPT_FILE="$SKILL_DIR/assets/task-executor-prompt.md"
PHASE_FINAL_PROMPT_FILE="$SKILL_DIR/assets/phase-final-prompt.md"
PHASE_TODO=".agent/phases/todo"
PHASE_DONE=".agent/phases/complete"
PHASE_REPORTS=".agent/reports"
PHASE_SESSIONS=".agent/phase-sessions"
PHASE_TODO=".agents/phases/todo"
PHASE_DONE=".agents/phases/complete"
PHASE_REPORTS=".agents/reports"
PHASE_SESSIONS=".agents/phase-sessions"
MAX_FIX_ATTEMPTS="${MAX_FIX_ATTEMPTS:-3}"
warn() { echo "⚠ $*" >&2; }
die() { echo "✗ ERROR: $*" >&2; exit 1; }
# --- project root -------------------------------------------------------------
# Walk up from $PWD to the nearest directory containing .agent/phases/todo.
# Walk up from $PWD to the nearest directory containing .agents/phases/todo.
find_root() {
local d
d="$(pwd)"
while :; do
if [[ -d "$d/.agent/phases/todo" ]]; then printf '%s\n' "$d"; return 0; fi
if [[ -d "$d/.agents/phases/todo" ]]; then printf '%s\n' "$d"; return 0; fi
[[ "$d" == "/" ]] && return 1
d="$(dirname "$d")"
done
}
# --- phase selection ----------------------------------------------------------
# First pending phase (alphanumerical sort), or empty when none remain.
next_phase() {
( cd "$PHASE_TODO" 2>/dev/null && ls -1 | grep -E '^[0-9]' | sort | head -n1 ) || true
# --- unit selection -------------------------------------------------------------
# A unit is the smallest schedulable piece of work, referenced relative to
# .agents/phases/todo/:
# directory phase → each task file "NN_name/NN_task.md" (sort order), then
# the phase overview "NN_name/00_phase.md" as the final pass
# legacy flat → the phase file itself, "NN_name.md"
# Pending phase entries in todo/ (phase directories or legacy .md files), in execution order.
phase_entries() {
( cd "$PHASE_TODO" 2>/dev/null && ls -1 | grep -E '^[0-9]' | sort ) || true
}
# First pending unit of one phase entry, or empty when the phase is done.
phase_next_unit() {
local p="$1" t
if [[ -d "$PHASE_TODO/$p" ]]; then
t="$(ls -1 "$PHASE_TODO/$p" 2>/dev/null | grep -E '^[0-9].*\.md$' | grep -vE '^00_phase\.md$' | sort | head -n1)"
if [[ -n "$t" ]]; then printf '%s/%s\n' "$p" "$t"; return 0; fi
if [[ -f "$PHASE_TODO/$p/00_phase.md" ]]; then printf '%s/00_phase.md\n' "$p"; return 0; fi
elif [[ -f "$PHASE_TODO/$p" ]]; then
printf '%s\n' "$p"
fi
return 0
}
# Next pending unit in the whole pipeline, or empty when everything is done.
next_unit() {
local p u
for p in $(phase_entries); do
u="$(phase_next_unit "$p")"
if [[ -n "$u" ]]; then printf '%s\n' "$u"; return 0; fi
done
return 0
}
# Count of pending units across all phases (task files + 00_phase.md + legacy files).
count_units() {
local p n=0
for p in $(phase_entries); do
if [[ -d "$PHASE_TODO/$p" ]]; then
n=$(( n + $(ls -1 "$PHASE_TODO/$p" 2>/dev/null | grep -cE '\.md$') ))
elif [[ -f "$PHASE_TODO/$p" ]]; then
n=$(( n + 1 ))
fi
done
printf '%s\n' "$n"
}
# Name used in reports/sessions: "NN_name__NN_task" (dir phases) or "NN_name" (flat).
unit_base() {
local u="${1%.md}"
printf '%s\n' "${u//\//__}"
}
# Phase name for a unit (both forms).
unit_phase() {
local u="${1%.md}"
printf '%s\n' "${u%%/*}"
}
# Reports directory for a unit: per-phase subdir for dir phases, flat otherwise.
unit_report_dir() {
local u="$1"
if [[ "$u" == */* ]]; then
printf '%s/%s\n' "$PHASE_REPORTS" "$(unit_phase "$u")"
else
printf '%s\n' "$PHASE_REPORTS"
fi
}
# Report/log path for a unit attempt: <dir>/<base>.a<attempt>.<ext>.
unit_report() {
local u="$1" attempt="$2" ext="$3"
printf '%s/%s.a%s.%s\n' "$(unit_report_dir "$u")" "$(unit_base "$u")" "$attempt" "$ext"
}
# Phase-end unit (the commit point): 00_phase.md, or the whole file for legacy.
is_phase_end() {
local u="$1"
if [[ "$u" == */* ]]; then [[ "${u##*/}" == "00_phase.md" ]]; else return 0; fi
}
# Move a completed unit from todo/ to complete/ (same relative path).
move_unit() {
local u="$1" p t
if [[ "$u" == */* ]]; then
p="${u%%/*}"; t="${u##*/}"
mkdir -p "$PHASE_DONE/$p"
mv -f "$PHASE_TODO/$u" "$PHASE_DONE/$p/$t"
if [[ -z "$(ls -A "$PHASE_TODO/$p" 2>/dev/null)" ]]; then rmdir "$PHASE_TODO/$p"; fi
else
mkdir -p "$PHASE_DONE"
mv -f "$PHASE_TODO/$u" "$PHASE_DONE/$u"
fi
}
# --- child pi arguments -------------------------------------------------------
@@ -89,38 +192,52 @@ recover_report() {
}
# --- prompts ------------------------------------------------------------------
# First-attempt prompt for a unit. Directory tasks render the task executor
# prompt; the phase overview renders the phase-final prompt; legacy flat
# phase files render the original phase executor prompt.
first_prompt() {
local phase="$1"
[[ -f "$EXECUTOR_PROMPT_FILE" ]] || die "missing $EXECUTOR_PROMPT_FILE"
sed "s|{{PHASE}}|$phase|g" "$EXECUTOR_PROMPT_FILE"
local unit="$1" p t
if [[ "$unit" == */* ]]; then
p="${unit%%/*}"; t="${unit##*/}"
if [[ "$t" == "00_phase.md" ]]; then
[[ -f "$PHASE_FINAL_PROMPT_FILE" ]] || die "missing $PHASE_FINAL_PROMPT_FILE"
sed "s|{{PHASE}}|$p|g" "$PHASE_FINAL_PROMPT_FILE"
else
[[ -f "$TASK_EXECUTOR_PROMPT_FILE" ]] || die "missing $TASK_EXECUTOR_PROMPT_FILE"
sed "s|{{PHASE}}|$p|g; s|{{TASK}}|$t|g" "$TASK_EXECUTOR_PROMPT_FILE"
fi
else
[[ -f "$EXECUTOR_PROMPT_FILE" ]] || die "missing $EXECUTOR_PROMPT_FILE"
sed "s|{{PHASE}}|$unit|g" "$EXECUTOR_PROMPT_FILE"
fi
}
fix_prompt() {
local errors="$1"
{
echo "Your previous attempt at this phase was rejected by the harness."
echo "The failures from the last attempt are below. Review them, fix the code, and re-run the full test suite and linter until everything is green. Do not start other phases' work."
echo "Your previous attempt at this task was rejected by the harness."
echo "The failures from the last attempt are below. Review them, fix the code, and re-run the full test suite and linter until everything is green. Do not start other tasks' work."
echo
echo "Failure output (may be truncated):"
echo '```'
printf '%s\n' "$errors" | tail -c 6000
echo '```'
echo
echo "When everything is green, reply with the same report as before (at most 15 lines: what was fixed, test/lint/coverage results, notable decisions, next pending phase)."
echo "When everything is green, reply with the same report as before (at most 15 lines: what was fixed, test/lint/coverage results, notable decisions, next pending task)."
}
}
# --- child executor -----------------------------------------------------------
# run_child <phase> <attempt> <prompt> [resume-session]
# Attempt 1: fresh session in .agent/phase-sessions/.
# run_child <unit> <attempt> <prompt> [resume-session]
# Attempt 1: fresh session in .agents/phase-sessions/.
# Attempt N>1: resumes the given session file — the failed executor's own
# session, tracked by execute_phase (so retries keep its work). When no
# session, tracked by execute_unit (so retries keep its work). When no
# session was captured (or FRESH_FIX=1) a fresh ephemeral session runs with
# the failure context instead.
# Progress (tool calls, assistant text, thinking, compaction) streams to the
# terminal live via scripts/progress.mjs, which also writes the final
# assistant message to: .agent/reports/<base>.a<attempt>.md
# Child stderr → .agent/reports/<base>.a<attempt>.err
# assistant message to: unit_report <unit> <attempt> md
# Child stderr → unit_report <unit> <attempt> err
# QUIET=1 suppresses the progress display (report file is still written).
# Sets CHILD_RC (pi's exit code) and PROGRESS_RC (progress.mjs's exit code;
# non-zero means the stream ended without a clean final report).
@@ -141,21 +258,23 @@ _run_pi_pipeline() {
# trap had fired, so the failure is always visible.
if (( CHILD_RC == 130 || PROGRESS_RC == 130 )); then
echo
echo "✗ ERROR: interrupted (SIGINT) — phase is left in $PHASE_TODO/; re-run to continue" >&2
echo "✗ ERROR: interrupted (SIGINT) — unit is left in $PHASE_TODO/; re-run to continue" >&2
exit 130
fi
if (( CHILD_RC == 143 || PROGRESS_RC == 143 )); then
echo
echo "✗ ERROR: interrupted (SIGTERM) — phase is left in $PHASE_TODO/; re-run to continue" >&2
echo "✗ ERROR: interrupted (SIGTERM) — unit is left in $PHASE_TODO/; re-run to continue" >&2
exit 143
fi
}
run_child() {
local phase="$1" attempt="$2" prompt="$3" resume_session="${4:-}"
local base="${phase%.md}"
local out="$PHASE_REPORTS/$base.a$attempt.md"
local errf="$PHASE_REPORTS/$base.a$attempt.err"
local unit="$1" attempt="$2" prompt="$3" resume_session="${4:-}"
local base out errf
base="$(unit_base "$unit")"
out="$(unit_report "$unit" "$attempt" md)"
errf="$(unit_report "$unit" "$attempt" err)"
mkdir -p "$(dirname "$out")"
local PROGRESS=(node "$SKILL_DIR/scripts/progress.mjs" "$out")
[[ "${QUIET:-0}" == "1" ]] && PROGRESS+=(--quiet)
@@ -172,52 +291,278 @@ run_child() {
# --- validation gate ----------------------------------------------------------
ensure_validate() {
if [[ ! -f .agent/validate.sh ]]; then
cp "$SKILL_DIR/assets/validate.sh" .agent/validate.sh
chmod +x .agent/validate.sh
warn "no .agent/validate.sh found — created it from the skill template."
warn "adapt it to this project's real test/lint/coverage commands; it is the pass/fail gate for every phase."
if [[ ! -f .agents/validate.sh ]]; then
cp "$SKILL_DIR/assets/validate.sh" .agents/validate.sh
chmod +x .agents/validate.sh
warn "no .agents/validate.sh found — created it from the skill template."
warn "adapt it to this project's real test/lint/coverage commands; it is the pass/fail gate after every task."
fi
}
# run_validation <logfile>; returns 0 iff .agent/validate.sh exits 0.
# run_validation <logfile>; returns 0 iff .agents/validate.sh exits 0.
run_validation() {
bash .agent/validate.sh >"$1" 2>&1
bash .agents/validate.sh >"$1" 2>&1
}
# --- one phase, with bounded fixer retries ------------------------------------
# execute_phase <phase-file>
# Returns 0 and moves the phase to complete/ on success; returns 1 after
# MAX_FIX_ATTEMPTS failed attempts (phase file is left in todo/).
execute_phase() {
local phase="$1"
local base="${phase%.md}"
local attempt=1 errors=""
# --- phase commit + push ------------------------------------------------------
# Commit ownership: child executors NEVER commit (their prompts forbid it,
# overriding any project instruction to commit per task/phase). By default
# (PHASE_COMMIT=1) the harness therefore makes ONE atomic commit per
# completed phase — the phase's code changes, the todo→complete file move,
# and the executor reports, together — at the phase's commit point, and
# pushes it right after (PHASE_PUSH=1, default — see push_phase).
#
# Staging is scoped so the owner's unrelated work is not swept in:
# stage = (everything dirty now)
# − (what was already dirty when the phase's first unit started —
# snapshot $PHASE_SESSIONS/dirty-<phase>, taken once per phase)
# − (pipeline runtime artifacts: phase-sessions/, pipeline.log)
# Work the owner dirtied mid-phase is indistinguishable from phase work and
# IS committed — the commit output prints exactly what went in.
# Snapshot the worktree's dirty state at the phase's first unit (no-op
# outside a git work tree, and when the phase already has a snapshot — a
# resumed phase keeps the one taken at its start).
phase_dirty_snapshot() {
local phase="$1" f
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || return 0
f="$PHASE_SESSIONS/dirty-$phase"
if [[ ! -f "$f" ]]; then
git status --porcelain >"$f" 2>/dev/null || : >"$f"
fi
}
# commit_phase <unit> <report-file>
# Returns 0 on success, when there is nothing left to commit, or outside a
# git work tree. Returns 1 (after printing a ✗ ERROR block with the git
# error and the hand-fix command) when the phase's own artifacts could not
# be committed, or the push of the phase commit failed (the commit is local
# and safe — the next phase's push sweeps it in once the push works). The
# phase stays COMPLETE either way — the work passed validation; the caller
# stops the run so a commit/push miss is visible, never retried as a task
# and never swept into a later phase's commit.
commit_phase() {
local unit="$1" report="${2:-}"
local phase pre curf paths lits moved staged addout commitout subject msgf
phase="$(unit_phase "$unit")"
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
warn "not a git work tree — no phase commit for $phase"
return 0
fi
pre="$PHASE_SESSIONS/dirty-$phase"
# Everything dirty now: tracked changes vs HEAD + untracked non-ignored.
curf="$(mktemp)"
{ git diff --name-only HEAD; git ls-files --others --exclude-standard; } | sort -u >"$curf"
# Quick exit: the phase's artifacts are already committed (moved file
# tracked and clean — e.g. a duplicate commit attempt after the snapshot
# was cleaned up) — never stage again, owner WIP stays untouched.
if git ls-files --error-unmatch -- "$PHASE_DONE/$unit" >/dev/null 2>&1 \
&& ! grep -qx -- "$PHASE_DONE/$unit" "$curf"; then
rm -f "$curf"
echo " (nothing to commit — the phase is already fully committed)"
return 0
fi
# Subtract pre-phase owner WIP (porcelain lines are "XY <path>") and the
# pipeline's own runtime artifacts.
paths="$(mktemp)"
if [[ -f "$pre" ]]; then
comm -23 "$curf" <(cut -c4- "$pre" | grep -v '^[[:space:]]*$' | sort -u) \
| grep -vE '^\.agents/(phase-sessions/|pipeline\.log$)' >"$paths" || true
else
grep -vE '^\.agents/(phase-sessions/|pipeline\.log$)' "$curf" >"$paths" || true
fi
rm -f "$curf"
if [[ ! -s "$paths" ]]; then
rm -f "$paths" "$pre"
echo " (nothing to commit — the phase is already fully committed)"
return 0
fi
# :(literal) keeps file names with glob characters ([?* ) from being read
# as pathspec patterns.
lits="$(mktemp)"
sed 's|^|:(literal)|' "$paths" >"$lits"
if ! addout="$(git add -A --pathspec-from-file="$lits" 2>&1)"; then
rm -f "$paths" "$lits"
echo "✗ ERROR: phase commit for $phase — staging failed:" >&2
printf '%s\n' "$addout" | sed 's/^/ /' >&2
return 1
fi
rm -f "$lits"
# Verify the artifacts that used to go missing: the moved phase file must
# be in the index (hard fail), and the executor report unless the project
# deliberately gitignores reports.
moved="$PHASE_DONE/$unit"
staged="$(git -c core.quotepath=off diff --cached --name-only -z | tr '\0' '\n')"
if ! grep -qx -- "$moved" <<<"$staged"; then
git reset -q
rm -f "$paths"
echo "✗ ERROR: phase commit for $phase — the moved phase file is not in the index ($moved)." >&2
if git check-ignore -q -- "$moved" 2>/dev/null; then
echo " your .gitignore excludes it — .agents/phases must be tracked (SKILL.md setup notes)." >&2
fi
echo " the work is complete and uncommitted; stage and commit it by hand." >&2
return 1
fi
if [[ -n "$report" && -f "$report" ]] && ! grep -qx -- "$report" <<<"$staged" \
&& ! git check-ignore -q -- "$report" 2>/dev/null; then
git reset -q
rm -f "$paths"
echo "✗ ERROR: phase commit for $phase — the executor report is not in the index ($report)." >&2
echo " the work is complete and uncommitted; stage and commit it by hand." >&2
return 1
fi
rm -f "$paths"
# Subject: PHASE_COMMIT_SUBJECT with {{PHASE}} substituted (default
# "phase: <phase>"); the executor's final report becomes the body.
# The default subject lives in a variable: a brace literal inside the
# ${var:-default} word would terminate the expansion early (bash does not
# nest plain braces).
local default_subject="phase: {{PHASE}}"
subject="${PHASE_COMMIT_SUBJECT:-$default_subject}"
subject="${subject//\{\{PHASE\}\}/$phase}"
msgf="$(mktemp)"
{
printf '%s\n' "$subject"
printf '\n'
[[ -n "$report" && -f "$report" ]] && sed -n '1,40p' "$report"
} >"$msgf"
if ! commitout="$(git commit --no-gpg-sign -F "$msgf" 2>&1)"; then
rm -f "$msgf"
echo "✗ ERROR: phase commit for $phase FAILED — the phase is complete but UNCOMMITTED (the scoped changes are still staged)." >&2
printf '%s\n' "$commitout" | sed 's/^/ /' >&2
echo " finish by hand: git commit --no-gpg-sign -m 'phase: $phase'" >&2
echo " re-running the pipeline will NOT commit this phase — fix the commit first." >&2
return 1
fi
rm -f "$msgf" "$pre"
echo " (committed: $(git log -1 --oneline))"
# Hand the commit to the remote (PHASE_PUSH, default on). A push failure
# returns 1 so the run stops — same contract as a commit failure.
if ! push_phase "$phase"; then
return 1
fi
return 0
}
# push_phase <phase>
# Pushes the phase commit made by commit_phase (PHASE_PUSH=1, default).
# No remote configured → notice and success (the commit stays local).
# Branch with an upstream → `git push`; without → `git push -u <first
# remote> <branch>`. On failure prints a ✗ ERROR block with the git error
# and the hand-fix command and returns 1 — the commit is local and safe,
# and the next phase's push sweeps the unpushed commit in once the push
# works.
push_phase() {
local phase="$1" remote branch cmd pushout
[[ "${PHASE_PUSH:-1}" == "1" ]] || { echo " (PHASE_PUSH=0 — commit not pushed, left local)"; return 0; }
remote="$(git remote 2>/dev/null | head -n1)"
if [[ -z "$remote" ]]; then
echo " (no git remote configured — skipping push)"
return 0
fi
branch="$(git symbolic-ref --short -q HEAD || true)"
if [[ -z "$branch" ]]; then
echo "✗ ERROR: phase push for $phase — detached HEAD, nothing to push." >&2
echo " the phase commit is local; push by hand once you are on a branch." >&2
return 1
fi
if git rev-parse --abbrev-ref --disambiguate '@{u}' >/dev/null 2>&1; then
cmd=(git push)
else
cmd=(git push -u "$remote" "$branch")
fi
if ! pushout="$("${cmd[@]}" 2>&1)"; then
echo "✗ ERROR: phase push for $phase FAILED — the phase is committed LOCALLY but NOT PUSHED." >&2
printf '%s\n' "$pushout" | sed 's/^/ /' >&2
echo " finish by hand: ${cmd[*]}" >&2
echo " re-running will NOT re-push it now — the next phase's push sweeps it in once the push works; fix the push first." >&2
return 1
fi
echo " (pushed: $(git log -1 --oneline))"
return 0
}
# --- notifications ------------------------------------------------------------
# Send a push notification via ntfy after a unit completes. Reads
# ~/.env/pi-ntfy.env (see the ntfy skill). No-ops (silently) when notifications
# are disabled (PHASE_NOTIFY=0) or ntfy is not configured, so it never breaks
# the pipeline. Gated on PHASE_NOTIFY so a misconfigured server can't stall a
# long run; default ON when the env file is present.
notify_task() {
local unit="$1" status="$2" # status: task | phase
[[ "${PHASE_NOTIFY:-1}" == "1" ]] || return 0
local envf="${NTFY_ENV_FILE:-$HOME/.env/pi-ntfy.env}"
[[ -f "$envf" ]] || return 0
# shellcheck disable=SC1090
source "$envf"
[[ -n "${NTFY_URL:-}" && -n "${NTFY_TOKEN:-}" ]] || return 0
local title body tags
if [[ "$status" == phase ]]; then
title="Phase $(unit_phase "$unit") done"
tags="heavy_check_mark,phase-complete"
body="Phase $(unit_phase "$unit") of the pipeline completed."
else
title="Task complete"
tags="heavy_check_mark,task-complete"
body="Task delivered: $unit"
fi
curl -sS -X POST "${NTFY_URL}/${NTFY_TOPIC:-pi}" \
-H "Authorization: Bearer ${NTFY_TOKEN}" \
-H "X-Title: $title" \
-H "X-Priority: 4" \
-H "X-Tags: $tags" \
-H "X-Markdown: yes" \
-d "$body" >/dev/null 2>&1 || warn "ntfy notification failed for $unit"
}
# --- one unit (task, phase final pass, or legacy phase), with retries ---------
# execute_unit <unit>
# Returns 0 and moves the unit to complete/ on success; returns 1 after
# MAX_FIX_ATTEMPTS failed attempts (unit file is left in todo/), or 1 when
# the phase commit or its push fails at a phase's commit point (unit stays
# in complete/ — the work is done; the commit/push must be finished by hand).
execute_unit() {
local unit="$1"
local base attempt=1 errors=""
local last_session="" pre post
command -v node >/dev/null 2>&1 || die "node not found on PATH (needed to render phase progress)"
mkdir -p "$PHASE_DONE" "$PHASE_REPORTS" "$PHASE_SESSIONS"
base="$(unit_base "$unit")"
command -v node >/dev/null 2>&1 || die "node not found on PATH (needed to render task progress)"
mkdir -p "$PHASE_DONE" "$PHASE_REPORTS" "$PHASE_SESSIONS" "$(unit_report_dir "$unit")"
ensure_validate
# First unit of the phase: freeze the owner's pre-existing dirty state so
# the phase commit (commit_phase) can exclude it from staging.
phase_dirty_snapshot "$(unit_phase "$unit")"
while (( attempt <= MAX_FIX_ATTEMPTS )); do
echo "━━ $phase — attempt $attempt/$MAX_FIX_ATTEMPTS ━━"
echo "━━ $unit — attempt $attempt/$MAX_FIX_ATTEMPTS ━━"
pre="$(latest_child_session)"
if (( attempt == 1 )); then
run_child "$phase" 1 "$(first_prompt "$phase")"
run_child "$unit" 1 "$(first_prompt "$unit")"
else
run_child "$phase" "$attempt" "$(fix_prompt "$errors")" "$last_session"
run_child "$unit" "$attempt" "$(fix_prompt "$errors")" "$last_session"
fi
# Track which session file this attempt used, so the next attempt resumes
# exactly this phase's failed session (not just "latest in the directory").
# exactly this unit's failed session (not just "latest in the directory").
post="$(latest_child_session)"
if [[ -n "$post" && "$post" != "$pre" ]]; then
last_session="$post"
fi
# The child can be signaled mid-flush: the session file then holds a final
# report the stream lost. Recover it so a completed phase is not retried.
if (( CHILD_RC == 0 )) && [[ -f "$PHASE_REPORTS/$base.a$attempt.md" ]] \
&& grep -q "no final assistant message" "$PHASE_REPORTS/$base.a$attempt.md"; then
if [[ -n "$last_session" ]] && recover_report "$PHASE_REPORTS/$base.a$attempt.md" "$last_session"; then
# report the stream lost. Recover it so a completed unit is not retried.
if (( CHILD_RC == 0 )) && [[ -f "$(unit_report "$unit" "$attempt" md)" ]] \
&& grep -q "no final assistant message" "$(unit_report "$unit" "$attempt" md)"; then
if [[ -n "$last_session" ]] && recover_report "$(unit_report "$unit" "$attempt" md)" "$last_session"; then
warn "stream lost the final message — report recovered from ${last_session##*/}"
PROGRESS_RC=0
fi
@@ -225,40 +570,51 @@ execute_phase() {
errors=""
if (( CHILD_RC != 0 )); then
warn "child pi exited with code $CHILD_RC — see $PHASE_REPORTS/$base.a$attempt.err"
errors+="[child pi exited with code $CHILD_RC]"$'\n'"$(tail -c 4000 "$PHASE_REPORTS/$base.a$attempt.err" 2>/dev/null)"
warn "child pi exited with code $CHILD_RC — see $(unit_report "$unit" "$attempt" err)"
errors+="[child pi exited with code $CHILD_RC]"$'\n'"$(tail -c 4000 "$(unit_report "$unit" "$attempt" err)" 2>/dev/null)"
fi
if (( PROGRESS_RC != 0 )); then
warn "child run ended without a clean final report — see $PHASE_REPORTS/$base.a$attempt.md"
errors+="[child run ended without a clean final report]"$'\n'"$(tail -c 4000 "$PHASE_REPORTS/$base.a$attempt.err" 2>/dev/null)"
warn "child run ended without a clean final report — see $(unit_report "$unit" "$attempt" md)"
errors+="[child run ended without a clean final report]"$'\n'"$(tail -c 4000 "$(unit_report "$unit" "$attempt" err)" 2>/dev/null)"
fi
if ! run_validation "$PHASE_REPORTS/$base.a$attempt.validate"; then
warn ".agent/validate.sh FAILED — see $PHASE_REPORTS/$base.a$attempt.validate"
errors+="[.agent/validate.sh FAILED]"$'\n'"$(tail -n 120 "$PHASE_REPORTS/$base.a$attempt.validate" 2>/dev/null)"
if ! run_validation "$(unit_report "$unit" "$attempt" validate)"; then
warn ".agents/validate.sh FAILED — see $(unit_report "$unit" "$attempt" validate)"
errors+="[.agents/validate.sh FAILED]"$'\n'"$(tail -n 120 "$(unit_report "$unit" "$attempt" validate)" 2>/dev/null)"
fi
if [[ -z "$errors" ]]; then
mv -f "$PHASE_TODO/$phase" "$PHASE_DONE/$phase"
echo "✓ $phase → complete"
if [[ "${PHASE_COMMIT:-0}" == "1" ]]; then
if git add -A 2>/dev/null && git commit --no-gpg-sign -m "phase: $phase" >/dev/null 2>&1; then
echo " (committed)"
move_unit "$unit"
if is_phase_end "$unit"; then
echo "✓ $unit → complete (phase $(unit_phase "$unit") done)"
if [[ "${PHASE_COMMIT:-1}" == "1" ]]; then
if ! commit_phase "$unit" "$(unit_report "$unit" "$attempt" md)"; then
# The work is done and validated; the unit stays in complete/ and
# re-running will NOT re-execute this phase. Stop the run so the
# commit miss is visible — commit_phase printed the hand-fix.
echo " phase commit/push did not complete — fix it first (see above); re-running continues at the next phase" >&2
return 1
fi
else
warn "git commit failed (continuing)"
# Explicit opt-out (PHASE_COMMIT=0): respect it, but never let the
# miss be silent — a completed phase with uncommitted work used to
# pile up in the worktree unnoticed.
warn "PHASE_COMMIT=0 — phase $(unit_phase "$unit") is COMPLETE but UNCOMMITTED; its work is left in the worktree — commit it by hand"
fi
else
echo "✓ $unit → complete"
fi
echo "── executor report ──"
cat "$PHASE_REPORTS/$base.a$attempt.md"
cat "$(unit_report "$unit" "$attempt" md)"
return 0
fi
attempt=$(( attempt + 1 ))
done
{
echo "✗ $phase FAILED after $MAX_FIX_ATTEMPTS attempts — left in $PHASE_TODO/."
echo "✗ $unit FAILED after $MAX_FIX_ATTEMPTS attempts — left in $PHASE_TODO/."
echo " last errors:"
printf '%s\n' "$errors" | tail -n 40 | sed 's/^/ /'
echo " logs: $PHASE_REPORTS/$base.a*.{md,err,validate}"
echo " logs: $(unit_report_dir "$unit")/$(unit_base "$unit").a*.{md,err,validate}"
if [[ -n "${last_session:-}" && -f "${last_session:-}" ]]; then
echo " resume: re-run this script (it auto-resumes the failed session), or manually:"
echo " pi --session $last_session -c \"review the failures above, fix them, re-validate\""
+30 -18
View File
@@ -1,41 +1,53 @@
#!/usr/bin/env bash
# run-phase.sh — execute exactly one phase in a fresh pi context.
# run-phase.sh — run every remaining task of one phase to completion.
#
# Each task runs in its own pi process (fresh context) with the
# .agents/validate.sh gate after every task; the phase ends with its
# 00_phase.md final pass.
#
# Usage:
# run-phase.sh # first pending phase (alphanumerical order)
# run-phase.sh 03_api.md # a specific pending phase (warns if out of order)
# run-phase.sh # next phase with pending work, to completion
# run-phase.sh 03_api # a specific phase (warns if out of order)
# run-phase.sh 03_api.md # a legacy single-file phase
#
# Env: see SKILL.md (MAX_FIX_ATTEMPTS, PHASE_MODEL, PHASE_THINKING,
# PHASE_COMMIT, PI_TRUST, FRESH_FIX).
# PHASE_COMMIT, PHASE_PUSH, PI_TRUST, FRESH_FIX).
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib.sh"
# Make interruptions visible: state stays in .agent/phases/todo, and the
# Make interruptions visible: state stays in .agents/phases/todo, and the
# failed executor's session is still resumable on the next run.
trap 'echo; echo "✗ ERROR: interrupted (SIGINT) — ${phase:-this phase} is left in $PHASE_TODO/; re-run to continue" >&2; exit 130' INT
trap 'echo; echo "✗ ERROR: interrupted (SIGTERM) — ${phase:-this phase} is left in $PHASE_TODO/; re-run to continue" >&2; exit 143' TERM
cd "$(find_root)" || die "no .agent/phases/todo found in this or parent directories (run /to-phase or /audit-create first)"
cd "$(find_root)" || die "no .agents/phases/todo found in this or parent directories (run /to-phase or /audit-create first)"
phase="${1:-}"
if [[ -n "$phase" ]]; then
[[ "$phase" == *.md ]] || phase="$phase.md"
[[ -f "$PHASE_TODO/$phase" ]] || die "$phase not found in $PHASE_TODO/"
first="$(next_phase)"
if [[ -n "$first" && "$first" != "$phase" ]]; then
phase="${phase%/}"; phase="${phase%.md}"; phase="${phase%%/*}"
[[ -d "$PHASE_TODO/$phase" || -f "$PHASE_TODO/$phase.md" ]] || die "$phase not found in $PHASE_TODO/"
first="$(next_unit)"
if [[ -n "$first" && "${first%%/*}" != "$phase" ]]; then
warn "dependency order: $first is pending before $phase — running out of order"
fi
else
phase="$(next_phase)"
[[ -n "$phase" ]] || { echo "✓ no pending phases in $PHASE_TODO/ — project complete."; exit 0; }
unit="$(next_unit)"
[[ -n "$unit" ]] || { echo "✓ no pending tasks in $PHASE_TODO/ — project complete."; exit 0; }
phase="$(unit_phase "$unit")"
fi
build_pi_args
if execute_phase "$phase"; then
exit 0
failed=0
while unit="$(phase_next_unit "$phase")"; do
[[ -n "$unit" ]] || break
execute_unit "$unit" || { failed=1; break; }
done
if (( failed )); then
# execute_unit printed the failure detail (task failure: errors, logs,
# resume command — or phase commit/push failure: the hand-fix command).
echo "✗ ERROR: phase $phase did not complete — see the error output above" >&2
exit 1
fi
echo "✗ ERROR: phase $phase FAILED after $MAX_FIX_ATTEMPTS attempts" >&2
echo " reports: $PHASE_REPORTS/${phase%.md}.a*.{md,err,validate}" >&2
echo " resume: re-run this script — the failed executor session is resumed automatically" >&2
exit 1
echo "✓ phase $phase complete"
exit 0
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# run-task.sh — execute exactly one task in a fresh pi context.
#
# A task is the smallest execution unit: a task file inside a phase
# directory (03_api/02_routes.md), a phase's 00_phase.md final pass, or a
# legacy single-file phase.
#
# Usage:
# run-task.sh # next pending task (pipeline order)
# run-task.sh 03_api/02_routes.md # a specific task (warns if out of order)
# run-task.sh 03_api/02_routes # ".md" is added when missing
#
# Env: see SKILL.md (MAX_FIX_ATTEMPTS, PHASE_MODEL, PHASE_THINKING,
# PHASE_COMMIT, PHASE_PUSH, PI_TRUST, FRESH_FIX).
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib.sh"
# Make interruptions visible: state stays in .agents/phases/todo, and the
# failed executor's session is still resumable on the next run.
trap 'echo; echo "✗ ERROR: interrupted (SIGINT) — ${unit:-this task} is left in $PHASE_TODO/; re-run to continue" >&2; exit 130' INT
trap 'echo; echo "✗ ERROR: interrupted (SIGTERM) — ${unit:-this task} is left in $PHASE_TODO/; re-run to continue" >&2; exit 143' TERM
cd "$(find_root)" || die "no .agents/phases/todo found in this or parent directories (run /to-phase or /audit-create first)"
unit="${1:-}"
if [[ -n "$unit" ]]; then
[[ "$unit" == *.md ]] || unit="$unit.md"
[[ -f "$PHASE_TODO/$unit" ]] || die "$unit not found in $PHASE_TODO/"
first="$(next_unit)"
if [[ -n "$first" && "$first" != "$unit" ]]; then
warn "execution order: $first is pending before $unit — running out of order"
fi
else
unit="$(next_unit)"
[[ -n "$unit" ]] || { echo "✓ no pending tasks in $PHASE_TODO/ — project complete."; exit 0; }
fi
build_pi_args
if execute_unit "$unit"; then
exit 0
fi
# execute_unit printed the failure detail (task failure: errors, logs,
# resume command — or phase commit/push failure: the hand-fix command).
echo "✗ ERROR: $unit did not complete — see the error output above" >&2
exit 1
+70
View File
@@ -0,0 +1,70 @@
---
name: security-audit
description: Performs a comprehensive application security audit and penetration test of a codebase. Use when the user asks to check code for vulnerabilities, security flaws, or insecure patterns, or to review against OWASP Top 10 / SANS Top 25. Produces a structured findings report and writes it to .agents/remediation_plan.md (or a path the user gives) so another agent can implement the fixes.
---
# Security Audit
You are a **Senior Application Security Engineer and Penetration Tester** with
expertise in the OWASP Top 10, SANS Top 25, and cloud-native security. You
perform a comprehensive security audit of the codebase in the current working
directory, identifying vulnerabilities, architectural weaknesses, and improper
implementation of security controls. You report every finding with a proof of
concept and an actionable remediation, and you persist the results so another
agent can implement the fixes.
## Scope (confirm or derive)
Establish before diving in:
- **Technology stack** (frameworks, language, database, infra/cloud).
- **Core functionality** and data sensitivity (PII, payments, auth).
- **Files/endpoints in scope** (default: the whole codebase, or a path the user gives).
If the user hasn't specified scope, state your assumptions and proceed.
## Methodology
Analyze through these lenses, tracing untrusted input (sources) to dangerous
functions (sinks) and checking controls end-to-end:
1. **Injection:** SQL/NoSQL/LDAP/command injection, XSS — validate all untrusted input at the boundary.
2. **Broken access control:** IDOR, missing authorization, over-privileged paths, failure of least privilege.
3. **Cryptographic failures:** deprecated hashing (MD5/SHA1), hardcoded secrets, weak RNG, improper TLS/SSL, secrets in code/config/commits.
4. **Insecure dependencies:** known-vulnerable / outdated libraries (check `requirements.txt`, `pyproject.toml`, `uv.lock`, `package.json`).
5. **Security misconfiguration:** permissive CORS, debug enabled in prod, missing security headers (HSTS, CSP), insecure defaults.
6. **Data integrity & privacy:** logging of PII/tokens/passwords, lack of encryption at rest and in transit.
7. **Business logic flaws:** checkout, password reset, registration, payment flows that can be bypassed.
Use tooling where available to support manual findings (e.g. `ruff` security rules, `bandit`, `gitleaks`/`trufflehog` for secrets, `pip-audit`), but rely on manual reasoning for logic and architecture issues.
## Reporting format
For **every** finding, provide:
- **[ID]** Title
- **Severity:** Critical | High | Medium | Low
- **Vulnerability type:** e.g. CWE-89 SQL Injection
- **Location:** file(s), line number(s)/function
- **Description:** why it is a vulnerability
- **Proof of Concept (PoC):** how an attacker exploits it (code snippet or steps)
- **Remediation:** specific, actionable code or architecture fixes
## Persistence
Write the full audit to **`.agents/remediation_plan.md`** (create `.agents/` if
missing) unless the user specifies another path. Make it self-contained so
another agent can implement the fixes without re-reading this session:
- Scope and stack summary.
- Methodology and tools run.
- The ordered findings list (severity-ranked, Critical first).
- A **remediation task list** — each fix expressed as concrete steps (file-level where possible), ordered by priority, so it can be carried out as phases under the `phased-execution` skill if desired.
- Any follow-up verification commands (re-run the suite, re-scan for secrets, re-check headers).
## Strict Operational Rules
- **Read-only audit:** you identify and document; you do **not** apply fixes in this session unless the user explicitly asks. Express fixes as ordered remediation steps.
- **No false comfort:** don't stop at "looks fine" — trace real sources to sinks and check the lenses above.
- **No destructive actions:** never modify production config, rotate real secrets, or change behavior during the audit.
- **Confidentiality:** treat discovered secrets as sensitive; do not print full credential values in the report beyond what's needed to locate them (redact where sensible).
+213
View File
@@ -0,0 +1,213 @@
---
name: todo-to-phased
description: Converts a TODO.md or TODO.txt file into a phased-execution roadmap — parses the file's sections and checkbox/bullet items, decomposes them into sequential phase directories (a 00_phase.md overview plus NN task files) at the agent's judgment (merging related small items, splitting large ones — items do not map one-to-one to phases or tasks), clears the TODO file once the roadmap is written, and scaffolds the .agents/ structure with a PLAN.md derived from the TODO when the project is not phased yet. Use when the user asks to convert a TODO file (TODO.md, TODO.txt, a todo list) into phases, to make TODO items executable as a phase pipeline, or to build a phase roadmap from a TODO file. This skill only writes planning files and never touches application code; the phased-execution skill runs the resulting phases, and the phase-authoring skill adds individual phases later.
---
# TODO to Phased
You are the **TODO Architect** — a senior engineer responsible for converting
a human-written TODO file (TODO.md / TODO.txt) into an executable
phased-execution roadmap. You parse the file, expand every unchecked item
into an executable task, group the tasks into sequential phases, and — when
the project is not phased yet — scaffold the minimal `.agents/` structure.
You **never implement code yourself**. The TODO file is the source of truth
for the conversion: it is read-only while you work and is **cleared as the
final step** (Phase 4) — its items now live in `.agents/phases/`. The
`phased-execution` skill executes the phases (one fresh subprocess per task
behind the validation gate), and `phase-authoring` handles individual phase
additions later.
Phase state lives in files, not chat:
- `.agents/PLAN.md` — master plan; **LOCKED DECISIONS** are binding
- `.agents/phases/todo/NN_name/` — a pending phase: `00_phase.md` overview + `NN_task.md` task files (task sort order = execution order)
- `.agents/phases/complete/` — finished phases, mirroring the todo/ layout (read-only history)
- `.agents/validate.sh` — the pass/fail gate, run after every task (installed by the `phased-execution` skill on first run)
**Traceability:** every phase overview and task file carries a **Source**
line citing the TODO file and line number(s) it was derived from, so any
generated task can be traced back to the original TODO item.
## Choosing the mode
Run the audit first (Phase 1), then:
- **Not phased** (no `.agents/phases/todo/`) → **Protocol A**: full
conversion — minimal scaffold + the TODO-derived roadmap.
- **Already phased** (`.agents/phases/todo/` exists) → **Protocol B**:
append mode — new phase directories only, numbered after the existing
ones.
- **No TODO file found** → the audit lists candidates in the tree; ask the
user for the path. If the items actually live only in chat, this skill
does not apply — use the `phase-authoring` skill instead.
## Phase 1 — TODO audit
1. Run `bash scripts/todo-audit.sh [path]` (resolve `scripts/` against this
skill's directory). It reports the TODO file's location, a parsed
outline (sections, unchecked/checked items, bullets, nesting), the
project's `.agents/` state, the next free phase number, git state, and
test tooling.
2. Read the **whole** TODO file with the read tool — the audit's outline is
a preview, not a substitute.
3. Classify the content:
- **Sections** — `#`/`##` headings (in .txt: any line starting with `#`).
- **Work items** — unchecked items: `- [ ]`, `* [ ]`, plain bullets,
numbered entries, and plain .txt lines.
- **Done items** — `[x]` items. Excluded from the roadmap; reported as
already done.
- **Detail** — indented sub-bullets and continuation lines under an
item; they become Work steps of whatever task implements the item.
- **Noise** — prose, dates, signatures: context only, never tasks.
## Phase 2 — Roadmap design (draft only, write nothing)
**Decomposition is your call.** TODO items do **not** map one-to-one to
phases (or tasks) — how the items become phases is an engineering judgment
you make, based on the content and the repository. The audit's stats are
starting points, not rules:
- **Merge** related small items into a single task when they are one
coherent change (e.g. "add `list` command" + "support JSON output" is one
task, not two).
- **Split** a large item into a phase of its own, decomposing its detail
lines into that phase's tasks.
- **Group by workstream, not by section** — TODO sections are hints, not
boundaries: a section may hold several phases, several small sections may
share one, and unsectioned items may form their own.
Constraints on the decomposition (these are non-negotiable):
1. **Coverage:** every unchecked item must be covered by at least one task —
nothing is dropped (done `[x]` items excepted).
2. **Shape:** one coherent capability or workstream per phase; 2–8 tasks per
phase; each task is one focused change (≤30 minutes of executor work).
3. **Order:** phases run in dependency order; each leaves the project
functional and launchable.
4. **Traceability:** each phase overview cites the TODO line range(s) its
tasks cover, and each task cites the item line(s) it implements — the
correspondence may be many items per task or one item per task, but every
source line must appear in some task's **Source** line.
**Task content.** A task's Objective captures the intent of the item(s) it
implements; its Work expands that into file-level steps — inspect the
repository for the files involved and never invent paths that do not exist;
the items' detail lines become Work sub-steps.
**Naming.** Phase directories `NN_name/` in short `snake_case` derived from
the capability they deliver (the section title is a good start); task files
`01_short_name.md` derived from the change they make.
**Expansion.** TODO items are terse. Where an item is too vague to execute
in isolation, expand it into concrete, plausible Work steps and mark every
invented decision with an `ASSUMPTION:` line in the task file. Collect all
assumptions for the confirmation table — the executor agent must never have
to guess at runtime.
**Foundation phase.** If the project has application code and the audit
shows no test tooling or no green baseline → phase `01_foundation`: adapt
`.agents/validate.sh` to the project's real test/lint/coverage checks, add
missing test/coverage tooling, fix or baseline existing test failures — the
full current suite must be green and the project fully launchable when this
phase completes. TODO phases then start at `02`. A pure docs/config project
skips the foundation; its first phase's first task adapts `validate.sh`
minimally instead.
**Presentation.** Show the **Proposed Roadmap** — a table with, per phase:
number, name, TODO source (line range(s) covered), its tasks (one line
each), and any assumptions — plus, in Protocol A, the anchors table
(existing stack
`LOCKED`, anything new `PROPOSED`) and the count of excluded done items.
**Wait for confirmation.** Do not write anything until the user confirms the
roadmap and locks any `PROPOSED` anchor.
## Phase 3 — Scaffold & write the phases
**Protocol A only** — create (or merge into what already exists) with file
tools:
- **`.agents/PLAN.md`** — from `assets/plan-template.md`: assumptions &
design principles, the anchors table, high-level architecture (mirroring
the actual code), validation workflow, and a phase-roadmap table whose
source column cites the TODO lines.
- **`AGENTS.md`** — the five base rules:
1. "Always read `.agents/PLAN.md` first to understand the project context and goals."
2. "Follow the phased execution protocol in `.agents/phases/`."
3. "Never modify `.agents/PLAN.md` or any files in `.agents/phases/complete/`."
4. "If you need to update any file in `.agents/phases/todo/`, you must ask the user for permission first."
5. "Strictly adhere to the **LOCKED DECISIONS** listed in `.agents/PLAN.md`."
- **`.agents/phases/todo/`** and **`.agents/phases/complete/`** (the latter
created empty).
- **`.gitignore`** — add `.agents/phase-sessions/` and `.agents/pipeline.log` if missing (never `.agents/` itself — the planning tree is tracked and committed); if an existing `.gitignore` has a `.agents/` ignore line, remove it.
Then write one phase directory per confirmed phase at
`.agents/phases/todo/NN_name/`, numbered from the audit's "next phase
number" (counting `todo/` and `complete/` together):
- **`00_phase.md`** — from `assets/phase-template.md`: **Source** (the TODO
line range(s) the phase's tasks cover / capability title), Objective,
Dependencies (the preceding phase by directory name, or the last existing
todo phase in Protocol B; or "— (none)"), Tasks (ordered index of the task
files), Testing & Quality
(unit + integration tests for all new/modified logic, coverage **>90%**
on new/modified code), Completion Criteria (observable checks).
- **Task files `01_…`, `02_…`, …** — from `assets/task-template.md`:
**Source** (TODO `file:line` — or a list of lines — + the quoted original
item(s)), Objective, Work
(file-level steps, including any `ASSUMPTION:` lines), Testing & Quality,
Completion Criteria.
**Task sizing:** each task is one focused change, roughly ≤30 minutes of
executor work; a phase typically holds 2–8 tasks.
**Protocol B** — write only the new phase directories (same content rules),
numbered after the existing ones. Do not touch `.agents/PLAN.md`,
`AGENTS.md`, or existing phases; note in the final summary that `PLAN.md`'s
roadmap table does not list these appended phases.
## Phase 4 — Version Control, TODO Clearing & Hand-Off
- Git is mandatory: `git init` if the project is not a repository.
- **Clear the TODO list** — the items are now phases, so the TODO file no
longer holds the plan. Overwrite the file with a clean, empty document:
just a single top-level title `# TODO` and nothing else (no pointer
line, no leftover items, no trailing notes). Never delete the file
itself — leave the empty `TODO.md` in place so the workspace keeps a
known, stable landing spot for future todos.
- Commit the conversion — `AGENTS.md`, the cleared TODO file, the whole
`.agents/` tree (it is tracked, not git-ignored), and any other modified
files — with a Conventional Commits message (e.g. `chore(agent): phase
roadmap from TODO.md, NN phases`), always with `--no-gpg-sign`.
Finish by summarizing: the anchors (Protocol A), the phase list (number,
name, TODO source, one-line objective), the excluded done items, every
`ASSUMPTION` made, and confirmation that the TODO file has been cleared.
Then hand off:
- **Execute:** the `phased-execution` skill — `run-task.sh` for a single
task, `run-phase.sh` for a phase, `auto-phase.sh` for the full pipeline.
- **Extend:** the `phase-authoring` skill for individual additional phases.
## Strict Operational Rules
- The conversion writes **planning files**: `.agents/**`, `AGENTS.md`, and
`.gitignore`. **Never modify application code.** The TODO file is
read-only during the conversion and is cleared (never deleted) as the
final step — only after the phase roadmap has been written.
- Never overwrite existing `.agents/` content — merge into it. Never modify
anything in `.agents/phases/complete/`.
- In Protocol B, never modify `.agents/PLAN.md` or `AGENTS.md`.
- Do not create `.agents/validate.sh` (the `phased-execution` skill installs
it on first run; the foundation phase — or the first phase's first task —
adapts it).
- Wait for confirmation of the Proposed Roadmap before writing any file,
and wait for the user to lock any `PROPOSED` anchor before writing
`PLAN.md`.
- Numbering: phase `NN` is the next free number after the highest existing
entry (directory or file), counting `todo/` and `complete/` together; task
`NN` is per-phase (`01`…). Never reuse or collide a number.
- **Executable in isolation:** an agent that sees only the repository, the
phase overview, the completed task files, and one task file — no chat
history, no follow-ups — must be able to finish that task. TODO items that
cannot be made executable without a user decision are surfaced in the
confirmation step, never left as runtime guesses.
+27
View File
@@ -0,0 +1,27 @@
# Phase {{NN}} — {{Short Title}}
**Source:** `{{TODO file}} L{{range(s) the phase's tasks cover, e.g. 34–38, 41}} — "{{capability / section title}}"`
**Story:** `{{.agents/user_stories/<story>.md or "n/a"}}`
**Context:** `{{PLAN.md sections / files this phase builds on}}`
## Objective
{{1–3 sentences: what this phase delivers}}
## Dependencies
- `{{NN_name}}` ({{complete|todo}}) — {{what is needed from it}}
{{or "— (none)"}}
## Tasks
1. `01_{{short_name}}.md` — {{one-line summary}}
2. `02_{{short_name}}.md` — {{one-line summary}}
## Testing & Quality
- Unit/integration: {{tests required for all new logic — name the behaviors to cover}}
- Coverage: **>90%** on new/modified code
{{project additions, e.g. a dedicated Playwright E2E suite run in isolation by this phase's final pass}}
## Completion Criteria
- [ ] {{observable check: command to run / endpoint to hit / artifact to exist}}
- [ ] test suite green, coverage >90%
- [ ] no behavior change in completed phases
{{project additions, e.g. a commit command per AGENTS.md conventions}}
+32
View File
@@ -0,0 +1,32 @@
# {{Project Name}} — Master Plan
> Master design document for a phased-execution project. The phase roadmap
> below was derived from `{{TODO file}}` on {{date}}; each phase overview and
> task file cites its source lines in that file. The **LOCKED DECISIONS**
> below are binding: no phase may introduce technology outside them without
> explicit user permission.
## Assumptions & Design Principles
- {{...}}
## Architectural Anchors
| COMPONENT | DECISION | RATIONALE | STATUS |
|-----------|----------|-----------|--------|
| {{component}} | {{decision — inherited from the existing codebase}} | {{why}} | LOCKED |
| {{component}} | {{decision awaiting ratification}} | {{why}} | PROPOSED |
## High-Level Architecture
{{Component breakdown and data flow, mirroring the actual code.}}
## Data Model
{{Existing schemas, state transitions, and persistence details.}}
## Validation / Verification Workflow
{{Multi-step logic ensuring high-confidence outputs (unit → integration →
contract/E2E → coverage floor).}}
## Phase Roadmap
| # | Phase | TODO source | Objective (one line) |
|---|-------|-------------|----------------------|
| 01 | {{NN_name}} | — | foundation: adapt validate.sh, green test baseline (where applicable) |
| 02 | {{NN_name}} | `{{TODO file}} L{{a}}–L{{b}}` | {{...}} |
+21
View File
@@ -0,0 +1,21 @@
# Task {{NN}} — {{Short Title}}
**Phase:** `{{NN_phase}}` · **Source:** `{{TODO file}}:{{line(s), e.g. 42 or 42–44}} — "{{original TODO item text — the item or items this task implements}}"`
**Story:** `{{.agents/user_stories/<story>.md or "n/a"}}`
## Objective
{{1–2 sentences: what this task delivers}}
## Work
1. `{{path/to/file}}` — {{specific change, file-level detail}}
2. `{{path/to/file}}` — {{specific change}}
{{for every decision the TODO item left open: "- ASSUMPTION: {{what was left open and the choice made}}"}}
## Testing & Quality
- Unit/integration: {{tests required for this task's logic}}
- Coverage: **>90%** on this task's new/modified code
## Completion Criteria
- [ ] {{observable check: command to run / endpoint to hit / artifact to exist}}
- [ ] full test suite green
- [ ] no behavior change in completed work
+241
View File
@@ -0,0 +1,241 @@
#!/usr/bin/env bash
# todo-audit.sh — TODO file & phased-readiness probe for the TODO Architect.
#
# Locates a TODO.md / TODO.txt (or takes one as an argument), prints its
# location, a parsed outline (sections, unchecked/checked items, bullets,
# nesting), and the project's .agents/ state, next free phase number
# (counting .agents/phases/todo/ and complete/ together, zero-padded to 2
# digits), git state, and test tooling.
#
# Usage:
# bash todo-audit.sh [path/to/TODO.(md|txt)]
# bash todo-audit.sh # auto-detect in cwd and git root
set -uo pipefail
# --- locate the TODO file ------------------------------------------------------
todo=""
if [[ $# -ge 1 ]]; then
[[ -f "$1" ]] || { echo "✗ ERROR: not a file: $1" >&2; exit 1; }
todo="$1"
else
# search the git root first, then cwd and its ancestors (up to the git
# root, or 3 levels when not in a repo)
dirs=()
gr="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [[ -n "$gr" ]]; then
dirs+=("$gr")
d="$(pwd)"
while [[ "$d" != "$gr" && "$d" != "/" ]]; do
dirs+=("$d")
d="$(dirname "$d")"
done
else
d="$(pwd)"
for _ in 0 1 2 3; do
[[ "$d" == "/" ]] && break
dirs+=("$d")
d="$(dirname "$d")"
done
fi
for r in "${dirs[@]}"; do
[[ -d "$r" ]] || continue
for f in "$r"/*; do
[[ -f "$f" ]] || continue
b="$(basename "$f" | tr '[:upper:]' '[:lower:]')"
case "$b" in
todo.md|todo.txt) todo="$f"; break 2 ;;
esac
done
done
fi
if [[ -z "$todo" ]]; then
echo "no TODO.md / TODO.txt in cwd, its ancestors, or the git root — candidates in tree (maxdepth 3):"
found=0
while IFS= read -r f; do
echo " $f"
found=1
done < <(find . -maxdepth 3 \( -name .git -o -name node_modules -o -name .venv \) -prune -o -type f -iname 'todo.*' -print 2>/dev/null)
(( found )) || echo " (none found — pass the file path as an argument)"
exit 1
fi
# project root: git toplevel of the TODO file's directory, else that directory
tdir="$(cd "$(dirname "$todo")" && pwd)"
gr2="$(git -C "$tdir" rev-parse --show-toplevel 2>/dev/null || true)"
if [[ -n "$gr2" ]]; then root="$gr2"; else root="$tdir"; fi
ext="markdown"
[[ "$todo" == *.txt ]] && ext="plain text"
echo "project root: $root"
echo "todo file: $todo ($(wc -l < "$todo" | tr -d ' ') lines, $ext)"
# --- parse the TODO -------------------------------------------------------------
trim() { local s="$1"; s="${s#"${s%%[![:space:]]*}"}"; printf '%s' "${s%"${s##*[![:space:]]}"}"; }
# headings: md allows up to 3 leading spaces; txt requires column 0
re_heading_md='^ {0,3}#{1,6}[[:space:]]+(.*)$'
re_heading_txt='^#{1,6}[[:space:]]+(.*)$'
if [[ "$ext" == "plain text" ]]; then re_heading="$re_heading_txt"; else re_heading="$re_heading_md"; fi
re_checkbox='^([[:space:]]*)[-*+][[:space:]]\[( |x|X)\][[:space:]]*(.*)$'
re_bullet='^([[:space:]]*)[-*+][[:space:]]+(.*)$'
re_numbered='^([[:space:]]*)[0-9]+[.)][[:space:]]+(.*)$'
ln=0 sections=0 top_items=0 nested_items=0 checked=0 detail=0 maxdepth=0
outline=()
while IFS= read -r line || [[ -n "$line" ]]; do
ln=$((ln + 1))
line="${line%$'\r'}"
[[ -z "${line//[[:space:]]/}" ]] && continue
if [[ "$line" =~ $re_heading ]]; then
sections=$((sections + 1))
outline+=("L$ln: # $(trim "${BASH_REMATCH[1]}")")
elif [[ "$line" =~ $re_checkbox ]]; then
d=$(( ${#BASH_REMATCH[1]} / 2 ))
(( d > maxdepth )) && maxdepth=$d
case "${BASH_REMATCH[2]}" in
x|X) checked=$((checked + 1)); m="[x]" ;;
*) (( d > 0 )) && nested_items=$((nested_items + 1)) || top_items=$((top_items + 1)); m="[ ]" ;;
esac
outline+=("L$ln: $m $(trim "${BASH_REMATCH[3]}")")
elif [[ "$line" =~ $re_bullet ]]; then
d=$(( ${#BASH_REMATCH[1]} / 2 ))
(( d > maxdepth )) && maxdepth=$d
(( d > 0 )) && nested_items=$((nested_items + 1)) || top_items=$((top_items + 1))
outline+=("L$ln: - $(trim "${BASH_REMATCH[2]}")")
elif [[ "$line" =~ $re_numbered ]]; then
d=$(( ${#BASH_REMATCH[1]} / 2 ))
(( d > maxdepth )) && maxdepth=$d
(( d > 0 )) && nested_items=$((nested_items + 1)) || top_items=$((top_items + 1))
outline+=("L$ln: n. $(trim "${BASH_REMATCH[2]}")")
elif [[ "$ext" == "plain text" ]]; then
top_items=$((top_items + 1))
outline+=("L$ln: · $(trim "$line")")
else
detail=$((detail + 1))
if [[ "$line" =~ ^[[:space:]]{2,} ]]; then
outline+=("L$ln: (detail) $(trim "$line")")
fi
fi
done < "$todo"
echo
echo "todo stats:"
echo " sections: $sections"
printf ' top-level work items (-> tasks): %s\n' "$top_items"
printf ' nested sub-items (-> task work steps): %s\n' "$nested_items"
printf ' done items [x] (excluded): %s\n' "$checked"
echo " detail/prose lines: $detail"
echo " max nesting depth: $maxdepth"
echo
if (( ${#outline[@]} > 150 )); then
echo "todo outline (first 150 of ${#outline[@]} structural lines):"
printf '%s\n' "${outline[@]:0:150}"
else
echo "todo outline (${#outline[@]} structural lines):"
printf '%s\n' ${outline[@]+"${outline[@]}"}
fi
# --- .agents state -----------------------------------------------------------------
echo
echo ".agents state:"
a=0
if [[ -f "$root/.agents/PLAN.md" ]]; then
echo " .agents/PLAN.md: present"
a=1
fi
if [[ -f "$root/AGENTS.md" ]]; then
echo " AGENTS.md: present"
a=1
fi
if [[ -f "$root/.agents/validate.sh" ]]; then
echo " .agents/validate.sh: present"
a=1
fi
todo_dir="$root/.agents/phases/todo"
complete_dir="$root/.agents/phases/complete"
list_phases() {
local dir="$1" entry n t any=0
for entry in "$dir"/*; do
if [[ ! -e "$entry" ]]; then continue; fi
n="$(basename "$entry")"
if [[ "$n" =~ ^[0-9] ]]; then
if [[ -d "$entry" ]]; then
t="$(ls -1 "$entry" 2>/dev/null | grep -cE '\.md$' || true)"
echo " $n/ ($t file(s): 00_phase.md + tasks)"
else
echo " $n (legacy single-file phase)"
fi
any=1
fi
done
if (( ! any )); then echo " (empty)"; fi
}
if [[ -d "$todo_dir" ]]; then
echo " .agents/phases/todo:"
list_phases "$todo_dir"
a=1
fi
if [[ -d "$complete_dir" ]]; then
echo " .agents/phases/complete:"
list_phases "$complete_dir"
a=1
fi
if (( ! a )); then echo " (no .agents/ artifacts — Protocol A full conversion needed)"; fi
max=0
for d in "$todo_dir" "$complete_dir"; do
[[ -d "$d" ]] || continue
for entry in "$d"/*; do
if [[ ! -e "$entry" ]]; then continue; fi
n="$(basename "$entry" .md)"
if [[ "$n" =~ ^([0-9]+) ]]; then
n=$((10#${BASH_REMATCH[1]}))
if (( n > max )); then max=$n; fi
fi
done
done
echo " next phase number: $(printf '%02d' $((max + 1)))"
# --- version control ----------------------------------------------------------------
echo
echo "version control:"
if [[ -d "$root/.git" ]]; then
branch="$(git -C "$root" branch --show-current 2>/dev/null || echo unknown)"
dirty="$(git -C "$root" status --porcelain 2>/dev/null | wc -l | tr -d ' ')"
echo " git repository (branch: ${branch:-detached}, uncommitted changes: ${dirty})"
else
echo " (not a git repository — the conversion must git init)"
fi
# --- test tooling --------------------------------------------------------------------
echo
echo "test tooling:"
t=0
if [[ -f "$root/pytest.ini" ]] || { [[ -f "$root/pyproject.toml" ]] && grep -q '\[tool\.pytest' "$root/pyproject.toml"; }; then
echo " pytest configured"
t=1
fi
if [[ -f "$root/conftest.py" ]] || [[ -d "$root/tests" ]]; then
echo " tests/ or conftest.py present"
t=1
fi
n_py="$(find "$root" \( -name 'test_*.py' -o -name '*_test.py' \) -not -path '*/.git/*' -not -path '*/node_modules/*' -not -path '*/.venv/*' -not -path '*/venv/*' 2>/dev/null | wc -l | tr -d ' ')"
if (( n_py > 0 )); then echo " $n_py python test file(s)"; t=1; fi
if [[ -f "$root/package.json" ]] && grep -q '"test"' "$root/package.json"; then
echo " npm test script"
t=1
fi
if [[ -f "$root/ruff.toml" ]] || { [[ -f "$root/pyproject.toml" ]] && grep -q '\[tool\.ruff\]' "$root/pyproject.toml"; }; then
echo " ruff configured"
t=1
fi
if [[ -f "$root/.coveragerc" ]] || { [[ -f "$root/pyproject.toml" ]] && grep -qE '\[tool\.(coverage|pytest-cov)\]' "$root/pyproject.toml"; }; then
echo " coverage configured"
t=1
fi
if (( ! t )); then echo " (none detected — a 01_foundation phase must establish a test baseline)"; fi
+60
View File
@@ -0,0 +1,60 @@
---
name: upgrade-existing-app
description: Upgrades an existing Python Web Application project to high-rigor architecture — user-story-driven development with independent Playwright E2E phases, no external CDNs, debugpy integration, PostgreSQL 17/Valkey/SeaweedFS orchestration, and modern WCAG 2.1 AA UI/UX standards. Use when the user asks to refactor, modernize, or bring an existing web project up to professional standards (not to scaffold a brand-new project — use the new-project skill for that). This skill performs real code and infrastructure changes, unlike convert-to-phased which only writes planning files.
---
# Upgrade Existing Web App
You are the **Lead Upgrade Engineer** — a senior architect who audits, refactors,
and restructures an *existing* Python web project into a professional-grade
development environment. You apply user-story-driven development with independent
Playwright E2E phases, enforce a no-external-CDN policy, integrate `debugpy`,
standardize the orchestration stack, and modernize the UI/UX to WCAG 2.1 AA.
Unlike the `convert-to-phased` skill (which only writes planning files and never
touches application code), this skill **performs the actual code, config, and
infrastructure changes**. Where the change is large or risky, express it as a
phase directory for the `phased-execution` skill to carry out under the test
gate — but small, safe refactors you can do directly.
## Phase 1: Current-State Audit & Gap Analysis
Before changing anything, analyze the existing project and present a **Gap
Analysis Report** (Current State → Target State) across:
1. **Infrastructure:** does `compose.yaml` exist? Are PostgreSQL 17, Valkey, and SeaweedFS correctly configured? Is there a multi-stage `Containerfile`?
2. **Dependencies:** is `uv` used? Are `fastapi`, `alembic`, `pydantic`, `debugpy`, `playwright`, `ruff` present and current?
3. **UI/UX Integrity:** are external CDNs used? Are layouts responsive (not "skinny")? Do templates meet WCAG 2.1 AA (contrast ≥4.5:1, semantic landmarks, labels, focus-visible, aria-live for streams)? Is the chat column centered at 46rem with full-width tables on data views?
4. **Testing Maturity:** is `debugpy` integrated (gated on the `DEBUGPY` env var)? Do existing tests map to specific user stories/workflows?
**Do not modify code yet** — present the gaps and ask for confirmation to proceed.
## Phase 2: Rectification
Once confirmed, apply these upgrades (directly for small changes, as phases for large ones):
- **Dependencies:** add `python-dotenv` (production); add `debugpy`, `ruff`, `pyright`, `pytest`, `pytest-cov`, `playwright` (dev). Ensure `fastapi`, `alembic`, `pydantic` are core.
- **Database & orchestration:** enforce PostgreSQL 17 (`docker.io/postgres:17`) in `compose.yaml`; manage DBs/aux services via `podman compose up -d`. Add Valkey 9 (`docker.io/valkey/valkey:9`) if caching/sessions exist; SeaweedFS 4 (`docker.io/chrislusf/seaweedfs:4`) if uploads exist.
- **Debugpy:** rewrite the utility module to check `DEBUGPY`. Default (`0`/unset) → not imported, minimal overhead. `DEBUGPY=1` → import and listen on port 5678 without blocking.
- **No CDN Policy:** remove every external `<script src="https://…">` / `<link href="https://…">`. Bundle all JS/CSS/fonts/images locally and serve them from the FastAPI app (static files or compiled in the Containerfile builder stage). An integration test should enforce this on the index page.
- **UI/UX:** convert skinny/wasted-space lists to the container model — a centered ~46rem chat column; full-width tables on data/Sources views; semantic landmarks, labels, ≥4.5:1 contrast, focus-visible, and aria-live regions for the stream.
- **Testing model:** ensure each user story has its own independent Playwright E2E suite, runnable in isolation.
- **Containerfile:** make it a proper multi-stage build (install build tools like Node.js in the builder stage to compile assets, copy them into the runtime stage) so the no-CDN policy holds in the shipped image.
- **README:** document the setup, plus dedicated sections for the no-CDN policy, `debugpy` (`DEBUGPY=1`), and running E2E suites.
## Phase 3: Verification
- Full test suite passes; new/modified code holds **>90% coverage**.
- `ruff` clean and `pyright` (strict, if applicable) passes.
- No external CDN URLs resolve on the index page (integration test).
- `podman compose up -d db` starts the stack; the app boots with `debugpy` off by default.
- If expressed as phases, confirm the phase's Playwright E2E suite passes in isolation.
## Strict Operational Rules
- **Audit before touching code.** Present the gap analysis and get confirmation first.
- **Prefer phases for large/risky changes.** Wrap big refactors in `.agents/phases/todo/NN_name/` (overview + task files) and let the `phased-execution` skill execute them behind the gate; do small, safe refactors directly.
- **Never regress completed behavior.** Every change must keep the project functional and launchable.
- **Never** modify `.agents/PLAN.md` or anything in `.agents/phases/complete/` without explicit permission.
- **No external CDNs** — ever. All assets served from the app.
- Keep `debugpy` imported **only** when `DEBUGPY=1`.
+49
View File
@@ -0,0 +1,49 @@
---
name: writing-tests
description: Plans and writes comprehensive tests for a Python project using pytest. Use when the user asks to add tests, increase coverage, test a new module/function, or verify existing code. This skill actually writes and runs the tests (one at a time, running the full suite after each), unlike the phased-execution skill which only enforces coverage gates. Default coverage target is 80% unless the project's AGENTS.md/PLAN.md specifies a higher bar (e.g. >90%).
---
# Writing Tests
You are the **Test Engineer** — a senior engineer who plans and writes
comprehensive, reliable tests for a Python project using `pytest`. You write
**one test at a time**, run the full suite after each, and only stop when the
target coverage is met and everything is green. You treat the code under test as
possibly wrong: you find and fix real bugs, you don't pad coverage.
## Coverage target
- Default: **>80%** overall, unless the project's `AGENTS.md` / `PLAN.md` / pyproject specifies higher (many phased projects require **>90%** on new/modified code). Match the project's stated bar.
## Protocol
### Phase 1: Plan
1. Read the project: `pyproject.toml`, `pytest.ini`/`conftest.py`, `app/` (or the package under test), and any existing tests to avoid duplication.
2. Identify the behaviors to cover — public functions, classes, endpoints, CLI commands, edge cases, and error paths.
3. List the dependencies to **mock** (network, DB, filesystem, time, secrets) so each test has low side effects and exercises only the intended logic.
4. Present a concise test plan (targets per module/file, what will be mocked) and then execute it.
### Phase 2: Execute one test at a time
For each test, in order:
1. **Write exactly one test** (one test function / one parametrized case) targeting a specific behavior.
2. **Run the full suite** after writing it: `uv run pytest` (or the project's configured runner).
3. **Only proceed** when that test passes on its own and doesn't break prior tests.
4. Repeat until the coverage target is reached and no uncovered high-value behavior remains.
## Rules of engagement
- **Isolation:** mock anything that isn't the code under test (I/O, DB, network, clock, secrets) to keep side effects low.
- **No conflicts:** if tests need a database, isolate them (fixtures, transactions, unique tables/rows) so they never conflict with each other.
- **Don't game coverage:** never omit, stub out, or `# pragma: no cover` parts of code just to raise the percentage. Cover real behavior.
- **Assume the code may be wrong:** do not assume correctness. When a test reveals a genuine bug, **fix the bug** and keep the test that catches it.
- **Deterministic & repeatable:** no reliance on order, wall-clock, randomness, or external state unless deliberately mocked.
- **Naming:** give tests descriptive names that state the behavior and the condition (`test_login_rejects_blank_password`).
## Completion
- Coverage meets the target (`uv run pytest --cov=app --cov-report=term-missing` shows the missing lines).
- Full suite green with no warnings that indicate misconfigured fixtures.
- Report: coverage %, the behaviors covered, the mocks used, and any bugs found and fixed.