diff --git a/src/generators/claude-code/__tests__/claude-code.test.ts b/src/generators/claude-code/__tests__/claude-code.test.ts index 891677c..5e19103 100644 --- a/src/generators/claude-code/__tests__/claude-code.test.ts +++ b/src/generators/claude-code/__tests__/claude-code.test.ts @@ -56,9 +56,6 @@ describe("ClaudeCodeGenerator", () => { it("generates hooks directory with scripts", async () => { await generateClaudeCode(tmpDir, baseConfig, baseVersions); - expect( - await fs.pathExists(path.join(tmpDir, ".claude", "hooks", "pre-bash.sh")), - ).toBe(true); expect( await fs.pathExists( path.join(tmpDir, ".claude", "hooks", "session-start.sh"), @@ -69,7 +66,7 @@ describe("ClaudeCodeGenerator", () => { it("makes hook scripts executable", async () => { await generateClaudeCode(tmpDir, baseConfig, baseVersions); const stat = await fs.stat( - path.join(tmpDir, ".claude", "hooks", "pre-bash.sh"), + path.join(tmpDir, ".claude", "hooks", "session-start.sh"), ); expect(stat.mode & 0o100).toBeTruthy(); }); @@ -102,16 +99,13 @@ describe("ClaudeCodeGenerator", () => { ).toBe(true); }); - it("settings.json contains hooks configuration", async () => { + it("settings.json contains a hooks object", async () => { await generateClaudeCode(tmpDir, baseConfig, baseVersions); const settings = await fs.readJson( path.join(tmpDir, ".claude", "settings.json"), ); expect(settings.hooks).toBeDefined(); - // PreToolUse is always present (project-specific pre-bash guards) - expect(settings.hooks.PreToolUse).toBeDefined(); - // In test environment (no parent ~/.claude/settings.json), SessionStart and PreCompact should be included - expect(Object.keys(settings.hooks).length).toBeGreaterThanOrEqual(1); + expect(typeof settings.hooks).toBe("object"); }); it("generates angular skill when frontend is enabled", async () => { @@ -371,98 +365,85 @@ describe("ClaudeCodeGenerator", () => { expect(content).not.toContain("## Workflow Mode: vibe"); }); - it("CLAUDE.md contains ## Claude Settings block when workflowMode is speckit", async () => { + it("CLAUDE.md never contains a Claude Settings table (settings.json is the SSOT)", async () => { + for (const mode of ["speckit", "vibe", "none"] as const) { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "forgekit-test-")); + await generateClaudeCode( + dir, + { + ...baseConfig, + workflowMode: mode, + speckitPreset: mode === "speckit" ? ("balanced" as const) : null, + }, + baseVersions, + ); + const content = await fs.readFile(path.join(dir, "CLAUDE.md"), "utf-8"); + expect(content).not.toContain("## Claude Settings"); + expect(content).not.toContain("`speckit.tests`"); + expect(content).not.toContain("`git.strategy`"); + await fs.remove(dir); + } + }); + + it("settings.json reflects balanced preset values in speckit mode", async () => { const config = { ...baseConfig, workflowMode: "speckit" as const, speckitPreset: "balanced" as const, }; await generateClaudeCode(tmpDir, config, baseVersions); - const content = await fs.readFile(path.join(tmpDir, "CLAUDE.md"), "utf-8"); - expect(content).toContain("## Claude Settings"); - expect(content).toContain("| `speckit.tests` | `settings.json` | `true` |"); - expect(content).toContain("| `speckit.tdd` | `settings.json` | `false` |"); - expect(content).toContain( - "| `speckit.code-review` | `settings.json` | `true` |", - ); - expect(content).toContain( - "| `speckit.verification` | `settings.json` | `minimal` |", - ); - expect(content).toContain( - "| `speckit.plan-detail` | `settings.json` | `medium` |", - ); - - const settingsContent = await fs.readFile( + const settings = await fs.readJson( path.join(tmpDir, ".claude", "settings.json"), - "utf-8", ); - const settings = JSON.parse(settingsContent); expect(settings.git.strategy).toBe("pr-required"); expect(settings.speckit.tests).toBe(true); expect(settings.speckit.tdd).toBe(false); + expect(settings.speckit["code-review"]).toBe(true); + expect(settings.speckit.verification).toBe("minimal"); + expect(settings.speckit["plan-detail"]).toBe("medium"); }); - it("CLAUDE.md Speckit Config — rigorous preset", async () => { + it("settings.json reflects rigorous preset values", async () => { const config = { ...baseConfig, workflowMode: "speckit" as const, speckitPreset: "rigorous" as const, }; await generateClaudeCode(tmpDir, config, baseVersions); - const content = await fs.readFile(path.join(tmpDir, "CLAUDE.md"), "utf-8"); - expect(content).toContain("| `speckit.tdd` | `settings.json` | `true` |"); - expect(content).toContain( - "| `speckit.verification` | `settings.json` | `full` |", - ); - expect(content).toContain( - "| `speckit.plan-detail` | `settings.json` | `high` |", + const settings = await fs.readJson( + path.join(tmpDir, ".claude", "settings.json"), ); + expect(settings.speckit.tdd).toBe(true); + expect(settings.speckit.verification).toBe("full"); + expect(settings.speckit["plan-detail"]).toBe("high"); }); - it("CLAUDE.md Speckit Config — fast preset (skip-clarify: true, no fast-mode line)", async () => { + it("settings.json reflects fast preset values", async () => { const config = { ...baseConfig, workflowMode: "speckit" as const, speckitPreset: "fast" as const, }; await generateClaudeCode(tmpDir, config, baseVersions); - const content = await fs.readFile(path.join(tmpDir, "CLAUDE.md"), "utf-8"); - expect(content).toContain( - "| `speckit.skip-clarify` | `settings.json` | `true` |", - ); - expect(content).toContain( - "| `speckit.code-review` | `settings.json` | `false` |", + const settings = await fs.readJson( + path.join(tmpDir, ".claude", "settings.json"), ); + expect(settings.speckit["skip-clarify"]).toBe(true); + expect(settings.speckit["code-review"]).toBe(false); }); - it("CLAUDE.md Speckit Config — bare-metal preset", async () => { + it("settings.json reflects bare-metal preset values", async () => { const config = { ...baseConfig, workflowMode: "speckit" as const, speckitPreset: "bare-metal" as const, }; await generateClaudeCode(tmpDir, config, baseVersions); - const content = await fs.readFile(path.join(tmpDir, "CLAUDE.md"), "utf-8"); - expect(content).toContain( - "| `speckit.tests` | `settings.json` | `false` |", - ); - expect(content).toContain( - "| `speckit.verification` | `settings.json` | `skip` |", + const settings = await fs.readJson( + path.join(tmpDir, ".claude", "settings.json"), ); - }); - - it("CLAUDE.md does not contain ## Claude Settings when workflowMode is vibe", async () => { - const config = { ...baseConfig, workflowMode: "vibe" as const }; - await generateClaudeCode(tmpDir, config, baseVersions); - const content = await fs.readFile(path.join(tmpDir, "CLAUDE.md"), "utf-8"); - expect(content).not.toContain("`speckit.tests`"); - }); - - it("CLAUDE.md does not contain ## Claude Settings when workflowMode is none", async () => { - const config = { ...baseConfig, workflowMode: "none" as const }; - await generateClaudeCode(tmpDir, config, baseVersions); - const content = await fs.readFile(path.join(tmpDir, "CLAUDE.md"), "utf-8"); - expect(content).not.toContain("`speckit.tests`"); + expect(settings.speckit.tests).toBe(false); + expect(settings.speckit.verification).toBe("skip"); }); it("CLAUDE.md contains '## Workflow Mode: vibe' when workflowMode is vibe", async () => { @@ -488,7 +469,6 @@ describe("ClaudeCodeGenerator", () => { expect(content).toContain("Merge direct sur `master`"); expect(content).toContain("`commit-commands:commit`"); expect(content).not.toContain("`commit-commands:commit-push-pr`"); - expect(content).toContain("| `git.strategy` | `settings.json` | `no-pr` |"); }); it("speckit mode → settings.json git.strategy=pr-required and CLAUDE.md uses commit-push-pr skill", async () => { @@ -505,9 +485,6 @@ describe("ClaudeCodeGenerator", () => { const content = await fs.readFile(path.join(tmpDir, "CLAUDE.md"), "utf-8"); expect(content).toContain("PR obligatoire"); expect(content).toContain("`commit-commands:commit-push-pr`"); - expect(content).toContain( - "| `git.strategy` | `settings.json` | `pr-required` |", - ); }); it("none mode → settings.json git.strategy=pr-required (default)", async () => { diff --git a/src/generators/claude-code/index.ts b/src/generators/claude-code/index.ts index d5dd908..ce9ce95 100644 --- a/src/generators/claude-code/index.ts +++ b/src/generators/claude-code/index.ts @@ -171,12 +171,6 @@ class ClaudeCodeGenerator extends BaseGenerator { data, ), // Hook scripts (templated, then chmod) - renderAndWrite( - "claude-code/hooks/pre-bash.sh.hbs", - path.join(hooksDir, "pre-bash.sh"), - data, - { mode: 0o755 }, - ), renderAndWrite( "claude-code/hooks/session-start.sh.hbs", path.join(hooksDir, "session-start.sh"), diff --git a/src/templates/claude-code/CLAUDE.md.hbs b/src/templates/claude-code/CLAUDE.md.hbs index 9ecf3fb..2e5a1a2 100644 --- a/src/templates/claude-code/CLAUDE.md.hbs +++ b/src/templates/claude-code/CLAUDE.md.hbs @@ -19,26 +19,6 @@ - Commit skill: `commit-commands:commit-push-pr` {{/if}} -## Claude Settings - -| Clé | Fichier | Valeur | Description | -|-----|---------|--------|-------------| -| `git.strategy` | `settings.json` | `{{gitStrategy}}` | Stratégie git — lue par `vibe.workflow` step 7 | -{{#if workflowSpeckit}} -| `speckit.tests` | `settings.json` | `{{speckitTests}}` | Génération de tests | -| `speckit.tdd` | `settings.json` | `{{speckitTdd}}` | Mode RED→GREEN | -| `speckit.test-types` | `settings.json` | `{{speckitTestTypes}}` | Types de tests | -| `speckit.code-review` | `settings.json` | `{{speckitCodeReview}}` | Code review automatique | -| `speckit.security-review` | `settings.json` | `{{speckitSecurityReview}}` | Security review | -| `speckit.verification` | `settings.json` | `{{speckitVerification}}` | Niveau de vérification | -| `speckit.plan-detail` | `settings.json` | `{{speckitPlanDetail}}` | Détail du plan | -| `speckit.skip-clarify` | `settings.json` | `{{speckitSkipClarify}}` | Sauter sk.clarify | -| `speckit.fast-mode` | `settings.json` | `{{speckitFastMode}}` | Fast mode | -| `speckit.subagents` | `settings.json` | `true` | Utiliser subagent-driven-development | - -> `settings.local.json` overrides `settings.json` pour tous ces champs. -{{/if}} - ## Architecture Constitution Read `.specify/memory/constitution.md` before any architectural decision. It is auto-loaded at session start. diff --git a/src/templates/claude-code/hookify/block-dangerous-rm.local.md b/src/templates/claude-code/hookify/block-dangerous-rm.local.md index befa032..cef0aaa 100644 --- a/src/templates/claude-code/hookify/block-dangerous-rm.local.md +++ b/src/templates/claude-code/hookify/block-dangerous-rm.local.md @@ -6,6 +6,6 @@ pattern: rm\s+(-rf|-fr) action: block --- -⚠️ **Commande rm -rf détectée** +⚠️ **`rm -rf` detected** -Cette commande peut supprimer des fichiers de façon irréversible. Vérifie bien le chemin avant de continuer. +This command can irreversibly delete files. Double-check the path before proceeding. diff --git a/src/templates/claude-code/hookify/block-force-push.local.md b/src/templates/claude-code/hookify/block-force-push.local.md index b82f6d0..382a2cf 100644 --- a/src/templates/claude-code/hookify/block-force-push.local.md +++ b/src/templates/claude-code/hookify/block-force-push.local.md @@ -6,6 +6,6 @@ pattern: git\s+push\s+.*--force(?!-with-lease)|git\s+push\s+.*\s-f\b action: block --- -🚫 **git push --force bloqué** +🚫 **`git push --force` blocked** -Le force push est interdit sur ce projet. Utilise `--force-with-lease` si tu sais ce que tu fais, ou ouvre une PR à la place. +Force push is not allowed on this project. Use `--force-with-lease` if you know what you're doing, or open a PR instead. diff --git a/src/templates/claude-code/hookify/block-no-verify.local.md b/src/templates/claude-code/hookify/block-no-verify.local.md index 90db516..77b231c 100644 --- a/src/templates/claude-code/hookify/block-no-verify.local.md +++ b/src/templates/claude-code/hookify/block-no-verify.local.md @@ -6,6 +6,6 @@ pattern: --no-verify action: block --- -🚫 **--no-verify bloqué** +🚫 **`--no-verify` blocked** -Le bypass des hooks de commit est interdit. Les tests et le lint doivent passer avant tout commit. +Bypassing commit hooks is not allowed. Tests and lint must pass before any commit. diff --git a/src/templates/claude-code/hookify/stop-verify-tests.local.md.hbs b/src/templates/claude-code/hookify/stop-verify-tests.local.md.hbs index aefc010..739ebce 100644 --- a/src/templates/claude-code/hookify/stop-verify-tests.local.md.hbs +++ b/src/templates/claude-code/hookify/stop-verify-tests.local.md.hbs @@ -6,16 +6,16 @@ pattern: .* action: warn --- -⚠️ **Avant de terminer — TDD check** +⚠️ **Before wrapping up — TDD check** -As-tu : -- [ ] Lancé les tests et vérifié qu'ils passent ? +Did you: +- [ ] Run the tests and confirm they pass? {{#if fastapi}} - `cd backend && .venv/bin/pytest tests/ -v` {{/if}}{{#if springBoot}} - `cd backend && ./mvnw test` {{/if}}{{#if frontend}} - `cd frontend && npm test -- --watch=false` -{{/if}}- [ ] Lancé le lint ? +{{/if}}- [ ] Run the linter? {{#if fastapi}} - `cd backend && .venv/bin/ruff check .` {{/if}}{{#if springBoot}} - `cd backend && ./mvnw checkstyle:check` (if configured) {{/if}}{{#if frontend}} - `cd frontend && npm run lint` {{/if}} -Iron law : aucun commit sans tests verts. +Iron law: no commit without green tests. diff --git a/src/templates/claude-code/hookify/warn-console-log.local.md b/src/templates/claude-code/hookify/warn-console-log.local.md index 5728758..0ae6bb6 100644 --- a/src/templates/claude-code/hookify/warn-console-log.local.md +++ b/src/templates/claude-code/hookify/warn-console-log.local.md @@ -1,11 +1,11 @@ --- name: warn-console-log -enabled: true +enabled: false event: file pattern: console\.log\( action: warn --- -⚠️ **console.log detected** +⚠️ **`console.log` detected** You're about to add a `console.log` statement. Make sure this is intentional and not a debug leftover. diff --git a/src/templates/claude-code/hookify/warn-env-edit.local.md b/src/templates/claude-code/hookify/warn-env-edit.local.md index d94fa72..fdcc8bb 100644 --- a/src/templates/claude-code/hookify/warn-env-edit.local.md +++ b/src/templates/claude-code/hookify/warn-env-edit.local.md @@ -6,6 +6,6 @@ pattern: (^|/)\.env(\.|$) action: warn --- -⚠️ **Fichier .env détecté** +⚠️ **`.env` file detected** -Tu es sur le point de modifier un fichier d'environnement. Assure-toi de ne pas committer de secrets — utilise `.env.example` pour les valeurs de référence. +You're about to modify an environment file. Do not commit secrets — use `.env.example` for reference values. diff --git a/src/templates/claude-code/hookify/warn-no-test-before-commit.local.md.hbs b/src/templates/claude-code/hookify/warn-no-test-before-commit.local.md.hbs index 852ef99..57238ec 100644 --- a/src/templates/claude-code/hookify/warn-no-test-before-commit.local.md.hbs +++ b/src/templates/claude-code/hookify/warn-no-test-before-commit.local.md.hbs @@ -6,13 +6,13 @@ pattern: git\s+commit action: warn --- -⚠️ **Commit détecté — tests lancés ?** +⚠️ **Commit detected — tests run?** -Avant de committer, confirme : -{{#if fastapi}}- [ ] `cd backend && .venv/bin/pytest tests/ -v` → vert -{{/if}}{{#if springBoot}}- [ ] `cd backend && ./mvnw test` → vert -{{/if}}{{#if frontend}}- [ ] `cd frontend && npm test -- --watch=false` → vert -{{/if}}{{#if fastapi}}- [ ] `cd backend && .venv/bin/ruff check .` → propre -{{/if}}{{#if frontend}}- [ ] `cd frontend && npm run lint` → propre +Before committing, confirm: +{{#if fastapi}}- [ ] `cd backend && .venv/bin/pytest tests/ -v` → green +{{/if}}{{#if springBoot}}- [ ] `cd backend && ./mvnw test` → green +{{/if}}{{#if frontend}}- [ ] `cd frontend && npm test -- --watch=false` → green +{{/if}}{{#if fastapi}}- [ ] `cd backend && .venv/bin/ruff check .` → clean +{{/if}}{{#if frontend}}- [ ] `cd frontend && npm run lint` → clean {{/if}} -Iron law : aucun commit sans tests verts. +Iron law: no commit without green tests. diff --git a/src/templates/claude-code/hookify/warn-todo-fixme.local.md b/src/templates/claude-code/hookify/warn-todo-fixme.local.md index 5de8679..2138a85 100644 --- a/src/templates/claude-code/hookify/warn-todo-fixme.local.md +++ b/src/templates/claude-code/hookify/warn-todo-fixme.local.md @@ -1,11 +1,11 @@ --- name: warn-todo-fixme -enabled: true +enabled: false event: file pattern: TODO|FIXME action: warn --- -⚠️ **TODO / FIXME détecté** +⚠️ **TODO / FIXME detected** -Tu ajoutes un marqueur TODO ou FIXME. Assure-toi qu'il ne sera pas oublié avant de committer. +You're adding a TODO or FIXME marker. Make sure it won't be forgotten before committing. diff --git a/src/templates/claude-code/hooks/pre-bash.sh.hbs b/src/templates/claude-code/hooks/pre-bash.sh.hbs deleted file mode 100644 index fdf9b2c..0000000 --- a/src/templates/claude-code/hooks/pre-bash.sh.hbs +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -# Hook: PreToolUse (Bash) — {{name}} project guards. -# Exit 2 = block | Exit 0 = allow - -command -v jq >/dev/null 2>&1 || exit 0 - -COMMAND=$(jq -r '.tool_input.command // ""') - -# Strip heredoc bodies so patterns don't match inside commit messages. -COMMAND_STRIPPED=$(echo "$COMMAND" | awk ' - /<&2 - exit 2 - fi -fi -{{/if}} diff --git a/src/templates/claude-code/settings.json.hbs b/src/templates/claude-code/settings.json.hbs index 2e4344d..5488da7 100644 --- a/src/templates/claude-code/settings.json.hbs +++ b/src/templates/claude-code/settings.json.hbs @@ -54,19 +54,9 @@ } ] } - ], + ]{{#unless hasParentPreCompact}},{{/unless}} {{/unless}} - "PreToolUse": [ - { - "matcher": "Bash", - "hooks": [ - { - "type": "command", - "command": "{{claudeDir}}/hooks/pre-bash.sh" - } - ] - } - ]{{#unless hasParentPreCompact}}, +{{#unless hasParentPreCompact}} "PreCompact": [ { "matcher": "", @@ -77,6 +67,7 @@ } ] } - ]{{/unless}} + ] +{{/unless}} } }