@comergehq/claude-plugin 0.1.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.
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "comergehq",
3
+ "owner": {
4
+ "name": "Comerge"
5
+ },
6
+ "metadata": {
7
+ "description": "Official Comerge plugins for Claude Code"
8
+ },
9
+ "plugins": [
10
+ {
11
+ "name": "comerge",
12
+ "description": "Comerge collaboration workflows for Claude Code",
13
+ "source": {
14
+ "source": "npm",
15
+ "package": "@comergehq/claude-plugin",
16
+ "version": "^0.1.0"
17
+ },
18
+ "homepage": "https://github.com/AbdelrahmanRizq97/comerge-claude-plugin",
19
+ "repository": "https://github.com/AbdelrahmanRizq97/comerge-claude-plugin",
20
+ "license": "MIT"
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "comerge",
3
+ "description": "Comerge collaboration workflows for Claude Code",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "Comerge"
7
+ },
8
+ "license": "MIT",
9
+ "repository": "https://github.com/AbdelrahmanRizq97/comerge-claude-plugin",
10
+ "skills": [
11
+ "./skills/safe-collab-workflow/SKILL.md",
12
+ "./skills/historical-memory-routing/SKILL.md",
13
+ "./skills/submit-change-step/SKILL.md",
14
+ "./skills/review-merge-request/SKILL.md",
15
+ "./skills/init-or-remix/SKILL.md",
16
+ "./skills/sync-and-reconcile/SKILL.md"
17
+ ],
18
+ "hooks": "./hooks/hooks.json",
19
+ "agents": [
20
+ "./agents/comerge-collab.md"
21
+ ],
22
+ "mcpServers": "./.mcp.json"
23
+ }
package/.mcp.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "mcpServers": {
3
+ "comerge": {
4
+ "command": "node",
5
+ "args": ["${CLAUDE_PLUGIN_ROOT}/dist/mcp-server.js"],
6
+ "cwd": "${CLAUDE_PLUGIN_ROOT}"
7
+ }
8
+ }
9
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Comerge
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,16 @@
1
+ ## @comergehq/claude-plugin
2
+
3
+ Claude Code plugin for Comerge collaboration workflows.
4
+
5
+ ### Install
6
+
7
+ ```bash
8
+ /plugin marketplace add AbdelrahmanRizq97/comerge-claude-plugin
9
+ /plugin install comerge@comergehq
10
+ ```
11
+
12
+ ### Usage
13
+
14
+ ```bash
15
+ /reload-plugins
16
+ ```
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: comerge-collab
3
+ description: Specialized Claude agent for Comerge repository collaboration, merge request review, sync, reconcile, and change-step recording workflows.
4
+ ---
5
+
6
+ You are the Comerge collaboration specialist.
7
+
8
+ Your job is to help Claude use Comerge as the primary collaboration workflow layer for bound repositories.
9
+
10
+ Operating rules:
11
+
12
+ 1. Start with `comerge_collab_status` for repo-bound collaboration tasks whenever the current state is unclear.
13
+ 2. In a Comerge-bound repo, treat Comerge MCP tools as the default workflow layer for collaboration state and historical reasoning. Raw git may still make sense for separate GitHub-only branch workflows or exact repository facts, but it is not the default way to answer why/history/failed-attempt questions.
14
+ 3. Use preview tools before apply tools whenever both exist.
15
+ 4. Treat reconcile as a last-resort, high-risk path.
16
+ 5. Prefer bounded merge request diffs first, then expand only when necessary.
17
+ 6. In a bound repo, use Comerge memory first for prompts about why something changed, prior prompts, failed attempts, decision trail, hidden assumptions, merge request backstory, reconcile history, or what context matters before modifying a subsystem.
18
+ 7. For historical reasoning, follow this order:
19
+ - use `comerge_collab_memory_summary` for the current high-level state,
20
+ - use `comerge_collab_memory_search` to find the most relevant historical items,
21
+ - use `comerge_collab_memory_timeline` when chronology matters,
22
+ - use `comerge_collab_memory_change_step_diff` only after you have identified a specific `changeStepId`.
23
+ 8. Use raw git for historical reads only as a fallback after Comerge memory has narrowed the relevant change, or when the user explicitly asks for exact commit, blame, ancestry, or raw patch details.
24
+ 9. Clearly explain local mutation risk before using tools that can modify the local repo.
25
+ 10. Assume the installed hook is the normal automatic recording path for completed assistant turns in a bound repo: changed turn => `comerge_collab_add`, no-diff turn => `comerge_collab_record_turn`.
26
+ 11. Do not proactively call `comerge_collab_add` or `comerge_collab_record_turn` during normal work. The hook should do the automatic per-turn recording.
27
+ 12. Only use manual `comerge_collab_add` or `comerge_collab_record_turn` when the user explicitly asks for it, or when doing operational recovery, backfills, or debugging.
28
+ 13. Do not duplicate core business logic in reasoning. Use the MCP tools to inspect and execute the workflow.
29
+
30
+ When appropriate:
31
+
32
+ - use `comerge_collab_init` to bind the current repo
33
+ - use `comerge_collab_remix` to start from an existing app lineage
34
+ - use memory summary/search/timeline tools before repo inspection when historical context or reasoning is needed
35
+ - use the explicit change-step diff tool only after you already know which `changeStepId` matters
36
+ - use `comerge_collab_add` only when a manual changed-turn recording step is explicitly needed
37
+ - use `comerge_collab_record_turn` only when a manual no-diff turn recording step is explicitly needed
38
+ - use merge request tools for review and approval flows
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/hook-state.ts
4
+ import fs from "fs/promises";
5
+ import os from "os";
6
+ import path from "path";
7
+ import { randomUUID } from "crypto";
8
+ function stateRoot() {
9
+ return path.join(os.tmpdir(), "comerge-claude-plugin-hooks");
10
+ }
11
+ function statePath(sessionId) {
12
+ return path.join(stateRoot(), `${sessionId}.json`);
13
+ }
14
+ async function writeJsonAtomic(filePath, value) {
15
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
16
+ const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
17
+ await fs.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
18
+ await fs.rename(tmpPath, filePath);
19
+ }
20
+ async function loadPendingTurnState(sessionId) {
21
+ const raw = await fs.readFile(statePath(sessionId), "utf8").catch(() => null);
22
+ if (!raw) return null;
23
+ try {
24
+ const parsed = JSON.parse(raw);
25
+ if (!parsed || typeof parsed !== "object") return null;
26
+ if (typeof parsed.sessionId !== "string" || typeof parsed.turnId !== "string" || typeof parsed.prompt !== "string") {
27
+ return null;
28
+ }
29
+ const intent = parsed.intent;
30
+ return {
31
+ sessionId: parsed.sessionId,
32
+ turnId: parsed.turnId,
33
+ prompt: parsed.prompt,
34
+ cwd: typeof parsed.cwd === "string" && parsed.cwd.trim() ? parsed.cwd : null,
35
+ intent: intent === "memory_first" || intent === "collab_state" || intent === "git_facts" ? intent : "neutral",
36
+ submittedAt: typeof parsed.submittedAt === "string" ? parsed.submittedAt : (/* @__PURE__ */ new Date()).toISOString(),
37
+ recordedByTool: Boolean(parsed.recordedByTool),
38
+ consultedMemory: Boolean(parsed.consultedMemory)
39
+ };
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+ async function savePendingTurnState(state) {
45
+ await writeJsonAtomic(statePath(state.sessionId), state);
46
+ }
47
+ async function markPendingTurnRecordedByTool(sessionId) {
48
+ const existing = await loadPendingTurnState(sessionId);
49
+ if (!existing) return;
50
+ existing.recordedByTool = true;
51
+ await savePendingTurnState(existing);
52
+ }
53
+ async function markPendingTurnConsultedMemory(sessionId) {
54
+ const existing = await loadPendingTurnState(sessionId);
55
+ if (!existing || existing.consultedMemory) return;
56
+ existing.consultedMemory = true;
57
+ await savePendingTurnState(existing);
58
+ }
59
+
60
+ // src/hook-utils.ts
61
+ import fs2 from "fs/promises";
62
+ import path2 from "path";
63
+ async function readJsonStdin() {
64
+ const chunks = [];
65
+ for await (const chunk of process.stdin) {
66
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
67
+ }
68
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
69
+ if (!raw) return {};
70
+ try {
71
+ const parsed = JSON.parse(raw);
72
+ return parsed && typeof parsed === "object" ? parsed : {};
73
+ } catch {
74
+ return {};
75
+ }
76
+ }
77
+ function extractString(input, keys) {
78
+ for (const key of keys) {
79
+ const value = input[key];
80
+ if (typeof value === "string" && value.trim()) {
81
+ return value.trim();
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+
87
+ // src/hook-post-collab.ts
88
+ function isRecordingToolName(toolName) {
89
+ return /comerge_collab_(add|record_turn)$/i.test(toolName);
90
+ }
91
+ function isMemoryToolName(toolName) {
92
+ return /comerge_collab_memory_(summary|search|timeline|change_step_diff)$/i.test(toolName);
93
+ }
94
+ async function main() {
95
+ const payload = await readJsonStdin();
96
+ const sessionId = extractString(payload, ["session_id"]);
97
+ const toolName = extractString(payload, ["tool_name"]);
98
+ if (!sessionId || !toolName) {
99
+ return;
100
+ }
101
+ if (isMemoryToolName(toolName)) {
102
+ await markPendingTurnConsultedMemory(sessionId);
103
+ }
104
+ if (isRecordingToolName(toolName)) {
105
+ await markPendingTurnRecordedByTool(sessionId);
106
+ }
107
+ }
108
+ main().catch((error) => {
109
+ const message = error instanceof Error ? error.message : String(error);
110
+ process.stderr.write(`${message}
111
+ `);
112
+ process.exitCode = 0;
113
+ });
114
+ //# sourceMappingURL=hook-post-collab.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hook-state.ts","../src/hook-utils.ts","../src/hook-post-collab.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { randomUUID } from \"node:crypto\";\n\nimport type { TurnIntent } from \"./history-routing.js\";\n\nexport type PendingTurnState = {\n sessionId: string;\n turnId: string;\n prompt: string;\n cwd: string | null;\n intent: TurnIntent;\n submittedAt: string;\n recordedByTool: boolean;\n consultedMemory: boolean;\n};\n\nfunction stateRoot(): string {\n return path.join(os.tmpdir(), \"comerge-claude-plugin-hooks\");\n}\n\nfunction statePath(sessionId: string): string {\n return path.join(stateRoot(), `${sessionId}.json`);\n}\n\nasync function writeJsonAtomic(filePath: string, value: unknown): Promise<void> {\n await fs.mkdir(path.dirname(filePath), { recursive: true });\n const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;\n await fs.writeFile(tmpPath, JSON.stringify(value, null, 2) + \"\\n\", \"utf8\");\n await fs.rename(tmpPath, filePath);\n}\n\nexport async function loadPendingTurnState(sessionId: string): Promise<PendingTurnState | null> {\n const raw = await fs.readFile(statePath(sessionId), \"utf8\").catch(() => null);\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw) as Partial<PendingTurnState>;\n if (!parsed || typeof parsed !== \"object\") return null;\n if (typeof parsed.sessionId !== \"string\" || typeof parsed.turnId !== \"string\" || typeof parsed.prompt !== \"string\") {\n return null;\n }\n\n const intent = parsed.intent;\n return {\n sessionId: parsed.sessionId,\n turnId: parsed.turnId,\n prompt: parsed.prompt,\n cwd: typeof parsed.cwd === \"string\" && parsed.cwd.trim() ? parsed.cwd : null,\n intent: intent === \"memory_first\" || intent === \"collab_state\" || intent === \"git_facts\" ? intent : \"neutral\",\n submittedAt: typeof parsed.submittedAt === \"string\" ? parsed.submittedAt : new Date().toISOString(),\n recordedByTool: Boolean(parsed.recordedByTool),\n consultedMemory: Boolean(parsed.consultedMemory),\n };\n } catch {\n return null;\n }\n}\n\nexport async function savePendingTurnState(state: PendingTurnState): Promise<void> {\n await writeJsonAtomic(statePath(state.sessionId), state);\n}\n\nexport async function createPendingTurnState(params: {\n sessionId: string;\n prompt: string;\n cwd?: string | null;\n intent: TurnIntent;\n}): Promise<PendingTurnState> {\n const state: PendingTurnState = {\n sessionId: params.sessionId,\n turnId: randomUUID(),\n prompt: params.prompt,\n cwd: params.cwd?.trim() || null,\n intent: params.intent,\n submittedAt: new Date().toISOString(),\n recordedByTool: false,\n consultedMemory: false,\n };\n await savePendingTurnState(state);\n return state;\n}\n\nexport async function markPendingTurnRecordedByTool(sessionId: string): Promise<void> {\n const existing = await loadPendingTurnState(sessionId);\n if (!existing) return;\n existing.recordedByTool = true;\n await savePendingTurnState(existing);\n}\n\nexport async function markPendingTurnConsultedMemory(sessionId: string): Promise<void> {\n const existing = await loadPendingTurnState(sessionId);\n if (!existing || existing.consultedMemory) return;\n existing.consultedMemory = true;\n await savePendingTurnState(existing);\n}\n\nexport async function clearPendingTurnState(sessionId: string): Promise<void> {\n await fs.rm(statePath(sessionId), { force: true }).catch(() => undefined);\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nexport async function readJsonStdin(): Promise<Record<string, unknown>> {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));\n }\n const raw = Buffer.concat(chunks).toString(\"utf8\").trim();\n if (!raw) return {};\n try {\n const parsed = JSON.parse(raw);\n return parsed && typeof parsed === \"object\" ? (parsed as Record<string, unknown>) : {};\n } catch {\n return {};\n }\n}\n\nfunction getNestedRecord(value: unknown): Record<string, unknown> | null {\n return value && typeof value === \"object\" ? (value as Record<string, unknown>) : null;\n}\n\nexport function extractToolInput(payload: Record<string, unknown>): Record<string, unknown> {\n return getNestedRecord(payload.tool_input) ?? getNestedRecord(payload.toolInput) ?? payload;\n}\n\nexport function extractString(input: Record<string, unknown>, keys: string[]): string | null {\n for (const key of keys) {\n const value = input[key];\n if (typeof value === \"string\" && value.trim()) {\n return value.trim();\n }\n }\n return null;\n}\n\nexport function extractBoolean(input: Record<string, unknown>, keys: string[]): boolean | null {\n for (const key of keys) {\n const value = input[key];\n if (typeof value === \"boolean\") {\n return value;\n }\n }\n return null;\n}\n\nexport async function findBoundRepo(startPath: string | null): Promise<string | null> {\n if (!startPath) return null;\n let current = path.resolve(startPath);\n let stats = await fs.stat(current).catch(() => null);\n if (stats?.isFile()) {\n current = path.dirname(current);\n }\n\n while (true) {\n const bindingPath = path.join(current, \".comerge\", \"config.json\");\n const bindingStats = await fs.stat(bindingPath).catch(() => null);\n if (bindingStats?.isFile()) return current;\n const parent = path.dirname(current);\n if (parent === current) return null;\n current = parent;\n }\n}\n","import { markPendingTurnConsultedMemory, markPendingTurnRecordedByTool } from \"./hook-state.js\";\nimport { extractString, readJsonStdin } from \"./hook-utils.js\";\n\nfunction isRecordingToolName(toolName: string): boolean {\n return /comerge_collab_(add|record_turn)$/i.test(toolName);\n}\n\nfunction isMemoryToolName(toolName: string): boolean {\n return /comerge_collab_memory_(summary|search|timeline|change_step_diff)$/i.test(toolName);\n}\n\nasync function main(): Promise<void> {\n const payload = await readJsonStdin();\n const sessionId = extractString(payload, [\"session_id\"]);\n const toolName = extractString(payload, [\"tool_name\"]);\n if (!sessionId || !toolName) {\n return;\n }\n\n if (isMemoryToolName(toolName)) {\n await markPendingTurnConsultedMemory(sessionId);\n }\n\n if (isRecordingToolName(toolName)) {\n await markPendingTurnRecordedByTool(sessionId);\n }\n}\n\nmain().catch((error) => {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`${message}\\n`);\n process.exitCode = 0;\n});\n"],"mappings":"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","names":["fs","path"]}
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/history-routing.ts
4
+ var STRONG_MEMORY_FIRST_PATTERNS = [
5
+ /\bwhy\b/i,
6
+ /\breason(?:ing)?\b/i,
7
+ /\brationale\b/i,
8
+ /\bintent\b/i,
9
+ /\bdecision(?: trail)?\b/i,
10
+ /\bhidden assumptions?\b/i,
11
+ /\bwhat led to\b/i,
12
+ /\btrying to solve\b/i,
13
+ /\bearlier prompts?\b/i,
14
+ /\brequirements?\b/i,
15
+ /\btemporary patch\b/i,
16
+ /\bworkaround\b/i,
17
+ /\blong[-\s]?term design\b/i,
18
+ /\bfailed attempts?\b/i,
19
+ /\btried before\b/i,
20
+ /\bprevious attempts?\b/i,
21
+ /\babandon(?:ed)?\b/i,
22
+ /\broll(?:ed)? back\b/i,
23
+ /\bregressions?\b/i,
24
+ /\berrors?\b.*\bkept happening\b/i,
25
+ /\bbefore i (?:touch|change|modify|refactor)\b/i,
26
+ /\bmerge request discussions?\b/i,
27
+ /\brecovery\b/i,
28
+ /\bdrift\b/i,
29
+ /\bcontext did the agent have\b/i,
30
+ /\buser (?:ask|request|approval)\b/i
31
+ ];
32
+ var MEMORY_FIRST_PATTERNS = [
33
+ /\brecent changes?\b/i,
34
+ /\bwhat led to\b/i,
35
+ /\bproblem\b/i,
36
+ /\bchange step\b/i,
37
+ /\bhistorical\b/i,
38
+ /\bhistory\b/i,
39
+ ...STRONG_MEMORY_FIRST_PATTERNS
40
+ ];
41
+ function shouldPreferComergeMemory(intent) {
42
+ return intent === "memory_first";
43
+ }
44
+
45
+ // src/hook-state.ts
46
+ import fs from "fs/promises";
47
+ import os from "os";
48
+ import path from "path";
49
+ import { randomUUID } from "crypto";
50
+ function stateRoot() {
51
+ return path.join(os.tmpdir(), "comerge-claude-plugin-hooks");
52
+ }
53
+ function statePath(sessionId) {
54
+ return path.join(stateRoot(), `${sessionId}.json`);
55
+ }
56
+ async function loadPendingTurnState(sessionId) {
57
+ const raw = await fs.readFile(statePath(sessionId), "utf8").catch(() => null);
58
+ if (!raw) return null;
59
+ try {
60
+ const parsed = JSON.parse(raw);
61
+ if (!parsed || typeof parsed !== "object") return null;
62
+ if (typeof parsed.sessionId !== "string" || typeof parsed.turnId !== "string" || typeof parsed.prompt !== "string") {
63
+ return null;
64
+ }
65
+ const intent = parsed.intent;
66
+ return {
67
+ sessionId: parsed.sessionId,
68
+ turnId: parsed.turnId,
69
+ prompt: parsed.prompt,
70
+ cwd: typeof parsed.cwd === "string" && parsed.cwd.trim() ? parsed.cwd : null,
71
+ intent: intent === "memory_first" || intent === "collab_state" || intent === "git_facts" ? intent : "neutral",
72
+ submittedAt: typeof parsed.submittedAt === "string" ? parsed.submittedAt : (/* @__PURE__ */ new Date()).toISOString(),
73
+ recordedByTool: Boolean(parsed.recordedByTool),
74
+ consultedMemory: Boolean(parsed.consultedMemory)
75
+ };
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ // src/hook-utils.ts
82
+ import fs2 from "fs/promises";
83
+ import path2 from "path";
84
+ async function readJsonStdin() {
85
+ const chunks = [];
86
+ for await (const chunk of process.stdin) {
87
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
88
+ }
89
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
90
+ if (!raw) return {};
91
+ try {
92
+ const parsed = JSON.parse(raw);
93
+ return parsed && typeof parsed === "object" ? parsed : {};
94
+ } catch {
95
+ return {};
96
+ }
97
+ }
98
+ function getNestedRecord(value) {
99
+ return value && typeof value === "object" ? value : null;
100
+ }
101
+ function extractToolInput(payload) {
102
+ return getNestedRecord(payload.tool_input) ?? getNestedRecord(payload.toolInput) ?? payload;
103
+ }
104
+ function extractString(input, keys) {
105
+ for (const key of keys) {
106
+ const value = input[key];
107
+ if (typeof value === "string" && value.trim()) {
108
+ return value.trim();
109
+ }
110
+ }
111
+ return null;
112
+ }
113
+ async function findBoundRepo(startPath) {
114
+ if (!startPath) return null;
115
+ let current = path2.resolve(startPath);
116
+ let stats = await fs2.stat(current).catch(() => null);
117
+ if (stats?.isFile()) {
118
+ current = path2.dirname(current);
119
+ }
120
+ while (true) {
121
+ const bindingPath = path2.join(current, ".comerge", "config.json");
122
+ const bindingStats = await fs2.stat(bindingPath).catch(() => null);
123
+ if (bindingStats?.isFile()) return current;
124
+ const parent = path2.dirname(current);
125
+ if (parent === current) return null;
126
+ current = parent;
127
+ }
128
+ }
129
+
130
+ // src/hook-pre-git.ts
131
+ var GIT_ADVISORIES = [
132
+ {
133
+ subcommand: "commit",
134
+ re: /\bgit\b(?:\s+-[^\s]+)*\s+commit\b/i,
135
+ category: "mutation",
136
+ message: "This repository is bound to Comerge. For Comerge collaboration, do not use raw `git commit` as the normal way to record work. In this plugin setup, changed turns are normally recorded automatically at the end of the assistant response with `comerge_collab_add`, which creates the next commit on the Comerge side from the submitted diff and then syncs the local repo to that commit when auto-sync succeeds. Before running Comerge mutating tools, prefer the checkout's configured preferred branch; branch-mismatch overrides should be exceptional."
137
+ },
138
+ {
139
+ subcommand: "push",
140
+ re: /\bgit\b(?:\s+-[^\s]+)*\s+push\b/i,
141
+ category: "mutation",
142
+ message: "This repository is bound to Comerge. Raw `git push` can still make sense for GitHub-only branch workflows, but it is not the normal way to publish Comerge collaboration state. Changed turns are normally recorded with `comerge_collab_add`, while no-diff turns are recorded with `comerge_collab_record_turn`. Before mutating Comerge state, prefer the checkout's configured preferred branch; branch-mismatch overrides should be exceptional."
143
+ },
144
+ {
145
+ subcommand: "pull",
146
+ re: /\bgit\b(?:\s+-[^\s]+)*\s+pull\b/i,
147
+ category: "mutation",
148
+ message: "This repository is bound to Comerge. Raw `git pull` can still make sense for GitHub-only changes, but do not use it to pull Comerge state. Run `comerge_collab_status` first, then use `comerge_collab_sync_preview` and `comerge_collab_sync_apply` for Comerge sync, `comerge_collab_reconcile_preview` and `comerge_collab_reconcile_apply` when local history diverged, or `comerge_collab_sync_upstream` to bring upstream changes into a remix. Prefer the checkout's configured preferred branch before applying Comerge mutations."
149
+ },
150
+ {
151
+ subcommand: "merge",
152
+ re: /\bgit\b(?:\s+-[^\s]+)*\s+merge\b/i,
153
+ category: "mutation",
154
+ message: "This repository is bound to Comerge. Raw `git merge` only makes sense for GitHub-style branch workflows. It is not the normal way to merge Comerge work, because Comerge merges happen through merge requests from remix app to upstream app. Use the Comerge merge-request tools for that flow, ideally from the checkout's configured preferred branch."
155
+ },
156
+ {
157
+ subcommand: "rebase",
158
+ re: /\bgit\b(?:\s+-[^\s]+)*\s+rebase\b/i,
159
+ category: "mutation",
160
+ message: "This repository is bound to Comerge. Raw `git rebase` rewrites local history, so it is not the normal way to realign Comerge state. Run `comerge_collab_status` first, then use `comerge_collab_sync_preview` or `comerge_collab_reconcile_preview` depending on whether Comerge can fast-forward or requires reconcile. Prefer the checkout's configured preferred branch before mutating Comerge state."
161
+ },
162
+ {
163
+ subcommand: "reset",
164
+ re: /\bgit\b(?:\s+-[^\s]+)*\s+reset\b/i,
165
+ category: "mutation",
166
+ message: "This repository is bound to Comerge. Raw `git reset` can discard work or rewrite local state, so it is not the normal way to realign Comerge collaboration state. Run `comerge_collab_status` first, then use Comerge sync or reconcile flows when the goal is to realign repository state. Prefer the checkout's configured preferred branch before mutating Comerge state."
167
+ },
168
+ {
169
+ subcommand: "log",
170
+ re: /\bgit\b(?:\s+-[^\s]+)*\s+log\b/i,
171
+ category: "history_read",
172
+ message: "This repository is bound to Comerge. Raw `git log` is a fallback for exact commit history, not the default starting point for historical reasoning. Use Comerge memory first when the goal is to understand intent, prior prompts, failed attempts, or decision trail context."
173
+ },
174
+ {
175
+ subcommand: "show",
176
+ re: /\bgit\b(?:\s+-[^\s]+)*\s+show\b/i,
177
+ category: "history_read",
178
+ message: "This repository is bound to Comerge. Raw `git show` is useful for exact patch inspection, but historical reasoning should start from Comerge memory so you can identify the right change step or merge context before expanding into raw diffs."
179
+ },
180
+ {
181
+ subcommand: "blame",
182
+ re: /\bgit\b(?:\s+-[^\s]+)*\s+blame\b/i,
183
+ category: "history_read",
184
+ message: "This repository is bound to Comerge. Raw `git blame` can identify line ownership, but it does not explain the user ask, reasoning, or failed attempts behind a change. Use Comerge memory first when you need historical intent."
185
+ },
186
+ {
187
+ subcommand: "diff",
188
+ re: /\bgit\b(?:\s+-[^\s]+)*\s+diff\b/i,
189
+ category: "history_read",
190
+ message: "This repository is bound to Comerge. Raw `git diff` is useful for exact patch detail, but Comerge memory should be the default first read for understanding why a change happened or which historical step matters before inspecting raw diffs."
191
+ },
192
+ {
193
+ subcommand: "rev-list",
194
+ re: /\bgit\b(?:\s+-[^\s]+)*\s+rev-list\b/i,
195
+ category: "history_read",
196
+ message: "This repository is bound to Comerge. Raw `git rev-list` is commit-level history detail; use Comerge memory first when the question is about reasoning, chronology, or related historical attempts rather than exact ancestry mechanics."
197
+ }
198
+ ];
199
+ function getGitAdvisory(command) {
200
+ for (const advisory of GIT_ADVISORIES) {
201
+ if (advisory.re.test(command)) {
202
+ return {
203
+ subcommand: advisory.subcommand,
204
+ category: advisory.category,
205
+ message: advisory.message
206
+ };
207
+ }
208
+ }
209
+ return null;
210
+ }
211
+ function buildMemoryFirstMessage(state) {
212
+ if (!state) {
213
+ return "Use `comerge_collab_memory_summary`, `comerge_collab_memory_search`, or `comerge_collab_memory_timeline` first, then fall back to raw git only if you still need exact commit or patch details.";
214
+ }
215
+ if (state.intent === "git_facts") {
216
+ return null;
217
+ }
218
+ if (state.intent === "collab_state") {
219
+ return "This turn looks like Comerge collaboration-state work. Start with `comerge_collab_status`, then use memory reads if you need related historical context before raw git history.";
220
+ }
221
+ if (shouldPreferComergeMemory(state.intent) && !state.consultedMemory) {
222
+ return "This turn is classified as a Comerge memory-first history question, and no memory tool has been used yet. Start with `comerge_collab_memory_summary`, `comerge_collab_memory_search`, or `comerge_collab_memory_timeline`. Only use `comerge_collab_memory_change_step_diff` after identifying the relevant `changeStepId`, and use raw git after that only if exact repository facts are still needed.";
223
+ }
224
+ if (!state.consultedMemory) {
225
+ return "Prefer Comerge memory before raw git history in a bound repo: start with `comerge_collab_memory_summary`, `comerge_collab_memory_search`, or `comerge_collab_memory_timeline`, then use raw git only if you still need exact repository facts.";
226
+ }
227
+ return null;
228
+ }
229
+ async function main() {
230
+ const payload = await readJsonStdin();
231
+ const toolInput = extractToolInput(payload);
232
+ const command = extractString(toolInput, ["command", "cmd", "bash_command"]);
233
+ if (!command) {
234
+ return;
235
+ }
236
+ const advisory = getGitAdvisory(command);
237
+ if (!advisory) {
238
+ return;
239
+ }
240
+ const cwd = extractString(payload, ["cwd"]) ?? extractString(toolInput, ["cwd"]) ?? process.cwd();
241
+ const boundRepo = await findBoundRepo(cwd);
242
+ if (!boundRepo) {
243
+ return;
244
+ }
245
+ const sessionId = extractString(payload, ["session_id"]);
246
+ const turnState = sessionId ? await loadPendingTurnState(sessionId) : null;
247
+ if (advisory.category === "history_read" && turnState?.intent === "git_facts") {
248
+ return;
249
+ }
250
+ const memoryFirstMessage = advisory.category === "history_read" ? buildMemoryFirstMessage(turnState) : null;
251
+ if (advisory.category === "history_read" && !memoryFirstMessage) {
252
+ return;
253
+ }
254
+ process.stdout.write(
255
+ [
256
+ "Comerge advisory:",
257
+ `Detected raw git ${advisory.subcommand} usage in a repo bound to Comerge: ${command}`,
258
+ advisory.message,
259
+ ...memoryFirstMessage ? [memoryFirstMessage] : []
260
+ ].join("\n")
261
+ );
262
+ }
263
+ main().catch((error) => {
264
+ const message = error instanceof Error ? error.message : String(error);
265
+ process.stderr.write(`${message}
266
+ `);
267
+ process.exitCode = 0;
268
+ });
269
+ //# sourceMappingURL=hook-pre-git.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/history-routing.ts","../src/hook-state.ts","../src/hook-utils.ts","../src/hook-pre-git.ts"],"sourcesContent":["export type TurnIntent = \"memory_first\" | \"collab_state\" | \"git_facts\" | \"neutral\";\n\nconst STRONG_MEMORY_FIRST_PATTERNS: RegExp[] = [\n /\\bwhy\\b/i,\n /\\breason(?:ing)?\\b/i,\n /\\brationale\\b/i,\n /\\bintent\\b/i,\n /\\bdecision(?: trail)?\\b/i,\n /\\bhidden assumptions?\\b/i,\n /\\bwhat led to\\b/i,\n /\\btrying to solve\\b/i,\n /\\bearlier prompts?\\b/i,\n /\\brequirements?\\b/i,\n /\\btemporary patch\\b/i,\n /\\bworkaround\\b/i,\n /\\blong[-\\s]?term design\\b/i,\n /\\bfailed attempts?\\b/i,\n /\\btried before\\b/i,\n /\\bprevious attempts?\\b/i,\n /\\babandon(?:ed)?\\b/i,\n /\\broll(?:ed)? back\\b/i,\n /\\bregressions?\\b/i,\n /\\berrors?\\b.*\\bkept happening\\b/i,\n /\\bbefore i (?:touch|change|modify|refactor)\\b/i,\n /\\bmerge request discussions?\\b/i,\n /\\brecovery\\b/i,\n /\\bdrift\\b/i,\n /\\bcontext did the agent have\\b/i,\n /\\buser (?:ask|request|approval)\\b/i,\n];\n\nconst MEMORY_FIRST_PATTERNS: RegExp[] = [\n /\\brecent changes?\\b/i,\n /\\bwhat led to\\b/i,\n /\\bproblem\\b/i,\n /\\bchange step\\b/i,\n /\\bhistorical\\b/i,\n /\\bhistory\\b/i,\n ...STRONG_MEMORY_FIRST_PATTERNS,\n];\n\nconst COLLAB_STATE_PATTERNS: RegExp[] = [\n /\\bcollab status\\b/i,\n /\\bsync\\b/i,\n /\\breconcile\\b/i,\n /\\bmerge request\\b/i,\n /\\brequest merge\\b/i,\n /\\breview\\b/i,\n /\\bbind(?:ing)?\\b/i,\n /\\bremix\\b/i,\n /\\bupstream\\b/i,\n];\n\nconst GIT_FACT_PATTERNS: RegExp[] = [\n /\\bgit (?:log|show|diff|blame|rev-list|whatchanged)\\b/i,\n /\\bcommit hash(?:es)?\\b/i,\n /\\bexact commits?\\b/i,\n /\\braw git\\b/i,\n /\\bgit history\\b/i,\n /\\bblame this\\b/i,\n /\\bwho changed (?:this line|this file|that line)\\b/i,\n /\\bbranch ancestr(?:y|ies)\\b/i,\n /\\bpatch[-\\s]?level\\b/i,\n];\n\nfunction hasMatch(prompt: string, patterns: RegExp[]): boolean {\n return patterns.some((pattern) => pattern.test(prompt));\n}\n\nexport function classifyTurnIntent(prompt: string): TurnIntent {\n const normalizedPrompt = prompt.trim();\n if (!normalizedPrompt) {\n return \"neutral\";\n }\n\n const hasStrongMemorySignals = hasMatch(normalizedPrompt, STRONG_MEMORY_FIRST_PATTERNS);\n const hasMemorySignals = hasMatch(normalizedPrompt, MEMORY_FIRST_PATTERNS);\n const hasGitFactSignals = hasMatch(normalizedPrompt, GIT_FACT_PATTERNS);\n\n if (hasGitFactSignals && !hasStrongMemorySignals) {\n return \"git_facts\";\n }\n\n if (hasMemorySignals) {\n return \"memory_first\";\n }\n\n if (hasMatch(normalizedPrompt, COLLAB_STATE_PATTERNS)) {\n return \"collab_state\";\n }\n\n if (hasMatch(normalizedPrompt, GIT_FACT_PATTERNS)) {\n return \"git_facts\";\n }\n\n return \"neutral\";\n}\n\nexport function shouldPreferComergeMemory(intent: TurnIntent): boolean {\n return intent === \"memory_first\";\n}\n\nexport function buildPromptRoutingAdvisory(intent: TurnIntent): string | null {\n if (intent === \"memory_first\") {\n return [\n \"Comerge advisory:\",\n \"This prompt looks like a historical reasoning request in a repo bound to Comerge.\",\n \"Start with `comerge_collab_memory_summary`, `comerge_collab_memory_search`, or `comerge_collab_memory_timeline` before raw git history. Only fetch `comerge_collab_memory_change_step_diff` after identifying a relevant `changeStepId`.\",\n ].join(\"\\n\");\n }\n\n if (intent === \"collab_state\") {\n return [\n \"Comerge advisory:\",\n \"This prompt looks like a repo collaboration-state request in a repo bound to Comerge.\",\n \"Start with `comerge_collab_status`, then follow the recommended sync, reconcile, merge-request, or memory reads from there.\",\n ].join(\"\\n\");\n }\n\n return null;\n}\n","import fs from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { randomUUID } from \"node:crypto\";\n\nimport type { TurnIntent } from \"./history-routing.js\";\n\nexport type PendingTurnState = {\n sessionId: string;\n turnId: string;\n prompt: string;\n cwd: string | null;\n intent: TurnIntent;\n submittedAt: string;\n recordedByTool: boolean;\n consultedMemory: boolean;\n};\n\nfunction stateRoot(): string {\n return path.join(os.tmpdir(), \"comerge-claude-plugin-hooks\");\n}\n\nfunction statePath(sessionId: string): string {\n return path.join(stateRoot(), `${sessionId}.json`);\n}\n\nasync function writeJsonAtomic(filePath: string, value: unknown): Promise<void> {\n await fs.mkdir(path.dirname(filePath), { recursive: true });\n const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;\n await fs.writeFile(tmpPath, JSON.stringify(value, null, 2) + \"\\n\", \"utf8\");\n await fs.rename(tmpPath, filePath);\n}\n\nexport async function loadPendingTurnState(sessionId: string): Promise<PendingTurnState | null> {\n const raw = await fs.readFile(statePath(sessionId), \"utf8\").catch(() => null);\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw) as Partial<PendingTurnState>;\n if (!parsed || typeof parsed !== \"object\") return null;\n if (typeof parsed.sessionId !== \"string\" || typeof parsed.turnId !== \"string\" || typeof parsed.prompt !== \"string\") {\n return null;\n }\n\n const intent = parsed.intent;\n return {\n sessionId: parsed.sessionId,\n turnId: parsed.turnId,\n prompt: parsed.prompt,\n cwd: typeof parsed.cwd === \"string\" && parsed.cwd.trim() ? parsed.cwd : null,\n intent: intent === \"memory_first\" || intent === \"collab_state\" || intent === \"git_facts\" ? intent : \"neutral\",\n submittedAt: typeof parsed.submittedAt === \"string\" ? parsed.submittedAt : new Date().toISOString(),\n recordedByTool: Boolean(parsed.recordedByTool),\n consultedMemory: Boolean(parsed.consultedMemory),\n };\n } catch {\n return null;\n }\n}\n\nexport async function savePendingTurnState(state: PendingTurnState): Promise<void> {\n await writeJsonAtomic(statePath(state.sessionId), state);\n}\n\nexport async function createPendingTurnState(params: {\n sessionId: string;\n prompt: string;\n cwd?: string | null;\n intent: TurnIntent;\n}): Promise<PendingTurnState> {\n const state: PendingTurnState = {\n sessionId: params.sessionId,\n turnId: randomUUID(),\n prompt: params.prompt,\n cwd: params.cwd?.trim() || null,\n intent: params.intent,\n submittedAt: new Date().toISOString(),\n recordedByTool: false,\n consultedMemory: false,\n };\n await savePendingTurnState(state);\n return state;\n}\n\nexport async function markPendingTurnRecordedByTool(sessionId: string): Promise<void> {\n const existing = await loadPendingTurnState(sessionId);\n if (!existing) return;\n existing.recordedByTool = true;\n await savePendingTurnState(existing);\n}\n\nexport async function markPendingTurnConsultedMemory(sessionId: string): Promise<void> {\n const existing = await loadPendingTurnState(sessionId);\n if (!existing || existing.consultedMemory) return;\n existing.consultedMemory = true;\n await savePendingTurnState(existing);\n}\n\nexport async function clearPendingTurnState(sessionId: string): Promise<void> {\n await fs.rm(statePath(sessionId), { force: true }).catch(() => undefined);\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nexport async function readJsonStdin(): Promise<Record<string, unknown>> {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));\n }\n const raw = Buffer.concat(chunks).toString(\"utf8\").trim();\n if (!raw) return {};\n try {\n const parsed = JSON.parse(raw);\n return parsed && typeof parsed === \"object\" ? (parsed as Record<string, unknown>) : {};\n } catch {\n return {};\n }\n}\n\nfunction getNestedRecord(value: unknown): Record<string, unknown> | null {\n return value && typeof value === \"object\" ? (value as Record<string, unknown>) : null;\n}\n\nexport function extractToolInput(payload: Record<string, unknown>): Record<string, unknown> {\n return getNestedRecord(payload.tool_input) ?? getNestedRecord(payload.toolInput) ?? payload;\n}\n\nexport function extractString(input: Record<string, unknown>, keys: string[]): string | null {\n for (const key of keys) {\n const value = input[key];\n if (typeof value === \"string\" && value.trim()) {\n return value.trim();\n }\n }\n return null;\n}\n\nexport function extractBoolean(input: Record<string, unknown>, keys: string[]): boolean | null {\n for (const key of keys) {\n const value = input[key];\n if (typeof value === \"boolean\") {\n return value;\n }\n }\n return null;\n}\n\nexport async function findBoundRepo(startPath: string | null): Promise<string | null> {\n if (!startPath) return null;\n let current = path.resolve(startPath);\n let stats = await fs.stat(current).catch(() => null);\n if (stats?.isFile()) {\n current = path.dirname(current);\n }\n\n while (true) {\n const bindingPath = path.join(current, \".comerge\", \"config.json\");\n const bindingStats = await fs.stat(bindingPath).catch(() => null);\n if (bindingStats?.isFile()) return current;\n const parent = path.dirname(current);\n if (parent === current) return null;\n current = parent;\n }\n}\n","import { shouldPreferComergeMemory } from \"./history-routing.js\";\nimport { loadPendingTurnState } from \"./hook-state.js\";\nimport { extractString, extractToolInput, findBoundRepo, readJsonStdin } from \"./hook-utils.js\";\n\ntype GitAdvisory = {\n subcommand: string;\n category: \"history_read\" | \"mutation\";\n message: string;\n};\n\nconst GIT_ADVISORIES: Array<{ subcommand: string; re: RegExp; category: \"history_read\" | \"mutation\"; message: string }> = [\n {\n subcommand: \"commit\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+commit\\b/i,\n category: \"mutation\",\n message:\n \"This repository is bound to Comerge. For Comerge collaboration, do not use raw `git commit` as the normal way to record work. In this plugin setup, changed turns are normally recorded automatically at the end of the assistant response with `comerge_collab_add`, which creates the next commit on the Comerge side from the submitted diff and then syncs the local repo to that commit when auto-sync succeeds. Before running Comerge mutating tools, prefer the checkout's configured preferred branch; branch-mismatch overrides should be exceptional.\",\n },\n {\n subcommand: \"push\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+push\\b/i,\n category: \"mutation\",\n message:\n \"This repository is bound to Comerge. Raw `git push` can still make sense for GitHub-only branch workflows, but it is not the normal way to publish Comerge collaboration state. Changed turns are normally recorded with `comerge_collab_add`, while no-diff turns are recorded with `comerge_collab_record_turn`. Before mutating Comerge state, prefer the checkout's configured preferred branch; branch-mismatch overrides should be exceptional.\",\n },\n {\n subcommand: \"pull\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+pull\\b/i,\n category: \"mutation\",\n message:\n \"This repository is bound to Comerge. Raw `git pull` can still make sense for GitHub-only changes, but do not use it to pull Comerge state. Run `comerge_collab_status` first, then use `comerge_collab_sync_preview` and `comerge_collab_sync_apply` for Comerge sync, `comerge_collab_reconcile_preview` and `comerge_collab_reconcile_apply` when local history diverged, or `comerge_collab_sync_upstream` to bring upstream changes into a remix. Prefer the checkout's configured preferred branch before applying Comerge mutations.\",\n },\n {\n subcommand: \"merge\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+merge\\b/i,\n category: \"mutation\",\n message:\n \"This repository is bound to Comerge. Raw `git merge` only makes sense for GitHub-style branch workflows. It is not the normal way to merge Comerge work, because Comerge merges happen through merge requests from remix app to upstream app. Use the Comerge merge-request tools for that flow, ideally from the checkout's configured preferred branch.\",\n },\n {\n subcommand: \"rebase\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+rebase\\b/i,\n category: \"mutation\",\n message:\n \"This repository is bound to Comerge. Raw `git rebase` rewrites local history, so it is not the normal way to realign Comerge state. Run `comerge_collab_status` first, then use `comerge_collab_sync_preview` or `comerge_collab_reconcile_preview` depending on whether Comerge can fast-forward or requires reconcile. Prefer the checkout's configured preferred branch before mutating Comerge state.\",\n },\n {\n subcommand: \"reset\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+reset\\b/i,\n category: \"mutation\",\n message:\n \"This repository is bound to Comerge. Raw `git reset` can discard work or rewrite local state, so it is not the normal way to realign Comerge collaboration state. Run `comerge_collab_status` first, then use Comerge sync or reconcile flows when the goal is to realign repository state. Prefer the checkout's configured preferred branch before mutating Comerge state.\",\n },\n {\n subcommand: \"log\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+log\\b/i,\n category: \"history_read\",\n message:\n \"This repository is bound to Comerge. Raw `git log` is a fallback for exact commit history, not the default starting point for historical reasoning. Use Comerge memory first when the goal is to understand intent, prior prompts, failed attempts, or decision trail context.\",\n },\n {\n subcommand: \"show\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+show\\b/i,\n category: \"history_read\",\n message:\n \"This repository is bound to Comerge. Raw `git show` is useful for exact patch inspection, but historical reasoning should start from Comerge memory so you can identify the right change step or merge context before expanding into raw diffs.\",\n },\n {\n subcommand: \"blame\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+blame\\b/i,\n category: \"history_read\",\n message:\n \"This repository is bound to Comerge. Raw `git blame` can identify line ownership, but it does not explain the user ask, reasoning, or failed attempts behind a change. Use Comerge memory first when you need historical intent.\",\n },\n {\n subcommand: \"diff\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+diff\\b/i,\n category: \"history_read\",\n message:\n \"This repository is bound to Comerge. Raw `git diff` is useful for exact patch detail, but Comerge memory should be the default first read for understanding why a change happened or which historical step matters before inspecting raw diffs.\",\n },\n {\n subcommand: \"rev-list\",\n re: /\\bgit\\b(?:\\s+-[^\\s]+)*\\s+rev-list\\b/i,\n category: \"history_read\",\n message:\n \"This repository is bound to Comerge. Raw `git rev-list` is commit-level history detail; use Comerge memory first when the question is about reasoning, chronology, or related historical attempts rather than exact ancestry mechanics.\",\n },\n];\n\nfunction getGitAdvisory(command: string): GitAdvisory | null {\n for (const advisory of GIT_ADVISORIES) {\n if (advisory.re.test(command)) {\n return {\n subcommand: advisory.subcommand,\n category: advisory.category,\n message: advisory.message,\n };\n }\n }\n return null;\n}\n\nfunction buildMemoryFirstMessage(state: Awaited<ReturnType<typeof loadPendingTurnState>>): string | null {\n if (!state) {\n return \"Use `comerge_collab_memory_summary`, `comerge_collab_memory_search`, or `comerge_collab_memory_timeline` first, then fall back to raw git only if you still need exact commit or patch details.\";\n }\n\n if (state.intent === \"git_facts\") {\n return null;\n }\n\n if (state.intent === \"collab_state\") {\n return \"This turn looks like Comerge collaboration-state work. Start with `comerge_collab_status`, then use memory reads if you need related historical context before raw git history.\";\n }\n\n if (shouldPreferComergeMemory(state.intent) && !state.consultedMemory) {\n return \"This turn is classified as a Comerge memory-first history question, and no memory tool has been used yet. Start with `comerge_collab_memory_summary`, `comerge_collab_memory_search`, or `comerge_collab_memory_timeline`. Only use `comerge_collab_memory_change_step_diff` after identifying the relevant `changeStepId`, and use raw git after that only if exact repository facts are still needed.\";\n }\n\n if (!state.consultedMemory) {\n return \"Prefer Comerge memory before raw git history in a bound repo: start with `comerge_collab_memory_summary`, `comerge_collab_memory_search`, or `comerge_collab_memory_timeline`, then use raw git only if you still need exact repository facts.\";\n }\n\n return null;\n}\n\nasync function main(): Promise<void> {\n const payload = await readJsonStdin();\n const toolInput = extractToolInput(payload);\n\n const command = extractString(toolInput, [\"command\", \"cmd\", \"bash_command\"]);\n if (!command) {\n return;\n }\n\n const advisory = getGitAdvisory(command);\n if (!advisory) {\n return;\n }\n\n const cwd = extractString(payload, [\"cwd\"]) ?? extractString(toolInput, [\"cwd\"]) ?? process.cwd();\n const boundRepo = await findBoundRepo(cwd);\n if (!boundRepo) {\n return;\n }\n\n const sessionId = extractString(payload, [\"session_id\"]);\n const turnState = sessionId ? await loadPendingTurnState(sessionId) : null;\n if (advisory.category === \"history_read\" && turnState?.intent === \"git_facts\") {\n return;\n }\n\n const memoryFirstMessage = advisory.category === \"history_read\" ? buildMemoryFirstMessage(turnState) : null;\n if (advisory.category === \"history_read\" && !memoryFirstMessage) {\n return;\n }\n\n process.stdout.write(\n [\n \"Comerge advisory:\",\n `Detected raw git ${advisory.subcommand} usage in a repo bound to Comerge: ${command}`,\n advisory.message,\n ...(memoryFirstMessage ? [memoryFirstMessage] : []),\n ].join(\"\\n\"),\n );\n}\n\nmain().catch((error) => {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`${message}\\n`);\n process.exitCode = 0;\n});\n"],"mappings":"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","names":["fs","path","path","fs"]}
@@ -0,0 +1,2 @@
1
+
2
+ export { }