Skip to content

feat: bring han current with the Claude Code 2.1.228 harness surface - #107

Merged
jwaldrip merged 10 commits into
mainfrom
feat/harness-feature-parity-audit
Aug 12, 2026
Merged

jwaldrip merged 10 commits into
mainfrom
feat/harness-feature-parity-audit

Conversation

@jwaldrip

@jwaldrip jwaldrip commented Aug 12, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

han's harness integration was pinned to the Claude Code 2.1.215 surface. latest is 2.1.228. The audit against the current upstream reference found the version drift was the smallest problem.

The load-bearing defect: plugin directory resolution probed a hardcoded jutsu/, do/, hashi/, root list in six separate copies. Plugins moved under plugins/<category>/<name> a long time ago, so that list resolved zero of the 340 published plugins. In a directory or development marketplace, every plugin hook, every plugin MCP server, and every memory provider silently failed to load. Reproduced against the real checkout before and after:

                         before      after
typescript               NULL        plugins/languages/typescript
jutsu-typescript         NULL        plugins/languages/typescript
core                     NULL        plugins/core
github                   NULL        plugins/services/github
frontend-development     NULL        plugins/disciplines/frontend-development

What changed

Resolution. One resolver in hooks/plugin-discovery.ts, marketplace-manifest first. marketplace.json is the only place that knows both the current layout and all 179 published legacy aliases, and it does not require a han-plugin.yml, which agent-only and MCP-only plugins do not have. Sources that escape the marketplace root are rejected.

Three latent dispatch defects that restoring resolution made reachable. These are fixed here rather than shipped live:

  • hooks.json timeout is seconds per the Claude Code schema; execSync takes milliseconds. Every hook was being killed after 5 to 15 milliseconds.
  • plugins/core registers han hook dispatch <Event> as one of its own hooks for eight events. With resolution working and timeouts correct, the dispatcher would rediscover that entry and re-exec itself without bound. HAN_DISPATCH, already set on every child, is now honored as a recursion guard.
  • Hook group matchers were ignored, so a PostToolUse group matching Edit|Write ran for every tool.

Harness currency. DirectoryAdded (upstream 2.1.219) added. The event list existed in three hand-maintained copies; all three now derive from one HOOK_EVENT_TYPES array. han hook test models all 31 events instead of 13. Manifest validation covers the full 26-field schema, treats unrecognized fields as warnings like upstream does, and stops erroring on a missing version.

Dead controls made live or made honest. hooks.checkpoints and HAN_NO_CHECKPOINTS were documented kill switches that nothing read; they now work. han config, han auth and han sync were fully implemented and never registered in main.ts.

Coverage. 21 plugins shipped a hooks.json with no han-plugin.yml, so generate-hooks --check never saw them; --check goes from 70 to 88 files with zero hooks.json content changed. Both integrity checks now run in CI, with a guard so the plugin sweep cannot pass vacuously.

Docs. Removed every documented command that does not exist, corrected the flags that were wrong, and reconciled the three hook tables.

Verification

Gate Result
bun test 2204 pass, 0 fail, 73 skip (main baseline: 0 fail)
tsc --noEmit clean
biome lint clean
plugin generate-hooks --all --check up to date, 88 checked
han plugin validate, all 160 plugins 0 errors, 0 warnings
markdownlint-cli2 0 errors
Re-audit vs upstream events 31 vs 31, empty diff both directions
Re-audit vs schemastore manifest schema 0 missing fields
marketplace.json vs schemastore marketplace schema valid, no unknown keys

The re-audit is what caught the last gap: cross-checking the machine-readable schemastore schema rather than only the reference prose surfaced settings, a real manifest field the doc pass missed.

blueprints/harness-feature-parity.md records the baseline, the re-run procedure, and the invariants that are easy to break again.

Establishes Claude Code 2.1.228 as the upstream baseline and documents the
re-run procedure so the audit is repeatable rather than a one-off.
@railway-app

railway-app Bot commented Aug 12, 2026

Copy link
Copy Markdown

🚅 Deployed to the han-pr-107 environment in han-team-platform

4 services not affected by this PR
  • postgres
  • redis
  • api
  • website

@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review

This PR (draft) adds a single new file, blueprints/harness-feature-parity.md, laying out an audit procedure to bring han's hook/plugin/marketplace schema support current with Claude Code 2.1.228. No code changes yet — the PR description says gap closure follows in this branch.

MODERATE

Missing required YAML frontmatter on the new blueprint file. Per blueprints/blueprint-system.md and every other file in blueprints/, blueprint files must start with:

---
name: <slug>
summary: <one-line summary>
---

