Skip to content

Implementation โ€‹

Execution notes and findings. What the building actually produced, including what it broke and what it revealed.

logging.md is the index of when things happened. This file is why they happened that way. Decisions and their reasoning live in design.md.


What did the PM skill refactor change? โ€‹

The operator's sketch was a routing table in pseudocode: two conditions, each naming a process. That shape was kept and widened to four branches with a shared preflight.

BranchWhat it does
rehydrate()Runs before every other branch. Sync the context home, read the recent agenda. Nothing below it can be trusted until it runs
startNewDayWorkflow()One paragraph delegating to agenda-creation. It was previously that skill restated in full
updateProjectStatusProcess()New. Seven steps. Two of them, reconciling project logs and close-and-notify, existed nowhere in the repo
dispatchTaskProcess()Split out, because handing work to a session happens all day, and a 2pm handoff is where the collision rules matter most
endOfDayWorkflow()The durability sweep, the EOD report, the digest

Two other things came out of it:

  • The communication section was cut roughly in half. The digest shape, the readability rules and the per-agent blocks had already landed in communication-voice, so the skill was carrying a stale second copy of a rule that had moved.
  • The live skill pointed at sections that no longer exist: "day-plan step 7" and "sections 4 and 5". day-plan has Phases 0 through 6, and the agenda sections are being renamed by the new format.

What did the five-phase restructure change? (2026-08-23) โ€‹

The four-branch shape was a routing table of INDEPENDENT conditions. Nothing in it said which to test first, or that satisfying one ruled out the others, so new_day, status_check, task_ready and end_of_day could all be true at 2pm and the seat would act on all four. Her diagnosis on 2026-08-21 was exactly that: the PM behaves erratically because nothing tells it which phase of the day it is in, so it attempts all of them at once.

The fix is an ordered walk. Five phases, each with the condition that opens it and the condition that closes it, and a rule that you are in exactly one at a time.

What did each phase absorb? โ€‹

PhaseWhat it absorbedWhy it needed its own name
PlanstartNewDayWorkflow(), plus the two Plan-only reads pulled out of the preflightIt runs once a day and everything downstream depends on her approving its output
GrilldispatchTaskProcess() in full, plus agenda-creation's grilling and project-creation stepsThe detail, the project folder and the prompt are one job, and all three need her in the room
QuietNew. CI, merge sequencing, the review-stage pairing, misdispatch recovery, and the do-not-push-status ruleThe old shape had no name for the middle of the day, which is why the seat kept filling it with a sweep nobody asked for
SyncupdateProjectStatusProcess() steps 1 through 6Its two triggers were buried inside it as prose; as an entry condition they are checkable
CloseendOfDayWorkflow() unchanged, plus a preconditionIts report was only as true as a board that nothing required to be swept first

Which steps were living in two places? โ€‹

  • The pre-Close sweep. It was trigger 2 of the status process AND an unstated precondition of the EOD report. It is now Close's entry condition, which is the one dependency the old shape could not express: a if (end_of_day) branch fires whether or not status_check ever ran.
  • Project creation. Described as agenda-creation's output in the new-day section and as a dispatch precondition in the dispatch section. Both are now Grill steps, ordered, because the prompt cannot name a project file that does not exist yet.
  • "Commit and push the context home." Preflight item 4 and the last line of the status write-back. Now one write-through rule that holds in every phase.
  • The other-worktree boundary. Stated only inside the status sweep, while the EOD durability sweep walks every worktree and never mentioned it.

Which steps were merely in the wrong place? โ€‹

  • Yesterday's Parked rows, and the rebuild-from-repo-state fallback. Both sat in rehydrate(), which runs before every branch, so every phase entry paid for a read that only Plan can act on.
  • CI babysitting. Step 8 of the status process, though CI moves all day and the sweep runs at most twice.
  • The review-stage pairing. Filed under dispatch, though it fires while work is out.

Which two rules contradicted each other? โ€‹

"Escalating to the operator is the first move, not the last" (inside the status sweep) and "Blockers escalate to the PM, not the operator" (in the operator section) both answered the same question, when to interrupt her, from two sections that never referenced each other. There is now one interrupt rule, in the every-phase section, and the silent-peer case is named under it as the one to escalate early.

