@handsupmin/gc-tree 0.2.1 → 0.4.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 CHANGED
@@ -62,7 +62,7 @@ gctree init
62
62
  2. Instalar los archivos de integración en el repositorio actual
63
63
  3. Completar el onboarding guiado para el gc-branch `main`
64
64
 
65
- Después, tu herramienta de IA aprenderá a llamar a `gctree resolve` antes de planificar o implementar.
65
+ Después, tu herramienta de IA instala integración real de hooks SessionStart/UserPromptSubmit: revisa gc-tree automáticamente antes de trabajar y cachea resultados vacíos/no-match durante la sesión.
66
66
 
67
67
  - **CLI:** `gctree`
68
68
  - **Requisito:** Node.js 20+
@@ -73,6 +73,14 @@ Después, tu herramienta de IA aprenderá a llamar a `gctree resolve` antes de p
73
73
 
74
74
  `gc-tree` opera **por encima del nivel del repositorio**. Almacena contexto en archivos Markdown estructurados y permite que tus herramientas de IA traigan solo lo relevante antes de cada sesión, de forma automática.
75
75
 
76
+ `gctree resolve` es la **capa de índice compacto** dentro de un flujo de progressive disclosure:
77
+
78
+ - `gctree resolve --query "..."` → lista compacta de coincidencias con IDs estables
79
+ - `gctree related --id <match-id>` → documentos de apoyo alrededor de una coincidencia
80
+ - `gctree show-doc --id <match-id>` → markdown completo de ese documento
81
+
82
+ Además, cuando la branch está vacía, el repositorio está excluido o la consulta no tiene resultados, gc-tree devuelve un estado explícito en lugar de fallar de forma ambigua.
83
+
76
84
  ```bash
77
85
  gctree resolve --query "auth token rotation policy"
78
86
  ```
package/README.ja.md CHANGED
@@ -62,7 +62,7 @@ gctree init
62
62
  2. 現在のリポジトリへの統合ファイルのインストール
63
63
  3. `main` gc-branch のガイド付きオンボーディング実行
64
64
 
65
- その後は、AI ツールが計画・実装の前に `gctree resolve` を呼ぶ流れを自然に覚えます。
65
+ その後は、AI ツールに実際の SessionStart / UserPromptSubmit フック統合が入り、作業前に gc-tree を自動確認し、空結果や no-match をセッション中キャッシュします。
66
66
 
67
67
  - **CLI:** `gctree`
68
68
  - **動作条件:** Node.js 20+
@@ -73,6 +73,14 @@ gctree init
73
73
 
74
74
  `gc-tree` は**リポジトリより上のレイヤー**に位置します。コンテキストを構造化された Markdown ファイルに保存し、AI ツールがセッションのたびに関連するものだけを自動で引き出せるようにします。
75
75
 
76
+ `gctree resolve` は、progressive-disclosure ワークフローにおける **compact index layer** です。
77
+
78
+ - `gctree resolve --query "..."` → stable ID 付きの compact なマッチ一覧
79
+ - `gctree related --id <match-id>` → そのマッチの周辺にある supporting docs
80
+ - `gctree show-doc --id <match-id>` → そのドキュメントの完全な markdown
81
+
82
+ また、文書が空・リポジトリが除外対象・クエリ結果なし、といった場合も曖昧に失敗せず、明示的な status を返します。
83
+
76
84
  ```bash
77
85
  gctree resolve --query "auth token rotation policy"
78
86
  ```
package/README.ko.md CHANGED
@@ -62,7 +62,7 @@ gctree init
62
62
  2. 현재 레포에 통합 파일 스캐폴딩
63
63
  3. `main` gc-branch 가이드형 온보딩 실행
64
64
 
65
- 이후 AI 도구가 계획·구현 전에 `gctree resolve`를 호출하는 법을 자동으로 알게 됩니다.
65
+ 이후 AI 도구에 실제 SessionStart/UserPromptSubmit 훅 통합이 설치되어, 작업 전에 gc-tree를 자동 확인하고 세션 동안 결과·no-match를 캐시합니다.
66
66
 
67
67
  - **CLI:** `gctree`
68
68
  - **요구 사항:** Node.js 20+
@@ -73,6 +73,14 @@ gctree init
73
73
 
74
74
  `gc-tree`는 **레포 위 레벨**에 위치합니다. 컨텍스트를 구조화된 마크다운 파일에 저장하고, AI 도구가 매 세션마다 관련된 것만 자동으로 가져오게 합니다.
75
75
 
76
+ `gctree resolve`는 progressive-disclosure 워크플로우의 **compact index layer**입니다:
77
+
78
+ - `gctree resolve --query "..."` → stable ID가 포함된 compact match 목록
79
+ - `gctree related --id <match-id>` → 특정 match 주변의 supporting docs
80
+ - `gctree show-doc --id <match-id>` → 해당 문서의 전체 markdown
81
+
82
+ 또한 문서가 비어 있거나, 저장소가 제외되어 있거나, 쿼리 결과가 없을 때는 모호하게 실패하지 않고 명시적인 status를 반환합니다.
83
+
76
84
  ```bash
77
85
  gctree resolve --query "auth token rotation policy"
78
86
  ```
package/README.md CHANGED
@@ -62,7 +62,7 @@ gctree init
62
62
  2. Scaffold the integration files into your current repo
63
63
  3. Run guided onboarding for the `main` gc-branch
64
64
 
65
- After that, your AI tool will know to call `gctree resolve` before planning or implementing.
65
+ After that, your AI tool gets real SessionStart/UserPromptSubmit hook integration, so it auto-checks gc-tree before work and caches empty/no-match results for the session.
66
66
 
67
67
  - **CLI:** `gctree`
68
68
  - **Requires:** Node.js 20+
@@ -73,6 +73,14 @@ After that, your AI tool will know to call `gctree resolve` before planning or i
73
73
 
74
74
  `gc-tree` sits **above the repo level**. It stores durable context in structured markdown files and lets your AI tools pull only what's relevant — before each session, automatically.
75
75
 
76
+ `gctree resolve` is the compact **index layer** in a progressive-disclosure workflow:
77
+
78
+ - `gctree resolve --query "..."` → compact matches with stable IDs
79
+ - `gctree related --id <match-id>` → supporting docs around one match
80
+ - `gctree show-doc --id <match-id>` → full markdown for that doc
81
+
82
+ When no docs exist, a repo is excluded, or a query has no hits, gc-tree returns an explicit status instead of failing ambiguously.
83
+
76
84
  ```bash
77
85
  gctree resolve --query "auth token rotation policy"
78
86
  ```
package/README.zh.md CHANGED
@@ -49,7 +49,7 @@ gctree init
49
49
  2. 在当前仓库安装集成文件
50
50
  3. 为 `main` gc-branch 完成引导式 onboarding
51
51
 
