@tienne/gestalt 0.83.2 → 0.84.1

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.
Files changed (142) hide show
  1. package/CLAUDE.md +3 -2
  2. package/README.ko.md +3 -1
  3. package/README.md +3 -1
  4. package/dist/package.json +1 -1
  5. package/dist/plugin/review-agents/harness-reviewer/AGENT.md +211 -0
  6. package/dist/plugin/role-agents/_shared/references/comment-rules.md +1 -1
  7. package/dist/plugin/role-agents/suggestion-verifier/AGENT.md +7 -1
  8. package/dist/plugin/skills/_shared/proactive-routing.md +1 -0
  9. package/dist/plugin/skills/_shared/rule-sources.md +1 -1
  10. package/dist/plugin/skills/agent/SKILL.md +2 -0
  11. package/dist/plugin/skills/review/SKILL.md +343 -7
  12. package/dist/plugin/skills/review-loop/SKILL.md +124 -2
  13. package/dist/plugin/skills/ship/SKILL.md +40 -1
  14. package/dist/schemas/gestalt.schema.json +12 -0
  15. package/dist/src/cli/commands/approve-gate.d.ts +24 -0
  16. package/dist/src/cli/commands/approve-gate.d.ts.map +1 -0
  17. package/dist/src/cli/commands/approve-gate.js +90 -0
  18. package/dist/src/cli/commands/approve-gate.js.map +1 -0
  19. package/dist/src/cli/commands/harness-refs-cross-pr.d.ts +218 -0
  20. package/dist/src/cli/commands/harness-refs-cross-pr.d.ts.map +1 -0
  21. package/dist/src/cli/commands/harness-refs-cross-pr.js +934 -0
  22. package/dist/src/cli/commands/harness-refs-cross-pr.js.map +1 -0
  23. package/dist/src/cli/commands/harness-refs.d.ts +19 -0
  24. package/dist/src/cli/commands/harness-refs.d.ts.map +1 -0
  25. package/dist/src/cli/commands/harness-refs.js +51 -0
  26. package/dist/src/cli/commands/harness-refs.js.map +1 -0
  27. package/dist/src/cli/index.d.ts.map +1 -1
  28. package/dist/src/cli/index.js +97 -0
  29. package/dist/src/cli/index.js.map +1 -1
  30. package/dist/src/core/config.d.ts +3 -0
  31. package/dist/src/core/config.d.ts.map +1 -1
  32. package/dist/src/core/config.js +23 -0
  33. package/dist/src/core/config.js.map +1 -1
  34. package/dist/src/core/types.d.ts +2 -0
  35. package/dist/src/core/types.d.ts.map +1 -1
  36. package/dist/src/harness-review/approve-gate.d.ts +53 -0
  37. package/dist/src/harness-review/approve-gate.d.ts.map +1 -0
  38. package/dist/src/harness-review/approve-gate.js +148 -0
  39. package/dist/src/harness-review/approve-gate.js.map +1 -0
  40. package/dist/src/harness-review/backward-search.d.ts +50 -0
  41. package/dist/src/harness-review/backward-search.d.ts.map +1 -0
  42. package/dist/src/harness-review/backward-search.js +183 -0
  43. package/dist/src/harness-review/backward-search.js.map +1 -0
  44. package/dist/src/harness-review/collect.d.ts +75 -0
  45. package/dist/src/harness-review/collect.d.ts.map +1 -0
  46. package/dist/src/harness-review/collect.js +253 -0
  47. package/dist/src/harness-review/collect.js.map +1 -0
  48. package/dist/src/harness-review/copy-drift.d.ts +83 -0
  49. package/dist/src/harness-review/copy-drift.d.ts.map +1 -0
  50. package/dist/src/harness-review/copy-drift.js +477 -0
  51. package/dist/src/harness-review/copy-drift.js.map +1 -0
  52. package/dist/src/harness-review/follow-up-marker.d.ts +29 -0
  53. package/dist/src/harness-review/follow-up-marker.d.ts.map +1 -0
  54. package/dist/src/harness-review/follow-up-marker.js +143 -0
  55. package/dist/src/harness-review/follow-up-marker.js.map +1 -0
  56. package/dist/src/harness-review/forward-search.d.ts +30 -0
  57. package/dist/src/harness-review/forward-search.d.ts.map +1 -0
  58. package/dist/src/harness-review/forward-search.js +381 -0
  59. package/dist/src/harness-review/forward-search.js.map +1 -0
  60. package/dist/src/harness-review/github-backend.d.ts +42 -0
  61. package/dist/src/harness-review/github-backend.d.ts.map +1 -0
  62. package/dist/src/harness-review/github-backend.js +130 -0
  63. package/dist/src/harness-review/github-backend.js.map +1 -0
  64. package/dist/src/harness-review/github-code-search.d.ts +46 -0
  65. package/dist/src/harness-review/github-code-search.d.ts.map +1 -0
  66. package/dist/src/harness-review/github-code-search.js +94 -0
  67. package/dist/src/harness-review/github-code-search.js.map +1 -0
  68. package/dist/src/harness-review/identifiers.d.ts +75 -0
  69. package/dist/src/harness-review/identifiers.d.ts.map +1 -0
  70. package/dist/src/harness-review/identifiers.js +678 -0
  71. package/dist/src/harness-review/identifiers.js.map +1 -0
  72. package/dist/src/harness-review/placeholders.d.ts +44 -0
  73. package/dist/src/harness-review/placeholders.d.ts.map +1 -0
  74. package/dist/src/harness-review/placeholders.js +138 -0
  75. package/dist/src/harness-review/placeholders.js.map +1 -0
  76. package/dist/src/harness-review/related-pr.d.ts +133 -0
  77. package/dist/src/harness-review/related-pr.d.ts.map +1 -0
  78. package/dist/src/harness-review/related-pr.js +553 -0
  79. package/dist/src/harness-review/related-pr.js.map +1 -0
  80. package/dist/src/harness-review/related-repos.d.ts +69 -0
  81. package/dist/src/harness-review/related-repos.d.ts.map +1 -0
  82. package/dist/src/harness-review/related-repos.js +430 -0
  83. package/dist/src/harness-review/related-repos.js.map +1 -0
  84. package/dist/src/harness-review/remote-url.d.ts +8 -0
  85. package/dist/src/harness-review/remote-url.d.ts.map +1 -0
  86. package/dist/src/harness-review/remote-url.js +35 -0
  87. package/dist/src/harness-review/remote-url.js.map +1 -0
  88. package/dist/src/harness-review/replay.d.ts +163 -0
  89. package/dist/src/harness-review/replay.d.ts.map +1 -0
  90. package/dist/src/harness-review/replay.js +575 -0
  91. package/dist/src/harness-review/replay.js.map +1 -0
  92. package/dist/src/harness-review/rule-id-lists.d.ts +38 -0
  93. package/dist/src/harness-review/rule-id-lists.d.ts.map +1 -0
  94. package/dist/src/harness-review/rule-id-lists.js +189 -0
  95. package/dist/src/harness-review/rule-id-lists.js.map +1 -0
  96. package/dist/src/harness-review/search-backend.d.ts +59 -0
  97. package/dist/src/harness-review/search-backend.d.ts.map +1 -0
  98. package/dist/src/harness-review/search-backend.js +178 -0
  99. package/dist/src/harness-review/search-backend.js.map +1 -0
  100. package/dist/src/harness-review/self-contamination.d.ts +83 -0
  101. package/dist/src/harness-review/self-contamination.d.ts.map +1 -0
  102. package/dist/src/harness-review/self-contamination.js +560 -0
  103. package/dist/src/harness-review/self-contamination.js.map +1 -0
  104. package/dist/src/harness-review/self-references.d.ts +20 -0
  105. package/dist/src/harness-review/self-references.d.ts.map +1 -0
  106. package/dist/src/harness-review/self-references.js +173 -0
  107. package/dist/src/harness-review/self-references.js.map +1 -0
  108. package/dist/src/harness-review/three-state.d.ts +79 -0
  109. package/dist/src/harness-review/three-state.d.ts.map +1 -0
  110. package/dist/src/harness-review/three-state.js +238 -0
  111. package/dist/src/harness-review/three-state.js.map +1 -0
  112. package/dist/src/harness-review/types.d.ts +148 -0
  113. package/dist/src/harness-review/types.d.ts.map +1 -0
  114. package/dist/src/harness-review/types.js +74 -0
  115. package/dist/src/harness-review/types.js.map +1 -0
  116. package/dist/src/index.d.ts +1 -0
  117. package/dist/src/index.d.ts.map +1 -1
  118. package/dist/src/index.js +1 -0
  119. package/dist/src/index.js.map +1 -1
  120. package/dist/src/mcp/tools/review-passthrough.js +7 -1
  121. package/dist/src/mcp/tools/review-passthrough.js.map +1 -1
  122. package/dist/src/review/agent-matcher.d.ts +16 -1
  123. package/dist/src/review/agent-matcher.d.ts.map +1 -1
  124. package/dist/src/review/agent-matcher.js +31 -4
  125. package/dist/src/review/agent-matcher.js.map +1 -1
  126. package/dist/src/review/passthrough-engine.d.ts.map +1 -1
  127. package/dist/src/review/passthrough-engine.js +40 -2
  128. package/dist/src/review/passthrough-engine.js.map +1 -1
  129. package/package.json +1 -1
  130. package/plugin/.codex-plugin/plugin.json +1 -1
  131. package/plugin/.mcp.json +1 -1
  132. package/plugin/mcp.json +1 -1
  133. package/plugin/review-agents/harness-reviewer/AGENT.md +211 -0
  134. package/plugin/role-agents/_shared/references/comment-rules.md +1 -1
  135. package/plugin/role-agents/suggestion-verifier/AGENT.md +7 -1
  136. package/plugin/skills/_shared/proactive-routing.md +1 -0
  137. package/plugin/skills/_shared/rule-sources.md +1 -1
  138. package/plugin/skills/agent/SKILL.md +2 -0
  139. package/plugin/skills/review/SKILL.md +343 -7
  140. package/plugin/skills/review-loop/SKILL.md +124 -2
  141. package/plugin/skills/ship/SKILL.md +40 -1
  142. package/schemas/gestalt.schema.json +12 -0