What is deliberately NOT a phase? โ€‹

  • rehydrate(). It is the preflight, and it is the only thing that repeats inside every phase entry. Calling it a phase would make "one phase at a time" false on its face.
  • The What holds in every phase? section. Board write-through, the question log, the interrupt rule, the worktree boundary, verify-before-relay, and the authorization check. Nothing there may be claimed by a phase, which is what makes "no step appears in two phases" a checkable property rather than an assertion.

What was deliberately left alone? โ€‹

  • It stays a skill. The failure mode here is bad judgment at a step, not a skipped step, so the ordering is a table plus entry conditions and not a workflow script. (Operator constraint, 2026-08-23.)
  • The word "board" was carried forward at first, then swept on her word. She retired it on 2026-08-22 ("what is a board? We are using an agenda"), agenda-creation was stripped that evening, and project-manager was not. A vocabulary rename is a structural call, so the 18 uses were left in place and surfaced as a question rather than swept in under cover of a phase change. She answered the same morning ("can we make sure we don't have any trailing board mentions?") and all 12 sites became "agenda". The only remaining match is the context-onboard skill name.

What did the status sweep's first run find? โ€‹

Four phases, seven agents, six read-only. Modelled on pm-morning-survey.js deliberately, so the two read as one system.

Run of 2026-08-22: 12m 10s, 403,929 subagent tokens, 92 tool uses, six of seven agents returned.

Five defects, four of them in the script.

Defect 1: polling was a no-op that cost 4.5 minutes โ€‹

  • A workflow subagent can SEND a message. Every SendMessage returned success: true.
  • It cannot RECEIVE one. From inside a workflow the only addressable peer is the parent, so every reply lands in the parent session's inbox.
  • The observer messaged seven peers. All seven answered promptly. It saw none of them, blocked from 09:03 to 09:07, and returned "No replies arrived within the run."
  • It then classified every lane INCONCLUSIVE for silence, correctly by its own rule, and reasoned at length about ambient silence that did not exist, proposing a longer reply window as the remedy.

That last point is the real damage. A correct classification reached through a wrong diagnosis produces a remedy that cannot work, and it is the stillUnknown channel being handed a transport bug and dressing it as a finding, which is the precise failure that channel exists to prevent.

  • Fix: polling moved to the caller, which can receive replies. The PM session collects the self-reports and passes them in as sessionReports.
  • Bonus: because parallel() is a barrier, the reply window blocked the whole phase. Three observers finished at 09:04:59, 09:05:21 and 09:06:38, then sat idle roughly three minutes. Fixing the transport removes most of the wall clock, not just the blind spot.

Defect 2: callerMustPublish was a hardcoded literal โ€‹

  • The Record phase died on a transient safety-classifier error and returned null.
  • The page was never regenerated, so agenda.html on disk was four hours old.
  • The return still said callerMustPublish: true, pointing at that file.
  • Obeying it would have published a stale page to the operator's bookmarked standing URL as if it were a fresh sweep. The test session did not publish.
  • Fix: gated on Boolean(recorded), with a publishNote that says DO NOT PUBLISH and why when Record did not return.

Defect 3: dispatch fired with nobody to dispatch to โ€‹

  • callerMustDispatch came back true on two log gaps whose owner was NOT STATED in both cases.
  • The board had no tasks, therefore no owners.
  • Correct per the expression, useless in practice.
  • Fix: it is an array now, filtered to gaps with a real owner, matching callerMustNotify.

Defect 4: a boardless day produced a fake reconcile โ€‹

  • There was no agenda file for 2026-08-22, so Claimed correctly returned NONE FOR TODAY.
  • The sweep then ran four observers and a full reconcile against claims that did not exist.
  • It produced four statusChanges whose claimedStatus was "ABSENT FROM BOARD", four times out of four.
  • The output was a genuinely good repo audit. The return shape presented it as a comparison.
  • The reconcile even named it: "Every delta above is therefore a comparison against silence, not against a claim."
  • Fix: early return with verdict: NO_BOARD_TO_RECONCILE and the observations labelled as an audit.

Defect 5: the Record self-skip is still untested โ€‹

  • The branch logic is correct and the prompt carried the instruction.
  • Record crashed before reaching it.
  • Verified independently rather than trusting the return: no 2026-08-22 file anywhere under the context home, agenda directory mtimes still 05:36 (predating the 09:02 run), context home's newest commit at 08:08 PDT, before the run.
  • The clean result was the crash, not the discipline.

What did the second run find? โ€‹

Run of 2026-08-22, wf_fd08afe9-c41. 4m14s, down from 12m10s. 274k tokens, 53 tool calls, 5 of 5 agents returned.