52
- 之后,AI 工具会自动学会在规划或实现前调用 `gctree resolve`。
52
+ 之后,AI 工具会安装真正的 SessionStart / UserPromptSubmit hook 集成:在开始工作前自动检查 gc-tree,并在整个会话内缓存空结果和 no-match。
53
53
 
54
54
  - **CLI:** `gctree`
55
55
  - **要求:** Node.js 20+
@@ -73,6 +73,14 @@ gctree init
73
73
 
74
74
  `gc-tree` 工作在**仓库之上**。它把上下文存储为结构化的 Markdown 文件,让 AI 工具在每次会话前自动拉取相关内容。
75
75
 
76
+ `gctree resolve` 是 progressive-disclosure 工作流中的**紧凑索引层**:
77
+
78
+ - `gctree resolve --query "..."` → 返回带稳定 ID 的紧凑匹配列表
79
+ - `gctree related --id <match-id>` → 返回某个匹配周围的 supporting docs
80
+ - `gctree show-doc --id <match-id>` → 返回该文档的完整 markdown
81
+
82
+ 另外,当 gc-branch 为空、仓库被排除、或查询没有匹配时,gc-tree 会返回明确的状态,而不是模糊地失败。
83
+
76
84
  ```bash
77
85
  gctree resolve --query "auth token rotation policy"
78
86
  ```
package/dist/src/cli.js CHANGED
@@ -1,11 +1,12 @@
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';
7
8
  import { branchRepoMapPath, branchScopeStatus, detectCurrentRepoId, promptResolveScopeDecision, readBranchRepoMap, resolveBranchForRepo, setRepoScopeForBranch, } from './repo-map.js';
8
- import { resolveContext } from './resolve.js';
9
+ import { findRelatedDocs, getDocById, resolveContext } from './resolve.js';
9
10
  import { scaffoldHostIntegration } from './scaffold.js';
10
11
  import { requirePreferredProvider, writeSettings, readSettings } from './settings.js';
11
12
  import { checkoutBranch, initHome, listBranches, readHead, resetBranchContext, statusForBranch, ensureBranchExists, isBranchContextEmpty } from './store.js';