@@ -0,0 +1,678 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { posix } from 'node:path';
3
+ function unquoteGitPath(raw) {
4
+ if (!raw.startsWith('"'))
5
+ return raw;
6
+ const body = raw.slice(1, -1);
7
+ const bytes = [];
8
+ for (let i = 0; i < body.length; i++) {
9
+ const ch = body[i];
10
+ if (ch !== '\\') {
11
+ bytes.push(...Buffer.from(ch, 'utf-8'));
12
+ continue;
13
+ }
14
+ const next = body[i + 1];
15
+ if (/[0-7]/.test(next)) {
16
+ bytes.push(parseInt(body.slice(i + 1, i + 4), 8));
17
+ i += 3;
18
+ }
19
+ else {
20
+ const map = { n: '\n', t: '\t', '"': '"', '\\': '\\' };
21
+ bytes.push(...Buffer.from(map[next] ?? next, 'utf-8'));
22
+ i += 1;
23
+ }
24
+ }
25
+ return Buffer.from(bytes).toString('utf-8');
26
+ }
27
+ function stripPrefix(p) {
28
+ const unq = unquoteGitPath(p.trim());
29
+ if (unq === '/dev/null')
30
+ return null;
31
+ return unq.replace(/^[ab]\//, '');
32
+ }
33
+ /** `git diff` 출력(unified diff)을 파일 단위로 나눈다. */
34
+ export function parseUnifiedDiff(diff) {
35
+ const files = [];
36
+ let cur = null;
37
+ let hunk = null;
38
+ let oldLine = 0;
39
+ let newLine = 0;
40
+ for (const line of diff.split('\n')) {
41
+ if (line.startsWith('diff --git ')) {
42
+ const m = /^diff --git (".*?"|a\/\S+) (".*?"|b\/\S+)$/.exec(line);
43
+ cur = {
44
+ oldPath: m ? stripPrefix(m[1]) : null,
45
+ newPath: m ? stripPrefix(m[2]) : null,
46
+ status: 'modified',
47
+ hunks: [],
48
+ };
49
+ files.push(cur);
50
+ hunk = null;
51
+ continue;
52
+ }
53
+ if (!cur)
54
+ continue;
55
+ if (!hunk) {
56
+ if (line.startsWith('new file mode'))
57
+ cur.status = 'added';
58
+ else if (line.startsWith('deleted file mode'))
59
+ cur.status = 'deleted';
60
+ else if (line.startsWith('rename from ')) {
61
+ cur.status = 'renamed';
62
+ cur.oldPath = unquoteGitPath(line.slice('rename from '.length));
63
+ }
64
+ else if (line.startsWith('rename to ')) {
65
+ cur.status = 'renamed';
66
+ cur.newPath = unquoteGitPath(line.slice('rename to '.length));
67
+ }
68
+ else if (line.startsWith('--- ')) {
69
+ cur.oldPath = stripPrefix(line.slice(4));
70
+ if (cur.oldPath === null)
71
+ cur.status = 'added';
72
+ continue;
73
+ }
74
+ else if (line.startsWith('+++ ')) {
75
+ cur.newPath = stripPrefix(line.slice(4));
76
+ if (cur.newPath === null)
77
+ cur.status = 'deleted';
78
+ continue;
79
+ }
80
+ }
81
+ const h = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
82
+ if (h) {
83
+ oldLine = Number(h[1]);
84
+ newLine = Number(h[2]);
85
+ hunk = { oldStart: oldLine, newStart: newLine, lines: [] };
86
+ cur.hunks.push(hunk);
87
+ continue;
88
+ }
89
+ if (!hunk)
90
+ continue;
91
+ if (line.startsWith('-')) {
92
+ hunk.lines.push({ type: 'removed', text: line.slice(1), lineNumber: oldLine++ });
93
+ }
94
+ else if (line.startsWith('+')) {
95
+ hunk.lines.push({ type: 'added', text: line.slice(1), lineNumber: newLine++ });
96
+ }
97
+ else if (line.startsWith(' ')) {
98
+ hunk.lines.push({ type: 'context', text: line.slice(1), lineNumber: oldLine });
99
+ oldLine++;
100
+ newLine++;
101
+ }
102
+ }
103
+ for (const f of files) {
104
+ if (f.status === 'deleted')
105
+ f.newPath = null;
106
+ if (f.status === 'added')
107
+ f.oldPath = null;
108
+ }
109
+ return files;
110
+ }
111
+ // ─── 경로 판정 ───────────────────────────────────────────────
112
+ function segmentsOf(path) {
113
+ return path.replace(/\\/g, '/').split('/').filter(Boolean);
114
+ }
115
+ function hasSegmentSeq(segs, seq) {
116
+ // 마지막 세그먼트는 파일 이름이라 디렉토리 매칭에서 뺀다
117
+ for (let i = 0; i + seq.length <= segs.length - 1; i++) {
118
+ if (seq.every((s, j) => segs[i + j] === s))
119
+ return true;
120
+ }
121
+ return false;
122
+ }
123
+ const HARNESS_DOC_NAMES = new Set(['SKILL.md', 'AGENT.md', 'CLAUDE.md', 'AGENTS.md', 'GEMINI.md']);
124
+ const PLUGIN_MANIFEST_NAMES = new Set(['plugin.json', 'marketplace.json']);
125
+ const RULE_DOC_NAMES = new Set([...HARNESS_DOC_NAMES, '.cursorrules', '.windsurfrules']);
126
+ /** 플러그인 매니페스트(plugin.json, marketplace.json)인가. */
127
+ export function isPluginManifestPath(path) {
128
+ return PLUGIN_MANIFEST_NAMES.has(posix.basename(path.replace(/\\/g, '/')));
129
+ }
130
+ // 이 디렉토리들은 하네스 말고는 쓰임이 없어 안에 든 파일은 확장자와 상관없이 하네스로 본다
131
+ const HARNESS_ONLY_DIRS = new Set([
132
+ '.claude',
133
+ '.agents',
134
+ '.claude-plugin',
135
+ '.codex-plugin',
136
+ '.grok-plugin',
137
+ ]);
138
+ // skills/, plugins/는 코드 디렉토리 이름과 겹친다(src/skills/index.ts). 그래서 문서꼴 파일만 하네스로 본다
139
+ const SHARED_NAME_DIRS = new Set(['skills', 'plugins']);
140
+ const HARNESS_TEXT_EXT_RE = /\.(md|mdc|json|toml|ya?ml|txt)$/i;
141
+ /**
142
+ * 다른 레포가 하네스로 읽는 경로인가. 역방향 검색 결과를 거르고 관련 레포 탐지가 읽을 문서를 고를 때 쓴다.
143
+ * SKILL.md, AGENT.md, CLAUDE.md, AGENTS.md, GEMINI.md, .mcp.json, 플러그인 매니페스트,
144
+ * .claude/, .agents/, .cursor/rules/, 플러그인 매니페스트 디렉토리 아래 파일,
145
+ * skills/와 plugins/ 아래 문서꼴 파일(md, mdc, json, toml, yaml, txt)이 해당한다.
146
+ */
147
+ export function isHarnessPath(path) {
148
+ const segs = segmentsOf(path);
149
+ const base = segs[segs.length - 1];
150
+ if (!base)
151
+ return false;
152
+ if (HARNESS_DOC_NAMES.has(base) || base === '.mcp.json' || PLUGIN_MANIFEST_NAMES.has(base)) {
153
+ return true;
154
+ }
155
+ const dirs = segs.slice(0, -1);
156
+ if (dirs.some((d) => HARNESS_ONLY_DIRS.has(d)))
157
+ return true;
158
+ if (hasSegmentSeq(segs, ['.cursor', 'rules']))
159
+ return true;
160
+ return HARNESS_TEXT_EXT_RE.test(base) && dirs.some((d) => SHARED_NAME_DIRS.has(d));
161
+ }
162
+ /**
163
+ * review 스킬 1단계가 이름만으로 ruleDocs에 넣는 경로인가.
164
+ * SKILL.md, AGENT.md, CLAUDE.md, AGENTS.md, GEMINI.md, .cursorrules, .windsurfrules,
165
+ * .claude/ 아래 .md, .cursor/rules/ 아래 파일.
166
+ */
167
+ export function isRuleDocPath(path) {
168
+ const segs = segmentsOf(path);
169
+ const base = segs[segs.length - 1];
170
+ if (!base)
171
+ return false;
172
+ if (RULE_DOC_NAMES.has(base))
173
+ return true;
174
+ if (segs.slice(0, -1).includes('.claude') && base.endsWith('.md'))
175
+ return true;
176
+ return hasSegmentSeq(segs, ['.cursor', 'rules']);
177
+ }
178
+ // ─── 패키지와 MCP 등록부 판정 ────────────────────────────────
179
+ export const MCP_SDK_PACKAGE = '@modelcontextprotocol/sdk';
180
+ function parseJson(content) {
181
+ try {
182
+ const v = JSON.parse(content);
183
+ return v && typeof v === 'object' && !Array.isArray(v) ? v : null;
184
+ }
185
+ catch {
186
+ return null;
187
+ }
188
+ }
189
+ /** private가 아니고 name이 있는 package.json인가. 다른 레포가 이 이름으로 설치하거나 부른다. */
190
+ export function isPublicPackageJson(path, content) {
191
+ if (posix.basename(path.replace(/\\/g, '/')) !== 'package.json')
192
+ return false;
193
+ const pkg = parseJson(content);
194
+ return !!pkg && typeof pkg.name === 'string' && pkg.name.length > 0 && pkg.private !== true;
195
+ }
196
+ /** package.json이 MCP SDK에 의존하는가. */
197
+ export function dependsOnMcpSdk(packageJsonContent) {
198
+ const pkg = parseJson(packageJsonContent);
199
+ if (!pkg)
200
+ return false;
201
+ for (const key of ['dependencies', 'devDependencies', 'peerDependencies']) {
202
+ const deps = pkg[key];
203
+ if (deps && typeof deps === 'object' && MCP_SDK_PACKAGE in deps)
204
+ return true;
205
+ }
206
+ return false;
207
+ }
208
+ const CODE_EXT_RE = /\.(?:[cm]?[jt]sx?)$/;
209
+ // server.tool('x'), server.registerTool('x'), 감싼 헬퍼 guardedTool('x') 같은 꼴
210
+ const TOOL_CALL_RE = /(?:\.tool|\b[a-z]\w*Tool)\s*\(\s*(['"`])([\w.:/-]+)\1/g;
211
+ const TOOL_NAME_FIELD_RE = /\bname\s*:\s*(['"`])([a-z][\w.:-]*)\1/g;
212
+ const REGISTRY_HINT_RE = /(?:\.tool\s*\(|\bregisterTool\s*\(|ListToolsRequestSchema|CallToolRequestSchema)/;
213
+ const TOOLISH_LINE_RE = /tool|inputSchema|outputSchema|description\s*:/i;
214
+ /** 파일 내용에 MCP 도구 등록부 꼴(server.tool, registerTool, ListTools 핸들러)이 있는가. */
215
+ export function isMcpToolRegistry(content) {
216
+ return REGISTRY_HINT_RE.test(content);
217
+ }
218
+ function toolNamesInLine(text, prevText, registry) {
219
+ const names = [];
220
+ for (const m of text.matchAll(TOOL_CALL_RE))
221
+ names.push(m[2]);
222
+ // name: 필드는 ListTools 응답에서 도구 이름이지만 서버 생성자의 name과 헷갈린다
223
+ if (registry && !/Server\s*\(/.test(text) && !(prevText && /Server\s*\(/.test(prevText))) {
224
+ for (const m of text.matchAll(TOOL_NAME_FIELD_RE))
225
+ names.push(m[2]);
226
+ }
227
+ return names;
228
+ }
229
+ /** 소스에서 패턴으로 잡히는 MCP 도구 이름을 전부 뽑는다. 등록부가 아니면 name: 필드는 안 본다. */
230
+ export function extractMcpToolNames(content) {
231
+ const registry = isMcpToolRegistry(content);
232
+ const lines = content.split('\n');
233
+ const out = new Set();
234
+ lines.forEach((l, i) => {
235
+ for (const n of toolNamesInLine(l, lines[i - 1], registry))
236
+ out.add(n);
237
+ });
238
+ return [...out];
239
+ }
240
+ function importsMcpSdk(text) {
241
+ return text.includes(MCP_SDK_PACKAGE);
242
+ }
243
+ function nearestPackageDependsOnSdk(path, readFile) {
244
+ if (!readFile)
245
+ return false;
246
+ let dir = posix.dirname(path);
247
+ for (;;) {
248
+ const pkgPath = dir === '.' ? 'package.json' : `${dir}/package.json`;
249
+ const content = readFile(pkgPath);
250
+ if (content !== undefined)
251
+ return dependsOnMcpSdk(content);
252
+ if (dir === '.' || dir === '/' || dir === '')
253
+ return false;
254
+ dir = posix.dirname(dir);
255
+ }
256
+ }
257
+ /**
258
+ * 변경 파일이 하네스 대상인지 판정한다. ruleDocs 꼴 문서이거나, 다른 레포가 이름으로 부르는
259
+ * 코드(MCP SDK에 의존하는 도구 등록부, private 아닌 package.json)면 그 이유를 돌려준다.
260
+ * content는 판정할 파일의 내용이다. readFile을 주면 가장 가까운 package.json의 의존성도 본다.
261
+ */
262
+ export function classifyHarnessTarget(path, content, readFile) {
263
+ if (isRuleDocPath(path))
264
+ return 'ruleDoc';
265
+ if (content === undefined)
266
+ return null;
267
+ if (isPublicPackageJson(path, content))
268
+ return 'publicPackage';
269
+ if (CODE_EXT_RE.test(path) &&
270
+ isMcpToolRegistry(content) &&
271
+ (importsMcpSdk(content) || nearestPackageDependsOnSdk(path, readFile))) {
272
+ return 'mcpToolRegistry';
273
+ }
274
+ return null;
275
+ }
276
+ // 흔한 이름이라 검색하면 온 조직이 걸린다. 레포에 하나뿐이어도 식별자로 쓰지 않는다
277
+ const GENERIC_FILE_NAMES = new Set([
278
+ ...RULE_DOC_NAMES,
279
+ 'README.md',
280
+ 'CHANGELOG.md',
281
+ 'LICENSE',
282
+ 'LICENSE.md',
283
+ 'package.json',
284
+ 'package-lock.json',
285
+ 'pnpm-lock.yaml',
286
+ 'tsconfig.json',
287
+ '.mcp.json',
288
+ 'mcp.json',
289
+ '.gitignore',
290
+ ...PLUGIN_MANIFEST_NAMES,
291
+ ]);
292
+ const HEADING_RE = /^\s{0,3}(#{1,6})\s+(.+?)\s*#*\s*$/;
293
+ const FRONTMATTER_NAME_RE = /^name:\s*['"]?([^'"#\s][^'"#]*?)['"]?\s*$/;
294
+ const JSON_NAME_RE = /^(\s*)"name"\s*:\s*"([^"]+)"/;
295
+ const FRONTMATTER_MAX_LINE = 30;
296
+ class IdentifierSet {
297
+ map = new Map();
298
+ static rank = { removed: 3, renamed: 2, modified: 1 };
299
+ add(kind, value, changeType, extractedBy) {
300
+ const v = value.trim();
301
+ if (!v)
302
+ return;
303
+ const key = `${kind}\u0000${v}\u0000${extractedBy}`;
304
+ const prev = this.map.get(key);
305
+ if (!prev || IdentifierSet.rank[changeType] > IdentifierSet.rank[prev.changeType]) {
306
+ this.map.set(key, { kind, value: v, changeType, extractedBy });
307
+ }
308
+ }
309
+ list() {
310
+ return [...this.map.values()];
311
+ }
312
+ }
313
+ function linesOf(file, type) {
314
+ return file.hunks.flatMap((h) => h.lines.filter((l) => l.type === type));
315
+ }
316
+ function fileChangeType(file) {
317
+ if (file.status === 'deleted')
318
+ return 'removed';
319
+ if (file.status === 'renamed')
320
+ return 'renamed';
321
+ return 'modified';
322
+ }
323
+ /** 지워진 값 가운데 다시 안 나온 것. 새로 생긴 값이 있으면 이름이 바뀐 것으로 본다. */
324
+ function diffValues(removed, added) {
325
+ const addedSet = new Set(added);
326
+ const removedSet = new Set(removed);
327
+ const renamedTo = added.some((a) => !removedSet.has(a));
328
+ return [...removedSet]
329
+ .filter((r) => !addedSet.has(r))
330
+ .map((value) => ({ value, changeType: renamedTo ? 'renamed' : 'removed' }));
331
+ }
332
+ function isMarkdown(path) {
333
+ return /\.mdc?$/i.test(path);
334
+ }
335
+ function extractPathIds(file, opts, out) {
336
+ const old = file.oldPath;
337
+ if (!old)
338
+ return;
339
+ const changeType = fileChangeType(file);
340
+ if (changeType === 'modified' && !isHarnessPath(old) && !isMarkdown(old))
341
+ return;
342
+ out.add('path', old, changeType, 'pattern');
343
+ const base = posix.basename(old);
344
+ if (!opts.baseFiles || GENERIC_FILE_NAMES.has(base))
345
+ return;
346
+ if (file.status === 'renamed' && file.newPath && posix.basename(file.newPath) === base)
347
+ return;
348
+ const same = opts.baseFiles.filter((f) => posix.basename(f) === base).length;
349
+ if (same === 1)
350
+ out.add('uniqueFileName', base, changeType, 'pattern');
351
+ }
352
+ function extractHeadings(file, out) {
353
+ const path = file.oldPath ?? file.newPath ?? '';
354
+ if (!isMarkdown(path) || !file.oldPath)
355
+ return;
356
+ const side = (types) => {
357
+ const found = [];
358
+ for (const h of file.hunks) {
359
+ let inFence = false;
360
+ for (const l of h.lines) {
361
+ if (!types.includes(l.type))
362
+ continue;
363
+ if (/^\s*(```|~~~)/.test(l.text)) {
364
+ inFence = !inFence;
365
+ continue;
366
+ }
367
+ if (inFence)
368
+ continue;
369
+ const m = HEADING_RE.exec(l.text);
370
+ if (m)
371
+ found.push({ text: m[2], type: l.type });
372
+ }
373
+ }
374
+ return found;
375
+ };
376
+ const removed = side(['context', 'removed'])
377
+ .filter((x) => x.type === 'removed')
378
+ .map((x) => x.text);
379
+ const added = side(['context', 'added'])
380
+ .filter((x) => x.type === 'added')
381
+ .map((x) => x.text);
382
+ for (const { value, changeType } of diffValues(removed, added)) {
383
+ out.add('heading', value, file.status === 'deleted' ? 'removed' : changeType, 'pattern');
384
+ }
385
+ }
386
+ /** 룰 문서에서 룰을 정의하는 줄이면 그 ID를 돌려준다. 규칙을 언급만 하는 줄은 정의로 안 본다 */
387
+ export function ruleIdOfDefinitionLine(text) {
388
+ // 기호를 한 번에 한 글자씩 먹는다. `[...]+`를 반복으로 감싸면 표 구분선 같은 긴 기호 줄에서 백트래킹이 폭증한다
389
+ const lead = /^\s*(?:[#>*|+-]\s*)*(?:\*\*|`)?\[?([A-Z]{1,2}-\d{1,3})(?![\w-])/.exec(text);
390
+ return lead ? lead[1] : null;
391
+ }
392
+ function ruleIdDefinitions(lines) {
393
+ const ids = [];
394
+ for (const l of lines) {
395
+ const id = ruleIdOfDefinitionLine(l.text);
396
+ if (id)
397
+ ids.push(id);
398
+ }
399
+ return ids;
400
+ }
401
+ function extractRuleIds(files, out) {
402
+ const mdFiles = files.filter((f) => isMarkdown(f.oldPath ?? f.newPath ?? ''));
403
+ const removedDefs = mdFiles.flatMap((f) => ruleIdDefinitions(linesOf(f, 'removed')));
404
+ const addedDefs = new Set(mdFiles.flatMap((f) => ruleIdDefinitions(linesOf(f, 'added'))));
405
+ for (const id of new Set(removedDefs)) {
406
+ out.add('ruleId', id, addedDefs.has(id) ? 'modified' : 'removed', 'pattern');
407
+ }
408
+ }
409
+ function nameKindOf(path) {
410
+ const segs = segmentsOf(path);
411
+ const base = segs[segs.length - 1] ?? '';
412
+ if (base === 'SKILL.md')
413
+ return 'skillName';
414
+ if (base === 'AGENT.md')
415
+ return 'agentName';
416
+ if (base.endsWith('.md') && segs.slice(0, -1).includes('agents'))
417
+ return 'agentName';
418
+ return null;
419
+ }
420
+ function nameFromPath(path, kind) {
421
+ const base = posix.basename(path);
422
+ if (kind === 'skillName' || base === 'AGENT.md')
423
+ return posix.basename(posix.dirname(path));
424
+ return base.replace(/\.md$/, '');
425
+ }
426
+ function extractFrontmatterNames(file, out) {
427
+ const old = file.oldPath;
428
+ if (!old)
429
+ return;
430
+ const kind = nameKindOf(old);
431
+ if (!kind)
432
+ return;
433
+ const pick = (type) => linesOf(file, type)
434
+ .filter((l) => l.lineNumber <= FRONTMATTER_MAX_LINE)
435
+ .map((l) => FRONTMATTER_NAME_RE.exec(l.text)?.[1])
436
+ .filter((v) => !!v);
437
+ const removed = pick('removed');
438
+ const found = diffValues(removed, pick('added'));
439
+ for (const { value, changeType } of found) {
440
+ out.add(kind, value, file.status === 'deleted' ? 'removed' : changeType, 'pattern');
441
+ }
442
+ // frontmatter를 안 건드린 채 디렉토리만 옮기거나 지우면 이름 줄이 diff에 없다
443
+ if (removed.length === 0 && (file.status === 'deleted' || file.status === 'renamed')) {
444
+ const oldName = nameFromPath(old, kind);
445
+ const newName = file.newPath ? nameFromPath(file.newPath, kind) : null;
446
+ if (oldName && oldName !== newName)
447
+ out.add(kind, oldName, fileChangeType(file), 'pattern');
448
+ }
449
+ }
450
+ function sideLines(file, type) {
451
+ return file.hunks.flatMap((h) => h.lines.filter((l) => l.type === 'context' || l.type === type));
452
+ }
453
+ /** 바뀐 줄의 "name" 값. 부모 키를 알려고 context 줄도 함께 훑는다. */
454
+ function namesOnChangedLines(file, type, maxIndent) {
455
+ const names = [];
456
+ for (const h of file.hunks) {
457
+ let parentKey = null;
458
+ for (const l of h.lines) {
459
+ if (l.type !== 'context' && l.type !== type)
460
+ continue;
461
+ const open = /"([^"]+)"\s*:\s*\{\s*$/.exec(l.text);
462
+ if (open)
463
+ parentKey = open[1];
464
+ else if (/^\s*\}/.test(l.text))
465
+ parentKey = null;
466
+ if (l.type !== type)
467
+ continue;
468
+ const m = JSON_NAME_RE.exec(l.text);
469
+ if (!m)
470
+ continue;
471
+ if (maxIndent !== null && m[1].replace(/\t/g, ' ').length > maxIndent)
472
+ continue;
473
+ // owner.name, author.name은 사람 이름이다
474
+ if (parentKey === 'owner' || parentKey === 'author')
475
+ continue;
476
+ names.push(m[2]);
477
+ }
478
+ }
479
+ return names;
480
+ }
481
+ function extractPluginNames(file, out) {
482
+ const path = file.oldPath;
483
+ if (!path || !isPluginManifestPath(path))
484
+ return;
485
+ const removed = namesOnChangedLines(file, 'removed', null);
486
+ const added = namesOnChangedLines(file, 'added', null);
487
+ for (const { value, changeType } of diffValues(removed, added)) {
488
+ out.add('pluginName', value, file.status === 'deleted' ? 'removed' : changeType, 'pattern');
489
+ }
490
+ }
491
+ function isPrivatePackage(file, opts) {
492
+ const content = opts.readFile?.(file.newPath ?? file.oldPath ?? '');
493
+ if (content !== undefined) {
494
+ const pkg = parseJson(content);
495
+ if (pkg)
496
+ return pkg.private === true;
497
+ }
498
+ const side = file.status === 'deleted' ? 'removed' : 'added';
499
+ return sideLines(file, side).some((l) => /"private"\s*:\s*true/.test(l.text));
500
+ }
501
+ function extractPackageNames(file, opts, out) {
502
+ const path = file.oldPath;
503
+ if (!path || posix.basename(path) !== 'package.json')
504
+ return;
505
+ if (isPrivatePackage(file, opts))
506
+ return;
507
+ // 최상위 name만. dependencies 안의 키나 workspaces 설정은 들여쓰기가 더 깊다
508
+ const removed = namesOnChangedLines(file, 'removed', 2);
509
+ const added = namesOnChangedLines(file, 'added', 2);
510
+ for (const { value, changeType } of diffValues(removed, added)) {
511
+ out.add('packageName', value, file.status === 'deleted' ? 'removed' : changeType, 'pattern');
512
+ }
513
+ }
514
+ // ─── MCP 도구 이름과 응답 필드 ───────────────────────────────
515
+ const RESPONSE_START_RE = /(?:JSON\.stringify\s*\(\s*|structuredContent\s*:\s*)\{/;
516
+ /** 응답 객체(JSON.stringify({...}), structuredContent: {...})의 최상위 키를 줄마다 뽑는다. */
517
+ function responseKeysPerLine(lines) {
518
+ const result = new Map();
519
+ const state = { depth: 0 };
520
+ for (const l of lines) {
521
+ let text = l.text;
522
+ if (state.depth === 0) {
523
+ const m = RESPONSE_START_RE.exec(text);
524
+ if (!m)
525
+ continue;
526
+ text = text.slice(m.index + m[0].length - 1);
527
+ }
528
+ const keys = [];
529
+ let segment = '';
530
+ let quote = null;
531
+ const flush = () => {
532
+ const k = /^\s*['"]?([A-Za-z_$][\w$]*)['"]?\s*(?::|$)/.exec(segment);
533
+ if (k && !segment.trim().startsWith('...'))
534
+ keys.push(k[1]);
535
+ segment = '';
536
+ };
537
+ for (const ch of text) {
538
+ if (quote) {
539
+ if (ch === quote)
540
+ quote = null;
541
+ if (state.depth === 1)
542
+ segment += ch;
543
+ continue;
544
+ }
545
+ if (ch === '"' || ch === "'" || ch === '`') {
546
+ quote = ch;
547
+ if (state.depth === 1)
548
+ segment += ch;
549
+ continue;
550
+ }
551
+ if (ch === '{' || ch === '[' || ch === '(') {
552
+ state.depth++;
553
+ if (state.depth === 1)
554
+ segment = '';
555
+ else if (state.depth === 2)
556
+ segment += ch;
557
+ continue;
558
+ }
559
+ if (ch === '}' || ch === ']' || ch === ')') {
560
+ if (state.depth === 1)
561
+ flush();
562
+ state.depth = Math.max(0, state.depth - 1);
563
+ if (state.depth === 0)
564
+ break;
565
+ continue;
566
+ }
567
+ if (state.depth === 1 && ch === ',') {
568
+ flush();
569
+ continue;
570
+ }
571
+ if (state.depth === 1)
572
+ segment += ch;
573
+ }
574
+ if (state.depth === 1)
575
+ flush();
576
+ if (keys.length)
577
+ result.set(l, keys);
578
+ }
579
+ return result;
580
+ }
581
+ function extractMcp(file, opts, out) {
582
+ const path = file.newPath ?? file.oldPath;
583
+ if (!path || !CODE_EXT_RE.test(path))
584
+ return;
585
+ const content = opts.readFile?.(path);
586
+ const diffText = file.hunks.flatMap((h) => h.lines.map((l) => l.text)).join('\n');
587
+ const fullText = `${content ?? ''}\n${diffText}`;
588
+ const isMcpFile = importsMcpSdk(fullText) || nearestPackageDependsOnSdk(path, opts.readFile);
589
+ if (!isMcpFile)
590
+ return;
591
+ const registry = isMcpToolRegistry(fullText);
592
+ const changeTypeOfFile = fileChangeType(file);
593
+ const namesOn = (type) => {
594
+ const names = [];
595
+ for (const h of file.hunks) {
596
+ h.lines.forEach((l, i) => {
597
+ if (l.type === type)
598
+ names.push(...toolNamesInLine(l.text, h.lines[i - 1]?.text, registry));
599
+ });
600
+ }
601
+ return names;
602
+ };
603
+ const removedTools = namesOn('removed');
604
+ const addedTools = namesOn('added');
605
+ const keysOn = (type) => {
606
+ const keys = [];
607
+ for (const h of file.hunks) {
608
+ const side = h.lines.filter((l) => l.type === 'context' || l.type === type);
609
+ for (const [line, ks] of responseKeysPerLine(side))
610
+ if (line.type === type)
611
+ keys.push(...ks);
612
+ }
613
+ return keys;
614
+ };
615
+ const removedKeys = keysOn('removed');
616
+ const addedKeys = keysOn('added');
617
+ for (const { value, changeType } of diffValues(removedTools, addedTools)) {
618
+ out.add('mcpToolName', value, file.status === 'deleted' ? 'removed' : changeType, 'pattern');
619
+ }
620
+ for (const { value, changeType } of diffValues(removedKeys, addedKeys)) {
621
+ out.add('responseField', value, file.status === 'deleted' ? 'removed' : changeType, 'pattern');
622
+ }
623
+ // 등록부 꼴이 바뀌었는데 패턴으로 이름을 못 잡았다. 파일 경로를 값으로 넘겨 LLM이 읽게 한다
624
+ const touched = [...linesOf(file, 'removed'), ...linesOf(file, 'added')];
625
+ const nothingCaught = removedTools.length + addedTools.length + removedKeys.length + addedKeys.length === 0;
626
+ if (nothingCaught && touched.some((l) => TOOLISH_LINE_RE.test(l.text))) {
627
+ out.add('mcpToolName', file.oldPath ?? path, changeTypeOfFile, 'llm');
628
+ }
629
+ }
630
+ /**
631
+ * diff에서 지워지거나 바뀐 식별자를 뽑는다. 역방향 검색이 이 값으로 관련 레포를 찾는다.
632
+ * 하네스 문서 PR이 아니어도 MCP 도구 정의나 패키지 이름이 바뀌면 그 식별자가 나온다.
633
+ * 패턴으로 도구 이름을 못 잡은 MCP 등록부 변경은 value에 파일 경로를 담고 extractedBy=llm으로 표시한다.
634
+ */
635
+ export function extractIdentifiers(diff, opts = {}) {
636
+ const files = parseUnifiedDiff(diff);
637
+ const out = new IdentifierSet();
638
+ for (const f of files) {
639
+ extractPathIds(f, opts, out);
640
+ extractHeadings(f, out);
641
+ extractFrontmatterNames(f, out);
642
+ extractPluginNames(f, out);
643
+ extractPackageNames(f, opts, out);
644
+ extractMcp(f, opts, out);
645
+ }
646
+ extractRuleIds(files, out);
647
+ return out.list();
648
+ }
649
+ /**
650
+ * git 레포의 두 ref 사이 diff에서 식별자를 뽑는다. base 트리 파일 목록으로 하나뿐인 파일 이름을 판정하고
651
+ * 파일 내용은 head에서 먼저 읽고 없으면 base에서 읽는다.
652
+ */
653
+ export function extractIdentifiersFromGit(repoRoot, baseRef, headRef) {
654
+ const git = (...args) => execFileSync('git', args, {
655
+ cwd: repoRoot,
656
+ encoding: 'utf-8',
657
+ maxBuffer: 64 * 1024 * 1024,
658
+ stdio: ['ignore', 'pipe', 'ignore'],
659
+ });
660
+ const show = (ref, p) => {
661
+ try {
662
+ return git('show', `${ref}:${p}`);
663
+ }
664
+ catch {
665
+ return undefined;
666
+ }
667
+ };
668
+ const diff = git('diff', '--no-color', '-M', `${baseRef}..${headRef}`);
669
+ const baseFiles = git('ls-tree', '-r', '--name-only', baseRef).split('\n').filter(Boolean);
670
+ const cache = new Map();
671
+ const readFile = (p) => {
672
+ if (!cache.has(p))
673
+ cache.set(p, show(headRef, p) ?? show(baseRef, p));
674
+ return cache.get(p);
675
+ };
676
+ return extractIdentifiers(diff, { baseFiles, readFile });
677
+ }
678
+ //# sourceMappingURL=identifiers.js.map