From 3d0a5f66eec0590fd470a1f014e3fb54ecb4a80a Mon Sep 17 00:00:00 2001 From: "fizzy-windows[bot]" <347008912+fizzy-windows[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 11:12:07 -0500 Subject: [PATCH 1/2] windows: work-723 (codex) Agent-Team: windows Worker: codex --- .worker-write-probe | 1 + 1 file changed, 1 insertion(+) create mode 100644 .worker-write-probe diff --git a/.worker-write-probe b/.worker-write-probe new file mode 100644 index 000000000..da0c4eb8d --- /dev/null +++ b/.worker-write-probe @@ -0,0 +1 @@ +probe From 2a87265a2728f28a04d3e39a5d4bdaf1c250086e Mon Sep 17 00:00:00 2001 From: "fizzy-windows[bot]" <347008912+fizzy-windows[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 12:33:05 -0500 Subject: [PATCH 2/2] scripts: agent PRs get real titles, a code snippet and notes check-pr-body refuses slug titles, a missing or oversized snippet, sections out of order and bad agent-notes JSON; agent-finish titles the claim from the work issue and writes the notes block; template and docs describe the layout. Removes the probe file. Agent-Team: windows Worker: claude Co-Authored-By: Claude Sonnet 5.5 --- .github/pull_request_template.md | 16 +++-- .worker-write-probe | 1 - docs/AGENT_WORK.md | 63 +++++++++++++------ scripts/agent-finish | 31 ++++++++-- scripts/check-pr-body | 101 +++++++++++++++++++++++++++---- scripts/test-agent-work.sh | 72 +++++++++++++++++++++- 6 files changed, 240 insertions(+), 44 deletions(-) delete mode 100644 .worker-write-probe 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/.worker-write-probe b/.worker-write-probe deleted file mode 100644 index da0c4eb8d..000000000 --- a/.worker-write-probe +++ /dev/null @@ -1 +0,0 @@ -probe 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 ] <file|PR number>` 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. + <details><summary>For agents: technical summary</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). </details> -``` + +<!-- agent-notes {"issue":978,"worker":"claude","slot":"web","status":"ready","area":"web","open_questions":[],"next":"review"} --> +```` ## 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='<!-- agent-finish: generated below -->' # 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='<details><summary>For agents: technical summary</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 `# <title>` 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 =~ /^<!-- agent-notes (\{.*\}) -->$/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 "<!-- agent-notes ", JSON::PP->new->canonical->encode(\%n), " -->"; +' "$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</details>\n\n<details><summary>Gates: %s</summary>\n\n' "$gates_label" cat "$tmp/gates" - printf '</details>\n' + printf '</details>\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 <file|PR number>`. +# 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 <title>] <file|PR number>`. +# 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 `# <title>` or `--title`. set -euo pipefail fail() { echo "check-pr-body: $*" >&2; exit 2; } -[[ $# == 1 ]] || fail 'usage: check-pr-body <file|PR number>' +title= +have_title=false +if [[ ${1:-} == --title ]]; then + [[ $# -ge 2 ]] || fail 'usage: check-pr-body [--title <title>] <file|PR number>' + title=$2; have_title=true; shift 2 +fi +[[ $# == 1 ]] || fail 'usage: check-pr-body [--title <title>] <file|PR number>' 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 '^<!-- agent-notes \{.*\} -->$' <<<"$body" || true) +if [[ -n $notes && $(grep -c . <<<"$notes") != 1 ]]; then + problems+=('more than one agent-notes block') +elif [[ -z $notes ]]; then + if grep -q '^<!-- *agent-notes' <<<"$body"; then + problems+=('the agent-notes block is malformed: expected one line <!-- agent-notes {...} -->') + elif $require_notes; then problems+=('no "<!-- agent-notes {...} -->" 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#'<!-- agent-notes '}; json=${json%' -->'} + 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 `## <heading>`, up to the next heading or the technical summary. + }' <<<"$body") +# The text under a `## <heading>`, 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 && (/^##? / || /^<details>/) { exit } + /^```/ && on { fence = !fence } + $0 == h && !fence { on = 1; next } + on && !fence && (/^##? / || /^<details>/) { 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#<anchor>') 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='<!-- agent-notes {"issue":978,"worker":"claude","slot":"windows","status":"ready","area":"web","open_questions":[],"next":"review"} -->' +details=$'<details><summary>For agents: technical summary</summary>\n\n'"$technical"$'\n\n</details>' +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() { # <file> <expected message> + 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' '' '<!-- agent-notes {"issue":978, -->' >"$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