Skip to content

feat(hooks): template-derived wish validator + corpus lint gate (hooks-v2#validator) - #2785

Merged
namastex888 merged 2 commits into
devfrom
wish/hooks-v2-validator
Aug 13, 2026
Merged

namastex888 merged 2 commits into
devfrom
wish/hooks-v2-validator

Conversation

@namastex888

@namastex888 namastex888 commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

hooks-v2 — Group validator: validate-wish that matches the template

Goal: The blocking write check validates against the canonical template, not vibecode regexes.

Deliverables

1. Template-derived parser — plugins/genie/scripts/src/validate-wish.ts rewritten. The canonical template fixture (skills/wish/templates/wish-template.md) is embedded in the shipped .cjs bundle by esbuild's text loader, and every structural rule is parsed from it at load time:

  • required ## sections + the Scope ### IN/### OUT subsections (the template's own headings),
  • the ### Group N: heading shape derived from the template's group heading — digit groups (the template's form) and named groups both pass,
  • status read from the template's metadata table row (| **Status** | … |), with the legacy **Status:** line tolerated,
  • the checkbox rule checks for - [ ] / - [x] items and exempts completed docs (SHIPPED / DONE / EXECUTED),
  • legacy formats tolerated by date: docs predating the template's current form (2026-07-29, the template's last revision b0e77cd15) need only a status signature; missing/invalid dates cannot bypass the strict contract.

2. CI lint over every current WISH.md — scripts/wishes-lint.ts now runs the validator on every WISH.md it scans, so bun run wishes:lint (already in CI) gates the genie corpus. The remotty corpus is gated by a new wish-validator workflow that ships in the sibling remotty corpus-repair PR (remotty is private, so its own CI fetches the public genie gate script at main HEAD and lints .genie/wishes on every wish-doc change — deliberately unpinned so the corpus tracks the current template).

3. Symlink reject + size cap — the read is readBoundedWishFile (the same lstat+O_NOFOLLOW+fstat primitive the board uses) with a 256 KiB cap. Symlinks, oversized files, and non-regular paths are refused with named errors, while a missing file (new wish) skips validation. Stat failures other than ENOENT/ENOTDIR are named refusals, not silent skips.

Blocking semantics (execution-review fix): hook-driven paths (PreToolUse/PostToolUse stdin JSON) exit 2 on validation failure and on symlink/size refusals — Claude Code treats command-hook exit 2 as the blocking failure (dispatch-runtime contract) — while --file CLI mode keeps the documented exit 1. PostToolUse exit 2 feeds the violations back to the agent on stderr.

