@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.
- package/dist/src/integration-files.js +28 -13
- package/dist/src/scaffold.js +51 -17
- package/dist/src/uninstall.js +16 -6
- package/dist/src/update.js +119 -0
- package/docs/usage.es.md +3 -2
- package/docs/usage.ja.md +3 -2
- package/docs/usage.ko.md +3 -2
- package/docs/usage.md +3 -2
- package/docs/usage.zh.md +3 -2
- package/package.json +1 -1
- package/skills/update-global-context/SKILL.md +31 -1
|
@@ -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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
|
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', '
|
|
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, '
|
|
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
|
}
|
package/dist/src/scaffold.js
CHANGED
|
@@ -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.
|
|
111
|
-
'3. Decide category and
|
|
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.
|
|
119
|
-
'5.
|
|
120
|
-
'
|
|
121
|
-
'
|
|
122
|
-
'
|
|
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
|
-
'
|
|
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: '
|
|
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/
|
|
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
|
|
313
|
-
await unmergeGcTreeHooksJson(
|
|
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
|
|
320
|
-
await unmergeGcTreeHooksJson(
|
|
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') {
|
package/dist/src/uninstall.js
CHANGED
|
@@ -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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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;
|
package/dist/src/update.js
CHANGED
|
@@ -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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
@@ -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.
|
|
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.
|