@handsupmin/gc-tree 0.8.2 → 0.8.4

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
@@ -74,7 +74,7 @@ gctree init
74
74
  2. Instalar los archivos de integración en el repositorio actual
75
75
  3. Completar el onboarding guiado para el gc-branch `main`
76
76
 
77
- 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.
77
+ 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. La salida del hook se mantiene compacta: muestra el estado found/no-match junto con IDs y resúmenes, y deja el documento completo detrás de `gctree resolve --id <id>`.
78
78
 
79
79
  - **CLI:** `gctree`
80
80
  - **Requisito:** Node.js 20+
package/README.ja.md CHANGED
@@ -74,7 +74,7 @@ gctree init
74
74
  2. 現在のリポジトリへの統合ファイルのインストール
75
75
  3. `main` gc-branch のガイド付きオンボーディング実行
76
76
 
77
- その後は、AI ツールに実際の SessionStart / UserPromptSubmit フック統合が入り、作業前に gc-tree を自動確認し、空結果や no-match をセッション中キャッシュします。
77
+ その後は、AI ツールに実際の SessionStart / UserPromptSubmit フック統合が入り、作業前に gc-tree を自動確認し、空結果や no-match をセッション中キャッシュします。フック出力はコンパクトに保たれ、found/no-match 状態、ドキュメント ID、要約だけを示し、全文は必要なときに `gctree resolve --id <id>` で開きます。
78
78
 
79
79
  - **CLI:** `gctree`
80
80
  - **動作条件:** Node.js 20+
package/README.ko.md CHANGED
@@ -74,7 +74,7 @@ gctree init
74
74
  2. 현재 레포에 통합 파일 스캐폴딩
75
75
  3. `main` gc-branch 가이드형 온보딩 실행
76
76
 
77
- 이후 AI 도구에 실제 SessionStart/UserPromptSubmit 훅 통합이 설치되어, 작업 전에 gc-tree를 자동 확인하고 세션 동안 빈 결과·no-match를 캐시합니다.
77
+ 이후 AI 도구에 실제 SessionStart/UserPromptSubmit 훅 통합이 설치되어, 작업 전에 gc-tree를 자동 확인하고 세션 동안 빈 결과·no-match를 캐시합니다. 훅 출력은 짧게 유지됩니다: 찾음/no-match 상태와 문서 ID, 요약만 보여주고 전체 문서는 `gctree resolve --id <id>`로 필요할 때 엽니다.
78
78
 
79
79
  - **CLI:** `gctree`
80
80
  - **요구 사항:** Node.js 20+
package/README.md CHANGED
@@ -74,7 +74,7 @@ gctree init
74
74
  2. Scaffold the integration files into your current repo
75
75
  3. Run guided onboarding for the `main` gc-branch
76
76
 
77
- 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.
77
+ 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. Hook output stays compact: it reports found/no-match state plus document IDs and summaries, and full docs stay behind `gctree resolve --id <id>`.
78
78
 
79
79
  - **CLI:** `gctree`
80
80
  - **Requires:** Node.js 20+
package/README.zh.md CHANGED
@@ -61,7 +61,7 @@ gctree init
61
61
  2. 在当前仓库安装集成文件
62
62
  3. 为 `main` gc-branch 完成引导式 onboarding
63
63
 
64
- 之后,AI 工具会安装真正的 SessionStart / UserPromptSubmit hook 集成:在开始工作前自动检查 gc-tree,并在整个会话内缓存空结果和 no-match。
64
+ 之后,AI 工具会安装真正的 SessionStart / UserPromptSubmit hook 集成:在开始工作前自动检查 gc-tree,并在整个会话内缓存空结果和 no-match。Hook 输出保持紧凑:只显示 found/no-match 状态、文档 ID 和摘要,完整文档需要时再通过 `gctree resolve --id <id>` 打开。
65
65
 
66
66
  - **CLI:** `gctree`
67
67
  - **要求:** Node.js 20+
