@remixhq/claude-plugin 0.1.2 → 0.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/agents/remix-collab.md +8 -6
- package/dist/hook-post-collab.js +394 -19
- package/dist/hook-post-collab.js.map +1 -1
- package/dist/hook-pre-git.js +87 -19
- package/dist/hook-pre-git.js.map +1 -1
- package/dist/hook-stop-collab.js +373 -38
- package/dist/hook-stop-collab.js.map +1 -1
- package/dist/hook-user-prompt.js +126 -13
- package/dist/hook-user-prompt.js.map +1 -1
- package/hooks/hooks.json +1 -1
- package/package.json +3 -3
- package/skills/historical-memory-routing/SKILL.md +2 -2
- package/skills/review-merge-request/SKILL.md +9 -2
- package/skills/safe-collab-workflow/SKILL.md +5 -4
- package/skills/submit-change-step/SKILL.md +5 -2
package/dist/hook-user-prompt.js
CHANGED
|
@@ -114,33 +114,146 @@ function stateRoot() {
|
|
|
114
114
|
function statePath(sessionId) {
|
|
115
115
|
return path.join(stateRoot(), `${sessionId}.json`);
|
|
116
116
|
}
|
|
117
|
+
function stateLockPath(sessionId) {
|
|
118
|
+
return path.join(stateRoot(), `${sessionId}.lock`);
|
|
119
|
+
}
|
|
120
|
+
function stateLockMetaPath(sessionId) {
|
|
121
|
+
return path.join(stateLockPath(sessionId), "owner.json");
|
|
122
|
+
}
|
|
117
123
|
async function writeJsonAtomic(filePath, value) {
|
|
118
124
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
119
125
|
const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
120
126
|
await fs.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
121
127
|
await fs.rename(tmpPath, filePath);
|
|
122
128
|
}
|
|
129
|
+
var STATE_LOCK_WAIT_MS = 2e3;
|
|
130
|
+
var STATE_LOCK_POLL_MS = 25;
|
|
131
|
+
var STATE_LOCK_STALE_MS = 3e4;
|
|
132
|
+
var STATE_LOCK_HEARTBEAT_MS = 5e3;
|
|
133
|
+
async function sleep(ms) {
|
|
134
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
135
|
+
}
|
|
136
|
+
async function readStateLockMetadata(sessionId) {
|
|
137
|
+
const raw = await fs.readFile(stateLockMetaPath(sessionId), "utf8").catch(() => null);
|
|
138
|
+
if (!raw) return null;
|
|
139
|
+
try {
|
|
140
|
+
const parsed = JSON.parse(raw);
|
|
141
|
+
if (typeof parsed.ownerId !== "string" || typeof parsed.pid !== "number" || typeof parsed.createdAt !== "string" || typeof parsed.heartbeatAt !== "string") {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
ownerId: parsed.ownerId,
|
|
146
|
+
pid: parsed.pid,
|
|
147
|
+
createdAt: parsed.createdAt,
|
|
148
|
+
heartbeatAt: parsed.heartbeatAt
|
|
149
|
+
};
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function writeStateLockMetadata(sessionId, metadata) {
|
|
155
|
+
await writeJsonAtomic(stateLockMetaPath(sessionId), metadata);
|
|
156
|
+
}
|
|
157
|
+
async function tryRemoveStaleStateLock(sessionId) {
|
|
158
|
+
const lockPath = stateLockPath(sessionId);
|
|
159
|
+
const metadata = await readStateLockMetadata(sessionId);
|
|
160
|
+
const staleByHeartbeat = metadata && Date.now() - new Date(metadata.heartbeatAt).getTime() > STATE_LOCK_STALE_MS;
|
|
161
|
+
if (staleByHeartbeat) {
|
|
162
|
+
await fs.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
if (!metadata) {
|
|
166
|
+
const lockStat = await fs.stat(lockPath).catch(() => null);
|
|
167
|
+
if (lockStat && Date.now() - lockStat.mtimeMs > STATE_LOCK_STALE_MS) {
|
|
168
|
+
await fs.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
async function acquireStateLock(sessionId) {
|
|
175
|
+
const lockPath = stateLockPath(sessionId);
|
|
176
|
+
const deadline = Date.now() + STATE_LOCK_WAIT_MS;
|
|
177
|
+
while (true) {
|
|
178
|
+
try {
|
|
179
|
+
await fs.mkdir(lockPath);
|
|
180
|
+
const ownerId = randomUUID();
|
|
181
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
182
|
+
const metadata = {
|
|
183
|
+
ownerId,
|
|
184
|
+
pid: process.pid,
|
|
185
|
+
createdAt,
|
|
186
|
+
heartbeatAt: createdAt
|
|
187
|
+
};
|
|
188
|
+
await writeStateLockMetadata(sessionId, metadata);
|
|
189
|
+
let released = false;
|
|
190
|
+
const heartbeat = setInterval(() => {
|
|
191
|
+
if (released) return;
|
|
192
|
+
void writeStateLockMetadata(sessionId, {
|
|
193
|
+
...metadata,
|
|
194
|
+
heartbeatAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
195
|
+
}).catch(() => void 0);
|
|
196
|
+
}, STATE_LOCK_HEARTBEAT_MS);
|
|
197
|
+
heartbeat.unref?.();
|
|
198
|
+
return async () => {
|
|
199
|
+
if (released) return;
|
|
200
|
+
released = true;
|
|
201
|
+
clearInterval(heartbeat);
|
|
202
|
+
const currentMetadata = await readStateLockMetadata(sessionId);
|
|
203
|
+
if (currentMetadata?.ownerId === ownerId) {
|
|
204
|
+
await fs.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
} catch (error) {
|
|
208
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : null;
|
|
209
|
+
if (code !== "EEXIST") {
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
if (await tryRemoveStaleStateLock(sessionId)) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (Date.now() >= deadline) {
|
|
216
|
+
throw new Error(`Timed out acquiring hook state lock for session ${sessionId}.`);
|
|
217
|
+
}
|
|
218
|
+
await sleep(STATE_LOCK_POLL_MS);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function withStateLock(sessionId, fn) {
|
|
223
|
+
const release = await acquireStateLock(sessionId);
|
|
224
|
+
try {
|
|
225
|
+
return await fn();
|
|
226
|
+
} finally {
|
|
227
|
+
await release();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
123
230
|
async function savePendingTurnState(state) {
|
|
124
231
|
await writeJsonAtomic(statePath(state.sessionId), state);
|
|
125
232
|
}
|
|
126
233
|
async function createPendingTurnState(params) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
234
|
+
return withStateLock(params.sessionId, async () => {
|
|
235
|
+
const state = {
|
|
236
|
+
sessionId: params.sessionId,
|
|
237
|
+
turnId: randomUUID(),
|
|
238
|
+
prompt: params.prompt,
|
|
239
|
+
initialCwd: params.initialCwd?.trim() || null,
|
|
240
|
+
intent: params.intent,
|
|
241
|
+
submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
242
|
+
consultedMemory: false,
|
|
243
|
+
touchedRepos: {},
|
|
244
|
+
turnFailureMessage: null,
|
|
245
|
+
turnFailureHint: null,
|
|
246
|
+
turnFailedAt: null
|
|
247
|
+
};
|
|
248
|
+
await savePendingTurnState(state);
|
|
249
|
+
return state;
|
|
250
|
+
});
|
|
139
251
|
}
|
|
140
252
|
|
|
141
253
|
// src/hook-utils.ts
|
|
142
254
|
import fs2 from "fs/promises";
|
|
143
255
|
import path2 from "path";
|
|
256
|
+
import { readCollabBinding } from "@remixhq/core/binding";
|
|
144
257
|
async function readJsonStdin() {
|
|
145
258
|
const chunks = [];
|
|
146
259
|
for await (const chunk of process.stdin) {
|
|
@@ -194,7 +307,7 @@ async function main() {
|
|
|
194
307
|
await createPendingTurnState({
|
|
195
308
|
sessionId,
|
|
196
309
|
prompt,
|
|
197
|
-
cwd,
|
|
310
|
+
initialCwd: cwd,
|
|
198
311
|
intent
|
|
199
312
|
});
|
|
200
313
|
const boundRepo = await findBoundRepo(cwd);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/history-routing.ts","../src/hook-state.ts","../src/hook-utils.ts","../src/hook-user-prompt.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 shouldPreferRemixMemory(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 \"Remix advisory:\",\n \"This prompt looks like a historical reasoning request in a repo bound to Remix.\",\n \"Start with `remix_collab_memory_summary`, `remix_collab_memory_search`, or `remix_collab_memory_timeline` before raw git history. Only fetch `remix_collab_memory_change_step_diff` after identifying a relevant `changeStepId`.\",\n ].join(\"\\n\");\n }\n\n if (intent === \"collab_state\") {\n return [\n \"Remix advisory:\",\n \"This prompt looks like a repo collaboration-state request in a repo bound to Remix.\",\n \"Start with `remix_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(), \"remix-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, \".remix\", \"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 { buildPromptRoutingAdvisory, classifyTurnIntent } from \"./history-routing.js\";\nimport { createPendingTurnState } from \"./hook-state.js\";\nimport { extractString, findBoundRepo, readJsonStdin } from \"./hook-utils.js\";\n\nasync function main(): Promise<void> {\n const payload = await readJsonStdin();\n const sessionId = extractString(payload, [\"session_id\"]);\n const prompt = extractString(payload, [\"prompt\"]);\n if (!sessionId || !prompt) {\n return;\n }\n\n const cwd = extractString(payload, [\"cwd\"]);\n const intent = classifyTurnIntent(prompt);\n await createPendingTurnState({\n sessionId,\n prompt,\n cwd,\n intent,\n });\n\n const boundRepo = await findBoundRepo(cwd);\n if (!boundRepo) {\n return;\n }\n\n const advisory = buildPromptRoutingAdvisory(intent);\n if (advisory) {\n process.stdout.write(advisory);\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"]}
|
|
1
|
+
{"version":3,"sources":["../src/history-routing.ts","../src/hook-state.ts","../src/hook-utils.ts","../src/hook-user-prompt.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 shouldPreferRemixMemory(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 \"Remix advisory:\",\n \"This prompt looks like a historical reasoning request in a repo bound to Remix.\",\n \"Start with `remix_collab_memory_summary`, `remix_collab_memory_search`, or `remix_collab_memory_timeline` before raw git history. Only fetch `remix_collab_memory_change_step_diff` after identifying a relevant `changeStepId`.\",\n ].join(\"\\n\");\n }\n\n if (intent === \"collab_state\") {\n return [\n \"Remix advisory:\",\n \"This prompt looks like a repo collaboration-state request in a repo bound to Remix.\",\n \"Start with `remix_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 RepoRecordMode = \"changed_turn\" | \"no_diff_turn\";\n\nexport type TouchedRepoState = {\n repoRoot: string;\n projectId: string | null;\n currentAppId: string | null;\n upstreamAppId: string | null;\n firstTouchedAt: string;\n lastTouchedAt: string;\n lastObservedWriteAt: string | null;\n touchedBy: string[];\n hasObservedWrite: boolean;\n manuallyRecorded: boolean;\n manuallyRecordedAt: string | null;\n manuallyRecordedByTool: string | null;\n stopAttempted: boolean;\n stopRecorded: boolean;\n stopRecordedAt: string | null;\n stopRecordedMode: RepoRecordMode | null;\n recordingFailureMessage: string | null;\n recordingFailureHint: string | null;\n recordingFailedAt: string | null;\n};\n\nexport type PendingTurnState = {\n sessionId: string;\n turnId: string;\n prompt: string;\n initialCwd: string | null;\n intent: TurnIntent;\n submittedAt: string;\n consultedMemory: boolean;\n touchedRepos: Record<string, TouchedRepoState>;\n turnFailureMessage: string | null;\n turnFailureHint: string | null;\n turnFailedAt: string | null;\n};\n\nfunction stateRoot(): string {\n return path.join(os.tmpdir(), \"remix-claude-plugin-hooks\");\n}\n\nfunction statePath(sessionId: string): string {\n return path.join(stateRoot(), `${sessionId}.json`);\n}\n\nfunction stateLockPath(sessionId: string): string {\n return path.join(stateRoot(), `${sessionId}.lock`);\n}\n\nfunction stateLockMetaPath(sessionId: string): string {\n return path.join(stateLockPath(sessionId), \"owner.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\nconst STATE_LOCK_WAIT_MS = 2_000;\nconst STATE_LOCK_POLL_MS = 25;\nconst STATE_LOCK_STALE_MS = 30_000;\nconst STATE_LOCK_HEARTBEAT_MS = 5_000;\n\ntype StateLockMetadata = {\n ownerId: string;\n pid: number;\n createdAt: string;\n heartbeatAt: string;\n};\n\nasync function sleep(ms: number): Promise<void> {\n await new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nasync function readStateLockMetadata(sessionId: string): Promise<StateLockMetadata | null> {\n const raw = await fs.readFile(stateLockMetaPath(sessionId), \"utf8\").catch(() => null);\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw) as Partial<StateLockMetadata>;\n if (\n typeof parsed.ownerId !== \"string\" ||\n typeof parsed.pid !== \"number\" ||\n typeof parsed.createdAt !== \"string\" ||\n typeof parsed.heartbeatAt !== \"string\"\n ) {\n return null;\n }\n return {\n ownerId: parsed.ownerId,\n pid: parsed.pid,\n createdAt: parsed.createdAt,\n heartbeatAt: parsed.heartbeatAt,\n };\n } catch {\n return null;\n }\n}\n\nasync function writeStateLockMetadata(sessionId: string, metadata: StateLockMetadata): Promise<void> {\n await writeJsonAtomic(stateLockMetaPath(sessionId), metadata);\n}\n\nasync function tryRemoveStaleStateLock(sessionId: string): Promise<boolean> {\n const lockPath = stateLockPath(sessionId);\n const metadata = await readStateLockMetadata(sessionId);\n const staleByHeartbeat =\n metadata && Date.now() - new Date(metadata.heartbeatAt).getTime() > STATE_LOCK_STALE_MS;\n if (staleByHeartbeat) {\n await fs.rm(lockPath, { recursive: true, force: true }).catch(() => undefined);\n return true;\n }\n\n if (!metadata) {\n const lockStat = await fs.stat(lockPath).catch(() => null);\n if (lockStat && Date.now() - lockStat.mtimeMs > STATE_LOCK_STALE_MS) {\n await fs.rm(lockPath, { recursive: true, force: true }).catch(() => undefined);\n return true;\n }\n }\n\n return false;\n}\n\nasync function acquireStateLock(sessionId: string): Promise<() => Promise<void>> {\n const lockPath = stateLockPath(sessionId);\n const deadline = Date.now() + STATE_LOCK_WAIT_MS;\n\n while (true) {\n try {\n await fs.mkdir(lockPath);\n const ownerId = randomUUID();\n const createdAt = new Date().toISOString();\n const metadata: StateLockMetadata = {\n ownerId,\n pid: process.pid,\n createdAt,\n heartbeatAt: createdAt,\n };\n await writeStateLockMetadata(sessionId, metadata);\n let released = false;\n const heartbeat = setInterval(() => {\n if (released) return;\n void writeStateLockMetadata(sessionId, {\n ...metadata,\n heartbeatAt: new Date().toISOString(),\n }).catch(() => undefined);\n }, STATE_LOCK_HEARTBEAT_MS);\n heartbeat.unref?.();\n\n return async () => {\n if (released) return;\n released = true;\n clearInterval(heartbeat);\n const currentMetadata = await readStateLockMetadata(sessionId);\n if (currentMetadata?.ownerId === ownerId) {\n await fs.rm(lockPath, { recursive: true, force: true }).catch(() => undefined);\n }\n };\n } catch (error) {\n const code = error && typeof error === \"object\" && \"code\" in error ? (error as { code?: unknown }).code : null;\n if (code !== \"EEXIST\") {\n throw error;\n }\n\n if (await tryRemoveStaleStateLock(sessionId)) {\n continue;\n }\n\n if (Date.now() >= deadline) {\n throw new Error(`Timed out acquiring hook state lock for session ${sessionId}.`);\n }\n await sleep(STATE_LOCK_POLL_MS);\n }\n }\n}\n\nasync function withStateLock<T>(sessionId: string, fn: () => Promise<T>): Promise<T> {\n const release = await acquireStateLock(sessionId);\n try {\n return await fn();\n } finally {\n await release();\n }\n}\n\nfunction normalizeIntent(value: unknown): TurnIntent {\n return value === \"memory_first\" || value === \"collab_state\" || value === \"git_facts\" ? value : \"neutral\";\n}\n\nfunction normalizeString(value: unknown): string | null {\n return typeof value === \"string\" && value.trim() ? value.trim() : null;\n}\n\nfunction normalizeStringArray(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n return Array.from(\n new Set(\n value\n .filter((entry): entry is string => typeof entry === \"string\" && entry.trim().length > 0)\n .map((entry) => entry.trim()),\n ),\n );\n}\n\nfunction normalizeTouchedRepo(value: unknown, repoRoot: string): TouchedRepoState | null {\n if (!value || typeof value !== \"object\") return null;\n const parsed = value as Partial<TouchedRepoState>;\n const normalizedRepoRoot = normalizeString(parsed.repoRoot) ?? repoRoot.trim();\n if (!normalizedRepoRoot) return null;\n\n return {\n repoRoot: normalizedRepoRoot,\n projectId: normalizeString(parsed.projectId),\n currentAppId: normalizeString(parsed.currentAppId),\n upstreamAppId: normalizeString(parsed.upstreamAppId),\n firstTouchedAt: normalizeString(parsed.firstTouchedAt) ?? new Date().toISOString(),\n lastTouchedAt: normalizeString(parsed.lastTouchedAt) ?? new Date().toISOString(),\n lastObservedWriteAt: normalizeString(parsed.lastObservedWriteAt),\n touchedBy: normalizeStringArray(parsed.touchedBy),\n hasObservedWrite: Boolean(parsed.hasObservedWrite),\n manuallyRecorded: Boolean(parsed.manuallyRecorded),\n manuallyRecordedAt: normalizeString(parsed.manuallyRecordedAt),\n manuallyRecordedByTool: normalizeString(parsed.manuallyRecordedByTool),\n stopAttempted: Boolean(parsed.stopAttempted),\n stopRecorded: Boolean(parsed.stopRecorded),\n stopRecordedAt: normalizeString(parsed.stopRecordedAt),\n stopRecordedMode: parsed.stopRecordedMode === \"changed_turn\" || parsed.stopRecordedMode === \"no_diff_turn\" ? parsed.stopRecordedMode : null,\n recordingFailureMessage: normalizeString(parsed.recordingFailureMessage),\n recordingFailureHint: normalizeString(parsed.recordingFailureHint),\n recordingFailedAt: normalizeString(parsed.recordingFailedAt),\n };\n}\n\nfunction normalizeTouchedRepos(value: unknown): Record<string, TouchedRepoState> {\n if (!value || typeof value !== \"object\") return {};\n const entries = Object.entries(value as Record<string, unknown>)\n .map(([repoRoot, repo]) => normalizeTouchedRepo(repo, repoRoot))\n .filter((repo): repo is TouchedRepoState => repo !== null)\n .sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));\n return Object.fromEntries(entries.map((repo) => [repo.repoRoot, repo]));\n}\n\nfunction createTouchedRepo(params: {\n repoRoot: string;\n projectId?: string | null;\n currentAppId?: string | null;\n upstreamAppId?: string | null;\n touchedBy?: string | null;\n hasObservedWrite?: boolean;\n}): TouchedRepoState {\n const now = new Date().toISOString();\n const touchedBy = params.touchedBy?.trim() ? [params.touchedBy.trim()] : [];\n return {\n repoRoot: params.repoRoot,\n projectId: normalizeString(params.projectId),\n currentAppId: normalizeString(params.currentAppId),\n upstreamAppId: normalizeString(params.upstreamAppId),\n firstTouchedAt: now,\n lastTouchedAt: now,\n lastObservedWriteAt: params.hasObservedWrite ? now : null,\n touchedBy,\n hasObservedWrite: Boolean(params.hasObservedWrite),\n manuallyRecorded: false,\n manuallyRecordedAt: null,\n manuallyRecordedByTool: null,\n stopAttempted: false,\n stopRecorded: false,\n stopRecordedAt: null,\n stopRecordedMode: null,\n recordingFailureMessage: null,\n recordingFailureHint: null,\n recordingFailedAt: null,\n };\n}\n\nasync function updatePendingTurnState(\n sessionId: string,\n updater: (state: PendingTurnState) => void | boolean,\n): Promise<PendingTurnState | null> {\n return withStateLock(sessionId, async () => {\n const existing = await loadPendingTurnState(sessionId);\n if (!existing) return null;\n const result = updater(existing);\n if (result === false) return existing;\n await savePendingTurnState(existing);\n return existing;\n });\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 return {\n sessionId: parsed.sessionId,\n turnId: parsed.turnId,\n prompt: parsed.prompt,\n initialCwd: normalizeString(parsed.initialCwd),\n intent: normalizeIntent(parsed.intent),\n submittedAt: typeof parsed.submittedAt === \"string\" ? parsed.submittedAt : new Date().toISOString(),\n consultedMemory: Boolean(parsed.consultedMemory),\n touchedRepos: normalizeTouchedRepos(parsed.touchedRepos),\n turnFailureMessage: normalizeString(parsed.turnFailureMessage),\n turnFailureHint: normalizeString(parsed.turnFailureHint),\n turnFailedAt: normalizeString(parsed.turnFailedAt),\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 initialCwd?: string | null;\n intent: TurnIntent;\n}): Promise<PendingTurnState> {\n return withStateLock(params.sessionId, async () => {\n const state: PendingTurnState = {\n sessionId: params.sessionId,\n turnId: randomUUID(),\n prompt: params.prompt,\n initialCwd: params.initialCwd?.trim() || null,\n intent: params.intent,\n submittedAt: new Date().toISOString(),\n consultedMemory: false,\n touchedRepos: {},\n turnFailureMessage: null,\n turnFailureHint: null,\n turnFailedAt: null,\n };\n await savePendingTurnState(state);\n return state;\n });\n}\n\nexport async function upsertTouchedRepo(\n sessionId: string,\n params: {\n repoRoot: string;\n projectId?: string | null;\n currentAppId?: string | null;\n upstreamAppId?: string | null;\n touchedBy?: string | null;\n hasObservedWrite?: boolean;\n },\n): Promise<TouchedRepoState | null> {\n const normalizedRepoRoot = params.repoRoot.trim();\n if (!normalizedRepoRoot) return null;\n const state = await updatePendingTurnState(sessionId, (existing) => {\n const current =\n existing.touchedRepos[normalizedRepoRoot] ??\n createTouchedRepo({\n repoRoot: normalizedRepoRoot,\n projectId: params.projectId,\n currentAppId: params.currentAppId,\n upstreamAppId: params.upstreamAppId,\n touchedBy: params.touchedBy,\n hasObservedWrite: params.hasObservedWrite,\n });\n\n current.projectId = normalizeString(params.projectId) ?? current.projectId;\n current.currentAppId = normalizeString(params.currentAppId) ?? current.currentAppId;\n current.upstreamAppId = normalizeString(params.upstreamAppId) ?? current.upstreamAppId;\n current.lastTouchedAt = new Date().toISOString();\n if (params.touchedBy?.trim() && !current.touchedBy.includes(params.touchedBy.trim())) {\n current.touchedBy = [...current.touchedBy, params.touchedBy.trim()].sort((a, b) => a.localeCompare(b));\n }\n if (params.hasObservedWrite) {\n current.hasObservedWrite = true;\n current.lastObservedWriteAt = new Date().toISOString();\n }\n existing.touchedRepos[normalizedRepoRoot] = current;\n });\n return state?.touchedRepos[normalizedRepoRoot] ?? null;\n}\n\nexport async function markTouchedRepoObservedWrite(\n sessionId: string,\n repoRoot: string,\n params?: { toolName?: string | null },\n): Promise<void> {\n await upsertTouchedRepo(sessionId, {\n repoRoot,\n touchedBy: params?.toolName ?? null,\n hasObservedWrite: true,\n });\n}\n\nexport async function markTouchedRepoManuallyRecorded(\n sessionId: string,\n repoRoot: string,\n params?: { toolName?: string | null },\n): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n const normalizedRepoRoot = repoRoot.trim();\n if (!normalizedRepoRoot) return false;\n const current =\n existing.touchedRepos[normalizedRepoRoot] ??\n createTouchedRepo({\n repoRoot: normalizedRepoRoot,\n touchedBy: params?.toolName ?? null,\n hasObservedWrite: false,\n });\n current.lastTouchedAt = new Date().toISOString();\n current.manuallyRecorded = true;\n current.manuallyRecordedAt = new Date().toISOString();\n current.manuallyRecordedByTool = normalizeString(params?.toolName) ?? current.manuallyRecordedByTool;\n current.recordingFailureMessage = null;\n current.recordingFailureHint = null;\n current.recordingFailedAt = null;\n if (params?.toolName?.trim() && !current.touchedBy.includes(params.toolName.trim())) {\n current.touchedBy = [...current.touchedBy, params.toolName.trim()].sort((a, b) => a.localeCompare(b));\n }\n existing.touchedRepos[normalizedRepoRoot] = current;\n });\n}\n\nexport async function markPendingTurnConsultedMemory(sessionId: string): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n if (existing.consultedMemory) return false;\n existing.consultedMemory = true;\n });\n}\n\nexport async function markTouchedRepoStopAttempted(sessionId: string, repoRoot: string): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n const current = existing.touchedRepos[repoRoot];\n if (!current) return false;\n current.stopAttempted = true;\n current.lastTouchedAt = new Date().toISOString();\n });\n}\n\nexport async function markTouchedRepoStopRecorded(\n sessionId: string,\n repoRoot: string,\n params: {\n mode: RepoRecordMode;\n },\n): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n const current = existing.touchedRepos[repoRoot];\n if (!current) return false;\n current.stopAttempted = true;\n current.stopRecorded = true;\n current.stopRecordedAt = new Date().toISOString();\n current.stopRecordedMode = params.mode;\n current.recordingFailureMessage = null;\n current.recordingFailureHint = null;\n current.recordingFailedAt = null;\n current.lastTouchedAt = new Date().toISOString();\n });\n}\n\nexport async function markTouchedRepoRecordingFailure(\n sessionId: string,\n repoRoot: string,\n params: {\n message: string;\n hint?: string | null;\n },\n): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n const current = existing.touchedRepos[repoRoot];\n if (!current) return false;\n current.stopAttempted = true;\n current.recordingFailureMessage = params.message.trim();\n current.recordingFailureHint = params.hint?.trim() || null;\n current.recordingFailedAt = new Date().toISOString();\n current.lastTouchedAt = new Date().toISOString();\n });\n}\n\nexport async function markPendingTurnFailure(\n sessionId: string,\n params: {\n message: string;\n hint?: string | null;\n },\n): Promise<void> {\n await updatePendingTurnState(sessionId, (existing) => {\n existing.turnFailureMessage = params.message.trim();\n existing.turnFailureHint = params.hint?.trim() || null;\n existing.turnFailedAt = new Date().toISOString();\n });\n}\n\nexport async function listTouchedRepos(sessionId: string): Promise<TouchedRepoState[]> {\n const existing = await loadPendingTurnState(sessionId);\n if (!existing) return [];\n return Object.values(existing.touchedRepos).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));\n}\n\nexport async function clearPendingTurnState(sessionId: string): Promise<void> {\n await withStateLock(sessionId, async () => {\n await fs.rm(statePath(sessionId), { force: true }).catch(() => undefined);\n });\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport { readCollabBinding } from \"@remixhq/core/binding\";\n\ntype BindingSummary = {\n repoRoot: string;\n projectId: string | null;\n currentAppId: string | null;\n upstreamAppId: string | null;\n};\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 extractToolResponse(payload: Record<string, unknown>): Record<string, unknown> | null {\n return getNestedRecord(payload.tool_response) ?? getNestedRecord(payload.toolResponse);\n}\n\nexport function extractToolName(payload: Record<string, unknown>): string | null {\n return extractString(payload, [\"tool_name\", \"toolName\"]);\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 function extractToolCwd(payload: Record<string, unknown>): string | null {\n const toolInput = extractToolInput(payload);\n return extractString(toolInput, [\"cwd\"]) ?? extractString(payload, [\"cwd\"]);\n}\n\nexport function didToolSucceed(payload: Record<string, unknown>): boolean {\n const toolResponse = extractToolResponse(payload);\n const explicitSuccess = toolResponse ? extractBoolean(toolResponse, [\"success\", \"ok\"]) : null;\n if (explicitSuccess !== null) {\n return explicitSuccess;\n }\n const hookEventName = extractString(payload, [\"hook_event_name\", \"hookEventName\"]);\n return hookEventName === \"PostToolUse\";\n}\n\nfunction collectStringPathValue(value: unknown): string[] {\n if (typeof value === \"string\" && value.trim()) return [value.trim()];\n if (Array.isArray(value)) {\n return value.flatMap((entry) => collectStringPathValue(entry));\n }\n return [];\n}\n\nfunction collectPathTargetsFromObject(input: Record<string, unknown>, keys: string[]): string[] {\n return keys.flatMap((key) => collectStringPathValue(input[key]));\n}\n\nfunction resolveCandidatePath(targetPath: string, baseDir: string): string {\n return path.isAbsolute(targetPath) ? path.normalize(targetPath) : path.resolve(baseDir, targetPath);\n}\n\nexport function extractToolPathTargets(payload: Record<string, unknown>, toolName?: string | null): string[] {\n const name = (toolName ?? extractToolName(payload) ?? \"\").trim().toLowerCase();\n const toolInput = extractToolInput(payload);\n const baseDir = extractToolCwd(payload) ?? process.cwd();\n const baseKeys = [\"path\", \"paths\", \"file_path\", \"filePath\", \"target_file\", \"targetFile\", \"filename\"];\n\n const targets =\n name === \"notebookedit\"\n ? collectPathTargetsFromObject(toolInput, [\"target_notebook\", \"notebook_path\", \"notebookPath\", ...baseKeys])\n : collectPathTargetsFromObject(toolInput, baseKeys);\n\n return Array.from(new Set(targets.map((entry) => resolveCandidatePath(entry, baseDir))));\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, \".remix\", \"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\nexport async function resolveBoundRepoSummary(startPath: string | null): Promise<BindingSummary | null> {\n const repoRoot = await findBoundRepo(startPath);\n if (!repoRoot) return null;\n const binding = await readCollabBinding(repoRoot).catch(() => null);\n if (!binding) return null;\n return {\n repoRoot,\n projectId: binding.projectId,\n currentAppId: binding.currentAppId,\n upstreamAppId: binding.upstreamAppId,\n };\n}\n\nexport async function resolveTouchedBoundReposFromPaths(paths: string[]): Promise<BindingSummary[]> {\n const resolved = await Promise.all(paths.map((targetPath) => resolveBoundRepoSummary(targetPath)));\n const unique = new Map<string, BindingSummary>();\n for (const repo of resolved) {\n if (!repo) continue;\n unique.set(repo.repoRoot, repo);\n }\n return Array.from(unique.values()).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));\n}\n\nexport async function resolveBoundRepoFromToolCwd(payload: Record<string, unknown>): Promise<BindingSummary | null> {\n return resolveBoundRepoSummary(extractToolCwd(payload));\n}\n","import { buildPromptRoutingAdvisory, classifyTurnIntent } from \"./history-routing.js\";\nimport { createPendingTurnState } from \"./hook-state.js\";\nimport { extractString, findBoundRepo, readJsonStdin } from \"./hook-utils.js\";\n\nasync function main(): Promise<void> {\n const payload = await readJsonStdin();\n const sessionId = extractString(payload, [\"session_id\"]);\n const prompt = extractString(payload, [\"prompt\"]);\n if (!sessionId || !prompt) {\n return;\n }\n\n const cwd = extractString(payload, [\"cwd\"]);\n const intent = classifyTurnIntent(prompt);\n await createPendingTurnState({\n sessionId,\n prompt,\n initialCwd: cwd,\n intent,\n });\n\n const boundRepo = await findBoundRepo(cwd);\n if (!boundRepo) {\n return;\n }\n\n const advisory = buildPromptRoutingAdvisory(intent);\n if (advisory) {\n process.stdout.write(advisory);\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"]}
|
package/hooks/hooks.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remixhq/claude-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Claude Code plugin for Remix collaboration workflows",
|
|
5
5
|
"homepage": "https://github.com/RemixDotOne/remix-claude-plugin",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"prepack": "npm run build"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@remixhq/core": "^0.1.
|
|
35
|
-
"@remixhq/mcp": "^0.1.
|
|
34
|
+
"@remixhq/core": "^0.1.5",
|
|
35
|
+
"@remixhq/mcp": "^0.1.5"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^25.4.0",
|
|
@@ -22,10 +22,10 @@ Default order:
|
|
|
22
22
|
2. `remix_collab_memory_search` for targeted historical lookup
|
|
23
23
|
3. `remix_collab_memory_timeline` when chronology matters
|
|
24
24
|
4. `remix_collab_memory_change_step_diff` only after identifying a relevant `changeStepId`
|
|
25
|
-
5. Raw git history only
|
|
25
|
+
5. Raw git history only for exact commit, blame, ancestry, or patch details after Remix memory has narrowed the relevant change
|
|
26
26
|
|
|
27
27
|
Important rules:
|
|
28
28
|
|
|
29
29
|
- Do not start these questions with `git log`, `git show`, `git blame`, or `git diff`.
|
|
30
30
|
- Use bounded memory reads first, then expand to the full stored diff only when the relevant change step is known.
|
|
31
|
-
- If the user explicitly asks for raw git facts, raw git can be used, but Remix memory
|
|
31
|
+
- If the user explicitly asks for raw git facts, raw git can be used for those exact facts, but Remix memory remains the authoritative source for intent and reasoning.
|
|
@@ -7,11 +7,17 @@ description: Use when reviewing, approving, rejecting, or summarizing Remix merg
|
|
|
7
7
|
|
|
8
8
|
Use Remix merge request tools as the default review workflow.
|
|
9
9
|
|
|
10
|
-
Remix merge requests are app-lineage review flows, not git branch merges.
|
|
10
|
+
Remix merge requests are app-lineage review flows, not git branch merges. In a bound repo, use Remix merge-request tools as the authoritative merge workflow rather than local `git merge`.
|
|
11
11
|
|
|
12
12
|
Recommended sequence:
|
|
13
13
|
|
|
14
|
-
1. Use
|
|
14
|
+
1. Use the queue tool that matches the intent when the target MR is not already known:
|
|
15
|
+
- `remix_collab_review_queue` for "what can I review?"
|
|
16
|
+
- `remix_collab_my_merge_requests` for "what did I create?"
|
|
17
|
+
- `remix_collab_list_app_merge_requests` for app-scoped requests, with required `queue`:
|
|
18
|
+
- `app_reviewable` for incoming reviewable requests on this app
|
|
19
|
+
- `app_outgoing` for requests opened from this app
|
|
20
|
+
- `app_related_visible` for all open visible requests where this app is source or target
|
|
15
21
|
2. Use `remix_collab_view_merge_request` to inspect the request.
|
|
16
22
|
3. Request bounded diff output first. Only request a larger diff if needed to answer the review task accurately.
|
|
17
23
|
4. If approval is requested:
|
|
@@ -22,6 +28,7 @@ Recommended sequence:
|
|
|
22
28
|
Important rules:
|
|
23
29
|
|
|
24
30
|
- In a bound repo, use Remix review tools as the default path for MR handling.
|
|
31
|
+
- Choose an explicit queue instead of treating merge requests as one broad inbox.
|
|
25
32
|
- Do not treat raw `git merge` as the way Remix merge requests are approved or applied.
|
|
26
33
|
- If approval would mutate the local target repo, explain that before applying it.
|
|
27
34
|
- If the repo context is unclear, stay in remote-only review mode until the target repo is confirmed.
|
|
@@ -5,7 +5,7 @@ description: Use when working in a repo that may be bound to Remix, especially f
|
|
|
5
5
|
|
|
6
6
|
# Safe Remix Collaboration Workflow
|
|
7
7
|
|
|
8
|
-
In a Remix-bound repo, treat Remix MCP tools as the
|
|
8
|
+
In a Remix-bound repo, treat Remix MCP tools as the authoritative workflow layer for collaboration state and historical reasoning.
|
|
9
9
|
|
|
10
10
|
For normal Remix collaboration:
|
|
11
11
|
|
|
@@ -25,7 +25,7 @@ Normal rule:
|
|
|
25
25
|
- do not proactively call `remix_collab_add` or `remix_collab_record_turn` during ordinary work
|
|
26
26
|
- use those tools manually only for explicit user intent, operational recovery, backfills, or debugging
|
|
27
27
|
|
|
28
|
-
Raw git
|
|
28
|
+
Raw git mutation commands must not be used for ordinary bound-repo collaboration work. Raw git reads are reserved for exact repository facts, not for normal collaboration-state or historical-reasoning flows.
|
|
29
29
|
|
|
30
30
|
Follow this order:
|
|
31
31
|
|
|
@@ -46,8 +46,9 @@ Follow this order:
|
|
|
46
46
|
Important rules:
|
|
47
47
|
|
|
48
48
|
- If the repo is unbound, use `remix_collab_init` for the current repo or `remix_collab_remix` for an existing Remix app lineage.
|
|
49
|
-
- In a bound repo, do not use raw git `commit`, `push`, `pull`, `merge`, `rebase`, or `reset`
|
|
49
|
+
- In a bound repo, do not use raw git `commit`, `push`, `pull`, `merge`, `rebase`, or `reset` for ordinary Remix collaboration work.
|
|
50
50
|
- In a bound repo, do not start why/history/failed-attempt questions with raw git `log`, `show`, `blame`, or `diff`; start with Remix memory reads instead.
|
|
51
51
|
- Do not manually fire per-turn recording calls during ordinary work. The end-of-response hook records one turn once per completed assistant response.
|
|
52
|
-
- Use raw git only when the task is clearly about a separate GitHub-only branch workflow rather than Remix state.
|
|
52
|
+
- Use raw git reads only when the task is clearly about exact repository facts or a separate GitHub-only branch workflow rather than Remix state.
|
|
53
53
|
- When viewing merge requests, request bounded diff output first and only ask for more detail if needed.
|
|
54
|
+
- For merge-request listing, choose the explicit queue that matches the intent: `remix_collab_review_queue`, `remix_collab_my_merge_requests`, or `remix_collab_list_app_merge_requests` with required `queue` set to `app_reviewable`, `app_outgoing`, or `app_related_visible`.
|
|
@@ -7,7 +7,8 @@ description: Use when code edits are complete in a Remix-bound repo and the work
|
|
|
7
7
|
|
|
8
8
|
Use this skill after edits are complete and the task should be captured as a Remix collaboration step.
|
|
9
9
|
|
|
10
|
-
In a Remix-bound repo, `remix_collab_add` is the
|
|
10
|
+
In a Remix-bound repo, `remix_collab_add` is the authoritative Remix recording path for completed work instead of raw git commit or push.
|
|
11
|
+
If the tool list is ambiguous, prefer the clearer alias `remix_collab_add_change_step` for manually recording a code-diff change step.
|
|
11
12
|
It creates the next commit on the Remix side from the submitted diff, then syncs the local repo to that new commit when auto-sync succeeds.
|
|
12
13
|
|
|
13
14
|
In this plugin setup, the preferred default is automatic end-of-response recording:
|
|
@@ -25,10 +26,12 @@ Workflow:
|
|
|
25
26
|
Important behavior:
|
|
26
27
|
|
|
27
28
|
- `remix_collab_add` defaults to auto-sync behavior in this Remix setup.
|
|
29
|
+
- `remix_collab_record_turn` is for no-diff prompt/response history only and should not be used when the worktree has code changes.
|
|
28
30
|
- During ordinary work, the hook is the normal automatic path for changed-turn recording.
|
|
29
31
|
- In the automatic hook path, the `remix_collab_add` prompt is the exact user prompt for that completed turn.
|
|
30
32
|
- The automatic hook may also attach the final assistant response so changed turns and no-diff turns share the same prompt/response chronology.
|
|
31
|
-
- Do not
|
|
33
|
+
- Do not use raw `git commit` or `git push` as collaboration-recording steps in a bound repo.
|
|
34
|
+
- If automatic recording cannot complete safely, recover through Remix status, sync, reconcile, and manual recording flows rather than local git mutation.
|
|
32
35
|
- When the diff comes from the live worktree, Remix may discard tracked changes and captured untracked paths locally as part of auto-sync.
|
|
33
36
|
- The resulting git commit is created on the Remix side first and appears in the local repo after sync or auto-sync.
|
|
34
37
|
- Surface these side effects clearly to the user when they matter.
|