4. Corpus repairs — shipped in the sibling remotty PR (automagik-dev/remotty#38): headless-turn-open depends-on/blocks prose → cross-wish note; roadmap-truth design-evidence re-review + re-stamp; agent-parity MERGED → SHIPPED; plus genie-board's two colon-drifted **Validation** labels surfaced by the new validator.

New-wish wedge, closed

The old validator demanded ### Group [A-Z]: while the template ships digit groups — every template-shaped write failed. Now the raw template validates clean (pinned by test), and PreToolUse validates the proposed write result (Write tool_input.content, simulated Edit), so fixing a broken doc is never blocked — only writes whose result is broken are.

Validation

bun test plugins/genie/scripts/validate-wish.test.ts   # 32 pass (new file beside the source)
bun run lint:hook-bundles                              # parity incl. embedded template
bun scripts/wishes-lint.ts --wishes-dir <remotty-corpus>/.genie/wishes
  → wishes-lint: OK (21 files scanned, 0 broken brainstorm links, template validator green)

bun run check:fast green (typecheck, lint, dead-code, skills/wishes/council/budget lints, hook-bundle parity, hook-content, plugin-executables). bun test shows no new failures vs the clean-dev baseline (the only deltas are the known doctor-latency env failure and network-bound release-asset timeouts, both also present in baseline runs).

Template drift now fails CI twice over: bundle parity (the embedded template must be regenerated) and the corpus lint (docs must follow the template).

Orchestrator merges; worker never merges.

…s-v2#validator)

The blocking write check now derives its rules from the canonical wish
template instead of vibecode regexes that predated the template's digit
groups and table-row metadata.

- validate-wish.ts: the template fixture (skills/wish/templates/
  wish-template.md) is embedded in the shipped bundle by esbuild's text
  loader, and every structural rule is parsed from it — required ##
  sections, the Scope IN/OUT subsections, the '### Group N:' heading
  shape (digits AND named groups), the '# Wish:' title, and the
  checkbox item syntax. Status is read from the template's metadata
  table row (legacy '**Status:**' line tolerated). The checkbox rule
  exempts completed docs (SHIPPED/DONE/EXECUTED). Legacy formats are
  tolerated by date: docs predating the template's current form
  (2026-07-29, b0e77cd) need only a title+status signature, while
  missing/invalid dates cannot bypass the strict contract.
- PreToolUse validates the PROPOSED write result (Write tool_input.
  content, simulated Edit), so fixing a broken doc is never blocked —
  only writes whose result is broken are.
- The read is bounded and refuses symlinks (readBoundedWishFile,
  256 KiB cap, matching the board).
- wishes-lint.ts runs the validator over every WISH.md, so the CI lint
  gates the corpus in both repos (remotty corpus gate: the sibling
  corpus-repair PR arms a wish-validator workflow in the remotty repo).
- hook-bundle-parity.ts gains the .md text loader so the bundled .cjs
  tracks the template byte for byte; template drift fails CI until the
  bundle is regenerated.
- New validate-wish.test.ts beside the source (dispatch-runtime.test.ts
  pattern): template pinning, digit/named groups, table-row status,
  checkbox exemption, legacy tolerance, symlink/size-cap rejects,
  hook-event CLI behavior, and the whole current genie corpus.
@coderabbitai

coderabbitai Bot commented Aug 13, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f8f144c8-2d5b-44d1-adca-d5350170842f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ccee440cd7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

for (const issue of result.issues) {
console.error(` - line ${issue.line}: ${issue.message}`);
}
return 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Return a blocking exit code on validation failures

In the PreToolUse registration in plugins/genie/hooks/hooks.json, this command is described as a blocking write check, but returning 1 only reports a non-blocking hook error, so Claude can still execute a Write/Edit whose proposed wish fails validation. Claude's hook documentation classifies exit code 2 as a “blocking error” and other nonzero codes as a “non-blocking error”; return 2 or emit an explicit deny response for the PreToolUse path instead. See Claude Code hook exit-code behavior.

Useful? React with 👍 / 👎.

Comment on lines +271 to +273
for (const section of CONTRACT.sections) {
if (!new RegExp(`^##\\s+${escapeRegExp(section)}\\s*$`, 'm').test(content)) {
issues.push({ line: 1, message: `Missing required section: ## ${section}` });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude fenced headings from structural validation

For post-contract wishes containing Markdown examples, these raw multiline regexes treat headings inside fenced code blocks as real sections, even though the contract parser itself explicitly skips fences. A document can therefore omit an actual required section while satisfying it with a fenced ## ... example; conversely, findHeading can select a fenced ## Success Criteria before the real section and incorrectly reject an otherwise valid document. Reuse the fence-aware line scan for required headings and semantic section lookup.

Useful? React with 👍 / 👎.

…sals (hooks-v2#validator)

Execution review FIX-FIRST (loop 1):

- HIGH: hook-driven paths (PreToolUse/PostToolUse stdin JSON) now exit 2
  on validation failure and on symlink/size-cap refusals — Claude Code
  treats command-hook exit 2 as the blocking failure (dispatch-runtime
  contract). The --file CLI keeps its documented exit 1. Tests moved to
  exit 2 with honest names, and a hook-path symlink refusal test proves
  the refusal is blocking too. PostToolUse exit 2 feeds the violations
  back to the agent on stderr.
- LOW: readWishFile no longer fails open on stat errors — ENOENT and
  ENOTDIR are the only 'missing' cases (new wish); every other errno
  (EACCES, ELOOP, I/O) is a named refusal. Regression test added.
@namastex888
namastex888 merged commit 1a798b9 into dev Aug 13, 2026
17 checks passed
@automagik-genie
automagik-genie deleted the wish/hooks-v2-validator branch September 25, 2026 04:53
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