package/dist/src/hook.js CHANGED
@@ -18,7 +18,7 @@ function limitMatches(matches, max = 3) {
18
18
  }
19
19
  function formatMatches(matches) {
20
20
  return limitMatches(matches)
21
- .map((match, index) => `${index + 1}. ${match.title} [id=${match.id}]\n Summary: ${match.summary}\n Excerpt: ${match.excerpt}`)
21
+ .map((match, index) => `${index + 1}. ${match.title} [id=${match.id}]: ${match.summary}`)
22
22
  .join('\n');
23
23
  }
24
24
  function buildSessionStartContext({ gcBranch, currentRepo, repoScopeStatus, }) {
@@ -31,33 +31,19 @@ function buildSessionStartContext({ gcBranch, currentRepo, repoScopeStatus, }) {
31
31
  ].join(' ');
32
32
  }
33
33
  function buildEmptyBranchContext({ gcBranch, currentRepo, cached, }) {
34
- return [
35
- `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.`,
36
- `Repo: ${currentRepo || 'unscoped'}.`,
37
- `Treat this as "no reusable global context yet" until the gc-branch content changes or a different repo/gc-branch is active.`,
38
- ].join(' ');
34
+ return `[gc-tree] no context docs in gc-branch "${gcBranch}" for repo "${currentRepo || 'unscoped'}"${cached ? ' (cached)' : ''}.`;
39
35
  }
40
36
  function buildExcludedContext({ gcBranch, currentRepo, cached, }) {
41
- return [
42
- `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}".`,
43
- `No reusable global context applies here unless the branch-repo mapping changes.`,
44
- ].join(' ');
37
+ return `[gc-tree] repo "${currentRepo || 'unknown'}" is excluded from gc-branch "${gcBranch}"${cached ? ' (cached)' : ''}.`;
45
38
  }
