@handsupmin/gc-tree 0.2.0 → 0.3.0

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.zh.md CHANGED
@@ -36,6 +36,39 @@
36
36
 
37
37
  ---
38
38
 
39
+ ## 安装与快速开始
40
+
41
+ ```bash
42
+ npm install -g @handsupmin/gc-tree
43
+ gctree init
44
+ ```
45
+
46
+ `gctree init` 会引导你:
47
+
48
+ 1. 选择 provider:`claude-code`、`codex` 或 `both`
49
+ 2. 在当前仓库安装集成文件
50
+ 3. 为 `main` gc-branch 完成引导式 onboarding
51
+
52
+ 之后,AI 工具会安装真正的 SessionStart / UserPromptSubmit hook 集成:在开始工作前自动检查 gc-tree,并在整个会话内缓存空结果和 no-match。
53
+
54
+ - **CLI:** `gctree`
55
+ - **要求:** Node.js 20+
56
+
57
+ ---
58
+
59
+ ## 适合哪些人
60
+
61
+ 如果以下几条符合你的情况,gc-tree 会很适合你:
62
+
63
+ - 工作横跨**多个仓库**(单体仓库团队、平台 + 客户仓库、后端 + 前端技术栈)
64
+ - 同一周内要在**多个产品或客户**之间来回切换
65
+ - 每次开 AI 会话都要**重复解释相同的上下文**
66
+ - 希望 AI 工具不只了解当前文件,还能理解**编码规范、架构和领域知识**
67
+
68
+ 如果你只在一个仓库、一个产品里工作,不需要这个工具。`CLAUDE.md` 或 `.cursorrules` 就够了。
69
+
70
+ ---
71
+
39
72
  ## gc-tree 做什么
40
73
 
41
74
  `gc-tree` 工作在**仓库之上**。它把上下文存储为结构化的 Markdown 文件,让 AI 工具在每次会话前自动拉取相关内容。
@@ -64,34 +97,21 @@ AI 工具拿到的是正确的上下文——不是整个知识库,而是恰
64
97
 
65
98
  ---
66
99
 
67
- ## 适合哪些人
68
-
69
- 如果以下几条符合你的情况,gc-tree 会很适合你:
70
-
71
- - 工作横跨**多个仓库**(单体仓库团队、平台 + 客户仓库、后端 + 前端技术栈)
72
- - 同一周内要在**多个产品或客户**之间来回切换
73
- - 每次开 AI 会话都要**重复解释相同的上下文**
74
- - 希望 AI 工具不只了解当前文件,还能理解**编码规范、架构和领域知识**
75
-
76
- 如果你只在一个仓库、一个产品里工作,不需要这个工具。`CLAUDE.md` 或 `.cursorrules` 就够了。
77
-
78
- ---
79
-
80
100
  ## 和 CLAUDE.md 或 cursor rules 有什么区别?
81
101
 
82
102
  `CLAUDE.md` 很好用——在单个仓库里。
83
103
 
84
104
  一旦你有了多个仓库、客户或工作线:
85
105
 
86
- | | `CLAUDE.md` / cursor rules | `gc-tree` |
87
- |---|---|---|
88
- | 作用范围 | 单个仓库 | 多个仓库,一套上下文 |
89
- | 持久化方式 | 仓库内的文件 | 存储在仓库外,跨会话复用 |
90
- | 切换上下文 | 手动编辑文件 | `gctree checkout client-b` |
91
- | 相关性过滤 | 全部或全无 | 只注入匹配的文档(约 4%) |
92
- | 初始化方式 | 手写 | 由 AI 工具引导完成 |
93
- | 支持 Codex | ✅ | ✅ |
94
- | 支持 Claude Code | ✅ | ✅ |
106
+ | | `CLAUDE.md` / cursor rules | `gc-tree` |
107
+ | ---------------- | -------------------------- | -------------------------- |
108
+ | 作用范围 | 单个仓库 | 多个仓库,一套上下文 |
109
+ | 持久化方式 | 仓库内的文件 | 存储在仓库外,跨会话复用 |
110
+ | 切换上下文 | 手动编辑文件 | `gctree checkout client-b` |
111
+ | 相关性过滤 | 全部或全无 | 只注入匹配的文档(约 4%) |
112
+ | 初始化方式 | 手写 | 由 AI 工具引导完成 |
113
+ | 支持 Codex | ✅ | ✅ |
114
+ | 支持 Claude Code | ✅ | ✅ |
95
115
 
