Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions plugin/agents/builder.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,40 @@
---
name: builder
description: FrinkLoop builder — implements one task in a worktree. The default workhorse. Reads task spec, edits files, commits. Implemented in Plan 2.
description: FrinkLoop builder — implements ONE task. Reads task spec, edits files, runs local checks, commits. Default workhorse for kind=feature/fix/test/doc. Sequential in Plan 2; worktree-isolated parallelism arrives in Plan 4.
---

# builder

Placeholder. Will be implemented in Plan 2.
## Inputs
- One task JSON from `<project>/.frinkloop/tasks.json` (passed by mvp-loop)
- `<project>/.frinkloop/spec.md` for context
- The full project working tree at `$PROJECT_DIR`

## Output
- One or more file edits / additions inside `$PROJECT_DIR/`
- One git commit per task with conventional-commit message: `<kind>(<scope>): <title>`
- No artifact file required (qa writes that separately)

## Job

1. Read the task title + kind. Plan the smallest set of edits that satisfies the task without scope creep.
2. If kind=test: write the test FIRST. Run it. Confirm it fails. (TDD discipline.)
3. If kind=feature: implement the smallest code that satisfies the task. If a paired test task exists ahead in the queue, the planner already enforced TDD ordering — your job is just to make it pass.
4. If kind=fix: read the parent task's failure mode (in `qa.json` and `decisions.md`). Make the minimal fix.
5. If kind=doc: edit README, JSDoc, or in-code comments only.
6. Run any obvious local check (typecheck, lint) before committing — but the qa subagent runs the formal verification, so don't overdo it.
7. Stage and commit:

```bash
git add <specific files>
git commit -m "<kind>(<scope>): <task title>"
```

## Constraints
- Write only inside `$PROJECT_DIR/`. Never edit the plugin.
- Don't refactor unrelated code. Stick to the task.
- Don't add new dependencies without an explicit task instruction.
- Don't push to a remote. Plan 8 handles deploy.

## Failure handling
If you can't make progress, return with status `BLOCKED` and a one-line reason. The mvp-loop will queue a fix task or escalate.
39 changes: 37 additions & 2 deletions plugin/agents/planner.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,43 @@
---
name: planner
description: FrinkLoop planner — turns spec changes into task deltas in tasks.json. Reads spec.md and current tasks.json; outputs a JSON-patch-style delta that the loop applies. Implemented in Plan 2.
description: FrinkLoop planner — turns a frozen spec into a tasks.json (initial plan) or applies deltas when the spec changes mid-loop. Inputs: spec.md + current tasks.json. Output: a new tasks.json (or a JSON patch) committed to disk. One-shot.
---

# planner

Placeholder. Will be implemented in Plan 2.
## Inputs
- `<project>/.frinkloop/spec.md` — frozen YC-shaped spec (Does / For / MVP proves / Done / In-MVP / Phase-2)
- `<project>/.frinkloop/tasks.json` — current task tree (may be empty on first run)
- `<project>/.frinkloop/config.yaml` — for tdd flag

## Output
- A new `<project>/.frinkloop/tasks.json` validated against `plugin/lib/schemas/tasks.schema.json`
- Append a one-paragraph rationale to `<project>/.frinkloop/decisions.md` describing the milestones chosen

## Job

Given the spec, decide:
1. Milestones (typically 3–6): coarse phases like "Scaffold", "Core flow", "Polish & deliver".
2. Tasks per milestone: each is a single 5–30 min unit of work. Use the `kind` enum: scaffold, feature, test, fix, doc, deploy, screenshot.
3. `depends_on` between tasks where order matters.
4. If `tdd: true` in config.yaml, every `kind=feature` task gets a paired `kind=test` task that comes BEFORE it in dependency order.

Use `T01..TNN` as ids, two digits, sequential across the whole project (not per milestone).

## What you must NOT do
- Do not add tasks for things in the Phase-2 list (the spec already defers them).
- Do not introduce new fields outside the schema.
- Do not write to anywhere other than `tasks.json` and `decisions.md`.

## Validation
After writing, run:

```bash
npx --no-install ajv validate -s plugin/lib/schemas/tasks.schema.json \
-d "$FRINKLOOP_DIR/tasks.json" --strict=false
```

If it fails, fix tasks.json before exiting.

## Status report
Return: number of milestones, number of tasks, whether tdd was applied. The orchestrator (mvp-loop) will read tasks.json directly — your prose response is for the human-readable log only.
24 changes: 22 additions & 2 deletions plugin/agents/qa.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
---
name: qa
description: FrinkLoop QA — runs tests, typecheck, lint after each task and milestone. Writes qa.json artifact. Implemented in Plan 2.
description: FrinkLoop QA — verifies a builder's task by running tests, typecheck, and lint where applicable. Writes qa.json artifact validated against schemas/qa-result.schema.json.
---

# qa

Placeholder. Will be implemented in Plan 2.
## Inputs
- One task JSON (passed by mvp-loop)
- The post-builder working tree at `$PROJECT_DIR`