46
- function buildNoMatchContext({ gcBranch, currentRepo, query, cached, }) {
47
- return [
48
- `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}".`,
49
- `Repo: ${currentRepo || 'unscoped'}.`,
50
- `Query: "${query}".`,
51
- `Only re-run resolve if the repo, gc-branch, or task changes materially.`,
52
- ].join(' ');
39
+ function buildNoMatchContext({ gcBranch, currentRepo, cached, }) {
40
+ return `[gc-tree] no matching context in gc-branch "${gcBranch}" for repo "${currentRepo || 'unscoped'}"${cached ? ' (cached)' : ''}.`;
53
41
  }
54
- function buildMatchContext({ gcBranch, currentRepo, repoScopeStatus, query, matches, }) {
42
+ function buildMatchContext({ gcBranch, currentRepo, repoScopeStatus, matches, }) {
55
43
  return [
56
- `[gc-tree] PRE-TASK CONTEXT read before any tool use or code exploration.`,
57
- `gc-branch: "${gcBranch}" | repo: "${currentRepo || 'unscoped'}" (scope: ${repoScopeStatus}) | query: "${query}".`,
58
- `If these summaries contain enough information to answer the question or complete the task, respond directly from them WITHOUT calling any tools. Do not grep, explore, or read code to confirm what the summaries already cover. Only use tools if the summaries are clearly insufficient.`,
44
+ `[gc-tree] found ${Math.min(matches.length, 3)} matching context doc(s) in gc-branch "${gcBranch}" for repo "${currentRepo || 'unscoped'}" (scope: ${repoScopeStatus}).`,
59
45
  formatMatches(matches),
60
- `Read full docs (gctree resolve --id <id>) only if summaries are clearly insufficient — meaning the answer cannot be derived from them at all.`,
46
+ `Use these summaries first; read full docs with \`gctree resolve --id <id>\` only if needed.`,
61
47
  ].join('\n');
62
48
  }
63
49
  function buildSelfReviewAppend(promptCount) {
@@ -155,17 +141,17 @@ export async function dispatchGcTreeHook({ event, home, payload, }) {
155
141
  const query = currentRepo ? `${currentRepo} ${prompt}` : prompt;
156
142
  const signature = hashQuery(query);
157
143
  if (cache.no_match_signatures.includes(signature)) {
158
- additionalContext = buildNoMatchContext({ gcBranch, currentRepo, query, cached: true });
144
+ additionalContext = buildNoMatchContext({ gcBranch, currentRepo, cached: true });
159
145
  }
160
146
  else {
161
147
  const result = await resolveContext({ home, branch: gcBranch, query });
162
148
  if (result.matches.length === 0) {
163
149
  cache.no_match_signatures = [...new Set([...cache.no_match_signatures, signature])];
164
- additionalContext = buildNoMatchContext({ gcBranch, currentRepo, query, cached: false });
150
+ additionalContext = buildNoMatchContext({ gcBranch, currentRepo, cached: false });
165
151
  }
166
152
  else {
167
153
  cache.no_match_signatures = cache.no_match_signatures.filter((entry) => entry !== signature);
168
- additionalContext = buildMatchContext({ gcBranch, currentRepo, repoScopeStatus, query, matches: result.matches });
154
+ additionalContext = buildMatchContext({ gcBranch, currentRepo, repoScopeStatus, matches: result.matches });
169
155
  }
170
156
  }
171
157
  if (cache.prompt_count > 0 && cache.prompt_count % SELF_REVIEW_INTERVAL === 0) {
@@ -1,6 +1,9 @@
1
- import { readFile } from 'node:fs/promises';
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile, readdir } from 'node:fs/promises';
3
+ import { join } from 'node:path';
2
4
  import { parseIndexEntries } from './markdown.js';
3
- import { branchIndexPath, DEFAULT_BRANCH } from './paths.js';
5
+ import { resolveContext } from './resolve.js';
6
+ import { branchDocsDir, branchIndexPath, DEFAULT_BRANCH } from './paths.js';
4
7
  import { ensureBranchExists, statusForBranch } from './store.js';
5
8
  const VALID_CATEGORIES = new Set(['role', 'repos', 'domain', 'workflows', 'conventions', 'infra', 'verification']);
6
9
  export async function verifyOnboarding({ home, branch, }) {
@@ -12,6 +15,42 @@ export async function verifyOnboarding({ home, branch, }) {
12
15
  const indexedDocCount = docs.length;
13
16
  const hasDocs = status.doc_count > 0 && indexedDocCount > 0;
14
17
  const qualityIssues = [];
18
+ const docsDir = branchDocsDir(home, gcBranch);
19
+ const sourceDocs = await listSourceDocPaths(docsDir);
20
+ const nonIndexSourceDocs = sourceDocs.filter((path) => !/(^|\/)index\.md$/i.test(path));
21
+ if (sourceDocs.some((path) => /(^|\/)index\.md$/i.test(path))) {
22
+ qualityIssues.push('Source docs must not include docs/index.md. The branch-level index.md is the only dictionary.');
23
+ }
24
+ const indexedPaths = new Set(docs.map((doc) => doc.path));
25
+ for (const path of indexedPaths) {
26
+ if (!existsSync(join(branchDirForDoc(home, gcBranch), path))) {
27
+ qualityIssues.push(`Index points to a missing source doc: ${path}.`);
28
+ }
29
+ }
30
+ for (const path of nonIndexSourceDocs) {
31
+ const indexPath = `docs/${path}`;
32
+ if (!indexedPaths.has(indexPath)) {
33
+ qualityIssues.push(`Source doc is not reachable from index.md: ${indexPath}.`);
34
+ }
35
+ }
36
+ const pathLikeLabels = docs
37
+ .filter((doc) => /^\.?docs\//i.test(doc.label) || /\.md$/i.test(doc.label))
38
+ .map((doc) => doc.label);
39
+ if (pathLikeLabels.length > 0) {
40
+ qualityIssues.push(`Index labels must be search terms, not paths: ${dedupe(pathLikeLabels).join(', ')}.`);
41
+ }
42
+ const prefixedLabels = docs
43
+ .filter((doc) => /^(repo|repository|domain|workflow|convention|role|infra|verification)\s*:/i.test(doc.label))
44
+ .map((doc) => doc.label);
45
+ if (prefixedLabels.length > 0) {
46
+ qualityIssues.push(`Index labels must not keep category prefixes: ${dedupe(prefixedLabels).join(', ')}.`);
47
+ }
48
+ const invalidCategories = docs
49
+ .filter((doc) => doc.category !== 'general' && !VALID_CATEGORIES.has(doc.category))
50
+ .map((doc) => `${doc.label} (${doc.category})`);
51
+ if (invalidCategories.length > 0) {
52
+ qualityIssues.push(`Index contains invalid categories: ${dedupe(invalidCategories).join(', ')}.`);
53
+ }
15
54
  if (hasDocs && indexedDocCount >= 3) {
16
55
  const categoryCounts = new Map();
17
56
  for (const doc of docs) {
@@ -25,6 +64,25 @@ export async function verifyOnboarding({ home, branch, }) {
25
64
  const generalDocLabels = docs.filter((d) => d.category === 'general').map((d) => d.label);
26
65
  qualityIssues.push(`${generalCount} doc(s) still have category "general": ${generalDocLabels.join(', ')}. Move them to one of: ${[...VALID_CATEGORIES].join(', ')}.`);
27
66
  }
67
+ if (indexedDocCount >= 5) {
68
+ const presentCategories = new Set(docs.map((doc) => doc.category));
69
+ const missingCore = ['role', 'repos', 'domain', 'workflows'].filter((category) => !presentCategories.has(category));
70
+ if (missingCore.length > 0) {
71
+ qualityIssues.push(`Onboarding is missing core dictionary categories: ${missingCore.join(', ')}. Capture them or explicitly add small docs for unavailable categories.`);
72
+ }
73
+ }
74
+ }
75
+ if (status.warnings.length > 0) {
76
+ qualityIssues.push(...status.warnings);
77
+ }
78
+ const searchQuality = [];
79
+ for (const doc of docs.slice(0, 12)) {
80
+ const resolved = await resolveContext({ home, branch: gcBranch, query: doc.label });
81
+ const matched = resolved.matches.some((match) => match.path === doc.path);
82
+ searchQuality.push({ query: doc.label, expected_path: doc.path, matched });
83
+ if (!matched) {
84
+ qualityIssues.push(`Resolve smoke test failed: query "${doc.label}" did not return ${doc.path}.`);
85
+ }
28
86
  }
29
87
  const complete = hasDocs && qualityIssues.length === 0;
30
88
  return {
@@ -35,6 +93,7 @@ export async function verifyOnboarding({ home, branch, }) {
35
93
  docs,
36
94
  warnings: status.warnings,
37
95
  quality_issues: qualityIssues,
96
+ search_quality: searchQuality,
38
97
  message: complete
39
98
  ? `Onboarding is complete for gc-branch "${gcBranch}".`
40
99
  : hasDocs
@@ -42,3 +101,23 @@ export async function verifyOnboarding({ home, branch, }) {
42
101
  : `Onboarding is incomplete for gc-branch "${gcBranch}". Docs or index entries are missing.`,
43
102
  };
44
103
  }
104
+ async function listSourceDocPaths(dir, prefix = '') {
105
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
106
+ const files = [];
107
+ for (const entry of entries) {
108
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
109
+ if (entry.isDirectory()) {
110
+ files.push(...(await listSourceDocPaths(join(dir, entry.name), relative)));
111
+ }
112
+ else if (entry.isFile() && entry.name.endsWith('.md')) {
113
+ files.push(relative);
114
+ }
115
+ }
116
+ return files.sort();
117
+ }
118
+ function branchDirForDoc(home, branch) {
119
+ return join(home, 'branches', branch);
120
+ }
121
+ function dedupe(values) {
122
+ return [...new Set(values)];
123
+ }
package/docs/usage.es.md CHANGED
@@ -157,6 +157,8 @@ Si un repositorio recién relevante también debería pasar a formar parte del c
157
157
 
158
158
  `gctree init` instala la superficie global de hooks orientada al proveedor. `gctree scaffold` instala un override local en un directorio de destino cuando un repositorio concreto necesita sus propios snippets markdown o una superficie de comandos local.
159
159
 
160
+ El hook UserPromptSubmit solo inyecta contexto previo compacto: estado found/no-match, IDs de documentos coincidentes y resúmenes. No incluye excerpts largos por defecto; abre documentos completos con `gctree resolve --id <id>` cuando el resumen no sea suficiente.
161
+
160
162
  ```bash
161
163
  gctree scaffold --host codex --target /path/to/repo
162
164
  gctree scaffold --host claude-code --target /path/to/repo
package/docs/usage.ja.md CHANGED
@@ -157,6 +157,8 @@ gctree ugc
157
157
 
158
158
  `gctree init` はグローバルなプロバイダーフック面をインストールします。`gctree scaffold` は特定のリポジトリに独自の markdown スニペットやローカルなコマンド面が必要なときに、ターゲットディレクトリへローカル override をインストールします。
159
159
 
160
+ UserPromptSubmit フックが注入するのは compact な pre-task context だけです。found/no-match 状態、マッチしたドキュメント ID、要約を示し、長い excerpt はデフォルトではインラインしません。要約だけで足りない場合は `gctree resolve --id <id>` で全文を開きます。
161
+
160
162
  ```bash
161
163
  gctree scaffold --host codex --target /path/to/repo
162
164
  gctree scaffold --host claude-code --target /path/to/repo
package/docs/usage.ko.md CHANGED
@@ -157,6 +157,8 @@ gctree ugc
157
157
 
158
158
  `gctree init`은 전역 프로바이더 훅 표면을 설치합니다. `gctree scaffold`는 특정 저장소에 자체 markdown 스니펫이나 로컬 명령 표면이 필요할 때 대상 디렉토리에 로컬 override를 설치합니다.
159
159
 
160
+ UserPromptSubmit 훅은 compact pre-task context만 주입합니다: 찾음/no-match 상태, 매칭된 문서 ID, 요약입니다. 기본적으로 긴 excerpt를 인라인하지 않으며, 요약만으로 부족할 때 `gctree resolve --id <id>`로 전체 문서를 엽니다.
161
+
160
162
  ```bash
161
163
  gctree scaffold --host codex --target /path/to/repo
162
164
  gctree scaffold --host claude-code --target /path/to/repo
package/docs/usage.md CHANGED
@@ -157,6 +157,8 @@ If a newly relevant repo should also become part of the durable context, the nat
157
157
 
158
158
  `gctree init` installs the provider-facing hook surface globally. `gctree scaffold` installs a local override into one target directory when a specific repository needs its own markdown snippets or local command surface.
159
159
 
160
+ The UserPromptSubmit hook injects compact pre-task context only: found/no-match state, matched document IDs, and summaries. It does not inline long excerpts by default; open full documents with `gctree resolve --id <id>` when the summary is not enough.
161
+
160
162
  ```bash
161
163
  gctree scaffold --host codex --target /path/to/repo
162
164
  gctree scaffold --host claude-code --target /path/to/repo
package/docs/usage.zh.md CHANGED
@@ -157,6 +157,8 @@ gctree ugc
157
157
 
158
158
  `gctree init` 会安装全局 provider hook 表面。`gctree scaffold` 则在某个仓库需要自己的 markdown 片段或本地命令表面时,将本地 override 安装到目标目录中。
159
159
 
160
+ UserPromptSubmit hook 只注入紧凑的 pre-task context:found/no-match 状态、匹配文档 ID 和摘要。默认不内联长 excerpt;当摘要不够时,用 `gctree resolve --id <id>` 打开完整文档。
161
+
160
162
  ```bash
161
163
  gctree scaffold --host codex --target /path/to/repo
162
164
  gctree scaffold --host claude-code --target /path/to/repo
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@handsupmin/gc-tree",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
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,