You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
All line anchors below were verified against HEAD 00af2568 ("docs(skill): add explicit HTTP verb decision guide for server actions (#1369)") on 2026-08-10. Re-check an anchor before editing if HEAD has moved.
Problem
The framework repo keeps its Claude Code skills at .claude/skills/<name>/ and mirrors them into .agents/skills/<name> as relative symlinks so Antigravity, which reads <workspace-root>/.agents/skills/<folder>/SKILL.md, sees the same set. That mirror has drifted in two independent directions, and a third claim in the original report needed correcting.
Gap 1, missing symlinks (confirmed).git ls-files .claude/skills lists ten committed skills, each with a committed SKILL.md:
git ls-files -s .agents/skills lists only seven mode-120000 entries (use-railway, webjs-blog-write, webjs-doc-sync, webjs-file-issue, webjs-list-todos, webjs-research-record, webjs-start-work). Missing, exactly as reported, are webjs-ready-for-dev, webjs-scaffold-sync, and webjs-instagram-post. All three targets exist and carry a committed SKILL.md, so the symlinks will resolve the moment they are added.
Two entries under .agents/skills/ are legitimate non-symlinks and must survive untouched. .agents/skills/webjs/ is a real committed directory (the framework teaching skill, SKILL.md plus references/, 15 tracked files) that has no .claude/skills counterpart and is the copy source in scripts/sync-scaffold-skill.mjs:21. .agents/skills/omarchy is a machine-local absolute symlink into ~/.local/share, deliberately untracked via .gitignore:98, and test/repo-health/no-committed-symlinks.test.mjs exists to keep links of that shape out of the index.
Gap 2, the documentation list (confirmed, and the drift is a different set from gap 1)..agents/AGENTS.md:48-56 lists six skills under ## Custom Skills Usage, omitting four: webjs-blog-write, webjs-ready-for-dev, webjs-scaffold-sync, and webjs-instagram-post. Note webjs-blog-write already HAS a symlink and is simply undocumented, so the symlink set and the documented set have drifted apart independently. Six listed plus four missing is ten, which matches the committed skill count.
Correction to the original report, part 1 of 2. The original body treated .agents/hooks.json as an obvious missing mirror of .claude/settings.json. The file is real (Antigravity reads hooks.json from the workspace .agents/ directory, docs), so the premise is not invented, but the conclusion does not follow. See the design section: part 2 is rejected on the merits, and the sub-item about making .claude/hooks/*.sh dual-engine is rejected with it.
Correction to the original report, part 2 of 2, and a real defect the report missed..agents/AGENTS.md is at a path nothing documents reading. Antigravity's rules reference states that "Workspace rules live in the .agents/rules folder of your workspace or git root" (docs), with GEMINI.md and a repo-root AGENTS.md as the other project-level surfaces. .agents/AGENTS.md is none of those. The repo already knows the correct path and says so in its own published prose: blog/ai-first-is-plumbing.md:25 and blog/stop-ai-agents-breaking-your-code.md:25 both label .agents/rules/workflow.md as the Antigravity workspace-rules file, root AGENTS.md:36 names .agents/rules/workflow.md as part of the single cross-agent source, and the scaffold ships exactly that path (packages/cli/templates/.agents/rules/workflow.md, copied by packages/cli/lib/create.js:575). The framework repo itself has no .agents/rules/ directory at all. A grep across the repo finds zero references to .agents/AGENTS.md from any test, script, workflow, or doc, so nothing depends on the current location. Adding four bullets to a file no tool loads would be a no-op, so the fix has to move the file as well as edit it.
Why this keeps reopening. Nothing asserts the three sets agree. test/hooks/route-skills.test.mjs:233-252 is the closest existing guard, and it only asserts that each project skill named inside .claude/hooks/route-skills.sh has a committed .claude/skills/<name>/SKILL.md. That test passes at HEAD, because the router already references all ten names, and it says nothing about .agents/skills or about the documented list. Without a guard, the eleventh skill reopens the same gap.
Design / approach
Three settled decisions.
1. Reject .agents/hooks.json and the scripts/sync-hooks.js generator
.agents/hooks.json is a genuine Antigravity surface, so the rejection is on cost and doctrine rather than on the file being fictional. Four reasons, in order of weight.
A mirror is a protocol port, not a field rename. The original sub-item framed it as "a jq fallback for .tool_input versus .toolCall.args". The two engines disagree on three axes, and the field paths are the least of them.
Axis
Claude Code
Antigravity
Block a tool call
exit 2 with the reason on stderr
stdout JSON {"decision":"deny","reason":"..."}, exit code is not the gate
Inject context
hookSpecificOutput.additionalContext on UserPromptSubmit
{"injectSteps":[{"ephemeralMessage":"..."}]} on PreInvocation
Nine of the eleven scripts in .claude/hooks/ block with exit 2 and would each need a second output path, and route-skills.sh would need its whole output rewritten rather than re-keyed. block-prose-punctuation.sh reads five distinct content fields (.claude/hooks/block-prose-punctuation.sh:43-48); its faithful Antigravity counterpart depends on the ReplacementChunks element shape, which the public docs do not specify, so part of the port cannot be written correctly from documentation alone.
A generated second copy of a rule source is what this repo argues against. Root AGENTS.md:36 states the doctrine outright: tools that do not read AGENTS.md natively get "a THIN bridge pointing at it ..., never a duplicated rule set", and the same line records that "Claude Code additionally ships the protective enforcement hooks (.claude/, .hooks/pre-commit)", so hooks being Claude-only here is a stated position rather than an oversight. A scripts/sync-hooks.js translator would need a hardcoded matcher, vocabulary, and protocol map, which means the map IS the configuration and generating a file from it removes no hand-written work. The output would also have to be committed, because Antigravity reads it off disk, which is precisely the committed-generated-duplicate the repo avoids elsewhere (the generated scaffold-skill bundle is gitignored at .gitignore:93 and produced at prepack by scripts/sync-scaffold-skill.mjs).
The repo moved the other way one commit ago.241c961b (#1368, HEAD~1) deleted .cursor/hooks.json, .cursor/hooks/nudge-uncommitted.sh, .gemini/settings.json, .gemini/hooks/nudge-uncommitted.sh, .opencode/plugins/nudge-uncommitted.ts, GEMINI.md, .cursorrules, and .github/copilot-instructions.md from the scaffold templates, keeping .claude/, .hooks/pre-commit, AGENTS.md, and CLAUDE.md. Adding a per-agent hook mirror to the framework repo one commit later reverses a decision the project just made deliberately.
Prior art agrees. Next.js, Remix, Astro, Svelte, and TanStack Router all keep skills under .agents/skills/ (verified in the local clones at ~/Documents/Projects/frameworks/), and none of the five ships a hooks.json at any level. Next.js goes further and makes the mirror structural: next.js/.claude contains a single symlink, skills -> ../.agents/skills, so a per-skill link can never go missing.
What actually covers the underlying need, chosen alternative. Enforcement for non-Claude agents already exists at two tool-agnostic layers, and the correct fix is to make an Antigravity session aware of them rather than to build a parallel engine. .hooks/pre-commit fires for every agent and every human on every commit, blocking a direct commit to main/master and blocking a published-library version bump off a chore/release-* branch. Its own header at .hooks/pre-commit:12-13 records the division of labour: "The test suite is NOT run here. CI (.github/workflows/ci.yml) is the test gate", and .github/workflows/ci.yml runs conventions (webjs check plus webjs doctor over blog and website), unit, and the Bun matrix, with branch protection blocking a merge until they pass. The Claude PreToolUse gates are a faster local feedback loop over enforcement that already binds every agent, not the only enforcement. The residual gap is awareness, which is a prose fix and is in scope for a documentation issue, so the moved rules file gains a short "Enforcement gates" section naming those two layers, the never---no-verify rule, and the fact that the .claude/hooks/* pre-tool gates do not fire outside Claude Code so the session must self-check the same rules.
2. Move the Antigravity rules file to the documented path
.agents/AGENTS.md becomes .agents/rules/workflow.md, matching the documented Antigravity workspace-rules folder, the scaffold's own filename, and the path root AGENTS.md:36 already advertises. This is a git mv plus a contained content edit, not a rewrite. It cannot affect webjs create, because packages/cli/lib/create.js:575 resolves .agents/rules/workflow.md from TEMPLATES only, and the sole repo-root fallback in that file is for the skill directory (packages/cli/lib/create.js:658-664). Nothing else in the repo references either path.
One accepted consequence: .claude/hooks/require-docs-with-src.sh:73-74 counts a doc surface with the pattern (^|/)(AGENTS|CLAUDE|CONVENTIONS|README)\.md$, which .agents/AGENTS.md currently satisfies incidentally and .agents/rules/workflow.md will not. That gate only fires when packages/*/src or packages/cli/lib is staged, and staging the Antigravity rules file was never a sensible way to satisfy a framework-source doc requirement, so the loss is not worth widening the gate's regex for.
3. Add a parity guard, as a new sibling in test/repo-health/
Three symlinks went missing once and the documented list has drifted separately, so detection has to be automatic. The new test goes in test/repo-health/, not test/hooks/, because the invariant is about repo structure rather than hook behaviour, and because the closest precedent lives there: test/repo-health/no-committed-symlinks.test.mjs:31-55 already reads git ls-files -s, filters mode 120000, and resolves each link target with git cat-file -p. The new file reuses that exact technique. test/hooks/route-skills.test.mjs is left alone, since its subject is the router hook and it already passes.
Reading the sets from git ls-files rather than from fs.readdirSync handles the machine-local exception for free: .agents/skills/omarchy is untracked, so git never reports it and no name needs special-casing. .agents/skills/webjs needs one explicit allowance, because it is a tracked real directory with no .claude/skills counterpart.
The Next.js single-directory-symlink inversion is the structurally stronger design and is recorded under Out of scope with its blockers, rather than smuggled into a chore issue.
Implementation plan
Step 0. Cut the worktree
git worktree add -b chore/1372-agents-skill-parity ../webjs-1372-agents-skill-parity origin/main
cd ../webjs-1372-agents-skill-parity
Do all work there. Tracked-file edits in the primary checkout are blocked by .claude/hooks/require-worktree-for-edits.sh. A fresh worktree has no node_modules, so run npm run worktree:link from inside it before running any test.
Step 1. Add the three missing symlinks
The one mechanical trap is getting git to record mode 120000 instead of a regular file. ln -s plus git add does this correctly; writing a text file whose contents are the target path does not. Create them relative to .agents/skills/, so the stored target string is exactly ../../.claude/skills/<name>.
Do not use an absolute target. test/repo-health/no-committed-symlinks.test.mjs fails any tracked symlink whose target is absolute or resolves outside the repo root.
Then edit .agents/rules/workflow.md. Three contained changes.
2a. Retitle and correct the provenance sentence. Lines 1 and 3 read today:
# Translated Claude Code rules for Antigravity
These project-level rules are translated from the user's global Claude configuration (`~/.claude/CLAUDE.md` and `~/.claude/hooks/*`). They govern all operations inside this workspace.
They should read:
# Workspace rules for agents that read `.agents/rules/`
These project-level rules govern all operations inside this workspace. Antigravity loads every markdown file in `.agents/rules/`; the repo-root `AGENTS.md` remains the full contract and this file does not replace it.
2b. Replace the six-skill list with all ten. Lines 48-56 read today:
## Custom Skills Usage- The following skills have been translated and are available in `.agents/skills/`:
-`webjs-start-work`: Trigger when starting a tracked issue on the webjs project board.
-`webjs-doc-sync`: Trigger when documenting new public surfaces or finding doc gaps/drift.
-`webjs-file-issue`: Trigger to file a grounded issue on the board.
-`webjs-list-todos`: Trigger to check the list of TODOs.
-`webjs-research-record`: Trigger to search and record findings.
-`use-railway`: Trigger when interacting with Railway deploys.
- Always use the `view_file` tool on the matched skill's `SKILL.md` before executing its tasks.
They should read:
## Custom Skills Usage- These skills are symlinked into `.agents/skills/` from `.claude/skills/`, so both engines load one copy. Every entry here must have a matching symlink, and every symlink must have an entry here; `test/repo-health/agent-skill-parity.test.mjs` enforces both directions.
-`webjs-start-work`: Trigger when starting a tracked issue on the webjs project board.
-`webjs-ready-for-dev`: Trigger when planning tracked issues into an implementable shape with verified plans.
-`webjs-file-issue`: Trigger to file a grounded issue on the board.
-`webjs-list-todos`: Trigger to check the list of TODOs.
-`webjs-research-record`: Trigger to search and record findings.
-`webjs-doc-sync`: Trigger when documenting new public surfaces or finding doc gaps or drift.
-`webjs-scaffold-sync`: Trigger when changing the CLI generators, the scaffold templates, or the agent teaching skill.
-`webjs-blog-write`: Trigger when writing, drafting, or editing a WebJs blog post under `blog/`.
-`webjs-instagram-post`: Trigger when publishing an SEO post to the WebJs Instagram account.
-`use-railway`: Trigger when interacting with Railway deploys.
- The framework teaching skill at `.agents/skills/webjs/` is a real directory rather than a symlink, and is the reference for building WebJs apps rather than a workflow trigger.
- Always use the `view_file` tool on the matched skill's `SKILL.md` before executing its tasks.
Keep the bullet order stable: it groups the workflow skills, then the authoring skills, then infrastructure.
2c. Append an enforcement section at the end of the file, after the skills section. This is the substantive answer to the rejected hooks mirror:
## Enforcement gates
The protective `PreToolUse` hooks under `.claude/hooks/` fire only inside Claude Code. Two gates bind every agent regardless of engine, and neither is optional.
-`.hooks/pre-commit` runs on every commit. It blocks a direct commit to `main` or `master` and blocks a published-library version bump on any branch that is not `chore/release-*`. Never pass `git commit --no-verify`.
-`.github/workflows/ci.yml` is the test gate (`conventions`, `unit`, and the Bun matrix), and branch protection blocks a merge until it is green. `.hooks/pre-commit` deliberately does not run the suite because CI does.
Because the pre-tool gates do not fire here, self-check what they would have caught before every commit: stage tests alongside any `packages/*/src` change, stage the doc surfaces that change with it, keep the work inside its own worktree, and obey invariant 11 on prose punctuation and `WebJs` brand casing.
Step 3. Add the parity guard
New file, test/repo-health/agent-skill-parity.test.mjs. It is picked up automatically: scripts/run-node-tests.js walks test/ recursively for .test.mjs. This body was run against HEAD from a scratch directory and reds on exactly the two real gaps, passing its other four assertions.
/** * The Claude skill set, the Antigravity symlink set, and the documented list * must agree. * * Skills live once at `.claude/skills/<name>/SKILL.md` and are mirrored into * `.agents/skills/<name>` as relative symlinks, because Antigravity reads * `<workspace-root>/.agents/skills/<folder>/SKILL.md`. Nothing kept the two in * step, so three skills shipped with no symlink and a fourth was symlinked but * never documented (#1372). Every new skill reopens that gap, so assert it. * * Both sets are read from `git ls-files`, not from the filesystem, so the * machine-local `.agents/skills/omarchy` link (untracked, .gitignore:98) is * invisible by construction and needs no special case. The one tracked * exception is `.agents/skills/webjs/`, a real directory holding the framework * teaching skill, which has no `.claude/skills` counterpart. */import{test}from'node:test';importassertfrom'node:assert/strict';import{execFileSync}from'node:child_process';import{readFileSync}from'node:fs';import{dirname,resolve}from'node:path';import{fileURLToPath}from'node:url';constrepoRoot=resolve(dirname(fileURLToPath(import.meta.url)),'../..');constRULES_DOC='.agents/rules/workflow.md';/** Tracked entries under .agents/skills/ that are directories, not mirrors. */constREAL_DIRS=newSet(['webjs']);constgit=(...args)=>execFileSync('git',args,{cwd: repoRoot,encoding: 'utf8'});/** Skill names with a committed `.claude/skills/<name>/SKILL.md`. */functionclaudeSkillNames(){return[
...newSet(git('ls-files','.claude/skills').split('\n').filter((p)=>p.endsWith('/SKILL.md')).map((p)=>p.split('/')[2]),),].sort();}/** Map of `.agents/skills/<name>` symlink name to its stored target string. */functionagentsSkillLinks(){constmap=newMap();for(constlineofgit('ls-files','-s','.agents/skills').split('\n')){// Mode 120000 is git's symlink mode; the blob content IS the target string.if(!line.startsWith('120000 '))continue;constpath=line.split('\t').slice(1).join('\t');map.set(path.slice('.agents/skills/'.length),git('cat-file','-p',`HEAD:${path}`).trim());}returnmap;}test('every committed .claude skill is mirrored into .agents/skills',()=>{constlinks=agentsSkillLinks();constmissing=claudeSkillNames().filter((n)=>!links.has(n));assert.deepEqual(missing,[],`no .agents/skills symlink for: ${missing.join(', ')}. `+`Add it with: cd .agents/skills && ln -s ../../.claude/skills/<name> <name>`,);});test('every .agents/skills symlink is relative and names its own skill',()=>{constbad=[];for(const[name,target]ofagentsSkillLinks()){constwant=`../../.claude/skills/${name}`;if(target!==want)bad.push(`${name} -> ${target} (expected ${want})`);}assert.deepEqual(bad,[],`a mirror symlink has the wrong target:\n ${bad.join('\n ')}`);});test('no .agents/skills symlink dangles',()=>{consttracked=newSet(git('ls-files','.claude/skills').split('\n').filter(Boolean));constdangling=[...agentsSkillLinks().keys()].filter((n)=>!tracked.has(`.claude/skills/${n}/SKILL.md`),);assert.deepEqual(dangling,[],`symlink with no committed target: ${dangling.join(', ')}`);});test('the workspace rules file documents exactly the mirrored skills',()=>{constsection=readFileSync(resolve(repoRoot,RULES_DOC),'utf8').split('## Custom Skills Usage',)[1];assert.ok(section,`${RULES_DOC} must keep its "## Custom Skills Usage" section`);constlisted=newSet([...section.matchAll(/^\s*-\s+`([a-z0-9-]+)`:/gm)].map((m)=>m[1]));constlinked=newSet(agentsSkillLinks().keys());constundocumented=[...linked].filter((n)=>!listed.has(n)).sort();constphantom=[...listed].filter((n)=>!linked.has(n)&&!REAL_DIRS.has(n)).sort();assert.deepEqual(undocumented,[],`symlinked but not listed in ${RULES_DOC}: ${undocumented.join(', ')}`);assert.deepEqual(phantom,[],`listed in ${RULES_DOC} but not symlinked: ${phantom.join(', ')}`);});test('the framework teaching skill stays a real committed directory',()=>{constmodes=git('ls-files','-s','.agents/skills/webjs').split('\n').filter(Boolean).map((l)=>l.split(' ')[0]);assert.ok(modes.length>0,'.agents/skills/webjs must stay committed');assert.ok(!modes.includes('120000'),'.agents/skills/webjs is the canonical teaching skill and must not become a symlink',);});test('the machine-local omarchy link stays untracked',()=>{assert.equal(git('ls-files','.agents/skills/omarchy').trim(),'','omarchy is a machine-local absolute symlink and must stay untracked (.gitignore:98)',);});
Step 4. Document the repo's own agent-config layout
Add a section to framework-dev.md, after ### Merged worktrees are auto-removed (cleanup-merged-worktree.sh) at line 125 and before ### Scaffold teaching-coverage gate at line 135, so the two agent-tooling sections sit together:
### Agent config in this repo: `.claude/` is canonical, `.agents/` mirrors it
Workflow skills live once at `.claude/skills/<name>/SKILL.md`. Antigravity reads
`<workspace-root>/.agents/skills/<folder>/SKILL.md`, so each one is mirrored as a
RELATIVE symlink `.agents/skills/<name> -> ../../.claude/skills/<name>`. Adding a
skill means adding the directory, the symlink, and a bullet in
`.agents/rules/workflow.md`; `test/repo-health/agent-skill-parity.test.mjs`
fails when the three drift apart. Create the link with `ln -s` so git records
mode `120000`, and keep the target relative, or
`test/repo-health/no-committed-symlinks.test.mjs` rejects it.
Two entries under `.agents/skills/` are not mirrors. `webjs/` is the real
committed teaching skill (`scripts/sync-scaffold-skill.mjs` bundles it into the
CLI at prepack), and `omarchy` is a machine-local absolute symlink kept
untracked by `.gitignore:98`.
Lifecycle HOOKS stay Claude-only on purpose. Antigravity supports a workspace
`.agents/hooks.json`, but its blocking protocol (stdout `{"decision":"deny"}`),
its context-injection shape (`injectSteps`), and its tool vocabulary all differ
from Claude Code's, so a mirror is a protocol port rather than a config copy, and
a generated copy would be the duplicated rule set root `AGENTS.md` rules out. The
gates that bind every agent are `.hooks/pre-commit` and CI (#1372).
Step 5. Commit and push
One logical unit, so one commit:
chore: mirror every Claude skill into .agents and guard the parity
Three skills had no .agents/skills symlink and a fourth was symlinked but
undocumented, so Antigravity saw a different skill set from Claude Code.
Move the workspace rules to the path Antigravity actually reads, list all
ten skills there, and add a repo-health test so the sets cannot drift again.
Then git push -u origin chore/1372-agents-skill-parity, open the PR as a draft with Closes #1372 in the body, and use a conventional chore: title so the squash subject feeds the changelog backfill correctly.
Tests
Unit, test/repo-health/, the only applicable layer. New file test/repo-health/agent-skill-parity.test.mjs, as written in step 3, following the sibling naming in that directory (no-committed-symlinks.test.mjs, gitignore-webjs-depth.test.mjs). Run it with:
Then run the full suite once before pushing, since scripts/run-node-tests.js discovers the new file automatically:
npm test
Counterfactual, verified. The prototype of this test was executed against HEAD 00af2568 from a scratch directory. Four assertions passed and exactly two failed, naming the two real gaps:
After the fix, reverting any single piece must red it. Deleting one symlink fails the first assertion, pointing the target at an absolute path or at the wrong skill fails the second, removing a bullet from ## Custom Skills Usage fails the fourth, and adding a bullet for a skill with no symlink fails it from the other direction.
Layers that do NOT apply, and why. This change touches no framework runtime code. Nothing under packages/*/src or packages/cli/lib is staged, no export, route, or component changes, and no served byte differs.
Browser (npm run test:browser) does not apply: no hydration, DOM, slot, client-router, or custom-element behaviour is involved.
e2e (WEBJS_E2E=1) does not apply: no request path, navigation, or streaming behaviour changes.
Smoke (test/examples/*/smoke/*) does not apply: the in-repo apps are untouched and their boot path is unchanged.
Bun parity (test/bun/**) does not apply: none of the runtime-sensitive surfaces is touched (the serializer, the listener and request path, SSR, action, or CSRF dispatch, streams, node:crypto, the TS stripper, auth, session, or cors). .claude/hooks/require-bun-parity-with-runtime-src.sh:61 gates on staged packages/*/src or packages/cli/lib, so it will not fire and WEBJS_BUN_VERIFIED=1 is not needed.
Docs
.agents/rules/workflow.md (moved from .agents/AGENTS.md, step 2). Retitled, provenance sentence corrected, ## Custom Skills Usage grown from six entries to ten with a note on the enforced parity, and a new ## Enforcement gates section covering .hooks/pre-commit plus CI.
framework-dev.md (step 4). New section between lines 125 and 135 recording the canonical-plus-mirror layout, the ln -s mode-120000 requirement, the two non-mirror entries, and the settled reason hooks stay Claude-only.
Root AGENTS.md needs no change. Line 36 describes what the SCAFFOLD ships, not this repo's own .agents/ layout, and it already names .agents/rules/workflow.md, which this change brings the framework repo into line with rather than contradicting.
The docs site, the marketing website, README.md, CONVENTIONS.md, and the scaffold templates need no change. None of them describes the framework repo's internal agent-config mirror, and webjs create output is byte-identical (packages/cli/lib/create.js:575 resolves the workflow rules from TEMPLATES, and its only repo-root fallback, at lines 658-664, is for .agents/skills/webjs).
No escape hatch is needed.WEBJS_NO_DOC_GATE=1 must NOT be used. .claude/hooks/require-docs-with-src.sh:59-60 exits 0 unless the staged diff matches ^packages/([^/]+/src|editors/[^/]+/src|cli/lib)/, and this change stages only .agents/**, test/**, and framework-dev.md, so the gate never fires. The same is true of require-tests-with-src.sh:59, require-scaffold-with-src.sh:78, and require-bun-parity-with-runtime-src.sh:61.
Acceptance criteria
git ls-files -s .agents/skills shows exactly ten mode-120000 entries, one per committed .claude/skills/<name>.
Each new symlink stores the relative target ../../.claude/skills/<name>, and test -f .agents/skills/<name>/SKILL.md succeeds for all ten.
.agents/skills/webjs/ is still a tracked real directory with its 15 files, and .agents/skills/omarchy is still untracked.
.agents/AGENTS.md no longer exists; .agents/rules/workflow.md does, with git recording a rename.
.agents/rules/workflow.md lists all ten skills under ## Custom Skills Usage and carries the ## Enforcement gates section.
framework-dev.md documents the mirror layout, the ln -s requirement, the two non-mirror entries, and the Claude-only hooks decision.
node --test test/repo-health/agent-skill-parity.test.mjs passes, and reverting any single piece of the change reds it.
test/repo-health/no-committed-symlinks.test.mjs and test/hooks/route-skills.test.mjs still pass.
npm test is green, and CI is green on the PR.
No .agents/hooks.json and no scripts/sync-hooks.js were added, and no file under .claude/hooks/ was modified.
Out of scope
.agents/hooks.json and scripts/sync-hooks.js. Rejected on the merits above. Do not add either file.
Making .claude/hooks/*.sh dual-engine. Rejected with the hooks mirror, since a dual-engine script has no second engine to serve once there is no .agents/hooks.json. The described change (a jq fallback from .tool_input to .toolCall.args) would also be insufficient on its own, because nine of the eleven scripts block with exit 2 while Antigravity blocks on stdout JSON. Leave every file under .claude/hooks/ untouched.
Flipping to the Next.js layout (skills canonical under .agents/skills/, with a single .claude/skills -> ../.agents/skills directory symlink). Structurally stronger, since it makes a missing per-skill link impossible rather than merely detectable, but it means relocating ten committed skill directories, it pulls .agents/skills/webjs/ and the machine-local omarchy link into the Claude skill namespace, and it depends on the maintainer's per-skill ~/.claude/skills/<name> links, which this repo cannot verify. The parity test delivers the detection now at a fraction of the risk.
The rules file duplicating global rules..agents/rules/workflow.md restates commit cadence, branch guards, and worktree isolation that root AGENTS.md also carries. That overlap predates this issue and de-duplicating it is a content decision about what a non-Claude agent needs loaded when it may not read the root contract, which is a different question from whether the skill list is correct. Change only the sections named in step 2.
Stale scaffold prose left by feat(cli): remove redundant per-agent instruction files from webjs create templates #1368. Root AGENTS.md:36 still names GEMINI.md and .github/copilot-instructions.md as bridges the scaffold ships, and three blog posts still list .cursorrules and per-agent hook files, all of which 241c961b removed from the templates. Real drift, but it is scaffold documentation rather than the .agents/ mirror, and correcting it means touching published blog posts. Do not fold it in.
Adding, renaming, or editing any skill's content under .claude/skills/ or .agents/skills/webjs/. This change moves no skill text.
All line anchors below were verified against HEAD
00af2568("docs(skill): add explicit HTTP verb decision guide for server actions (#1369)") on 2026-08-10. Re-check an anchor before editing if HEAD has moved.Problem
The framework repo keeps its Claude Code skills at
.claude/skills/<name>/and mirrors them into.agents/skills/<name>as relative symlinks so Antigravity, which reads<workspace-root>/.agents/skills/<folder>/SKILL.md, sees the same set. That mirror has drifted in two independent directions, and a third claim in the original report needed correcting.Gap 1, missing symlinks (confirmed).
git ls-files .claude/skillslists ten committed skills, each with a committedSKILL.md:git ls-files -s .agents/skillslists only seven mode-120000entries (use-railway,webjs-blog-write,webjs-doc-sync,webjs-file-issue,webjs-list-todos,webjs-research-record,webjs-start-work). Missing, exactly as reported, arewebjs-ready-for-dev,webjs-scaffold-sync, andwebjs-instagram-post. All three targets exist and carry a committedSKILL.md, so the symlinks will resolve the moment they are added.Two entries under
.agents/skills/are legitimate non-symlinks and must survive untouched..agents/skills/webjs/is a real committed directory (the framework teaching skill,SKILL.mdplusreferences/, 15 tracked files) that has no.claude/skillscounterpart and is the copy source inscripts/sync-scaffold-skill.mjs:21..agents/skills/omarchyis a machine-local absolute symlink into~/.local/share, deliberately untracked via.gitignore:98, andtest/repo-health/no-committed-symlinks.test.mjsexists to keep links of that shape out of the index.Gap 2, the documentation list (confirmed, and the drift is a different set from gap 1).
.agents/AGENTS.md:48-56lists six skills under## Custom Skills Usage, omitting four:webjs-blog-write,webjs-ready-for-dev,webjs-scaffold-sync, andwebjs-instagram-post. Notewebjs-blog-writealready HAS a symlink and is simply undocumented, so the symlink set and the documented set have drifted apart independently. Six listed plus four missing is ten, which matches the committed skill count.Correction to the original report, part 1 of 2. The original body treated
.agents/hooks.jsonas an obvious missing mirror of.claude/settings.json. The file is real (Antigravity readshooks.jsonfrom the workspace.agents/directory, docs), so the premise is not invented, but the conclusion does not follow. See the design section: part 2 is rejected on the merits, and the sub-item about making.claude/hooks/*.shdual-engine is rejected with it.Correction to the original report, part 2 of 2, and a real defect the report missed.
.agents/AGENTS.mdis at a path nothing documents reading. Antigravity's rules reference states that "Workspace rules live in the.agents/rulesfolder of your workspace or git root" (docs), withGEMINI.mdand a repo-rootAGENTS.mdas the other project-level surfaces..agents/AGENTS.mdis none of those. The repo already knows the correct path and says so in its own published prose:blog/ai-first-is-plumbing.md:25andblog/stop-ai-agents-breaking-your-code.md:25both label.agents/rules/workflow.mdas the Antigravity workspace-rules file, rootAGENTS.md:36names.agents/rules/workflow.mdas part of the single cross-agent source, and the scaffold ships exactly that path (packages/cli/templates/.agents/rules/workflow.md, copied bypackages/cli/lib/create.js:575). The framework repo itself has no.agents/rules/directory at all. A grep across the repo finds zero references to.agents/AGENTS.mdfrom any test, script, workflow, or doc, so nothing depends on the current location. Adding four bullets to a file no tool loads would be a no-op, so the fix has to move the file as well as edit it.Why this keeps reopening. Nothing asserts the three sets agree.
test/hooks/route-skills.test.mjs:233-252is the closest existing guard, and it only asserts that each project skill named inside.claude/hooks/route-skills.shhas a committed.claude/skills/<name>/SKILL.md. That test passes at HEAD, because the router already references all ten names, and it says nothing about.agents/skillsor about the documented list. Without a guard, the eleventh skill reopens the same gap.Design / approach
Three settled decisions.
1. Reject
.agents/hooks.jsonand thescripts/sync-hooks.jsgenerator.agents/hooks.jsonis a genuine Antigravity surface, so the rejection is on cost and doctrine rather than on the file being fictional. Four reasons, in order of weight.A mirror is a protocol port, not a field rename. The original sub-item framed it as "a
jqfallback for.tool_inputversus.toolCall.args". The two engines disagree on three axes, and the field paths are the least of them.exit 2with the reason on stderr{"decision":"deny","reason":"..."}, exit code is not the gatehookSpecificOutput.additionalContextonUserPromptSubmit{"injectSteps":[{"ephemeralMessage":"..."}]}onPreInvocationhooksmap keyed by event{"<hook-name>": {"enabled": true, "PreToolUse": [...]}}Write|Edit|MultiEdit|NotebookEditwrite_to_file|replace_file_content|multi_replace_file_content.tool_input.file_path,.tool_input.notebook_path.toolCall.args.TargetFile.tool_input.content,.new_string,.new_source,.edits[].new_string.CodeContent,.ReplacementContent,.ReplacementChunks[](element shape undocumented).tool_input.command.toolCall.args.CommandLineNine of the eleven scripts in
.claude/hooks/block withexit 2and would each need a second output path, androute-skills.shwould need its whole output rewritten rather than re-keyed.block-prose-punctuation.shreads five distinct content fields (.claude/hooks/block-prose-punctuation.sh:43-48); its faithful Antigravity counterpart depends on theReplacementChunkselement shape, which the public docs do not specify, so part of the port cannot be written correctly from documentation alone.A generated second copy of a rule source is what this repo argues against. Root
AGENTS.md:36states the doctrine outright: tools that do not readAGENTS.mdnatively get "a THIN bridge pointing at it ..., never a duplicated rule set", and the same line records that "Claude Code additionally ships the protective enforcement hooks (.claude/,.hooks/pre-commit)", so hooks being Claude-only here is a stated position rather than an oversight. Ascripts/sync-hooks.jstranslator would need a hardcoded matcher, vocabulary, and protocol map, which means the map IS the configuration and generating a file from it removes no hand-written work. The output would also have to be committed, because Antigravity reads it off disk, which is precisely the committed-generated-duplicate the repo avoids elsewhere (the generated scaffold-skill bundle is gitignored at.gitignore:93and produced at prepack byscripts/sync-scaffold-skill.mjs).The repo moved the other way one commit ago.
241c961b(#1368, HEAD~1) deleted.cursor/hooks.json,.cursor/hooks/nudge-uncommitted.sh,.gemini/settings.json,.gemini/hooks/nudge-uncommitted.sh,.opencode/plugins/nudge-uncommitted.ts,GEMINI.md,.cursorrules, and.github/copilot-instructions.mdfrom the scaffold templates, keeping.claude/,.hooks/pre-commit,AGENTS.md, andCLAUDE.md. Adding a per-agent hook mirror to the framework repo one commit later reverses a decision the project just made deliberately.Prior art agrees. Next.js, Remix, Astro, Svelte, and TanStack Router all keep skills under
.agents/skills/(verified in the local clones at~/Documents/Projects/frameworks/), and none of the five ships ahooks.jsonat any level. Next.js goes further and makes the mirror structural:next.js/.claudecontains a single symlink,skills -> ../.agents/skills, so a per-skill link can never go missing.What actually covers the underlying need, chosen alternative. Enforcement for non-Claude agents already exists at two tool-agnostic layers, and the correct fix is to make an Antigravity session aware of them rather than to build a parallel engine.
.hooks/pre-commitfires for every agent and every human on every commit, blocking a direct commit tomain/masterand blocking a published-library version bump off achore/release-*branch. Its own header at.hooks/pre-commit:12-13records the division of labour: "The test suite is NOT run here. CI (.github/workflows/ci.yml) is the test gate", and.github/workflows/ci.ymlrunsconventions(webjs checkpluswebjs doctorover blog and website),unit, and the Bun matrix, with branch protection blocking a merge until they pass. The ClaudePreToolUsegates are a faster local feedback loop over enforcement that already binds every agent, not the only enforcement. The residual gap is awareness, which is a prose fix and is in scope for a documentation issue, so the moved rules file gains a short "Enforcement gates" section naming those two layers, the never---no-verifyrule, and the fact that the.claude/hooks/*pre-tool gates do not fire outside Claude Code so the session must self-check the same rules.2. Move the Antigravity rules file to the documented path
.agents/AGENTS.mdbecomes.agents/rules/workflow.md, matching the documented Antigravity workspace-rules folder, the scaffold's own filename, and the path rootAGENTS.md:36already advertises. This is agit mvplus a contained content edit, not a rewrite. It cannot affectwebjs create, becausepackages/cli/lib/create.js:575resolves.agents/rules/workflow.mdfromTEMPLATESonly, and the sole repo-root fallback in that file is for the skill directory (packages/cli/lib/create.js:658-664). Nothing else in the repo references either path.One accepted consequence:
.claude/hooks/require-docs-with-src.sh:73-74counts a doc surface with the pattern(^|/)(AGENTS|CLAUDE|CONVENTIONS|README)\.md$, which.agents/AGENTS.mdcurrently satisfies incidentally and.agents/rules/workflow.mdwill not. That gate only fires whenpackages/*/srcorpackages/cli/libis staged, and staging the Antigravity rules file was never a sensible way to satisfy a framework-source doc requirement, so the loss is not worth widening the gate's regex for.3. Add a parity guard, as a new sibling in
test/repo-health/Three symlinks went missing once and the documented list has drifted separately, so detection has to be automatic. The new test goes in
test/repo-health/, nottest/hooks/, because the invariant is about repo structure rather than hook behaviour, and because the closest precedent lives there:test/repo-health/no-committed-symlinks.test.mjs:31-55already readsgit ls-files -s, filters mode120000, and resolves each link target withgit cat-file -p. The new file reuses that exact technique.test/hooks/route-skills.test.mjsis left alone, since its subject is the router hook and it already passes.Reading the sets from
git ls-filesrather than fromfs.readdirSynchandles the machine-local exception for free:.agents/skills/omarchyis untracked, so git never reports it and no name needs special-casing..agents/skills/webjsneeds one explicit allowance, because it is a tracked real directory with no.claude/skillscounterpart.The Next.js single-directory-symlink inversion is the structurally stronger design and is recorded under Out of scope with its blockers, rather than smuggled into a chore issue.
Implementation plan
Step 0. Cut the worktree
git worktree add -b chore/1372-agents-skill-parity ../webjs-1372-agents-skill-parity origin/main cd ../webjs-1372-agents-skill-parityDo all work there. Tracked-file edits in the primary checkout are blocked by
.claude/hooks/require-worktree-for-edits.sh. A fresh worktree has nonode_modules, so runnpm run worktree:linkfrom inside it before running any test.Step 1. Add the three missing symlinks
The one mechanical trap is getting git to record mode
120000instead of a regular file.ln -splusgit adddoes this correctly; writing a text file whose contents are the target path does not. Create them relative to.agents/skills/, so the stored target string is exactly../../.claude/skills/<name>.Verify the mode and the resolution before committing. Every line must start with
120000, and every target must resolve to a readableSKILL.md:Do not use an absolute target.
test/repo-health/no-committed-symlinks.test.mjsfails any tracked symlink whose target is absolute or resolves outside the repo root.Step 2. Move the Antigravity rules file
Then edit
.agents/rules/workflow.md. Three contained changes.2a. Retitle and correct the provenance sentence. Lines 1 and 3 read today:
They should read:
2b. Replace the six-skill list with all ten. Lines 48-56 read today:
They should read:
Keep the bullet order stable: it groups the workflow skills, then the authoring skills, then infrastructure.
2c. Append an enforcement section at the end of the file, after the skills section. This is the substantive answer to the rejected hooks mirror:
Step 3. Add the parity guard
New file,
test/repo-health/agent-skill-parity.test.mjs. It is picked up automatically:scripts/run-node-tests.jswalkstest/recursively for.test.mjs. This body was run against HEAD from a scratch directory and reds on exactly the two real gaps, passing its other four assertions.Step 4. Document the repo's own agent-config layout
Add a section to
framework-dev.md, after### Merged worktrees are auto-removed (cleanup-merged-worktree.sh)at line 125 and before### Scaffold teaching-coverage gateat line 135, so the two agent-tooling sections sit together:Step 5. Commit and push
One logical unit, so one commit:
Then
git push -u origin chore/1372-agents-skill-parity, open the PR as a draft withCloses #1372in the body, and use a conventionalchore:title so the squash subject feeds the changelog backfill correctly.Tests
Unit,
test/repo-health/, the only applicable layer. New filetest/repo-health/agent-skill-parity.test.mjs, as written in step 3, following the sibling naming in that directory (no-committed-symlinks.test.mjs,gitignore-webjs-depth.test.mjs). Run it with:Then run the full suite once before pushing, since
scripts/run-node-tests.jsdiscovers the new file automatically:npm testCounterfactual, verified. The prototype of this test was executed against HEAD
00af2568from a scratch directory. Four assertions passed and exactly two failed, naming the two real gaps:After the fix, reverting any single piece must red it. Deleting one symlink fails the first assertion, pointing the target at an absolute path or at the wrong skill fails the second, removing a bullet from
## Custom Skills Usagefails the fourth, and adding a bullet for a skill with no symlink fails it from the other direction.Layers that do NOT apply, and why. This change touches no framework runtime code. Nothing under
packages/*/srcorpackages/cli/libis staged, no export, route, or component changes, and no served byte differs.npm run test:browser) does not apply: no hydration, DOM, slot, client-router, or custom-element behaviour is involved.WEBJS_E2E=1) does not apply: no request path, navigation, or streaming behaviour changes.test/examples/*/smoke/*) does not apply: the in-repo apps are untouched and their boot path is unchanged.test/bun/**) does not apply: none of the runtime-sensitive surfaces is touched (the serializer, the listener and request path, SSR, action, or CSRF dispatch, streams,node:crypto, the TS stripper, auth, session, or cors)..claude/hooks/require-bun-parity-with-runtime-src.sh:61gates on stagedpackages/*/srcorpackages/cli/lib, so it will not fire andWEBJS_BUN_VERIFIED=1is not needed.Docs
.agents/rules/workflow.md(moved from.agents/AGENTS.md, step 2). Retitled, provenance sentence corrected,## Custom Skills Usagegrown from six entries to ten with a note on the enforced parity, and a new## Enforcement gatessection covering.hooks/pre-commitplus CI.framework-dev.md(step 4). New section between lines 125 and 135 recording the canonical-plus-mirror layout, theln -smode-120000requirement, the two non-mirror entries, and the settled reason hooks stay Claude-only.AGENTS.mdneeds no change. Line 36 describes what the SCAFFOLD ships, not this repo's own.agents/layout, and it already names.agents/rules/workflow.md, which this change brings the framework repo into line with rather than contradicting.README.md,CONVENTIONS.md, and the scaffold templates need no change. None of them describes the framework repo's internal agent-config mirror, andwebjs createoutput is byte-identical (packages/cli/lib/create.js:575resolves the workflow rules fromTEMPLATES, and its only repo-root fallback, at lines 658-664, is for.agents/skills/webjs).WEBJS_NO_DOC_GATE=1must NOT be used..claude/hooks/require-docs-with-src.sh:59-60exits 0 unless the staged diff matches^packages/([^/]+/src|editors/[^/]+/src|cli/lib)/, and this change stages only.agents/**,test/**, andframework-dev.md, so the gate never fires. The same is true ofrequire-tests-with-src.sh:59,require-scaffold-with-src.sh:78, andrequire-bun-parity-with-runtime-src.sh:61.Acceptance criteria
git ls-files -s .agents/skillsshows exactly ten mode-120000entries, one per committed.claude/skills/<name>.../../.claude/skills/<name>, andtest -f .agents/skills/<name>/SKILL.mdsucceeds for all ten..agents/skills/webjs/is still a tracked real directory with its 15 files, and.agents/skills/omarchyis still untracked..agents/AGENTS.mdno longer exists;.agents/rules/workflow.mddoes, with git recording a rename..agents/rules/workflow.mdlists all ten skills under## Custom Skills Usageand carries the## Enforcement gatessection.framework-dev.mddocuments the mirror layout, theln -srequirement, the two non-mirror entries, and the Claude-only hooks decision.node --test test/repo-health/agent-skill-parity.test.mjspasses, and reverting any single piece of the change reds it.test/repo-health/no-committed-symlinks.test.mjsandtest/hooks/route-skills.test.mjsstill pass.npm testis green, and CI is green on the PR..agents/hooks.jsonand noscripts/sync-hooks.jswere added, and no file under.claude/hooks/was modified.Out of scope
.agents/hooks.jsonandscripts/sync-hooks.js. Rejected on the merits above. Do not add either file..claude/hooks/*.shdual-engine. Rejected with the hooks mirror, since a dual-engine script has no second engine to serve once there is no.agents/hooks.json. The described change (ajqfallback from.tool_inputto.toolCall.args) would also be insufficient on its own, because nine of the eleven scripts block withexit 2while Antigravity blocks on stdout JSON. Leave every file under.claude/hooks/untouched..agents/skills/, with a single.claude/skills -> ../.agents/skillsdirectory symlink). Structurally stronger, since it makes a missing per-skill link impossible rather than merely detectable, but it means relocating ten committed skill directories, it pulls.agents/skills/webjs/and the machine-localomarchylink into the Claude skill namespace, and it depends on the maintainer's per-skill~/.claude/skills/<name>links, which this repo cannot verify. The parity test delivers the detection now at a fraction of the risk..agents/rules/workflow.mdrestates commit cadence, branch guards, and worktree isolation that rootAGENTS.mdalso carries. That overlap predates this issue and de-duplicating it is a content decision about what a non-Claude agent needs loaded when it may not read the root contract, which is a different question from whether the skill list is correct. Change only the sections named in step 2.AGENTS.md:36still namesGEMINI.mdand.github/copilot-instructions.mdas bridges the scaffold ships, and three blog posts still list.cursorrulesand per-agent hook files, all of which241c961bremoved from the templates. Real drift, but it is scaffold documentation rather than the.agents/mirror, and correcting it means touching published blog posts. Do not fold it in..claude/skills/or.agents/skills/webjs/. This change moves no skill text.