diff --git a/packages/cli/AGENTS.md b/packages/cli/AGENTS.md index 30cd5a8f5..92c099808 100644 --- a/packages/cli/AGENTS.md +++ b/packages/cli/AGENTS.md @@ -237,8 +237,7 @@ verbatim. Apps must NEVER use JSON files for persistence. This is a project convention (documented in the scaffold's CONVENTIONS.md). 4. **Template files are verbatim copies** with `{{APP_NAME}}` substitution. - When editing `templates/AGENTS.md`, `templates/CLAUDE.md`, - `templates/CONVENTIONS.md`, `.cursorrules`, etc., remember they ship + When editing `templates/AGENTS.md`, `templates/CONVENTIONS.md`, or `.agents/skills/webjs/`, remember they ship into every scaffolded app. Write for the audience of an AI agent working inside a freshly-scaffolded WebJs project. `templates/AGENTS.md` is the one exception to "verbatim": it carries a `{{PLAYBOOK}}` marker diff --git a/packages/cli/README.md b/packages/cli/README.md index d9671e8b9..d02535236 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -68,8 +68,8 @@ the CLI gives you `webjs ui` automatically. See The scaffold seeds opinionated defaults so AI agents produce consistent code: -- `AGENTS.md` + `CONVENTIONS.md` (the machine-readable contract) -- `.claude/`, `.cursorrules`, `.agents/rules/workflow.md` (Antigravity), `.github/copilot-instructions.md` +- `AGENTS.md` + `CONVENTIONS.md` + `.agents/skills/webjs/` (single cross-agent source of truth) +- `.agents/rules/workflow.md` & `.claude/` protective hooks - `test//` (with optional `browser/` / `e2e/` subfolders per kind) with example tests - Tailwind CSS via CLI (no browser runtime at build time) - TypeScript, `.editorconfig`, `.gitignore` diff --git a/packages/cli/lib/create.js b/packages/cli/lib/create.js index a00b466b0..9523b8c36 100644 --- a/packages/cli/lib/create.js +++ b/packages/cli/lib/create.js @@ -568,26 +568,12 @@ export async function scaffoldApp(name, cwd, opts = {}) { // --- Templates (AGENTS.md, CONVENTIONS.md, CLAUDE.md, test files, Claude hooks) --- const templateFiles = [ - // Single cross-agent source: a thin AGENTS.md points at the skill; the + // Single cross-agent source: AGENTS.md points at .agents/skills/webjs/; the // .agents/rules workflow rules and the Claude enforcement hooks back it up. 'AGENTS.md', 'CONVENTIONS.md', '.agents/rules/workflow.md', - // Per-agent files. Content is single-source (AGENTS.md + the skill); these - // are thin bridges plus each tool's own config and commit-nudge hook. Claude - // Code (CLAUDE.md @-imports AGENTS.md), Gemini CLI (GEMINI.md), Copilot in VS - // Code (copilot-instructions.md). Cursor / opencode / Antigravity read - // AGENTS.md natively; Cursor also gets a .cursorrules bridge, and each of - // Cursor / Gemini / opencode ships a "commit often" nudge hook. 'CLAUDE.md', - 'GEMINI.md', - '.github/copilot-instructions.md', - '.cursorrules', - '.cursor/hooks.json', - '.cursor/hooks/nudge-uncommitted.sh', - '.gemini/settings.json', - '.gemini/hooks/nudge-uncommitted.sh', - '.opencode/plugins/nudge-uncommitted.ts', // Claude Code config + the protective enforcement hooks (no design ceremony). '.claude.json', '.claude/settings.json', @@ -629,7 +615,7 @@ export async function scaffoldApp(name, cwd, opts = {}) { // rewrites; the three infra files get their file-specific transform. On Node, // every file is copied byte-identical (the map is empty). const PROSE_REWRITE = new Set([ - 'AGENTS.md', 'CLAUDE.md', 'CONVENTIONS.md', '.cursorrules', + 'AGENTS.md', 'CLAUDE.md', 'CONVENTIONS.md', '.agents/rules/workflow.md', 'test/hello/browser/hello.test.js', 'test/hello/e2e/hello.test.ts', ]); diff --git a/packages/cli/templates/.cursor/hooks.json b/packages/cli/templates/.cursor/hooks.json deleted file mode 100644 index b948d2fc8..000000000 --- a/packages/cli/templates/.cursor/hooks.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "version": 1, - "hooks": { - "afterFileEdit": [ - { "command": ".cursor/hooks/nudge-uncommitted.sh" } - ] - } -} diff --git a/packages/cli/templates/.cursor/hooks/nudge-uncommitted.sh b/packages/cli/templates/.cursor/hooks/nudge-uncommitted.sh deleted file mode 100644 index 6d0ee906a..000000000 --- a/packages/cli/templates/.cursor/hooks/nudge-uncommitted.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -# -# Cursor afterFileEdit hook. -# -# Counterpart of .claude/hooks/nudge-uncommitted.sh. After each -# file edit, counts uncommitted changes in the working tree. -# When the count crosses a threshold (default 4, override with -# WEBJS_COMMIT_NUDGE_THRESHOLD), injects a reminder via the -# top-level additional_context field (snake_case, unlike Claude -# Code's nested hookSpecificOutput.additionalContext). -# -# Soft nudge. Exit 0 always. Skipped on main/master and outside -# a git work tree. - -set -e - -THRESHOLD="${WEBJS_COMMIT_NUDGE_THRESHOLD:-4}" - -if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then - exit 0 -fi - -BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "") -if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then - exit 0 -fi - -cat /dev/stdin >/dev/null 2>&1 || true - -CHANGED=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ') - -if [ -z "$CHANGED" ] || [ "$CHANGED" -lt "$THRESHOLD" ]; then - exit 0 -fi - -REASON="You have ${CHANGED} uncommitted changes on '${BRANCH}'. The webjs convention is small, focused commits per logical unit (one feature, one fix, one rename, one doc rewrite). Before continuing with more edits, group the current changes into a meaningful commit. See AGENTS.md \"Git workflow\" for the rule and the rationale. To raise the threshold for this hook in long-running tasks, set WEBJS_COMMIT_NUDGE_THRESHOLD." - -jq -n --arg ctx "$REASON" '{ additional_context: $ctx }' diff --git a/packages/cli/templates/.cursorrules b/packages/cli/templates/.cursorrules deleted file mode 100644 index f9406fead..000000000 --- a/packages/cli/templates/.cursorrules +++ /dev/null @@ -1,21 +0,0 @@ -# WebJs app rules (Cursor) - -Cursor reads `AGENTS.md` natively. This file points you at it and the agent -skill, and carries the commit rule. - -- **Read `AGENTS.md` first, then `.agents/skills/webjs/SKILL.md`** (the guide to - building a WebJs app; it routes to focused references under - `.agents/skills/webjs/references/`). These are required context, not optional - reading: WebJs is not React, Next, or Lit, so gather this context before you - write code. -- **Study the shipped examples, then clear them and build.** The scaffold ships - a browsable showcase to learn the real idioms from (a full-stack app ships a - UI feature gallery, the api template ships a backend-features showcase). Read - the parts that match your task, run `npm run gallery:clear` to shed the - showcase and reset to a clean base, then grow the app in place: add routes - under `app/`, features under `modules//`, and keep server-only code - behind `.server.ts`. `AGENTS.md` carries the full template-specific playbook. -- **Use the wired-up database (Drizzle)** for persistence. Never a JSON file, an - in-memory array, or localStorage. -- **Commit per logical unit** as soon as it is complete, and never commit to - `main`. diff --git a/packages/cli/templates/.gemini/hooks/nudge-uncommitted.sh b/packages/cli/templates/.gemini/hooks/nudge-uncommitted.sh deleted file mode 100644 index 73432034a..000000000 --- a/packages/cli/templates/.gemini/hooks/nudge-uncommitted.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -# -# Gemini CLI AfterTool hook. -# -# Counterpart of .claude/hooks/nudge-uncommitted.sh. After each -# write_file or replace, counts uncommitted changes in the working -# tree. When the count crosses a threshold (default 4, override -# with the WEBJS_COMMIT_NUDGE_THRESHOLD env var), injects a -# reminder via hookSpecificOutput.additionalContext (same shape -# as Claude Code). -# -# Soft nudge. Does NOT block the edit (exit 0). Skipped on -# main/master and outside a git work tree. - -set -e - -THRESHOLD="${WEBJS_COMMIT_NUDGE_THRESHOLD:-4}" - -if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then - exit 0 -fi - -BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "") -if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then - exit 0 -fi - -cat /dev/stdin >/dev/null 2>&1 || true - -CHANGED=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ') - -if [ -z "$CHANGED" ] || [ "$CHANGED" -lt "$THRESHOLD" ]; then - exit 0 -fi - -REASON="You have ${CHANGED} uncommitted changes on '${BRANCH}'. The webjs convention is small, focused commits per logical unit (one feature, one fix, one rename, one doc rewrite). Before continuing with more edits, group the current changes into a meaningful commit. See AGENTS.md \"Git workflow\" for the rule and the rationale. To raise the threshold for this hook in long-running tasks, set WEBJS_COMMIT_NUDGE_THRESHOLD." - -jq -n --arg ctx "$REASON" '{ - hookSpecificOutput: { - additionalContext: $ctx - } -}' diff --git a/packages/cli/templates/.gemini/settings.json b/packages/cli/templates/.gemini/settings.json deleted file mode 100644 index 4c729398c..000000000 --- a/packages/cli/templates/.gemini/settings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "hooks": { - "AfterTool": [ - { - "matcher": "write_file|replace", - "hooks": [ - { - "type": "command", - "command": ".gemini/hooks/nudge-uncommitted.sh" - } - ] - } - ] - } -} diff --git a/packages/cli/templates/.github/copilot-instructions.md b/packages/cli/templates/.github/copilot-instructions.md deleted file mode 100644 index cb1d69c16..000000000 --- a/packages/cli/templates/.github/copilot-instructions.md +++ /dev/null @@ -1,9 +0,0 @@ -# Copilot instructions - -This is a thin bridge to the single source. GitHub Copilot always reads this -file; in VS Code it reads `AGENTS.md` directly only when `chat.useAgentsMdFile` -is enabled, so this bridge keeps Copilot pointed at the instructions regardless. - -The instructions for this app live in `AGENTS.md` (the cross-agent source) and -the skill at `.agents/skills/webjs/SKILL.md`. Read `AGENTS.md` first, then the -skill (it routes to focused references on demand). diff --git a/packages/cli/templates/.opencode/plugins/nudge-uncommitted.ts b/packages/cli/templates/.opencode/plugins/nudge-uncommitted.ts deleted file mode 100644 index 1e8259479..000000000 --- a/packages/cli/templates/.opencode/plugins/nudge-uncommitted.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * OpenCode commit-frequency nudge plugin. - * - * Counterpart of the Claude Code, Gemini CLI, and Cursor hooks in - * `.claude/hooks/`, `.gemini/hooks/`, and `.cursor/hooks/`. After - * each edit/write tool call, counts uncommitted changes in the - * working tree. When the count crosses a threshold (default 4, - * override with the WEBJS_COMMIT_NUDGE_THRESHOLD env var), appends - * a reminder to the tool result so the agent sees it on the next - * turn. - * - * Soft nudge by design. Does NOT block the edit. The goal is to - * keep the agent honest about the "commit per logical unit" rule, - * not to interrupt valid work. - * - * Skipped on main/master (different guard rules cover that) and - * outside a git work tree. - * - * Auto-discovered by OpenCode at startup. No opencode.json entry - * needed. Lives in .opencode/plugins/ at the project root. - * - * Docs: https://opencode.ai/docs/plugins/ - */ -import type { Plugin } from "@opencode-ai/plugin"; - -export const NudgeUncommitted: Plugin = async ({ $ }) => { - const THRESHOLD = Number(process.env.WEBJS_COMMIT_NUDGE_THRESHOLD ?? 4); - - return { - "tool.execute.after": async (input, output) => { - if (input.tool !== "edit" && input.tool !== "write") return; - - let branch = ""; - try { - branch = (await $`git symbolic-ref --short HEAD`.text()).trim(); - } catch { - return; // not in a git work tree - } - if (branch === "main" || branch === "master") return; - - let changed = 0; - try { - const out = (await $`git status --porcelain`.text()).trim(); - changed = out === "" ? 0 : out.split("\n").length; - } catch { - return; - } - if (changed < THRESHOLD) return; - - const reason = - `[webjs] You have ${changed} uncommitted changes on '${branch}'. ` + - `The webjs convention is small, focused commits per logical unit ` + - `(one feature, one fix, one rename, one doc rewrite). Before ` + - `continuing with more edits, group the current changes into a ` + - `meaningful commit. See AGENTS.md "Git workflow" for the rule ` + - `and the rationale. To raise the threshold for this hook in ` + - `long-running tasks, set WEBJS_COMMIT_NUDGE_THRESHOLD.`; - - output.output = output.output ? `${output.output}\n\n${reason}` : reason; - }, - }; -}; diff --git a/packages/cli/templates/GEMINI.md b/packages/cli/templates/GEMINI.md deleted file mode 100644 index d798a4a25..000000000 --- a/packages/cli/templates/GEMINI.md +++ /dev/null @@ -1,11 +0,0 @@ -# GEMINI.md - -Gemini CLI reads `GEMINI.md`, not `AGENTS.md`, by default, so this file is a -thin bridge to the single source. - -The instructions for this app live in `AGENTS.md` (the cross-agent source) and -the skill at `.agents/skills/webjs/SKILL.md`. Read `AGENTS.md` first, then the -skill (it routes to focused references on demand). - -To have Gemini read `AGENTS.md` directly instead of this bridge, add it to -`context.fileName` in `.gemini/settings.json`. diff --git a/test/scaffolds/scaffold-integration.test.js b/test/scaffolds/scaffold-integration.test.js index d23af3ac2..0a20fba09 100644 --- a/test/scaffolds/scaffold-integration.test.js +++ b/test/scaffolds/scaffold-integration.test.js @@ -87,19 +87,16 @@ test('scaffoldApp full-stack: writes the canonical full-stack app layout', async assert.ok(existsSync(join(appDir, 'tsconfig.json'))); // Single cross-agent source (AGENTS.md + the one skill + the .agents workflow - // rules), with a per-agent file for each tool: thin bridges for the ones that - // do not read AGENTS.md natively (CLAUDE.md, GEMINI.md, copilot), a .cursorrules - // bridge, CONVENTIONS.md, and a commit-nudge hook for Cursor / Gemini / opencode. - for (const f of ['AGENTS.md', '.agents/skills/webjs/SKILL.md', '.agents/rules/workflow.md', 'CLAUDE.md', 'GEMINI.md', '.github/copilot-instructions.md', 'CONVENTIONS.md', '.cursorrules', '.cursor/hooks/nudge-uncommitted.sh', '.gemini/hooks/nudge-uncommitted.sh', '.opencode/plugins/nudge-uncommitted.ts', '.claude/settings.json', '.editorconfig']) { + // rules), CONVENTIONS.md, CLAUDE.md bridge, and Claude protective hooks. + for (const f of ['AGENTS.md', '.agents/skills/webjs/SKILL.md', '.agents/rules/workflow.md', 'CLAUDE.md', 'CONVENTIONS.md', '.claude/settings.json', '.editorconfig']) { assert.ok(existsSync(join(appDir, f)), `${f} should exist`); } - // No design-distinctness ceremony (retired: gallery:clear does the reset job). - for (const f of ['LAYOUT-REFERENCE.md', '.claude/hooks/design-review-before-stop.sh', '.claude/skills/webjs-design-review']) { + // Per-agent files and design-distinctness ceremony removed. + for (const f of ['GEMINI.md', '.github/copilot-instructions.md', '.cursorrules', 'LAYOUT-REFERENCE.md', '.claude/hooks/design-review-before-stop.sh', '.claude/skills/webjs-design-review']) { assert.ok(!existsSync(join(appDir, f)), `${f} should NOT exist in the scaffold`); } - // The rule bridges are THIN (pointers to AGENTS.md / the skill), not full - // duplicates. .cursorrules + CONVENTIONS.md point at the skill. - for (const f of ['CLAUDE.md', 'GEMINI.md', '.github/copilot-instructions.md', '.cursorrules', 'CONVENTIONS.md']) { + // Thin bridges (pointers to AGENTS.md / the skill). + for (const f of ['CLAUDE.md', 'CONVENTIONS.md']) { const src = readFileSync(join(appDir, f), 'utf8'); assert.ok(src.length < 2200, `${f} is a thin bridge, not a full rule duplicate`); assert.match(src, /AGENTS\.md|\.agents\/skills\/webjs/, `${f} points at AGENTS.md or the skill`); @@ -781,8 +778,7 @@ test('scaffoldApp: AGENTS.md build playbook is template-specific (#1076)', async // and they must acknowledge the api showcase rather than only the UI gallery. const apiConv = readFileSync(join(cwd, 'api-app', 'CONVENTIONS.md'), 'utf8'); const apiFlow = readFileSync(join(cwd, 'api-app', '.agents/rules/workflow.md'), 'utf8'); - const apiCursor = readFileSync(join(cwd, 'api-app', '.cursorrules'), 'utf8'); - for (const [label, md] of [['CONVENTIONS.md', apiConv], ['workflow.md', apiFlow], ['.cursorrules', apiCursor]]) { + for (const [label, md] of [['CONVENTIONS.md', apiConv], ['workflow.md', apiFlow]]) { assert.doesNotMatch(md, /only while exploring|do not have to read|only (if|when) a task needs/i, `api ${label}: no opt-out phrasing`); }