Repository navigation
feat: bring han current with the Claude Code 2.1.228 harness surface - #107
Conversation
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.
|
🚅 Deployed to the han-pr-107 environment in han-team-platform 4 services not affected by this PR
|
ReviewThis PR (draft) adds a single new file, MODERATEMissing required YAML frontmatter on the new blueprint file. Per ---
name: <slug>
summary: <one-line summary>
---
Suggest adding: ---
name: harness-feature-parity
summary: Audit procedure and coverage tracking for han's Claude Code harness feature parity
---MINOR
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. |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
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.
Verification ReviewChecked original concerns from the first review against current state (0 lines changed since last review, no author responses on the thread).
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.
Verification Review0 lines changed since the last review (commit a345d9b), no author responses on the thread.
Verdict: Not yet approved — frontmatter gap remains the only real blocker, and it's a two-line fix. |
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.
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 LGTM - Approving after 3 review cycles |
Problem
han's harness integration was pinned to the Claude Code 2.1.215 surface.
latestis 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 underplugins/<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:What changed
Resolution. One resolver in
hooks/plugin-discovery.ts, marketplace-manifest first.marketplace.jsonis the only place that knows both the current layout and all 179 published legacy aliases, and it does not require ahan-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.jsontimeoutis seconds per the Claude Code schema;execSynctakes milliseconds. Every hook was being killed after 5 to 15 milliseconds.plugins/coreregistershan 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.PostToolUsegroup matchingEdit|Writeran 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 oneHOOK_EVENT_TYPESarray.han hook testmodels 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 missingversion.Dead controls made live or made honest.
hooks.checkpointsandHAN_NO_CHECKPOINTSwere documented kill switches that nothing read; they now work.han config,han authandhan syncwere fully implemented and never registered inmain.ts.Coverage. 21 plugins shipped a
hooks.jsonwith nohan-plugin.yml, sogenerate-hooks --checknever saw them;--checkgoes from 70 to 88 files with zerohooks.jsoncontent 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
bun testtsc --noEmitbiome lintplugin generate-hooks --all --checkhan plugin validate, all 160 pluginsmarkdownlint-cli2marketplace.jsonvs schemastore marketplace schemaThe 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.mdrecords the baseline, the re-run procedure, and the invariants that are easy to break again.