Build out all 27 modules + capstone (#1)

Co-authored-by: claude <claude@jpaul.io>
Co-committed-by: claude <claude@jpaul.io>
This commit was merged in pull request #1.
This commit is contained in:
2026-06-22 12:19:01 -04:00
committed by Claude (agent)
parent 4bd586bbd0
commit 2684095e2f
117 changed files with 15131 additions and 1 deletions
@@ -0,0 +1,24 @@
{
"summary": "Adds a `clear` command. The core logic is fine, but the CLI handler never persists the change, so the command looks like it works while doing nothing on disk. No test covers the new behavior.",
"recommendation": "request_changes",
"comments": [
{
"file": "cli.py",
"line": 49,
"severity": "blocker",
"comment": "The `clear` branch never calls save(tlist). The list is emptied in memory and the process exits, so tasks.json is untouched. It prints 'cleared all tasks' but the next `list` shows everything still there — a silent no-op. Add save(tlist) before printing."
},
{
"file": "tasks.py",
"line": 28,
"severity": "suggestion",
"comment": "No test covers clear(). Add one that adds two tasks, calls clear(), and asserts the list is empty — matching the Module 13 suite style."
},
{
"file": "tasks.py",
"line": 28,
"severity": "nit",
"comment": "clear() rebinds with self.tasks = []; self.tasks.clear() is equivalent and avoids replacing the list object. Minor."
}
]
}
@@ -0,0 +1,6 @@
{
"labels": ["type:bug", "priority:p2", "area:cli", "ready:ai-ready"],
"assignee_type": "agent",
"rationale": "Reproducible crash with exact steps and environment, and the fix is small and well-scoped (add a bounds check / friendly error in the `done` branch, mirroring how the other commands handle empty state). No data loss, so p2. Clear enough to hand to an issue-to-PR agent.",
"confidence": "high"
}
@@ -0,0 +1,39 @@
diff --git a/cli.py b/cli.py
index 91e9276..b2c4f1a 100644
--- a/cli.py
+++ b/cli.py
@@ -31,7 +31,7 @@ def save(tlist: TaskList) -> None:
def main(argv: list[str]) -> int:
tlist = load()
if not argv:
- print("usage: python cli.py [add <title> | list | done <index>]")
+ print("usage: python cli.py [add <title> | list | done <index> | clear]")
return 1
command = argv[0]
@@ -45,6 +45,9 @@ def main(argv: list[str]) -> int:
elif command == "done":
tlist.complete(int(argv[1]))
save(tlist)
print("updated")
+ elif command == "clear":
+ tlist.clear()
+ print("cleared all tasks")
else:
print(f"unknown command: {command}")
return 1
diff --git a/tasks.py b/tasks.py
index 5d7d637..a1b2c3d 100644
--- a/tasks.py
+++ b/tasks.py
@@ -25,6 +25,9 @@ class TaskList:
return task
def complete(self, index: int) -> None:
self.tasks[index].done = True
+ def clear(self) -> None:
+ self.tasks = []
+
def pending(self) -> list[Task]:
return [t for t in self.tasks if not t.done]
@@ -0,0 +1,49 @@
# Label taxonomy — the triage agent's instructions
The triage agent reads this file, then reads one incoming issue, and proposes labels, a priority,
and where the issue should be routed. Like the review rubric, this is committed and versioned: your
triage taxonomy is a project decision, not a setting buried in some bot's web UI.
**The labels below are the only labels that exist.** The agent must choose from this list. If it
invents a label that isn't here, the lab's `triage.py` rejects the whole suggestion — that rejection
is a guardrail, not a bug. An agent that can mint arbitrary labels is an agent that can quietly
reshape your taxonomy; keeping the allowed set in version control and validating against it is how
you keep the agent inside its lane (the least-privilege idea from Module 22).
## Allowed labels
Type (exactly one):
- `type:bug` — something is broken or behaves wrong
- `type:feature` — a request for new behavior
- `type:docs` — documentation only
- `type:question` — a usage question, not a code change
Priority (exactly one):
- `priority:p0` — data loss, security, or the app is unusable for everyone
- `priority:p1` — a serious bug with no good workaround
- `priority:p2` — a real bug with a workaround, or a wanted feature
- `priority:p3` — minor, cosmetic, or nice-to-have
Area (zero or more):
- `area:cli` — the command-line front end (`cli.py`)
- `area:core` — task logic (`tasks.py`)
- `area:docs` — README and lesson text
Readiness (exactly one) — this is the one that decides routing, and it's the Module 9 idea made
concrete: an issue can go to a person *or* be handed to an agent.
- `ready:ai-ready` — small, well-scoped, reproducible; safe to hand to an issue-to-PR agent (the
kind of agent Module 25 builds). Route `assignee_type: agent`.
- `ready:needs-human` — ambiguous, risky, or needs a product decision. Route `assignee_type: human`.
## Output format
Return one JSON object, nothing else:
```json
{
"labels": ["type:bug", "priority:p2", "area:cli", "ready:ai-ready"],
"assignee_type": "agent | human",
"rationale": "one or two sentences justifying the labels and the route",
"confidence": "high | medium | low"
}
```
@@ -0,0 +1,41 @@
# Review rubric — the AI reviewer's instructions
This is the committed instruction set the AI reviewer reads before it looks at a diff. It lives in
the repo on purpose: like the committed AI config from Module 5 and the skills from Module 21, a
review rubric is a durable, versioned artifact. Change how the reviewer behaves and that change
arrives as a diff in a PR, reviewable like any other.
Keep it short and opinionated. A vague rubric produces vague, noisy comments — the fastest way to
get a team to ignore the AI reviewer entirely.
## What to check, in priority order
1. **Plausibility traps (the Module 10 skill).** Code that reads correctly but does the wrong thing:
a handler that prints success without persisting, an off-by-one, a branch that silently no-ops.
This is the highest-value thing you can catch.
2. **Missing tests.** New behavior with no test in the suite (Module 13). Name the specific case.
3. **Security smells (Module 15).** Hardcoded secrets, shelling out on unsanitized input, a new
dependency that doesn't obviously exist.
4. **Correctness on edge cases.** Empty input, bad index, missing file.
5. **Style nits — last, and clearly labeled.** Only if they matter. Nits drown signal.
## How to comment
- Be specific: file, line, what's wrong, and the fix. "This could be cleaner" is useless.
- Label every comment with a severity: `blocker`, `suggestion`, or `nit`.
- You do **not** approve, request changes as a gate, or merge. You produce comments and a
recommendation. A human decides what happens.
## Output format
Return one JSON object, nothing else:
```json
{
"summary": "one or two sentences on the overall state of the diff",
"recommendation": "comment | request_changes",
"comments": [
{"file": "cli.py", "line": 49, "severity": "blocker", "comment": "..."}
]
}
```
@@ -0,0 +1,98 @@
"""Assistive AI reviewer — local simulation of a PR-reviewer bot.
This stands in for a forge-native reviewer (an app/bot triggered when a PR opens, running on a
runner from Module 19) without needing any hosted account. It does the two deterministic halves of
the job and leaves the one judgment call — what actually happens to the PR — to you.
python reviewer.py prompt # assemble the prompt: rubric + diff. Paste to your AI.
python reviewer.py apply ai-review.sample.json # ingest the AI's JSON, render it, gate it
The point of this module: the agent produces comments and a recommendation. It never approves,
never requests-changes-as-a-gate, never merges. The `apply` step ends at a HUMAN DECISION, every
time. Stdlib only — no pip install.
"""
import argparse
import json
import sys
from pathlib import Path
HERE = Path(__file__).parent
PROMPT_HEADER = """\
You are an assistive code reviewer. Follow the rubric below exactly, then review the diff that
follows it. Return ONLY the JSON object the rubric specifies — no prose before or after.
================ REVIEW RUBRIC ================
{rubric}
================ DIFF UNDER REVIEW ============
{diff}
"""
def cmd_prompt(args: argparse.Namespace) -> int:
rubric = Path(args.rubric).read_text()
diff = Path(args.patch).read_text()
print(PROMPT_HEADER.format(rubric=rubric, diff=diff))
return 0
def cmd_apply(args: argparse.Namespace) -> int:
try:
review = json.loads(Path(args.response).read_text())
except (json.JSONDecodeError, FileNotFoundError) as exc:
print(f"error: could not read a JSON review from {args.response}: {exc}")
return 1
summary = review.get("summary", "(no summary)")
recommendation = review.get("recommendation", "comment")
comments = review.get("comments", [])
print("=" * 70)
print("AI REVIEWER — first pass (advisory only)")
print("=" * 70)
print(f"\nSummary: {summary}\n")
if not comments:
print("No line comments.\n")
order = {"blocker": 0, "suggestion": 1, "nit": 2}
for c in sorted(comments, key=lambda c: order.get(c.get("severity", "nit"), 9)):
sev = c.get("severity", "nit").upper()
loc = f"{c.get('file', '?')}:{c.get('line', '?')}"
print(f" [{sev:10}] {loc}")
print(f" {c.get('comment', '')}\n")
print("-" * 70)
print(f"Agent's recommendation: {recommendation}")
print("-" * 70)
print(
"\nThis is the human decision gate. The agent did NOT merge, approve, or block.\n"
"It only commented. You decide what happens next:\n"
" - merge you read the comments, you disagree or they're addressed\n"
" - request changes you agree; push the fix on the branch and re-run\n"
" - dismiss the agent is wrong or noisy; ignore and move on\n"
"\nNothing in this repo changes until you act. That's the whole point of Module 24.\n"
)
return 0
def main(argv: list[str]) -> int:
parser = argparse.ArgumentParser(description=__doc__)
sub = parser.add_subparsers(dest="cmd", required=True)
p = sub.add_parser("prompt", help="assemble the review prompt to paste to your AI")
p.add_argument("--rubric", default=str(HERE / "review-rubric.md"))
p.add_argument("--patch", default=str(HERE / "feature.patch"))
p.set_defaults(func=cmd_prompt)
a = sub.add_parser("apply", help="ingest the AI's JSON review and render the decision gate")
a.add_argument("response", help="path to the JSON the AI returned")
a.set_defaults(func=cmd_apply)
args = parser.parse_args(argv)
return args.func(args)
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,14 @@
Title: `done` command crashes on an empty list
When I run `python cli.py done 0` right after a fresh checkout — before adding any tasks — it throws
an IndexError and dumps a stack trace instead of a friendly message. Every other command handles the
empty-list case fine, so this one feels like an oversight.
Steps to reproduce:
1. Delete tasks.json (or clone fresh).
2. Run `python cli.py done 0`.
3. See the traceback.
Expected: a clear message like "no task at index 0", exit non-zero, no traceback.
Environment: Python 3.12, macOS.
+110
View File
@@ -0,0 +1,110 @@
"""Assistive issue-triage agent — local simulation of a triage bot.
Stands in for a forge-native triage agent (triggered when an issue opens) without a hosted account.
It assembles the prompt, then validates and renders the AI's suggestion — and stops at a human
confirm. The agent proposes labels and a route; it does not apply them.
python triage.py prompt # taxonomy + issue -> prompt. Paste to your AI.
python triage.py apply ai-triage.sample.json # validate + render + confirm gate
The validation step matters: the agent may only use labels that exist in label-taxonomy.md. A
hallucinated label is rejected. Stdlib only — no pip install.
"""
import argparse
import json
import re
import sys
from pathlib import Path
HERE = Path(__file__).parent
PROMPT_HEADER = """\
You are an assistive issue-triage agent. Using ONLY the taxonomy below, propose labels, a route,
and a rationale for the issue that follows. Return ONLY the JSON object the taxonomy specifies.
================ LABEL TAXONOMY ===============
{taxonomy}
================ INCOMING ISSUE ===============
{issue}
"""
# Allowed labels are the backticked `prefix:value` tokens in the taxonomy file. Keeping the source
# of truth in the committed markdown — not hardcoded here — is the point.
LABEL_RE = re.compile(r"`([a-z]+:[a-z0-9-]+)`")
def allowed_labels(taxonomy_text: str) -> set[str]:
return set(LABEL_RE.findall(taxonomy_text))
def cmd_prompt(args: argparse.Namespace) -> int:
taxonomy = Path(args.taxonomy).read_text()
issue = Path(args.issue).read_text()
print(PROMPT_HEADER.format(taxonomy=taxonomy, issue=issue))
return 0
def cmd_apply(args: argparse.Namespace) -> int:
allowed = allowed_labels(Path(args.taxonomy).read_text())
try:
sug = json.loads(Path(args.response).read_text())
except (json.JSONDecodeError, FileNotFoundError) as exc:
print(f"error: could not read a JSON suggestion from {args.response}: {exc}")
return 1
labels = sug.get("labels", [])
bogus = [l for l in labels if l not in allowed]
if bogus:
print("=" * 70)
print("REJECTED — the agent suggested labels that aren't in the taxonomy:")
for l in bogus:
print(f" - {l}")
print(
"\nThis is the guardrail working. The agent can only use labels you've committed to\n"
"label-taxonomy.md. Fix the prompt or the taxonomy and re-run; do not apply this.\n"
)
return 1
print("=" * 70)
print("TRIAGE AGENT — suggestion (advisory only)")
print("=" * 70)
print(f"\n Labels: {', '.join(labels) or '(none)'}")
print(f" Route to: {sug.get('assignee_type', '?')}")
print(f" Confidence: {sug.get('confidence', '?')}")
print(f" Rationale: {sug.get('rationale', '')}\n")
print("-" * 70)
print(
"Human confirm gate. The agent did NOT apply these labels or assign anyone.\n"
"You decide:\n"
" - confirm apply the labels and route as proposed\n"
" - edit change a label or the route, then apply\n"
" - reject the triage is wrong; do it yourself\n"
"\nA wrong label here costs one glance and one click to fix — which is exactly why\n"
"triage is the safe place to let an agent in first.\n"
)
return 0
def main(argv: list[str]) -> int:
parser = argparse.ArgumentParser(description=__doc__)
sub = parser.add_subparsers(dest="cmd", required=True)
p = sub.add_parser("prompt", help="assemble the triage prompt to paste to your AI")
p.add_argument("--taxonomy", default=str(HERE / "label-taxonomy.md"))
p.add_argument("--issue", default=str(HERE / "sample-issue.md"))
p.set_defaults(func=cmd_prompt)
a = sub.add_parser("apply", help="validate + render the AI's suggestion, then gate it")
a.add_argument("response", help="path to the JSON the AI returned")
a.add_argument("--taxonomy", default=str(HERE / "label-taxonomy.md"))
a.set_defaults(func=cmd_apply)
args = parser.parse_args(argv)
return args.func(args)
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))