What the fixes proved โ€‹

  • The polling fix works, and it is the clearest win. sessionReportsSupplied: 7. The observer produced a per-session finding for each, every one citing the git command behind it rather than restating the claim: it mapped one session's five commits onto its claimed deliverables via git show --stat, found another's ORIGINS.md entry verbatim inside ab8b8b3c, and used git ls-tree to prove a third's file exists at the claimed ref.
  • It named what it could not check. Every commit is authored cattreedev, so per-session authorship is not observable from git at all. Any "none of these are mine" rests on inference from file content, and the coverage note says so instead of papering over it.
  • The planted trap was caught. This session claimed 9 commits on the branch, none of them its own. The reader verified the count and the timestamp, marked it AGREES, and separately reported the checkout as LIVE RIGHT NOW. The branch was never recorded as cold.
  • It found a disagreement nobody planted. The claim said 9 uncommitted paths; git showed 10. The extra was implementation.md, created at 09:43:13, after the claim was written. Marked OBSERVED, with the read that the session should be treated as actively working rather than finished.
  • verdict: NO_BOARD_TO_RECONCILE worked exactly as designed. No "ABSENT FROM BOARD" rows anywhere.

Defect 6: the args payload arrived as a string โ€‹

  • The first launch died in 19ms with zero agents: repoRoot is required...
  • Cause: a roughly 9KB args payload carrying seven verbose session reports arrived as a string rather than an object, so every args?.x read as undefined.
  • Probed by the tester: small objects and nested-array objects both arrive as objects. Payload size is the trigger.
  • The guard fired correctly and its message pointed away from the cause. It told the caller to pass repoRoot through args, which is exactly what the caller had done.
  • This script's entire input design is verbose session reports, so it will meet this again.
  • Fix: a typeof args === 'string' check ahead of required(), with a message that names the real cause. Added to all three live workflows too, since preflight-prompt-check takes a whole prompt and test-rank takes candidate lists, so both are at genuine risk.

Defect 7: one fix shadowed another โ€‹

  • The NONE FOR TODAY early return was placed between Observed and Reconcile.
  • So on a no-agenda day the script returns before Record, and Record's "there is no file for today, write nothing" branch became unreachable in exactly the scenario it was written for.
  • Nothing was written, which is the correct outcome, reached by the wrong path. Verified independently: the agenda directory still holds only 08-19, 08-20 and 08-21, and agenda.html still has its 05:36 mtime.
  • Fix: the board check moved to immediately after Claimed, before the observer fan-out, and Record's dead branch was deleted.

Defect 8: the early return omitted keys the main path promises โ€‹

  • publishNote was absent entirely, count zero in the raw output. So a caller read the absence as "no instruction" rather than as "do not publish".
  • callerMustDispatch and callerMustNotify were also absent, giving undefined where the main path returns empty arrays.
  • The callerMustPublish: false that came back was the early return's hardcoded literal, not the Boolean(recorded) gate added after run 1. That gate is still untested.
  • Fix: the early return now returns the full shape.

Defect 9: three readers did work that was thrown away โ€‹

  • On a no-board day, merges ran a 24-hour scan plus four gh pr view calls plus a repo-wide changelog grep. project-logs burned 46k tokens to report that it had been handed an empty task list and could therefore answer none of its three questions.
  • The early return sat after all of it. 86% of the run was spent on a day that had nothing to compare.
  • Fix: same as defect 7. Checking the board first is what makes the cheap day cheap.

Still unproven after two runs โ€‹

  • The Reconcile phase has never run. Both trap catches came from a reader prompt, not from the step built to adjudicate them. The reader is doing the reconcile's job, which means the reconcile is untested on the exact case it exists for.
  • Record has never executed. Run 1 crashed before it; run 2 returned before it.
  • readerFailures has never held a real throw. Both runs had every reader return, so the failure-partitioning branch is exercised only by its happy case.
  • The Boolean(recorded) publish gate has never been hit.

All four need a run on a day with an actual agenda file. That is not something to simulate.

An unowned question the fix created โ€‹

Polling moved to the caller, and the "how long do I wait for replies" question moved with it. One session replied 90 seconds after the first launch, so run 1 recorded NO REPLY for it and run 2 had its real report. A caller-side poll has no natural close signal, and nothing currently owns that timeout.

What did the run get right? โ€‹