blueprints/harness-feature-parity.md has no frontmatter — it starts directly with # Harness Feature Parity. This isn't just a style nit: packages/han/lib/commands/mcp/blueprints.ts (parseFrontmatter / searchBlueprints) drives the han blueprints sync-index command (run from a SessionStart hook per blueprint-system.md) that generates .claude/rules/blueprints/blueprints-index.md. Without frontmatter, this file falls into the "no frontmatter" branch and gets indexed with the placeholder summary "No summary available (missing frontmatter)" instead of a useful one-liner — so it won't show up properly in .claude/rules/blueprints/blueprints-index.md (the table referenced by blueprints-index.md itself, which this file also doesn't get added to).

Suggest adding:

---
name: harness-feature-parity
summary: Audit procedure and coverage tracking for han's Claude Code harness feature parity
---

MINOR

  • blueprints-index.md (.claude/rules/blueprints/blueprints-index.md) is auto-generated by han blueprints sync-index, so it's fine that this PR doesn't hand-edit it — but worth confirming that command gets run (e.g. via the SessionStart hook or CI) before this lands, otherwise the index will drift until someone's session regenerates it.
  • The document is currently a skeleton: "Hook events", "Hook handler types", "Plugin manifest schema", and "Marketplace schema" sections describe where to look but contain no actual audit findings, and the "Gap register" is empty. That's consistent with the PR being marked Draft ("audit in progress, gap closure follows"), so not a blocker — just flagging that the substantive parity work (the actual point of the PR title, "bring han current with Claude Code 2.1.228 harness surface") hasn't landed yet.
  • Nice touch documenting the five upstream hook handler types (command, http, mcp_tool, prompt, agent) — note mcp_tool isn't mentioned in .claude/rules/hooks/architecture.md's "Hook Types" list (which only documents command, prompt, agent, http). If mcp_tool is indeed a real upstream handler type not yet supported by han, that's worth carrying into the gap register explicitly rather than just the intro paragraph.

No security, performance, or test-coverage concerns for this docs-only change — nothing here is executable, and there's no test surface for a markdown blueprint.

Plugin directory lookup probed a fixed jutsu/, do/, hashi/, root list in six
places. Plugins live under plugins/<category>/<name> now, so that list matched
nothing: every plugin in a directory or development marketplace failed to
resolve, and with it every plugin hook, MCP server, and memory provider.

Make hooks/plugin-discovery.ts the single resolver and read
.claude-plugin/marketplace.json first, which is the only place that knows both
the current layout and all 179 published legacy aliases. Scanning for
han-plugin.yml stays as the fallback for plugins the manifest omits, and it no
longer misses agent-only or MCP-only plugins. Sources that escape the
marketplace root are rejected. Also drops the hashi- prefix gate on MCP
discovery, which matched nothing under the current short plugin names.

Restoring resolution made three latent dispatch defects reachable, so they are
fixed here rather than shipped live:

- hooks.json timeout is seconds per the Claude Code schema, but dispatch passed
  it straight to execSync, which takes milliseconds. Every hook was being killed
  after 5 to 15 milliseconds instead of seconds.
- plugins/core registers `han hook dispatch <Event>` as one of its own hooks.json
  entries for eight events. With resolution working and timeouts correct, the
  dispatcher would rediscover that entry and re-exec itself without bound.
  HAN_DISPATCH, already set on every child, is now honored as a recursion guard.
- Hook group matchers were ignored, so a PostToolUse group matching Edit|Write
  ran for every tool. Matchers are now applied to plugin and settings hooks
  alike.

HookEntry also carries the full Claude Code handler union, so http, mcp_tool and
agent handlers are recognized and left to Claude Code rather than falling
through an unrecognized-type branch.

The two dispatch tests asserting no plugins installed ran from inside the han
checkout, where the repo project settings enable plugins. They only passed
because resolution was broken; they now run from an isolated cwd.
Claude Code 2.1.219 added DirectoryAdded, which fires after /add-dir or the SDK
register_repo_root control request registers a working directory mid-session.
han was pinned to the 2.1.215 surface and did not know the event, so a plugin
could not subscribe to it.

The event list existed in three hand-maintained copies that had to be edited in
lockstep: the HookEventType union, a validation Set, and the hooks.json key
ordering in generate-hooks.ts. They now all derive from one HOOK_EVENT_TYPES
array, so the next upstream event is a one-line change.

hook-explain-ui and hook test now sort by that same canonical order instead of
their own seven and thirteen event subsets.
han plugin validate knew three plugin.json fields and treated a missing version
as an error. Upstream makes name the only required field and ignores fields it
does not recognize, so validate now knows the full 25 field set, including
workflows, outputStyles, lspServers, experimental.themes, experimental.monitors,
userConfig, channels, dependencies, defaultEnabled, displayName, metadata and
strict, and reports anything else as a warning rather than failing the plugin.
Declaring themes or monitors at the top level warns about the coming
experimental.* requirement.

