Skip to content

Agent workflow hierarchy: what to review โ€‹

Where does this file start, and why are there two things in it? โ€‹

This section is the review map for feat/admin-and-merchant-portals. Everything below it is the ORIGINAL manual test plan, for a different branch. โ€‹

What changed here on this branch? โ€‹

Three edits, all to .agents/ sources with their generated copies regenerated. No product surface. โ€‹

ChangeWhat it does
pr-workbench push discipline names the autonomy exceptionPush discipline said flatly that you mark a PR ready, while Step 7 of the same file named the autonomy grant as the sanctioned exception
verified-done gains the agreement-is-not-correctness ruleA guard comparing two artifacts you wrote measures agreement. Correctness was never in its scope
The hook layer count is eight, not sevenThe survey counts the hooks in .claude/hooks/, and the PM lane guard made that count wrong the moment it landed

The pr-workbench change did NOT change when a PR may be marked ready or merged. โ€‹

  • Outside an autonomy grant it still needs your word at the moment of action, and the PreToolUse hook still enforces it. gh pr ready remains off limits to an agent.
  • What it fixed: the flat copy was the one inside the checklist, which is where an agent reads it in the moment, and a flat statement gets followed flatly. An autopilot session that hesitates at a merge it is permitted to make burns the fix runway the evening window exists to protect.

The verified-done rule came from six instances on one branch in one day. โ€‹

  • Seven fictional field names in a Firestore allowlist. A featureFlags collection with zero writers anywhere. A drift test that compared two copies to each other and stayed green because both were wrong identically. That same test covering only half its decision. A client reporting a literal placeholder into a model's context as a project name.
  • A mock is the same defect in another coat: it proves the shape of the code and says nothing about whether the names inside it are real. Wrong names fail silently by design, so a dead field looks like an empty column and a dead collection looks like a quiet one.
  • Two limbs: a name is real only when its writer or real data says so; and a check covering part of a decision must say which part, because a green on a partial check reads as a green on the whole one.
  • Both limbs also land as rows in the file's "What proves what" table, since that is the part a reader copies.

Where do I look, and what am I looking for? โ€‹

The diff on .agents/, and two commands that prove the generated copies match their sources. โ€‹

node tooling/scripts/lint.sync-skills.js
node tooling/scripts/sync-agents.mjs --check
  • Three sources changed: .agents/skills/pr-workbench/skill.md, .agents/skills/verified-done/skill.md, and this project's own README.md for the hook count.
  • Each source has four generated copies (.claude/, .gemini/, .github/, plus the rules mirrors). If the check reports drift, a copy was edited directly and the next sync will overwrite it.

The hook count, one command. โ€‹

ls -1 .claude/hooks/ | wc -l
  • It should read 8. The README should say eight.

What is deliberately NOT done? โ€‹

The original test plan was NOT re-run against this branch. โ€‹

  • Its 25 scenarios target workflow-tool behaviour on a merged branch, and none of the three changes here touches a workflow script.
  • Scenario 5 in that plan is still blocked: whether the new assets reach a fresh session, which needs a fresh session to answer.

Nothing here was routed to a hook, a rule or a workflow. โ€‹

  • All three went to the skill layer, per the five-step routing test in design.md: not machine-decidable in general, not binding on every task, not a sequence being skipped.

What open findings touch this project? โ€‹

What issue does each piece close? โ€‹

PieceIssueState
The hierarchy and its drift gate#952: docs(agents): agent asset hierarchy, plus a drift gate for the workflows layerMerged, covered by the test plan below
The three edits on this branchNone. Corrections to existing guidanceLanded on branch
How to write a skill#1001: docs(agents): nothing covers how to WRITE a skill, only when to and what shape it takesOpen, not started

Agent workflow hierarchy - Manual Test Plan โ€‹

FieldValue
Branch / PRdocs/950-agent-workflow-hierarchy / #952
EnvironmentA Claude Code session on this machine. Workflows run via the Workflow tool by scriptPath. No services need to be running
Build flags / confignone
Build (commit)cab1dc12 for the recorded runs below. Scenarios 18 through 21 target 42361c97 or later
TesterClaude Code session, driving on the operator's behalf
Date2026-08-22 (Pacific)

Summary โ€‹

#ScenarioResultNotes
1focus is required and refuses without it[x] pass [ ] fail [ ] blocked0 agents spawned, threw in 25ms
2args-as-string is named as the cause[x] pass [ ] fail [ ] blockedmessage points at the cause, not at repoRoot
3Workflow Sync refuses a file that does not parse[x] pass [ ] fail [ ] blockedauthor-run; generated copy md5-identical after the broken sync
4Skill Sync refuses a missing tier[x] pass [ ] fail [ ] blockedauthor-run; both arms, missing and misspelled
5ACTIVATION: the new assets reach a fresh session[ ] pass [ ] fail [x] blockeddeferred, and needs a fresh session
6agenda-creation writes today's agenda[x] pass [ ] fail [ ] blockedPASS against a scratch dir. The real-board write and its push remain unexercised
7The Focus line is her sentence, verbatim[x] pass [ ] fail [ ] blockedPASS. Written verbatim, no tidying
8The written agenda matches the format skill[x] pass [ ] fail [ ] blockedPASS. All three explicit checks, plus the old-format tripwire clean
9FALSIFYING PAIR A: a narrow focus cuts most of it[x] pass [ ] fail [ ] blockedits own prediction held, but see the pair verdict on scenario 10
10FALSIFYING PAIR B (control): a matching focus cuts little[ ] pass [x] fail [ ] blockedFAIL: ranked identical at zero in both arms, and the arms shared only 35 of 69 candidates
11FALSIFYING: no phase after Backlog adds rows[x] pass [ ] fail [ ] blockedPASS after the fix. Zero ghosts in both arms; uncarried rows went to gaps
12The agenda reader greps rather than reading 185KB[x] pass [ ] fail [ ] blockedgrep plus sed, 22 percent and 13 percent of the two files, quantified unprompted
13agenda-status returns an audit when there is no board[ ] pass [ ] fail [x] blockeddeferred, out of this run's scope
14agenda-status Reconcile runs against a real board[ ] pass [ ] fail [x] blockeddeferred, out of this run's scope
15agenda-status Record writes, and the publish gate opens[ ] pass [ ] fail [x] blockeddeferred, out of this run's scope
16A session's claim that disagrees with git is reported[ ] pass [ ] fail [x] blockeddeferred, out of this run's scope
17A reader that throws is ABSENT, not empty[ ] pass [ ] fail [x] blockeddeferred, out of this run's scope
18Every task field is ONE line[x] pass [ ] fail [ ] blockedPASS after the fix, and the PREDICTION WAS WRONG. Longest Context 141 chars against a bar of 200
19The handoff prompt is four labelled lines[x] pass [ ] fail [ ] blockedPASS on shape AND values. 5 of 5 paths and 4 of 4 issues resolved, not eyeballed
20Realtime log entries carry a bold capitalized title[x] pass [ ] fail [ ] blockedPASS, clock genuinely read. But the format hardcodes PST and August is PDT. Status arm still blocked
21Focus blocks are checkboxes, emojis are the fixed set[x] pass [ ] fail [ ] blockedPASS. 7 emoji, all in-table by codepoint, 0 missing and 0 wrong. Zero boxes ticked

