@remixhq/claude-plugin 0.1.12 → 0.1.15

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.
@@ -1 +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 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\";\nexport type ManualRecordingScope = \"change_step\" | \"full_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 manualRecordingScope: ManualRecordingScope | null;\n manualRemoteChangeRecordedAt: 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\nexport type PendingTouchedRepoSummary = {\n repoRoot: string;\n projectId: string | null;\n currentAppId: string | null;\n upstreamAppId: string | null;\n lastTouchedAt: string;\n lastObservedWriteAt: string | null;\n touchedBy: string[];\n hasObservedWrite: boolean;\n manuallyRecorded: boolean;\n manualRecordingScope: ManualRecordingScope | null;\n stopAttempted: boolean;\n stopRecorded: boolean;\n stopRecordedMode: RepoRecordMode | null;\n recordingFailureMessage: string | null;\n recordingFailureHint: string | null;\n recordingFailedAt: string | null;\n};\n\nexport type PendingTurnStateSummary = {\n sessionId: string;\n turnId: string;\n initialCwd: string | null;\n submittedAt: string;\n consultedMemory: boolean;\n promptLength: number;\n touchedRepoCount: number;\n turnFailureMessage: string | null;\n turnFailureHint: string | null;\n turnFailedAt: string | null;\n touchedRepos: PendingTouchedRepoSummary[];\n};\n\nfunction stateRoot(): string {\n const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_STATE_ROOT?.trim();\n return configured || 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 await fs.mkdir(stateRoot(), { recursive: true });\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 manualRecordingScope:\n parsed.manualRecordingScope === \"change_step\" || parsed.manualRecordingScope === \"full_turn\"\n ? parsed.manualRecordingScope\n : null,\n manualRemoteChangeRecordedAt: normalizeString(parsed.manualRemoteChangeRecordedAt),\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 manualRecordingScope: null,\n manualRemoteChangeRecordedAt: 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\nfunction summarizeTouchedRepo(repo: TouchedRepoState): PendingTouchedRepoSummary {\n return {\n repoRoot: repo.repoRoot,\n projectId: repo.projectId,\n currentAppId: repo.currentAppId,\n upstreamAppId: repo.upstreamAppId,\n lastTouchedAt: repo.lastTouchedAt,\n lastObservedWriteAt: repo.lastObservedWriteAt,\n touchedBy: [...repo.touchedBy],\n hasObservedWrite: repo.hasObservedWrite,\n manuallyRecorded: repo.manuallyRecorded,\n manualRecordingScope: repo.manualRecordingScope,\n stopAttempted: repo.stopAttempted,\n stopRecorded: repo.stopRecorded,\n stopRecordedMode: repo.stopRecordedMode,\n recordingFailureMessage: repo.recordingFailureMessage,\n recordingFailureHint: repo.recordingFailureHint,\n recordingFailedAt: repo.recordingFailedAt,\n };\n}\n\nfunction summarizePendingTurnState(state: PendingTurnState): PendingTurnStateSummary {\n const touchedRepos = Object.values(state.touchedRepos)\n .sort((a, b) => a.repoRoot.localeCompare(b.repoRoot))\n .map((repo) => summarizeTouchedRepo(repo));\n return {\n sessionId: state.sessionId,\n turnId: state.turnId,\n initialCwd: state.initialCwd,\n submittedAt: state.submittedAt,\n consultedMemory: state.consultedMemory,\n promptLength: state.prompt.length,\n touchedRepoCount: touchedRepos.length,\n turnFailureMessage: state.turnFailureMessage,\n turnFailureHint: state.turnFailureHint,\n turnFailedAt: state.turnFailedAt,\n touchedRepos,\n };\n}\n\nexport function getPendingTurnStateRootPath(): string {\n return stateRoot();\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; scope?: ManualRecordingScope | null; remoteChangeRecorded?: boolean },\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.manualRecordingScope = params?.scope ?? current.manualRecordingScope;\n if (params?.remoteChangeRecorded) {\n current.manualRemoteChangeRecordedAt = current.manuallyRecordedAt;\n }\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 listPendingTurnStateSummaries(): Promise<PendingTurnStateSummary[]> {\n const root = stateRoot();\n const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []);\n const sessionIds = entries\n .filter((entry) => entry.isFile() && entry.name.endsWith(\".json\"))\n .map((entry) => entry.name.replace(/\\.json$/, \"\"))\n .sort((a, b) => a.localeCompare(b));\n const states = await Promise.all(sessionIds.map((sessionId) => loadPendingTurnState(sessionId)));\n return states\n .filter((state): state is PendingTurnState => state !== null)\n .sort((a, b) => b.submittedAt.localeCompare(a.submittedAt))\n .map((state) => summarizePendingTurnState(state));\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 normalizeHookToolName(toolName: string | null): string | null {\n if (!toolName) return null;\n const trimmed = toolName.trim();\n if (!trimmed) return null;\n\n const remixToolIndex = trimmed.toLowerCase().indexOf(\"remix_collab_\");\n if (remixToolIndex >= 0) {\n return trimmed.slice(remixToolIndex);\n }\n\n return trimmed;\n}\n\nexport function extractAssistantResponse(payload: Record<string, unknown>): string | null {\n const candidateKeys = [\n \"last_assistant_message\",\n \"lastAssistantMessage\",\n \"assistant_response\",\n \"assistantResponse\",\n \"assistant_message\",\n \"assistantMessage\",\n \"response\",\n \"message\",\n ];\n\n return (\n extractString(payload, candidateKeys) ??\n extractString(extractToolResponse(payload) ?? {}, candidateKeys) ??\n extractString(extractToolInput(payload), candidateKeys)\n );\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 extractBashCommand(payload: Record<string, unknown>): string | null {\n const toolInput = extractToolInput(payload);\n return extractString(toolInput, [\"command\", \"cmd\", \"bash_command\", \"bashCommand\"]);\n}\n\nexport function extractToolErrorMessage(payload: Record<string, unknown>): string | null {\n const toolResponse = extractToolResponse(payload);\n const toolError = getNestedRecord(toolResponse?.error) ?? getNestedRecord(payload.error);\n return (\n extractString(toolError ?? {}, [\"message\"]) ??\n extractString(toolResponse ?? {}, [\"errorMessage\", \"message\"]) ??\n extractString(payload, [\"errorMessage\"])\n );\n}\n\nfunction extractToolStatus(payload: Record<string, unknown>): string | null {\n const toolResponse = extractToolResponse(payload);\n return (\n extractString(toolResponse ?? {}, [\"status\", \"state\"]) ??\n extractString(payload, [\"status\", \"state\"])\n );\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\n if (extractToolErrorMessage(payload)) {\n return false;\n }\n\n const status = extractToolStatus(payload)?.toLowerCase();\n if (status === \"error\" || status === \"failed\" || status === \"failure\") {\n return false;\n }\n\n const hookEventName = extractString(payload, [\"hook_event_name\", \"hookEventName\"]);\n return hookEventName === \"PostToolUse\";\n}\n\nexport function isRemoteChangeRecordedButLocalSyncFailed(payload: Record<string, unknown>): boolean {\n return extractToolErrorMessage(payload) === \"Change step succeeded remotely, but automatic local sync failed.\";\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 { shouldPreferRemixMemory } from \"./history-routing.js\";\nimport { loadPendingTurnState } from \"./hook-state.js\";\nimport { extractString, extractToolCwd, extractToolInput, findBoundRepo, readJsonStdin } from \"./hook-utils.js\";\n\ntype GitAdvisory = {\n subcommand: string;\n category: \"history_read\" | \"mutation\";\n message: string;\n blocked: boolean;\n};\n\nconst GIT_ADVISORIES: Array<{ subcommand: string; re: RegExp; category: \"history_read\" | \"mutation\"; message: string; blocked: boolean }> = [\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 Remix. Raw `git commit` is blocked for ordinary collaboration work because it bypasses Remix change-step recording. Let the stop hook record changed turns automatically, or use `remix_collab_add` only for explicit recovery, backfill, or manual-recording workflows.\",\n blocked: true,\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 Remix. Raw `git push` is blocked for ordinary collaboration work because publishing Remix collaboration state must happen through Remix recording, sync, and merge-request flows rather than direct branch pushes.\",\n blocked: true,\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 Remix. Raw `git pull` is blocked because Remix-managed repo alignment must go through `remix_collab_status`, `remix_collab_sync_preview` / `remix_collab_sync_apply`, or reconcile and upstream-sync flows when history diverges.\",\n blocked: true,\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 Remix. Raw `git merge` is blocked because Remix work must be merged through Remix merge requests rather than local branch merges.\",\n blocked: true,\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 Remix. Raw `git rebase` is blocked because it rewrites local history outside Remix reconcile and sync controls. Use `remix_collab_status` first, then follow the sync or reconcile path.\",\n blocked: true,\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 Remix. Raw `git reset` is blocked because it can discard or rewrite local state outside Remix recovery flows. Use `remix_collab_status` first, then use Remix sync or reconcile flows to realign state safely.\",\n blocked: true,\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 Remix. Raw `git log` is a fallback for exact commit history, not the default starting point for historical reasoning. Use Remix memory first when the goal is to understand intent, prior prompts, failed attempts, or decision trail context.\",\n blocked: false,\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 Remix. Raw `git show` is useful for exact patch inspection, but historical reasoning should start from Remix memory so you can identify the right change step or merge context before expanding into raw diffs.\",\n blocked: false,\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 Remix. Raw `git blame` can identify line ownership, but it does not explain the user ask, reasoning, or failed attempts behind a change. Use Remix memory first when you need historical intent.\",\n blocked: false,\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 Remix. Raw `git diff` is useful for exact patch detail, but Remix memory should be the default first read for understanding why a change happened or which historical step matters before inspecting raw diffs.\",\n blocked: false,\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 Remix. Raw `git rev-list` is commit-level history detail; use Remix memory first when the question is about reasoning, chronology, or related historical attempts rather than exact ancestry mechanics.\",\n blocked: false,\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 blocked: advisory.blocked,\n };\n }\n }\n return null;\n}\n\nfunction buildMemoryFirstMessage(state: Awaited<ReturnType<typeof loadPendingTurnState>>): string | null {\n if (!state) {\n return \"Use `remix_collab_memory_summary`, `remix_collab_memory_search`, or `remix_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 Remix collaboration-state work. Start with `remix_collab_status`, then use memory reads if you need related historical context before raw git history.\";\n }\n\n if (shouldPreferRemixMemory(state.intent) && !state.consultedMemory) {\n return \"This turn is classified as a Remix memory-first history question, and no memory tool has been used yet. Start with `remix_collab_memory_summary`, `remix_collab_memory_search`, or `remix_collab_memory_timeline`. Only use `remix_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 Remix memory before raw git history in a bound repo: start with `remix_collab_memory_summary`, `remix_collab_memory_search`, or `remix_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 = extractToolCwd(payload) ?? 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 advisory.blocked ? \"Remix guardrail:\" : \"Remix advisory:\",\n `Detected raw git ${advisory.subcommand} usage in a repo bound to Remix: ${command}`,\n advisory.message,\n ...(memoryFirstMessage ? [memoryFirstMessage] : []),\n ].join(\"\\n\"),\n );\n if (advisory.blocked) {\n process.exitCode = 2;\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":["path","os","fs","import_promises","import_node_path","path","fs"]}
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 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\";\nexport type ManualRecordingScope = \"change_step\" | \"full_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 manualRecordingScope: ManualRecordingScope | null;\n manualRemoteChangeRecordedAt: 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\nexport type PendingTouchedRepoSummary = {\n repoRoot: string;\n projectId: string | null;\n currentAppId: string | null;\n upstreamAppId: string | null;\n lastTouchedAt: string;\n lastObservedWriteAt: string | null;\n touchedBy: string[];\n hasObservedWrite: boolean;\n manuallyRecorded: boolean;\n manualRecordingScope: ManualRecordingScope | null;\n stopAttempted: boolean;\n stopRecorded: boolean;\n stopRecordedMode: RepoRecordMode | null;\n recordingFailureMessage: string | null;\n recordingFailureHint: string | null;\n recordingFailedAt: string | null;\n};\n\nexport type PendingTurnStateSummary = {\n sessionId: string;\n turnId: string;\n initialCwd: string | null;\n submittedAt: string;\n consultedMemory: boolean;\n promptLength: number;\n touchedRepoCount: number;\n turnFailureMessage: string | null;\n turnFailureHint: string | null;\n turnFailedAt: string | null;\n touchedRepos: PendingTouchedRepoSummary[];\n};\n\nfunction stateRoot(): string {\n const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_STATE_ROOT?.trim();\n return configured || 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 await fs.mkdir(stateRoot(), { recursive: true });\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 manualRecordingScope:\n parsed.manualRecordingScope === \"change_step\" || parsed.manualRecordingScope === \"full_turn\"\n ? parsed.manualRecordingScope\n : null,\n manualRemoteChangeRecordedAt: normalizeString(parsed.manualRemoteChangeRecordedAt),\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 manualRecordingScope: null,\n manualRemoteChangeRecordedAt: 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\nfunction summarizeTouchedRepo(repo: TouchedRepoState): PendingTouchedRepoSummary {\n return {\n repoRoot: repo.repoRoot,\n projectId: repo.projectId,\n currentAppId: repo.currentAppId,\n upstreamAppId: repo.upstreamAppId,\n lastTouchedAt: repo.lastTouchedAt,\n lastObservedWriteAt: repo.lastObservedWriteAt,\n touchedBy: [...repo.touchedBy],\n hasObservedWrite: repo.hasObservedWrite,\n manuallyRecorded: repo.manuallyRecorded,\n manualRecordingScope: repo.manualRecordingScope,\n stopAttempted: repo.stopAttempted,\n stopRecorded: repo.stopRecorded,\n stopRecordedMode: repo.stopRecordedMode,\n recordingFailureMessage: repo.recordingFailureMessage,\n recordingFailureHint: repo.recordingFailureHint,\n recordingFailedAt: repo.recordingFailedAt,\n };\n}\n\nfunction summarizePendingTurnState(state: PendingTurnState): PendingTurnStateSummary {\n const touchedRepos = Object.values(state.touchedRepos)\n .sort((a, b) => a.repoRoot.localeCompare(b.repoRoot))\n .map((repo) => summarizeTouchedRepo(repo));\n return {\n sessionId: state.sessionId,\n turnId: state.turnId,\n initialCwd: state.initialCwd,\n submittedAt: state.submittedAt,\n consultedMemory: state.consultedMemory,\n promptLength: state.prompt.length,\n touchedRepoCount: touchedRepos.length,\n turnFailureMessage: state.turnFailureMessage,\n turnFailureHint: state.turnFailureHint,\n turnFailedAt: state.turnFailedAt,\n touchedRepos,\n };\n}\n\nexport function getPendingTurnStateRootPath(): string {\n return stateRoot();\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; scope?: ManualRecordingScope | null; remoteChangeRecorded?: boolean },\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.manualRecordingScope = params?.scope ?? current.manualRecordingScope;\n if (params?.remoteChangeRecorded) {\n current.manualRemoteChangeRecordedAt = current.manuallyRecordedAt;\n }\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 listPendingTurnStateSummaries(): Promise<PendingTurnStateSummary[]> {\n const root = stateRoot();\n const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []);\n const sessionIds = entries\n .filter((entry) => entry.isFile() && entry.name.endsWith(\".json\"))\n .map((entry) => entry.name.replace(/\\.json$/, \"\"))\n .sort((a, b) => a.localeCompare(b));\n const states = await Promise.all(sessionIds.map((sessionId) => loadPendingTurnState(sessionId)));\n return states\n .filter((state): state is PendingTurnState => state !== null)\n .sort((a, b) => b.submittedAt.localeCompare(a.submittedAt))\n .map((state) => summarizePendingTurnState(state));\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\nfunction extractToolStructuredData(payload: Record<string, unknown>): Record<string, unknown> | null {\n const toolResponse = extractToolResponse(payload);\n const structuredContent = getNestedRecord(toolResponse?.structuredContent) ?? getNestedRecord(payload.structuredContent);\n return (\n getNestedRecord(toolResponse?.data) ??\n getNestedRecord(structuredContent?.data) ??\n structuredContent\n );\n}\n\nexport function extractToolName(payload: Record<string, unknown>): string | null {\n return extractString(payload, [\"tool_name\", \"toolName\"]);\n}\n\nexport function normalizeHookToolName(toolName: string | null): string | null {\n if (!toolName) return null;\n const trimmed = toolName.trim();\n if (!trimmed) return null;\n\n const remixToolIndex = trimmed.toLowerCase().indexOf(\"remix_collab_\");\n if (remixToolIndex >= 0) {\n return trimmed.slice(remixToolIndex);\n }\n\n return trimmed;\n}\n\nexport function extractAssistantResponse(payload: Record<string, unknown>): string | null {\n const candidateKeys = [\n \"last_assistant_message\",\n \"lastAssistantMessage\",\n \"assistant_response\",\n \"assistantResponse\",\n \"assistant_message\",\n \"assistantMessage\",\n \"response\",\n \"message\",\n ];\n\n return (\n extractString(payload, candidateKeys) ??\n extractString(extractToolResponse(payload) ?? {}, candidateKeys) ??\n extractString(extractToolStructuredData(payload) ?? {}, candidateKeys) ??\n extractString(extractToolInput(payload), candidateKeys)\n );\n}\n\nexport function extractFinalizeTurnMode(payload: Record<string, unknown>): \"changed_turn\" | \"no_diff_turn\" | null {\n const mode =\n extractString(extractToolStructuredData(payload) ?? {}, [\"mode\"]) ??\n extractString(extractToolResponse(payload) ?? {}, [\"mode\"]) ??\n extractString(payload, [\"mode\"]);\n if (mode === \"changed_turn\" || mode === \"no_diff_turn\") {\n return mode;\n }\n return null;\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 extractBashCommand(payload: Record<string, unknown>): string | null {\n const toolInput = extractToolInput(payload);\n return extractString(toolInput, [\"command\", \"cmd\", \"bash_command\", \"bashCommand\"]);\n}\n\nexport function extractToolErrorMessage(payload: Record<string, unknown>): string | null {\n const toolResponse = extractToolResponse(payload);\n const toolError = getNestedRecord(toolResponse?.error) ?? getNestedRecord(payload.error);\n return (\n extractString(toolError ?? {}, [\"message\"]) ??\n extractString(toolResponse ?? {}, [\"errorMessage\", \"message\"]) ??\n extractString(payload, [\"errorMessage\"])\n );\n}\n\nfunction extractToolStatus(payload: Record<string, unknown>): string | null {\n const toolResponse = extractToolResponse(payload);\n return (\n extractString(toolResponse ?? {}, [\"status\", \"state\"]) ??\n extractString(payload, [\"status\", \"state\"])\n );\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\n if (extractToolErrorMessage(payload)) {\n return false;\n }\n\n const status = extractToolStatus(payload)?.toLowerCase();\n if (status === \"error\" || status === \"failed\" || status === \"failure\") {\n return false;\n }\n\n const hookEventName = extractString(payload, [\"hook_event_name\", \"hookEventName\"]);\n return hookEventName === \"PostToolUse\";\n}\n\nexport function isRemoteChangeRecordedButLocalSyncFailed(payload: Record<string, unknown>): boolean {\n return extractToolErrorMessage(payload) === \"Change step succeeded remotely, but automatic local sync failed.\";\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 { shouldPreferRemixMemory } from \"./history-routing.js\";\nimport { loadPendingTurnState } from \"./hook-state.js\";\nimport { extractString, extractToolCwd, extractToolInput, findBoundRepo, readJsonStdin } from \"./hook-utils.js\";\n\ntype GitAdvisory = {\n subcommand: string;\n category: \"history_read\" | \"mutation\";\n message: string;\n blocked: boolean;\n};\n\nconst GIT_ADVISORIES: Array<{ subcommand: string; re: RegExp; category: \"history_read\" | \"mutation\"; message: string; blocked: boolean }> = [\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 Remix. Raw `git commit` is blocked for ordinary collaboration work because it bypasses Remix turn recording. You must record the completed turn with `remix_collab_finalize_turn` before ending the response.\",\n blocked: true,\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 Remix. Raw `git push` is blocked for ordinary collaboration work because publishing Remix collaboration state must happen through Remix recording, sync, and merge-request flows rather than direct branch pushes.\",\n blocked: true,\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 Remix. Raw `git pull` is blocked because Remix-managed repo alignment must go through `remix_collab_status`, `remix_collab_sync_preview` / `remix_collab_sync_apply`, or reconcile and upstream-sync flows when history diverges.\",\n blocked: true,\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 Remix. Raw `git merge` is blocked because Remix work must be merged through Remix merge requests rather than local branch merges.\",\n blocked: true,\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 Remix. Raw `git rebase` is blocked because it rewrites local history outside Remix reconcile and sync controls. Use `remix_collab_status` first, then follow the sync or reconcile path.\",\n blocked: true,\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 Remix. Raw `git reset` is blocked because it can discard or rewrite local state outside Remix recovery flows. Use `remix_collab_status` first, then use Remix sync or reconcile flows to realign state safely.\",\n blocked: true,\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 Remix. Raw `git log` is a fallback for exact commit history, not the default starting point for historical reasoning. Use Remix memory first when the goal is to understand intent, prior prompts, failed attempts, or decision trail context.\",\n blocked: false,\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 Remix. Raw `git show` is useful for exact patch inspection, but historical reasoning should start from Remix memory so you can identify the right change step or merge context before expanding into raw diffs.\",\n blocked: false,\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 Remix. Raw `git blame` can identify line ownership, but it does not explain the user ask, reasoning, or failed attempts behind a change. Use Remix memory first when you need historical intent.\",\n blocked: false,\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 Remix. Raw `git diff` is useful for exact patch detail, but Remix memory should be the default first read for understanding why a change happened or which historical step matters before inspecting raw diffs.\",\n blocked: false,\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 Remix. Raw `git rev-list` is commit-level history detail; use Remix memory first when the question is about reasoning, chronology, or related historical attempts rather than exact ancestry mechanics.\",\n blocked: false,\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 blocked: advisory.blocked,\n };\n }\n }\n return null;\n}\n\nfunction buildMemoryFirstMessage(state: Awaited<ReturnType<typeof loadPendingTurnState>>): string | null {\n if (!state) {\n return \"Use `remix_collab_memory_summary`, `remix_collab_memory_search`, or `remix_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 Remix collaboration-state work. Start with `remix_collab_status`, then use memory reads if you need related historical context before raw git history.\";\n }\n\n if (shouldPreferRemixMemory(state.intent) && !state.consultedMemory) {\n return \"This turn is classified as a Remix memory-first history question, and no memory tool has been used yet. Start with `remix_collab_memory_summary`, `remix_collab_memory_search`, or `remix_collab_memory_timeline`. Only use `remix_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 Remix memory before raw git history in a bound repo: start with `remix_collab_memory_summary`, `remix_collab_memory_search`, or `remix_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 = extractToolCwd(payload) ?? 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 advisory.blocked ? \"Remix guardrail:\" : \"Remix advisory:\",\n `Detected raw git ${advisory.subcommand} usage in a repo bound to Remix: ${command}`,\n advisory.message,\n ...(memoryFirstMessage ? [memoryFirstMessage] : []),\n ].join(\"\\n\"),\n );\n if (advisory.blocked) {\n process.exitCode = 2;\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":["path","os","fs","import_promises","import_node_path","path","fs"]}
@@ -553,7 +553,7 @@ var RemixError = class extends Error {
553
553
  }
