@remixhq/claude-plugin 0.1.11 → 0.1.14

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,7 +1,7 @@
1
1
  {
2
2
  "name": "remix",
3
3
  "description": "Remix collaboration workflows for Claude Code",
4
- "version": "0.1.11",
4
+ "version": "0.1.14",
5
5
  "author": {
6
6
  "name": "Remix"
7
7
  },
@@ -22,10 +22,10 @@ Operating rules:
22
22
  - use `remix_collab_memory_change_step_diff` only after you have identified a specific `changeStepId`.
23
23
  8. Use raw git for historical reads only after Remix memory has narrowed the relevant change, or when the user explicitly asks for exact commit, blame, ancestry, or raw patch details.
24
24
  9. Clearly explain local mutation risk before using tools that can modify the local repo.
25
- 10. Assume the installed hook is the normal automatic recording path for completed assistant turns in a bound repo: changed turn => `remix_collab_add`, no-diff turn => `remix_collab_record_turn`.
26
- 11. Do not proactively call `remix_collab_add` or `remix_collab_record_turn` during normal work. The hook should do the automatic per-turn recording unless recovery is required.
27
- 12. Only use manual `remix_collab_add` or `remix_collab_record_turn` when the user explicitly asks for it, or when doing operational recovery, backfills, or debugging after automatic recording could not complete safely.
28
- 13. When you must choose manually, treat `remix_collab_add` or the clearer alias `remix_collab_add_change_step` as the code-diff tool, and treat `remix_collab_record_turn` or `remix_collab_record_no_diff_turn` as no-diff prompt/response history only.
25
+ 10. In a bound repo, Remix MCP tools are the required workflow layer for ordinary collaboration work.
26
+ 11. In a bound repo, exactly one final `remix_collab_finalize_turn` is required before the final user-facing response.
27
+ 12. The final recording call must use the exact user prompt and your final assistant response.
28
+ 13. Do not finish the turn before recording. Do not make additional repo mutations after the final turn-recording call unless you intend to record again.
29
29
  14. Do not duplicate core business logic in reasoning. Use the MCP tools to inspect and execute the workflow.
30
30
 
31
31
  When appropriate:
@@ -35,7 +35,6 @@ When appropriate:
35
35
  - use `remix_collab_checkout` to continue work on an existing app id without creating a fork
36
36
  - use memory summary/search/timeline tools before repo inspection when historical context or reasoning is needed
37
37
  - use the explicit change-step diff tool only after you already know which `changeStepId` matters
38
- - use `remix_collab_add` only when a manual changed-turn recording step is explicitly needed
39
- - use `remix_collab_record_turn` only when a manual no-diff turn recording step is explicitly needed
38
+ - use `remix_collab_finalize_turn` as the final turn recorder
40
39
  - use `remix_collab_review_queue` for reviewable merge requests, `remix_collab_my_merge_requests` for authored requests, and `remix_collab_list_app_merge_requests` for app-scoped MR flows with required `queue` set to `app_reviewable`, `app_outgoing`, or `app_related_visible`
41
40
  - use merge request tools for review and approval flows
@@ -6,6 +6,10 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
9
13
  var __copyProps = (to, from, except, desc) => {
10
14
  if (from && typeof from === "object" || typeof from === "function") {
11
15
  for (let key of __getOwnPropNames(from))
@@ -22,6 +26,20 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
26
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
27
  mod
24
28
  ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/hook-post-collab.ts
32
+ var hook_post_collab_exports = {};
33
+ __export(hook_post_collab_exports, {
34
+ runHookPostCollab: () => runHookPostCollab
35
+ });
36
+ module.exports = __toCommonJS(hook_post_collab_exports);
37
+
38
+ // src/hook-diagnostics.ts
39
+ var import_node_crypto2 = require("crypto");
40
+ var import_promises2 = __toESM(require("fs/promises"), 1);
41
+ var import_node_os2 = __toESM(require("os"), 1);
42
+ var import_node_path2 = __toESM(require("path"), 1);
25
43
 
26
44
  // src/hook-state.ts
27
45
  var import_promises = __toESM(require("fs/promises"), 1);
@@ -29,7 +47,8 @@ var import_node_os = __toESM(require("os"), 1);
29
47
  var import_node_path = __toESM(require("path"), 1);
30
48
  var import_node_crypto = require("crypto");
31
49
  function stateRoot() {
32
- return import_node_path.default.join(import_node_os.default.tmpdir(), "remix-claude-plugin-hooks");
50
+ const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_STATE_ROOT?.trim();
51
+ return configured || import_node_path.default.join(import_node_os.default.tmpdir(), "remix-claude-plugin-hooks");
33
52
  }
34
53
  function statePath(sessionId) {
35
54
  return import_node_path.default.join(stateRoot(), `${sessionId}.json`);
@@ -94,6 +113,7 @@ async function tryRemoveStaleStateLock(sessionId) {
94
113
  async function acquireStateLock(sessionId) {
95
114
  const lockPath = stateLockPath(sessionId);
96
115
  const deadline = Date.now() + STATE_LOCK_WAIT_MS;
116
+ await import_promises.default.mkdir(stateRoot(), { recursive: true });
97
117
  while (true) {
98
118
  try {
99
119
  await import_promises.default.mkdir(lockPath);
@@ -328,19 +348,139 @@ async function markPendingTurnConsultedMemory(sessionId) {
328
348
  });
329
349
  }
330
350
 
351
+ // package.json
352
+ var package_default = {
353
+ name: "@remixhq/claude-plugin",
354
+ version: "0.1.14",
355
+ description: "Claude Code plugin for Remix collaboration workflows",
356
+ homepage: "https://github.com/RemixDotOne/remix-claude-plugin",
357
+ license: "MIT",
358
+ repository: {
359
+ type: "git",
360
+ url: "https://github.com/RemixDotOne/remix-claude-plugin.git"
361
+ },
362
+ type: "module",
363
+ engines: {
364
+ node: ">=20"
365
+ },
366
+ publishConfig: {
367
+ access: "public"
368
+ },
369
+ files: [
370
+ "dist",
371
+ ".claude-plugin/plugin.json",
372
+ ".mcp.json",
373
+ "skills",
374
+ "hooks",
375
+ "agents"
376
+ ],
377
+ scripts: {
378
+ build: "tsup",
379
+ postbuild: `node -e "const fs=require('node:fs'); for (const p of ['dist/mcp-server.cjs','dist/hook-pre-git.cjs','dist/hook-user-prompt.cjs','dist/hook-post-collab.cjs','dist/hook-stop-collab.cjs']) fs.chmodSync(p, 0o755);"`,
380
+ dev: "tsx src/mcp-server.ts",
381
+ typecheck: "tsc -p tsconfig.json --noEmit",
382
+ prepack: "npm run build"
383
+ },
384
+ dependencies: {
385
+ "@remixhq/core": "^0.1.9",
386
+ "@remixhq/mcp": "^0.1.9"
387
+ },
388
+ devDependencies: {
389
+ "@types/node": "^25.4.0",
390
+ tsup: "^8.5.1",
391
+ tsx: "^4.21.0",
392
+ typescript: "^5.9.3"
393
+ }
394
+ };
395
+
396
+ // src/metadata.ts
397
+ var pluginMetadata = {
398
+ name: package_default.name,
399
+ version: package_default.version,
400
+ description: package_default.description,
401
+ pluginId: "remix",
402
+ agentName: "remix-collab"
403
+ };
404
+
405
+ // src/hook-diagnostics.ts
406
+ var MAX_LOG_BYTES = 512 * 1024;
407
+ function resolveClaudeRoot() {
408
+ const configured = process.env.CLAUDE_CONFIG_DIR?.trim();
409
+ return configured || import_node_path2.default.join(import_node_os2.default.homedir(), ".claude");
410
+ }
411
+ function resolvePluginDataDirName() {
412
+ return `${pluginMetadata.pluginId}-${pluginMetadata.pluginId}`;
413
+ }
414
+ function getHookDiagnosticsDirPath() {
415
+ const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_DIAGNOSTICS_DIR?.trim();
416
+ return configured || import_node_path2.default.join(resolveClaudeRoot(), "plugins", "data", resolvePluginDataDirName());
417
+ }
418
+ function getHookDiagnosticsLogPath() {
419
+ return import_node_path2.default.join(getHookDiagnosticsDirPath(), "hooks.ndjson");
420
+ }
421
+ function toFieldValue(value) {
422
+ if (value === null) return null;
423
+ if (typeof value === "string") return value;
424
+ if (typeof value === "number" && Number.isFinite(value)) return value;
425
+ if (typeof value === "boolean") return value;
426
+ return void 0;
427
+ }
428
+ function normalizeFields(fields) {
429
+ if (!fields) return {};
430
+ const normalizedEntries = Object.entries(fields).map(([key, value]) => {
431
+ const normalized = toFieldValue(value);
432
+ return normalized === void 0 ? null : [key, normalized];
433
+ }).filter((entry) => entry !== null);
434
+ return Object.fromEntries(normalizedEntries);
435
+ }
436
+ async function rotateLogIfNeeded(logPath) {
437
+ const stat = await import_promises2.default.stat(logPath).catch(() => null);
438
+ if (!stat || stat.size < MAX_LOG_BYTES) {
439
+ return;
440
+ }
441
+ const rotatedPath = `${logPath}.1`;
442
+ await import_promises2.default.rm(rotatedPath, { force: true }).catch(() => void 0);
443
+ await import_promises2.default.rename(logPath, rotatedPath).catch(() => void 0);
444
+ }
445
+ async function appendHookDiagnosticsEvent(params) {
446
+ try {
447
+ const logPath = getHookDiagnosticsLogPath();
448
+ await import_promises2.default.mkdir(import_node_path2.default.dirname(logPath), { recursive: true });
449
+ await rotateLogIfNeeded(logPath);
450
+ const event = {
451
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
452
+ hook: params.hook,
453
+ pluginVersion: pluginMetadata.version,
454
+ pid: process.pid,
455
+ sessionId: params.sessionId?.trim() || null,
456
+ turnId: params.turnId?.trim() || null,
457
+ stage: params.stage.trim(),
458
+ result: params.result,
459
+ reason: params.reason?.trim() || null,
460
+ toolName: params.toolName?.trim() || null,
461
+ repoRoot: params.repoRoot?.trim() || null,
462
+ message: params.message?.trim() || null,
463
+ fields: normalizeFields(params.fields)
464
+ };
465
+ await import_promises2.default.appendFile(logPath, `${JSON.stringify(event)}
466
+ `, "utf8");
467
+ } catch {
468
+ }
469
+ }
470
+
331
471
  // src/hook-utils.ts
332
- var import_promises3 = __toESM(require("fs/promises"), 1);
333
- var import_node_path2 = __toESM(require("path"), 1);
472
+ var import_promises4 = __toESM(require("fs/promises"), 1);
473
+ var import_node_path3 = __toESM(require("path"), 1);
334
474
 
335
- // node_modules/@remixhq/core/dist/chunk-FAZUMWBS.js
336
- var import_promises2 = __toESM(require("fs/promises"), 1);
475
+ // node_modules/@remixhq/core/dist/chunk-GEHSFPCD.js
476
+ var import_promises3 = __toESM(require("fs/promises"), 1);
337
477
  var import_path = __toESM(require("path"), 1);
338
478
  function getCollabBindingPath(repoRoot) {
339
479
  return import_path.default.join(repoRoot, ".remix", "config.json");
340
480
  }
341
481
  async function readCollabBinding(repoRoot) {
342
482
  try {
343
- const raw = await import_promises2.default.readFile(getCollabBindingPath(repoRoot), "utf8");
483
+ const raw = await import_promises3.default.readFile(getCollabBindingPath(repoRoot), "utf8");
344
484
  const parsed = JSON.parse(raw);
345
485
  if (parsed?.schemaVersion !== 1) return null;
346
486
  if (!parsed.projectId || !parsed.currentAppId || !parsed.upstreamAppId) return null;
@@ -384,6 +524,11 @@ function extractToolInput(payload) {
384
524
  function extractToolResponse(payload) {
385
525
  return getNestedRecord(payload.tool_response) ?? getNestedRecord(payload.toolResponse);
386
526
  }
527
+ function extractToolStructuredData(payload) {
528
+ const toolResponse = extractToolResponse(payload);
529
+ const structuredContent = getNestedRecord(toolResponse?.structuredContent) ?? getNestedRecord(payload.structuredContent);
530
+ return getNestedRecord(toolResponse?.data) ?? getNestedRecord(structuredContent?.data) ?? structuredContent;
531
+ }
387
532
  function extractToolName(payload) {
388
533
  return extractString(payload, ["tool_name", "toolName"]);
389
534
  }
@@ -397,6 +542,26 @@ function normalizeHookToolName(toolName) {
397
542
  }
398
543
  return trimmed;
399
544
  }
545
+ function extractAssistantResponse(payload) {
546
+ const candidateKeys = [
547
+ "last_assistant_message",
548
+ "lastAssistantMessage",
549
+ "assistant_response",
550
+ "assistantResponse",
551
+ "assistant_message",
552
+ "assistantMessage",
553
+ "response",
554
+ "message"
555
+ ];
556
+ return extractString(payload, candidateKeys) ?? extractString(extractToolResponse(payload) ?? {}, candidateKeys) ?? extractString(extractToolStructuredData(payload) ?? {}, candidateKeys) ?? extractString(extractToolInput(payload), candidateKeys);
557
+ }
558
+ function extractFinalizeTurnMode(payload) {
559
+ const mode = extractString(extractToolStructuredData(payload) ?? {}, ["mode"]) ?? extractString(extractToolResponse(payload) ?? {}, ["mode"]) ?? extractString(payload, ["mode"]);
560
+ if (mode === "changed_turn" || mode === "no_diff_turn") {
561
+ return mode;
562
+ }
563
+ return null;
564
+ }
400
565
  function extractString(input, keys) {
401
566
  for (const key of keys) {
402
567
  const value = input[key];
@@ -462,7 +627,7 @@ function collectPathTargetsFromObject(input, keys) {
462
627
  return keys.flatMap((key) => collectStringPathValue(input[key]));
463
628
  }
464
629
  function resolveCandidatePath(targetPath, baseDir) {
465
- return import_node_path2.default.isAbsolute(targetPath) ? import_node_path2.default.normalize(targetPath) : import_node_path2.default.resolve(baseDir, targetPath);
630
+ return import_node_path3.default.isAbsolute(targetPath) ? import_node_path3.default.normalize(targetPath) : import_node_path3.default.resolve(baseDir, targetPath);
466
631
  }
467
632
  function extractToolPathTargets(payload, toolName) {
468
633
  const name = (toolName ?? extractToolName(payload) ?? "").trim().toLowerCase();
@@ -474,16 +639,16 @@ function extractToolPathTargets(payload, toolName) {
474
639
  }
475
640
  async function findBoundRepo(startPath) {
476
641
  if (!startPath) return null;
477
- let current = import_node_path2.default.resolve(startPath);
478
- let stats = await import_promises3.default.stat(current).catch(() => null);
642
+ let current = import_node_path3.default.resolve(startPath);
643
+ let stats = await import_promises4.default.stat(current).catch(() => null);
479
644
  if (stats?.isFile()) {
480
- current = import_node_path2.default.dirname(current);
645
+ current = import_node_path3.default.dirname(current);
481
646
  }
482
647
  while (true) {
483
- const bindingPath = import_node_path2.default.join(current, ".remix", "config.json");
484
- const bindingStats = await import_promises3.default.stat(bindingPath).catch(() => null);
648
+ const bindingPath = import_node_path3.default.join(current, ".remix", "config.json");
649
+ const bindingStats = await import_promises4.default.stat(bindingPath).catch(() => null);
485
650
  if (bindingStats?.isFile()) return current;
486
- const parent = import_node_path2.default.dirname(current);
651
+ const parent = import_node_path3.default.dirname(current);
487
652
  if (parent === current) return null;
488
653
  current = parent;
489
654
  }
@@ -532,15 +697,28 @@ function isShellToolName(toolName) {
532
697
  function isRemoteChangeRecordingToolName(toolName) {
533
698
  return /remix_collab_(add|add_change_step)$/i.test(toolName);
534
699
  }
535
- function getManualRecordingScope(toolName) {
536
- if (/remix_collab_add_change_step$/i.test(toolName)) {
537
- return "change_step";
700
+ function hasManualFullTurnPayload(payload) {
701
+ const toolInput = extractToolInput(payload);
702
+ return Boolean(extractString(toolInput, ["prompt"]) && extractAssistantResponse(payload));
703
+ }
704
+ function getManualRecordingScope(payload, toolName) {
705
+ if (/remix_collab_finalize_turn$/i.test(toolName)) {
706
+ return "full_turn";
707
+ }
708
+ if (/remix_collab_(add|add_change_step)$/i.test(toolName)) {
709
+ return hasManualFullTurnPayload(payload) ? "full_turn" : "change_step";
538
710
  }
539
- if (/remix_collab_(add|record_turn|record_no_diff_turn)$/i.test(toolName)) {
711
+ if (/remix_collab_(record_turn|record_no_diff_turn)$/i.test(toolName)) {
540
712
  return "full_turn";
541
713
  }
542
714
  return null;
543
715
  }
716
+ function didFinalizeTurnRecordRemoteChange(payload, toolName) {
717
+ if (!/remix_collab_finalize_turn$/i.test(toolName)) {
718
+ return false;
719
+ }
720
+ return extractFinalizeTurnMode(payload) === "changed_turn";
721
+ }
544
722
  function isLikelyMutatingShellCommand(command) {
545
723
  const normalized = command.trim().toLowerCase();
546
724
  if (!normalized) return false;
@@ -580,22 +758,71 @@ function isLikelyMutatingShellCommand(command) {
580
758
  }
581
759
  return false;
582
760
  }
583
- async function main() {
584
- const payload = await readJsonStdin();
761
+ async function runHookPostCollab(payload) {
585
762
  const sessionId = typeof payload.session_id === "string" && payload.session_id.trim() ? payload.session_id.trim() : null;
586
763
  const toolName = normalizeHookToolName(extractToolName(payload));
764
+ await appendHookDiagnosticsEvent({
765
+ hook: "PostToolUse",
766
+ sessionId,
767
+ stage: "payload_received",
768
+ result: "start",
769
+ toolName,
770
+ fields: {
771
+ hasSessionId: Boolean(sessionId),
772
+ hasToolName: Boolean(toolName)
773
+ }
774
+ });
587
775
  if (!sessionId || !toolName) {
776
+ await appendHookDiagnosticsEvent({
777
+ hook: "PostToolUse",
778
+ sessionId,
779
+ stage: "payload_validation",
780
+ result: "skip",
781
+ reason: !sessionId ? "missing_session_id" : "missing_tool_name",
782
+ toolName
783
+ });
588
784
  return;
589
785
  }
590
786
  const toolSucceeded = didToolSucceed(payload);
591
- const remoteChangeRecordedButSyncFailed = isRemoteChangeRecordingToolName(toolName) && isRemoteChangeRecordedButLocalSyncFailed(payload);
787
+ const remoteChangeRecordedButSyncFailed = (isRemoteChangeRecordingToolName(toolName) || /remix_collab_finalize_turn$/i.test(toolName)) && isRemoteChangeRecordedButLocalSyncFailed(payload);
788
+ await appendHookDiagnosticsEvent({
789
+ hook: "PostToolUse",
790
+ sessionId,
791
+ stage: "tool_classified",
792
+ result: "info",
793
+ toolName,
794
+ fields: {
795
+ toolSucceeded,
796
+ remoteChangeRecordedButSyncFailed,
797
+ isMemoryTool: isMemoryToolName(toolName),
798
+ isRepoMutationTool: isRepoMutationToolName(toolName),
799
+ isShellTool: isShellToolName(toolName),
800
+ isStructuredLocalWriteTool: isStructuredLocalWriteToolName(toolName),
801
+ isStructuredLocalReadTool: isStructuredLocalReadToolName(toolName)
802
+ }
803
+ });
592
804
  if (!toolSucceeded && !remoteChangeRecordedButSyncFailed) {
805
+ await appendHookDiagnosticsEvent({
806
+ hook: "PostToolUse",
807
+ sessionId,
808
+ stage: "tool_result_gate",
809
+ result: "skip",
810
+ reason: "tool_failed_or_skipped",
811
+ toolName
812
+ });
593
813
  return;
594
814
  }
595
815
  if (toolSucceeded && isMemoryToolName(toolName)) {
596
816
  await markPendingTurnConsultedMemory(sessionId);
817
+ await appendHookDiagnosticsEvent({
818
+ hook: "PostToolUse",
819
+ sessionId,
820
+ stage: "memory_marked_consulted",
821
+ result: "success",
822
+ toolName
823
+ });
597
824
  }
598
- const manualRecordingScope = getManualRecordingScope(toolName);
825
+ const manualRecordingScope = getManualRecordingScope(payload, toolName);
599
826
  if (isRepoMutationToolName(toolName) || manualRecordingScope) {
600
827
  const targetRepo = await resolveBoundRepoFromToolCwd(payload);
601
828
  if (targetRepo) {
@@ -614,9 +841,31 @@ async function main() {
614
841
  await markTouchedRepoManuallyRecorded(sessionId, targetRepo.repoRoot, {
615
842
  toolName,
616
843
  scope: manualRecordingScope,
617
- remoteChangeRecorded: toolSucceeded ? isRemoteChangeRecordingToolName(toolName) : remoteChangeRecordedButSyncFailed
844
+ remoteChangeRecorded: toolSucceeded ? isRemoteChangeRecordingToolName(toolName) || didFinalizeTurnRecordRemoteChange(payload, toolName) : remoteChangeRecordedButSyncFailed
618
845
  });
619
846
  }
847
+ await appendHookDiagnosticsEvent({
848
+ hook: "PostToolUse",
849
+ sessionId,
850
+ stage: "repo_marked_from_tool_cwd",
851
+ result: "success",
852
+ toolName,
853
+ repoRoot: targetRepo.repoRoot,
854
+ fields: {
855
+ hasObservedWrite: isRepoMutationToolName(toolName),
856
+ manualRecordingScope,
857
+ remoteChangeRecordedButSyncFailed
858
+ }
859
+ });
860
+ } else {
861
+ await appendHookDiagnosticsEvent({
862
+ hook: "PostToolUse",
863
+ sessionId,
864
+ stage: "repo_marked_from_tool_cwd",
865
+ result: "skip",
866
+ reason: "repo_not_bound_from_tool_cwd",
867
+ toolName
868
+ });
620
869
  }
621
870
  }
622
871
  if (toolSucceeded && isShellToolName(toolName)) {
@@ -635,10 +884,44 @@ async function main() {
635
884
  if (hasObservedWrite) {
636
885
  await markTouchedRepoObservedWrite(sessionId, targetRepo.repoRoot, { toolName });
637
886
  }
887
+ await appendHookDiagnosticsEvent({
888
+ hook: "PostToolUse",
889
+ sessionId,
890
+ stage: "shell_repo_marked",
891
+ result: "success",
892
+ toolName,
893
+ repoRoot: targetRepo.repoRoot,
894
+ fields: {
895
+ hasObservedWrite,
896
+ bashCommandLength: bashCommand?.length ?? 0
897
+ }
898
+ });
899
+ } else {
900
+ await appendHookDiagnosticsEvent({
901
+ hook: "PostToolUse",
902
+ sessionId,
903
+ stage: "shell_repo_marked",
904
+ result: "skip",
905
+ reason: "repo_not_bound_from_shell_cwd",
906
+ toolName
907
+ });
638
908
  }
639
909
  }
640
910
  if (toolSucceeded && (isStructuredLocalWriteToolName(toolName) || isStructuredLocalReadToolName(toolName))) {
641
- const touchedRepos = await resolveTouchedBoundReposFromPaths(extractToolPathTargets(payload, toolName));
911
+ const touchedPaths = extractToolPathTargets(payload, toolName);
912
+ const touchedRepos = await resolveTouchedBoundReposFromPaths(touchedPaths);
913
+ await appendHookDiagnosticsEvent({
914
+ hook: "PostToolUse",
915
+ sessionId,
916
+ stage: "path_targets_resolved",
917
+ result: touchedRepos.length > 0 ? "info" : "skip",
918
+ reason: touchedRepos.length > 0 ? null : "no_bound_repos_from_paths",
919
+ toolName,
920
+ fields: {
921
+ touchedPathCount: touchedPaths.length,
922
+ boundRepoCount: touchedRepos.length
923
+ }
924
+ });
642
925
  for (const repo of touchedRepos) {
643
926
  await upsertTouchedRepo(sessionId, {
644
927
  repoRoot: repo.repoRoot,
@@ -651,13 +934,46 @@ async function main() {
651
934
  if (isStructuredLocalWriteToolName(toolName)) {
652
935
  await markTouchedRepoObservedWrite(sessionId, repo.repoRoot, { toolName });
653
936
  }
937
+ await appendHookDiagnosticsEvent({
938
+ hook: "PostToolUse",
939
+ sessionId,
940
+ stage: "path_repo_marked",
941
+ result: "success",
942
+ toolName,
943
+ repoRoot: repo.repoRoot,
944
+ fields: {
945
+ hasObservedWrite: isStructuredLocalWriteToolName(toolName)
946
+ }
947
+ });
654
948
  }
655
949
  }
950
+ await appendHookDiagnosticsEvent({
951
+ hook: "PostToolUse",
952
+ sessionId,
953
+ stage: "completed",
954
+ result: "success",
955
+ toolName
956
+ });
957
+ }
958
+ async function main() {
959
+ const payload = await readJsonStdin();
960
+ await runHookPostCollab(payload);
656
961
  }
657
962
  main().catch((error) => {
658
963
  const message = error instanceof Error ? error.message : String(error);
964
+ void appendHookDiagnosticsEvent({
965
+ hook: "PostToolUse",
966
+ stage: "unhandled_error",
967
+ result: "error",
968
+ reason: "exception",
969
+ message
970
+ });
659
971
  process.stderr.write(`${message}
660
972
  `);
661
973
  process.exitCode = 0;
662
974
  });
975
+ // Annotate the CommonJS export names for ESM import in node:
976
+ 0 && (module.exports = {
977
+ runHookPostCollab
978
+ });
663
979
  //# sourceMappingURL=hook-post-collab.cjs.map