diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 18b10145f..50f4d2441 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,5 +1,5 @@
@@ -10,10 +10,18 @@ Part of #
+## What changes
+
+
+
## What you will notice
+## How we know it works
+
+
+
## How this serves fizzy
Anchors: speed, quality, demos, mobile, platform, agents, collaboration, materials; principles: zig-quality, lean,
measured, respect, every-platform, delight, craft, app-owns-look, evidence. -->
-## How we know it works
-
-
-
For agents: technical summary
## Goal
@@ -54,3 +58,5 @@ measured, respect, every-platform, delight, craft, app-owns-look, evidence. -->
+
+
diff --git a/docs/AGENT_WORK.md b/docs/AGENT_WORK.md
index de204e9e7..e2f314823 100644
--- a/docs/AGENT_WORK.md
+++ b/docs/AGENT_WORK.md
@@ -104,49 +104,74 @@ bottom. The body, in order:
1. `## In plain words`: two to four sentences. What changes for a person using fizzy, and why it
matters. No function, file or flag names.
-2. `## What you will notice`: what a person sees or feels, or `Nothing visible: internal` and
+2. `## What changes`: bullets, in plain terms, with at least one fenced code snippet taken from the
+ diff (a before/after or the key function, 15 lines at most), so a reader sees the change itself.
+3. `## What you will notice`: what a person sees or feels, or `Nothing visible: internal` and
what it makes possible.
-3. `## How this serves fizzy`: one sentence naming the goal or principle the change moves and the
- result for people, linked to its anchor in [`VISION.md`](VISION.md), for example
- `[Phones and tablets](https://github.com/fizzyedit/fizzy/blob/main/docs/VISION.md#mobile)`.
4. `## How we know it works`: in plain terms: the tests that fail without the change, the check
on a real device, the measurement.
-5. A collapsed `For agents: technical summary
` block holding `## Goal`,
+5. `## How this serves fizzy`: one sentence naming the goal or principle the change moves and the
+ result for people, linked to its anchor in [`VISION.md`](VISION.md), for example
+ `[Phones and tablets](https://github.com/fizzyedit/fizzy/blob/main/docs/VISION.md#mobile)`.
+6. A collapsed `For agents: technical summary
` block holding `## Goal`,
`## What changed and why`, `## How it was verified` and `## Not done / follow-ups`.
-
-The title follows the same rule: `area: what changes`, in words a visitor understands. The stage
-is not in the body: the PM's status comment, kept current, opens with it in one plain sentence.
+7. The **agent notes**, the last line of the body: a hidden comment
+ ``.
+ It is the one known place for agents to leave each other notes; agents read and write only this
+ block (and the PR thread for findings), never free text in the body. `agent-finish` writes it
+ (worker and slot from `FIZZY_WORKER` and `FIZZY_SLOT`, `status` `draft` at the claim and `ready`
+ after the gates) and on a rerun keeps the `open_questions` and `next` the last agent set.
+
+The title follows the same rule: `area: what changes`, 72 characters at most, in words a visitor
+understands. It is never a branch slug: `windows: work-723` and `linux: touch-slop` are refused.
+The claim's draft PR takes the work issue's title; the finished title is the first line
+`# area: what changes` of `PR_SUMMARY.md`, which `agent-finish` sets. The stage is not in the body:
+the PM's status comment, kept current, opens with it in one plain sentence.
A worker writes the plain sections first, in `PR_SUMMARY.md` (step 5 below), and
`scripts/agent-finish` puts the technical half in the collapsed block. `scripts/check-pr-body
-` checks a body: the four sections are there and not empty, the vision link names
-an anchor that exists, and the block is there. `agent-finish` runs it before the gates and
-refuses to publish a body that fails; the PM runs it on a person's PR at triage.
+[--title ] ` checks a page: the title, the five sections there, in order and
+not empty or still the template's text, the snippet, the vision link naming an anchor that exists,
+the block, and the agent notes (required on a PR number, checked when a file has them: valid JSON
+with every key above). A page whose notes say `"status":"draft"` may still have empty sections;
+a `ready` one may not. `agent-finish` runs it before the gates and refuses to publish a page that
+fails; the PM runs it on a person's PR at triage.
An example, PR #978 rewritten:
-```markdown
+````markdown
## In plain words
On a phone, tapping a text box opened the keyboard and closed it again straight away, so you could
not type in fizzy's web app at all. Typing on a phone is how many people talk to fizzy, so this
had to work.
+## What changes
+
+- A touch on the screen no longer also counts as a mouse click, and that click was what took the
+ focus back from the text box.
+
+```js
+// web/fizzy.js, pointerInput
+canvas.addEventListener("touchstart", (e) => e.preventDefault(), { capture: true, passive: false });
+canvas.addEventListener("touchend", (e) => e.preventDefault(), { capture: true, passive: false });
+```
+
## What you will notice
Tap a text box on a phone or tablet and the keyboard opens and stays open. Nothing changes with a
mouse or a keyboard.
-## How this serves fizzy
-
-[Phones and tablets](https://github.com/fizzyedit/fizzy/blob/main/docs/VISION.md#mobile): typing
-on a phone works in the web app.
-
## How we know it works
A new test replays the browser's events for a tap: without the fix the text box loses focus, with
it the text box keeps it. The check on a real phone is still to do and is listed below.
+## How this serves fizzy
+
+[Phones and tablets](https://github.com/fizzyedit/fizzy/blob/main/docs/VISION.md#mobile): typing
+on a phone works in the web app.
+
For agents: technical summary
## Goal
@@ -169,7 +194,9 @@ which the hidden input keeps focus. Fails without the change.
The 10-of-10 phone check needs a real phone (the QA loop).
-```
+
+
+````
## Trust boundary
diff --git a/scripts/agent-finish b/scripts/agent-finish
index 267d7463c..4846154a5 100755
--- a/scripts/agent-finish
+++ b/scripts/agent-finish
@@ -52,7 +52,7 @@ fi
$claim || [[ -s $tmp/stat ]] || fail 'no changes relative to main'
marker=''
# The plain sections a visitor reads, then the technical ones (docs/AGENT_WORK.md, "The PR page").
-headings=$'## In plain words\n\n%s\n\n## What you will notice\n\n%s\n\n## How this serves fizzy\n\n%s\n\n## How we know it works\n\n%s\n\n## Goal\n\n%s\n\n## What changed and why\n\n%s\n\n## How it was verified\n\n%s\n\n## Not done / follow-ups\n\n%s'
+headings=$'## In plain words\n\n%s\n\n## What changes\n\n%s\n\n## What you will notice\n\n%s\n\n## How we know it works\n\n%s\n\n## How this serves fizzy\n\n%s\n\n## Goal\n\n%s\n\n## What changed and why\n\n%s\n\n## How it was verified\n\n%s\n\n## Not done / follow-ups\n\n%s'
details='For agents: technical summary
'
if $use_jj && ! git rev-parse --git-dir >/dev/null 2>&1; then
summary_file="$(jj root)/.jj/PR_SUMMARY.md"
@@ -71,11 +71,15 @@ if [[ -f $summary_file ]]; then
elif grep -qxF "$marker" <<<"$pr_body"; then
awk -v m="$marker" '$0 == m { exit } !started && (/^Part of / || /^Rework: / || /^$/) { next } { started = 1; print }' <<<"$pr_body" >"$tmp/summary"
elif $claim; then
- printf "$headings\n" 'Draft: work in progress' '' '' '' '' '' '' 'Draft: work in progress' >"$tmp/summary"
+ printf "$headings\n" 'Draft: work in progress' '' '' '' '' '' '' '' 'Draft: work in progress' >"$tmp/summary"
else
- printf "$headings\n" '(to fill)' '(to fill)' '(to fill)' '(to fill)' '(to fill)' '(to fill)' '(to fill)' '(to fill)' >"$tmp/summary"
+ printf "$headings\n" '(to fill)' '(to fill)' '(to fill)' '(to fill)' '(to fill)' '(to fill)' '(to fill)' '(to fill)' '(to fill)' >"$tmp/summary"
fi
retitle=()
+if $claim && [[ -z $pr ]]; then
+ # The claim's draft is titled by the work issue until the real summary names the change.
+ title=$(gh issue view "$issue" --repo fizzyedit/work --json title --jq .title) || fail 'could not read the work issue title'
+fi
# A first line `# ` is the PR title (the commit on main), in plain words.
if [[ $(head -n 1 "$tmp/summary") == '# '* ]]; then
title=$(head -n 1 "$tmp/summary" | cut -c3-)
@@ -91,7 +95,7 @@ fi
# Checked before the gates run, so a missing plain section costs no build.
if ! $claim; then
reason='the summary misses part of the PR page (docs/AGENT_WORK.md, "The PR page")'
- scripts/check-pr-body "$tmp/summary"
+ scripts/check-pr-body --title "$title" "$tmp/summary"
fi
changes=$(tail -n 1 "$tmp/stat" | awk '
/files? changed/ { n = $1; a = 0; d = 0 }
@@ -138,6 +142,23 @@ else
done
gates_label="$gates_passed of $gates_total passed"
fi
+# The agent-notes block is the one place agents leave each other notes; a rerun keeps the open
+# questions and next step the last agent wrote there.
+status=ready
+if $claim; then status=draft; fi
+notes=$(perl -MJSON::PP -e '
+ my ($issue, $worker, $slot, $status, $area, $old) = @ARGV;
+ my %n = (issue => $issue + 0, worker => $worker, slot => $slot, status => $status, area => $area,
+ open_questions => [], next => "review");
+ if ($old =~ /^$/m) {
+ my $o = eval { decode_json($1) };
+ if (ref $o eq "HASH") {
+ $n{open_questions} = $o->{open_questions} if ref $o->{open_questions} eq "ARRAY";
+ $n{next} = $o->{next} if ($o->{next} // "") =~ /^(review|fixes|merge)$/;
+ }
+ }
+ print "";
+' "$issue" "${FIZZY_WORKER:-unknown}" "${FIZZY_SLOT:-unknown}" "$status" "${branch%%/*}" "$pr_body")
# A rework run on an existing PR keeps the PR's own work issue: its running: lock and
# close-merged find the PR by that line.
part_of=$(grep -m1 -E '^Part of fizzyedit/work#[1-9][0-9]*$' <<<"$pr_body" || true)
@@ -152,7 +173,7 @@ part_of=$(grep -m1 -E '^Part of fizzyedit/work#[1-9][0-9]*$' <<<"$pr_body" || tr
head -n 100 "$tmp/stat"
printf '```\n\n \n\nGates: %s
\n\n' "$gates_label"
cat "$tmp/gates"
- printf ' \n'
+ printf ' \n\n%s\n' "$notes"
} >"$tmp/body"
reason='could not publish branch'
if $use_jj; then
diff --git a/scripts/check-pr-body b/scripts/check-pr-body
index db5e547e2..7fa806b39 100755
--- a/scripts/check-pr-body
+++ b/scripts/check-pr-body
@@ -1,17 +1,69 @@
#!/usr/bin/env bash
-# Check a PR body against docs/AGENT_WORK.md "The PR page": the plain sections a visitor reads, the
-# vision anchor, and the collapsed technical summary. `check-pr-body `.
+# Check a PR page against docs/AGENT_WORK.md "The PR page": a real title, the plain sections a visitor
+# reads in order (with a code snippet under "What changes"), the vision anchor, the collapsed technical
+# summary and the agent-notes block. `check-pr-body [--title ] `.
+# A PR number reads the live title and requires the agent-notes block; a file checks the notes only
+# when it holds them, and takes its title from a first line `# ` or `--title`.
set -euo pipefail
fail() { echo "check-pr-body: $*" >&2; exit 2; }
-[[ $# == 1 ]] || fail 'usage: check-pr-body '
+title=
+have_title=false
+if [[ ${1:-} == --title ]]; then
+ [[ $# -ge 2 ]] || fail 'usage: check-pr-body [--title ] '
+ title=$2; have_title=true; shift 2
+fi
+[[ $# == 1 ]] || fail 'usage: check-pr-body [--title ] '
cd "$(dirname "$0")/.."
+require_notes=false
if [[ $1 =~ ^[1-9][0-9]*$ ]]; then
+ title=$(gh pr view "$1" --repo fizzyedit/fizzy --json title --jq .title) || fail "cannot read PR #$1"
+ have_title=true; require_notes=true
body=$(gh pr view "$1" --repo fizzyedit/fizzy --json body --jq .body) || fail "cannot read PR #$1"
else
body=$(<"$1") || fail "cannot read $1"
+ if ! $have_title && [[ $body == '# '* ]]; then
+ title=$(head -n 1 <<<"$body" | tr -d '\r' | cut -c3-); have_title=true
+ fi
+fi
+body=$(tr -d '\r' <<<"$body")
+problems=()
+if $have_title; then
+ if [[ ${#title} -gt 72 ]]; then problems+=("title is over 72 characters: \"$title\""); fi
+ if [[ ! $title =~ ^[a-z0-9_-]+:\ .+ ]]; then
+ problems+=("title \"$title\" is not \"area: what changes\"")
+ elif [[ $title =~ ^[A-Za-z0-9_]+:\ work-[0-9]+ || $title =~ ^[A-Za-z0-9_]+:\ [a-z-]+$ ]]; then
+ problems+=("title \"$title\" is a slug; say what changes, in words a person understands")
+ fi
+fi
+# Agent notes: the one place agents talk to each other, a hidden comment at the end of the body.
+notes=$(grep -E '^$' <<<"$body" || true)
+if [[ -n $notes && $(grep -c . <<<"$notes") != 1 ]]; then
+ problems+=('more than one agent-notes block')
+elif [[ -z $notes ]]; then
+ if grep -q '^')
+ elif $require_notes; then problems+=('no "" block'); fi
+else
+ last=$(grep -v '^[[:space:]]*$' <<<"$body" | tail -n 1)
+ [[ $last == "$notes" ]] || problems+=('the agent-notes block is not the last thing in the body')
+ json=${notes#''}
+ if ! why=$(perl -MJSON::PP -e '
+ my $j = eval { decode_json($ARGV[0]) } or die "is not valid JSON\n";
+ ref $j eq "HASH" or die "is not a JSON object\n";
+ for (qw(issue worker slot status area open_questions next)) { exists $j->{$_} or die "lacks \"$_\"\n" }
+ ref $j->{open_questions} eq "ARRAY" or die "\"open_questions\" is not a list\n";
+ $j->{status} =~ /^(draft|ready)$/ or die "\"status\" is not draft or ready\n";
+ $j->{next} =~ /^(review|fixes|merge)$/ or die "\"next\" is not review, fixes or merge\n";
+ ' "$json" 2>&1); then
+ problems+=("agent-notes ${why:-is invalid}")
+ fi
+ # A claim draft may still be empty; a ready page may not.
+ draft=false
+ draft_re='"status": *"draft"'; if [[ $json =~ $draft_re ]]; then draft=true; fi
fi
+draft=${draft:-false}
# Template hints are HTML comments, often several lines long: they are not content.
-body=$(tr -d '\r' <<<"$body" | awk '
+body=$(awk '
{
line = $0; out = ""
while (line != "") {
@@ -26,22 +78,47 @@ body=$(tr -d '\r' <<<"$body" | awk '
}
}
print out
- }')
-problems=()
-# The text under a `## `, up to the next heading or the technical summary.
+ }' <<<"$body")
+# The text under a `## `, up to the next heading or the technical summary; a `#` line
+# inside a code fence is code.
section() {
awk -v h="## $1" '
- $0 == h { on = 1; next }
- on && (/^##? / || /^/) { exit }
+ /^```/ && on { fence = !fence }
+ $0 == h && !fence { on = 1; next }
+ on && !fence && (/^##? / || /^/) { exit }
on && !/^[[:space:]]*$/ { print }
' <<<"$body"
}
-for heading in 'In plain words' 'What you will notice' 'How this serves fizzy' 'How we know it works'; do
+empty=()
+order=
+for heading in 'In plain words' 'What changes' 'What you will notice' 'How we know it works' 'How this serves fizzy'; do
text=$(section "$heading")
- if [[ -z $text || $text == '(to fill)' ]]; then problems+=("\"## $heading\" is missing or empty"); fi
+ if [[ -z $text || $text == '(to fill)' || $text == 'Draft: work in progress' ]]; then
+ empty+=("\"## $heading\" is missing or empty")
+ fi
+ order+="$(grep -n -m1 -xF "## $heading" <<<"$body" | cut -d: -f1 || true) "
done
+if [[ $(tr ' ' '\n' <<<"$order" | grep . | sort -n | tr '\n' ' ') != "$order" ||
+ $(tr ' ' '\n' <<<"$order" | grep -c .) != 5 ]]; then
+ problems+=('the plain sections are not all there in order: In plain words, What changes, What you will notice, How we know it works, How this serves fizzy')
+fi
+changes=$(section 'What changes')
+if [[ -n $changes && $changes != 'Draft: work in progress' ]]; then
+ # Each fenced snippet is at most 15 lines; at least one is there.
+ sizes=$(awk '/^```/ { if (inside) { print n; inside = 0 } else { inside = 1; n = 0 } next } inside { n++ }' <<<"$changes")
+ if [[ -z $sizes ]]; then
+ empty+=('"## What changes" has no fenced code snippet (a before/after or the key function)')
+ elif [[ $(sort -n <<<"$sizes" | tail -n 1) -gt 15 ]]; then
+ problems+=('a code snippet under "## What changes" is over 15 lines')
+ fi
+fi
+if $draft; then
+ for e in ${empty[@]+"${empty[@]}"}; do echo "check-pr-body: $1: draft, still to write: $e" >&2; done
+else
+ problems+=(${empty[@]+"${empty[@]}"})
+fi
anchors=$(grep -o 'docs/VISION\.md#[a-z0-9-]*' <<<"$(section 'How this serves fizzy')" | sed 's/.*#//' | sort -u || true)
-if [[ -z $anchors ]]; then
+if [[ -z $anchors ]] && ! $draft; then
problems+=('"## How this serves fizzy" links no docs/VISION.md#')
fi
for anchor in $anchors; do
diff --git a/scripts/test-agent-work.sh b/scripts/test-agent-work.sh
index ca4face85..f77d5fd09 100755
--- a/scripts/test-agent-work.sh
+++ b/scripts/test-agent-work.sh
@@ -10,12 +10,16 @@ mkdir -p "$tmp/repo/scripts" "$tmp/repo/docs" "$tmp/repo/.jj" "$tmp/repo/.git" "
cp scripts/agent-{prepare,finish} scripts/check-pr-body "$tmp/repo/scripts/"
cp docs/VISION.md "$tmp/repo/docs/"
# A summary in the shape of docs/AGENT_WORK.md "The PR page", where jj and git each keep it.
-plain=$'## In plain words\n\nA tap opens the keyboard.\n\n## What you will notice\n\nTyping works.\n\n'
-plain+=$'## How this serves fizzy\n\n[Phones and tablets](https://github.com/fizzyedit/fizzy/blob/main/docs/VISION.md#mobile).\n\n'
+fence='```'
+plain=$'## In plain words\n\nA tap opens the keyboard.\n\n'
+plain+=$'## What changes\n\n- A tap no longer closes the keyboard.\n\n'"${fence}zig"$'\nevent.preventDefault();\n'"$fence"$'\n\n'
+plain+=$'## What you will notice\n\nTyping works.\n\n'
plain+=$'## How we know it works\n\nA test that fails without it.\n\n'
+plain+=$'## How this serves fizzy\n\n[Phones and tablets](https://github.com/fizzyedit/fizzy/blob/main/docs/VISION.md#mobile).\n\n'
technical=$'## Goal\n\ng\n\n## What changed and why\n\nw\n\n## How it was verified\n\nv\n\n## Not done / follow-ups\n\nn'
summary() { printf '%s\n' "$plain$technical" | tee "$tmp/repo/.jj/PR_SUMMARY.md" >"$tmp/repo/.git/PR_SUMMARY.md"; }
no_summary() { rm -f "$tmp/repo/.jj/PR_SUMMARY.md" "$tmp/repo/.git/PR_SUMMARY.md"; }
+titled() { printf '# ci: a tap opens the keyboard\n%s\n' "$plain$technical" | tee "$tmp/repo/.jj/PR_SUMMARY.md" >"$tmp/repo/.git/PR_SUMMARY.md"; }
summary
export AGENT_TEST_DIR=$tmp
export AGENT_PREPARE_ALLOW_TMP=1 # the test checkout may itself sit under /tmp
@@ -45,6 +49,7 @@ cat >"$tmp/bin/gh" <<'FAKE'
#!/usr/bin/env bash
set -e
printf '%s\n' "$*" >>"$AGENT_TEST_DIR/calls"
+if [[ $1 == issue && $2 == view ]]; then echo 'Typing works on a phone'; fi
if [[ $1 == pr && $2 == list && ${AGENT_TEST_EXISTING:-} == 1 ]]; then echo 12; fi
if [[ $1 == pr && $2 == view && -n ${AGENT_TEST_BODY:-} ]]; then printf '%s\n' "$AGENT_TEST_BODY"; fi
if [[ $1 == pr && ( $2 == create || $2 == edit ) ]]; then
@@ -93,9 +98,14 @@ grep -q 'earlier output omitted' "$tmp/body" || fail 'tail marker'
rm "$tmp/long"
: >"$tmp/calls"
touch "$tmp/no-desc"
+# The branch's slug is no title: a summary that names none is refused.
+if "$finish" 42 >"$tmp/output" 2>&1; then fail 'slug title accepted'; fi
+grep -q 'is a slug' "$tmp/output" || fail 'slug title not named'
+titled
"$finish" 42 >"$tmp/output"
grep -q 'describe -r @ -m ci: example' "$tmp/calls" || fail 'empty description filled'
rm "$tmp/no-desc"
+summary
: >"$tmp/calls"
"$finish" 42 >"$tmp/output"
! grep -q 'describe' "$tmp/calls" || fail 'worker description overwritten'
@@ -155,11 +165,13 @@ FAKE
chmod +x "$tmp/bin/git"
AGENT_TEST_GIT=1 "$prepare" ci/git >"$tmp/output"
grep -q 'git worktree add -b ci/git' "$tmp/calls" || fail 'git worktree fallback'
+titled
AGENT_TEST_GIT=1 "$finish" 42 >"$tmp/output"
+summary
grep -q 'git push --set-upstream origin ci/git' "$tmp/calls" || fail 'git push fallback'
: >"$tmp/calls"
AGENT_TEST_GIT=1 AGENT_TEST_AHEAD=0 "$finish" --claim 42 >"$tmp/output"
-grep -q 'git commit --allow-empty -m ci: git: claim' "$tmp/calls" || fail 'git claim empty commit'
+grep -q 'git commit --allow-empty -m Typing works on a phone: claim' "$tmp/calls" || fail 'git claim empty commit'
: >"$tmp/calls"
AGENT_TEST_GIT=1 "$finish" --claim 42 >"$tmp/output"
! grep -q 'allow-empty' "$tmp/calls" || fail 'empty commit despite commits ahead'
@@ -181,6 +193,60 @@ AGENT_TEST_EXISTING=1 AGENT_TEST_BODY=$'Part of fizzyedit/work#42\n\nx' "$finish
! grep -q '^Rework:' "$tmp/body" || fail 'rework line on a same-issue run'
AGENT_TEST_EXISTING=1 "$finish" 42 >"$tmp/output"
grep -q '^Part of fizzyedit/work#42$' "$tmp/body" || fail 'fresh PR issue line'
+# The PR page checks: title, layout with a code snippet, and the agent-notes block.
+check=$tmp/repo/scripts/check-pr-body
+notes=''
+details=$'For agents: technical summary
\n\n'"$technical"$'\n\n '
+page() { printf '%s\n%s\n\n%s\n\n%s\n' "$1" "$plain$details" "${2-}" "${3-}"; }
+page '# web: a tap on a phone keeps the keyboard open' '' "$notes" >"$tmp/good.md"
+"$check" "$tmp/good.md" >"$tmp/output" 2>&1 || fail "good page refused: $(cat "$tmp/output")"
+bad() { #
+ if "$check" "$1" >"$tmp/output" 2>&1; then fail "accepted: $2"; fi
+ grep -q -- "$2" "$tmp/output" || fail "not named: $2 ($(cat "$tmp/output"))"
+}
+page '# windows: work-723' '' "$notes" >"$tmp/bad.md"; bad "$tmp/bad.md" 'is a slug'
+page '# linux: touch-slop' '' "$notes" >"$tmp/bad.md"; bad "$tmp/bad.md" 'is a slug'
+page '# web: a tap on a phone keeps the keyboard open and the page scrolls to the field at once' '' "$notes" >"$tmp/bad.md"
+bad "$tmp/bad.md" 'over 72 characters'
+page 'Part of #1' '' "$notes" >"$tmp/bad.md"
+"$check" --title 'work-723' "$tmp/bad.md" >"$tmp/output" 2>&1 && fail 'title without an area accepted'
+grep -q 'not "area: what changes"' "$tmp/output" || fail 'title shape not named'
+good=$(<"$tmp/good.md")
+printf '%s\n' "${good/${fence}zig/${fence}}" | sed '/^event.preventDefault/d;/^```$/d' >"$tmp/bad.md"
+bad "$tmp/bad.md" 'no fenced code snippet'
+long=$(seq 1 16 | sed 's/^/line /')
+printf '%s\n' "${good/event.preventDefault();/$long}" >"$tmp/bad.md"; bad "$tmp/bad.md" 'over 15 lines'
+swapped=${good/$'## What you will notice\n\nTyping works.\n\n'/}
+printf '%s\n' "${swapped/$'## In plain words'/$'## What you will notice\n\nTyping works.\n\n## In plain words'}" >"$tmp/bad.md"
+bad "$tmp/bad.md" 'not all there in order'
+page '# web: a tap on a phone keeps the keyboard open' '' '' >"$tmp/bad.md"
+bad "$tmp/bad.md" 'agent-notes'
+page '# web: a tap on a phone keeps the keyboard open' '' "${notes/\"slot\":\"windows\",/}" >"$tmp/bad.md"
+bad "$tmp/bad.md" 'lacks "slot"'
+page '# web: a tap on a phone keeps the keyboard open' '' "${notes/\"next\":\"review\"/\"next\":\"later\"}" >"$tmp/bad.md"
+bad "$tmp/bad.md" '"next" is not'
+page '# web: a tap on a phone keeps the keyboard open' "$notes" 'trailing text' >"$tmp/bad.md"
+bad "$tmp/bad.md" 'not the last thing'
+# A claim draft may be empty; a ready page may not.
+draft=${notes/ready/draft}
+empty_plain=${plain//A tap opens the keyboard./Draft: work in progress}
+printf '%s\n%s\n' "$empty_plain$details" "$draft" >"$tmp/draft.md"
+"$check" "$tmp/draft.md" >"$tmp/output" 2>&1 || fail "empty draft refused: $(cat "$tmp/output")"
+printf '%s\n%s\n' "$empty_plain$details" "$notes" >"$tmp/bad.md"; bad "$tmp/bad.md" 'missing or empty'
+# The worked example in docs/AGENT_WORK.md (PR #978 rewritten) passes the check.
+awk '/^````markdown/ { on = 1; next } /^````$/ { on = 0 } on' "$source_root/docs/AGENT_WORK.md" >"$tmp/example.md"
+"$check" --title 'web: a tap on a phone keeps the keyboard open' "$tmp/example.md" >"$tmp/output" 2>&1 ||
+ fail "the documented example fails: $(cat "$tmp/output")"
+# agent-finish: the claim draft takes the work issue's title; every page ends with its notes.
+: >"$tmp/calls"
+"$finish" --claim 42 >"$tmp/output"
+grep -q -- '--title Typing works on a phone' "$tmp/calls" || fail 'claim title from the work issue'
+tail -n 1 "$tmp/body" | grep -q '"issue":42.*"status":"draft"' || fail 'claim agent-notes'
+"$finish" 42 >"$tmp/output"
+tail -n 1 "$tmp/body" | grep -q '"status":"ready"' || fail 'finish agent-notes'
+"$check" "$tmp/body" >"$tmp/output" 2>&1 || fail "published page refused: $(cat "$tmp/output")"
+AGENT_TEST_EXISTING=1 AGENT_TEST_BODY=$'Part of fizzyedit/work#42\n\n'"${notes/\"open_questions\":[]/\"open_questions\":[\"which phone?\"]}" "$finish" 42 >"$tmp/output"
+tail -n 1 "$tmp/body" | grep -q 'which phone?' || fail 'agent-notes questions lost on a rerun'
cat >"$tmp/bin/uname" <<'FAKE'
#!/usr/bin/env bash
echo Linux