96
116
  ---
97
117
 
@@ -99,13 +119,13 @@ AI 工具拿到的是正确的上下文——不是整个知识库,而是恰
99
119
 
100
120
  基于真实内部文档测试(4 份 Notion 导出,中英文混合查询):
101
121
 
102
- | 指标 | 结果 |
103
- |---|---|
104
- | Recall——相关查询找到正确文档的比例 | **100%**(16/16) |
105
- | Precision——无关查询返回空结果的比例 | **80%**(4/5) |
106
- | F1 分数 | **88.9%** |
107
- | 每次查询注入的 token 占总上下文的比例 | **约 4%** |
108
- | 支持中英文混合查询 | ✅ |
122
+ | 指标 | 结果 |
123
+ | ------------------------------------- | ----------------- |
124
+ | Recall——相关查询找到正确文档的比例 | **100%**(16/16) |
125
+ | Precision——无关查询返回空结果的比例 | **80%**(4/5) |
126
+ | F1 分数 | **88.9%** |
127
+ | 每次查询注入的 token 占总上下文的比例 | **约 4%** |
128
+ | 支持中英文混合查询 | ✅ |
109
129
 
110
130
  ---
111
131
 
@@ -133,25 +153,6 @@ gctree resolve --query 'NestJS DTO plainToInstance'
133
153
 
134
154
  ---
135
155
 
136
- ## 安装与快速开始
137
-
138
- ```bash
139
- npm install -g @handsupmin/gc-tree
140
- gctree init
141
- ```
142
-
143
- `gctree init` 会引导你:
144
- 1. 选择 provider:`claude-code`、`codex` 或 `both`
145
- 2. 在当前仓库安装集成文件
146
- 3. 为 `main` gc-branch 完成引导式 onboarding
147
-
148
- 之后,AI 工具会自动学会在规划或实现前调用 `gctree resolve`。
149
-
150
- - **CLI:** `gctree`
151
- - **要求:** Node.js 20+
152
-
153
- ---
154
-
155
156
  ## 常用操作
156
157
 
157
158
  ### 为不同工作线创建独立上下文
@@ -214,19 +215,19 @@ gctree set-repo-scope --branch client-b --exclude # 排除当前仓库
214
215
 
215
216
  ## 核心命令
216
217
 
217
- | 目标 | 命令 |
218
- |---|---|
219
- | 初始化 gc-tree 并选择 provider | `gctree init` |
220
- | 确认当前激活的 gc-branch | `gctree status` |
221
- | 搜索当前上下文 | `gctree resolve --query "..."` |
222
- | 创建或切换 gc-branch | `gctree checkout <branch>` / `gctree checkout -b <branch>` |
223
- | 列出所有 gc-branch | `gctree branches` |
224
- | 对空 gc-branch 进行引导式 onboarding | `gctree onboard` |
225
- | 对当前 gc-branch 进行引导式更新 | `gctree update-global-context` / `gctree ugc` |
226
- | 查看仓库范围规则 | `gctree repo-map` |
227
- | 为 gc-branch 包含或排除当前仓库 | `gctree set-repo-scope --branch <name> --include` / `--exclude` |
228
- | 重新 onboarding 前重置 gc-branch | `gctree reset-gc-branch --branch <name> --yes` |
229
- | 在新环境中安装 scaffold | `gctree scaffold --host codex --target /path/to/repo` |
218
+ | 目标 | 命令 |
219
+ | ------------------------------------ | --------------------------------------------------------------- |
220
+ | 初始化 gc-tree 并选择 provider | `gctree init` |
221
+ | 确认当前激活的 gc-branch | `gctree status` |
222
+ | 搜索当前上下文 | `gctree resolve --query "..."` |
223
+ | 创建或切换 gc-branch | `gctree checkout <branch>` / `gctree checkout -b <branch>` |
224
+ | 列出所有 gc-branch | `gctree branches` |
225
+ | 对空 gc-branch 进行引导式 onboarding | `gctree onboard` |
226
+ | 对当前 gc-branch 进行引导式更新 | `gctree update-global-context` / `gctree ugc` |
227
+ | 查看仓库范围规则 | `gctree repo-map` |
228
+ | 为 gc-branch 包含或排除当前仓库 | `gctree set-repo-scope --branch <name> --include` / `--exclude` |
229
+ | 重新 onboarding 前重置 gc-branch | `gctree reset-gc-branch --branch <name> --yes` |
230
+ | 在新环境中安装 scaffold | `gctree scaffold --host codex --target /path/to/repo` |
230
231
 
231
232
  ---
232
233
 
package/dist/src/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { stdin } from 'node:process';
4
+ import { dispatchGcTreeHook } from './hook.js';
4
5
  import { onboardBranch } from './onboard.js';
5
6
  import { buildProviderLaunchPlan, maybeLaunchProvider, promptLanguageSelection, promptLaunchProviderSelection, promptProviderSelection, } from './provider.js';
6
7
  import { DEFAULT_BRANCH, resolveHome } from './paths.js';
@@ -37,12 +38,20 @@ function usage() {
37
38
  Internal commands:
38
39
  gctree __apply-onboarding --input FILE [--home DIR] [--branch NAME]
39
40
  gctree __apply-update --input FILE [--home DIR] [--branch NAME]
41
+ gctree __hook --event <SessionStart|UserPromptSubmit> [--home DIR]
40
42
  `);
41
43
  process.exit(1);
42
44
  }
43
45
  async function readJsonFile(path) {
44
46
  return JSON.parse(await readFile(path, 'utf8'));
45
47
  }
48
+ async function readStdinText() {
49
+ const chunks = [];
50
+ for await (const chunk of process.stdin) {
51
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
52
+ }
53
+ return Buffer.concat(chunks).toString('utf8');
54
+ }
46
55
  function normalizeProvider(value) {
47
56
  if (!value)
48
57
  return undefined;
@@ -337,6 +346,22 @@ async function main() {
337
346
  console.log(JSON.stringify(result, null, 2));
338
347
  return;
339
348
  }
349
+ case '__hook': {
350
+ const event = readArg('--event');
351
+ if (event !== 'SessionStart' && event !== 'UserPromptSubmit')
352
+ usage();
353
+ const raw = await readStdinText();
354
+ const payload = raw.trim() ? JSON.parse(raw) : {};
355
+ const result = await dispatchGcTreeHook({
356
+ event,
357
+ home,
358
+ payload,
359
+ });
360
+ if (result) {
361
+ console.log(JSON.stringify(result, null, 2));
362
+ }
363
+ return;
364
+ }
340
365
  case 'scaffold': {
341
366
  const host = normalizeProviderMode(readArg('--host'));
342
367
  if (!host)
@@ -0,0 +1,209 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { createHash } from 'node:crypto';
3
+ import { dirname } from 'node:path';
4
+ import { DEFAULT_BRANCH } from './paths.js';
5
+ import { hookCachePath, } from './paths.js';
6
+ import { branchScopeStatus, readBranchRepoMap, resolveBranchForRepo } from './repo-map.js';
7
+ import { resolveContext } from './resolve.js';
8
+ import { readHead, statusForBranch } from './store.js';
9
+ function normalizeText(value) {
10
+ return value.trim().replace(/\s+/g, ' ');
11
+ }
12
+ function hashQuery(query) {
13
+ return createHash('sha1').update(normalizeText(query).toLowerCase()).digest('hex');
14
+ }
15
+ function limitMatches(matches, max = 3) {
16
+ return matches.slice(0, max);
17
+ }
18
+ function formatMatches(matches) {
19
+ return limitMatches(matches)
20
+ .map((match, index) => `${index + 1}. ${match.title}\n Summary: ${match.summary}\n Excerpt: ${match.excerpt}`)
21
+ .join('\n');
22
+ }
23
+ function buildSessionStartContext({ gcBranch, currentRepo, repoScopeStatus, }) {
24
+ return [
25
+ `gc-tree auto-resolve is active for this session.`,
26
+ `Active gc-branch: "${gcBranch}".`,
27
+ `Current repo: ${currentRepo || 'unscoped'}.`,
28
+ `Repo scope status: ${repoScopeStatus}.`,
29
+ `Before acting on new user prompts, use gc-tree hook context first. If the hook reports no reusable global context, avoid redundant resolve calls for the same session state unless the repo, gc-branch, or task changes materially.`,
30
+ ].join(' ');
31
+ }
32
+ function buildEmptyBranchContext({ gcBranch, currentRepo, cached, }) {
33
+ return [
34
+ `gc-tree auto-resolve ${cached ? 'used cached state' : 'checked the active gc-branch'} and found no reusable global context for this session because gc-branch "${gcBranch}" currently has 0 docs.`,
35
+ `Repo: ${currentRepo || 'unscoped'}.`,
36
+ `Treat this as "no reusable global context yet" until the gc-branch content changes or a different repo/gc-branch is active.`,
37
+ ].join(' ');
38
+ }
39
+ function buildExcludedContext({ gcBranch, currentRepo, cached, }) {
40
+ return [
41
+ `gc-tree auto-resolve ${cached ? 'used cached state' : 'checked the active gc-branch'} and found that repo "${currentRepo || 'unknown'}" is excluded from gc-branch "${gcBranch}".`,
42
+ `No reusable global context applies here unless the branch-repo mapping changes.`,
43
+ ].join(' ');
44
+ }
45
+ function buildNoMatchContext({ gcBranch, currentRepo, query, cached, }) {
46
+ return [
47
+ `gc-tree auto-resolve ${cached ? 'skipped a redundant lookup using cached no-match state' : 'checked the active gc-branch'} and found no reusable global context for this prompt in gc-branch "${gcBranch}".`,
48
+ `Repo: ${currentRepo || 'unscoped'}.`,
49
+ `Query: "${query}".`,
50
+ `Only re-run resolve if the repo, gc-branch, or task changes materially.`,
51
+ ].join(' ');
52
+ }
53
+ function buildMatchContext({ gcBranch, currentRepo, repoScopeStatus, query, matches, }) {
54
+ return [
55
+ `gc-tree auto-resolve checked gc-branch "${gcBranch}" for repo "${currentRepo || 'unscoped'}" (scope: ${repoScopeStatus}) before this prompt.`,
56
+ `Query: "${query}".`,
57
+ `Use these matching gc-tree summaries before planning or implementation:`,
58
+ formatMatches(matches),
59
+ `Read full docs only if the summaries are insufficient.`,
60
+ ].join('\n');
61
+ }
62
+ async function readHookCache(home, sessionId) {
63
+ try {
64
+ const raw = await readFile(hookCachePath(home, sessionId), 'utf8');
65
+ return JSON.parse(raw);
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ async function writeHookCache(home, cache) {
72
+ const path = hookCachePath(home, cache.session_id);
73
+ await mkdir(dirname(path), { recursive: true });
74
+ await writeFile(path, `${JSON.stringify(cache, null, 2)}\n`, 'utf8');
75
+ }
76
+ function nextCacheState(previous, { sessionId, gcBranch, currentRepo, repoScopeStatus, }) {
77
+ const changed = !previous ||
78
+ previous.gc_branch !== gcBranch ||
79
+ previous.current_repo !== currentRepo ||
80
+ previous.repo_scope_status !== repoScopeStatus;
81
+ return {
82
+ version: 1,
83
+ session_id: sessionId,
84
+ gc_branch: gcBranch,
85
+ current_repo: currentRepo,
86
+ repo_scope_status: repoScopeStatus,
87
+ branch_empty: changed ? false : previous.branch_empty,
88
+ branch_excluded: changed ? false : previous.branch_excluded,
89
+ no_match_signatures: changed ? [] : previous.no_match_signatures,
90
+ updated_at: new Date().toISOString(),
91
+ };
92
+ }
93
+ function readSessionId(payload) {
94
+ const raw = normalizeText(payload.session_id || '');
95
+ return raw || 'default-session';
96
+ }
97
+ export async function dispatchGcTreeHook({ event, home, payload, }) {
98
+ const cwd = payload.cwd || process.cwd();
99
+ const head = (await readHead(home)) || DEFAULT_BRANCH;
100
+ const resolved = await resolveBranchForRepo({ home, head, cwd });
101
+ const gcBranch = resolved.gc_branch;
102
+ const currentRepo = resolved.current_repo;
103
+ const mapping = await readBranchRepoMap(home);
104
+ const repoScopeStatus = branchScopeStatus(mapping, gcBranch, currentRepo);
105
+ const sessionId = readSessionId(payload);
106
+ if (event === 'SessionStart') {
107
+ return {
108
+ hookSpecificOutput: {
109
+ hookEventName: event,
110
+ additionalContext: buildSessionStartContext({
111
+ gcBranch,
112
+ currentRepo,
113
+ repoScopeStatus,
114
+ }),
115
+ },
116
+ };
117
+ }
118
+ const prompt = normalizeText(payload.user_prompt || '');
119
+ if (!prompt)
120
+ return null;
121
+ const branchStatus = await statusForBranch(home, gcBranch);
122
+ const previousCache = await readHookCache(home, sessionId);
123
+ const cache = nextCacheState(previousCache, {
124
+ sessionId,
125
+ gcBranch,
126
+ currentRepo,
127
+ repoScopeStatus,
128
+ });
129
+ if (repoScopeStatus === 'excluded') {
130
+ cache.branch_excluded = true;
131
+ cache.updated_at = new Date().toISOString();
132
+ await writeHookCache(home, cache);
133
+ return {
134
+ hookSpecificOutput: {
135
+ hookEventName: event,
136
+ additionalContext: buildExcludedContext({
137
+ gcBranch,
138
+ currentRepo,
139
+ cached: previousCache?.branch_excluded === true,
140
+ }),
141
+ },
142
+ };
143
+ }
144
+ if (branchStatus.doc_count === 0) {
145
+ const wasCached = cache.branch_empty;
146
+ cache.branch_empty = true;
147
+ cache.updated_at = new Date().toISOString();
148
+ await writeHookCache(home, cache);
149
+ return {
150
+ hookSpecificOutput: {
151
+ hookEventName: event,
152
+ additionalContext: buildEmptyBranchContext({
153
+ gcBranch,
154
+ currentRepo,
155
+ cached: wasCached,
156
+ }),
157
+ },
158
+ };
159
+ }
160
+ const query = currentRepo ? `${currentRepo} ${prompt}` : prompt;
161
+ const signature = hashQuery(query);
162
+ if (cache.no_match_signatures.includes(signature)) {
163
+ cache.updated_at = new Date().toISOString();
164
+ await writeHookCache(home, cache);
165
+ return {
166
+ hookSpecificOutput: {
167
+ hookEventName: event,
168
+ additionalContext: buildNoMatchContext({
169
+ gcBranch,
170
+ currentRepo,
171
+ query,
172
+ cached: true,
173
+ }),
174
+ },
175
+ };
176
+ }
177
+ const result = await resolveContext({ home, branch: gcBranch, query });
178
+ if (result.matches.length === 0) {
179
+ cache.no_match_signatures = [...new Set([...cache.no_match_signatures, signature])];
180
+ cache.updated_at = new Date().toISOString();
181
+ await writeHookCache(home, cache);
182
+ return {
183
+ hookSpecificOutput: {
184
+ hookEventName: event,
185
+ additionalContext: buildNoMatchContext({
186
+ gcBranch,
187
+ currentRepo,
188
+ query,
189
+ cached: false,
190
+ }),
191
+ },
192
+ };
193
+ }
194
+ cache.no_match_signatures = cache.no_match_signatures.filter((entry) => entry !== signature);
195
+ cache.updated_at = new Date().toISOString();
196
+ await writeHookCache(home, cache);
197
+ return {
198
+ hookSpecificOutput: {
199
+ hookEventName: event,
200
+ additionalContext: buildMatchContext({
201
+ gcBranch,
202
+ currentRepo,
203
+ repoScopeStatus,
204
+ query,
205
+ matches: result.matches,
206
+ }),
207
+ },
208
+ };
209
+ }
package/dist/src/paths.js CHANGED
@@ -26,3 +26,9 @@ export function branchIndexPath(home, branch) {
26
26
  export function branchDocsDir(home, branch) {
27
27
  return join(branchDir(home, branch), 'docs');
28
28
  }
29
+ export function hookCacheDir(home) {
30
+ return join(home, 'hook-cache');
31
+ }
32
+ export function hookCachePath(home, sessionId) {
33
+ return join(hookCacheDir(home), `${sessionId}.json`);
34
+ }
@@ -6,8 +6,8 @@ function renderCodexAgentsSnippet() {
6
6
  '# gctree Codex integration snippet',
7
7
  '',
8
8
  '- Treat the active gctree branch as a **gc-branch** when you describe it to users.',
9
- '- Before planning or implementation, run `gctree status` to confirm the active gc-branch if it is unclear.',
10
- '- Use `gctree resolve --query "<task>"` when reusable global context may matter.',
9
+ '- gctree init installs SessionStart and UserPromptSubmit hooks that auto-check gc-tree before work.',
10
+ '- Use the hook-injected gc-tree context first. If hooks are unavailable or clearly stale, run `gctree status` and `gctree resolve --query "<task>"` yourself before planning or implementation.',
11
11
  '- Use `$gc-onboard` only for an empty gc-branch.',
12
12
  '- Use `$gc-update-global-context` when durable context in the active gc-branch should change.',
13
13
  '',
@@ -18,12 +18,39 @@ function renderCodexBootstrapPrompt() {
18
18
  '# gctree Bootstrap',
19
19
  '',
20
20
  '- Keep the active gc-branch explicit whenever global context matters.',
21
- '- Resolve reusable global context before planning or implementation when it may change the answer.',
21
+ '- Trust auto-resolved gc-tree hook context first; if it is missing, resolve reusable global context before planning or implementation.',
22
22
  '- Read summaries first and only open full docs when needed.',
23
23
  '- Treat gctree docs as explicit source-of-truth markdown, not hidden memory.',
24
24
  '',
25
25
  ].join('\n');
26
26
  }
27
+ function renderCodexHooksJson() {
28
+ return JSON.stringify({
29
+ hooks: {
30
+ SessionStart: [
31
+ {
32
+ matcher: 'startup|resume',
33
+ hooks: [
34
+ {
35
+ type: 'command',
36
+ command: 'gctree __hook --event SessionStart',
37
+ },
38
+ ],
39
+ },
40
+ ],
41
+ UserPromptSubmit: [
42
+ {
43
+ hooks: [
44
+ {
45
+ type: 'command',
46
+ command: 'gctree __hook --event UserPromptSubmit',
47
+ },
48
+ ],
49
+ },
50
+ ],
51
+ },
52
+ }, null, 2);
53
+ }
27
54
  function renderCodexResolveSkill() {
28
55
  return [
29
56
  '---',
@@ -80,8 +107,8 @@ function renderClaudeSnippet() {
80
107
  '# gctree Claude Code integration snippet',
81
108
  '',
82
109
  '- Treat the active gctree branch as a **gc-branch** in user-facing language.',
83
- '- Run `gctree status` before relying on global context if the active gc-branch is unclear.',
84
- '- Use `gctree resolve --query "<task>"` when reusable global context may matter.',
110
+ '- gctree init installs SessionStart and UserPromptSubmit hooks that auto-check gc-tree before work.',
111
+ '- Use the hook-injected gc-tree context first. If hooks are unavailable or clearly stale, run `gctree status` and `gctree resolve --query "<task>"` yourself before planning or implementation.',
85
112
  '- Use `/gc-onboard` only for an empty gc-branch.',
86
113
  '- Use `/gc-update-global-context` when durable context in the active gc-branch should change.',
87
114
  '',
@@ -91,12 +118,43 @@ function renderClaudeSessionStartHook() {
91
118
  return [
92
119
  '# gctree Claude Code SessionStart note',
93
120
  '',
94
- '- At session start, confirm the active gc-branch with `gctree status` when reusable global context may matter.',
121
+ '- gctree init installs real SessionStart/UserPromptSubmit hooks via `.claude/hooks/hooks.json`.',
122
+ '- At session start, use the injected hook context to confirm the active gc-branch.',
95
123
  '- Refer to gctree branches as **gc-branches** in user-facing language.',
96
- '- Resolve summaries before planning or implementation when branch-level context may change the answer.',
124
+ '- If hook context is missing or stale, resolve summaries before planning or implementation when branch-level context may change the answer.',
97
125
  '',
98
126
  ].join('\n');
99
127
  }
128
+ function renderClaudeHooksJson() {
129
+ return JSON.stringify({
130
+ hooks: {
131
+ SessionStart: [
132
+ {
133
+ matcher: '*',
134
+ hooks: [
135
+ {
136
+ type: 'command',
137
+ command: 'gctree __hook --event SessionStart',
138
+ timeout: 10,
139
+ },
140
+ ],
141
+ },
142
+ ],
143
+ UserPromptSubmit: [
144
+ {
145
+ matcher: '*',
146
+ hooks: [
147
+ {
148
+ type: 'command',
149
+ command: 'gctree __hook --event UserPromptSubmit',
150
+ timeout: 10,
151
+ },
152
+ ],
153
+ },
154
+ ],
155
+ },
156
+ }, null, 2);
157
+ }
100
158
  function renderClaudeResolveCommand() {
101
159
  return [
102
160
  '---',
@@ -150,6 +208,7 @@ function scaffoldFiles(host) {
150
208
  if (host === 'codex') {
151
209
  return [
152
210
  { path: 'AGENTS.md', content: renderCodexAgentsSnippet() },
211
+ { path: '.codex/hooks.json', content: renderCodexHooksJson() },
153
212
  { path: '.codex/prompts/gctree-bootstrap.md', content: renderCodexBootstrapPrompt() },
154
213
  { path: '.codex/skills/gc-resolve-context/SKILL.md', content: renderCodexResolveSkill() },
155
214
  { path: '.codex/skills/gc-onboard/SKILL.md', content: renderCodexOnboardSkill() },
@@ -158,6 +217,7 @@ function scaffoldFiles(host) {
158
217
  }
159
218
  return [
160
219
  { path: 'CLAUDE.md', content: renderClaudeSnippet() },
220
+ { path: '.claude/hooks/hooks.json', content: renderClaudeHooksJson() },
161
221
  { path: '.claude/hooks/gctree-session-start.md', content: renderClaudeSessionStartHook() },
162
222
  { path: '.claude/commands/gc-resolve-context.md', content: renderClaudeResolveCommand() },
163
223
  { path: '.claude/commands/gc-onboard.md', content: renderClaudeOnboardCommand() },