@searls/turbocommit 0.12.1 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,28 +2,32 @@
2
2
 
3
3
  # turbocommit
4
4
 
5
- turbocommit creates a git commit containing everything Claude Code changes
6
- on each turn. Think of it like save state in a game emulator: whenever you
7
- fall in a pit, you can safely rewind and try again.
5
+ turbocommit creates a git commit containing everything Claude Code or Codex
6
+ changes on each turn. Think of it like save state in a game emulator: whenever
7
+ you fall in a pit, you can safely rewind and try again.
8
8
 
9
9
  Captures every prompt/response transcript for the associated changes so you
10
10
  and your agent never lose the thread on what you were doing after the fact,
11
11
  even if you didn't leave behind any comments or docs.
12
12
 
13
- Each commit also [links back to the previous commit from the same Claude Code
14
- session](#continuity-across-workstreams), so you can run multiple agent
13
+ Each commit also [links back to the previous commit from the same agent
14
+ session](#continuity-across-workstreams), so you can run multiple agent
15
15
  sessions concurrently and detangle which agent committed what after the fact.
16
16
 
17
17
  ## How it works
18
18
 
19
- turbocommit registers four hooks with Claude Code: **PreToolUse**,
20
- **SessionStart**, **SessionEnd**, and **Stop**.
19
+ turbocommit registers hooks with the harnesses you use:
20
+
21
+ - Claude Code: **PreToolUse**, **SessionStart**, **SessionEnd**, and **Stop**.
22
+ - Codex: **PreToolUse**, **SessionStart**, **PreCompact**, and
23
+ **Stop**.
21
24
 
22
25
  - **PreToolUse** tracks which sessions actually modify files (Write, Edit,
23
26
  MultiEdit, NotebookEdit, MCP tools). Read-only sessions (Grep, Read,
24
27
  Bash-only) are never committed.
25
28
  - **SessionStart / SessionEnd** chain sessions across `/clear` boundaries
26
29
  so planning context survives into the eventual commit.
30
+ - **PreCompact** on Codex buffers visible transcript context before compaction.
27
31
  - **Stop** fires after every turn. If the session modified files, it
28
32
  commits with `git add -A`. If not, it buffers the transcript for pickup
29
33
  by a later session that does commit.
@@ -40,6 +44,22 @@ brew install searlsco/tap/turbocommit
40
44
  turbocommit install
41
45
  ```
42
46
 
47
+ Or install from npm:
48
+
49
+ ```bash
50
+ npm install -g @searls/turbocommit
51
+ turbocommit install
52
+ ```
53
+
54
+ With no flags, `turbocommit install` installs into existing harness config
55
+ directories only: `~/.claude` and/or `~/.codex`. To force one harness and create
56
+ its config directory if needed:
57
+
58
+ ```bash
59
+ turbocommit install --harness claude
60
+ turbocommit install --harness codex
61
+ ```
62
+
43
63
  Then enable it per-project:
44
64
 
45
65
  ```bash
@@ -57,20 +77,24 @@ brew uninstall turbocommit
57
77
 
58
78
  ## Where turbocommit goes in your hook chain
59
79
 
60
- `turbocommit install` adds one hook group per event (PreToolUse,
61
- SessionStart, SessionEnd, Stop). The hooks are fire-and-forget: they
62
- always exit 0 and never block Claude.
80
+ `turbocommit install` adds one hook group per event for each installed harness.
81
+ The hooks are fire-and-forget: they always exit 0 and never block the agent.
82
+
83
+ If you rearrange your hooks manually, the turbocommit hooks can go anywhere.
84
+ Order doesn't matter since they never block.
63
85
 
64
- If you rearrange your hooks manually, the turbocommit hooks can go
65
- anywhere order doesn't matter since they never block.
86
+ Codex users should restart Codex and run `/hooks` after install to review and
87
+ trust the hook commands.
66
88
 
67
89
  ## Commands
68
90
 
69
91
  | Command | Description |
70
92
  |---------|-------------|
71
- | `turbocommit install` | Add hooks to `~/.claude/settings.json` |
93
+ | `turbocommit install` | Add hooks to existing Claude and/or Codex config dirs |
94
+ | `turbocommit install --harness claude` | Add hooks to `~/.claude/settings.json` |
95
+ | `turbocommit install --harness codex` | Add hooks to `~/.codex/hooks.json` |
72
96
  | `turbocommit uninstall` | Remove them |
73
- | `turbocommit init` | Create `.claude/turbocommit.json` in current repo |
97
+ | `turbocommit init` | Create `.turbocommit.json` in current repo |
74
98
  | `turbocommit deinit` | Remove it |
75
99
  | `turbocommit doctor` | Check hook and config health |
76
100
  | `turbocommit monitor` | Tail the event log (start/success/skip/fail) |
@@ -80,8 +104,8 @@ anywhere — order doesn't matter since they never block.
80
104
  ## Continuity across workstreams
81
105
 
82
106
  Every turbocommit adds a `Continuation of <SHA>` reference linking back to
83
- the previous commit from the same Claude session. This means you can run
84
- multiple sessions in parallel different tabs, different tasks all
107
+ the previous commit from the same agent session. This means you can run
108
+ multiple sessions in parallel, across different tabs and different tasks, all
85
109
  committing to a single branch, and still trace each logical workstream
86
110
  by following the SHA chain.
87
111
 
@@ -119,9 +143,24 @@ is traceable.
119
143
 
120
144
  ## Configuration
121
145
 
122
- `turbocommit init` creates `.claude/turbocommit.json` with `{"enabled": true}`.
123
- You can also place a global config at `~/.claude/turbocommit.json` project
124
- config is deep-merged on top.
146
+ `turbocommit init` creates `.turbocommit.json` with `{"enabled": true}`.
147
+ The legacy project path `.claude/turbocommit.json` is still read when the
148
+ neutral file is absent.
149
+
150
+ Global config follows the XDG Unix config standard:
151
+
152
+ ```text
153
+ $XDG_CONFIG_HOME/turbocommit/config.json
154
+ ```
155
+
156
+ When `XDG_CONFIG_HOME` is unset, turbocommit reads:
157
+
158
+ ```text
159
+ ~/.config/turbocommit/config.json
160
+ ```
161
+
162
+ The legacy global path `~/.claude/turbocommit.json` is still read when the XDG
163
+ file is absent. Project config is deep-merged on top of global config.
125
164
 
126
165
  Here's every property with its default:
127
166
 
@@ -152,7 +191,7 @@ Here's every property with its default:
152
191
 
153
192
  // Prompt template sent to the title agent. Use {{transcript}} as the
154
193
  // placeholder for the session transcript.
155
- "prompt": "You have 10 seconds. Write a single-line git commit headline (max 72 chars) from this coding session transcript. Speed over perfection a rough title beats no title.\n\nRules:\n- Imperative mood (\"Add\", \"Fix\", \"Update\")\n- Specific about what changed\n- No trailing period\n- No conventional commit prefixes unless clearly a fix/feat\n\nTranscript:\n{{transcript}}\n\nRespond with ONLY the headline, nothing else. Do not deliberate."
194
+ "prompt": "You have 10 seconds. Write a single-line git commit headline (max 72 chars) from this coding session transcript. Speed over perfection: a rough title beats no title.\n\nRules:\n- Imperative mood (\"Add\", \"Fix\", \"Update\")\n- Specific about what changed\n- No trailing period\n- No conventional commit prefixes unless clearly a fix/feat\n\nTranscript:\n{{transcript}}\n\nRespond with ONLY the headline, nothing else. Do not deliberate."
156
195
  },
157
196
 
158
197
  "body": {
@@ -165,7 +204,7 @@ Here's every property with its default:
165
204
 
166
205
  // Prompt template sent to the body agent. Use {{transcript}} as the
167
206
  // placeholder for the session transcript.
168
- "prompt": "Given this transcript of a coding session, write a concise git commit body.\n\nRules:\n- Summarize what was done and why\n- Be concise a few sentences or bullet points\n- Focus on the \"why\" more than the \"what\"\n\nTranscript:\n{{transcript}}\n\nRespond with ONLY the commit body, nothing else.",
207
+ "prompt": "Given this transcript of a coding session, write a concise git commit body.\n\nRules:\n- Summarize what was done and why\n- Be concise: a few sentences or bullet points\n- Focus on the \"why\" more than the \"what\"\n\nTranscript:\n{{transcript}}\n\nRespond with ONLY the commit body, nothing else.",
169
208
 
170
209
  // Wrap prose lines at this width. Code blocks, tables, headers, and
171
210
  // other structured content are preserved verbatim.
@@ -182,4 +221,4 @@ runtime without changing config.
182
221
 
183
222
  - Node.js >= 18
184
223
  - Git
185
- - Claude Code
224
+ - Claude Code or Codex CLI
package/cli.js CHANGED
@@ -3,26 +3,27 @@
3
3
  const { readStdin } = require('./lib/io')
4
4
  const { install, uninstall } = require('./lib/install')
5
5
  const { init, deinit } = require('./lib/init')
6
- const { run } = require('./lib/run')
6
+ const { run, runPreCompact } = require('./lib/run')
7
7
  const { handleTrack } = require('./lib/track')
8
8
  const { handleSessionStart, handleSessionEnd } = require('./lib/session')
9
9
  const { doctor } = require('./lib/doctor')
10
10
  const { monitor } = require('./lib/monitor')
11
11
  const { gitRoot } = require('./lib/git')
12
+ const { parseHarnessArg, normalizeHookInput } = require('./lib/harness')
12
13
 
13
14
  const VERSION = require('./package.json').version
14
15
 
15
16
  const USAGE = `turbocommit v${VERSION}
16
- Auto-commit after every Claude Code turn.
17
+ Auto-commit after every AI coding agent turn.
17
18
 
18
19
  Commands:
19
- install Add turbocommit hooks to ~/.claude/settings.json
20
- uninstall Remove turbocommit from settings
21
- init Create .claude/turbocommit.json in current git repo
22
- deinit Remove .claude/turbocommit.json
20
+ install Add turbocommit hooks to installed harnesses
21
+ uninstall Remove turbocommit hooks
22
+ init Create .turbocommit.json in current git repo
23
+ deinit Remove turbocommit project config
23
24
  doctor Check hook and config health
24
25
  monitor Tail the event log (start/success/fail)
25
- hook Hook entry points (called by Claude Code, not manually)
26
+ hook Hook entry points (called by harness hooks, not manually)
26
27
  help Show this help text
27
28
  --version, -v Show version
28
29
 
@@ -34,23 +35,30 @@ Usage:
34
35
  `
35
36
 
36
37
  function main (argv) {
37
- const cmd = argv[0]
38
+ const parsed = parseHarnessArg(argv)
39
+ if (!parsed.ok) {
40
+ console.error(parsed.error)
41
+ console.error('Run "turbocommit help" for usage.')
42
+ process.exitCode = 1
43
+ return
44
+ }
45
+ const cmd = parsed.args[0]
38
46
 
39
47
  switch (cmd) {
40
48
  case 'install':
41
- return cmdInstall()
49
+ return cmdInstall(parsed.harness)
42
50
  case 'uninstall':
43
- return cmdUninstall()
51
+ return cmdUninstall(parsed.harness)
44
52
  case 'doctor':
45
- return cmdDoctor()
53
+ return cmdDoctor(parsed.harness)
46
54
  case 'monitor':
47
- return cmdMonitor()
55
+ return cmdMonitor(parsed.harness)
48
56
  case 'init':
49
57
  return cmdInit()
50
58
  case 'deinit':
51
59
  return cmdDeinit()
52
60
  case 'hook':
53
- return cmdHook(argv.slice(1))
61
+ return cmdHook(parsed.args.slice(1), parsed.harness)
54
62
  case 'run':
55
63
  return cmdRunDeprecated()
56
64
  case '--version':
@@ -71,33 +79,42 @@ function main (argv) {
71
79
  }
72
80
  }
73
81
 
74
- function cmdInstall () {
75
- const result = install()
76
- if (result.alreadyInstalled) {
77
- console.log('turbocommit already installed.')
78
- console.log(` Settings: ${result.settingsPath}`)
82
+ function cmdInstall (harness) {
83
+ const result = install({ harness })
84
+ if (result.results.length === 0) {
85
+ console.log('No supported harness config dirs found.')
79
86
  return
80
87
  }
81
- console.log('turbocommit installed.')
82
- console.log(` Settings: ${result.settingsPath}`)
88
+ for (const r of result.results) {
89
+ console.log(r.alreadyInstalled ? `turbocommit already installed for ${r.harness}.` : `turbocommit installed for ${r.harness}.`)
90
+ console.log(` ${r.harness === 'codex' ? 'Hooks' : 'Settings'}: ${r.hooksPath || r.settingsPath}`)
91
+ }
83
92
  console.log('')
84
93
  console.log('════════════════════════════════════════════════════════════════════')
85
- console.log('IMPORTANT: Restart Claude Code for the hooks to take effect.')
94
+ for (const r of result.results) {
95
+ if (r.harness === 'codex') {
96
+ console.log('IMPORTANT: Restart Codex and run /hooks to review turbocommit hooks.')
97
+ } else {
98
+ console.log('IMPORTANT: Restart Claude Code for the hooks to take effect.')
99
+ }
100
+ }
86
101
  console.log('════════════════════════════════════════════════════════════════════')
87
102
  console.log('')
88
103
  console.log('Next steps:')
89
- console.log(' 1. Restart Claude Code (required)')
104
+ console.log(' 1. Restart the installed harness')
90
105
  console.log(' 2. Run: turbocommit init in a repo to enable auto-commits')
91
106
  }
92
107
 
93
- function cmdUninstall () {
94
- const result = uninstall()
95
- if (!result.wasInstalled) {
96
- console.log('turbocommit was not installed.')
108
+ function cmdUninstall (harness) {
109
+ const result = uninstall({ harness })
110
+ if (result.results.length === 0) {
111
+ console.log('No supported harness config dirs found.')
97
112
  return
98
113
  }
99
- console.log('turbocommit uninstalled.')
100
- console.log(` Settings: ${result.settingsPath}`)
114
+ for (const r of result.results) {
115
+ console.log(r.wasInstalled ? `turbocommit uninstalled for ${r.harness}.` : `turbocommit was not installed for ${r.harness}.`)
116
+ console.log(` ${r.harness === 'codex' ? 'Hooks' : 'Settings'}: ${r.hooksPath || r.settingsPath}`)
117
+ }
101
118
  }
102
119
 
103
120
  function cmdInit () {
@@ -124,16 +141,17 @@ function cmdDeinit () {
124
141
  return
125
142
  }
126
143
  if (!result.existed) {
127
- console.log('turbocommit was not enabled in this repo.')
144
+ console.log('turbocommit project config was not found.')
145
+ console.log('Global config may still enable turbocommit.')
128
146
  return
129
147
  }
130
148
  console.log('turbocommit disabled.')
131
- console.log(` Removed: ${result.path}`)
149
+ for (const p of result.paths) console.log(` Removed: ${p}`)
132
150
  }
133
151
 
134
- function cmdDoctor () {
152
+ function cmdDoctor (harness) {
135
153
  const STATUS = { ok: ' ok', warn: 'warn', error: ' err', info: 'info' }
136
- const result = doctor()
154
+ const result = doctor({ harness })
137
155
  for (const check of result.checks) {
138
156
  console.log(`[${STATUS[check.status] || check.status}] ${check.name}: ${check.message}`)
139
157
  }
@@ -142,34 +160,39 @@ function cmdDoctor () {
142
160
  }
143
161
  }
144
162
 
145
- function cmdMonitor () {
146
- monitor()
163
+ function cmdMonitor (harness) {
164
+ if (!monitor(harness)) process.exitCode = 1
147
165
  }
148
166
 
149
- function cmdHook (argv) {
167
+ function cmdHook (argv, harness) {
150
168
  const event = argv[0]
151
169
  try {
152
170
  const input = readStdin()
153
- const root = gitRoot()
171
+ const hookInput = normalizeHookInput(input, event, harness)
172
+ if (!hookInput) return
173
+ const root = gitRoot(hookInput.cwd || process.cwd())
154
174
  switch (event) {
155
175
  case 'pre-tool-use':
156
- handleTrack(input, root)
176
+ handleTrack(hookInput, root)
157
177
  return
158
178
  case 'session-start':
159
- handleSessionStart(input, root)
179
+ handleSessionStart(hookInput, root)
160
180
  return
161
181
  case 'session-end':
162
- handleSessionEnd(input, root)
182
+ handleSessionEnd(hookInput, root)
183
+ return
184
+ case 'pre-compact':
185
+ runPreCompact(hookInput)
163
186
  return
164
187
  case 'stop':
165
- run(input)
188
+ run(hookInput)
166
189
  break
167
190
  default:
168
- // Unknown hook event ignore silently (never fail)
191
+ // Unknown hook event: ignore silently (never fail)
169
192
  break
170
193
  }
171
194
  } catch {
172
- // Never fail fire and forget
195
+ // Never fail: fire and forget
173
196
  }
174
197
  }
175
198
 
package/lib/agent.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const { tryRun } = require('./io')
2
2
 
3
3
  const DEFAULT_COMMAND = 'claude -p --model haiku'
4
+ const DEFAULT_CODEX_COMMAND = 'codex exec --ephemeral -'
4
5
 
5
6
  const DEFAULT_TITLE_PROMPT = `You have 10 seconds. Write a single-line git commit headline (max 72 chars) from this coding session transcript. Speed over perfection — a rough title beats no title.
6
7
 
@@ -49,8 +50,12 @@ function runAgent (root, command, prompt) {
49
50
  return output || null
50
51
  }
51
52
 
52
- function runTitleAgent (root, titleCfg, transcript) {
53
- const command = titleCfg.command || DEFAULT_COMMAND
53
+ function defaultCommand (harness) {
54
+ return harness === 'codex' ? DEFAULT_CODEX_COMMAND : DEFAULT_COMMAND
55
+ }
56
+
57
+ function runTitleAgent (root, titleCfg, transcript, harness) {
58
+ const command = titleCfg.command || defaultCommand(harness)
54
59
  const template = titleCfg.prompt || DEFAULT_TITLE_PROMPT
55
60
  const prompt = renderPrompt(template, transcript)
56
61
  const result = runAgent(root, command, prompt)
@@ -59,8 +64,8 @@ function runTitleAgent (root, titleCfg, transcript) {
59
64
  return result.split('\n')[0].slice(0, 72) || null
60
65
  }
61
66
 
62
- function runBodyAgent (root, bodyCfg, transcript) {
63
- const command = bodyCfg.command || DEFAULT_COMMAND
67
+ function runBodyAgent (root, bodyCfg, transcript, harness) {
68
+ const command = bodyCfg.command || defaultCommand(harness)
64
69
  const template = bodyCfg.prompt || DEFAULT_BODY_PROMPT
65
70
  const prompt = renderPrompt(template, transcript)
66
71
  return runAgent(root, command, prompt)
@@ -68,8 +73,10 @@ function runBodyAgent (root, bodyCfg, transcript) {
68
73
 
69
74
  module.exports = {
70
75
  DEFAULT_COMMAND,
76
+ DEFAULT_CODEX_COMMAND,
71
77
  DEFAULT_TITLE_PROMPT,
72
78
  DEFAULT_BODY_PROMPT,
79
+ defaultCommand,
73
80
  renderPrompt,
74
81
  runAgent,
75
82
  runTitleAgent,
package/lib/codex.js ADDED
@@ -0,0 +1,34 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const { codexHome } = require('./harness')
4
+
5
+ function resolveCodexTranscriptPath (hookInput) {
6
+ if (hookInput.transcriptPath) return hookInput.transcriptPath
7
+ if (!hookInput.sessionId) return null
8
+
9
+ const catalog = path.join(codexHome(), 'sessions', 'index', 'catalog.jsonl')
10
+ let lines
11
+ try {
12
+ lines = fs.readFileSync(catalog, 'utf8').split('\n')
13
+ } catch {
14
+ return null
15
+ }
16
+
17
+ for (const line of lines) {
18
+ if (!line.trim()) continue
19
+ let entry
20
+ try {
21
+ entry = JSON.parse(line)
22
+ } catch {
23
+ continue
24
+ }
25
+ if (entry.session_id === hookInput.sessionId && entry.rollout_path) {
26
+ return path.isAbsolute(entry.rollout_path)
27
+ ? entry.rollout_path
28
+ : path.join(codexHome(), entry.rollout_path)
29
+ }
30
+ }
31
+ return null
32
+ }
33
+
34
+ module.exports = { resolveCodexTranscriptPath }
package/lib/config.js ADDED
@@ -0,0 +1,52 @@
1
+ const os = require('os')
2
+ const path = require('path')
3
+ const { loadJson, mergeConfig } = require('./io')
4
+
5
+ function xdgConfigPath () {
6
+ const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config')
7
+ return path.join(base, 'turbocommit', 'config.json')
8
+ }
9
+
10
+ function legacyGlobalConfigPath () {
11
+ return path.join(os.homedir(), '.claude', 'turbocommit.json')
12
+ }
13
+
14
+ function globalConfigPath () {
15
+ return loadJson(xdgConfigPath()) ? xdgConfigPath() : legacyGlobalConfigPath()
16
+ }
17
+
18
+ function neutralProjectConfigPath (root) {
19
+ return path.join(root, '.turbocommit.json')
20
+ }
21
+
22
+ function legacyProjectConfigPath (root) {
23
+ return path.join(root, '.claude', 'turbocommit.json')
24
+ }
25
+
26
+ function projectConfigPath (root) {
27
+ return loadJson(neutralProjectConfigPath(root)) ? neutralProjectConfigPath(root) : legacyProjectConfigPath(root)
28
+ }
29
+
30
+ function activeConfig (root) {
31
+ const globalPath = globalConfigPath()
32
+ const projectPath = root ? projectConfigPath(root) : null
33
+ const globalCfg = loadJson(globalPath)
34
+ const projectCfg = projectPath ? loadJson(projectPath) : null
35
+ return {
36
+ globalPath,
37
+ projectPath,
38
+ globalCfg,
39
+ projectCfg,
40
+ config: mergeConfig(globalCfg || {}, projectCfg || {})
41
+ }
42
+ }
43
+
44
+ module.exports = {
45
+ xdgConfigPath,
46
+ legacyGlobalConfigPath,
47
+ globalConfigPath,
48
+ neutralProjectConfigPath,
49
+ legacyProjectConfigPath,
50
+ projectConfigPath,
51
+ activeConfig
52
+ }
package/lib/doctor.js CHANGED
@@ -1,20 +1,46 @@
1
1
  const os = require('os')
2
2
  const path = require('path')
3
3
  const { loadJson, mergeConfig } = require('./io')
4
- const { hasTurbocommit, isFullyInstalled, getSettingsPath, HOOK_DEFS } = require('./install')
4
+ const { hasTurbocommit, isFullyInstalled, isCodexFullyInstalled, getSettingsPath, getCodexHooksPath, HOOK_DEFS, CODEX_HOOK_DEFS } = require('./install')
5
5
  const { gitRoot } = require('./git')
6
6
  const { configPath } = require('./init')
7
+ const { activeConfig, xdgConfigPath, legacyGlobalConfigPath, neutralProjectConfigPath, legacyProjectConfigPath } = require('./config')
8
+ const { selectedHarnesses } = require('./harness')
7
9
 
8
10
  function globalConfigPath () {
9
11
  return path.join(os.homedir(), '.claude', 'turbocommit.json')
10
12
  }
11
13
 
12
- function doctor (settingsPath, cwd) {
14
+ function doctor (settingsPathOrOptions, cwd) {
15
+ if (typeof settingsPathOrOptions === 'string' || settingsPathOrOptions == null) {
16
+ return doctorLegacy(settingsPathOrOptions, cwd)
17
+ }
18
+ const options = settingsPathOrOptions || {}
19
+ cwd = options.cwd || process.cwd()
20
+ const checks = []
21
+ const harnesses = selectedHarnesses(options.harness)
22
+
23
+ for (const harness of harnesses) {
24
+ if (harness === 'codex') {
25
+ checkCodex(checks, options.codexHooksPath || getCodexHooksPath())
26
+ } else {
27
+ checkClaude(checks, options.claudeSettingsPath || getSettingsPath())
28
+ }
29
+ }
30
+ if (harnesses.length === 0) {
31
+ checks.push({ name: 'Harnesses', status: 'info', message: 'No supported harness config dirs found' })
32
+ }
33
+
34
+ checkConfig(checks, cwd)
35
+
36
+ const ok = checks.every(c => c.status !== 'error')
37
+ return { ok, checks }
38
+ }
39
+
40
+ function doctorLegacy (settingsPath, cwd) {
13
41
  settingsPath = settingsPath || getSettingsPath()
14
42
  cwd = cwd || process.cwd()
15
43
  const checks = []
16
-
17
- // 1. Global settings exist
18
44
  const settings = loadJson(settingsPath)
19
45
  if (!settings) {
20
46
  checks.push({ name: 'Global settings', status: 'error', message: `Not found: ${settingsPath}` })
@@ -32,7 +58,7 @@ function doctor (settingsPath, cwd) {
32
58
  }
33
59
  checks.push({ name: 'Hooks installed', status: 'ok', message: `All ${Object.keys(HOOK_DEFS).length} hooks installed` })
34
60
 
35
- // 3. Stop group isolation turbocommit must be the sole hook in the last group
61
+ // 3. Stop group isolation: turbocommit must be the sole hook in the last group
36
62
  const stopGroups = (settings.hooks && settings.hooks.Stop) || []
37
63
  const tcGroupIndex = findTurbocommitGroup(stopGroups)
38
64
  if (tcGroupIndex >= 0) {
@@ -41,7 +67,7 @@ function doctor (settingsPath, cwd) {
41
67
  const isSoleHook = tcGroup.hooks.length === 1
42
68
 
43
69
  if (!isSoleHook) {
44
- checks.push({ name: 'Group isolation', status: 'warn', message: 'turbocommit shares a Stop group with other hooks will commit even if another hook blocks' })
70
+ checks.push({ name: 'Group isolation', status: 'warn', message: 'turbocommit shares a Stop group with other hooks; will commit even if another hook blocks' })
45
71
  } else if (!isLastGroup) {
46
72
  checks.push({ name: 'Group isolation', status: 'warn', message: 'Another group runs after turbocommit in Stop' })
47
73
  } else {
@@ -61,21 +87,23 @@ function doctor (settingsPath, cwd) {
61
87
  // 5. Local config exists
62
88
  const root = gitRoot(cwd)
63
89
  if (!root) {
64
- checks.push({ name: 'Local config', status: 'info', message: 'Not in a git repo skipping local checks' })
90
+ checks.push({ name: 'Local config', status: 'info', message: 'Not in a git repo; skipping local checks' })
65
91
  } else {
66
- const localPath = configPath(root)
92
+ const neutralPath = configPath(root)
93
+ const legacyPath = legacyProjectConfigPath(root)
94
+ const localPath = loadJson(neutralPath) ? neutralPath : legacyPath
67
95
  const localConfig = loadJson(localPath)
68
96
  if (!localConfig) {
69
97
  if (globalCfg) {
70
98
  checks.push({ name: 'Local config', status: 'info', message: 'Not found (using global config)' })
71
99
  } else {
72
- checks.push({ name: 'Local config', status: 'warn', message: `Not found: ${localPath}. Run: turbocommit init` })
100
+ checks.push({ name: 'Local config', status: 'warn', message: `Not found: ${neutralPath}. Run: turbocommit init` })
73
101
  }
74
102
  } else {
75
103
  checks.push({ name: 'Local config', status: 'ok', message: localPath })
76
104
  }
77
105
 
78
- // 6. Enabled evaluate merged config
106
+ // 6. Enabled: evaluate merged config
79
107
  const merged = mergeConfig(globalCfg || {}, localConfig || {})
80
108
  if (merged.enabled !== true) {
81
109
  checks.push({ name: 'Enabled', status: 'warn', message: 'enabled is not true in merged config' })
@@ -88,6 +116,70 @@ function doctor (settingsPath, cwd) {
88
116
  return { ok, checks }
89
117
  }
90
118
 
119
+ function checkClaude (checks, settingsPath) {
120
+ const settings = loadJson(settingsPath)
121
+ if (!settings) {
122
+ checks.push({ name: 'Claude settings', status: 'error', message: `Not found: ${settingsPath}` })
123
+ return
124
+ }
125
+ checks.push({ name: 'Claude settings', status: 'ok', message: settingsPath })
126
+
127
+ if (!isFullyInstalled(settings)) {
128
+ const missing = Object.keys(HOOK_DEFS).filter(event =>
129
+ !hasTurbocommit((settings.hooks && settings.hooks[event]) || [])
130
+ )
131
+ checks.push({ name: 'Claude hooks', status: 'error', message: `Missing hooks: ${missing.join(', ')}` })
132
+ return
133
+ }
134
+ checks.push({ name: 'Claude hooks', status: 'ok', message: `All ${Object.keys(HOOK_DEFS).length} hooks installed` })
135
+ }
136
+
137
+ function checkCodex (checks, hooksPath) {
138
+ const hooks = loadJson(hooksPath)
139
+ if (!hooks) {
140
+ checks.push({ name: 'Codex hooks', status: 'error', message: `Not found: ${hooksPath}` })
141
+ return
142
+ }
143
+ if (!isCodexFullyInstalled(hooks)) {
144
+ const missing = Object.keys(CODEX_HOOK_DEFS).filter(event =>
145
+ !hasTurbocommit((hooks.hooks && hooks.hooks[event]) || [])
146
+ )
147
+ checks.push({ name: 'Codex hooks', status: 'error', message: `Missing hooks: ${missing.join(', ')}` })
148
+ return
149
+ }
150
+ checks.push({ name: 'Codex hooks', status: 'ok', message: `All ${Object.keys(CODEX_HOOK_DEFS).length} hooks installed` })
151
+ checks.push({ name: 'Codex hook trust', status: 'warn', message: 'Unknown. Run /hooks in Codex to verify trust.' })
152
+ }
153
+
154
+ function checkConfig (checks, cwd) {
155
+ const root = gitRoot(cwd)
156
+ const xdgPath = xdgConfigPath()
157
+ const legacyGlobal = legacyGlobalConfigPath()
158
+ const globalPath = loadJson(xdgPath) ? xdgPath : legacyGlobal
159
+ const globalCfg = loadJson(globalPath)
160
+ checks.push(globalCfg
161
+ ? { name: 'Global config', status: 'ok', message: globalPath }
162
+ : { name: 'Global config', status: 'info', message: 'Not found (optional)' })
163
+
164
+ if (!root) {
165
+ checks.push({ name: 'Project config', status: 'info', message: 'Not in a git repo' })
166
+ return
167
+ }
168
+
169
+ const neutralPath = neutralProjectConfigPath(root)
170
+ const legacyPath = legacyProjectConfigPath(root)
171
+ const projectPath = loadJson(neutralPath) ? neutralPath : legacyPath
172
+ const projectCfg = loadJson(projectPath)
173
+ checks.push(projectCfg
174
+ ? { name: 'Project config', status: 'ok', message: projectPath }
175
+ : { name: 'Project config', status: 'info', message: `Not found: ${configPath(root)}` })
176
+
177
+ const { config } = activeConfig(root)
178
+ checks.push(config.enabled === true
179
+ ? { name: 'Enabled', status: 'ok', message: 'Enabled' }
180
+ : { name: 'Enabled', status: 'warn', message: 'enabled is not true in active config' })
181
+ }
182
+
91
183
  function findTurbocommitGroup (groups) {
92
184
  for (let i = 0; i < groups.length; i++) {
93
185
  const hooks = (groups[i] && groups[i].hooks) || []