Worth recording, because a defect list reads as if nothing worked.

  • Held three cold worktrees as INCONCLUSIVE rather than idle, which was the rule under test. It then went past the prompt and noticed their tmux sessions were created 2026-08-10 at 11:54, 11:55 and 12:04 and never attached, the same day those worktrees went cold.
  • Caught that PR #952's all-green box was hollow: 27 of 29 check rows skipped, with the "CI Actually Ran" log reading PR_IS_DRAFT: true and "nothing to verify".
  • Reasoned that cwd-based process mapping cannot see a session editing a worktree by absolute path, which turned out to be exactly what one session was doing.
  • Refused to fall back to yesterday's agenda when today's file was missing, on the grounds that carrying yesterday's claims forward would misrepresent them as current. Right call, well flagged, and now an explicit instruction in the prompt rather than something an agent has to re-derive.

What did the reconcile catch in a self-report? โ€‹

This session's own answer, and it was wrong.

  • Asked where its work stood, it said "git will show four uncommitted paths and zero commits today, which is accurate and not a stalled lane."
  • Four paths was right. Zero commits was not: nine commits had landed on docs/950-agent-workflow-hierarchy that day, newest at 07:58:34.
  • The charitable reading is "zero from me", which is true.

A report can be true about the SESSION and false about the BRANCH, and a reconcile taking that sentence at face value records a live branch as cold. The observer prompt now hunts for this shape specifically.

Not a disagreement despite looking like one: two other sessions reported "four commits" and "5 commits", which sum exactly to the branch's nine. Each was reporting its own share correctly.

What did it find outside this project? โ€‹

The changelog bot silently dropped entries โ€‹

PR #947 merged to dev with no changelog entry, and the mechanism is structural rather than a one-off.

  • concurrency: cancel-in-progress: false queues runs on the same ref rather than cancelling. That part was correct.
  • actions/checkout defaults to github.sha, the pinned merge commit that triggered the run. So a run that queued behind another merge checked out a tree predating the earlier run's changelog commit.
  • Both runs then appended an Update block to the same region of the same daily file.
  • The second push was rejected as non-fast-forward, and the retry was a bare git pull --rebase --autostash with no conflict handling.
  • Under bash -e the conflict exited the step, leaving the entry uncommitted and discarded.

Fixed in two parts:

  1. ref: ${{ github.ref_name }} on checkout. The concurrency group already serialises runs on the ref, so a queued run now starts from a tip containing the entry before it. This also makes a re-run work, which it previously could not: a re-run checked out the same pinned SHA and failed identically.
  2. The conflict path aborts the rebase and fails with an explicit message, instead of dying mid-loop.

Verified: the diff range is read from the event payload (BEFORE_SHA..AFTER_SHA), not from HEAD, so taking the branch tip does not change what gets summarised. YAML parses. actionlint is not installed locally; CI enforces it.

Not fixed: #947's own entry was not backfilled, and nobody knows how many were lost before 2026-08-21. The bot's commits carry the skip directive, so nothing ever re-reads the file. That is why this went unnoticed.

preflight-prompt-check.js documented an argument it required โ€‹

  • Line 11 said repoRoot?: string.
  • Line 29 called required(args?.repoRoot, ...).
  • One line, fixed.

What changed in the context home? โ€‹

Deleted, both recoverable from the context repo's history:

  • audits/, 2 files.
  • memory-curation/, 2 files, including the 2026-08-19 graduation ledger.
  • Both held finished output, and nothing in the repo read either one.

Not deleted, because both are coupled to code:

  • active/ is recreated by context-bootstrap.mjs on every bootstrap. Nothing reads it any more: the agenda-creation skill and test-rank.js both pointed at active/focus.md and both now take the focus as an argument. The bootstrap is the only remaining coupling.
  • boards/ is named by context-bootstrap.mjs, context-dated.mjs, and context-dated.test.js. Corrected 2026-08-23: this line also named the project-manager skill, and the skill has never contained the string boards/. Three namers, not four, all of them code.
  • Deleting either without the matching code change produces the exact silent-failure shape this project exists to remove.

The memory migration is already filed as #654 and was commented rather than duplicated. Two facts worth having there:

  • context:sync skips index regeneration on every single run while the symlink direction stands, and says so in its output.
  • The two machines have genuinely diverged: 51 notes on this laptop against the 60 the 08-19 ledger counted on the dev VM.

What did the four parallel sessions change in the .agents/ layer? (2026-08-23) โ€‹