## Output
- `<project>/.frinkloop/qa.json` validated against `plugin/lib/schemas/qa-result.schema.json`
- Optionally one or more diagnostic snippets quoted in qa.json under `output_excerpt`

## Job

Run `bash plugin/lib/verify.sh; verify_task '<task json>'`. The helper handles the kind-driven branching (lightweight kinds vs. test-running kinds).

If `verify_task` returns 0, qa.json shows `outcome=pass`. If non-zero, qa.json shows `outcome=fail` with `error_summary` populated.

## What you must NOT do
- Do not modify project files. You are read-only against the working tree.
- Do not skip checks. If the helper reports `unknown-kind`, that's a real failure.
- Do not write anything outside `$FRINKLOOP_DIR/`.

## Reporting
Return: outcome (pass/fail) and the path to qa.json. The orchestrator reads qa.json directly.
12 changes: 10 additions & 2 deletions plugin/commands/frinkloop-pause.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ description: Pause a running FrinkLoop loop, flush state, write a handoff.

# /frinkloop pause <project>

(Plan 2.) Will set state.status = "paused", append a final iteration-log line, and trigger `/handoff`.
Pause the build loop for `<project>`.

For now: print "Pause arrives in Plan 2."
## Steps

1. Resolve `<project>` and export `FRINKLOOP_DIR`, `PROJECT_DIR` as in resume.
2. Source `plugin/lib/state.sh`.
3. `state_set status paused`.
4. `log_iteration '{"event":"pause","reason":"user-requested"}'`.
5. Trigger the user's `/handoff` skill so the handoff lands in the project Handoffs dir, `~/.claude/handoffs/`, the Obsidian vault, and Notion (for opted-in projects).
6. Print a one-line confirmation: `paused at iteration <N>, milestone <id>, task <id>`.
7. Exit. The Stop hook will not re-prompt because status=paused.
17 changes: 15 additions & 2 deletions plugin/commands/frinkloop-resume.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ description: Resume a paused or quota-stopped FrinkLoop loop for the named proje

# /frinkloop resume <project>

(Plan 2.) Will load `<project>/.frinkloop/state.json`, validate, and resume the `mvp-loop` skill.
Resume the build loop for `<project>`.

For now: print "Resume arrives in Plan 2."
## Steps

1. Resolve `<project>` to a directory (treat as path; if relative, resolve against `~/Developer/`).
2. Export `FRINKLOOP_DIR=<project>/.frinkloop` and `PROJECT_DIR=<project>`.
3. Validate `state.json` against `plugin/lib/schemas/state.schema.json`. If invalid, abort with a clear error.
4. Source `plugin/lib/state.sh`, `plugin/lib/loop.sh`, `plugin/lib/verify.sh`, `plugin/lib/recovery.sh`.
5. Run `resume_or_block` (from `recovery.sh`). It checks the working tree.
- If output is `block` → tell the user the working tree was modified and a blocker was logged. Stop.
- If output is `resume` → continue.
6. Set `status=running` via `state_set status running`.
7. Print a one-line status summary (status, current_milestone, current_task, iteration_count).
8. Hand off to the `mvp-loop` skill with PROMPT.md re-fed.

The Stop hook will then keep the loop ticking until DONE.
23 changes: 21 additions & 2 deletions plugin/hooks/post-iteration.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,23 @@
#!/usr/bin/env bash
# FrinkLoop post-iteration hook — placeholder.
# Plan 2 will implement: append a structured iteration-log.jsonl line.
# FrinkLoop post-iteration hook.
# Increments iteration_count and appends an iteration-log entry.
# FRINKLOOP_DIR must be exported.

set -euo pipefail

: "${FRINKLOOP_DIR:=}"

if [ -z "$FRINKLOOP_DIR" ] || [ ! -f "$FRINKLOOP_DIR/state.json" ]; then
exit 0
fi

# Source state helpers via path relative to this hook.
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$HOOK_DIR/../lib/state.sh"

state_increment_iteration

iter=$(state_get iteration_count)
log_iteration "$(jq -nc --arg i "$iter" '{event:"iteration", iter:($i|tonumber)}')"

exit 0
36 changes: 33 additions & 3 deletions plugin/hooks/stop.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,34 @@
#!/usr/bin/env bash
# FrinkLoop Stop hook — placeholder.
# Plan 2 will implement: re-feed PROMPT.md to keep the loop running until DONE.
exit 0
# FrinkLoop Stop hook.
# Exit 0 → let the session end.
# Exit 2 → continue the loop (Claude Code re-prompts the model).
# FRINKLOOP_DIR must be exported by the session preamble.

set -euo pipefail

: "${FRINKLOOP_DIR:=}"

if [ -z "$FRINKLOOP_DIR" ] || [ ! -f "$FRINKLOOP_DIR/state.json" ]; then
exit 0
fi

status=$(jq -r '.status' "$FRINKLOOP_DIR/state.json")