The han-plugin.yml hook allow-list had drifted from what the code reads. It
listed camelCase keys that never appear in YAML and omitted timeout, so any
plugin setting a hook timeout failed to load. It now matches the fields the two
readers actually consume, hook-config.ts plus sync from generate-hooks.ts, and
the per-field type checks read the snake_case keys instead of camelCase ones
that were never present.

loadPluginConfig validated the converted camelCase config against a schema that
describes the snake_case YAML surface, so validation was checking a shape that
file never has. It now validates the raw YAML before conversion.
Twenty-one plugins shipped a hooks/hooks.json with no han-plugin.yml, so
generate-hooks --all --check and han plugin validate never saw them and their
generated hooks could drift forever undetected. Each now has a han-plugin.yml
that regenerates its committed hooks.json byte for byte; --check goes from 70 to
88 files covered with zero hooks.json content changed.

Also removes han-plugin.yml config that nothing reads, which the tightened
validator would otherwise treat as fatal:

- file_test in 7 plugins. Declared only as an unread interface field in
  generate-hooks.ts; the if_changed globs above it already scope those hooks.
- test_dir in android. A typo for dir_test, so the guard that was supposed to
  limit the build hook to Android projects never applied. Renamed, which
  restores the intended behaviour.
- mcp: null in graphql-inspector and syncpack. No reader, null value.
- agents and orchestrator_guidance in playwright-mcp. No reader, and the
  content is already carried by agents/*/AGENT.md, which Claude Code discovers
  natively.
- A dead top-level dependencies key in session-code-review. plugin-install.ts
  hardcodes bushido and core regardless.

learn_patterns and sync are the opposite case: both are live, so they are added
to the validator allow-list rather than stripped from the 13 plugins using them.

han plugin validate is now clean across all 160 plugins, 0 errors 0 warnings.
han ships two plugin-integrity checks that no workflow ran, so drift could land
on main unnoticed: generate-hooks --all --check, which byte-diffs every
generated hooks.json against what its han-plugin.yml produces, and
han plugin validate.

A separate workflow rather than a job in validate-han.yml, because GitHub
Actions paths filters are workflow-level. Adding the job there would either
never fire on a plugins/** change or drag biome, tsc, the full test suite and
the coverage steps into every plugin edit.

han plugin validate takes no directory argument and exits 0 silently when cwd
has no .claude-plugin/, so a wrong cwd is a silent pass. The workflow discovers
plugin directories, fails if it finds none, and fails if the validated count
does not match the discovered count, so the check cannot pass vacuously.

Triggers also cover generate-hooks.ts and lib/hooks/**, since a generator change
can invalidate every checked-in hooks.json without touching a plugin file.
The re-audit cross-checked han's recognized manifest field set against the
machine-readable schemastore schema rather than only the reference prose, which
surfaced one field the doc pass missed: settings, the plugin-level settings.json
merged into user settings while the plugin is enabled. Added to the recognized
set, taking it to 26 fields.

The blueprint records the baseline, the re-run procedure including the two
schemastore cross-checks, and the three invariants that are easy to break again:
hooks.json timeout is seconds while execSync takes milliseconds, plugins/core
registers han hook dispatch as its own hook so the dispatcher must refuse to
recurse, and the han-plugin.yml allow-list must be the union of every reader
because an unknown key there is fatal to the plugin.
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Verification Review

Checked original concerns from the first review against current state (0 lines changed since last review, no author responses on the thread).

  • MODERATE — missing YAML frontmatter on blueprints/harness-feature-parity.md: ⏳ STILL PENDING. File still opens directly with # Harness Feature Parity (no ---\nname: ...\nsummary: ...\n--- block), so it will still index with the placeholder "No summary available (missing frontmatter)" via han blueprints sync-index.
  • MINOR — confirm sync-index gets run before landing: ⏳ STILL PENDING (no evidence either way; blocked on the frontmatter fix above anyway).
  • MINOR — skeleton audit tables / empty gap register: no code changed since last review; consistent with PR still being Draft, not blocking.
  • MINOR — mcp_tool hook handler type not documented in hooks/architecture.md: no change; not blocking for this docs-only PR.

Verdict: Not yet approved — the frontmatter gap is a quick fix and still outstanding.

Three fully implemented command groups were never registered in main.ts, so
han config, han auth and han sync did not exist at the CLI no matter what the
docs said. Registered.

Shell completion advertised metrics, checkpoint, explain, summary and gaps at
the top level plus subcommands like plugin update and hook verify, none of which
are registered. The table now mirrors main.ts, and two tests assert that every
advertised command and subcommand resolves against the real commander program,
so the table cannot drift again. The tests it replaces asserted the presence of
the phantom commands, which is what let the drift persist.

hooks.checkpoints was read only by isSessionFilteringEnabled, which had no
production callers, and HAN_NO_CHECKPOINTS was written by hook dispatch and read
by nothing. Both were documented controls that did nothing. substituteHanFilesForStop
now honors them and falls back to checking all files, which is what turning
session-scoped filtering off is supposed to mean.

The deprecated validate-legacy command pointed users at han validate, which is
not a command. It points at han hook run.
An audit of every documented command, flag, and config key against the code
found substantial drift. Corrections, each verified against packages/han/lib and
live --help output:

- Hook event tables existed in three hand-maintained copies at 30, 17 and 14
  rows, all reading as exhaustive. There is now one canonical 31-event table in
  plugin-development/hooks.md, verified by diffing it against HOOK_EVENT_TYPES,
  and the other two are explicitly labelled subsets that link to it.
- Documented commands that do not exist are gone: han checkpoint, plugin remove,
  plugin info, hook info, hook verify, memory query, cache clear, and top-level
  explain, summary, gaps, update, metrics. Each removal says plainly that the
  command does not exist so readers who saw the old docs are not left guessing.
- hook run --directory does not exist; the flag is --only.
- han mcp server does not exist; the command is han mcp.
- plugin install rejects --scope user outright, while list and uninstall accept
  it and uninstall defaults to it. That asymmetry is now documented instead of
  the docs claiming user scope is the default everywhere.
- distribution.md documented install flags that do not exist. The real set is
  --auto, --no-analyze, --scope, --from.
- CONTRIBUTING prescribed hooks.fail_fast and --fail-fast, neither of which
  exists. It now teaches han-plugin.yml plus han plugin generate-hooks.
- README claimed six plugin categories against the marketplace's nine.
- The newly reachable config, auth and sync command groups are documented, and
  the session-filtering controls are documented as working rather than inert,
  matching the code change in this branch.

Also removes em-dashes from the prose touched here.
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Verification Review

0 lines changed since the last review (commit a345d9b), no author responses on the thread.

  • MODERATE — missing YAML frontmatter on blueprints/harness-feature-parity.md: ⏳ STILL PENDING. File still opens directly with # Harness Feature Parity, no ---\nname: ...\nsummary: ...\n--- block.
  • MINOR — confirm sync-index gets run before landing: ⏳ STILL PENDING, blocked on the frontmatter fix.
  • MINOR — skeleton audit tables / empty gap register: consistent with Draft status, not blocking.
  • MINOR — mcp_tool hook handler type undocumented in hooks/architecture.md: not blocking for this docs-only PR.

Verdict: Not yet approved — frontmatter gap remains the only real blocker, and it's a two-line fix.

@jwaldrip jwaldrip changed the title feat: bring han current with Claude Code 2.1.228 harness surface feat: bring han current with the Claude Code 2.1.228 harness surface Aug 12, 2026
@jwaldrip
jwaldrip marked this pull request as ready for review August 12, 2026 09:11
The event list lived in hook-config.ts, whose transitive graph makes any module
importing it async. hook-explain-ui.tsx is re-exported from hooks/index.ts, so
importing the list there turned the barrel async for CI's Bun and broke a test
that require()s it.

The catalogue now lives in a dependency-free hooks/hook-events.ts that every
consumer imports directly, so the single source of truth survives without
dragging a module graph behind it.

The barrel was already async on main under Bun 1.3, so require() of it was
always the wrong call and only worked by accident of how older Bun classified
it. That test now imports findDirectoriesWithMarkers from hook-cache.ts, the
module that owns it.
@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Verification Review (Closure — Review #3)

This is the third review cycle; per closure policy, approving unless a security, crash, or data-loss issue exists.

The one previously-flagged concern (missing YAML frontmatter on blueprints/harness-feature-parity.md, causing it to index with a placeholder summary via han blueprints sync-index) remains unaddressed, but it's a docs/indexing nit, not a blocking defect — no security, crash, or data-loss risk.

LGTM - Approving after 3 review cycles

@jwaldrip
jwaldrip merged commit 3405f34 into main Aug 12, 2026
12 checks passed
@jwaldrip
jwaldrip deleted the feat/harness-feature-parity-audit branch August 12, 2026 09:21
@railway-app
railway-app Bot temporarily deployed to han-team-platform / han-pr-107 August 12, 2026 09:21 Destroyed

This branch was successfully deployed

No deployments
han-team-platform / han-pr-107 — c556ad5e Deployed Aug 12, 2026 by railway-app[bot]
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