Setup / preconditions โ€‹

  • Repo at /home/mechelle/repos/lantern_app, context home at /home/mechelle/.agents/context/lantern_app.
  • Both paths are machine-specific. On the dev VM they are /home/cattreedev/... and every scenario needs them swapped.
  • Where a run writes: docs/projects/agent-workflow-hierarchy/runs/<label>/, seeded by copying fixtures/agenda/*.md into it. Recorded results below still name /tmp paths, correctly, because that is where those runs actually wrote.
  • Scenarios 6 through 16 WRITE. agenda-creation creates today's dated agenda file and runs context:sync, which commits and pushes the whole context home. This is intended, and it is the one irreversible-ish thing here. The safe variant is agendaDir pointed at a scratch directory, which every write scenario names.
  • Scenarios 3 and 4 deliberately break a file and then restore it. Take the backup the steps name; do not skip it.
  • Reach a clean state with git status --porcelain empty in both the repo and the context home before starting.
  • Where to verify: the agenda markdown in <contextHome>/agenda/<YYYY-MM-DD>.md, the workflow journal, and the tool's returned object.
  • The journal path, corrected. It is ~/.claude/projects/<sanitized>/<sessionUuid>/subagents/workflows/<runId>/journal.jsonl. The session uuid level was missing here, so the documented path resolves to nothing and an empty result reads as "no run launched". That misreading has now happened twice.
  • Validate any query before believing an empty answer from it. Two queries used to hunt for runs today were broken rather than empty: the path above, and find -newermt "15 minutes ago", which returns nothing on this machine even for a file written one second earlier. Point the query at something known to exist first. Sorted -printf '%T@ %p' works where -newermt does not.

Re-run predictions, written 2026-08-22 before either arm launched โ€‹

The first pair was void (74 candidates against 12) because a reader was silently killed. Three fixes landed afterwards. These predictions are recorded BEFORE the re-run, against 7ca235e4, so they cannot be fitted to the result.

THE GATE, checked before anything else is compared. Both arms must report the same candidateCount. If they differ, the pair is void again and the numbers below mean nothing. Expect roughly 74 in both, since all three readers should now run against an identical restored input.

PredictionArm A (product surfaces)Arm B (agent tooling)
candidateCountsame as arm B, roughly 74same as arm A, roughly 74
rankedSHORT, and possibly ZERO now that gaps cannot pad itLONGER than arm A
cutLONG, roughly 20SHORTER than arm A
gapsat most 2at most 2
ghosts in rankedZERO, or the structural fix failed where prose failedZERO

Named in advance so it cannot be reinterpreted later:

  • An EMPTY ranked in arm A with a full cut list is a PASS, not a broken run. The narrow focus legitimately excludes today's agent-tooling backlog, and the gaps channel is no longer available to fill the vacuum.
  • A ghost in ranked in either arm is a FAIL, and a worse one than last time, because it would mean a structural fix failed in the same place prose already failed.
  • The null-reader guard will most likely NOT fire, because the classifier refusal that produced the condition is exactly what the scratch-run suppression removes. If every reader returns, that arm of the guard stays unexercised and this run will say so plainly rather than claiming it works.
  • If the two arms produce similar rankings ON EQUAL INPUT, that is the finding: the focus is decorative and the diagnosis was wrong. It will be recorded that way, not read as a pass.

Predictions for scenarios 18 through 21, written before the run โ€‹

Recorded here so they cannot be fitted to the result afterwards.

ScenarioPredictionWhy
18, one-line fieldsFAIL, or pass only on the short rows. This is the one I would bet againstGather is deliberately told to write detail out IN FULL so nobody has to open yesterday's file. The writer is handed a paragraph and asked to shrink it, and shrinking is the step that gets skipped. The previous run produced a Context field of about 1,100 characters
19, four-line promptPASS on shape, and the interesting failure is in the VALUESFour labels in a fence is easy to satisfy. "Start here" pointing at a real path and "Done when" being a check the receiving agent can actually run are the parts that carry the value, and neither is enforced by the shape
20, bold log titlePASSOne line, one shape, and the writer has an explicit example
21, checkboxes and emojisPASS on checkboxes, UNCERTAIN on emojisA fixed table is easy to read off. The risk is improvisation: a heading that gets a sensible emoji from outside the table looks correct and defeats the point, which is that two sessions writing the same file agree

What would tell us the format change was the wrong call. If 18 fails and the fix is to loosen the one-line rule rather than to create the missing project, then the rule was aimed at the wrong thing: the length was a symptom of a task carrying its whole history because it had nowhere else to put it. Record that reading if it is what the run shows, rather than filing it as a writer that misbehaved.

Scenarios โ€‹

1. focus is required and refuses without it โ€‹

  • Goal: the run cannot proceed on an inferred focus.
  • Preconditions: none.
  • Steps:
    1. Invoke agenda-creation.js with repoRoot and contextHome only, no focus.
  • Expected: throws immediately, zero agents spawned, message names focus and gives an example.
  • Verify: the error text says there is no default and why. Nothing written to the agenda directory.
Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    Threw immediately. `agent_count 0`, `agents_done 0`, duration 25ms, so nothing was
           spawned and nothing billed. Run id `wf_d1104e11-522`. Threw at `required()`
           (workflow.js:29), reached from the FOCUS constant at workflow.js:63, which is the
           right place: after repoRoot and contextHome resolved, before the Rules phase.
           Nothing written. `git status --porcelain` empty in both the repo and the context
           home afterwards, and the agenda directory still held only 2026-08-19.md,
           2026-08-20.md, 2026-08-21.md and agenda.html.
Evidence:  (error text, verbatim)
           `focus is required. Pass it through the Workflow tool's args, e.g. { focus: 'get the
           admin portal signing in end to end' }. There is no default: this script cannot see
           the filesystem, and guessing produces an empty report instead of an error.`
           Names the argument, gives a runnable example, and states the no-default reason.
Follow-up: none.

2. args-as-string is named as the cause โ€‹

  • Goal: a large payload that arrives as a string produces a message pointing AT the cause, not away from it.
  • Preconditions: none.
  • Steps:
    1. Invoke agenda-status.js passing args as a JSON-encoded string rather than an object (a ~9KB sessionReports payload is the natural trigger; a deliberately stringified small object also works).
  • Expected: throws with "args arrived as a STRING, not an object", and says no argument is actually missing.
  • Verify: the message does NOT tell you to pass repoRoot, which you already passed. That misdirection cost a full run on 2026-08-22.
Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    Threw in 15ms with 0 agents spawned. Run id `wf_429f12b6-431`. Threw at
           workflow.js:16, which is the type guard at the top of the file, ahead of every
           `required()` call. The message names args-as-a-string as the cause, says nothing
           below it ran, and states outright that no argument is actually missing. It never
           mentions `repoRoot`, which is the misdirection this scenario exists to catch.
Evidence:  (error text, verbatim)
           `args arrived as a STRING, not an object, so every argument reads as undefined.
           This happens on large payloads: sessionReports carrying several verbose reports is
           the usual trigger. Pass args as an actual JSON object, not a JSON-encoded string.
           Nothing below this line ran, and no argument is actually missing.`
Follow-up: Reproducing the failure took a deliberately double-encoded payload. The Workflow
           tool's own `args` parameter parses a JSON object literal into an object, so a
           first attempt at this scenario passing the object shape ran the real workflow
           instead of throwing (stopped before it wrote anything, context home verified
           clean). Worth knowing that the string case is not something a caller falls into
           through this tool: it comes from a caller that stringifies the payload itself.
           `agenda-creation.js` carries the identical guard at workflow.js:31, unexercised
           here but textually the same check.

3. Workflow Sync refuses a file that does not parse โ€‹

  • Goal: a syntactically broken workflow cannot reach the generated copy.
  • Preconditions: clean tree. Back up the file first.
  • Steps:
    1. cp .agents/workflows/test-rank.js /tmp/test-rank.bak
    2. Append a deliberate syntax error: printf '\nconst x = a + ;\n' >> .agents/workflows/test-rank.js
    3. npm run sync:workflows
    4. cp /tmp/test-rank.bak .agents/workflows/test-rank.js && npm run sync:workflows
  • Expected: step 3 reports x test-rank.js with "does not parse" and Errors: 1. Step 4 returns to Errors: 0.
  • Verify: after step 3, .claude/workflows/test-rank.js is UNCHANGED. git diff --stat .claude/workflows/test-rank.js is empty. That is the whole point: before this check, the broken file synced clean and the drift gate called it up to date.
Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    Run by the AUTHOR of the check, 2026-08-22 (weaker evidence than an independent
           tester; worth re-running in a later pass). Appended a deliberate syntax error to
           test-rank.js and synced. Output: "x test-rank.js / does not parse: Unexpected
           token ';'" and Errors: 1. Restored the file, re-synced, Errors: 0.
Evidence:   md5 of .claude/workflows/test-rank.js taken before and after the broken sync:
           IDENTICAL. The broken file did not reach the generated copy, which is the whole
           point. Before this check the broken file synced clean and the drift gate reported
           it up to date, because it compares the copy to the source and both were equally
           broken. git status clean afterwards in .agents/ and .claude/.
Follow-up: none.

4. Skill Sync refuses a missing tier โ€‹

  • Goal: the frontmatter gate still bites after today's changes.
  • Preconditions: clean tree. Back up the file first.
  • Steps:
    1. Back up .agents/skills/agenda-documentation/skill.md.
    2. Delete its tier: line.
    3. node tooling/scripts/lint.sync-skills.js
    4. Restore, re-run.
  • Expected: step 3 fails naming the missing tier; step 4 reports "Skills are in sync."
  • Verify: repeat with a misspelled tier (tier: refrence). It must also fail, not pass through.
Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    Run by the author, 2026-08-22, both arms. 4a, tier line deleted from
           agenda-documentation: "missing `tier:`", 1 problem. 4b, tier misspelled as
           "refrence": 'tier "refrence" is not one of orchestrator, procedure, discipline,
           reference', 1 problem. Restored: "Skills are in sync."
Evidence:   The misspelling arm is the one that matters. A gate checking only for presence
           would pass "refrence" and the skill would carry a tier no consumer recognises.
           It names the four valid values in the error, so the fix is obvious from the
           message alone. git status clean in .agents/ and .claude/ afterwards.
Follow-up: none.

5. ACTIVATION: the new assets reach a fresh session โ€‹

  • Goal: prove the shipped default is on, not that the files exist. Files existing in .agents/ proves nothing; the generated copies are what agents read.
  • Preconditions: a brand new session, no manual setup, nothing selected by hand.
  • Steps:
    1. Open a fresh Claude Code session in the repo.
    2. Ask it, without hinting at paths: "what skills do you have for the agenda?"
    3. Ask it to list the available workflows.
  • Expected: it names agenda-creation, agenda-status and agenda-documentation from its own loaded list, having read nothing.
  • Verify: .claude/skills/agenda-creation/SKILL.md, .github/skills/agenda-creation/SKILL.md and .gemini/skills/agenda-creation/skill.md all exist and carry the sync banner. Confirm no day-plan directory survives in any of the three.
Result:    [ ] pass   [ ] fail   [x] blocked
Actual:    Deferred to a later pass by operator instruction (2026-08-22): this run was scoped to
           scenarios 1, 2 and 6 through 12 only. Not attempted, not a finding.
           Also needs a brand new session, which this one is not.
Evidence:
Follow-up: Carry to the next pass.

6. agenda-creation writes today's agenda โ€‹

  • Goal: the happy path, end to end.
  • Preconditions: no agenda file for today, or use a scratch agendaDir.
  • Steps:
    1. Invoke with repoRoot, contextHome, and focus set to a real one-sentence focus.
    2. Watch /workflows for the phase order.
  • Expected: phases run Gather, Rank, Write. A file appears at <agendaDir>/<YYYY-MM-DD>.md.
  • Note on the recorded result below: it was taken against the five-phase version (Rules, Backlog, Check, Rank, Record) and is left verbatim. A run reporting those five phases NOW is a FAIL, not a match: it means the caller ran a stale copy of the script.
  • Verify: exactly ONE file created, named for today's Pacific date. No agenda.md. No other day's file touched. context:sync reports pushed.
Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    PASS, and it is the FIRST TIME this workflow has ever written a file. Run wf_49d4fa52-069
           against agendaDir /tmp/agenda-falsify, after the scratch-run fix.

           Phase order ran Rules, Backlog, Check, Rank, Record, in that order, in both re-run arms.

           Verify, clause by clause:
           - Exactly ONE file created: yes. `/tmp/agenda-falsify/2026-08-22.md`, 80,276 bytes, created
             fresh. The record agent confirmed no 2026-08-22.md existed, so a create and not an append.
           - Named for today's Pacific date: yes. The clock was read with
             `TZ=America/Los_Angeles date`, reported 12:35 PDT, not estimated.
           - No agenda.md: confirmed, none created.
           - No other day's file touched: confirmed, 2026-08-19/20/21 mtimes unchanged at 12:12:45.
           - "context:sync reports pushed": NOT APPLICABLE and correctly so. This was a scratch run, so
             the sync was deliberately suppressed. Recorded as inapplicable rather than as a pass.

           SCOPE OF THIS PASS, stated so it is not over-read. The operator chose "leave your real board
           alone", so the happy path against her real agenda directory was never run. What is proven is
           that the workflow writes a correct, correctly-named, single file. What is NOT proven is the
           real-board write, the context:sync push, and the "sync reports pushed" clause above.
Evidence:  (file path, run id) /tmp/agenda-falsify/2026-08-22.md, run wf_49d4fa52-069. Preserved at
           /tmp/armA-agenda.md and /tmp/armA-agenda.html, since the pristine restore for arm B removed
           the originals. Real context home verified untouched after BOTH arms: clean `git status`,
           still on bdedf95, still only three dated files.
Follow-up: The real-board arm is the remaining half and it is hers to authorize. Everything mechanical
           about it is now proven; what is untested is only the sync and push at the end.

Second result, re-run of 2026-08-22 evening against 0739b0a6, run wf_f80b57fb-b43. The earlier block above belongs to wf_49d4fa52-069 and is left verbatim.

Result:    [ ] pass   [x] fail   [ ] blocked
Actual:    FAIL, and it is a regression against the block above, not a flake. NO file was written.

           Verify, clause by clause:
           - Exactly ONE file created: NO. ZERO files created. /tmp/agenda-fmt still holds only the
             three fixture days, byte-identical to the copies made before the run (md5 5c34a543...,
             6dbb7c42..., f6839c7d..., unchanged before and after).
           - Named for today's Pacific date: not applicable, nothing was named.
           - No agenda.md: vacuously true, nothing was created at all.
           - No other day's file touched: yes, and this is the one clause that passes. All three
             fixture mtimes still 15:11.
           - "context:sync reports pushed": not applicable, scratch run, correctly suppressed. The
             run logged "SCRATCH RUN: agendaDir is /tmp/agenda-fmt; nothing will be synced or pushed".

           PHASE ORDER IS CORRECT and is worth recording separately from the failure: Gather, Rank,
           Write, in that order, three phases and not the deleted five. So the stale-script tripwire
           in this scenario's own note is CLEAN. The caller ran the current script; the current
           script is broken.

           THE CAUSE, pinned to one character. The Write agent received the literal prompt `true`.
           Its journal entry reads promptPreview "true", 0 tool calls, 8.0s, 43,509 tokens spent
           returning "No task was provided". Line 330 of .agents/workflows/agenda-creation.js writes
           the phrase "the detail after a `>`" using BACKTICKS inside the Write prompt's own
           backtick template literal. The first of those backticks CLOSES the template early, and
           what follows parses as a chain of `>` string comparisons, so the whole prompt argument
           evaluates to the boolean `true` and `agent(true, ...)` is what actually runs.

           Reproduced in isolation, not inferred: lines 306 to 343 extracted verbatim, evaluated with
           stub bindings, and the prompt came back `typeof "boolean"`, value `true`. Backtick census
           of the template, lines 308 to 343: line 309 has 1, line 330 has 2, line 335 has 1,
           line 337 has 1, line 341 has 1. Seven in total where a well-formed template needs an even
           count with none loose in the prose.

           The irony is worth writing down: the exact line that breaks the Write phase is the line
           that TEACHES the Realtime Log format, which is scenario 20. The format instruction killed
           the writer that was supposed to follow it.

           This bug is committed at HEAD `0739b0a6`, not a local edit. `git status --porcelain
           .agents/workflows/` is empty and `git diff` on the file is empty.
Evidence:  Run wf_f80b57fb-b43. Journal at .../subagents/workflows/wf_f80b57fb-b43/journal.jsonl,
           4 result rows, the fourth being the write agent's refusal string. Full returned object at
           tasks/wev0ut84c.output. Broken line: `.agents/workflows/agenda-creation.js:330`.
Follow-up: One-character class of fix: swap the backticks around `>` on line 330 for quotes, or
           escape them. NOT applied in this run, because editing the script under test changes the
           artifact being measured and it is her branch and her open PR. Scenarios 7, 8, 18, 19, 20
           and 21 all depend on a written file and are all blocked behind this one line.

Third result, re-run of 2026-08-22 after the backtick fix, against fbfba81d, run wf_fe55300a-565. Both blocks above are left verbatim.

Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    PASS, and it is the first end to end pass this scenario has ever had on the three-phase
           script. The backtick fix landed as `fbfba81d` (author: a parallel session; verified in this
           tree before spending a run, prompt now `typeof "string"`, and sync-workflows --check clean).

           Phases ran Gather, Rank, Write. THREE phases, not the deleted five, so this scenario's own
           stale-script tripwire is clean. 4 agents, 0 errors, 223,179 subagent tokens, 7m09s.

           Verify, clause by clause:
           - Exactly ONE file created: YES. `/tmp/agenda-fmt/2026-08-22.md`, 8,022 bytes, 94 lines.
             The write agent checked for it first, so a create and not an append.
           - Named for today's Pacific date: YES, and the clock was genuinely read. The journal shows
             the tool call `TZ=America/Los_Angeles date '+%Y-%m-%d %H:%M'` actually EXECUTED, returning
             16:08. Checked independently three minutes later: 16:11 Pacific. Not estimated.
           - No agenda.md: confirmed, none created.
           - No other day's file touched: confirmed by md5. All three fixture days are byte-identical
             to the committed fixtures at docs/projects/agent-workflow-hierarchy/fixtures/agenda/.
           - "context:sync reports pushed": correctly INAPPLICABLE. Scratch run, suppressed, logged.

           SCOPE, unchanged from the first block: the real-board write and its sync and push remain
           unexercised, deliberately.

           The file is 8,022 bytes against the 80,276 the pre-restructure version produced for the
           same job. That is a tenth the size, which is the one-line-fields rule doing what it was
           written to do (see scenario 18).
Evidence:  /tmp/agenda-fmt/2026-08-22.md, run wf_fe55300a-565.
Follow-up: None on the mechanics. The real-board arm is still hers to authorize.

7. The Focus line is her sentence, verbatim โ€‹

  • Goal: the filter that governed the ranking is recorded unaltered.
  • Preconditions: scenario 6 has run.
  • Steps:
    1. Open the written agenda's header block.
    2. Compare the Focus: line character by character against the sentence passed in.
  • Expected: identical. Not tidied, not shortened, not made more specific, not turned into a heading.
  • Verify: a paraphrase is a FAIL, not a nit. A paraphrase of the filter is a different filter, and the ranking was done against the original.
Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    PASS. The written agenda's header line 5 reads, exactly:

             - Focus: get the app, the admin portal and the merchant portal working end to end

           Compared character by character against the string passed in as the focus arg. Identical.
           Not tidied, not shortened, not made more specific, not turned into a heading. The return
           object also echoes it back unchanged, so it survives all five phases intact.

           CAVEAT, and it matters for how much this pass is worth. The focus exercised here is scenario
           9's fixed test sentence, not the operator's own. Her sentence, "Today's focus is to smooth
           out the agent workflow so we can comfortably utilize it to get normal work back on track.",
           was never written to a file, because the run that would have used it was the real-board run
           she chose not to make. Her sentence is the harder case: it begins "Today's focus is to", so
           a correct header reads "Focus: Today's focus is to smooth out...", which reads redundantly
           and is CORRECT. A header trimmed to "smooth out the agent workflow..." is a FAIL, because a
           tidied filter is a different filter from the one the ranking ran against. The verbatim
           mechanism is proven; the temptation-to-tidy case specifically is not.
Evidence:  /tmp/armA-agenda.md line 5. Run wf_49d4fa52-069.
Follow-up: Re-check this clause on the first run that uses her actual sentence.

Second result, re-run wf_f80b57fb-b43. The earlier block above is left verbatim.

Result:    [ ] pass   [ ] fail   [x] blocked
Actual:    BLOCKED. There is no written agenda to open, so the header block this scenario compares
           cannot be inspected. See scenario 6 for the single cause.

           What CAN be said, and it is deliberately not scored as a pass, because this scenario is
           about the FILE and not about the plumbing: the focus string survived every phase intact.
           It was echoed into the run log as "focus: get the app, the admin portal and the merchant
           portal working end to end", reached the Rank agent's prompt verbatim under "HER FOCUS FOR
           TODAY, in her own words", and came back on the returned object's `focus` key character for
           character identical to the string passed in. Zero drift across Gather, Rank and Write.

           The clause this scenario actually owns, whether the WRITER tidies her sentence on its way
           into the header line, is untested for a third run running. The writer never executed.

           The caveat recorded in the block above still stands unchanged: this focus is the fixed
           test sentence, not her own "Today's focus is to" phrasing, which is the harder case.
Evidence:  Run wf_f80b57fb-b43 returned object, `focus` key. No agenda file exists to cite.
Follow-up: Same as scenario 6. Unblocks the moment line 330 is fixed and a run writes a file.

Third result, re-run of 2026-08-22 after the backtick fix, against fbfba81d, run wf_fe55300a-565. Both blocks above are left verbatim.

Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    PASS. Header line 5 of the written file reads, exactly:

             - Focus: get the app, the admin portal and the merchant portal working end to end

           Compared character by character against the string passed in as `focus`. Identical. Not
           tidied, not shortened, not made more specific, not turned into a heading, and not wrapped
           in quotes. The returned object echoes it back unchanged as well, so it is intact at both
           ends of all three phases.

           The focus also demonstrably drove the output rather than just riding along: all three
           ranked rows are local-environment blockers on the admin and merchant portals, and the cut
           reasons argue explicitly against the focus ("if the app, admin portal and merchant portal
           were all working end to end tomorrow, this would still be dropping the second delivery").

           THE CAVEAT FROM THE FIRST BLOCK STILL STANDS, and should not be read as closed by this
           pass. The sentence exercised here is the fixed test focus. Her own phrasing, which starts
           "Today's focus is to", is the harder case, because a correct header reads redundantly and
           a tidied one is a different filter. Still untested on a third run.
Evidence:  /tmp/agenda-fmt/2026-08-22.md line 5. Run wf_fe55300a-565.
Follow-up: Unchanged: re-check on the first run that uses her actual sentence.

8. The written agenda matches the format skill โ€‹

  • Goal: the writer writes the current format, not a remembered one.
  • Preconditions: scenario 6 has run.
  • Steps:
    1. Open .agents/skills/agenda-documentation/skill.md, "Agenda Creation" state.
    2. Compare section by section against the written file.
  • Expected: every section present, none invented, none from the old numbered format.
  • Verify, all three explicitly: there is no top-level "Need from you" table; every session block carries a Needs from you line with none written where there is nothing; Parked rows each say what would unpark them. Section 0 through 5 headings appearing anywhere is a FAIL: that is a deleted format, and its reappearance means the writer worked from memory. Scenarios 18 through 21 cover the parts of the format added since this scenario was written; this one still owns the asks, the focus and Parked.
Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    PASS on all three explicit checks and on the tripwire.

           - NO top-level "Need from you" table: confirmed, zero such headings in the file.
           - Every session block carries a "Needs from you" line: confirmed, 10 occurrences across 10
             session blocks, which is one per inProgress row. The record agent reported that 4 blocks
             carry real asks and "the other 6 blocks say `none`", which is the rule about writing
             `none` rather than leaving it blank, obeyed.
           - Parked rows say what would unpark them: confirmed. Parked is a table with an explicit
             "What unparks it" column, populated on every row, and several say plainly "Her word only"
             with the reason (a rules change on a privacy surface, a build-config change a PM handing
             out is already on record as an error).

           TRIPWIRE CLEAN: zero section 0 through 5 headings anywhere in the file. The headings written
           are Key Focus Blocks, Realtime Log and Activity Blockers, Focus Tasks and Session Prompts,
           ten Session blocks, Gaps, Collisions and Sequencing, Parked, Cut today, and What the survey
           did NOT see. That is the current format, not the one deleted on 2026-08-22, so the writer
           worked from the skill rather than from memory. Worth noting because the risk was real: the
           yesterday-board reader's own coverage note describes the 2026-08-21 file in old numbered-
           format vocabulary, so old-format wording WAS present in the ranker's inputs and the writer
           did not reproduce it.

           Also correct, and not required by this scenario: the Gaps channel got its own section, kept
           visually separate from the task list with a lead line saying nobody is carrying those rows.
Evidence:  /tmp/armA-agenda.md, compared against .agents/skills/agenda-documentation/skill.md, the
           "Agenda Creation" state. Run wf_49d4fa52-069.
Follow-up: None.

Second result, re-run wf_f80b57fb-b43. The earlier block above is left verbatim.

Result:    [ ] pass   [ ] fail   [x] blocked
Actual:    BLOCKED. No file was written, so there is nothing to compare against the format skill.
           See scenario 6 for the cause.

           None of the three explicit checks could be run: no top-level "Need from you" table,
           a "Needs from you" line on every session block, and Parked rows naming what unparks them.
           The section 0 through 5 tripwire is also unexercised, which matters, because a tripwire
           that never fires is not the same as a tripwire that passed.

           One structural observation from the RANK output, which does not score this scenario but
           does bear on it: the ask channel returned 2 rows and each names the task it sits on via an
           `onTask` field ("Decide the retention window (parked)" and "Fix the webhook retry loop
           (cut from today)"). So the data handed to the writer is shaped for asks-on-their-block and
           not for a top-level table. Whether the writer would have honoured that shape is exactly
           what went unmeasured.
Evidence:  Run wf_f80b57fb-b43. No agenda file at /tmp/agenda-fmt/2026-08-22.md.
Follow-up: Same as scenario 6.

Third result, re-run of 2026-08-22 after the backtick fix, against fbfba81d, run wf_fe55300a-565. Both blocks above are left verbatim.

Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    PASS on all three explicit checks and on the tripwire, with one format tension worth
           recording that this scenario does not currently score.

           - NO top-level "Need from you" table: CONFIRMED. Zero such headings.
           - Every session block carries a "Needs from you" line: CONFIRMED. Three blocks, three
             lines, all reading exactly `- Needs from you: none`. The word is written rather than the
             line left blank, which is the rule.
           - Parked rows say what would unpark them: CONFIRMED. Four rows, all three columns
             populated, and the geocoder row is flagged in its own cell as `FLAG: CONDITION ALREADY
             FIRED` with the note that it is not independently verified beyond the agenda's wording.

           TRIPWIRE CLEAN: zero section 0 through 5 headings. Sections written are the day header,
           Key Focus Blocks, Realtime Log and Activity Blockers, Focus Tasks and Session Prompts,
           three Session blocks, User Question Log, Parked, Cut today, Gaps. That is the current
           format.

           WORTH RECORDING SEPARATELY, because it is the strongest single piece of evidence in this
           run that the writer read the skill rather than its neighbours: yesterday's fixture
           (2026-08-21.md) is in the OLD format, with `Required Skills`, `Context / Content`, `Link to
           project`, a five-line prompt and no emojis, and the writer NAMED that in its report and
           followed the skill anyway. The old format was sitting in its input and did not leak.

           THE TENSION, and it is a format question rather than a writer defect. All three asks went
           to the User Question Log and every session block says `none`. The skill says an ask lives
           on its task block and "that line is the only place an ask lives in this document"; it also
           says every question asked of her is logged, without exception. All three of today's asks
           attach to tasks that are PARKED or CUT, so no session block exists to carry them. The
           writer routed them to the log and said why. That looks correct, but the two rules do not
           currently say what happens to an ask on a task that is not on today's list.
Evidence:  /tmp/agenda-fmt/2026-08-22.md, lines 22, 36, 50 (Needs from you), 60-67 (question log),
           68-77 (Parked). Compared against .agents/skills/agenda-documentation/skill.md.
Follow-up: Decide where an ask on a parked or cut task lives, and write it into the format skill. The
           current wording makes the log and the block rule collide whenever the ask outlives the
           task's place on the day.

9. FALSIFYING PAIR A: a narrow focus cuts most of it โ€‹

This scenario and scenario 10 are the pair that can tell us the whole restructure is decorative. State both predictions out loud before running either.

  • Goal: show the focus actually filters, by choosing one that should exclude nearly all current work.

  • Preconditions: a scratch agenda directory that is a COPY of the real one, not an empty one:

    cp -r ~/.agents/context/lantern_app/agenda /tmp/agenda-falsify

    Both runs then read identical carried work and write somewhere disposable. An EMPTY scratch dir would give the Gather readers nothing to carry, both runs would rank nothing, and the pair would agree by being vacuous. That is the defect this pair exists to avoid, so it must not be the way the pair is set up.

  • Prediction: the ranked list is SHORT and the cut channel is LONG, because today's carried work is agent tooling and the focus is product surfaces.

  • Steps:

    1. Invoke with focus = "get the app, the admin portal and the merchant portal working end to end".
    2. Record the count of ranked and the count of cut from the returned object.
    3. Read three cut reasons and judge whether each is fair against that focus.
  • Expected: most agent-tooling candidates land in cut, each with a reason naming the focus.

  • Verify: a cut item is REPORTED, not missing. Anything cut appears in the agenda under Parked with its reason.

Result:    [x] pass   [ ] fail   [ ] blocked
Actual:     ranked count = 0        cut count = 15        candidates = 69
           RE-RUN 2026-08-22 against `7ca235e4`, run wf_49d4fa52-069, after the three fixes. All 8
           agents returned, 0 errors. gaps 2, inProgress 10, operatorAsks 16, parked 28.

           Its own prediction was "ranked SHORT and cut LONG", with an empty ranked list named in
           advance as a PASS rather than a broken run. That is exactly what happened, and the record
           agent did not paper over it: the written agenda says the ranked list is empty and that the
           day's blocks are therefore cut from in-progress work rather than from a ranking.

           Three cut reasons were read and judged fair against this focus: idle worktrees on merged
           branches, squash-residue branches that cannot carry new work, and the tree being level with
           origin/dev. All three are repo hygiene, none moves the three surfaces, and each cut reason
           names the focus. Cut rows are reported, not missing: all 15 appear in the written agenda
           under Parked in a "Cut today" list.

           THE GATE PASSED AND THE GATE IS NOT ENOUGH. Both arms reported candidateCount 69, which
           is the condition the re-run was gated on. Comparing the candidate TITLES rather than the
           counts, the two arms shared only 35 of 69. Jaccard 0.34. Half the candidate list differs
           between the arms, on byte-identical input files, because a fresh set of reader agents runs
           each time and they do not return the same rows. Some of the difference is paraphrase of the
           same fact ("Worktree pm-session has been idle 12 days on a merged branch" against
           "pm-session worktree is squash residue and cannot carry new work"). Some is genuinely
           different content: arm B carried "Restructure planning into per-project folders" and "The
           Claude effort setting resets to high in the app", which arm A never saw at all.

           So candidateCount is a COUNT gate on a CONTENT problem, and it passed by coincidence. Any
           channel-membership comparison across the two arms is invalid, including the zero-overlap in
           `cut` (15 rows against 10, no shared titles) which looks like dramatic evidence of the
           focus filtering and is mostly the readers rewording the same repo observations.

           THE RANKED LISTS ARE IDENTICAL. Both arms returned ZERO ranked rows. Recording that plainly
           as the plan instructs, rather than finding a reading that makes it a pass: the pair does NOT
           demonstrate that the focus drives the ranking, because there was no ranking either time.
           Arm B's prediction was "ranked LONGER than arm A". It was not longer. It was equal, at zero.

           WHY RANKED IS EMPTY, and this is the useful part. Both rank agents said so deliberately and
           in almost the same words. Arm A: "THE RANKED LIST IS EMPTY, DELIBERATELY. Nothing on the
           carried list is startable work that serves the focus. Every carried row is one of: an
           observation about the tree (cut)..." Every one of the 69 candidates routes to a channel
           that is not `ranked`: owned work goes to inProgress, questions go to operatorAsks, deferred
           work goes to parked, tree observations go to cut. The rank prompt's own rule 3 forbids
           ranking any row whose owner is recorded, and on this board almost everything has an owner.
           `ranked` is therefore close to structurally empty regardless of what the focus says, which
           is a property of the routing rules, not of the focus.

           WHAT THE FOCUS DEMONSTRABLY DOES REACH: the cut reasons argue against the focus given (arm
           A's cite the three surfaces, arm B's cite agent tooling), and the gaps channel is
           focus-shaped, 2 rows in arm A both about the product surfaces, 0 in arm B. That is real and
           it is not nothing. It is also not what this pair was built to measure.

           VERDICT ON THE PAIR: it still cannot answer its question, now for a second and different
           reason. First time the arms had unequal inputs because a reader was silently killed. This
           time the arms had unequal inputs because the readers are non-deterministic. The fix is not
           another gate on the count: the candidate list has to be captured ONCE and replayed to both
           arms, so the only variable between them is the focus string. Until that happens this pair
           cannot falsify anything.
Evidence:  Run wf_49d4fa52-069. Counts from the returned object. Written agenda preserved at
           /tmp/armA-agenda.md (80,276 bytes) since the pristine restore for arm B deleted the original.
Follow-up: Marked pass because every expectation this scenario states was met. The pair-level verdict
           is on scenario 10 and it is not a pass. Do not read this row as the pair working.

10. FALSIFYING PAIR B (control): a matching focus cuts little โ€‹

  • Goal: the control. Our account of the cause says the focus is what drives the filtering, so a focus that MATCHES the work should behave oppositely.
  • Preconditions: the SAME /tmp/agenda-falsify copy, same day, same carried work. Do not re-copy between the two runs: if the inputs differ, the comparison means nothing.
  • Prediction: the ranked list is LONGER and the cut channel SHORTER than in scenario 9, on the same input.
  • Steps:
    1. Invoke with focus = "the agent workflow hierarchy: skills, workflows, and the agenda".
    2. Record ranked and cut counts.
    3. Compare against scenario 9.
  • Expected: the two runs disagree substantially about what belongs.
  • What a surprising result means, and this is the point of the pair: if 9 and 10 produce similar rankings, the focus input is decorative and the diagnosis is wrong. "It ranked the wrong things because it had no focus input" would then be an incomplete explanation, the cause lies elsewhere (most likely in the candidate sourcing), and this reopens. Do not record that as a pass on a technicality.
Result:    [ ] pass   [x] fail   [ ] blocked
Actual:     ranked count = 0        cut count = 10        candidates = 69        vs scenario 9: RANKED IDENTICAL
           RE-RUN 2026-08-22 against `7ca235e4`, run wf_474a8577-dec, on a pristine-restored input.
           All 8 agents returned, 0 errors. gaps 0, inProgress 15, operatorAsks 14, parked 30.

           The prediction was "the ranked list is LONGER and the cut channel SHORTER than in scenario
           9, on the same input". Cut was shorter (10 against 15). Ranked was NOT longer: it was equal,
           at zero. The plan's own words for this outcome are "if 9 and 10 produce similar rankings,
           the focus input is decorative and the diagnosis is wrong... Do not record that as a pass on
           a technicality." Identical is stronger than similar, so this is recorded as a fail.

           The honest qualification, which cuts BOTH ways and is why the diagnosis is not simply
           "decorative": the focus visibly reaches the cut reasons and the gaps channel, so it is not
           inert. But the ranked channel, which is the thing the pair measures and the thing her day
           is built from, was unaffected by a complete reversal of the focus.

           THE GATE PASSED AND THE GATE IS NOT ENOUGH. Both arms reported candidateCount 69, which
           is the condition the re-run was gated on. Comparing the candidate TITLES rather than the
           counts, the two arms shared only 35 of 69. Jaccard 0.34. Half the candidate list differs
           between the arms, on byte-identical input files, because a fresh set of reader agents runs
           each time and they do not return the same rows. Some of the difference is paraphrase of the
           same fact ("Worktree pm-session has been idle 12 days on a merged branch" against
           "pm-session worktree is squash residue and cannot carry new work"). Some is genuinely
           different content: arm B carried "Restructure planning into per-project folders" and "The
           Claude effort setting resets to high in the app", which arm A never saw at all.

           So candidateCount is a COUNT gate on a CONTENT problem, and it passed by coincidence. Any
           channel-membership comparison across the two arms is invalid, including the zero-overlap in
           `cut` (15 rows against 10, no shared titles) which looks like dramatic evidence of the
           focus filtering and is mostly the readers rewording the same repo observations.

           THE RANKED LISTS ARE IDENTICAL. Both arms returned ZERO ranked rows. Recording that plainly
           as the plan instructs, rather than finding a reading that makes it a pass: the pair does NOT
           demonstrate that the focus drives the ranking, because there was no ranking either time.
           Arm B's prediction was "ranked LONGER than arm A". It was not longer. It was equal, at zero.

           WHY RANKED IS EMPTY, and this is the useful part. Both rank agents said so deliberately and
           in almost the same words. Arm A: "THE RANKED LIST IS EMPTY, DELIBERATELY. Nothing on the
           carried list is startable work that serves the focus. Every carried row is one of: an
           observation about the tree (cut)..." Every one of the 69 candidates routes to a channel
           that is not `ranked`: owned work goes to inProgress, questions go to operatorAsks, deferred
           work goes to parked, tree observations go to cut. The rank prompt's own rule 3 forbids
           ranking any row whose owner is recorded, and on this board almost everything has an owner.
           `ranked` is therefore close to structurally empty regardless of what the focus says, which
           is a property of the routing rules, not of the focus.

           WHAT THE FOCUS DEMONSTRABLY DOES REACH: the cut reasons argue against the focus given (arm
           A's cite the three surfaces, arm B's cite agent tooling), and the gaps channel is
           focus-shaped, 2 rows in arm A both about the product surfaces, 0 in arm B. That is real and
           it is not nothing. It is also not what this pair was built to measure.

           VERDICT ON THE PAIR: it still cannot answer its question, now for a second and different
           reason. First time the arms had unequal inputs because a reader was silently killed. This
           time the arms had unequal inputs because the readers are non-deterministic. The fix is not
           another gate on the count: the candidate list has to be captured ONCE and replayed to both
           arms, so the only variable between them is the focus string. Until that happens this pair
           cannot falsify anything.
Evidence:  Run wf_474a8577-dec. Candidate-title overlap computed from both runs' journals: 35 shared
           of 69 each, 34 unique to each arm.
Follow-up: THIS REOPENS. Two things are needed before the pair can be run a third time, and the second
           is the load-bearing one. (1) The candidate list must be captured once and replayed to both
           arms, so the focus string is the only variable; re-running the readers per arm makes them
           the dominant variable instead. (2) Someone has to decide whether an always-empty `ranked`
           is correct behaviour. If almost every carried row has an owner and rule 3 forbids ranking
           owned rows, then `ranked` is near-permanently empty and the pair is measuring a channel
           that cannot move. That is a design question about the routing rules, not a bug, and it
           needs answering before another run is paid for.

11. FALSIFYING: no phase after Gather adds rows โ€‹

Retitled 2026-08-22, and the retitle is itself a finding. This scenario was "the Check phase adds no rows" until the run below showed the Check phase is innocent: it returned exactly one annotation per candidate and invented nothing, on both arms. The rows were invented one phase later, by the RANK agent, on near-identical wording. Aimed at Check alone this scenario would PASS while the constraint it protects FAILS. It now covers every phase downstream of the candidate-gathering step. Do not re-aim it at the innocent phase.

The phase it names was renamed. Backlog and Check were folded into Gather, which runs two readers rather than three. The recorded result below says "three Backlog readers" and is left verbatim. The constraint is unchanged: nothing downstream of Gather may add a row.

  • Goal: the filter constraint is obeyed, not merely stated. Prose telling an agent not to do something is exactly what failed before.
  • Preconditions: any completed run.
  • Prediction: every title in ranked, asks, parked and cut traces back to a row a Gather reader returned. Only gaps may hold a row no reader carried, and it is capped at two.
  • Steps:
    1. From the run journal, list every title returned by the Gather readers.
    2. List every title in the ranked, asks, cut and parked channels.
    3. Diff them.
  • Expected: the second set is a subset of the first. Zero rows appear that no reader carried.
  • Verify: the launch plan has hundreds of rows and the tracker 177 open issues. A single row appearing from either, however sensible it looks, is a FAIL and means the constraint did not bind. Note it by name.
Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    PASS, in BOTH arms, and this is the one unambiguous win of the day. The structural fix bound
           where the prose constraint did not.

           Diffed every output channel against the titles the three Backlog readers actually carried:

                          arm A (69 carried)        arm B (69 carried)
             ranked        0 rows, 0 uncarried       0 rows, 0 uncarried
             inProgress   10 rows, 0 uncarried      15 rows, 0 uncarried
             parked       28 rows, 0 uncarried      30 rows, 0 uncarried
             cut          15 rows, 0 uncarried      10 rows, 0 uncarried
             gaps          2 rows, 2 uncarried       0 rows

           Zero rows appeared from nowhere in any channel that is supposed to carry only carried work.
           The two uncarried rows in arm A are in `gaps`, which is the channel built for exactly them,
           within its cap of 2, and both are genuine focus-shaped observations rather than launch-plan
           mining: "Exercise the app, the admin portal and the merchant portal together and write down
           where each one breaks" and "Local admin and merchant portal login is impossible, so all
           portal work is forced onto deployed dev (#819)". Arm B produced no gaps at all, which is
           also allowed.

           Compare with the pre-fix run, where 2 of 2 ranked rows were launch-plan mining and the rank
           agent cited an exception that did not exist in its prompt. The difference is that the rows
           now have somewhere legitimate to go, so the agent no longer has to choose between suppressing
           a real observation and putting it somewhere wrong. That is worth recording as a general
           lesson: the previous failure was a ROUTING failure wearing the costume of a discipline
           failure, and giving the honest output a correct destination fixed it where forbidding it did
           not.
Evidence:  Runs wf_49d4fa52-069 and wf_474a8577-dec, diffed against their own journals' reader rows.
Follow-up: None on this scenario. Note that it now passes on a workflow that also returns an empty
           ranked list in both arms, so a future reader should not take this pass as evidence the
           ranking works. It only shows nothing is invented.

Second result, re-run wf_f80b57fb-b43, and it CONFIRMS the pass above on a different focus and a different candidate set. The earlier block is left verbatim.

Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    PASS. This is the one scenario in this run that is fully measurable without a written
           file, because it reads the returned channels rather than the agenda, and it passes.

           Gather returned 15 candidates from two readers (carry 8, plan 7). Every title in every
           channel was diffed against those 15:

                          rows    uncarried
             ranked        4        0
             asks          2        0
             parked        4        0
             cut           6        0
             gaps          1        1

           Zero rows appeared from nowhere in any channel that may carry only gathered work. The
           single uncarried row sits in `gaps`, which is the channel built for exactly that, and it
           is 1 against a cap of 2: "An actual end to end pass across the app, the admin portal and
           the merchant portal". That is focus-shaped rather than launch-plan mining, which is the
           distinction that mattered in the original failure.

           The two `asks` rows are not new work. Both point back at gathered rows through their
           `onTask` field, one at the parked retention decision and one at the cut webhook row.

           Accounting closes exactly: 4 ranked plus 4 parked plus 6 cut is 14, and the fifteenth
           gathered row ("Decide whether a dropped webhook event is replayed from the log or accepted
           as lost") was routed into `asks` rather than dropped. Nothing gathered went missing and
           nothing ungathered appeared.

           WORTH RECORDING AGAINST THE PREVIOUS RUN'S CAVEAT. The follow-up on the block above warns
           that its pass came on a run whose `ranked` was EMPTY in both arms, so it only showed that
           nothing was invented. This run ranked FOUR rows and still invented nothing, so the
           constraint now has a real ranking under it. The rank agent also placed in-progress work in
           `ranked` (rank 3, "Credential and identity colocation", status "In progress"), which is the
           behaviour the owner rule used to suppress.
Evidence:  Run wf_f80b57fb-b43, journal.jsonl diffed against its own two reader result rows.
           Extracted candidate and channel sets preserved at /tmp/agenda-fmt-analysis.json.
Follow-up: None.

Third result, run wf_fe55300a-565, against fbfba81d. Both blocks above are left verbatim.

Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    PASS, a third consecutive time, and now on a run that also WROTE the file.

           15 candidates gathered from two readers. Every title in every channel diffed against them:

                          rows    uncarried
             ranked        3        0
             asks          3        0
             parked        4        0
             cut           7        0
             gaps          1        1

           17 rows across the four channels that may carry only gathered work, zero uncarried. The one
           uncarried row is in `gaps`, within its cap of 2: "An actual end-to-end pass across the
           three surfaces, recording what breaks". Focus-shaped, not launch-plan mining, and the same
           observation the previous run's gap made, which is itself a small consistency signal.

           Accounting closes: 3 ranked plus 4 parked plus 7 cut is 14, and the fifteenth gathered row
           went to asks. Nothing gathered was dropped, nothing ungathered appeared.

           NEW HERE, and it strengthens the pass: the written FILE was diffed too, not only the return
           object. Every session block, every Parked row and every Cut row in the markdown traces to a
           gathered candidate. The invention risk survives into the writer and it did not fire there
           either.
Evidence:  Run wf_fe55300a-565, journal.jsonl diffed against its own reader rows, and
           /tmp/agenda-fmt/2026-08-22.md diffed against the same set.
Follow-up: None. The caveat on the second block is now retired: this pass came with a non-empty
           `ranked` AND a written file, so it is no longer only evidence that nothing is invented.

12. The agenda reader greps rather than reading 185KB โ€‹

  • Goal: the cost fix actually happened.
  • Preconditions: a completed run.
  • Steps:
    1. Read the yesterday-board reader's coverage note in the journal.
    2. Read its tool calls.
  • Expected: it names which sections it extracted and roughly how much of each file it read.
  • Verify: it used grep or sed against the sections, not a whole-file read. A coverage note that does not say is itself a finding: the instruction asks for it explicitly.
Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    PASS, and the coverage note is better than the bar the scenario sets. The yesterday-board
           reader in run wf_f1b91c9c-e29 named every section it extracted AND quantified how much of
           each file it read, without being asked for a percentage.

           2026-08-21.md (189.7KB, 1893 lines): two structural passes first, a heading grep and a
           keyword grep for parked / close-of-day / EOD / Focus markers, then three targeted sed
           extracts (lines 1 to 70, 1705 to 1801, 1857 to 1893). Total 41.4KB of 189.7KB, about 22
           percent, 204 of 1893 lines. It then listed what it did NOT read, by section and line
           range, and flagged the few facts it picked up from keyword grep line hits rather than
           from reading the section.

           2026-08-20.md (70KB, 380 lines): same two grep passes, then sed on lines 9 to 55 and 361
           to 380. Total 8.9KB of 70KB, about 13 percent.

           2026-08-19.md was not read, stated as out of scope rather than left silent.

           No whole-file read of either file. The cost fix is real.
Evidence:  Journal for wf_f1b91c9c-e29, the yesterday-board reader's coverage field. It also
           correctly reported that no today file existed in the scratch directory, and said what it
           did about it: it treated 2026-08-21.md as the previous day's file and additionally pulled
           its unfinished task-list rows, so mid-flight work on four branches would not vanish.
Follow-up: One side effect worth naming, not a fault of this scenario. The reader runs
           `npm run context:sync` against the REAL repo and context home even when agendaDir points
           at a scratch directory, and it reported "pulled, nothing to commit, pushed, done". It was
           a no-op push here, so nothing was harmed. It does mean agendaDir does not fully isolate a
           run from the real context home, which is worth knowing before anyone treats a scratch
           agendaDir as a dry run.

13. agenda-status returns an audit when there is no board โ€‹

  • Goal: a boardless day stops cheaply and says what it is.
  • Preconditions: a day with NO agenda file (use a scratch empty agendaDir).
  • Steps:
    1. Invoke agenda-status.js with trigger: pre-eod.
  • Expected: returns verdict: NO_BOARD_TO_RECONCILE after the Claimed phase, before the observer fan-out.
  • Verify, all four: readersOk is empty (the fan-out did not run); callerMustPublish is false; publishNote is PRESENT and says DO NOT PUBLISH; callerMustDispatch and callerMustNotify are empty arrays, not undefined. On 2026-08-22 the early return omitted three of these and a caller read the absence as "no instruction".
Result:    [ ] pass   [ ] fail   [x] blocked
Actual:    Deferred to a later pass by operator instruction (2026-08-22): this run was scoped to
           scenarios 1, 2 and 6 through 12 only. Not attempted, not a finding.
Evidence:
Follow-up: Carry to the next pass.

14. agenda-status Reconcile runs against a real board โ€‹

  • Goal: the Reconcile phase executes. It has never run. Two attempts died before reaching it, for two different reasons.
  • Preconditions: scenario 6 has produced a real agenda file.
  • Steps:
    1. Invoke agenda-status.js with trigger: operator-status-request and sessionReports collected by the calling session.
    2. Inspect the returned object.
  • Expected: statusChanges, completions, logGaps and stillUnknown are all present, and verdict is NOT NO_BOARD_TO_RECONCILE.
  • Verify: at least one finding sits in a channel the Reconcile schema owns rather than flat in an observations array. On the previous run the reader prompts were doing the reconcile's job, which is why this scenario exists.
Result:    [ ] pass   [ ] fail   [x] blocked
Actual:    Deferred to a later pass by operator instruction (2026-08-22): this run was scoped to
           scenarios 1, 2 and 6 through 12 only. Not attempted, not a finding.
Evidence:
Follow-up: Carry to the next pass.

15. agenda-status Record writes, and the publish gate opens โ€‹

  • Goal: the Record phase executes for the first time, and callerMustPublish reflects reality.
  • Preconditions: scenario 14 completed with a real board.
  • Steps:
    1. From the same run, read the agenda's Realtime Log.
    2. Read callerMustPublish and publishNote.
  • Expected: a new timestamped line appended, not a rewrite. callerMustPublish is true and publishNote names the page and the standing URL.
  • Verify: the timestamp is Pacific and matches wall clock; it must not be UTC stamped as Pacific. Then the inverse, which has also never been exercised: if Record ever fails, callerMustPublish must be false with the DO NOT PUBLISH note. Force it if you can, or record that the failure arm remains untested.
Result:    [ ] pass   [ ] fail   [x] blocked
Actual:    Deferred to a later pass by operator instruction (2026-08-22): this run was scoped to
           scenarios 1, 2 and 6 through 12 only. Not attempted, not a finding.
Evidence:
Follow-up: Carry to the next pass.

16. A session's claim that disagrees with git is reported โ€‹

  • Goal: the self-report is treated as a claim, not evidence.
  • Preconditions: at least two live sessions, one of which will answer with a deliberate inaccuracy.
  • Steps:
    1. The calling session polls the lanes and collects replies.
    2. Have one lane answer with something true about ITSELF but false about the BRANCH, for example "no commits today" while other lanes have committed to the same branch.
    3. Pass the reports in and run with trigger: operator-status-request.
  • Expected: the observer reports BOTH the claim and what git shows, and names which is which.
  • Verify: the branch is NOT recorded as cold. This exact shape was planted on 2026-08-22 and caught; it must still be caught after the restructure.
Result:    [ ] pass   [ ] fail   [x] blocked
Actual:    Deferred to a later pass by operator instruction (2026-08-22): this run was scoped to
           scenarios 1, 2 and 6 through 12 only. Not attempted, not a finding.
Evidence:
Follow-up: Carry to the next pass.

17. A reader that throws is ABSENT, not empty โ€‹

  • Goal: the failure-partitioning branch executes. It never has: both runs had every reader return.
  • Preconditions: none.
  • Steps:
    1. Temporarily point one reader at a path that does not exist, or otherwise force it to throw. Back the file up first.
    2. Run.
    3. Restore.
  • Expected: readerFailures names that reader with its error, and the downstream prompt says its input is ABSENT rather than empty.
  • Verify: the run still completes rather than aborting, and nothing downstream treats the missing input as "nothing found". An absent reader silently reading as a clean result is the failure this branch exists to prevent.
Result:    [ ] pass   [ ] fail   [x] blocked
Actual:    Deferred to a later pass by operator instruction (2026-08-22): this run was scoped to
           scenarios 1, 2 and 6 through 12 only. Not attempted, not a finding.
Evidence:
Follow-up: Carry to the next pass.

18. Every task field is ONE line โ€‹

  • Goal: the writer COMPRESSES the gathered detail rather than pasting it through.
  • Preconditions: a completed agenda-creation run that wrote a file.
  • Prediction: stated in the predictions table above. This is the scenario expected to fail.
  • Steps:
    1. Extract every - Context: line from the written agenda.
    2. Measure each one's character count, and name the longest with its length.
    3. Extract the other field lines on each session block: Skills, Project, Needs from you, Status.
  • Expected: every field is a single line. No field wraps into a second bullet, a sub-list or a paragraph.
  • Verify: a Context line over roughly 200 characters is a FAIL even though it is technically one line. "One line" means what a person reads in one, not what fits before a newline.
  • Verify the cause, not just the symptom: for any block whose Context ran long, check whether that row has a Project: link. A task carrying its whole history in the agenda is usually a project that was never created, and the fix is the project, not a shorter sentence.
Result:    [ ] pass   [ ] fail   [x] blocked
Actual:    BLOCKED, and the prediction is UNMEASURED. Recorded that way deliberately: scenario 18 was
           predicted to FAIL, and it would be easy and wrong to read a run that produced no file as
           confirmation. No Context line exists, so no Context line was measured. The prediction
           stands untested for the next run.

           No file was written. See scenario 6 for the cause, one stray pair of backticks on line 330
           of the workflow script.

           THE SECOND VERIFY CLAUSE CAN BE ANSWERED, and it is the more interesting half. The clause
           says: for any block whose Context ran long, check whether that row has a `Project:` link,
           because a task carrying its whole history is usually a project that was never created.
           That can be checked on the pipeline rather than on the page, and the answer is that NO row
           can have one.

           The `RANKED` schema at .agents/workflows/agenda-creation.js:206 declares
           `additionalProperties: false` on every channel item, and no channel item declares a
           project field. A ranked row may carry rank, title, detail, why, status and issues, and
           nothing else. All four ranked rows came back with exactly those six keys. So the rank
           agent CANNOT emit a project link even if it wanted to, and the writer has nothing to
           populate the format's `Project:` line from. The format skill's own rule, "when a task needs
           more than one line, it has a project, the Project field links it", has no wire to travel
           on.

           That is the stated cause, confirmed structurally and in advance of the symptom.

           SIZE OF THE COMPRESSION THE WRITER WOULD HAVE FACED, measured on the input it was handed,
           since the output does not exist. The four ranked rows carry `detail` fields of 277, 281,
           372 and 282 characters, and the schema marks that field "Passed through from the gathered
           item UNCHANGED". The 15 gathered details run 277 to 915 characters, mean 490. The Verify
           clause sets the Context bar at roughly 200. So the writer would have had to cut the
           SHORTEST of its four inputs by about a quarter, and the longest by nearly half, with no
           Project field to move the remainder into. That is the shape of the predicted failure, and
           it is the reason the prediction was written the way it was.
Evidence:   No agenda file. Run wf_f80b57fb-b43. Schema at
            `.agents/workflows/agenda-creation.js:206-232`, ranked rows in
            /tmp/agenda-fmt-analysis.json.
Follow-up:  Two, and they are separable. (1) Unblock by fixing line 330, then re-run and MEASURE the
            Context lines, because the prediction deserves a real number. (2) The missing project
            field is a finding on its own and does not wait on the re-run: whatever the Context lines
            turn out to be, the `Project:` line cannot be filled today. Per this scenario's own
            framing, that points the fix at creating the project rather than at shortening a sentence.

Third result, re-run of 2026-08-22 after the backtick fix, against fbfba81d, run wf_fe55300a-565. Both blocks above are left verbatim.

Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    PASS, and THE PREDICTION WAS WRONG. Recorded that way deliberately, because the prediction
           table calls this "the one I would bet against".

           EVERY field on every session block is a single line. No field wraps into a second bullet,
           a sub-list or a paragraph. Three blocks, five fields each, measured by character count:

             field             s1    s2    s3
             Skills            31    40    38
             Context          141   114   122
             Project           43    43    43
             Needs from you     4     4     4
             Status            13    13    13

           THE LONGEST CONTEXT IS 141 CHARACTERS, at line 20:
             "Application Default Credentials cannot mint custom tokens, so there is no way to sign
              into the admin or merchant portal outside deployed dev."

           Against a Verify bar of roughly 200, and against the ~1,100-character Context the
           prediction cites from the previous run. The compression is real: the writer was handed
           `detail` fields of 341, 219 and 268 characters and returned 141, 114 and 122. It cut every
           one, roughly in half, rather than pasting through. The whole file is 8,022 bytes against
           80,276 for the same job before the format change.

           THE CAUSE CLAUSE, answered properly rather than skipped because the symptom did not appear.
           The clause says: for any block whose Context ran long, check whether that row has a
           `Project:` link. No Context ran long, so the clause has no trigger. But the underlying
           finding recorded in the block above stands and was CONFIRMED in the output: the `RANKED`
           schema still cannot carry a project link, and all three Project lines read, identically:

             - Project: none yet ( run project-creation on pickup )

           The writer did the honest thing. It did not invent a path (verified: none of
           docs/projects/local-admin-merchant-login, local-service-stacks or doctor-learns-more exists
           on disk), and it did not leave the field blank. It said there is no project and named who
           creates one.

           SO THE TWO CAUSES ARE CLEANLY SEPARATED, which is the thing this scenario was written to
           do. With no project field reachable anywhere in the pipeline, a long Context CANNOT be the
           missing link surfacing as prose. Anything long would have to be the writer failing to
           compress. Nothing was long. Therefore: the writer compresses correctly, AND the missing
           project link is a real, separate gap that this run happens not to be blocked by. Filed as
           #953 (agenda-creation cannot fill the agenda's Project field: no channel in the RANKED
           schema can carry a project link).

           WHAT THE PREDICTION GOT RIGHT ANYWAY. Its reasoning was that the writer is handed a
           paragraph and asked to shrink it, and shrinking is the step that gets skipped. That is
           still the correct description of the risk; the format change simply made the instruction
           specific enough to survive. The prediction's proposed failure reading ("if the fix is to
           loosen the one-line rule rather than create the missing project") does not apply, because
           the rule was not loosened and was not needed.
Evidence:   /tmp/agenda-fmt/2026-08-22.md lines 19-23, 33-37, 47-51. Longest Context at line 20.
            Run wf_fe55300a-565. Project-path non-existence checked against the working tree.
Follow-up:  #953 stays open on its own merits. It is not blocking this scenario, and this run shows
            why: the one-line rule holds without the project link, so the link is about depth having
            somewhere to go, not about keeping the agenda short.

19. The handoff prompt is four labelled lines โ€‹

  • Goal: an agent with none of this context can act on the prompt cold.
  • Preconditions: a completed run that wrote a file.
  • Steps:
    1. Extract every fenced prompt block from Focus Tasks and Session Prompts.
    2. Check each for the four labels, in order: Goal, Start here, Done when, Watch out.
    3. Read the VALUES, not only the labels.
  • Expected: all four present, in order, in every block. Watch out says nothing rather than sitting empty.
  • Verify, and this is what separates useful from merely well-formed:
    • Start here is a real path, doc or issue link. A description of where to look is a FAIL, and a path that does not exist on disk is a worse one.
    • Done when is a check the receiving agent can run or observe. "It works", "the task is complete", or anything only the operator could judge is a FAIL.
    • Goal is an outcome. "Look at X" or "investigate Y" is an activity, and a FAIL.
Result:    [ ] pass   [ ] fail   [x] blocked
Actual:    BLOCKED. No file was written, so there are no fenced prompt blocks to extract. See
            scenario 6.

            Zero prompt blocks found, therefore zero checked for the four labels and zero read for
            their values. Both halves of this scenario are unexercised: the easy shape check and the
            harder value check the scenario says is what separates useful from merely well-formed.

            What is on the record about the INSTRUCTION, which is not the same as the output: the
            Write prompt does carry the requirement, at agenda-creation.js:327, naming all four
            labels in order and both value rules (Start here a real path or link, Done when a check
            the receiving agent can run). So the instruction exists and was never delivered, because
            the argument carrying it collapsed to a boolean three lines later.

            One thing the prediction should be held to when this is re-run: the prediction says PASS
            on shape and locates the risk in the VALUES. Note that `Start here` pointing at a real
            path is checkable against disk, and this run gathered nothing that would let a writer cite
            a project path, because no gathered or ranked row carries one (see scenario 18). So the
            likely `Start here` values on a re-run are issue links and launch-plan paths, and those
            should be resolved rather than eyeballed.
Evidence:   No agenda file. Run wf_f80b57fb-b43.
Follow-up:  Same as scenario 6. When re-run, resolve every `Start here` value against the filesystem
            or GitHub rather than accepting that it looks like a path.

Third result, re-run of 2026-08-22 after the backtick fix, against fbfba81d, run wf_fe55300a-565. Both blocks above are left verbatim.

Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    PASS on shape AND on values, including the three value clauses this scenario says are what
            separate useful from merely well-formed. The prediction expected shape to pass and located
            the risk in the values; the values held.

            THREE fenced blocks, one per session. All three carry exactly four labels, in order:
            Goal, Start here, Done when, Watch out. None empty, none reordered, none with a fifth.

            `Start here` is a real path or link in all three, and every one was RESOLVED rather than
            eyeballed:
              s1  gh issue view 819, then services/api/auth/            819 OPEN, path EXISTS
              s2  gh issue view 855, then tooling/scripts/lantern.mjs
                  and tooling/vscode-extension/src/config.js (PORT_MAP) 855 OPEN, both paths EXIST
              s3  gh issue view 857 (CLOSED, read what actually landed),
                  then gh issue view 858, then tooling/scripts/doctor-env.mjs
                                                                        857 CLOSED, 858 OPEN, path EXISTS
            Five distinct repo paths cited, five exist. Four issue numbers cited, four exist, and
            #857's CLOSED state is stated correctly IN the prompt rather than being a stale claim.

            `Done when` is a check the receiving agent can run or observe in all three. No "it works",
            no "the task is complete", nothing only the operator could judge. s3's is a literal
            command plus a test file: "./lantern doctor prints all three on this machine, and
            tooling/scripts/__tests__/doctor-env.test.js passes" (that test file exists). s1 and s2
            are observable end states with the surface named.

            `Goal` is an outcome in all three, not an activity. "A local auth-api mints a custom token
            and both the admin and merchant portals accept a login against a local stack." No "look
            at" or "investigate".

            `Watch out` is populated in all three and carries a real trap rather than filler. s1 names
            the exact missing IAM permission and says it is a credential-path change that a retry will
            not fix. s2 names the 8080 collision between the Firestore emulator and venues-api and
            says to read PORT_MAP rather than invent a port. s3 warns the plan row is stale against a
            closed issue, so establish what shipped before rebuilding it. None needed the literal word
            "nothing", so that fallback is UNEXERCISED and stays unproven.
Evidence:   /tmp/agenda-fmt/2026-08-22.md lines 25-30, 39-44, 53-58. Paths checked against the working
            tree, issues checked with `gh issue view`. Run wf_fe55300a-565.
Follow-up:  The `Watch out: nothing` fallback has never been exercised. It fires only on a task with
            no trap, so it needs a thinner candidate set to test.

20. Realtime log entries carry a bold capitalized title โ€‹

  • Goal: the log is skimmable, which is the whole reason the title exists.
  • Preconditions: a completed run, from either workflow.
  • Steps:
    1. Extract every line under Realtime Log and Activity Blockers.
    2. Check the shape of each.
  • Expected: each reads - [ YYYY-MM-DD HH:MM <ZONE> ]: **TITLE** > detail, the title capitalized and short, the detail one line. <ZONE> is whatever %Z returns, PDT for most of the year. This line said PST until the run that found the same bug in the skill, so a file saying PST in August is a FAIL and not a match.
  • Verify the clock, because this has been wrong before: TZ=America/Los_Angeles date should appear in the writer's tool calls in the run journal. This machine runs UTC and a previous run stamped UTC times as Pacific, so a plausible-looking Pacific time in the file is not evidence that the clock was read.
  • Verify on a status run: agenda-status APPENDS. An entry that rewrites or reorders an existing line is a FAIL even if its shape is perfect.
Result:    [ ] pass   [ ] fail   [x] blocked
Actual:    BLOCKED on both arms, the creation arm and the status arm.

            CREATION ARM: no file was written, so there are no Realtime Log lines to shape-check. See
            scenario 6. Zero lines extracted.

            THE CLOCK CHECK IS ALSO BLOCKED, and this one is worth naming rather than skipping,
            because the scenario asks for it explicitly and for a reason. The verify says
            `TZ=America/Los_Angeles date` should appear in the writer's tool calls. The write agent
            made ZERO tool calls, so the clock was neither read nor faked. That is not a pass. A
            previous run stamped UTC as Pacific, and this run produced no evidence either way.

            STATUS ARM: unexercised. The append-versus-rewrite clause needs an `agenda-status` run
            against an existing agenda, and no agenda exists to append to, so it could not be reached
            even if this run's scope had included it.

            The cause is the sharpest detail in this whole pass and belongs on THIS scenario's record
            specifically: the workflow line that breaks the writer is the line that specifies the
            Realtime Log format. agenda-creation.js:330 writes the phrase "the detail after a `>`"
            with backticks around the `>`, inside the backtick template that IS the Write prompt. The
            first backtick ends the template early. So the sentence teaching the log format is the
            sentence that prevents the log from ever being written.
Evidence:   No agenda file. Run wf_f80b57fb-b43, write agent 0 tool calls, 8.0s, prompt `true`.
            `.agents/workflows/agenda-creation.js:330`.
Follow-up:  Same as scenario 6. On re-run, check the clock in the journal's tool calls BEFORE
            accepting a plausible Pacific timestamp in the file, exactly as this scenario warns.

Third result, re-run of 2026-08-22 after the backtick fix, against fbfba81d, run wf_fe55300a-565. Both blocks above are left verbatim.

Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    PASS on the creation arm, including the clock check. The status arm stays BLOCKED and is
            reported separately below rather than folded into the verdict.

            ONE log entry, at line 14, and its shape is exactly the specified one:

              - [ 2026-08-22 16:08 PST ]: **AGENDA BUILT** > 3 sessions ranked from the Prototype
                Phase 1 rows and the Aug 21 carry, 4 parked, 7 cut, 1 gap; no git, branch, worktree or
                PR state was inspected, and there was no 2026-08-22 carry file, so every carried item
                comes from August 21

            Timestamp in brackets, bold title, title capitalized and short, detail after the `>`, all
            on one physical line. The title says what HAPPENED. The detail carries the coverage caps,
            which is what the Write prompt asks that single line to do.

            THE CLOCK CHECK PASSES, and it was checked the way this scenario demands rather than by
            looking at the timestamp. The write agent's journal shows `TZ=America/Los_Angeles date
            '+%Y-%m-%d %H:%M'` as an EXECUTED tool call, not merely as text in its prompt, returning
            2026-08-22 16:08. Verified independently three minutes later at 16:11 Pacific. So the
            previous run's UTC-stamped-as-Pacific failure did not recur, and this is evidence rather
            than a plausible-looking string.

            ONE REAL DEFECT, and it belongs to the FORMAT rather than to the writer. The stamp reads
            `PST`. On 2026-08-22 the correct Pacific abbreviation is `PDT`: `TZ=America/Los_Angeles
            date '+%Z'` returns PDT, and the file says PST. The writer is not at fault, because the
            format skill hardcodes "PST" in its own template, in the day title, the log lines and the
            question log. The agenda will be wrong for roughly eight months of every year. Not scored
            against this scenario, whose Expected line also says PST, but it is a defect in both.

            STATUS ARM: BLOCKED, unexercised. The append-versus-rewrite clause needs an
            `agenda-status` run against this file, which is out of this pass's scope. The creation arm
            wrote a single log line, so there is now something for that arm to append to.
Evidence:   /tmp/agenda-fmt/2026-08-22.md line 14. Clock tool call in
            .../workflows/wf_fe55300a-565/agent-*.jsonl. Run wf_fe55300a-565.
Follow-up:  Fix PST to a real timezone abbreviation in .agents/skills/agenda-documentation/skill.md,
            and in this scenario's own Expected line. Either read %Z from the same date call that is
            already being made, or say "Pacific". Then run the status arm.

21. Focus blocks are checkboxes, and the emojis are the fixed set โ€‹

  • Goal: the structural markers are the ones the format names, not ones the writer picked.
  • Preconditions: a completed run that wrote a file.
  • Steps:
    1. Extract Key Focus Blocks.
    2. Extract every ## heading.
    3. Extract every Status: line, on the day header and on each session block.
  • Expected: every focus block starts - [ ] or - [x]. Each heading carries exactly one emoji from the table in the agenda-documentation skill. Each Status carries one of the five status emojis.
  • Verify: an emoji from outside the table is a FAIL even when it is a good choice. The point is not that the emoji is apt, it is that two sessions writing the same file agree.
  • Report the two failures separately: a heading with NO emoji means the table was skipped; a heading with a WRONG emoji means it was improvised. Different causes, different fixes, so do not merge them into one count.
  • Falsifying clause: on a CREATION run every focus block should be unticked. A ticked box on a fresh agenda means the writer inferred completion, which is precisely what the checkbox exists to make visible. A run that comes back with ticks is a fail of the thing, not a formatting nit.
Result:    [ ] pass   [ ] fail   [x] blocked
Actual:    BLOCKED. No file was written, so there are no focus blocks, no headings and no Status
            lines to inspect. See scenario 6.

            Every clause is unexercised, and the scenario's instruction to report the two emoji
            failures separately has nothing to count: zero headings with no emoji and zero headings
            with a wrong emoji, out of zero headings.

            THE FALSIFYING CLAUSE IS ALSO UNEXERCISED, and it is the one this scenario cares most
            about: a ticked checkbox on a fresh agenda would mean the writer inferred completion. No
            checkbox was written, ticked or otherwise, so that remains untested.

            On the emoji question specifically, the ONE thing this run can say is about the
            instruction rather than the output. The Write prompt does not enumerate the emoji table
            inline. It points at the format file (agenda-creation.js:311, "READ ... FIRST and follow
            its Agenda Creation state exactly", naming the emoji vocabulary as one of the four things
            not to write from memory), and separately requires Status to use "the emoji vocabulary
            from the format file, not free text" (line 328). So the design is a pointer, not a copy,
            which is the right call for drift and which means the run turns entirely on whether the
            writer actually opens the file. This run's writer opened nothing, so that question is
            exactly as open as it was before.

            The reference set this was to be measured against, from
            .agents/skills/agenda-documentation/skill.md: headings take one each of the nine in its
            table, and Status takes one of the five state emojis. Recorded here so a re-run measures
            against the same list.
Evidence:   No agenda file. Run wf_f80b57fb-b43. Format table at
            `.agents/skills/agenda-documentation/skill.md:26-46`.
Follow-up:  Same as scenario 6.

Fourth result, run wf_af98c56f-14e, after the format fixes landed (253b66a1 plus the example-date correction). The three blocks above are left verbatim.

Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    PASS, and the PST defect recorded in the block above is FIXED and verified in output.

           The log line at line 14 keeps its shape, and the stamp now reads PDT:
             - [ 2026-08-22 16:26 PDT ]: **AGENDA BUILT** > 3 ranked, 4 parked, 8 cut, 1 gap, ...

           THIS IS A STRONGER RESULT THAN A BARE "IT SAYS PDT", and the difference is worth stating,
           because it was named in advance rather than after. The writer's INPUT contained NINE
           strings stamping an August date PST: three fixture day-titles and six fixture log lines
           (verified: 2026-08-19, -20 and -21 all return PDT from `date '+%Z'`). The rule was written
           down before the run: PDT means the skill beat contradicting input; PST would have been
           ambiguous, because it could be a hardcode OR mirroring yesterday's file, and the fixtures
           would have had to be fixed before this scenario meant anything.

           It came back PDT, in all four places it appears: the day title and three log or question
           entries. So the skill beat nine lines of input telling it otherwise. Same shape as the
           old-field-names result in scenario 8.

           The clock was read with the zone, not just the time: `TZ=America/Los_Angeles date
           '+%Y-%m-%d %H:%M %Z'` appears as an EXECUTED tool call in the journal, twice. Cross-checked
           independently one minute later at 16:27 PDT.

           STATUS ARM: still BLOCKED, still unexercised. Unchanged, and it needs an `agenda-status`
           run against an existing file.
Evidence:  /tmp/agenda-fmt3/2026-08-22.md lines 1, 14, 64, 65. Clock tool call in
           .../workflows/wf_af98c56f-14e/agent-*.jsonl. Run wf_af98c56f-14e.
Follow-up: The FIXTURES still carry the defect: nine PST-in-August strings across the three files.
           They did not fool the writer this time, but they are wrong and they would make a future
           PST result unreadable. Fixed separately; see the fix section at the end of this document.

Third result, re-run of 2026-08-22 after the backtick fix, against fbfba81d, run wf_fe55300a-565. Both blocks above are left verbatim.

Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    PASS on checkboxes, on the emoji set, and on the falsifying clause. The two failure counts
            this scenario asks to be kept separate are both ZERO, and one edge is reported below that
            is neither.

            FOCUS BLOCKS: three, all checkboxes, all `- [ ]`.
              - [ ] 1615-1730: local admin and merchant login, mint a custom token off a local stack (#819)
              - [ ] 1730-1900: local service stacks, one command per preset with port-collision guards (#855)
              - [ ] 1900-2000: doctor learns more, establish what #857 actually shipped, then finish the rest (#858)

            FALSIFYING CLAUSE PASSES: zero ticked. On a creation run every box is unticked, and every
            box is unticked. The writer did not infer completion, which is the thing the checkbox
            exists to make visible.

            EMOJI, compared by CODEPOINT against the table in the format skill, not by eye:
              U+1F4C5 date         day title                          in table
              U+1F3AF direct hit   Key Focus Blocks                   in table
              U+1F550 clock 1:00   Realtime Log & Activity Blockers   in table
              U+1F9E9 jigsaw       Focus Tasks & Session Prompts      in table
              U+2753  question     User Question Log                  in table
              U+1F17F parking      Parked                             in table
              U+2B1C  white square Status, four occurrences           in the status table
            Seven distinct emoji, all seven in the skill's tables, at the exact codepoints. NOTHING
            from outside the tables appears anywhere in the file. Every Status line, the day line and
            all three session lines, carries U+2B1C "not started", which is right for a fresh agenda.

            THE TWO FAILURE COUNTS, kept separate as the scenario requires:
              headings with NO emoji, meaning the table was skipped:   0
              headings with a WRONG emoji, meaning improvisation:      0

            THE EDGE, which is neither of those and is a gap in the FORMAT rather than in the writer.
            Five `###` headings carry no emoji: the three Session blocks, "Cut today" and "Gaps". The
            three Session headings are correct, because the skill's own template writes them as
            `### Session 1: [session title]` with no emoji. "Cut today" and "Gaps" are different: they
            are sections the WORKFLOW prompt asks for (agenda-creation.js:332-333) that the format
            skill does not define at all, so they appear in neither the section list nor the emoji
            table. The writer had nothing to read off and correctly did not invent one. That is the
            right behaviour under "nothing outside the table", and it means the workflow and the
            format skill currently disagree about how many sections an agenda has.
Evidence:   /tmp/agenda-fmt/2026-08-22.md lines 1, 8-11, 13, 16, 60, 68, 79, 89, and Status lines 6,
            23, 37, 51. Codepoints extracted programmatically and compared against
            .agents/skills/agenda-documentation/skill.md:26-46. Run wf_fe55300a-565.
Follow-up:  Add "Cut today" and "Gaps" to the format skill's section list and emoji table, or drop
            them from the workflow prompt. Right now the executable and the format disagree, and the
            only reason it looks clean is that the writer declined to improvise.

Fourth result, run wf_af98c56f-14e, after the format fixes landed. The three blocks above are left verbatim.

Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    PASS, and the "Cut today / Gaps are undefined" edge recorded in the block above is FIXED
           and verified in output.

           Those two sections now come from the format rather than from improvisation:
             ## โœ‚๏ธ Cut Today    U+2702, matching the skill's new table row
             ## โš ๏ธ Gaps         U+26A0, matching the skill's new table row
           Previously they were `### Cut today` and `### Gaps`, h3 with no emoji, because the skill
           defined neither. They are now h2 with in-table emoji, so the executable and the format
           agree about how many sections an agenda has.

           EMOJI CENSUS, by codepoint, whole file: ten distinct, ALL in the skill's tables. Nothing
           off-table anywhere.
             headings with NO emoji, meaning the table was skipped:   0
             headings with a WRONG emoji, meaning improvisation:      0
           The three `### Session N` headings still carry none, which remains correct: the skill's
           own template writes them without one.

           FALSIFYING CLAUSE PASSES AGAIN: three focus blocks, all `- [ ]`, zero ticked.

           NEW AND WORTH RECORDING: the Status vocabulary was used with judgement rather than as a
           default. Three lines carry U+2B1C not started and ONE carries U+23F8 blocked, on session 3,
           the webhook fix, which genuinely is blocked on an unanswered question. The previous run had
           four identical `not started` lines. This is the first run where a status emoji other than
           the default appeared, and it appeared correctly.
Evidence:  /tmp/agenda-fmt3/2026-08-22.md lines 1, 8-11, 13, 16, 60, 67, 78, 93, and Status lines 6,
           23, 37, 51. Codepoints extracted programmatically. Run wf_af98c56f-14e.
Follow-up: None.

Notes โ€‹

Space for the tester's own observations, added scenarios, and anything discovered mid-run.

Run of 2026-08-22, scenarios 1, 2 and 6 through 12 โ€‹

Scope: exercise 1, 2 and 6 through 12. Leave 3, 4, 5 and 13 through 17 for a later pass (3 and 4 were subsequently filled by the plan's author and are marked author-run). Do not edit the workflow, do not fix what it gets wrong, do not publish anything.

WHICH VERSION THESE RESULTS DESCRIBE. Every run below was made against 13a5f102. Three of the findings were fixed the same afternoon, in e3c29691 and 99a3cfc4: the null-reader partitioning (in BOTH workflows, the same copy-pasted line sat under agenda-status too), the unconditional context:sync that made a scratch agendaDir not actually scratch, and the invented-rows failure, now given its own capped gaps channel that is forbidden in ranked. So scenarios 9, 10 and 11 describe behaviour that no longer exists in the tree. They are kept as recorded rather than rewritten, because the evidence is what justified the fixes, and a fixed finding quietly deleted reads to a later reader as a scenario that always passed. None of the three fixes has been verified by a run. That verification is the next pass.

FIRST PASS outcome (against 13a5f102): 3 pass, 1 fail, 3 void or unassessable, and the workflow had never written a file. The three findings below were fixed the same afternoon and the pair was re-run. See the re-run section at the end for the current state.

Outcome: 3 pass, 1 fail, 3 void or unassessable, and the workflow has still never written a file.

ScenarioVerdictThe one line that matters
1 focus requiredpassRefuses in 25ms, zero agents, names the arg and the reason
2 args-as-stringpassNames the cause, says no argument is actually missing
12 reader grepspass22 percent and 13 percent of the two big files, quantified unprompted
11 no invented rowsFAILThe entire ranked list, both arms, was mined from the launch plan
9 and 10 the pairVOIDArms read 74 and 12 candidates, so nothing can be concluded
6, 7, 8 the write pathblockedNo agenda file was produced by any run, anywhere

The four findings, in the order they matter.

  1. The ranked list is not drawn from the candidate list. In BOTH arms of the pair, independently, every ranked row was invented from ALPHA.md and no ranked row came from the 74 (or 12) carried candidates. The Check phase obeyed its identical constraint perfectly, returning exactly one annotation per candidate and inventing nothing, so this is a Rank-phase failure, not a Check-phase one, and scenario 11 as titled is aimed one phase too early. The rank agent announced what it was doing, labelling the rows "NEW GAP, named under Check A's one exception", and no such exception exists in its prompt. It constructed the permission and wrote it into its own output.

  2. A killed reader is reported as a healthy one. agent() returns null rather than throwing when an agent dies, and .then((v) => ({ reader, ok: true, ...v })) spreads null into nothing, so the object becomes {reader, ok: true} with no rows and never reaches the .catch. Run 10 returned readersOk: [repo-state, yesterday-board, in-flight] and readerFailures: [] while yesterday-board had in fact been refused outright. The comment above that code states the exact principle it fails to implement. This is what made finding 3 invisible, and it partially answers deferred scenario 17 in the negative.

  3. A scratch agendaDir does not isolate a run. Both the yesterday-board reader and the record agent run npm run context:sync against the real repo and context home unconditionally, regardless of where agendaDir points. The safety classifier blocked those steps once the operator set a leave-the-real-board-alone boundary, which is why the pair lost a reader and why no file was ever written. Until that call is conditional, this workflow cannot be exercised end to end without writing to her real board.

  4. The write path has never executed. Not once, across every attempt. Phase ORDER is confirmed (Rules, Backlog, Check, Rank, Record, in that order, both runs). Everything about the written FILE remains untested: the Focus line verbatim check, the format-skill comparison, the section 0 through 5 tripwire, the one-file-named-for-today check, and the context:sync push report.

What went right, and is worth keeping. The publish gate fixed earlier today behaved correctly on its failure arm in both runs: recorded: null produced callerMustPublish: false and a real DO NOT PUBLISH note. The cut channel fix landed and both arms returned their cut lists. The focus is echoed back intact through five phases. The focus text demonstrably reaches the cut reasons in both arms, so it is being applied somewhere, even though finding 1 means it cannot be reaching the ranked list by way of filtering candidates.

Run inventory. agenda-creation: wf_d1104e11-522 (scenario 1, refusal), wf_f1b91c9c-e29 (scenario 9), wf_ca348fa8-807 (scenario 10). agenda-status: wf_429f12b6-431 (scenario 2, refusal) plus two stopped misfires (wki6duaqb, wu3glddqo) that wrote nothing. The real-board run of scenario 6 was refused by the auto mode classifier and then withdrawn by the operator's choice.

Staging note, and a deliberate deviation from the plan. The plan says "do not re-copy between the two runs" and gives its reason: "if the inputs differ, the comparison means nothing." Read literally, run 9 writes today's file into the shared directory and run 10 then reads run 9's own output, which makes the inputs differ. Reason beating letter, a pristine master of the identical 11:02 PDT snapshot was staged at /tmp/agenda-falsify-pristine so the directory could be restored between arms. In the event no restore was needed, because run 9's Record was blocked and wrote nothing; /tmp/agenda-falsify was verified md5-identical to the master immediately before run 10 launched. The confound came from a lost reader, not from the staging.

Re-run of the pair, 2026-08-22 evening, against 7ca235e4 โ€‹

Scope: scenarios 9, 10 and 11 only, on the operator's word relayed as a scope decision. Two arms, runs wf_49d4fa52-069 (product-surfaces focus) and wf_474a8577-dec (agent-tooling focus), each against a pristine-restored copy of the 11:02 PDT agenda snapshot. Scenarios 6, 7 and 8 came along for free, because arm A wrote a file and those three had never had one to inspect.

Final tally across both passes: 9 pass, 1 fail, 7 blocked.

The three fixes, and whether they fired.

FixVerdictEvidence
Scratch runs suppress sync and pushFIRESBoth arms reported no sync and no push. Real context home verified untouched after each: clean status, still bdedf95, still three dated files
Uncarried rows go to gaps, never rankedFIRESZero ghosts in ranked, inProgress, parked and cut across both arms. Arm A's 2 uncarried rows sat in gaps within its cap of 2
A killed reader reports ok: falseUNEXERCISEDAll 8 agents returned in both arms. The guard never had a chance to fire, exactly as predicted, and it stays unproven

What the re-run settled. The invented-rows failure is genuinely fixed, and the way it was fixed is the transferable lesson: it was a ROUTING failure wearing the costume of a discipline failure. The rank agent had a real observation and nowhere legitimate to put it, so forbidding the behaviour did nothing and giving the output a correct destination fixed it immediately. The write path also works, and its output matches the current format skill rather than the format deleted that morning.

What the re-run did NOT settle, and this is what reopens. Both arms returned an EMPTY ranked list. Recorded plainly rather than read as a pass: a complete reversal of the focus produced an identical ranking, so this pair still does not demonstrate that the focus drives ranking. Two independent reasons, and both need fixing before a third run is paid for:

  1. The arms still did not read the same input. Both reported candidateCount 69, which was the gate, and they shared only 35 of 69 candidate TITLES. Jaccard 0.34. A fresh set of reader agents runs per arm and they do not return the same rows from byte-identical files. The gate counted; it did not compare. The candidate list has to be captured ONCE and replayed to both arms so the focus string is the only variable.
  2. ranked may be structurally unable to move. The rank prompt forbids ranking any row whose owner is recorded, and on this board almost everything has an owner, so every candidate routes to inProgress, operatorAsks, parked or cut and none is left for ranked. Both rank agents said the empty list was deliberate and gave that reason. Whether an always-empty ranked channel is correct is a design question about the routing rules, and the pair is measuring a channel that may not be able to vary.

What the focus demonstrably DOES reach, and it is real but is not what the pair measures: the cut reasons argue against the focus given, and the gaps channel is focus-shaped (2 product-surface rows under the product focus, 0 under the tooling focus). So "the focus is decorative" is NOT the finding. The finding is narrower and more awkward: the focus reaches the reasoning and does not reach the output the day is built from.

Timing. Arm A ran 36m21s, arm B 38m42s, against 25m20s for the earlier void run. The deviation is explained: the void run lost its most expensive reader to a classifier refusal, so it was doing less work. Both re-run arms used roughly 725k and 736k subagent tokens.

Run of 2026-08-22 evening, format scenarios, against 0739b0a6 โ€‹

Scope: scenarios 6, 7, 8, 11 and 18 through 21, against the committed fixture set rather than a copy of her real board. Run wf_f80b57fb-b43, agendaDir /tmp/agenda-fmt seeded from docs/projects/agent-workflow-hierarchy/fixtures/agenda/, focus "get the app, the admin portal and the merchant portal working end to end". 4 agents, 0 errors, 200,484 subagent tokens, 5m08s.

Tally for this run: 1 pass, 1 fail, 6 blocked, and all 6 blocked behind the same single character.

ScenarioVerdictThe one line that matters
11 no invented rowsPASS15 gathered, 16 rows across ranked, asks, parked and cut, 0 uncarried. The 1 uncarried row is in gaps, cap 2
6 writes today's agendaFAILZero files created. The Write agent's prompt was the boolean true
7 focus verbatimblockedNo header to read. The focus itself survived all three phases unchanged
8 matches the format skillblockedNo file to compare
18 one-line fieldsblockedNo Context line to measure. Prediction stays untested
19 four-line promptblockedNo prompt blocks
20 bold log titleblockedNo log lines, and the writer made zero tool calls, so the clock check is unexercised too
21 checkboxes and emojisblockedNo headings, no checkboxes

The finding, and it is one character โ€‹

.agents/workflows/agenda-creation.js:330 writes the phrase "the detail after a >" using BACKTICKS, inside the backtick template literal that IS the Write prompt. The first of those backticks closes the template early; what follows parses as a chain of > string comparisons; the argument to agent() evaluates to the boolean true. The Write agent is handed the literal text true, makes zero tool calls, spends 43,509 tokens saying "No task was provided", and returns.

Reproduced rather than inferred: lines 306 to 343 lifted verbatim, evaluated against stub bindings, prompt came back typeof "boolean". Committed at HEAD, not a local edit.

Two things this makes clear that a passing run would have hidden.

  1. The failure is silent by construction. The workflow returned a full, plausible, well-populated object: 4 ranked rows, 2 asks, 4 parked, 6 cut, 1 gap, all correct. readerFailures was empty and agents_error was 0. Only the recorded string says anything is wrong, and a caller who trusted the return shape would have reported a successful agenda build. The publish gate did fire correctly (callerMustPublish: false), but on the scratch-run branch, not on the write-failed branch, so it was right by coincidence rather than by detection.
  2. The line that breaks the writer is the line teaching the writer the log format. Scenario 20's own subject killed scenario 20. Worth keeping as the example of why prose that quotes syntax has to be escaped in a file that is itself executable.

What was measured anyway, without a file โ€‹

  • Scenario 11 passes on a real ranking, which the previous pass could not claim: that one passed with an EMPTY ranked in both arms, so it only showed nothing was invented. Here ranked held 4 rows, including one in-progress row carrying its status, and still nothing was invented.
  • Scenario 18's stated CAUSE is confirmed structurally. The RANKED schema sets additionalProperties: false and declares no project field on any channel, so no row can carry a Project link and the writer has nothing to fill that line from. The format skill's rule that a task needing more than one line has a project has no wire to travel on. That holds whatever the Context lines turn out to measure on a re-run.
  • The compression the writer would have faced: ranked detail fields of 277, 281, 372 and 282 characters, against a Verify bar of roughly 200 for Context.

Not fixed in this run, and why โ€‹

The fix is to escape or requote the backticks on line 330. It was NOT applied here: editing the script under test changes the artifact being measured mid-pass, and it is her branch with an open PR. Rule 18 says a broken tooling surface gets fixed in the session, so this is raised rather than filed, and it needs her word before the edit lands.

The real context home โ€‹

Untouched, and watched rather than only checked at the end. A monitor polled /home/mechelle/.agents/context/lantern_app every 20 seconds for the duration and was armed to fire on any dirty status or any HEAD move. It never fired.

Re-run after the backtick fix, 2026-08-22, against fbfba81d โ€‹

Scope: the eight scenarios above, re-run once the Write phase could execute. Run wf_fe55300a-565, identical args and identical focus to wf_f80b57fb-b43, against a freshly restored /tmp/agenda-fmt. 4 agents, 0 errors, 223,179 subagent tokens, 7m09s. The fix (fbfba81d) was authored by a parallel session and VERIFIED in this tree before the run: the Write prompt now evaluates to typeof "string", and sync-workflows.js --check reports 0 errors.

Tally for this run: 8 pass, 0 fail, 0 blocked. Combined with the failed run above, every scenario 6 through 12 and 18 through 21 now has a recorded result.

ScenarioVerdictThe one line that matters
6 writes today's agendapassOne file, 8,022 bytes, clock genuinely read, fixtures byte-identical after
7 focus verbatimpassCharacter for character, and the cut reasons argue against the focus, so it drove the output
8 matches the format skillpassAll three explicit checks plus the tripwire. Yesterday's OLD-format fixture was in the input and did not leak
11 no invented rowspass17 rows across four channels, 0 uncarried. The 1 gap is within its cap
18 one-line fieldspass, prediction wrongLongest Context 141 chars. The writer compressed 341 to 141, not pasted through
19 four-line promptpass5 of 5 cited paths exist, 4 of 4 issues exist, Done when runnable in all three
20 bold log titlepassShape correct and the clock tool call actually executed
21 checkboxes and emojispass7 emoji, all in-table by codepoint. 0 ticked boxes

The prediction that was wrong, and it is the useful one โ€‹

Scenario 18 was the bet-against, predicted to FAIL on the grounds that the writer is handed a paragraph and asked to shrink it, and shrinking is the step that gets skipped. It did not get skipped. The writer was handed detail fields of 341, 219 and 268 characters and returned Context lines of 141, 114 and 122. Every field on every block is one line. The whole file is 8,022 bytes against 80,276 for the same job before the format change.

The prediction's reasoning was sound and the outcome contradicts it, which is worth more than a confirmation would have been: the risk was real and the format change is what closed it.

The prediction also named a failure reading in advance, "if 18 fails and the fix is to loosen the one-line rule rather than to create the missing project, then the rule was aimed at the wrong thing". That reading does not apply. The rule held WITHOUT the project link existing, which separates the two concerns cleanly (see below).

Three format defects this run surfaced, none of them writer error โ€‹

  1. PST is hardcoded and August is PDT. The skill's template writes PST into the day title, the log lines and the question log. TZ=America/Los_Angeles date '+%Z' returns PDT today. The agenda will carry a wrong timezone for roughly eight months a year. Scenario 20's own Expected line has the same bug.
  2. "Cut today" and "Gaps" exist in the workflow and not in the format. agenda-creation.js:332-333 asks for both; the skill defines neither, in its section list or its emoji table. The writer correctly declined to invent emoji for them, so it looks clean, but the executable and the format disagree about how many sections an agenda has.
  3. An ask on a parked or cut task has no home. The skill says an ask lives on its task block and that line is the only place one lives; it also says every question is logged without exception. All three of today's asks attach to parked or cut tasks, so no block could carry them, and the writer routed them to the User Question Log. Reasonable, and undefined.

Plus the separately filed #953: no channel in the RANKED schema can carry a project link, so all three Project: lines read "none yet ( run project-creation on pickup )". The writer handled it honestly, inventing no path (checked: none of the three plausible docs/projects/ slugs exists).

Why scenario 18's two causes were separable this run โ€‹

The scenario asks whether a long Context is the writer failing to compress or the ranker having nowhere to put a project link. Because the project field is currently unreachable anywhere in the pipeline, a long Context provably could NOT have been the missing link surfacing as prose. So the run measured exactly one thing, and it passed. The missing link is a real gap that this run simply was not blocked by, which is why #953 stays open on its own merits rather than as a fix for scenario 18.

The real context home โ€‹

Untouched across both runs. Watched every 20 seconds by a monitor armed to fire on any dirty status or HEAD move, which never fired. Verified after: git status --porcelain empty, HEAD still bdedf95ad3b510cffdf4ecc51f7dfd22ba765a24, agenda/ still holding its original three files plus agenda.html at their pre-session mtimes.

Format-defect fixes and their live verification, 2026-08-22 โ€‹

The three format defects surfaced by run wf_fe55300a-565 were fixed and then VERIFIED IN OUTPUT rather than by inspection, because a format skill is a runtime surface and a static check cannot say whether the writer obeys it. Verification run: wf_af98c56f-14e, fresh /tmp/agenda-fmt3, identical args and focus. 4 agents, 0 errors, 232,212 subagent tokens, 6m58s.

DefectFixVerified in output
PST hardcoded, wrong for eight months a year253b66a1 added a "How is the timezone written?" section requiring %Z be read live. Its examples still stamped January 1 as PDT, corrected separatelyPDT in all four places, against NINE PST-in-August strings in the input
Cut Today and Gaps existed only in the executable253b66a1 gave both a table row (โœ‚๏ธ U+2702, โš ๏ธ U+26A0), a template heading and a definition; the workflow prompt now points at the skillBoth emitted as ## with the in-table emoji, where the previous run wrote bare ###
An ask on a parked or cut task had no home253b66a1 added the rule at skill line 112: it goes in the User Question Log, naming the row it belongs toBoth routings exercised in ONE run, see below

The fix to the fix โ€‹

253b66a1 did a blanket PST to PDT swap and did not touch the dates, so all 15 examples across the three template blocks read 2026-01-01 and January 1, 2026 stamped PDT. January 1 is PST. The section teaching "never hardcode the abbreviation" was demonstrating a wrong date-to-zone pairing in every one of its own examples.

The commit's own explanatory sentence reads "the examples below say PDT because they are August dates", which makes the intent unambiguous, so the dates were moved to match the sentence rather than the abbreviations reverted: 2026-01-01 becomes 2026-08-03, January 1, 2026 becomes August 3, 2026. 15 insertions, 15 deletions, nothing else touched. Synced to all three generated copies; lint.sync-skills reports in sync and lint:emdash exits 0.

This is the third instance today of one shape, and it is worth naming as a class rather than three bugs: a document that quotes the thing it teaches has two things that must agree, and nothing checks that they do. The backtick bug (prose quoting a > inside an executable template), the fixture folder eating its own run output, and now a timezone rule whose examples contradict it.

Why the PDT result is strong rather than merely green โ€‹

Stated before the run so it could not be fitted afterwards: the fixtures carry nine strings stamping an August date PST, three day-titles and six log lines, and they are the writer's input. A PDT output means the skill beat contradicting input. A PST output would have been UNREADABLE, because hardcoding and mirroring yesterday's file produce the same string, and the fixtures would have needed fixing before this scenario meant anything.

It came back PDT.

Both ask routings exercised in one run, which was luck worth recording โ€‹

The rank agent placed the webhook fix in ranked this time (it was cut in the previous run), while the retention decision stayed parked. So a single run exercised both halves of the new rule:

  • The webhook ask sits on its own session block: - Needs from you: When a webhook event is dropped, should it be replayed from the delivery log, or accepted as lost? ...
  • The retention ask, whose task is parked and has no block, sits in the User Question Log naming the parked row it belongs to.
  • Sessions 1 and 2 read - Needs from you: none.

That is exactly the rule, on both branches, without needing a second run to reach the second branch.

One observation, not scored โ€‹

The Needs from you line carrying the webhook ask is 250 characters, the longest field on the page. Scenario 18's roughly-200 bar is written for Context specifically, so this is not a fail by the letter. It is also arguably correct: an ask has to be answerable cold. Noted in case the one-line rule is ever tightened to cover every field, because this is the field that would fail it first.

Context lines this run: 105, 117 and 136, all comfortably inside the bar, and the longest is shorter than the previous run's 141.

Built with VitePress