case "$status" in
done|paused|blocked|quota-stopped|idle)
exit 0
;;
running)
if [ ! -f "$FRINKLOOP_DIR/tasks.json" ]; then
exit 0
fi
pending_count=$(jq '[.milestones[].tasks[] | select(.status == "pending")] | length' "$FRINKLOOP_DIR/tasks.json")
if [ "$pending_count" -gt 0 ]; then
exit 2
fi
exit 0
;;
*)
exit 0
;;
esac
120 changes: 120 additions & 0 deletions plugin/lib/loop.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env bash
# FrinkLoop loop helpers — task picking, status mutations, decisions log.
# Caller must export FRINKLOOP_DIR and source plugin/lib/state.sh first.

set -euo pipefail

: "${FRINKLOOP_DIR:?FRINKLOOP_DIR must be set}"

# Returns the id of the first in-progress or pending milestone, or empty string.
active_milestone() {
jq -r '
.milestones[]
| select(.status == "in-progress" or .status == "pending")
| .id
' "$FRINKLOOP_DIR/tasks.json" | head -1
}

# Returns the task id of the next task to work on, or empty string if none.
# Skips tasks whose depends_on still contain pending task ids.
pick_next_task() {
local mid
mid=$(active_milestone)
if [ -z "$mid" ]; then
echo ""
return 0
fi

jq -r --arg mid "$mid" '
(.milestones[] | select(.id == $mid) | .tasks) as $tasks
| ($tasks | map(select(.status == "pending")) | map(.id)) as $pending_ids
| $tasks
| map(select(.status == "pending"))
| map(select(
((.depends_on // []) | length == 0)
or
((.depends_on // []) | all(. as $dep | $pending_ids | index($dep) | . == null))
))
| .[0].id // ""
' "$FRINKLOOP_DIR/tasks.json"
}

# Mark a task done by id; append an entry to decisions.md.
mark_task_done() {
local task_id="$1"
local note="${2:-}"
local path="$FRINKLOOP_DIR/tasks.json"
local tmp
tmp=$(mktemp)
jq --arg tid "$task_id" '
.milestones |= map(
.tasks |= map(
if .id == $tid then .status = "done" else . end
)
)
' "$path" > "$tmp"
mv "$tmp" "$path"

local ts
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
printf "\n## %s — %s\n%s\n" "$ts" "$task_id" "$note" >> "$FRINKLOOP_DIR/decisions.md"
}

# Queue a new fix task that depends on parent_task_id.
# Appends the fix task to the active milestone and returns the new task id.
queue_fix_task() {
local parent="$1"
local error_summary="$2"
local path="$FRINKLOOP_DIR/tasks.json"
local mid
mid=$(active_milestone)

# Generate next id: T<N+1> zero-padded to 2 digits
local next_id
next_id=$(jq -r '
[.milestones[].tasks[].id | ltrimstr("T") | tonumber] | max as $m
| ($m + 1) as $n
| "T" + (if $n < 10 then "0" else "" end) + ($n | tostring)
' "$path")

local tmp
tmp=$(mktemp)
jq --arg mid "$mid" --arg pid "$parent" --arg nid "$next_id" --arg err "$error_summary" '
.milestones |= map(
if .id == $mid
then .tasks += [{
"id": $nid,
"title": ("Fix: " + $err),
"status": "pending",
"kind": "fix",
"depends_on": [$pid],
"retries": 0
}]
else . end
)
' "$path" > "$tmp"
mv "$tmp" "$path"

echo "$next_id"
}

# Mark a milestone done if and only if all its tasks are done.
# Returns 1 (no-op) if any task is still not done.
mark_milestone_done() {
local mid="$1"
local path="$FRINKLOOP_DIR/tasks.json"
local all_done
all_done=$(jq -r --arg mid "$mid" '
.milestones[] | select(.id == $mid)
| (.tasks | map(.status == "done") | all)
' "$path")
if [ "$all_done" != "true" ]; then
return 1
fi
local tmp
tmp=$(mktemp)
jq --arg mid "$mid" '
.milestones |= map(if .id == $mid then .status = "done" else . end)
' "$path" > "$tmp"
mv "$tmp" "$path"
}
35 changes: 35 additions & 0 deletions plugin/lib/recovery.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# FrinkLoop crash recovery: detect mid-loop user edits, open blockers.

set -euo pipefail

: "${FRINKLOOP_DIR:?FRINKLOOP_DIR must be set}"

# Returns 0 if working tree is clean, 1 if dirty.
detect_dirty_tree() {
local out
out=$(git status --porcelain 2>/dev/null)
if [ -z "$out" ]; then
return 0
fi
return 1
}

open_blocker() {
local task_id="$1"
local reason="$2"
local ts
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
printf "\n## %s — BLOCKED on %s\n%s\n" "$ts" "$task_id" "$reason" >> "$FRINKLOOP_DIR/blockers.md"
}

# Decides whether to resume the loop or open a blocker.
# Prints "resume" or "block" to stdout.
resume_or_block() {
if detect_dirty_tree; then
echo "resume"
return 0
fi
open_blocker "<resume>" "Working tree dirty on resume — user may have edited files mid-loop. Manual cleanup required before resume."
echo "block"
}
Loading