@handsupmin/gc-tree 1.1.1 → 1.1.3

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.
@@ -65,14 +65,15 @@ export async function removeManagedMarkdownBlock({ filePath, marker, }) {
65
65
  await writeFile(filePath, next, 'utf8');
66
66
  return 'removed';
67
67
  }
68
- function buildHookEntry(event) {
68
+ function buildHookEntry(event, target) {
69
69
  return {
70
70
  type: 'command',
71
71
  command: `gctree __hook --event ${event}`,
72
72
  metadata: {
73
73
  owner: 'gctree',
74
+ managed_by: '@handsupmin/gc-tree',
74
75
  },
75
- ...(event === 'UserPromptSubmit' ? { timeout: 10 } : {}),
76
+ ...(target === 'claude-code' || event === 'UserPromptSubmit' ? { timeout: 10 } : {}),
76
77
  };
77
78
  }
78
79
  function ensureObject(value) {
@@ -95,21 +96,29 @@ function mergeHookJson(raw, target) {
95
96
  };
96
97
  const upsertGroup = (event, matcher) => {
97
98
  const groups = ensureEventArray(event);
98
- let group = groups.find((candidate) => {
99
- const hooksArr = Array.isArray(candidate?.hooks) ? candidate.hooks : [];
100
- return hooksArr.some((entry) => isGcTreeHook(ensureObject(entry), event));
99
+ hooks[event] = groups
100
+ .map((candidate) => {
101
+ const group = ensureObject(candidate);
102
+ const hooksArr = Array.isArray(group.hooks) ? group.hooks : [];
103
+ const nextHooks = hooksArr.filter((entry) => !isGcTreeHook(ensureObject(entry), event));
104
+ return nextHooks.length > 0 ? { ...group, hooks: nextHooks } : null;
105
+ })
106
+ .filter(Boolean);
107
+ let group = hooks[event].find((candidate) => {
108
+ if (matcher === undefined)
109
+ return candidate.matcher === undefined;
110
+ return String(candidate.matcher ?? '') === matcher;
101
111
  });
102
112
  if (!group) {
103
- group = matcher ? { matcher, hooks: [] } : { hooks: [] };
104
- groups.push(group);
113
+ group = matcher !== undefined ? { matcher, hooks: [] } : { hooks: [] };
114
+ hooks[event].push(group);
105
115
  }
106
116
  if (!Array.isArray(group.hooks))
107
117
  group.hooks = [];
108
- group.hooks = group.hooks.filter((entry) => !isGcTreeHook(ensureObject(entry), event));
109
- group.hooks.push(buildHookEntry(event));
118
+ group.hooks.push(buildHookEntry(event, target));
110
119
  };
111
- upsertGroup(sessionEvent, target === 'codex' ? 'startup|resume' : '*');
112
- upsertGroup(promptEvent, target === 'codex' ? undefined : '*');
120
+ upsertGroup(sessionEvent, target === 'codex' ? 'startup|resume' : '');
121
+ upsertGroup(promptEvent, target === 'codex' ? undefined : '');
113
122
  return `${JSON.stringify({ ...parsed, hooks }, null, 2)}\n`;
114
123
  }
115
124
  function unmergeHookJson(raw, events) {
@@ -166,7 +175,7 @@ export function gctreeManagedMarkdownTargets(targetDir) {
166
175
  export function gctreeHookJsonTargets(targetDir) {
167
176
  return {
168
177
  codex: join(targetDir, '.codex', 'hooks.json'),
169
- claude: join(targetDir, '.claude', 'hooks', 'hooks.json'),
178
+ claude: join(targetDir, '.claude', 'settings.json'),
170
179
  };
171
180
  }
172
181
  export function gctreeGlobalRoot(host) {
@@ -177,5 +186,11 @@ export function gctreeGlobalRoot(host) {
177
186
  }
178
187
  export function gctreeGlobalHookJsonTarget(host) {
179
188
  const root = gctreeGlobalRoot(host);
180
- return host === 'codex' ? join(root, 'hooks.json') : join(root, 'hooks', 'hooks.json');
189
+ return host === 'codex' ? join(root, 'hooks.json') : join(root, 'settings.json');
190
+ }
191
+ export function gctreeLegacyClaudeHooksJsonTarget(targetDir) {
192
+ return join(targetDir, 'hooks', 'hooks.json');
193
+ }
194
+ export function gctreeLegacyLocalClaudeHooksJsonTarget(targetDir) {
195
+ return join(targetDir, '.claude', 'hooks', 'hooks.json');
181
196
  }
@@ -1,6 +1,6 @@
1
1
  import { access, mkdir, writeFile } from 'node:fs/promises';
2
2
  import { dirname, join } from 'node:path';
3
- import { gctreeGlobalHookJsonTarget, gctreeGlobalRoot, gctreeHookJsonTargets, gctreeManagedMarkdownTargets, mergeGcTreeHooksJson, unmergeGcTreeHooksJson, upsertManagedMarkdownBlock, } from './integration-files.js';
3
+ import { gctreeGlobalHookJsonTarget, gctreeGlobalRoot, gctreeHookJsonTargets, gctreeLegacyClaudeHooksJsonTarget, gctreeLegacyLocalClaudeHooksJsonTarget, gctreeManagedMarkdownTargets, mergeGcTreeHooksJson, unmergeGcTreeHooksJson, upsertManagedMarkdownBlock, } from './integration-files.js';
4
4
  import { onboardingCompletionLines, onboardingProtocolLines } from './onboarding-protocol.js';
5
5
  function renderCodexAgentsSnippet() {
6
6
  return [
@@ -107,25 +107,45 @@ function renderCodexOnboardSkill() {
107
107
  function renderUpdateProtocol() {
108
108
  return [
109
109
  '1. Run `gctree status` and state the active gc-branch.',
110
- '2. Understand what changed read the user\'s request, inspect relevant code or docs if needed, and determine the right content to capture.',
111
- '3. Decide category and path automatically based on content — never ask the user about scope or placement:',
110
+ '2. Find the target doc before writing. Use `gctree resolve --query "<new fact or term>"`; if a match is incomplete, read the full doc with `gctree show-doc --id "<id>"`. For an existing doc, preserve useful existing details and index entries unless the user explicitly wants them removed.',
111
+ '3. Decide category and slug automatically based on content — never ask the user about scope or placement:',
112
112
  ' - `docs/workflows/<name>.md` — cross-repo action sequences, step-by-step procedures',
113
113
  ' - `docs/conventions/<repo>.md` — code patterns, naming, validation, DTO/service conventions for a repo',
114
114
  ' - `docs/repos/<repo>.md` — repo role, key paths, cross-repo dependencies',
115
115
  ' - `docs/domain/<concept>.md` — domain terms, acronyms, business logic',
116
116
  ' - `docs/infra/<topic>.md` — infra, deployment, environment config',
117
117
  ' - `docs/role/<name>.md` — team member roles, responsibilities',
118
- '4. Write a `## Summary` that is actionable: actual patterns/commands/constraints a developer needs not a sentence about what the doc covers.',
119
- '5. Include a `## Index Entries` section in each doc\'s content with many keywords: aliases, related terms, command names, field names, workflow names, acronyms. More entries = more chances of being found by `gctree resolve`. Fewer than 5 entries almost always means more are needed.',
120
- '5a. **Bilingual index entries**: when the workflow language is not English, every doc\'s `## Index Entries` MUST contain BOTH the workflow language AND English forms for every technical term, repo name, workflow name, and concept. gc-tree retrieval is token-overlap based and cannot translate at query time, so a Korean-language doc that omits English aliases will silently miss any English query (and vice versa). Acronyms (JWT, EMPI, VCF) stay in original form in both. Body and `## Summary` stay in the workflow language; bilingual coverage lives in the index.',
121
- '6. Create a temporary JSON file with the updated `docs[]` and run `gctree __apply-update --input <temp-file>`.',
122
- '7. Run `gctree verify-onboarding --branch <gc-branch>` and inspect the output:',
118
+ '4. In the update JSON, `slug` is the path without `docs/` and without `.md`. Example: existing file `docs/conventions/mvldev-assignment-backend-smson.md` must use `slug: "conventions/mvldev-assignment-backend-smson"`.',
119
+ '5. Use exactly this JSON schema. Unknown fields are rejected; do not use `id`, `path`, or `content`.',
120
+ '```json',
121
+ '{',
122
+ ' "branch": "<gc-branch>",',
123
+ ' "docs": [',
124
+ ' {',
125
+ ' "title": "conventions: example repo",',
126
+ ' "slug": "conventions/example-repo",',
127
+ ' "category": "conventions",',
128
+ ' "summary": "Actionable one-paragraph summary with patterns, commands, and constraints.",',
129
+ ' "body": "## Patterns\\n\\n- Details body only. Do not include # title, ## Summary, or ## Index Entries here.",',
130
+ ' "indexLabel": "example repo conventions",',
131
+ ' "indexEntries": ["example repo", "ExampleRepo", "주요 한국어 검색어", "command names", "field names"]',
132
+ ' }',
133
+ ' ]',
134
+ '}',
135
+ '```',
136
+ '6. Write an actionable `summary`: actual patterns/commands/constraints a developer needs — not a sentence about what the doc covers.',
137
+ '7. Put search terms in `indexEntries`, not inside `body`. Include aliases, related terms, command names, field names, workflow names, acronyms, and the exact phrases users are likely to ask. Fewer than 5 entries almost always means more are needed.',
138
+ '7a. **Bilingual index entries**: when the workflow language is not English, `indexEntries` MUST contain BOTH the workflow language AND English forms for every technical term, repo name, workflow name, and concept. gc-tree retrieval is token-overlap based and cannot translate at query time. Acronyms (JWT, EMPI, VCF) stay in original form in both. Body and summary stay in the workflow language; bilingual coverage lives in the index.',
139
+ '8. Create a temporary JSON file with the updated `docs[]` and run `gctree __apply-update --input <temp-file>`.',
140
+ '9. Run `gctree verify-onboarding --branch <gc-branch>` and inspect the output:',
123
141
  ' - Check that every updated doc appears in the verified doc list.',
124
142
  ' - Check that index entries for the new content exist and are searchable (not just generic titles).',
125
143
  ' - Check that no doc has `category: "general"` — reassign to correct category if so.',
126
144
  ' - Check that important concepts from the update are present as index labels — if missing, re-apply with added index entries.',
127
145
  ' - Self-heal any issues without asking the user: fix category, add index entries, re-run `gctree __apply-update`, re-verify.',
128
- '8. Show the user which docs were updated and confirm the index now covers the new content.',
146
+ '10. Run `gctree resolve --query "<new keyword>"` and `gctree show-doc --id "<slug>"` for at least one new keyword/doc to prove retrieval and details are correct.',
147
+ '11. If you intended to update an existing doc and `doc_count` increased unexpectedly, do not report success. Inspect the duplicate, remove the accidental doc, then re-apply with the correct `slug`.',
148
+ '12. Show the user which docs were updated and confirm the index now covers the new content.',
129
149
  ];
130
150
  }
131
151
  function renderCodexUpdateSkill() {
@@ -181,11 +201,15 @@ function renderClaudeHooksJson() {
181
201
  hooks: {
182
202
  SessionStart: [
183
203
  {
184
- matcher: '*',
204
+ matcher: '',
185
205
  hooks: [
186
206
  {
187
207
  type: 'command',
188
208
  command: 'gctree __hook --event SessionStart',
209
+ metadata: {
210
+ owner: 'gctree',
211
+ managed_by: '@handsupmin/gc-tree',
212
+ },
189
213
  timeout: 10,
190
214
  },
191
215
  ],
@@ -193,11 +217,15 @@ function renderClaudeHooksJson() {
193
217
  ],
194
218
  UserPromptSubmit: [
195
219
  {
196
- matcher: '*',
220
+ matcher: '',
197
221
  hooks: [
198
222
  {
199
223
  type: 'command',
200
224
  command: 'gctree __hook --event UserPromptSubmit',
225
+ metadata: {
226
+ owner: 'gctree',
227
+ managed_by: '@handsupmin/gc-tree',
228
+ },
201
229
  timeout: 10,
202
230
  },
203
231
  ],
@@ -272,7 +300,7 @@ export function scaffoldFiles(host, scope = 'local') {
272
300
  }
273
301
  if (scope === 'global') {
274
302
  return [
275
- { path: 'hooks/hooks.json', content: renderClaudeHooksJson() },
303
+ { path: 'settings.json', content: renderClaudeHooksJson() },
276
304
  { path: 'hooks/gctree-session-start.md', content: renderClaudeSessionStartHook() },
277
305
  { path: 'commands/gc-resolve-context.md', content: renderClaudeResolveCommand() },
278
306
  { path: 'commands/gc-onboard.md', content: renderClaudeOnboardCommand() },
@@ -281,7 +309,7 @@ export function scaffoldFiles(host, scope = 'local') {
281
309
  }
282
310
  return [
283
311
  { path: 'CLAUDE.md', content: renderClaudeSnippet() },
284
- { path: '.claude/hooks/hooks.json', content: renderClaudeHooksJson() },
312
+ { path: '.claude/settings.json', content: renderClaudeHooksJson() },
285
313
  { path: '.claude/hooks/gctree-session-start.md', content: renderClaudeSessionStartHook() },
286
314
  { path: '.claude/commands/gc-resolve-context.md', content: renderClaudeResolveCommand() },
287
315
  { path: '.claude/commands/gc-onboard.md', content: renderClaudeOnboardCommand() },
@@ -309,15 +337,21 @@ export async function scaffoldHostIntegration({ host, targetDir, force = false,
309
337
  written.push(managedMarkdownPath);
310
338
  hookPath = isCodex ? hookTargets.codex : hookTargets.claude;
311
339
  if (!isCodex) {
312
- const oldHooksPath = join(resolvedTargetDir, '.claude', 'settings.json');
313
- await unmergeGcTreeHooksJson(oldHooksPath);
340
+ const legacyHooksPath = gctreeLegacyLocalClaudeHooksJsonTarget(resolvedTargetDir);
341
+ const legacyStatus = await unmergeGcTreeHooksJson(legacyHooksPath);
342
+ if (legacyStatus !== 'missing') {
343
+ written.push(legacyHooksPath);
344
+ }
314
345
  }
315
346
  }
316
347
  else {
317
348
  hookPath = gctreeGlobalHookJsonTarget(host);
318
349
  if (!isCodex) {
319
- const oldHooksPath = join(resolvedTargetDir, 'settings.json');
320
- await unmergeGcTreeHooksJson(oldHooksPath);
350
+ const legacyHooksPath = gctreeLegacyClaudeHooksJsonTarget(resolvedTargetDir);
351
+ const legacyStatus = await unmergeGcTreeHooksJson(legacyHooksPath);
352
+ if (legacyStatus !== 'missing') {
353
+ written.push(legacyHooksPath);
354
+ }
321
355
  }
322
356
  }
323
357
  if (scope === 'local') {
@@ -1,6 +1,6 @@
1
1
  import { readdir, rm } from 'node:fs/promises';
2
2
  import { dirname, join } from 'node:path';
3
- import { gctreeGlobalHookJsonTarget, gctreeGlobalRoot, gctreeHookJsonTargets, gctreeManagedMarkdownTargets, pathExists, removeManagedMarkdownBlock, unmergeGcTreeHooksJson, } from './integration-files.js';
3
+ import { gctreeGlobalHookJsonTarget, gctreeGlobalRoot, gctreeHookJsonTargets, gctreeLegacyClaudeHooksJsonTarget, gctreeLegacyLocalClaudeHooksJsonTarget, gctreeManagedMarkdownTargets, pathExists, removeManagedMarkdownBlock, unmergeGcTreeHooksJson, } from './integration-files.js';
4
4
  import { scaffoldFiles } from './scaffold.js';
5
5
  async function pathHasEntries(path) {
6
6
  const entries = await readdir(path).catch(() => []);
@@ -26,6 +26,14 @@ async function uninstallHostScaffold({ host, scope, targetDir, removed, }) {
26
26
  const hookPath = scope === 'local'
27
27
  ? (isCodex ? gctreeHookJsonTargets(targetDir).codex : gctreeHookJsonTargets(targetDir).claude)
28
28
  : gctreeGlobalHookJsonTarget(host);
29
+ const hookPaths = isCodex
30
+ ? [hookPath]
31
+ : [
32
+ hookPath,
33
+ scope === 'local'
34
+ ? gctreeLegacyLocalClaudeHooksJsonTarget(targetDir)
35
+ : gctreeLegacyClaudeHooksJsonTarget(targetDir),
36
+ ];
29
37
  if (managedMarkdownPath) {
30
38
  const markdownStatus = await removeManagedMarkdownBlock({
31
39
  filePath: managedMarkdownPath,
@@ -36,14 +44,16 @@ async function uninstallHostScaffold({ host, scope, targetDir, removed, }) {
36
44
  await pruneEmptyParents(managedMarkdownPath, targetDir);
37
45
  }
38
46
  }
39
- const hookStatus = await unmergeGcTreeHooksJson(hookPath);
40
- if (hookStatus !== 'missing') {
41
- removed.push(hookPath);
42
- await pruneEmptyParents(hookPath, targetDir);
47
+ for (const candidate of [...new Set(hookPaths)]) {
48
+ const hookStatus = await unmergeGcTreeHooksJson(candidate);
49
+ if (hookStatus !== 'missing') {
50
+ removed.push(candidate);
51
+ await pruneEmptyParents(candidate, targetDir);
52
+ }
43
53
  }
44
54
  const extraTargets = scaffoldFiles(host, scope)
45
55
  .map((file) => join(targetDir, file.path))
46
- .filter((path) => path !== hookPath && path !== managedMarkdownPath);
56
+ .filter((path) => !hookPaths.includes(path) && path !== managedMarkdownPath);
47
57
  for (const target of [...new Set(extraTargets)]) {
48
58
  if (!(await pathExists(target)))
49
59
  continue;
@@ -4,6 +4,14 @@ import { dirname, join } from 'node:path';
4
4
  import { renderDocMarkdown, slugify } from './markdown.js';
5
5
  import { branchDocsDir, DEFAULT_BRANCH, settingsPath } from './paths.js';
6
6
  import { ensureBranchExists, updateBranchMeta, writeIndexFromDocs } from './store.js';
7
+ const UPDATE_ROOT_KEYS = new Set(['branch', 'branchSummary', 'docs']);
8
+ const UPDATE_DOC_KEYS = new Set(['title', 'slug', 'summary', 'body', 'tags', 'category', 'indexLabel', 'indexEntries']);
9
+ const VALID_UPDATE_CATEGORIES = new Set(['role', 'repos', 'domain', 'workflows', 'conventions', 'infra', 'verification']);
10
+ const LEGACY_DOC_FIELD_HINTS = {
11
+ id: 'use `slug` instead, without `docs/` or `.md`',
12
+ path: 'use `slug` instead, without `docs/` or `.md`',
13
+ content: 'use `body` instead, and put search terms in `indexEntries`',
14
+ };
7
15
  function docRelativePath(doc) {
8
16
  if (doc.slug?.includes('/'))
9
17
  return `${doc.slug.replace(/\.md$/i, '')}.md`;
@@ -11,7 +19,118 @@ function docRelativePath(doc) {
11
19
  const category = doc.category ? slugify(doc.category) : '';
12
20
  return category ? `${category}/${fileBase}.md` : `${fileBase}.md`;
13
21
  }
22
+ function isRecord(value) {
23
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
24
+ }
25
+ function unknownKeys(record, allowed) {
26
+ return Object.keys(record).filter((key) => !allowed.has(key));
27
+ }
28
+ function requireNonEmptyString(record, key, subject) {
29
+ const value = record[key];
30
+ if (typeof value !== 'string' || value.trim() === '') {
31
+ throw new Error(`Invalid gctree update input: ${subject}.${key} must be a non-empty string.`);
32
+ }
33
+ return value.trim();
34
+ }
35
+ function validateOptionalString(record, key, subject) {
36
+ const value = record[key];
37
+ if (value === undefined)
38
+ return undefined;
39
+ if (typeof value !== 'string' || value.trim() === '') {
40
+ throw new Error(`Invalid gctree update input: ${subject}.${key} must be a non-empty string when provided.`);
41
+ }
42
+ return value.trim();
43
+ }
44
+ function validateOptionalStringArray(record, key, subject) {
45
+ const value = record[key];
46
+ if (value === undefined)
47
+ return undefined;
48
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
49
+ throw new Error(`Invalid gctree update input: ${subject}.${key} must be an array of strings when provided.`);
50
+ }
51
+ const entries = value.map((entry) => entry.trim()).filter(Boolean);
52
+ if (entries.length === 0) {
53
+ throw new Error(`Invalid gctree update input: ${subject}.${key} must contain at least one non-empty string when provided.`);
54
+ }
55
+ return entries;
56
+ }
57
+ function validateUpdateSlug(slug, subject) {
58
+ if (slug.startsWith('docs/')) {
59
+ throw new Error(`Invalid gctree update input: ${subject}.slug must omit the docs/ prefix. Example: use "conventions/example" instead of "docs/conventions/example.md".`);
60
+ }
61
+ if (slug.endsWith('.md')) {
62
+ throw new Error(`Invalid gctree update input: ${subject}.slug must omit the .md suffix. Example: use "conventions/example" instead of "conventions/example.md".`);
63
+ }
64
+ if (slug.startsWith('/') || slug.includes('\\') || slug.split('/').includes('..')) {
65
+ throw new Error(`Invalid gctree update input: ${subject}.slug must be a relative doc slug without absolute paths, backslashes, or "..".`);
66
+ }
67
+ if (slug.includes('/')) {
68
+ const category = slug.split('/')[0];
69
+ if (!VALID_UPDATE_CATEGORIES.has(category)) {
70
+ throw new Error(`Invalid gctree update input: ${subject}.slug category must be one of ${[...VALID_UPDATE_CATEGORIES].join(', ')}.`);
71
+ }
72
+ }
73
+ }
74
+ function validateUpdateBody(body, subject) {
75
+ if (/^\s*#\s+/.test(body)) {
76
+ throw new Error(`Invalid gctree update input: ${subject}.body must not include the top-level markdown title; use the title field.`);
77
+ }
78
+ if (/^## Summary\b/m.test(body)) {
79
+ throw new Error(`Invalid gctree update input: ${subject}.body must not include ## Summary; use the summary field.`);
80
+ }
81
+ if (/^## Index Entries\b/m.test(body)) {
82
+ throw new Error(`Invalid gctree update input: ${subject}.body must not include ## Index Entries; use the indexEntries array.`);
83
+ }
84
+ }
85
+ function validateContextUpdateInput(input) {
86
+ if (!isRecord(input)) {
87
+ throw new Error('Invalid gctree update input: root must be an object with docs[].');
88
+ }
89
+ const extraRootKeys = unknownKeys(input, UPDATE_ROOT_KEYS);
90
+ if (extraRootKeys.length > 0) {
91
+ throw new Error(`Invalid gctree update input: unsupported root field(s): ${extraRootKeys.join(', ')}.`);
92
+ }
93
+ validateOptionalString(input, 'branch', 'input');
94
+ validateOptionalString(input, 'branchSummary', 'input');
95
+ if (!Array.isArray(input.docs) || input.docs.length === 0) {
96
+ throw new Error('Invalid gctree update input: docs must be a non-empty array.');
97
+ }
98
+ input.docs.forEach((doc, index) => {
99
+ const subject = `docs[${index}]`;
100
+ if (!isRecord(doc)) {
101
+ throw new Error(`Invalid gctree update input: ${subject} must be an object.`);
102
+ }
103
+ const extraDocKeys = unknownKeys(doc, UPDATE_DOC_KEYS);
104
+ if (extraDocKeys.length > 0) {
105
+ const hints = extraDocKeys
106
+ .map((key) => LEGACY_DOC_FIELD_HINTS[key])
107
+ .filter((hint) => Boolean(hint));
108
+ const suffix = hints.length > 0 ? ` ${[...new Set(hints)].join('; ')}.` : '';
109
+ throw new Error(`Invalid gctree update input: ${subject} has unsupported field(s): ${extraDocKeys.join(', ')}.${suffix}`);
110
+ }
111
+ requireNonEmptyString(doc, 'title', subject);
112
+ const slug = requireNonEmptyString(doc, 'slug', subject);
113
+ requireNonEmptyString(doc, 'summary', subject);
114
+ const body = requireNonEmptyString(doc, 'body', subject);
115
+ const category = validateOptionalString(doc, 'category', subject);
116
+ validateOptionalString(doc, 'indexLabel', subject);
117
+ validateOptionalStringArray(doc, 'tags', subject);
118
+ const indexEntries = validateOptionalStringArray(doc, 'indexEntries', subject);
119
+ if (!indexEntries) {
120
+ throw new Error(`Invalid gctree update input: ${subject}.indexEntries must be a non-empty array of search terms.`);
121
+ }
122
+ validateUpdateSlug(slug, subject);
123
+ validateUpdateBody(body, subject);
124
+ if (category && !VALID_UPDATE_CATEGORIES.has(category)) {
125
+ throw new Error(`Invalid gctree update input: ${subject}.category must be one of ${[...VALID_UPDATE_CATEGORIES].join(', ')}.`);
126
+ }
127
+ if (category && slug.includes('/') && slug.split('/')[0] !== category) {
128
+ throw new Error(`Invalid gctree update input: ${subject}.category must match the first segment of slug "${slug}".`);
129
+ }
130
+ });
131
+ }
14
132
  export async function updateBranchContext({ home, input, branch, }) {
133
+ validateContextUpdateInput(input);
15
134
  const targetBranch = branch || input.branch || DEFAULT_BRANCH;
16
135
  await ensureBranchExists(home, targetBranch);
17
136
  await mkdir(branchDocsDir(home, targetBranch), { recursive: true });
package/docs/usage.es.md CHANGED
@@ -181,7 +181,7 @@ AGENTS.md ← fragmento de gctree añadido a la
181
181
  **Archivos globales de Claude Code (`gctree init`):**
182
182
 
183
183
  ```
184
- ~/.claude/hooks/hooks.json ← hooks de auto-resolve para SessionStart/UserPromptSubmit
184
+ ~/.claude/settings.json bloque de hooks de auto-resolve para SessionStart/UserPromptSubmit
185
185
  ~/.claude/hooks/gctree-session-start.md ← nota fallback de inicio de sesión
186
186
  ~/.claude/commands/gc-resolve-context.md ← comando slash de resolve
187
187
  ~/.claude/commands/gc-onboard.md ← comando slash de onboard
@@ -190,9 +190,10 @@ AGENTS.md ← fragmento de gctree añadido a la
190
190
 
191
191
  **Archivos de override local para `gctree scaffold --host claude-code`:**
192
192
 
193
+ Los hooks de Claude Code permanecen en el `~/.claude/settings.json` global; el scaffold local no escribe un archivo de hooks duplicado.
194
+
193
195
  ```
194
196
  CLAUDE.md ← fragmento de gctree añadido
195
- .claude/hooks/hooks.json ← hooks de auto-resolve para SessionStart/UserPromptSubmit
196
197
  .claude/hooks/gctree-session-start.md ← nota fallback de inicio de sesión
197
198
  .claude/commands/gc-resolve-context.md ← comando slash de resolve
198
199
  .claude/commands/gc-onboard.md ← comando slash de onboard
package/docs/usage.ja.md CHANGED
@@ -181,7 +181,7 @@ AGENTS.md ← gctree のスニペットがエ
181
181
  **Claude Code のグローバルファイル(`gctree init`):**
182
182
 
183
183
  ```
184
- ~/.claude/hooks/hooks.json ← SessionStart / UserPromptSubmit の自動 resolve フック
184
+ ~/.claude/settings.json ← SessionStart / UserPromptSubmit の自動 resolve フックブロック
185
185
  ~/.claude/hooks/gctree-session-start.md ← セッション開始のフォールバックメモ
186
186
  ~/.claude/commands/gc-resolve-context.md ← resolve スラッシュコマンド
187
187
  ~/.claude/commands/gc-onboard.md ← onboard スラッシュコマンド
@@ -190,9 +190,10 @@ AGENTS.md ← gctree のスニペットがエ
190
190
 
191
191
  **`gctree scaffold --host claude-code` のローカル override ファイル:**
192
192
 
193
+ Claude Code のフックはグローバルの `~/.claude/settings.json` に残ります。ローカル scaffold は重複するフックファイルを書きません。
194
+
193
195
  ```
194
196
  CLAUDE.md ← gctree のスニペットが追記される
195
- .claude/hooks/hooks.json ← SessionStart / UserPromptSubmit の自動 resolve フック
196
197
  .claude/hooks/gctree-session-start.md ← セッション開始のフォールバックメモ
197
198
  .claude/commands/gc-resolve-context.md ← resolve スラッシュコマンド
198
199
  .claude/commands/gc-onboard.md ← onboard スラッシュコマンド
package/docs/usage.ko.md CHANGED
@@ -181,7 +181,7 @@ AGENTS.md ← 에이전트 지시사항에 gctr
181
181
  **Claude Code 전역 파일 (`gctree init`):**
182
182
 
183
183
  ```
184
- ~/.claude/hooks/hooks.json ← SessionStart/UserPromptSubmit 자동 resolve 훅
184
+ ~/.claude/settings.json ← SessionStart/UserPromptSubmit 자동 resolve 훅 블록
185
185
  ~/.claude/hooks/gctree-session-start.md ← 세션 시작 fallback 메모
186
186
  ~/.claude/commands/gc-resolve-context.md ← resolve 슬래시 명령
187
187
  ~/.claude/commands/gc-onboard.md ← onboard 슬래시 명령
@@ -190,9 +190,10 @@ AGENTS.md ← 에이전트 지시사항에 gctr
190
190
 
191
191
  **`gctree scaffold --host claude-code` 로컬 override 파일:**
192
192
 
193
+ Claude Code 훅은 전역 `~/.claude/settings.json`에 유지됩니다. 로컬 scaffold는 중복 훅 파일을 쓰지 않습니다.
194
+
193
195
  ```
194
196
  CLAUDE.md ← gctree 스니펫 추가
195
- .claude/hooks/hooks.json ← SessionStart/UserPromptSubmit 자동 resolve 훅
196
197
  .claude/hooks/gctree-session-start.md ← 세션 시작 fallback 메모
197
198
  .claude/commands/gc-resolve-context.md ← resolve 슬래시 명령
198
199
  .claude/commands/gc-onboard.md ← onboard 슬래시 명령
package/docs/usage.md CHANGED
@@ -181,7 +181,7 @@ AGENTS.md ← gctree snippet appended to agent
181
181
  **Global files for Claude Code (`gctree init`):**
182
182
 
183
183
  ```
184
- ~/.claude/hooks/hooks.json ← SessionStart/UserPromptSubmit auto-resolve hooks
184
+ ~/.claude/settings.json ← SessionStart/UserPromptSubmit auto-resolve hooks block
185
185
  ~/.claude/hooks/gctree-session-start.md ← session-start fallback note
186
186
  ~/.claude/commands/gc-resolve-context.md ← resolve slash command
187
187
  ~/.claude/commands/gc-onboard.md ← onboard slash command
@@ -190,9 +190,10 @@ AGENTS.md ← gctree snippet appended to agent
190
190
 
191
191
  **Local override files for `gctree scaffold --host claude-code`:**
192
192
 
193
+ Claude Code hooks remain in the global `~/.claude/settings.json`; local scaffolding does not write a duplicate hook file.
194
+
193
195
  ```
194
196
  CLAUDE.md ← gctree snippet appended
195
- .claude/hooks/hooks.json ← SessionStart/UserPromptSubmit auto-resolve hooks
196
197
  .claude/hooks/gctree-session-start.md ← session-start fallback note
197
198
  .claude/commands/gc-resolve-context.md ← resolve slash command
198
199
  .claude/commands/gc-onboard.md ← onboard slash command
package/docs/usage.zh.md CHANGED
@@ -181,7 +181,7 @@ AGENTS.md ← gctree 代码片段追加到 agen
181
181
  **Claude Code 全局文件(`gctree init`):**
182
182
 
183
183
  ```
184
- ~/.claude/hooks/hooks.json ← SessionStart / UserPromptSubmit 自动 resolve hook
184
+ ~/.claude/settings.json ← SessionStart / UserPromptSubmit 自动 resolve hook
185
185
  ~/.claude/hooks/gctree-session-start.md ← 会话启动 fallback 说明
186
186
  ~/.claude/commands/gc-resolve-context.md ← resolve 斜杠命令
187
187
  ~/.claude/commands/gc-onboard.md ← onboard 斜杠命令
@@ -190,9 +190,10 @@ AGENTS.md ← gctree 代码片段追加到 agen
190
190
 
191
191
  **`gctree scaffold --host claude-code` 的本地 override 文件:**
192
192
 
193
+ Claude Code hook 保留在全局 `~/.claude/settings.json` 中;本地 scaffold 不写入重复的 hook 文件。
194
+
193
195
  ```
194
196
  CLAUDE.md ← gctree 代码片段追加
195
- .claude/hooks/hooks.json ← SessionStart / UserPromptSubmit 自动 resolve hook
196
197
  .claude/hooks/gctree-session-start.md ← 会话启动 fallback 说明
197
198
  .claude/commands/gc-resolve-context.md ← resolve 斜杠命令
198
199
  .claude/commands/gc-onboard.md ← onboard 斜杠命令
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@handsupmin/gc-tree",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
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,
@@ -18,4 +18,34 @@ Use guided updates for durable changes and reserve onboarding for empty gc-branc
18
18
  - `gctree update-global-context`
19
19
  - `gctree update-gc`
20
20
  - `gctree ugc`
21
- 4. Keep the current gc-branch explicit throughout the conversation.
21
+ 4. Before writing update JSON, find the target doc with `gctree resolve --query "<new fact or term>"`. If the summary is insufficient, read the full doc with `gctree show-doc --id "<id>"`.
22
+ 5. For an existing doc, preserve useful existing Details and Index Entries unless the user explicitly wants them removed.
23
+ 6. Use `slug` as the durable path without `docs/` and without `.md`. Example: `docs/conventions/mvldev-assignment-backend-smson.md` becomes `"slug": "conventions/mvldev-assignment-backend-smson"`.
24
+ 7. Use exactly this `gctree __apply-update` JSON schema. Do not invent fields. In particular, never use `id`, `path`, or `content`; use `slug`, `body`, and `indexEntries`.
25
+
26
+ ```json
27
+ {
28
+ "branch": "<gc-branch>",
29
+ "docs": [
30
+ {
31
+ "title": "conventions: example repo",
32
+ "slug": "conventions/example-repo",
33
+ "category": "conventions",
34
+ "summary": "Actionable one-paragraph summary with patterns, commands, and constraints.",
35
+ "body": "## Patterns\n\n- Details body only. Do not include # title, ## Summary, or ## Index Entries here.",
36
+ "indexLabel": "example repo conventions",
37
+ "indexEntries": ["example repo", "ExampleRepo", "주요 한국어 검색어", "command names", "field names"]
38
+ }
39
+ ]
40
+ }
41
+ ```
42
+
43
+ 8. `body` is only the Details body. It may contain useful second-level sections such as `## Patterns`, but must not contain the top-level title, `## Summary`, or `## Index Entries`.
44
+ 9. `indexEntries` is mandatory. Add aliases, related terms, command names, field names, workflow names, acronyms, Korean terms, and English terms so `gctree resolve` can find the doc without translation.
45
+ 10. Apply the update with `gctree __apply-update --input <temp-file>`.
46
+ 11. Verify immediately:
47
+ - `gctree verify-onboarding --branch <gc-branch>`
48
+ - `gctree resolve --query "<new keyword>"`
49
+ - `gctree show-doc --id "<slug>"`
50
+ 12. If you intended to update an existing doc and `doc_count` increases unexpectedly, do not report success. Inspect the duplicate, remove the accidental doc, and re-apply with the correct `slug`.
51
+ 13. Keep the current gc-branch explicit throughout the conversation and tell the user exactly which docs changed.