@@ -29,6 +30,8 @@ function usage() {
29
30
  gctree onboard [--home DIR] [--branch NAME] [--provider <codex|claude-code>] [--target DIR] [--no-launch]
30
31
  gctree reset-gc-branch [--home DIR] [--branch NAME] --yes
31
32
  gctree resolve --query TEXT [--home DIR] [--branch NAME] [--cwd DIR]
33
+ gctree show-doc --id ID [--home DIR] [--branch NAME]
34
+ gctree related --id ID [--home DIR] [--branch NAME]
32
35
  gctree update-global-context [--home DIR] [--branch NAME] [--provider <codex|claude-code>] [--target DIR] [--no-launch]
33
36
  gctree update-gc [--home DIR] [--branch NAME] [--provider <codex|claude-code>] [--target DIR] [--no-launch]
34
37
  gctree ugc [--home DIR] [--branch NAME] [--provider <codex|claude-code>] [--target DIR] [--no-launch]
@@ -37,12 +40,28 @@ function usage() {
37
40
  Internal commands:
38
41
  gctree __apply-onboarding --input FILE [--home DIR] [--branch NAME]
39
42
  gctree __apply-update --input FILE [--home DIR] [--branch NAME]
43
+ gctree __hook --event <SessionStart|UserPromptSubmit> [--home DIR]
40
44
  `);
41
45
  process.exit(1);
42
46
  }
43
47
  async function readJsonFile(path) {
44
48
  return JSON.parse(await readFile(path, 'utf8'));
45
49
  }
50
+ async function readStdinText() {
51
+ const chunks = [];
52
+ for await (const chunk of process.stdin) {
53
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
54
+ }
55
+ return Buffer.concat(chunks).toString('utf8');
56
+ }
57
+ function compactMatchCommands(id, home, branch) {
58
+ const homeArg = `--home ${JSON.stringify(home)}`;
59
+ const branchArg = `--branch ${JSON.stringify(branch)}`;
60
+ return {
61
+ show_doc: `gctree show-doc --id ${JSON.stringify(id)} ${homeArg} ${branchArg}`,
62
+ related: `gctree related --id ${JSON.stringify(id)} ${homeArg} ${branchArg}`,
63
+ };
64
+ }
46
65
  function normalizeProvider(value) {
47
66
  if (!value)
48
67
  return undefined;
@@ -285,23 +304,98 @@ async function main() {
285
304
  }
286
305
  if (currentRepo && scopeStatus === 'excluded') {
287
306
  console.log(JSON.stringify({
307
+ status: 'excluded',
288
308
  gc_branch: gcBranch,
289
309
  query,
290
310
  current_repo: currentRepo,
291
311
  source: resolved.source,
292
312
  repo_scope_status: 'excluded',
313
+ message: `Repo "${currentRepo}" is excluded from gc-branch "${gcBranch}".`,
314
+ matches: [],
315
+ }, null, 2));
316
+ return;
317
+ }
318
+ const branchStatus = await statusForBranch(home, gcBranch);
319
+ if (branchStatus.doc_count === 0) {
320
+ console.log(JSON.stringify({
321
+ status: 'empty_branch',
322
+ gc_branch: gcBranch,
323
+ query,
324
+ current_repo: currentRepo,
325
+ source: resolved.source,
326
+ repo_scope_status: scopeStatus,
327
+ message: `gc-branch "${gcBranch}" has no docs yet. Run gctree onboard to add durable context.`,
293
328
  matches: [],
294
329
  }, null, 2));
295
330
  return;
296
331
  }
297
332
  const result = await resolveContext({ home, branch: gcBranch, query });
298
333
  console.log(JSON.stringify({
334
+ status: result.matches.length > 0 ? 'matched' : 'no_match',
299
335
  gc_branch: result.branch,
300
336
  query: result.query,
301
337
  current_repo: currentRepo,
302
338
  source: resolved.source,
303
339
  repo_scope_status: scopeStatus,
304
- matches: result.matches,
340
+ message: result.matches.length > 0
341
+ ? `Found ${result.matches.length} matching docs. Use show-doc/related for progressive disclosure.`
342
+ : `No matching docs found in gc-branch "${gcBranch}" for this query.`,
343
+ matches: result.matches.map((match) => ({
344
+ ...match,
345
+ commands: compactMatchCommands(match.id, home, gcBranch),
346
+ })),
347
+ }, null, 2));
348
+ return;
349
+ }
350
+ case 'show-doc': {
351
+ const id = readArg('--id');
352
+ if (!id)
353
+ usage();
354
+ const gcBranch = readArg('--branch') || (await readHead(home)) || DEFAULT_BRANCH;
355
+ const branchStatus = await statusForBranch(home, gcBranch);
356
+ if (branchStatus.doc_count === 0) {
357
+ console.log(JSON.stringify({ status: 'empty_branch', gc_branch: gcBranch, id, message: `gc-branch "${gcBranch}" has no docs yet.`, doc: null }, null, 2));
358
+ return;
359
+ }
360
+ const doc = await getDocById({ home, branch: gcBranch, id });
361
+ console.log(JSON.stringify({
362
+ status: doc ? 'matched' : 'doc_not_found',
363
+ gc_branch: gcBranch,
364
+ id,
365
+ message: doc ? `Loaded doc "${id}".` : `Doc "${id}" was not found in gc-branch "${gcBranch}".`,
366
+ doc,
367
+ }, null, 2));
368
+ return;
369
+ }
370
+ case 'related': {
371
+ const id = readArg('--id');
372
+ if (!id)
373
+ usage();
374
+ const gcBranch = readArg('--branch') || (await readHead(home)) || DEFAULT_BRANCH;
375
+ const result = await findRelatedDocs({ home, branch: gcBranch, id });
376
+ console.log(JSON.stringify({
377
+ status: result.status,
378
+ gc_branch: gcBranch,
379
+ id,
380
+ message: result.status === 'matched'
381
+ ? `Found ${result.matches.length} related docs for "${id}".`
382
+ : result.status === 'no_related_docs'
383
+ ? `No related docs found for "${id}".`
384
+ : result.status === 'doc_not_found'
385
+ ? `Doc "${id}" was not found in gc-branch "${gcBranch}".`
386
+ : `gc-branch "${gcBranch}" has no docs yet.`,
387
+ selected: result.selected
388
+ ? {
389
+ id: result.selected.id,
390
+ title: result.selected.title,
391
+ path: result.selected.path,
392
+ summary: result.selected.summary,
393
+ }
394
+ : null,
395
+ matches: result.matches.map((match) => ({
396
+ ...match,
397
+ commands: compactMatchCommands(match.id, home, gcBranch),
398
+ })),
305
399
  }, null, 2));
306
400
  return;
307
401
  }
@@ -337,6 +431,22 @@ async function main() {
337
431
  console.log(JSON.stringify(result, null, 2));
338
432
  return;
339
433
  }
434
+ case '__hook': {
435
+ const event = readArg('--event');
436
+ if (event !== 'SessionStart' && event !== 'UserPromptSubmit')
437
+ usage();
438
+ const raw = await readStdinText();
439
+ const payload = raw.trim() ? JSON.parse(raw) : {};
440
+ const result = await dispatchGcTreeHook({
441
+ event,
442
+ home,
443
+ payload,
444
+ });
445
+ if (result) {
446
+ console.log(JSON.stringify(result, null, 2));
447
+ }
448
+ return;
449
+ }
340
450
  case 'scaffold': {
341
451
  const host = normalizeProviderMode(readArg('--host'));
342
452
  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} [id=${match.id}]\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
+ }
@@ -52,6 +52,13 @@ export function renderIndexMarkdown(input) {
52
52
  }
53
53
  return lines.join('\n');
54
54
  }
55
+ export function docIdFromPath(docPath) {
56
+ const fileName = String(docPath || '')
57
+ .trim()
58
+ .split('/')
59
+ .pop() || '';
60
+ return fileName.replace(/\.md$/i, '') || 'doc';
61
+ }
55
62
  export function parseIndexEntries(indexContent) {
56
63
  return String(indexContent || '')
57
64
  .split(/\r?\n/)
@@ -60,7 +67,7 @@ export function parseIndexEntries(indexContent) {
60
67
  .map((line) => {
61
68
  const body = line.slice(2);
62
69
  const [title, path] = body.split('->').map((part) => part.trim());
63
- return { title, path };
70
+ return { id: docIdFromPath(path), title, path };
64
71
  });
65
72
  }
66
73
  export function extractSummary(markdown) {
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
+ }
@@ -1,6 +1,6 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
- import { extractExcerpt, extractSummary, parseIndexEntries } from './markdown.js';
3
+ import { docIdFromPath, extractExcerpt, extractSummary, parseIndexEntries } from './markdown.js';
4
4
  import { branchDir, branchIndexPath } from './paths.js';
5
5
  // Common English stop words that are too short or generic to be meaningful query signals.
6
6
  const STOP_WORDS = new Set([
@@ -41,27 +41,39 @@ function scoreText(text, query) {
41
41
  }
42
42
  }, 0);
43
43
  }
44
- export async function resolveContext({ home, branch, query, }) {
44
+ async function readBranchDocs(home, branch) {
45
45
  const indexRaw = await readFile(branchIndexPath(home, branch), 'utf8');
46
46
  const entries = parseIndexEntries(indexRaw);
47
- const matches = [];
48
- for (const entry of entries) {
47
+ return Promise.all(entries.map(async (entry) => {
49
48
  const fullPath = join(branchDir(home, branch), entry.path);
50
49
  const raw = await readFile(fullPath, 'utf8');
51
- const summary = extractSummary(raw);
52
- const combined = `${entry.title}\n${summary}\n${raw}`;
50
+ return {
51
+ id: entry.id || docIdFromPath(entry.path),
52
+ title: entry.title,
53
+ path: entry.path,
54
+ summary: extractSummary(raw),
55
+ content: raw,
56
+ };
57
+ }));
58
+ }
59
+ export async function resolveContext({ home, branch, query, }) {
60
+ const entries = await readBranchDocs(home, branch);
61
+ const matches = [];
62
+ for (const entry of entries) {
63
+ const combined = `${entry.title}\n${entry.summary}\n${entry.content}`;
53
64
  // Title matches count double (higher signal density)
54
65
  const titleScore = scoreText(entry.title, query);
55
- const bodyScore = scoreText(`${summary}\n${raw}`, query);
66
+ const bodyScore = scoreText(`${entry.summary}\n${entry.content}`, query);
56
67
  const score = titleScore * 2 + bodyScore;
57
68
  if (score <= 0)
58
69
  continue;
59
70
  matches.push({
71
+ id: entry.id,
60
72
  title: entry.title,
61
73
  path: entry.path,
62
74
  score,
63
- summary,
64
- excerpt: extractExcerpt(raw, query),
75
+ summary: entry.summary,
76
+ excerpt: extractExcerpt(entry.content, query),
65
77
  });
66
78
  }
67
79
  return {
@@ -70,3 +82,43 @@ export async function resolveContext({ home, branch, query, }) {
70
82
  matches: matches.sort((a, b) => b.score - a.score || a.title.localeCompare(b.title)),
71
83
  };
72
84
  }
85
+ export async function getDocById({ home, branch, id, }) {
86
+ const docs = await readBranchDocs(home, branch);
87
+ return docs.find((doc) => doc.id === id) || null;
88
+ }
89
+ export async function findRelatedDocs({ home, branch, id, limit = 5, }) {
90
+ const docs = await readBranchDocs(home, branch);
91
+ if (docs.length === 0) {
92
+ return { status: 'empty_branch', selected: null, matches: [] };
93
+ }
94
+ const selected = docs.find((doc) => doc.id === id) || null;
95
+ if (!selected) {
96
+ return { status: 'doc_not_found', selected: null, matches: [] };
97
+ }
98
+ const query = `${selected.title}\n${selected.summary}`;
99
+ const matches = docs
100
+ .filter((doc) => doc.id !== selected.id)
101
+ .map((doc) => {
102
+ const titleScore = scoreText(doc.title, query);
103
+ const bodyScore = scoreText(`${doc.summary}\n${doc.content}`, query);
104
+ const score = titleScore * 2 + bodyScore;
105
+ return score > 0
106
+ ? {
107
+ id: doc.id,
108
+ title: doc.title,
109
+ path: doc.path,
110
+ score,
111
+ summary: doc.summary,
112
+ excerpt: extractExcerpt(doc.content, query),
113
+ }
114
+ : null;
115
+ })
116
+ .filter((match) => Boolean(match))
117
+ .sort((a, b) => b.score - a.score || a.title.localeCompare(b.title))
118
+ .slice(0, limit);
119
+ return {
120
+ status: matches.length > 0 ? 'matched' : 'no_related_docs',
121
+ selected,
122
+ matches,
123
+ };
124
+ }
@@ -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() },
package/docs/usage.es.md CHANGED
@@ -14,10 +14,12 @@ Un flujo de trabajo estándar con `gctree` tiene este aspecto: inicializar gc-tr
14
14
  4. si elegiste `both`, elegir qué proveedor debe iniciar el onboarding ahora
15
15
  5. completar el onboarding guiado para la gc-branch `main` predeterminada
16
16
  6. hacer resolve del contexto relevante con `gctree resolve --query "..."`
17
- 7. crear o cambiar de gc-branch con `gctree checkout`
18
- 8. ejecutar `gctree onboard` solo para una gc-branch vacía
19
- 9. usar el mapeo de alcance del repositorio para que una gc-branch solo aplique donde corresponde
20
- 10. usar `gctree update-global-context` para cambios duraderos más adelante
17
+ 7. inspeccionar documentos de apoyo con `gctree related --id <match-id>`
18
+ 8. leer el documento completo solo cuando haga falta con `gctree show-doc --id <match-id>`
19
+ 9. crear o cambiar de gc-branch con `gctree checkout`
20
+ 10. ejecutar `gctree onboard` solo para una gc-branch vacía
21
+ 11. usar el mapeo de alcance del repositorio para que una gc-branch solo aplique donde corresponde
22
+ 12. usar `gctree update-global-context` para cambios duraderos más adelante
21
23
 
22
24
  ## Comandos principales
23
25
 
@@ -28,7 +30,9 @@ Un flujo de trabajo estándar con `gctree` tiene este aspecto: inicializar gc-tr
28
30
  | `gctree checkout -b <branch>` | Crear y cambiar a una nueva gc-branch vacía. |
29
31
  | `gctree branches` | Listar las gc-branches disponibles y mostrar la activa. |
30
32
  | `gctree status` | Mostrar la gc-branch activa, el repositorio actual, el estado del alcance del repositorio actual, advertencias y el proveedor preferido. |
31
- | `gctree resolve --query TEXT` | Buscar contexto en la gc-branch relevante. Si el repositorio actual no está mapeado, `gctree` puede preguntar cómo debe tratarse ese repositorio. |
33
+ | `gctree resolve --query TEXT` | Devuelve la capa de índice compacto para una consulta. Las coincidencias incluyen IDs estables y comandos de seguimiento para profundizar. |
34
+ | `gctree related --id <match-id>` | Devuelve documentos de apoyo relacionados con una coincidencia resuelta, sin expandir todavía el markdown completo. |
35
+ | `gctree show-doc --id <match-id>` | Devuelve el documento source-of-truth en markdown completo para un ID estable. |
32
36
  | `gctree repo-map` | Mostrar el contenido actual de `branch-repo-map.json`. |
33
37
  | `gctree set-repo-scope --branch <name> --include` | Marcar el repositorio actual como incluido para esa gc-branch. |
34
38
  | `gctree set-repo-scope --branch <name> --exclude` | Marcar el repositorio actual como ignorado para esa gc-branch. |
@@ -40,7 +44,7 @@ Un flujo de trabajo estándar con `gctree` tiene este aspecto: inicializar gc-tr
40
44
 
41
45
  ## Qué devuelve resolve
42
46
 
43
- `gctree resolve` puntúa cada documento de la gc-branch activa contra tu consulta y devuelve solo los que coinciden. Las coincidencias en el título cuentan el doble que las coincidencias en el cuerpo.
47
+ `gctree resolve` es ahora la **capa de índice compacto** dentro de un flujo de progressive disclosure. Puntúa cada documento de la gc-branch activa contra tu consulta y devuelve solo coincidencias con IDs estables. Las coincidencias en el título cuentan el doble que las coincidencias en el cuerpo.
44
48
 
45
49
  ```bash
46
50
  gctree resolve --query "auth token rotation policy"
@@ -50,19 +54,36 @@ gctree resolve --query "auth token rotation policy"
50
54
  {
51
55
  "gc_branch": "main",
52
56
  "query": "auth token rotation policy",
57
+ "status": "matched",
53
58
  "matches": [
54
59
  {
60
+ "id": "auth",
55
61
  "title": "Auth & Session Conventions",
56
62
  "path": "docs/auth.md",
57
63
  "score": 4,
58
64
  "summary": "JWT rotation on every request, refresh tokens in httpOnly cookies, 15-min access token TTL",
59
- "excerpt": "## Auth Flow\nAccess token: 15-min TTL, rotated on every authenticated request..."
65
+ "excerpt": "## Auth Flow\nAccess token: 15-min TTL, rotated on every authenticated request...",
66
+ "commands": {
67
+ "show_doc": "gctree show-doc --id \"auth\" --home \"~/.gctree\" --branch \"main\"",
68
+ "related": "gctree related --id \"auth\" --home \"~/.gctree\" --branch \"main\""
69
+ }
60
70
  }
61
71
  ]
62
72
  }
63
73
  ```
64
74
 
65
- La herramienta recibe primero el resumen y el fragmento. Lee el documento completo en `path` solo cuando el resumen no es suficiente. Una consulta que no coincide con nada devuelve `"matches": []`.
75
+ El flujo recomendado es:
76
+
77
+ 1. `resolve` → índice compacto
78
+ 2. `related` → documentos de apoyo alrededor de una coincidencia
79
+ 3. `show-doc` → markdown completo solo cuando hace falta
80
+
81
+ La degradación elegante también es explícita:
82
+
83
+ - gc-branch vacía → `status: "empty_branch"`
84
+ - repo excluido → `status: "excluded"`
85
+ - sin resultados → `status: "no_match"`
86
+ - ID de documento inexistente → `status: "doc_not_found"`
66
87
 
67
88
  ## Ejemplo de flujo de alcance del repositorio
68
89
 
@@ -145,6 +166,7 @@ gctree scaffold --host both --target /path/to/repo
145
166
 
146
167
  ```
147
168
  AGENTS.md ← fragmento de gctree añadido a las instrucciones del agente
169
+ .codex/hooks.json ← hooks de auto-resolve para SessionStart/UserPromptSubmit
148
170
  .codex/prompts/gctree-bootstrap.md ← contexto de arranque para las sesiones de Codex
149
171
  .codex/skills/gc-resolve-context/SKILL.md ← skill de resolve
150
172
  .codex/skills/gc-onboard/SKILL.md ← skill de onboarding
@@ -155,7 +177,8 @@ AGENTS.md ← fragmento de gctree añadido a la
155
177
 
156
178
  ```
157
179
  CLAUDE.md ← fragmento de gctree añadido
158
- .claude/hooks/gctree-session-start.md hook de inicio de sesión
180
+ .claude/hooks/hooks.json hooks de auto-resolve para SessionStart/UserPromptSubmit
181
+ .claude/hooks/gctree-session-start.md ← nota fallback de inicio de sesión
159
182
  .claude/commands/gc-resolve-context.md ← comando slash de resolve
160
183
  .claude/commands/gc-onboard.md ← comando slash de onboard
161
184
  .claude/commands/gc-update-global-context.md ← comando slash de actualización
package/docs/usage.ja.md CHANGED
@@ -14,10 +14,12 @@
14
14
  4. `both` を選んだ場合は、今回のオンボーディングをどちらのプロバイダーで始めるか選ぶ
15
15
  5. デフォルトの `main` gc-branch に対するガイド付きオンボーディングを完了する
16
16
  6. `gctree resolve --query "..."` で必要なコンテキストを取り出す
17
- 7. `gctree checkout` gc-branch を作成または切り替える
18
- 8. `gctree onboard` は空の gc-branch に対してだけ使う
19
- 9. repo scope を設定して gc-branch が本来のリポジトリにだけ作用するようにする
20
- 10. 後からの永続的な変更には `gctree update-global-context` を使う
17
+ 7. `gctree related --id <match-id>` で supporting docs を確認する
18
+ 8. `gctree show-doc --id <match-id>` で必要なときだけフルドキュメントを読む
19
+ 9. `gctree checkout` gc-branch を作成または切り替える
20
+ 10. `gctree onboard` は空の gc-branch に対してだけ使う
21
+ 11. repo scope を設定して gc-branch が本来のリポジトリにだけ作用するようにする
22
+ 12. 後からの永続的な変更には `gctree update-global-context` を使う
21
23
 
22
24
  ## 主要コマンド
23
25
 
@@ -28,7 +30,9 @@
28
30
  | `gctree checkout -b <branch>` | 新しい空の gc-branch を作成し、そのまま切り替えます。 |
29
31
  | `gctree branches` | 利用可能な gc-branch 一覧と、現在アクティブなものを表示します。 |
30
32
  | `gctree status` | アクティブな gc-branch、現在のリポジトリ、現在の repo-scope 状態、警告、優先プロバイダーを表示します。 |
31
- | `gctree resolve --query TEXT` | 関係する gc-branch からコンテキストを検索します。現在のリポジトリが未マップなら、そのリポジトリをどう扱うかを尋ねる場合があります。 |
33
+ | `gctree resolve --query TEXT` | クエリに対する compact index layer を返します。各 match には stable ID と次の確認コマンドが含まれます。 |
34
+ | `gctree related --id <match-id>` | 1つの resolved match に関連する supporting docs を、フル markdown を開かずに返します。 |
35
+ | `gctree show-doc --id <match-id>` | stable match ID に対応するフル markdown の source-of-truth ドキュメントを返します。 |
32
36
  | `gctree repo-map` | 現在の `branch-repo-map.json` の内容を表示します。 |
33
37
  | `gctree set-repo-scope --branch <name> --include` | 現在のリポジトリを、その gc-branch に含まれるものとして記録します。 |
34
38
  | `gctree set-repo-scope --branch <name> --exclude` | 現在のリポジトリを、その gc-branch では無視するものとして記録します。 |
@@ -40,7 +44,7 @@
40
44
 
41
45
  ## resolve が返す内容
42
46
 
43
- `gctree resolve` はアクティブな gc-branch 内のすべてのドキュメントをクエリに対してスコアリングし、マッチしたものだけを返します。タイトルの一致はボディの一致の 2 倍の重みを持ちます。
47
+ `gctree resolve` progressive-disclosure ワークフローにおける **compact index layer** です。アクティブな gc-branch 内のドキュメントをクエリに対してスコアリングし、stable ID 付きの match だけを返します。タイトルの一致は本文の一致の 2 倍の重みを持ちます。
44
48
 
45
49
  ```bash
46
50
  gctree resolve --query "auth token rotation policy"
@@ -50,19 +54,36 @@ gctree resolve --query "auth token rotation policy"
50
54
  {
51
55
  "gc_branch": "main",
52
56
  "query": "auth token rotation policy",
57
+ "status": "matched",
53
58
  "matches": [
54
59
  {
60
+ "id": "auth",
55
61
  "title": "Auth & Session Conventions",
56
62
  "path": "docs/auth.md",
57
63
  "score": 4,
58
64
  "summary": "JWT rotation on every request, refresh tokens in httpOnly cookies, 15-min access token TTL",
59
- "excerpt": "## Auth Flow\nAccess token: 15-min TTL, rotated on every authenticated request..."
65
+ "excerpt": "## Auth Flow\nAccess token: 15-min TTL, rotated on every authenticated request...",
66
+ "commands": {
67
+ "show_doc": "gctree show-doc --id \"auth\" --home \"~/.gctree\" --branch \"main\"",
68
+ "related": "gctree related --id \"auth\" --home \"~/.gctree\" --branch \"main\""
69
+ }
60
70
  }
61
71
  ]
62
72
  }
63
73
  ```
64
74
 
65
- ツールはまずサマリーと抜粋を受け取ります。サマリーだけでは不足する場合にのみ、`path` にあるフルドキュメントを読み込みます。何もマッチしないクエリの場合は `"matches": []` が返ります。
75
+ 推奨フローは次のとおりです。
76
+
77
+ 1. `resolve` → compact index
78
+ 2. `related` → その match の周辺にある supporting docs
79
+ 3. `show-doc` → 必要なときだけフル markdown
80
+
81
+ Graceful degradation も明示的です。
82
+
83
+ - 空の gc-branch → `status: "empty_branch"`
84
+ - 除外された repo → `status: "excluded"`
85
+ - ヒットなし → `status: "no_match"`
86
+ - 文書 ID が見つからない → `status: "doc_not_found"`
66
87
 
67
88
  ## repo scope の設定例
68
89
 
@@ -145,6 +166,7 @@ gctree scaffold --host both --target /path/to/repo
145
166
 
146
167
  ```
147
168
  AGENTS.md ← gctree のスニペットがエージェント指示に追記される
169
+ .codex/hooks.json ← SessionStart / UserPromptSubmit の自動 resolve フック
148
170
  .codex/prompts/gctree-bootstrap.md ← Codex セッション向けのブートストラップコンテキスト
149
171
  .codex/skills/gc-resolve-context/SKILL.md ← resolve スキル
150
172
  .codex/skills/gc-onboard/SKILL.md ← オンボーディングスキル
@@ -155,7 +177,8 @@ AGENTS.md ← gctree のスニペットがエ
155
177
 
156
178
  ```
157
179
  CLAUDE.md ← gctree のスニペットが追記される
158
- .claude/hooks/gctree-session-start.md セッション開始フック
180
+ .claude/hooks/hooks.json SessionStart / UserPromptSubmit の自動 resolve フック
181
+ .claude/hooks/gctree-session-start.md ← セッション開始のフォールバックメモ
159
182
  .claude/commands/gc-resolve-context.md ← resolve スラッシュコマンド
160
183
  .claude/commands/gc-onboard.md ← onboard スラッシュコマンド
161
184
  .claude/commands/gc-update-global-context.md ← 更新スラッシュコマンド
package/docs/usage.ko.md CHANGED
@@ -14,10 +14,12 @@
14
14
  4. `both`를 선택한 경우, 지금 온보딩을 시작할 프로바이더 선택
15
15
  5. 기본 `main` gc-branch의 안내된 온보딩 완료
16
16
  6. `gctree resolve --query "..."`로 관련 컨텍스트 resolve
17
- 7. `gctree checkout`으로 gc-branch 생성 또는 전환
18
- 8. gc-branch에만 `gctree onboard` 실행
19
- 9. 저장소 범위 매핑으로 gc-branch 해당하는 곳에만 적용되도록 설정
20
- 10. 이후 지속 변경에는 `gctree update-global-context` 사용
17
+ 7. `gctree related --id <match-id>`로 supporting docs 확인
18
+ 8. `gctree show-doc --id <match-id>`로 필요할 때만 전체 문서 읽기
19
+ 9. `gctree checkout`으로 gc-branch 생성 또는 전환
20
+ 10. gc-branch에만 `gctree onboard` 실행
21
+ 11. 저장소 범위 매핑으로 gc-branch가 해당하는 곳에만 적용되도록 설정
22
+ 12. 이후 지속 변경에는 `gctree update-global-context` 사용
21
23
 
22
24
  ## 핵심 명령어
23
25
 
@@ -28,7 +30,9 @@
28
30
  | `gctree checkout -b <branch>` | 새 빈 gc-branch를 생성하고 전환. |
29
31
  | `gctree branches` | 사용 가능한 gc-branch 목록 표시 및 현재 활성 브랜치 표시. |
30
32
  | `gctree status` | 활성 gc-branch, 현재 저장소, 현재 저장소 범위 상태, 경고, 선호 프로바이더 표시. |
31
- | `gctree resolve --query TEXT` | 관련 gc-branch에서 컨텍스트 검색. 현재 저장소가 매핑되지 않은 경우, `gctree`가 해당 저장소 처리 방법을 물어볼 수 있습니다. |
33
+ | `gctree resolve --query TEXT` | 쿼리에 대한 compact index layer를 반환합니다. match에는 stable ID와 후속 조회 명령이 포함됩니다. |
34
+ | `gctree related --id <match-id>` | 하나의 resolved match 주변 supporting docs를 전체 markdown 없이 반환합니다. |
35
+ | `gctree show-doc --id <match-id>` | stable match ID 하나에 대한 전체 markdown source-of-truth 문서를 반환합니다. |
32
36
  | `gctree repo-map` | `branch-repo-map.json`의 현재 내용 표시. |
33
37
  | `gctree set-repo-scope --branch <name> --include` | 현재 저장소를 해당 gc-branch에 포함으로 표시. |
34
38
  | `gctree set-repo-scope --branch <name> --exclude` | 현재 저장소를 해당 gc-branch에서 무시로 표시. |
@@ -40,7 +44,7 @@
40
44
 
41
45
  ## resolve가 반환하는 것
42
46
 
43
- `gctree resolve`는 활성 gc-branch의 모든 문서를 쿼리에 대해 점수를 매기고 매칭된 것만 반환합니다. 제목 매칭은 본문 매칭보다 두 배의 가중치를 가집니다.
47
+ `gctree resolve`는 progressive-disclosure 워크플로우의 **compact index layer**입니다. 활성 gc-branch의 문서를 쿼리에 대해 점수화하고 stable ID가 포함된 match만 반환합니다. 제목 매칭은 본문 매칭보다 두 배의 가중치를 가집니다.
44
48
 
45
49
  ```bash
46
50
  gctree resolve --query "auth token rotation policy"
@@ -50,19 +54,36 @@ gctree resolve --query "auth token rotation policy"
50
54
  {
51
55
  "gc_branch": "main",
52
56
  "query": "auth token rotation policy",
57
+ "status": "matched",
53
58
  "matches": [
54
59
  {
60
+ "id": "auth",
55
61
  "title": "Auth & Session Conventions",
56
62
  "path": "docs/auth.md",
57
63
  "score": 4,
58
64
  "summary": "JWT rotation on every request, refresh tokens in httpOnly cookies, 15-min access token TTL",
59
- "excerpt": "## Auth Flow\nAccess token: 15-min TTL, rotated on every authenticated request..."
65
+ "excerpt": "## Auth Flow\nAccess token: 15-min TTL, rotated on every authenticated request...",
66
+ "commands": {
67
+ "show_doc": "gctree show-doc --id \"auth\" --home \"~/.gctree\" --branch \"main\"",
68
+ "related": "gctree related --id \"auth\" --home \"~/.gctree\" --branch \"main\""
69
+ }
60
70
  }
61
71
  ]
62
72
  }
63
73
  ```
64
74
 
65
- 도구는 먼저 요약과 발췌문을 받습니다. 요약만으로 충분하지 않을 때만 `path`의 전체 문서를 읽습니다. 아무것도 매칭되지 않는 쿼리는 `"matches": []`를 반환합니다.
75
+ 권장 흐름은 다음과 같습니다:
76
+
77
+ 1. `resolve` → compact index
78
+ 2. `related` → 특정 match 주변 supporting docs
79
+ 3. `show-doc` → 필요할 때만 전체 markdown
80
+
81
+ Graceful degradation도 명시적입니다:
82
+
83
+ - 빈 gc-branch → `status: "empty_branch"`
84
+ - 제외된 repo → `status: "excluded"`
85
+ - 결과 없음 → `status: "no_match"`
86
+ - 문서 ID 없음 → `status: "doc_not_found"`
66
87
 
67
88
  ## 저장소 범위 설정 예시 흐름
68
89
 
@@ -145,6 +166,7 @@ gctree scaffold --host both --target /path/to/repo
145
166
 
146
167
  ```
147
168
  AGENTS.md ← 에이전트 지시사항에 gctree 스니펫 추가
169
+ .codex/hooks.json ← SessionStart/UserPromptSubmit 자동 resolve 훅
148
170
  .codex/prompts/gctree-bootstrap.md ← Codex 세션의 부트스트랩 컨텍스트
149
171
  .codex/skills/gc-resolve-context/SKILL.md ← resolve 스킬
150
172
  .codex/skills/gc-onboard/SKILL.md ← 온보딩 스킬
@@ -155,7 +177,8 @@ AGENTS.md ← 에이전트 지시사항에 gctr
155
177
 
156
178
  ```
157
179
  CLAUDE.md ← gctree 스니펫 추가
158
- .claude/hooks/gctree-session-start.md 세션 시작
180
+ .claude/hooks/hooks.json SessionStart/UserPromptSubmit 자동 resolve
181
+ .claude/hooks/gctree-session-start.md ← 세션 시작 fallback 메모
159
182
  .claude/commands/gc-resolve-context.md ← resolve 슬래시 명령
160
183
  .claude/commands/gc-onboard.md ← onboard 슬래시 명령
161
184
  .claude/commands/gc-update-global-context.md ← 업데이트 슬래시 명령
package/docs/usage.md CHANGED
@@ -14,10 +14,12 @@ A standard `gctree` workflow looks like this: initialize gc-tree, choose a provi
14
14
  4. if you picked `both`, choose which provider should start onboarding now
15
15
  5. complete guided onboarding for the default `main` gc-branch
16
16
  6. resolve relevant context with `gctree resolve --query "..."`
17
- 7. create or switch gc-branches with `gctree checkout`
18
- 8. run `gctree onboard` only for an empty gc-branch
19
- 9. use repo scope mapping so a gc-branch only applies where it belongs
20
- 10. use `gctree update-global-context` for durable changes later
17
+ 7. inspect supporting docs with `gctree related --id <match-id>`
18
+ 8. read the full doc only when needed with `gctree show-doc --id <match-id>`
19
+ 9. create or switch gc-branches with `gctree checkout`
20
+ 10. run `gctree onboard` only for an empty gc-branch
21
+ 11. use repo scope mapping so a gc-branch only applies where it belongs
22
+ 12. use `gctree update-global-context` for durable changes later
21
23
 
22
24
  ## Core commands
23
25
 
@@ -28,7 +30,9 @@ A standard `gctree` workflow looks like this: initialize gc-tree, choose a provi
28
30
  | `gctree checkout -b <branch>` | Create and switch to a new empty gc-branch. |
29
31
  | `gctree branches` | List available gc-branches and show the active one. |
30
32
  | `gctree status` | Show the active gc-branch, the current repo, the current repo-scope status, warnings, and the preferred provider. |
31
- | `gctree resolve --query TEXT` | Search the relevant gc-branch for context. If the current repo is unmapped, `gctree` can ask how that repo should be treated. |
33
+ | `gctree resolve --query TEXT` | Return the compact index layer for a query. Matches include stable IDs plus follow-up commands for deeper inspection. |
34
+ | `gctree related --id <match-id>` | Return supporting docs related to one resolved match without expanding the full markdown yet. |
35
+ | `gctree show-doc --id <match-id>` | Return the full markdown source-of-truth doc for one stable match ID. |
32
36
  | `gctree repo-map` | Show the current contents of `branch-repo-map.json`. |
33
37
  | `gctree set-repo-scope --branch <name> --include` | Mark the current repo as included for that gc-branch. |
34
38
  | `gctree set-repo-scope --branch <name> --exclude` | Mark the current repo as ignored for that gc-branch. |
@@ -40,7 +44,7 @@ A standard `gctree` workflow looks like this: initialize gc-tree, choose a provi
40
44
 
41
45
  ## What resolve returns
42
46
 
43
- `gctree resolve` scores every document in the active gc-branch against your query and returns only the matching ones. Title matches count twice as much as body matches.
47
+ `gctree resolve` is the compact **index layer** in a progressive-disclosure workflow. It scores every document in the active gc-branch against your query and returns only matching docs with stable IDs. Title matches count twice as much as body matches.
44
48
 
45
49
  ```bash
46
50
  gctree resolve --query "auth token rotation policy"
@@ -50,19 +54,36 @@ gctree resolve --query "auth token rotation policy"
50
54
  {
51
55
  "gc_branch": "main",
52
56
  "query": "auth token rotation policy",
57
+ "status": "matched",
53
58
  "matches": [
54
59
  {
60
+ "id": "auth",
55
61
  "title": "Auth & Session Conventions",
56
62
  "path": "docs/auth.md",
57
63
  "score": 4,
58
64
  "summary": "JWT rotation on every request, refresh tokens in httpOnly cookies, 15-min access token TTL",
59
- "excerpt": "## Auth Flow\nAccess token: 15-min TTL, rotated on every authenticated request..."
65
+ "excerpt": "## Auth Flow\nAccess token: 15-min TTL, rotated on every authenticated request...",
66
+ "commands": {
67
+ "show_doc": "gctree show-doc --id \"auth\" --home \"~/.gctree\" --branch \"main\"",
68
+ "related": "gctree related --id \"auth\" --home \"~/.gctree\" --branch \"main\""
69
+ }
60
70
  }
61
71
  ]
62
72
  }
63
73
  ```
64
74
 
65
- The tool receives the summary and excerpt first. It reads the full document at `path` only when the summary is not enough. A query that matches nothing returns `"matches": []`.
75
+ The intended flow is:
76
+
77
+ 1. `resolve` → compact index
78
+ 2. `related` → supporting docs around one match
79
+ 3. `show-doc` → full markdown only when needed
80
+
81
+ Graceful degradation is explicit:
82
+
83
+ - empty gc-branch → `status: "empty_branch"`
84
+ - excluded repo → `status: "excluded"`
85
+ - no hits → `status: "no_match"`
86
+ - missing doc id → `status: "doc_not_found"`
66
87
 
67
88
  ## Example repo-scope flow
68
89
 
@@ -145,6 +166,7 @@ gctree scaffold --host both --target /path/to/repo
145
166
 
146
167
  ```
147
168
  AGENTS.md ← gctree snippet appended to agent instructions
169
+ .codex/hooks.json ← SessionStart/UserPromptSubmit auto-resolve hooks
148
170
  .codex/prompts/gctree-bootstrap.md ← bootstrap context for Codex sessions
149
171
  .codex/skills/gc-resolve-context/SKILL.md ← resolve skill
150
172
  .codex/skills/gc-onboard/SKILL.md ← onboarding skill
@@ -155,7 +177,8 @@ AGENTS.md ← gctree snippet appended to agent
155
177
 
156
178
  ```
157
179
  CLAUDE.md ← gctree snippet appended
158
- .claude/hooks/gctree-session-start.md session-start hook
180
+ .claude/hooks/hooks.json SessionStart/UserPromptSubmit auto-resolve hooks
181
+ .claude/hooks/gctree-session-start.md ← session-start fallback note
159
182
  .claude/commands/gc-resolve-context.md ← resolve slash command
160
183
  .claude/commands/gc-onboard.md ← onboard slash command
161
184
  .claude/commands/gc-update-global-context.md ← update slash command
package/docs/usage.zh.md CHANGED
@@ -14,10 +14,12 @@
14
14
  4. 如果选择了 `both`,选择哪个提供商现在开始 onboarding
15
15
  5. 对默认的 `main` gc-branch 完成有引导的 onboarding
16
16
  6. 使用 `gctree resolve --query "..."` 解析相关上下文
17
- 7. 使用 `gctree checkout` 创建或切换 gc-branch
18
- 8. 仅对空的 gc-branch 运行 `gctree onboard`
19
- 9. 使用仓库作用范围映射,使 gc-branch 只在其适用的地方生效
20
- 10. 使用 `gctree update-global-context` 进行后续的持久化变更
17
+ 7. 使用 `gctree related --id <match-id>` 查看 supporting docs
18
+ 8. 仅在需要时使用 `gctree show-doc --id <match-id>` 读取完整文档
19
+ 9. 使用 `gctree checkout` 创建或切换 gc-branch
20
+ 10. 仅对空的 gc-branch 运行 `gctree onboard`
21
+ 11. 使用仓库作用范围映射,使 gc-branch 只在其适用的地方生效
22
+ 12. 使用 `gctree update-global-context` 进行后续的持久化变更
21
23
 
22
24
  ## 核心命令
23
25
 
@@ -28,7 +30,9 @@
28
30
  | `gctree checkout -b <branch>` | 创建并切换到一个新的空 gc-branch。 |
29
31
  | `gctree branches` | 列出可用的 gc-branch 并显示当前活跃的那个。 |
30
32
  | `gctree status` | 显示活跃的 gc-branch、当前仓库、当前仓库的作用范围状态、警告信息及首选提供商。 |
31
- | `gctree resolve --query TEXT` | 在相关的 gc-branch 中搜索上下文。如果当前仓库尚未映射,`gctree` 会询问该如何处理这个仓库。 |
33
+ | `gctree resolve --query TEXT` | 返回查询的紧凑索引层。匹配项包含稳定 ID 和后续深入查看的命令。 |
34
+ | `gctree related --id <match-id>` | 返回某个 resolved 匹配周围的 supporting docs,而不立即展开完整 markdown。 |
35
+ | `gctree show-doc --id <match-id>` | 返回某个稳定匹配 ID 对应的完整 markdown source-of-truth 文档。 |
32
36
  | `gctree repo-map` | 显示 `branch-repo-map.json` 的当前内容。 |
33
37
  | `gctree set-repo-scope --branch <name> --include` | 将当前仓库标记为该 gc-branch 的包含仓库。 |
34
38
  | `gctree set-repo-scope --branch <name> --exclude` | 将当前仓库标记为该 gc-branch 的忽略仓库。 |
@@ -40,7 +44,7 @@
40
44
 
41
45
  ## resolve 返回的内容
42
46
 
43
- `gctree resolve` 对活跃 gc-branch 中的每个文档与查询内容进行评分,只返回匹配的文档。标题匹配的权重是正文匹配的两倍。
47
+ `gctree resolve` progressive-disclosure 工作流中的**紧凑索引层**。它对活跃 gc-branch 中的每个文档与查询内容进行评分,只返回带稳定 ID 的匹配文档。标题匹配的权重是正文匹配的两倍。
44
48
 
45
49
  ```bash
46
50
  gctree resolve --query "auth token rotation policy"
@@ -50,19 +54,36 @@ gctree resolve --query "auth token rotation policy"
50
54
  {
51
55
  "gc_branch": "main",
52
56
  "query": "auth token rotation policy",
57
+ "status": "matched",
53
58
  "matches": [
54
59
  {
60
+ "id": "auth",
55
61
  "title": "Auth & Session Conventions",
56
62
  "path": "docs/auth.md",
57
63
  "score": 4,
58
64
  "summary": "JWT rotation on every request, refresh tokens in httpOnly cookies, 15-min access token TTL",
59
- "excerpt": "## Auth Flow\nAccess token: 15-min TTL, rotated on every authenticated request..."
65
+ "excerpt": "## Auth Flow\nAccess token: 15-min TTL, rotated on every authenticated request...",
66
+ "commands": {
67
+ "show_doc": "gctree show-doc --id \"auth\" --home \"~/.gctree\" --branch \"main\"",
68
+ "related": "gctree related --id \"auth\" --home \"~/.gctree\" --branch \"main\""
69
+ }
60
70
  }
61
71
  ]
62
72
  }
63
73
  ```
64
74
 
65
- 工具首先接收摘要和摘录。只有在摘要不足以满足需求时,才会读取 `path` 指向的完整文档。如果查询没有任何匹配,则返回 `"matches": []`。
75
+ 推荐的流程是:
76
+
77
+ 1. `resolve` → 紧凑索引
78
+ 2. `related` → 某个匹配周围的 supporting docs
79
+ 3. `show-doc` → 仅在需要时读取完整 markdown
80
+
81
+ Graceful degradation 也是显式的:
82
+
83
+ - 空 gc-branch → `status: "empty_branch"`
84
+ - 被排除的 repo → `status: "excluded"`
85
+ - 无匹配结果 → `status: "no_match"`
86
+ - 文档 ID 不存在 → `status: "doc_not_found"`
66
87
 
67
88
  ## 仓库作用范围示例流程
68
89
 
@@ -145,6 +166,7 @@ gctree scaffold --host both --target /path/to/repo
145
166
 
146
167
  ```
147
168
  AGENTS.md ← gctree 代码片段追加到 agent 指令中
169
+ .codex/hooks.json ← SessionStart / UserPromptSubmit 自动 resolve hook
148
170
  .codex/prompts/gctree-bootstrap.md ← Codex 会话的引导上下文
149
171
  .codex/skills/gc-resolve-context/SKILL.md ← resolve 技能
150
172
  .codex/skills/gc-onboard/SKILL.md ← onboarding 技能
@@ -155,7 +177,8 @@ AGENTS.md ← gctree 代码片段追加到 agen
155
177
 
156
178
  ```
157
179
  CLAUDE.md ← gctree 代码片段追加
158
- .claude/hooks/gctree-session-start.md 会话启动钩子
180
+ .claude/hooks/hooks.json SessionStart / UserPromptSubmit 自动 resolve hook
181
+ .claude/hooks/gctree-session-start.md ← 会话启动 fallback 说明
159
182
  .claude/commands/gc-resolve-context.md ← resolve 斜杠命令
160
183
  .claude/commands/gc-onboard.md ← onboard 斜杠命令
161
184
  .claude/commands/gc-update-global-context.md ← 更新斜杠命令
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@handsupmin/gc-tree",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Global Context Tree, a lightweight branch-aware global context orchestrator for AI coding tools",
5
5
  "type": "module",
6
6
  "private": false,