554
554
  };
555
555
 
556
- // node_modules/@remixhq/core/dist/chunk-FAZUMWBS.js
556
+ // node_modules/@remixhq/core/dist/chunk-GEHSFPCD.js
557
557
  var import_promises = __toESM(require("fs/promises"), 1);
558
558
  var import_path = __toESM(require("path"), 1);
559
559
  function getCollabBindingPath(repoRoot) {
@@ -590,7 +590,7 @@ var REMIX_ERROR_CODES = {
590
590
  PREFERRED_BRANCH_MISMATCH: "PREFERRED_BRANCH_MISMATCH"
591
591
  };
592
592
 
593
- // node_modules/@remixhq/core/dist/chunk-RREREIGW.js
593
+ // node_modules/@remixhq/core/dist/chunk-J3J4PBQ7.js
594
594
  var import_promises13 = __toESM(require("fs/promises"), 1);
595
595
  var import_crypto = require("crypto");
596
596
  var import_os = __toESM(require("os"), 1);
@@ -7371,7 +7371,7 @@ var {
7371
7371
  getCancelSignal: getCancelSignal2
7372
7372
  } = getIpcExport();
7373
7373
 
7374
- // node_modules/@remixhq/core/dist/chunk-RREREIGW.js
7374
+ // node_modules/@remixhq/core/dist/chunk-J3J4PBQ7.js
7375
7375
  async function runGit(args, cwd) {
7376
7376
  const res = await execa("git", args, { cwd, stderr: "ignore" });
7377
7377
  return String(res.stdout || "").trim();
@@ -7887,6 +7887,25 @@ function formatCliErrorDetail(err) {
7887
7887
  }
7888
7888
  return typeof err === "string" && err.trim() ? err.trim() : null;
7889
7889
  }
7890
+ async function pollAppReady(api, appId) {
7891
+ const started = Date.now();
7892
+ let delay = 2e3;
7893
+ while (Date.now() - started < 20 * 60 * 1e3) {
7894
+ const appResp = await api.getApp(appId);
7895
+ const app = unwrapResponseObject(appResp, "app");
7896
+ const status = typeof app.status === "string" ? app.status : "";
7897
+ if (status === "ready") return app;
7898
+ if (status === "error") {
7899
+ throw new RemixError("App is in error state.", {
7900
+ exitCode: 1,
7901
+ hint: typeof app.statusError === "string" ? app.statusError : null
7902
+ });
7903
+ }
7904
+ await sleep(delay);
7905
+ delay = Math.min(1e4, Math.floor(delay * 1.4));
7906
+ }
7907
+ throw new RemixError("Timed out waiting for app to become ready.", { exitCode: 1 });
7908
+ }
7890
7909
  async function pollChangeStep(api, appId, changeStepId) {
7891
7910
  const started = Date.now();
7892
7911
  let delay = 1500;
@@ -8708,6 +8727,7 @@ async function collabAdd(params) {
8708
8727
  }
8709
8728
  const { backupPath } = await writeTempUnifiedDiffBackup(diff, "remix-add");
8710
8729
  try {
8730
+ await pollAppReady(params.api, binding.currentAppId);
8711
8731
  if (submissionSnapshot) {
8712
8732
  await assertRepoSnapshotUnchanged(repoRoot, submissionSnapshot, {
8713
8733
  operation: "`remix collab add` auto-sync",
@@ -8853,7 +8873,7 @@ async function collabRecordTurn(params) {
8853
8873
  return unwrapResponseObject(resp, "collab turn");
8854
8874
  }
8855
8875
 
8856
- // node_modules/@remixhq/core/dist/chunk-4276ARDF.js
8876
+ // node_modules/@remixhq/core/dist/chunk-B5S3PUIR.js
8857
8877
  async function readJsonSafe(res) {
8858
8878
  const ct = res.headers.get("content-type") ?? "";
8859
8879
  if (!ct.toLowerCase().includes("application/json")) return null;
@@ -8967,11 +8987,33 @@ function createApiClient(config, opts) {
8967
8987
  const qs = new URLSearchParams();
8968
8988
  if (params?.projectId) qs.set("projectId", params.projectId);
8969
8989
  if (params?.organizationId) qs.set("organizationId", params.organizationId);
8990
+ if (params?.ownership) qs.set("ownership", params.ownership);
8991
+ if (params?.accessScope) qs.set("accessScope", params.accessScope);
8992
+ if (params?.createdBy) qs.set("createdBy", params.createdBy);
8970
8993
  if (params?.forked) qs.set("forked", params.forked);
8994
+ if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
8995
+ if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
8971
8996
  const suffix = qs.toString() ? `?${qs.toString()}` : "";
8972
8997
  return request(`/v1/apps${suffix}`, { method: "GET" });
8973
8998
  },
8974
8999
  getApp: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}`, { method: "GET" }),
9000
+ getAppContext: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/context`, { method: "GET" }),
9001
+ getAppOverview: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/overview`, { method: "GET" }),
9002
+ listAppTimeline: (appId, params) => {
9003
+ const qs = new URLSearchParams();
9004
+ if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
9005
+ if (params?.cursor) qs.set("cursor", params.cursor);
9006
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
9007
+ return request(`/v1/apps/${encodeURIComponent(appId)}/timeline${suffix}`, { method: "GET" });
9008
+ },
9009
+ getAppTimelineEvent: (appId, eventId) => request(`/v1/apps/${encodeURIComponent(appId)}/timeline/${encodeURIComponent(eventId)}`, { method: "GET" }),
9010
+ listAppEditQueue: (appId, params) => {
9011
+ const qs = new URLSearchParams();
9012
+ if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
9013
+ if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
9014
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
9015
+ return request(`/v1/apps/${encodeURIComponent(appId)}/edit-queue${suffix}`, { method: "GET" });
9016
+ },
8975
9017
  getMergeRequest: (mrId) => request(`/v1/merge-requests/${encodeURIComponent(mrId)}`, { method: "GET" }),
8976
9018
  presignImportUpload: (payload) => request("/v1/apps/import/upload/presign", { method: "POST", body: JSON.stringify(payload) }),
8977
9019
  importFromUpload: (payload) => request("/v1/apps/import/upload", { method: "POST", body: JSON.stringify(payload) }),
@@ -9053,6 +9095,8 @@ function createApiClient(config, opts) {
9053
9095
  qs.set("status", params.status);
9054
9096
  }
9055
9097
  if (params?.kind) qs.set("kind", params.kind);
9098
+ if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
9099
+ if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
9056
9100
  const suffix = qs.toString() ? `?${qs.toString()}` : "";
9057
9101
  return request(`/v1/merge-requests${suffix}`, { method: "GET" });
9058
9102
  },
@@ -9071,24 +9115,60 @@ function createApiClient(config, opts) {
9071
9115
  method: "POST",
9072
9116
  body: JSON.stringify(payload)
9073
9117
  }),
9074
- listOrganizationMembers: (orgId) => request(`/v1/organizations/${encodeURIComponent(orgId)}/members`, { method: "GET" }),
9118
+ listOrganizationMembers: (orgId, params) => {
9119
+ const qs = new URLSearchParams();
9120
+ if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
9121
+ if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
9122
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
9123
+ return request(`/v1/organizations/${encodeURIComponent(orgId)}/members${suffix}`, { method: "GET" });
9124
+ },
9075
9125
  updateOrganizationMember: (orgId, userId, payload) => request(`/v1/organizations/${encodeURIComponent(orgId)}/members/${encodeURIComponent(userId)}`, {
9076
9126
  method: "PATCH",
9077
9127
  body: JSON.stringify(payload)
9078
9128
  }),
9079
- listProjectMembers: (projectId) => request(`/v1/projects/${encodeURIComponent(projectId)}/members`, { method: "GET" }),
9129
+ listProjectMembers: (projectId, params) => {
9130
+ const qs = new URLSearchParams();
9131
+ if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
9132
+ if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
9133
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
9134
+ return request(`/v1/projects/${encodeURIComponent(projectId)}/members${suffix}`, { method: "GET" });
9135
+ },
9080
9136
  updateProjectMember: (projectId, userId, payload) => request(`/v1/projects/${encodeURIComponent(projectId)}/members/${encodeURIComponent(userId)}`, {
9081
9137
  method: "PATCH",
9082
9138
  body: JSON.stringify(payload)
9083
9139
  }),
9084
- listAppMembers: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/members`, { method: "GET" }),
9140
+ listAppMembers: (appId, params) => {
9141
+ const qs = new URLSearchParams();
9142
+ if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
9143
+ if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
9144
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
9145
+ return request(`/v1/apps/${encodeURIComponent(appId)}/members${suffix}`, { method: "GET" });
9146
+ },
9085
9147
  updateAppMember: (appId, userId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/members/${encodeURIComponent(userId)}`, {
9086
9148
  method: "PATCH",
9087
9149
  body: JSON.stringify(payload)
9088
9150
  }),
9089
- listOrganizationInvites: (orgId) => request(`/v1/organizations/${encodeURIComponent(orgId)}/invitations`, { method: "GET" }),
9090
- listProjectInvites: (projectId) => request(`/v1/projects/${encodeURIComponent(projectId)}/invitations`, { method: "GET" }),
9091
- listAppInvites: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/invitations`, { method: "GET" }),
9151
+ listOrganizationInvites: (orgId, params) => {
9152
+ const qs = new URLSearchParams();
9153
+ if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
9154
+ if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
9155
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
9156
+ return request(`/v1/organizations/${encodeURIComponent(orgId)}/invitations${suffix}`, { method: "GET" });
9157
+ },
9158
+ listProjectInvites: (projectId, params) => {
9159
+ const qs = new URLSearchParams();
9160
+ if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
9161
+ if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
9162
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
9163
+ return request(`/v1/projects/${encodeURIComponent(projectId)}/invitations${suffix}`, { method: "GET" });
9164
+ },
9165
+ listAppInvites: (appId, params) => {
9166
+ const qs = new URLSearchParams();
9167
+ if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
9168
+ if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
9169
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
9170
+ return request(`/v1/apps/${encodeURIComponent(appId)}/invitations${suffix}`, { method: "GET" });
9171
+ },
9092
9172
  resendOrganizationInvite: (orgId, inviteId, payload) => request(`/v1/organizations/${encodeURIComponent(orgId)}/invitations/${encodeURIComponent(inviteId)}/resend`, {
9093
9173
  method: "POST",
9094
9174
  body: JSON.stringify(payload ?? {})
@@ -9110,6 +9190,7 @@ function createApiClient(config, opts) {
9110
9190
  revokeAppInvite: (appId, inviteId) => request(`/v1/apps/${encodeURIComponent(appId)}/invitations/${encodeURIComponent(inviteId)}`, {
9111
9191
  method: "DELETE"
9112
9192
  }),
9193
+ acceptInvitation: (payload) => request("/v1/invitations/accept", { method: "POST", body: JSON.stringify(payload) }),
9113
9194
  syncUpstreamApp: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/sync-upstream`, {
9114
9195
  method: "POST",
9115
9196
  body: JSON.stringify({})
@@ -9145,7 +9226,31 @@ function createApiClient(config, opts) {
9145
9226
  `/v1/apps/${encodeURIComponent(appId)}/bundles/${encodeURIComponent(bundleId)}/assets/download?${qs.toString()}`,
9146
9227
  { method: "GET" }
9147
9228
  );
9148
- }
9229
+ },
9230
+ listAgentRuns: (appId, params) => {
9231
+ const qs = new URLSearchParams();
9232
+ if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
9233
+ if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
9234
+ if (params?.status) qs.set("status", params.status);
9235
+ if (params?.currentPhase) qs.set("currentPhase", params.currentPhase);
9236
+ if (params?.createdAfter) qs.set("createdAfter", params.createdAfter);
9237
+ if (params?.createdBefore) qs.set("createdBefore", params.createdBefore);
9238
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
9239
+ return request(`/v1/apps/${encodeURIComponent(appId)}/agent-runs${suffix}`, { method: "GET" });
9240
+ },
9241
+ getAgentRun: (appId, runId) => request(`/v1/apps/${encodeURIComponent(appId)}/agent-runs/${encodeURIComponent(runId)}`, { method: "GET" }),
9242
+ listAgentRunEvents: (appId, runId, params) => {
9243
+ const qs = new URLSearchParams();
9244
+ if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
9245
+ if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
9246
+ if (params?.createdAfter) qs.set("createdAfter", params.createdAfter);
9247
+ if (params?.createdBefore) qs.set("createdBefore", params.createdBefore);
9248
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
9249
+ return request(`/v1/apps/${encodeURIComponent(appId)}/agent-runs/${encodeURIComponent(runId)}/events${suffix}`, {
9250
+ method: "GET"
9251
+ });
9252
+ },
9253
+ getSandboxStatus: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/sandbox/status`, { method: "GET" })
9149
9254
  };
9150
9255
  }
9151
9256
 
@@ -33420,7 +33525,7 @@ async function clearPendingTurnState(sessionId) {
33420
33525
  // package.json
33421
33526
  var package_default = {
33422
33527
  name: "@remixhq/claude-plugin",
33423
- version: "0.1.12",
33528
+ version: "0.1.15",
33424
33529
  description: "Claude Code plugin for Remix collaboration workflows",
33425
33530
  homepage: "https://github.com/RemixDotOne/remix-claude-plugin",
33426
33531
  license: "MIT",
@@ -33451,8 +33556,8 @@ var package_default = {
33451
33556
  prepack: "npm run build"
33452
33557
  },
33453
33558
  dependencies: {
33454
- "@remixhq/core": "^0.1.8",
33455
- "@remixhq/mcp": "^0.1.8"
33559
+ "@remixhq/core": "^0.1.10",
33560
+ "@remixhq/mcp": "^0.1.10"
33456
33561
  },
33457
33562
  devDependencies: {
33458
33563
  "@types/node": "^25.4.0",
@@ -33578,6 +33683,11 @@ function extractToolInput(payload) {
33578
33683
  function extractToolResponse(payload) {
33579
33684
  return getNestedRecord(payload.tool_response) ?? getNestedRecord(payload.toolResponse);
33580
33685
  }
33686
+ function extractToolStructuredData(payload) {
33687
+ const toolResponse = extractToolResponse(payload);
33688
+ const structuredContent = getNestedRecord(toolResponse?.structuredContent) ?? getNestedRecord(payload.structuredContent);
33689
+ return getNestedRecord(toolResponse?.data) ?? getNestedRecord(structuredContent?.data) ?? structuredContent;
33690
+ }
33581
33691
  function extractAssistantResponse(payload) {
33582
33692
  const candidateKeys = [
33583
33693
  "last_assistant_message",
@@ -33589,7 +33699,7 @@ function extractAssistantResponse(payload) {
33589
33699
  "response",
33590
33700
  "message"
33591
33701
  ];
33592
- return extractString(payload, candidateKeys) ?? extractString(extractToolResponse(payload) ?? {}, candidateKeys) ?? extractString(extractToolInput(payload), candidateKeys);
33702
+ return extractString(payload, candidateKeys) ?? extractString(extractToolResponse(payload) ?? {}, candidateKeys) ?? extractString(extractToolStructuredData(payload) ?? {}, candidateKeys) ?? extractString(extractToolInput(payload), candidateKeys);
33593
33703
  }
33594
33704
  function extractString(input, keys) {
33595
33705
  for (const key of keys) {
@@ -33649,52 +33759,52 @@ function getErrorDetails(error) {
33649
33759
  if (error instanceof Error) {
33650
33760
  const hint = typeof error.hint === "string" ? String(error.hint) : null;
33651
33761
  return {
33652
- message: error.message || "Automatic Remix turn recording failed.",
33762
+ message: error.message || "Fallback Remix turn recording failed.",
33653
33763
  hint
33654
33764
  };
33655
33765
  }
33656
- const message = typeof error === "string" && error.trim() ? error.trim() : "Automatic Remix turn recording failed.";
33766
+ const message = typeof error === "string" && error.trim() ? error.trim() : "Fallback Remix turn recording failed.";
33657
33767
  return { message, hint: null };
33658
33768
  }
33659
33769
  function getRecordingBlockedMessage(status, repoRoot) {
33660
33770
  switch (status.status) {
33661
33771
  case "not_git_repo":
33662
33772
  return {
33663
- message: "Automatic Remix turn recording failed because the repository is no longer inside a git repository.",
33773
+ message: "Fallback Remix turn recording failed because the repository is no longer inside a git repository.",
33664
33774
  hint: status.hint || `Repo root: ${repoRoot}`
33665
33775
  };
33666
33776
  case "not_bound":
33667
33777
  return {
33668
- message: "Automatic Remix turn recording failed because the repository is no longer bound to Remix.",
33778
+ message: "Fallback Remix turn recording failed because the repository is no longer bound to Remix.",
33669
33779
  hint: status.hint || `Repo root: ${repoRoot}`
33670
33780
  };
33671
33781
  case "missing_head":
33672
33782
  return {
33673
- message: "Automatic Remix turn recording failed because the repository HEAD could not be resolved.",
33783
+ message: "Fallback Remix turn recording failed because the repository HEAD could not be resolved.",
33674
33784
  hint: status.hint || `Repo root: ${repoRoot}`
33675
33785
  };
33676
33786
  case "branch_mismatch":
33677
33787
  return {
33678
- message: "Automatic Remix turn recording was blocked by the checkout's preferred-branch policy.",
33788
+ message: "Fallback Remix turn recording was blocked by the checkout's preferred-branch policy.",
33679
33789
  hint: status.hint || `Repo root: ${repoRoot}`
33680
33790
  };
33681
33791
  case "metadata_conflict":
33682
33792
  return {
33683
- message: "Automatic Remix turn recording was blocked because local repository metadata conflicts with the bound Remix app.",
33793
+ message: "Fallback Remix turn recording was blocked because local repository metadata conflicts with the bound Remix app.",
33684
33794
  hint: status.hint || `Repo root: ${repoRoot}`
33685
33795
  };
33686
33796
  case "reconcile_required":
33687
33797
  return {
33688
- message: "Automatic Remix turn recording was blocked because the repository must be reconciled before recording can continue safely.",
33798
+ message: "Fallback Remix turn recording was blocked because the repository must be reconciled before recording can continue safely.",
33689
33799
  hint: status.hint || `Repo root: ${repoRoot}`
33690
33800
  };
33691
33801
  default:
33692
33802
  return null;
33693
33803
  }
33694
33804
  }
33695
- function buildRepoIdempotencyKey(turnId, repo, mode) {
33805
+ function buildRepoIdempotencyKey(turnId, repo) {
33696
33806
  const repoToken = repo.currentAppId?.trim() || repo.repoRoot;
33697
- return `${turnId}:${repoToken}:${mode}`;
33807
+ return `${turnId}:${repoToken}:finalize_turn`;
33698
33808
  }
33699
33809
  function shouldSkipStopRecording(repo) {
33700
33810
  if (repo.stopRecorded) {
@@ -33766,7 +33876,7 @@ async function recordTouchedRepo(params) {
33766
33876
  const binding = await readCollabBinding(repo.repoRoot).catch(() => null);
33767
33877
  if (!binding) {
33768
33878
  await markTouchedRepoRecordingFailure(sessionId, repo.repoRoot, {
33769
- message: "Automatic Remix turn recording failed because the repository is no longer bound to Remix.",
33879
+ message: "Fallback Remix turn recording failed because the repository is no longer bound to Remix.",
33770
33880
  hint: `Repo root: ${repo.repoRoot}`
33771
33881
  });
33772
33882
  await appendHookDiagnosticsEvent({
@@ -33835,7 +33945,7 @@ async function recordTouchedRepo(params) {
33835
33945
  prompt,
33836
33946
  assistantResponse,
33837
33947
  diffSource: "worktree",
33838
- idempotencyKey: buildRepoIdempotencyKey(turnId, repo, "changed_turn"),
33948
+ idempotencyKey: buildRepoIdempotencyKey(turnId, repo),
33839
33949
  actor: HOOK_ACTOR
33840
33950
  });
33841
33951
  await markTouchedRepoStopRecorded(sessionId, repo.repoRoot, { mode: "changed_turn" });
@@ -33889,7 +33999,7 @@ async function recordTouchedRepo(params) {
33889
33999
  cwd: repo.repoRoot,
33890
34000
  prompt,
33891
34001
  assistantResponse,
33892
- idempotencyKey: buildRepoIdempotencyKey(turnId, repo, "no_diff_turn"),
34002
+ idempotencyKey: buildRepoIdempotencyKey(turnId, repo),
33893
34003
  actor: HOOK_ACTOR
33894
34004
  });
33895
34005
  await markTouchedRepoStopRecorded(sessionId, repo.repoRoot, { mode: "no_diff_turn" });
@@ -34044,7 +34154,7 @@ async function runHookStopCollab(payload) {
34044
34154
  });
34045
34155
  if (!prompt || !assistantResponse) {
34046
34156
  await markPendingTurnFailure(sessionId, {
34047
- message: "Automatic Remix turn recording failed because the prompt or assistant response was missing."
34157
+ message: "Fallback Remix turn recording failed because the prompt or assistant response was missing."
34048
34158
  });
34049
34159
  await appendHookDiagnosticsEvent({
34050
34160
  hook,