diff --git a/README.md b/README.md index 9a21342994..d635be0a6b 100644 --- a/README.md +++ b/README.md @@ -491,7 +491,7 @@ Local settings override project settings field-by-field. When you run `entire st ### Agent-Specific Steps & Limitations -- When enabling Entire for Codex, the command will also create or update `.codex/config.toml` with `hooks = true` to enable Codex hooks. If you configure Codex manually, make sure this flag is set in your `.codex/config.toml`. Or select Codex from the interactive agent picker when running `entire enable`. +- Codex hooks are enabled by default (codex-cli 0.124.0+), so enabling Entire for Codex only installs `.codex/hooks.json` — no `config.toml` is needed and Entire never creates one. If an older Entire version left a `.codex/config.toml` behind and your repo lives inside `~/.codex/agents`, delete that file to stop Codex's "malformed agent role definition" startup warning. - Entire supports Cursor IDE and Cursor Agent CLI tool, but `entire rewind` is not available at this time. Other commands (`doctor`, `status` etc.) work the same as all other agents. - Entire supports Copilot CLI, but not Copilot in VS Code, in other IDEs, or on github.com. - Entire supports Pi coding agent (Preview). Pi uses a TypeScript extension instead of a JSON hook config. Subagent capture is not currently available. diff --git a/cmd/entire/cli/agent/codex/hooks.go b/cmd/entire/cli/agent/codex/hooks.go index e3c0d01642..13e258cce4 100644 --- a/cmd/entire/cli/agent/codex/hooks.go +++ b/cmd/entire/cli/agent/codex/hooks.go @@ -6,7 +6,6 @@ import ( "fmt" "os" "path/filepath" - "strings" "github.com/entireio/cli/cmd/entire/cli/agent" "github.com/entireio/cli/cmd/entire/cli/jsonutil" @@ -118,11 +117,6 @@ func (c *CodexAgent) InstallHooks(ctx context.Context, localDev bool, force bool } if count == 0 { - // Still ensure the feature flag is configured even if hooks - // were already present (e.g., manually installed). - if err := ensureProjectFeatureEnabled(repoRoot); err != nil { - return 0, fmt.Errorf("failed to enable codex_hooks feature: %w", err) - } return 0, nil } @@ -158,12 +152,11 @@ func (c *CodexAgent) InstallHooks(ctx context.Context, localDev bool, force bool return 0, fmt.Errorf("failed to write hooks.json: %w", err) } - // Enable the codex_hooks feature in the project-level .codex/config.toml. - // This keeps the feature flag per-repo rather than global. - if err := ensureProjectFeatureEnabled(repoRoot); err != nil { - return count, fmt.Errorf("failed to enable codex_hooks feature: %w", err) - } - + // No .codex/config.toml is written: hooks are enabled by default in + // Codex (since 0.124.0), and a TOML file inside Codex's reserved + // /agents tree would be rejected by its agent-role scanner + // at every startup (entireio/cli#842). A leftover config.toml written + // by an older entire version must be removed manually. return count, nil } @@ -358,83 +351,3 @@ func removeEntireHooks(groups []MatcherGroup) []MatcherGroup { } return result } - -// configFileName is the Codex config file name. -const configFileName = "config.toml" - -// featureLine is the TOML line that enables the hooks feature. The flag was -// renamed from `codex_hooks` to `hooks` in Codex 0.129.0; the old name is -// still accepted as a legacy alias but emits a deprecation warning at -// every startup. ensureProjectFeatureEnabled rewrites the legacy form when -// it sees it. -const ( - featureLine = "hooks = true" - legacyFeatureLine = "codex_hooks = true" -) - -// ensureProjectFeatureEnabled writes features.hooks = true to the -// project-level .codex/config.toml. This keeps the feature flag per-repo. -// Replaces the deprecated codex_hooks = true line if it's present. -func ensureProjectFeatureEnabled(repoRoot string) error { - configPath := filepath.Join(repoRoot, ".codex", configFileName) - - data, err := os.ReadFile(configPath) //nolint:gosec // path constructed from repo root - if err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to read config.toml: %w", err) - } - - content := string(data) - hasNew := containsFeatureLine(content, featureLine) - hasLegacy := containsFeatureLine(content, legacyFeatureLine) - switch { - case hasNew && hasLegacy: - content = stripLegacyFeatureLine(content) - case hasNew: - return nil - case hasLegacy: - content = strings.Replace(content, legacyFeatureLine, featureLine, 1) - case strings.Contains(content, "[features]"): - content = strings.Replace(content, "[features]", "[features]\n"+featureLine, 1) - default: - if len(content) > 0 && !strings.HasSuffix(content, "\n") { - content += "\n" - } - content += "\n[features]\n" + featureLine + "\n" - } - - if err := os.MkdirAll(filepath.Dir(configPath), 0o750); err != nil { - return fmt.Errorf("failed to create .codex directory: %w", err) - } - if err := os.WriteFile(configPath, []byte(content), 0o600); err != nil { //nolint:gosec // path constructed from repo root - return fmt.Errorf("failed to write config.toml: %w", err) - } - return nil -} - -// containsFeatureLine checks for an exact line match. A plain -// strings.Contains is wrong because "hooks = true" is a substring of -// "codex_hooks = true" — without the line-boundary anchor we'd treat the -// legacy form as if the new form was already present. -func containsFeatureLine(content, line string) bool { - for _, raw := range strings.Split(content, "\n") { - if strings.TrimSpace(raw) == line { - return true - } - } - return false -} - -// stripLegacyFeatureLine removes the deprecated `codex_hooks = true` line -// from a TOML config string, dropping a trailing blank line so the file -// stays tidy. The new `hooks = true` is added separately by the caller. -func stripLegacyFeatureLine(content string) string { - idx := strings.Index(content, legacyFeatureLine) - if idx < 0 { - return content - } - end := idx + len(legacyFeatureLine) - if end < len(content) && content[end] == '\n' { - end++ - } - return content[:idx] + content[end:] -} diff --git a/cmd/entire/cli/agent/codex/hooks_test.go b/cmd/entire/cli/agent/codex/hooks_test.go index c9f5eb99b1..45ecc1ea18 100644 --- a/cmd/entire/cli/agent/codex/hooks_test.go +++ b/cmd/entire/cli/agent/codex/hooks_test.go @@ -21,7 +21,7 @@ func setupTestEnv(t *testing.T) string { return tempDir } -func TestInstallHooks_CreatesConfig(t *testing.T) { +func TestInstallHooks_CreatesHooksJSONOnly(t *testing.T) { tempDir := setupTestEnv(t) ag := &CodexAgent{} @@ -42,14 +42,13 @@ func TestInstallHooks_CreatesConfig(t *testing.T) { assertHookCommand(t, hooksFile.Hooks.Stop, agentpkg.WrapProductionSilentHookCommand("entire hooks codex stop"), "Stop") assertHookCommand(t, hooksFile.Hooks.PostToolUse, agentpkg.WrapProductionSilentHookCommand("entire hooks codex post-tool-use"), "PostToolUse") - // Verify project-level config.toml enables the hooks feature (per-repo) - projectConfig := filepath.Join(tempDir, ".codex", configFileName) - projectData, err := os.ReadFile(projectConfig) - require.NoError(t, err) - require.Contains(t, string(projectData), "hooks = true") - require.NotContains(t, string(projectData), "codex_hooks = true", - "deprecated codex_hooks line must not be written by fresh installs") - require.Contains(t, string(projectData), "[features]") + // Hooks are enabled by default in Codex, so no .codex/config.toml is + // written. A TOML file there is actively harmful when the repo lives + // inside /agents, where Codex's agent-role scanner rejects + // it at startup (entireio/cli#842). + projectConfig := filepath.Join(tempDir, ".codex", "config.toml") + _, err = os.Stat(projectConfig) + require.True(t, os.IsNotExist(err), "install must not create .codex/config.toml") } func TestInstallHooks_WindowsWrapperProbeSuccessKeepsWrappedCommands(t *testing.T) { @@ -350,42 +349,46 @@ func TestInstallHooks_DoesNotModifyUserConfig(t *testing.T) { require.NoError(t, os.MkdirAll(codexHome, 0o750)) existingConfig := "model = \"gpt-4.1\"\n" - require.NoError(t, os.WriteFile(filepath.Join(codexHome, configFileName), []byte(existingConfig), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(codexHome, "config.toml"), []byte(existingConfig), 0o600)) ag := &CodexAgent{} _, err := ag.InstallHooks(context.Background(), false, false) require.NoError(t, err) - configData, err := os.ReadFile(filepath.Join(codexHome, configFileName)) + configData, err := os.ReadFile(filepath.Join(codexHome, "config.toml")) require.NoError(t, err) require.Contains(t, string(configData), "model = \"gpt-4.1\"") require.NotContains(t, string(configData), `trust_level = "trusted"`) } -// TestInstallHooks_RewritesLegacyFeatureLine pins the rule that an existing -// `codex_hooks = true` line — written by older entire CLI versions — must -// be rewritten to the new `hooks = true` form on the next install. Codex -// 0.129.0 still accepts the legacy alias but prints a deprecation warning -// at every startup; rewriting silences it without forcing the user to -// touch their .codex/config.toml. -func TestInstallHooks_RewritesLegacyFeatureLine(t *testing.T) { - tempDir := setupTestEnv(t) - - codexDir := filepath.Join(tempDir, ".codex") - require.NoError(t, os.MkdirAll(codexDir, 0o750)) - existingConfig := "[features]\ncodex_hooks = true\n" - configPath := filepath.Join(codexDir, configFileName) - require.NoError(t, os.WriteFile(configPath, []byte(existingConfig), 0o600)) - - ag := &CodexAgent{} - _, err := ag.InstallHooks(context.Background(), false, false) - require.NoError(t, err) - - configData, err := os.ReadFile(configPath) - require.NoError(t, err) - require.Contains(t, string(configData), "hooks = true") - require.NotContains(t, string(configData), "codex_hooks = true", - "legacy codex_hooks line must be replaced, not left alongside the new form") +// TestInstallHooks_LeavesExistingLocalConfigUntouched pins that install +// never reads, rewrites, or deletes a project-local .codex/config.toml — +// whether it's a user's own file or a feature-flag leftover from an older +// entire version. The CLI no longer manages that file at all; leftovers +// under /agents must be removed manually (entireio/cli#842). +func TestInstallHooks_LeavesExistingLocalConfigUntouched(t *testing.T) { + contents := map[string]string{ + "old entire leftover": "[features]\nhooks = true\n", + "user file": "model = \"gpt-4.1\"\n", + } + for name, content := range contents { + t.Run(name, func(t *testing.T) { + tempDir := setupTestEnv(t) + + codexDir := filepath.Join(tempDir, ".codex") + require.NoError(t, os.MkdirAll(codexDir, 0o750)) + configPath := filepath.Join(codexDir, "config.toml") + require.NoError(t, os.WriteFile(configPath, []byte(content), 0o600)) + + ag := &CodexAgent{} + _, err := ag.InstallHooks(context.Background(), false, false) + require.NoError(t, err) + + data, err := os.ReadFile(configPath) + require.NoError(t, err) + require.Equal(t, content, string(data), "install must not touch an existing .codex/config.toml") + }) + } } // assertHookCommand verifies that one of the hook entries in groups contains the expected command.