Skip to content

chore(tooling): post fewer screenshots and lint a deferred mounted-flag reset - #1399

Merged
mforce merged 12 commits into
mainfrom
chore/agent-tooling-retro
Nov 7, 2026
Merged

mforce merged 12 commits into
mainfrom
chore/agent-tooling-retro

Conversation

@mforce

@mforce mforce commented Nov 7, 2026

Copy link
Copy Markdown
Owner

Why

These are two fixes from the retro on the TOTP slice (#1277). UI PRs posted more screenshots than anyone reads: #1395 posted 28. And the Back-button race that #1395 fixed had no check to stop it coming back.

What changes

1. The verify skill asks for fewer frames. tools/verify/SKILL.md asked for 1280x800 and 390x844 in both light and dark. Now a PR posts one frame per distinct state at 1280 light. It adds a 390 or dark frame only where the layout or theme visibly differs. Within the before frames, and separately within the after frames, it skips frames that differ only in text. A changed existing screen always keeps its before and after pair, as AGENTS.md requires, even when only its text changed. The target is about 12 frames or fewer. The 1:1, viewport-only and rebuilt-at-head rules stay. capture.sh still captures all four frames so they can be compared, and the skill now says to post only the frames the rule keeps.

2. A lint for a flag cleared in a useEffect cleanup. In #1395, useDialogSession cleared mounted.current in a useEffect cleanup. React can run that cleanup a task after the commit that removes the screen, so a step-up grant that landed in between still sent its write. A no-restricted-syntax selector in web/lint/eslint.config.js now flags x.current = false inside a function that a useEffect returns. That covers a return () => … that belongs to the effect callback (a conditional return included, a return inside a helper defined in the effect excluded) and the () => () => … form. The message points to useLayoutEffect. It applies to all of web/src, and AGENTS.md's Enforced rules table has a new row for it.

It found one site on main: web/src/components/Dialog.tsx:321, the mount-once cleanup that pays back the open-dialog count. It gets an inline eslint-disable-next-line with its reason, as useLatestLoad.ts:36 does for the #1190 rule. reportUnusedDisableDirectives fails lint once that line stops violating. The frozen-suppressions file cannot hold this entry: check-suppressions.mjs --base keys by rule id, and no-restricted-syntax already applied to that file. Candidate for a later look: async code reads entered through anyDialogOpen(), so the same one-task delay could apply. No race has been shown, and this PR does not change product code.

Dropped

The retro also asked the agent git guard to stop refusing harmless commands whose quoted arguments mention a git write. This branch tried a data-only exemption for scripts and then dropped it. Three review rounds each found a bypass: command runners that ship with the toolchain (npm exec -c, pdb -c), a symlink plus .. that made the guard read a different file than Node ran, and a step that swaps the script before running it. tools/agent-guard/ is unchanged from main. The false blocks stay, and decision logging keeps its two-step workaround.

Proof

Each mutation broke the new code, and a committed check then ran against it. Every row went red.

Mutation Check Result
useDialogSession.ts reverted to useEffect npm run lint useDialogSession.ts 64:20 with the new message
The inline directive removed from Dialog.tsx npm run lint Dialog.tsx 321:7
The effect-ownership check dropped from the selector web/lint/cleanup-selector.test.mjs nestedFactory and immediateFactory fail

web/lint/cleanup-selector.test.mjs holds the selector's cases. It lints ten in-memory effects with the real config and compares each against an expected count. It expects a diagnostic for a block return, a conditional return, the concise form, a function expression and React.useEffect. It expects none for a timer callback, a layout effect, or a helper defined or invoked inside the effect. CI's Lint step runs it with node --test. It is not part of npm run lint, so npm run lint -- --prune-suppressions still passes its arguments to ESLint.

Runs at the head: npm run lint, npm run typecheck, check-suppressions.mjs --base and the selector test pass. The guard's own tests (git_guard_test.py, git_hooks_test.py, pi_extension_test.mjs) pass on the restored guard. The guards that walk every tracked file (the two image-pin tests, TenancyDocsFreshnessTests, FileSizeLimitTests) pass with the new test file tracked.

Review guide

  • web/lint/eslint.config.js: the two selectors and ownReturn. The second config block repeats them because its no-restricted-syntax replaces the first block's for routes and components.
  • web/lint/cleanup-selector.test.mjs and one line in .github/workflows/ci.yml's Lint step that runs it.
  • web/src/components/Dialog.tsx: one comment line, the inline disable.
  • tools/verify/SKILL.md: rule 5 is new. The old rule 5 is now rule 6.
  • AGENTS.md: one Enforced rules row.
  • The branch history holds the dropped guard commits and their revert. The squash merge carries only the net diff.

mforce added 12 commits November 6, 2026 23:49
A python3 or node command that runs a script file no longer counts its arguments as a git write. Inline code, interpreter options, shells, wrappers and a bare git or gh argument still block.
1280 light by default; a 390 or dark frame only where layout or theme visibly differs; no frames that differ only in text; about 12 or fewer per PR. #1395 posted 28.
A useEffect cleanup can run a task after the commit that removes the screen, so a continuation in between still reads the flag as set (#1395). The rule points to useLayoutEffect. Dialog.tsx:321 clears the open-dialog count the same way; it gets an inline disable with its reason, and no product change here.
A return inside a helper defined in the effect is not the cleanup. Round 1 review, P3.
…data-only

Round 1 review, P1: npm-cli.js exec -c and pdb.py -c run their argument, so an interpreter-wide exemption let a git write through. The script must now be a regular file whose first five lines carry 'git-guard: data-only-args'; a missing, unreadable or unmarked script blocks as before.
Round 1 review, P2: de-duplicate within the before set and within the after set, never across the pair AGENTS.md requires.
Without them the marker lookup alone blocked python3 -c and node -e, so the option rule had no test.
… symlink

Round 2 review, P1: Python's open follows a symlink before .., while Node drops link/.. first, so the guard could read a marked file while Node ran an unmarked runner. The exemption now needs a script path with no .. whose realpath equals its normalized path, and reads the marker through it.
Three review rounds each found a bypass (runner scripts, symlink plus .., swap-then-run). The maintainer chose to drop the guard change; tools/agent-guard/ is back at 1aa3f51.
Ten in-memory effects linted with the real config, run by CI's Lint step. Dropping the effect-ownership check turns nestedFactory and immediateFactory red.
@mforce
mforce merged commit 7093fc1 into main Nov 7, 2026
21 checks passed
@mforce
mforce deleted the chore/agent-tooling-retro branch November 7, 2026 06:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant