@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.es.md +60 -59
- package/README.ja.md +60 -59
- package/README.ko.md +62 -61
- package/README.md +61 -60
- package/README.zh.md +62 -61
- package/dist/src/cli.js +25 -0
- package/dist/src/hook.js +209 -0
- package/dist/src/paths.js +6 -0
- package/dist/src/scaffold.js +67 -7
- package/docs/concept.es.md +45 -33
- package/docs/concept.ja.md +41 -29
- package/docs/concept.ko.md +48 -36
- package/docs/concept.md +13 -1
- package/docs/concept.zh.md +48 -36
- package/docs/local-development.es.md +44 -22
- package/docs/local-development.ja.md +40 -18
- package/docs/local-development.ko.md +47 -25
- package/docs/local-development.md +27 -5
- package/docs/local-development.zh.md +27 -5
- package/docs/principles.es.md +39 -26
- package/docs/principles.ja.md +26 -13
- package/docs/principles.ko.md +40 -27
- package/docs/principles.md +15 -2
- package/docs/principles.zh.md +42 -29
- package/docs/usage.es.md +100 -50
- package/docs/usage.ja.md +73 -23
- package/docs/usage.ko.md +102 -52
- package/docs/usage.md +53 -3
- package/docs/usage.zh.md +97 -47
- package/package.json +3 -1
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
|
-
|
|
|
87
|
-
|
|
88
|
-
| 作用范围
|
|
89
|
-
| 持久化方式
|
|
90
|
-
| 切换上下文
|
|
91
|
-
| 相关性过滤
|
|
92
|
-
| 初始化方式
|
|
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——相关查询找到正确文档的比例
|
|
105
|
-
| Precision——无关查询返回空结果的比例
|
|
106
|
-
| F1 分数
|
|
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
|
|
220
|
-
| 确认当前激活的 gc-branch
|
|
221
|
-
| 搜索当前上下文
|
|
222
|
-
| 创建或切换 gc-branch
|
|
223
|
-
| 列出所有 gc-branch
|
|
224
|
-
| 对空 gc-branch 进行引导式 onboarding | `gctree onboard`
|
|
225
|
-
| 对当前 gc-branch 进行引导式更新
|
|
226
|
-
| 查看仓库范围规则
|
|
227
|
-
| 为 gc-branch 包含或排除当前仓库
|
|
228
|
-
| 重新 onboarding 前重置 gc-branch
|
|
229
|
-
| 在新环境中安装 scaffold
|
|
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)
|
package/dist/src/hook.js
ADDED
|
@@ -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
|
+
}
|
package/dist/src/scaffold.js
CHANGED
|
@@ -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
|
-
'-
|
|
10
|
-
'- Use `gctree resolve --query "<task>"`
|
|
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
|
-
'-
|
|
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
|
-
'-
|
|
84
|
-
'- Use `gctree resolve --query "<task>"`
|
|
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
|
-
'-
|
|
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
|
-
'-
|
|
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() },
|