Four sessions ran against four different assets and landed as one commit, 1d676630. Two of the four produced a mechanism worth more than a log line, and one produced a defect in a drift gate this project's own table lists as working.

Why the PR state check is computed in JavaScript rather than asked of the agent โ€‹

The boundary is verify, never discover, and prose could not hold it. The carry reader copies a claim out of yesterday's markdown; nothing checks that claim against the repo, which is how four merged PRs appeared on her agenda as unfinished work. A verification step is the obvious fix and it is also one sentence away from a backlog sweep, which is the single slowest thing the workflow could do and the thing she named as the drag.

  • The number list is built in the script, by regex over the carry reader's own returned items. The agent receives a fixed list and is told it is the whole job.
  • An agent asked to "check these numbers" can widen the set. An agent handed a computed array cannot, because it has no way to add to it and no tool to go looking.
  • The cap on that list is logged and stated in coverage when it bites. A silent cap reads downstream as full coverage, which is the same defect class as the check failing quietly.

A failed check reports UNKNOWN, and that is a separate decision from the boundary. The old shape of this failure is a check that does not return and a ranker that carries on repeating the carried claim, so the absence of a contradiction reads as a confirmation. The rank prompt now gets an explicit block saying the state was not verified, in either direction.

Why test-rank.js was the file that mattered โ€‹

A harness carrying its own copy of the rule under test is testing the copy. test-rank.js exists to catch a regression in how work is ranked. Its prompt held a private copy of the ranking rules, and that copy said a candidate whose owner is recorded is set aside and not ranked. The live rule is the opposite: work already under way belongs in ranked, carrying its status, because the list is meant to show the whole shape of her day.

  • So the harness would have scored the correct behaviour as a regression, and the regression as correct.
  • The fix is not a corrected copy. The prompt now names two paths, the agenda-creation skill and the launch plan README, and states that it deliberately holds no ranking rule of its own.
  • The same reasoning made candidates a required input. A baked-in set freezes a stage, a phase name and a priority on its write date and is scored against one particular focus, so a stale fixture and a real regression produce identical output.

The five known instances were fixed in 8cdbd3e0 at 10:09 by another session (curate-memory, self-improve, browser-test, autopilot, context-onboard), rewritten to repo-root-relative form. The root cause is untouched and it is the part that outlives today, so read what follows as the shape of the next instance, not as a live break.

Skill Sync checks that copies match their source, not that a copied link still resolves. A pointer written ../../../docs/..., correct from .agents/skills/<name>/skill.md, is copied verbatim by sync-skills.js into six destinations, two of which sit two directories deep rather than three.

DestinationDepthResolves
.agents/skills/<name>/skill.md3yes, the canonical source
.claude/skills/<name>/SKILL.md3yes
.github/skills/<name>/SKILL.md3yes
.gemini/skills/<name>/skill.md3yes
.github/prompts/<name>.prompt.md2no, resolves above the repo root
.gemini/commands/<name>.toml2no, resolves above the repo root
  • It was never one session's defect. Session 2's routing-test pointer was the instance that surfaced it, but four other skills already carried the same shape, and every one of them predates this project.
  • The damage is worst where the link replaced something. The whole point of that pointer was to delete a second copy of the routing test. For a Copilot prompt or a Gemini command, the pointer led nowhere and the copy was already gone.

The rule this breaks is already written down, which is what makes it a gate question rather than a docs question โ€‹

AGENTS.md already says to use repo-root-relative links, in exactly these words: "docs/engineering/guides/X.md, not ../../../docs/...", and gives the reason, "so they still resolve once injected into AGENTS.md at the repo root". Five skills violated it anyway, for however long, and the repo noticed only when someone hand-checked one link.

  • That is this project's own thesis, turned on the project. A rule whose failure mode is a writer not remembering it is a rule prose cannot hold. It is step 1 of the routing test in design.md: a machine can decide it, so it belongs in a gate.
  • Nothing enforces it today. lint.sync-skills.js checks orphans and source-versus-copy drift, and has no link check of any kind.
  • Two candidate fixes, and they are not exclusive. Reject a ../ link in a skill or rule source at lint time, which enforces the rule the repo already has. Or rewrite the link per destination during sync, in sync-skills.js, which already imports relative from path. The first is the smaller change and matches what AGENTS.md asks for.
  • Not built here. Another session is taking it to her as a backlog row citing this table; whether it becomes a gate is her call.

Built with VitePress