@primitive.ai/prim 0.1.0-alpha.92 → 0.1.0-alpha.94

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +84 -10
  2. package/SKILL.md +5 -2
  3. package/dist/{chunk-67VNLLBN.js → chunk-2GP5UFV7.js} +1 -0
  4. package/dist/{chunk-26P57LAR.js → chunk-AXQQMX2U.js} +203 -9
  5. package/dist/{chunk-M6RIJVRE.js → chunk-DSWT6BCC.js} +78 -4
  6. package/dist/{chunk-FBAYCS3X.js → chunk-DXJQR4QI.js} +150 -38
  7. package/dist/chunk-FXF4JWK2.js +162 -0
  8. package/dist/{chunk-PCVGBE5B.js → chunk-G2VMA2QO.js} +2 -1
  9. package/dist/{chunk-NAHHFSLH.js → chunk-ICM2XCXS.js} +2 -2
  10. package/dist/{chunk-5HRR4AOR.js → chunk-IMK4JKTA.js} +1 -1
  11. package/dist/{chunk-OAAP2MY6.js → chunk-K7D2EYBS.js} +2 -2
  12. package/dist/{chunk-ADUTW66W.js → chunk-O7YAW7G6.js} +6 -6
  13. package/dist/{chunk-5AFRMOPX.js → chunk-OH3EZXYX.js} +1 -1
  14. package/dist/{chunk-KFXF2V5G.js → chunk-OTHKG76J.js} +1 -1
  15. package/dist/chunk-OYHYCK4K.js +13 -0
  16. package/dist/{chunk-FKXFUUWJ.js → chunk-TYAHC25F.js} +1 -1
  17. package/dist/{chunk-5GYQ4J3D.js → chunk-UKUAAAXE.js} +2 -2
  18. package/dist/{chunk-LGGMJYY4.js → chunk-WUXFLE7X.js} +720 -369
  19. package/dist/{chunk-WX52U7Z2.js → chunk-ZU563PQI.js} +58 -2
  20. package/dist/daemon/server.js +372 -123
  21. package/dist/hook-runtime/daemon/server.js +372 -123
  22. package/dist/hook-runtime/hooks/post-commit.js +192 -58
  23. package/dist/hook-runtime/hooks/post-rewrite.js +58 -40
  24. package/dist/hook-runtime/hooks/post-tool-use.js +150 -67
  25. package/dist/hook-runtime/hooks/pre-tool-use.js +87 -15
  26. package/dist/hook-runtime/hooks/prim-hook.js +197 -107
  27. package/dist/hook-runtime/hooks/session-end.js +53 -5
  28. package/dist/hook-runtime/hooks/session-start.js +880 -280
  29. package/dist/hook-runtime/index.js +4008 -2438
  30. package/dist/hook-runtime/statusline-main.js +23 -1
  31. package/dist/hooks/post-commit.js +15 -8
  32. package/dist/hooks/post-rewrite.js +11 -8
  33. package/dist/hooks/post-tool-use.js +13 -15
  34. package/dist/hooks/pre-commit.js +4 -4
  35. package/dist/hooks/pre-tool-use.js +10 -12
  36. package/dist/hooks/prim-hook.js +17 -14
  37. package/dist/hooks/session-end.js +4 -4
  38. package/dist/hooks/session-start.js +35 -45
  39. package/dist/index.js +1401 -620
  40. package/dist/statusline-main.js +23 -1
  41. package/package.json +1 -1
  42. package/dist/chunk-BTNNEH2W.js +0 -79
@@ -242,6 +242,13 @@ function pinnedNpxCommand(bin, args = [], options = {}) {
242
242
  }
243
243
  var STABLE_HOOK_ARGUMENTS_RE = /^[-A-Za-z0-9_ ]*$/u;
244
244
  var STABLE_HOOK_BIN_RE = /^[A-Za-z0-9][A-Za-z0-9-]*$/u;
245
+ var PRIM_CONFIG_SHELL_RESOLVER = [
246
+ 'prim_absolute() { case "$1" in [/]* ) ;; *) return 1 ;; esac; case "$1" in [[:space:]]*|*[[:space:]]|*[/][/]*|*[/].[/]*|*[/]..[/]*|*[/].|*[/]..|?*[/]) return 1 ;; esac; }; ',
247
+ 'prim_config=${PRIM_CONFIG_DIR:-}; if ! prim_absolute "$prim_config"; then prim_config=${XDG_CONFIG_HOME:-}; if prim_absolute "$prim_config"; then ',
248
+ 'case "$prim_config" in /) prim_config=/prim ;; *) prim_config="$prim_config/prim" ;; esac; ',
249
+ 'else prim_home=${HOME:-}; prim_absolute "$prim_home" || exit 78; case "$prim_home" in /) prim_config=/.config/prim ;; *) prim_config="$prim_home/.config/prim" ;; esac; ',
250
+ "fi; fi; "
251
+ ].join("");
245
252
  function stableHookCommand(bin, args = "") {
246
253
  if (!STABLE_HOOK_BIN_RE.test(bin) || !STABLE_HOOK_ARGUMENTS_RE.test(args)) {
247
254
  throw new Error("invalid stable hook command");
@@ -251,11 +258,7 @@ function stableHookCommand(bin, args = "") {
251
258
  // it as inert data during the rolling window so an older uninstall or
252
259
  // reinstall can remove the stable entry without invoking npm or PATH.
253
260
  `prim_legacy_reader='-p ${PKG_NAME}@stable prim-shim.sh ${bin} '; `,
254
- 'prim_absolute() { case "$1" in [/]* ) ;; *) return 1 ;; esac; case "$1" in [[:space:]]*|*[[:space:]]|*[/][/]*|*[/].[/]*|*[/]..[/]*|*[/].|*[/]..|?*[/]) return 1 ;; esac; }; ',
255
- 'prim_config=${PRIM_CONFIG_DIR:-}; if ! prim_absolute "$prim_config"; then prim_config=${XDG_CONFIG_HOME:-}; if prim_absolute "$prim_config"; then ',
256
- 'case "$prim_config" in /) prim_config=/prim ;; *) prim_config="$prim_config/prim" ;; esac; ',
257
- 'else prim_home=${HOME:-}; prim_absolute "$prim_home" || exit 78; case "$prim_home" in /) prim_config=/.config/prim ;; *) prim_config="$prim_home/.config/prim" ;; esac; ',
258
- "fi; fi; ",
261
+ PRIM_CONFIG_SHELL_RESOLVER,
259
262
  `case "$prim_config" in /) prim_launcher=/${STABLE_HOOK_LAUNCHER_NAME} ;; *) prim_launcher="$prim_config/${STABLE_HOOK_LAUNCHER_NAME}" ;; esac; `,
260
263
  'exec "$prim_launcher" "$@"'
261
264
  ].join("");
@@ -427,14 +430,14 @@ function throwIfAborted(signal) {
427
430
  }
428
431
  async function defaultSleep(ms, signal) {
429
432
  if (!signal) {
430
- await new Promise((resolve9) => setTimeout(resolve9, ms));
433
+ await new Promise((resolve10) => setTimeout(resolve10, ms));
431
434
  return;
432
435
  }
433
436
  throwIfAborted(signal);
434
- await new Promise((resolve9, reject) => {
437
+ await new Promise((resolve10, reject) => {
435
438
  const timer = setTimeout(() => {
436
439
  signal.removeEventListener("abort", onAbort);
437
- resolve9();
440
+ resolve10();
438
441
  }, ms);
439
442
  const onAbort = () => {
440
443
  clearTimeout(timer);
@@ -476,6 +479,144 @@ async function withFileLock(lockDir, operation, options = {}) {
476
479
  }
477
480
  }
478
481
 
482
+ // src/lib/git-hook-contract.ts
483
+ var MANAGED_GIT_HOOK_NAMES = ["pre-commit", "post-commit", "post-rewrite"];
484
+ var GIT_HOOK_ENTRYPOINT_NAME = "prim-git-hook-v1";
485
+ var GIT_HOOK_CONTRACT_VERSION = 1;
486
+ var CONTRACT_LINE_PREFIX = "# prim git hook v";
487
+ var CONTRACT_LINE_RE = /^# prim git hook v([0-9]+)\b/mu;
488
+ function blockMarkers(hook) {
489
+ return { start: `# >>> prim ${hook} hook >>>`, end: `# <<< prim ${hook} hook <<<` };
490
+ }
491
+ function entrypointCommand(hook) {
492
+ const script = [
493
+ PRIM_CONFIG_SHELL_RESOLVER,
494
+ `case "$prim_config" in /) prim_entry=/${GIT_HOOK_ENTRYPOINT_NAME} ;; `,
495
+ `*) prim_entry="$prim_config/${GIT_HOOK_ENTRYPOINT_NAME}" ;; esac; `,
496
+ '[ -x "$prim_entry" ] || exit 0; exec "$prim_entry" "$@"'
497
+ ].join("");
498
+ return `/bin/sh -c ${shellQuote(script)} ${GIT_HOOK_ENTRYPOINT_NAME} ${hook} "$@"`;
499
+ }
500
+ function managedHookBlock(hook) {
501
+ const { start, end } = blockMarkers(hook);
502
+ const header = `${CONTRACT_LINE_PREFIX}${GIT_HOOK_CONTRACT_VERSION}: a no-op unless prim is installed and enabled here (see \`prim hooks snippet ${hook}\`).`;
503
+ if (hook !== "post-rewrite") {
504
+ return `${start}
505
+ ${header}
506
+ # shellcheck disable=SC2016
507
+ ${entrypointCommand(hook)} || :
508
+ ${end}`;
509
+ }
510
+ return `${start}
511
+ ${header}
512
+ prim_rewrite_stdin=$(mktemp 2>/dev/null) || prim_rewrite_stdin=
513
+ if test -n "\${prim_rewrite_stdin}" && cat >"\${prim_rewrite_stdin}"; then
514
+ # shellcheck disable=SC2016
515
+ ${entrypointCommand(hook)} <"\${prim_rewrite_stdin}" || :
516
+ exec <"\${prim_rewrite_stdin}"
517
+ fi
518
+ if test -n "\${prim_rewrite_stdin}"; then rm -f "\${prim_rewrite_stdin}"; fi
519
+ unset prim_rewrite_stdin
520
+ ${end}`;
521
+ }
522
+ var OPEN_WORDS = /* @__PURE__ */ new Set(["then", "do", "else", "{", "(", "&&", "||", "|"]);
523
+ function canonicalHookBlock(text) {
524
+ const tokens = [];
525
+ let word = "";
526
+ let quote;
527
+ const separate = () => {
528
+ const last = tokens.at(-1);
529
+ if (last !== void 0 && last !== ";" && !OPEN_WORDS.has(last)) tokens.push(";");
530
+ };
531
+ const flush = () => {
532
+ if (word === "") return;
533
+ const last = tokens.at(-1);
534
+ if (last !== void 0 && /^[0-9]*(?:<|>|>>|<&|>&)$/u.test(last))
535
+ tokens[tokens.length - 1] += word;
536
+ else tokens.push(word);
537
+ word = "";
538
+ };
539
+ for (let index = 0; index < text.length; index += 1) {
540
+ const char = text[index];
541
+ if (quote) {
542
+ word += char;
543
+ if (char === quote) quote = void 0;
544
+ continue;
545
+ }
546
+ if (char === "'" || char === '"') {
547
+ quote = char;
548
+ word += char;
549
+ } else if (char === "#" && word === "") {
550
+ const end = text.indexOf("\n", index);
551
+ const comment = text.slice(index, end === -1 ? text.length : end);
552
+ tokens.push(comment.replace(/\s+/gu, " ").trim());
553
+ index = (end === -1 ? text.length : end) - 1;
554
+ } else if (char === "\n" || char === ";") {
555
+ flush();
556
+ if (char === ";" && text[index + 1] === ";") {
557
+ tokens.push(";;");
558
+ index += 1;
559
+ } else {
560
+ separate();
561
+ }
562
+ } else if (/\s/u.test(char)) {
563
+ flush();
564
+ } else {
565
+ word += char;
566
+ }
567
+ }
568
+ flush();
569
+ while (tokens.at(-1) === ";") tokens.pop();
570
+ return tokens.join("\0");
571
+ }
572
+ function hookBlockContractVersion(block) {
573
+ const match = CONTRACT_LINE_RE.exec(block);
574
+ return match?.[1] ? Number(match[1]) : void 0;
575
+ }
576
+ var GIT_HOOK_ENTRYPOINT_CONTENT = `#!/bin/sh
577
+ # prim git hook entrypoint v1 \u2014 managed by prim; do not edit.
578
+ # Run by the "# >>> prim <hook> hook >>>" block in a Git hook file. Acts only
579
+ # where prim is active (git config prim.active true) and never fails the hook.
580
+ case "$0" in */*) prim_dir=\${0%/*} ;; *) exit 0 ;; esac
581
+ prim_launcher="$prim_dir/${STABLE_HOOK_LAUNCHER_NAME}"
582
+ [ -x "$prim_launcher" ] || exit 0
583
+ [ "$(git config --get prim.active 2>/dev/null)" = "true" ] || exit 0
584
+ prim_hook=\${1-}
585
+ if [ "$#" -gt 0 ]; then shift; fi
586
+ case "$prim_hook" in
587
+ pre-commit)
588
+ "$prim_launcher" prim-pre-commit "$@" || :
589
+ ;;
590
+ post-commit)
591
+ prim_sha=$(git rev-parse --verify HEAD 2>/dev/null) || exit 0
592
+ case "$prim_sha" in *[!0-9a-f]* | "") exit 0 ;; esac
593
+ case "\${#prim_sha}" in 40 | 64) ;; *) exit 0 ;; esac
594
+ prim_branch=$(git symbolic-ref --quiet --short HEAD 2>/dev/null) || prim_branch=
595
+ prim_observed=$(mktemp "\${TMPDIR:-/tmp}/prim-post-commit-observed.XXXXXXXX" 2>/dev/null) || prim_observed=
596
+ (
597
+ trap '' HUP
598
+ trap 'if [ -n "$prim_observed" ]; then rm -f "$prim_observed"; fi' 0
599
+ PRIM_COMMIT_SHA=$prim_sha PRIM_COMMIT_BRANCH=$prim_branch PRIM_COMMIT_OBSERVED_FILE=$prim_observed "$prim_launcher" prim-post-commit
600
+ ) </dev/null >/dev/null 2>&1 &
601
+ ;;
602
+ post-rewrite)
603
+ case "\${1-}" in amend | rebase) prim_source=$1 ;; *) exit 0 ;; esac
604
+ prim_pairs=$(mktemp "\${TMPDIR:-/tmp}/prim-post-rewrite-pairs.XXXXXXXX" 2>/dev/null) || exit 0
605
+ if ! chmod 600 "$prim_pairs" 2>/dev/null || ! cat >"$prim_pairs"; then
606
+ rm -f "$prim_pairs"
607
+ exit 0
608
+ fi
609
+ prim_branch=$(git symbolic-ref --quiet --short HEAD 2>/dev/null) || prim_branch=
610
+ (
611
+ trap '' HUP
612
+ trap 'rm -f "$prim_pairs"' 0
613
+ PRIM_REWRITE_SOURCE=$prim_source PRIM_REWRITE_BRANCH=$prim_branch PRIM_REWRITE_PAIRS_FILE=$prim_pairs "$prim_launcher" prim-post-rewrite
614
+ ) </dev/null >/dev/null 2>&1 &
615
+ ;;
616
+ esac
617
+ exit 0
618
+ `;
619
+
479
620
  // src/lib/paths.ts
480
621
  import { homedir as homedir2 } from "os";
481
622
  import { isAbsolute as isAbsolute2, join as join4, normalize } from "path";
@@ -529,6 +670,24 @@ var SOURCE_ENTRIES = Object.fromEntries(
529
670
  target.replace(/^dist\//u, "")
530
671
  ])
531
672
  );
673
+ function hookRuntimePaths(options = {}) {
674
+ const env = options.env ?? process.env;
675
+ const configDir = primConfigDirectory({
676
+ ...options,
677
+ env,
678
+ homeDir: options.homeDir ?? env.HOME ?? ""
679
+ });
680
+ const runtimeDir = join5(configDir, "hook-runtime");
681
+ return {
682
+ configDir,
683
+ launcher: join5(configDir, STABLE_HOOK_LAUNCHER_NAME),
684
+ gitHookEntrypoint: join5(configDir, GIT_HOOK_ENTRYPOINT_NAME),
685
+ runtimeDir,
686
+ releasesDir: join5(runtimeDir, "releases"),
687
+ current: join5(runtimeDir, "current"),
688
+ selectionLock: join5(runtimeDir, "selection.lock")
689
+ };
690
+ }
532
691
  var STABLE_HOOK_LAUNCHER_CONTENT = `#!/bin/sh
533
692
  set -eu
534
693
  case "$0" in */*) prim_launcher_dir=\${0%/*} ;; *) exit 78 ;; esac
@@ -548,6 +707,40 @@ case "$prim_node" in /*) ;; *) exit 69 ;; esac
548
707
  [ -x "$prim_node" ] && [ -f "$prim_entry" ] || exit 69
549
708
  exec "$prim_node" "$prim_entry" "$@"
550
709
  `;
710
+ function gitHookEntrypointCurrent(paths) {
711
+ try {
712
+ return readFileSync3(paths.gitHookEntrypoint, "utf8") === GIT_HOOK_ENTRYPOINT_CONTENT;
713
+ } catch {
714
+ return false;
715
+ }
716
+ }
717
+ function inspectGitHookEntrypoint(options = {}) {
718
+ let paths;
719
+ try {
720
+ paths = hookRuntimePaths(options);
721
+ } catch {
722
+ return "invalid";
723
+ }
724
+ try {
725
+ const entrypoint = lstatIfPresent(paths.gitHookEntrypoint);
726
+ if (!entrypoint) return "missing";
727
+ if (!entrypoint.isFile() || (Number(entrypoint.mode) & 64) === 0 || !gitHookEntrypointCurrent(paths) || readFileSync3(paths.launcher, "utf8") !== STABLE_HOOK_LAUNCHER_CONTENT || !lstatIfPresent(paths.current)?.isFile()) {
728
+ return "invalid";
729
+ }
730
+ return "ready";
731
+ } catch {
732
+ return "invalid";
733
+ }
734
+ }
735
+ function lstatIfPresent(path) {
736
+ try {
737
+ return lstatSync(path);
738
+ } catch (error) {
739
+ const code = error.code;
740
+ if (code === "ENOENT" || code === "ENOTDIR") return void 0;
741
+ throw error;
742
+ }
743
+ }
551
744
 
552
745
  // src/commands/claude-install.ts
553
746
  import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
@@ -881,13 +1074,13 @@ function buildHookOutput(options) {
881
1074
  return output;
882
1075
  }
883
1076
  function writeHookOutput(output, stream = process.stdout) {
884
- return new Promise((resolve9) => {
1077
+ return new Promise((resolve10) => {
885
1078
  let settled = false;
886
1079
  const finish = (ok, retainErrorListener = false) => {
887
1080
  if (settled) return;
888
1081
  settled = true;
889
1082
  if (!retainErrorListener) stream.off("error", onError);
890
- resolve9(ok);
1083
+ resolve10(ok);
891
1084
  };
892
1085
  const onError = () => {
893
1086
  if (settled) {
@@ -922,7 +1115,7 @@ async function handoffHookOutput(output, acknowledge, stream = process.stdout) {
922
1115
  // src/hooks/hook-stdin.ts
923
1116
  var DEFAULT_MAX_BYTES = 1024 * 1024;
924
1117
  function readHookStdin(timeoutMs = 1e3, maxBytes = DEFAULT_MAX_BYTES) {
925
- return new Promise((resolve9, reject) => {
1118
+ return new Promise((resolve10, reject) => {
926
1119
  const chunks = [];
927
1120
  let bytes = 0;
928
1121
  let settled = false;
@@ -934,7 +1127,7 @@ function readHookStdin(timeoutMs = 1e3, maxBytes = DEFAULT_MAX_BYTES) {
934
1127
  process.stdin.removeAllListeners("end");
935
1128
  process.stdin.removeAllListeners("error");
936
1129
  if (error) reject(error);
937
- else resolve9(Buffer.concat(chunks, bytes).toString("utf-8"));
1130
+ else resolve10(Buffer.concat(chunks, bytes).toString("utf-8"));
938
1131
  };
939
1132
  const timer = setTimeout(() => finish(new Error("stdin read timeout")), timeoutMs);
940
1133
  process.stdin.on("data", (chunk) => {
@@ -5191,10 +5384,10 @@ function resolveBlockMap({ composeNode: composeNode2, composeEmptyNode: composeE
5191
5384
  let offset = bm.offset;
5192
5385
  let commentEnd = null;
5193
5386
  for (const collItem of bm.items) {
5194
- const { start, key, sep: sep3, value } = collItem;
5387
+ const { start, key, sep: sep4, value } = collItem;
5195
5388
  const keyProps = resolveProps(start, {
5196
5389
  indicator: "explicit-key-ind",
5197
- next: key ?? sep3?.[0],
5390
+ next: key ?? sep4?.[0],
5198
5391
  offset,
5199
5392
  onError,
5200
5393
  parentIndent: bm.indent,
@@ -5208,7 +5401,7 @@ function resolveBlockMap({ composeNode: composeNode2, composeEmptyNode: composeE
5208
5401
  else if ("indent" in key && key.indent !== bm.indent)
5209
5402
  onError(offset, "BAD_INDENT", startColMsg);
5210
5403
  }
5211
- if (!keyProps.anchor && !keyProps.tag && !sep3) {
5404
+ if (!keyProps.anchor && !keyProps.tag && !sep4) {
5212
5405
  commentEnd = keyProps.end;
5213
5406
  if (keyProps.comment) {
5214
5407
  if (map2.comment)
@@ -5232,7 +5425,7 @@ function resolveBlockMap({ composeNode: composeNode2, composeEmptyNode: composeE
5232
5425
  ctx.atKey = false;
5233
5426
  if (mapIncludes(ctx, map2.items, keyNode))
5234
5427
  onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
5235
- const valueProps = resolveProps(sep3 ?? [], {
5428
+ const valueProps = resolveProps(sep4 ?? [], {
5236
5429
  indicator: "map-value-ind",
5237
5430
  next: value,
5238
5431
  offset: keyNode.range[2],
@@ -5248,7 +5441,7 @@ function resolveBlockMap({ composeNode: composeNode2, composeEmptyNode: composeE
5248
5441
  if (ctx.options.strict && keyProps.start < valueProps.found.offset - 1024)
5249
5442
  onError(keyNode.range, "KEY_OVER_1024_CHARS", "The : indicator must be at most 1024 chars after the start of an implicit block mapping key");
5250
5443
  }
5251
- const valueNode = value ? composeNode2(ctx, value, valueProps, onError) : composeEmptyNode2(ctx, offset, sep3, null, valueProps, onError);
5444
+ const valueNode = value ? composeNode2(ctx, value, valueProps, onError) : composeEmptyNode2(ctx, offset, sep4, null, valueProps, onError);
5252
5445
  if (ctx.schema.compat)
5253
5446
  flowIndentCheck(bm.indent, value, onError);
5254
5447
  offset = valueNode.range[2];
@@ -5324,7 +5517,7 @@ function resolveEnd(end, offset, reqSpace, onError) {
5324
5517
  let comment = "";
5325
5518
  if (end) {
5326
5519
  let hasSpace = false;
5327
- let sep3 = "";
5520
+ let sep4 = "";
5328
5521
  for (const token of end) {
5329
5522
  const { source, type } = token;
5330
5523
  switch (type) {
@@ -5338,13 +5531,13 @@ function resolveEnd(end, offset, reqSpace, onError) {
5338
5531
  if (!comment)
5339
5532
  comment = cb;
5340
5533
  else
5341
- comment += sep3 + cb;
5342
- sep3 = "";
5534
+ comment += sep4 + cb;
5535
+ sep4 = "";
5343
5536
  break;
5344
5537
  }
5345
5538
  case "newline":
5346
5539
  if (comment)
5347
- sep3 += source;
5540
+ sep4 += source;
5348
5541
  hasSpace = true;
5349
5542
  break;
5350
5543
  default:
@@ -5373,18 +5566,18 @@ function resolveFlowCollection({ composeNode: composeNode2, composeEmptyNode: co
5373
5566
  let offset = fc.offset + fc.start.source.length;
5374
5567
  for (let i = 0; i < fc.items.length; ++i) {
5375
5568
  const collItem = fc.items[i];
5376
- const { start, key, sep: sep3, value } = collItem;
5569
+ const { start, key, sep: sep4, value } = collItem;
5377
5570
  const props = resolveProps(start, {
5378
5571
  flow: fcName,
5379
5572
  indicator: "explicit-key-ind",
5380
- next: key ?? sep3?.[0],
5573
+ next: key ?? sep4?.[0],
5381
5574
  offset,
5382
5575
  onError,
5383
5576
  parentIndent: fc.indent,
5384
5577
  startOnNewline: false
5385
5578
  });
5386
5579
  if (!props.found) {
5387
- if (!props.anchor && !props.tag && !sep3 && !value) {
5580
+ if (!props.anchor && !props.tag && !sep4 && !value) {
5388
5581
  if (i === 0 && props.comma)
5389
5582
  onError(props.comma, "UNEXPECTED_TOKEN", `Unexpected , in ${fcName}`);
5390
5583
  else if (i < fc.items.length - 1)
@@ -5438,8 +5631,8 @@ function resolveFlowCollection({ composeNode: composeNode2, composeEmptyNode: co
5438
5631
  }
5439
5632
  }
5440
5633
  }
5441
- if (!isMap2 && !sep3 && !props.found) {
5442
- const valueNode = value ? composeNode2(ctx, value, props, onError) : composeEmptyNode2(ctx, props.end, sep3, null, props, onError);
5634
+ if (!isMap2 && !sep4 && !props.found) {
5635
+ const valueNode = value ? composeNode2(ctx, value, props, onError) : composeEmptyNode2(ctx, props.end, sep4, null, props, onError);
5443
5636
  coll.items.push(valueNode);
5444
5637
  offset = valueNode.range[2];
5445
5638
  if (isBlock(value))
@@ -5451,7 +5644,7 @@ function resolveFlowCollection({ composeNode: composeNode2, composeEmptyNode: co
5451
5644
  if (isBlock(key))
5452
5645
  onError(keyNode.range, "BLOCK_IN_FLOW", blockMsg);
5453
5646
  ctx.atKey = false;
5454
- const valueProps = resolveProps(sep3 ?? [], {
5647
+ const valueProps = resolveProps(sep4 ?? [], {
5455
5648
  flow: fcName,
5456
5649
  indicator: "map-value-ind",
5457
5650
  next: value,
@@ -5462,8 +5655,8 @@ function resolveFlowCollection({ composeNode: composeNode2, composeEmptyNode: co
5462
5655
  });
5463
5656
  if (valueProps.found) {
5464
5657
  if (!isMap2 && !props.found && ctx.options.strict) {
5465
- if (sep3)
5466
- for (const st of sep3) {
5658
+ if (sep4)
5659
+ for (const st of sep4) {
5467
5660
  if (st === valueProps.found)
5468
5661
  break;
5469
5662
  if (st.type === "newline") {
@@ -5480,7 +5673,7 @@ function resolveFlowCollection({ composeNode: composeNode2, composeEmptyNode: co
5480
5673
  else
5481
5674
  onError(valueProps.start, "MISSING_CHAR", `Missing , or : between ${fcName} items`);
5482
5675
  }
5483
- const valueNode = value ? composeNode2(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode2(ctx, valueProps.end, sep3, null, valueProps, onError) : null;
5676
+ const valueNode = value ? composeNode2(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode2(ctx, valueProps.end, sep4, null, valueProps, onError) : null;
5484
5677
  if (valueNode) {
5485
5678
  if (isBlock(value))
5486
5679
  onError(valueNode.range, "BLOCK_IN_FLOW", blockMsg);
@@ -5640,7 +5833,7 @@ function resolveBlockScalar(ctx, scalar, onError) {
5640
5833
  chompStart = i + 1;
5641
5834
  }
5642
5835
  let value = "";
5643
- let sep3 = "";
5836
+ let sep4 = "";
5644
5837
  let prevMoreIndented = false;
5645
5838
  for (let i = 0; i < contentStart; ++i)
5646
5839
  value += lines[i][0].slice(trimIndent) + "\n";
@@ -5657,24 +5850,24 @@ function resolveBlockScalar(ctx, scalar, onError) {
5657
5850
  indent = "";
5658
5851
  }
5659
5852
  if (type === Scalar.BLOCK_LITERAL) {
5660
- value += sep3 + indent.slice(trimIndent) + content;
5661
- sep3 = "\n";
5853
+ value += sep4 + indent.slice(trimIndent) + content;
5854
+ sep4 = "\n";
5662
5855
  } else if (indent.length > trimIndent || content[0] === " ") {
5663
- if (sep3 === " ")
5664
- sep3 = "\n";
5665
- else if (!prevMoreIndented && sep3 === "\n")
5666
- sep3 = "\n\n";
5667
- value += sep3 + indent.slice(trimIndent) + content;
5668
- sep3 = "\n";
5856
+ if (sep4 === " ")
5857
+ sep4 = "\n";
5858
+ else if (!prevMoreIndented && sep4 === "\n")
5859
+ sep4 = "\n\n";
5860
+ value += sep4 + indent.slice(trimIndent) + content;
5861
+ sep4 = "\n";
5669
5862
  prevMoreIndented = true;
5670
5863
  } else if (content === "") {
5671
- if (sep3 === "\n")
5864
+ if (sep4 === "\n")
5672
5865
  value += "\n";
5673
5866
  else
5674
- sep3 = "\n";
5867
+ sep4 = "\n";
5675
5868
  } else {
5676
- value += sep3 + content;
5677
- sep3 = " ";
5869
+ value += sep4 + content;
5870
+ sep4 = " ";
5678
5871
  prevMoreIndented = false;
5679
5872
  }
5680
5873
  }
@@ -5848,25 +6041,25 @@ function foldLines(source) {
5848
6041
  if (!match)
5849
6042
  return source;
5850
6043
  let res = match[1];
5851
- let sep3 = " ";
6044
+ let sep4 = " ";
5852
6045
  let pos = first.lastIndex;
5853
6046
  line.lastIndex = pos;
5854
6047
  while (match = line.exec(source)) {
5855
6048
  if (match[1] === "") {
5856
- if (sep3 === "\n")
5857
- res += sep3;
6049
+ if (sep4 === "\n")
6050
+ res += sep4;
5858
6051
  else
5859
- sep3 = "\n";
6052
+ sep4 = "\n";
5860
6053
  } else {
5861
- res += sep3 + match[1];
5862
- sep3 = " ";
6054
+ res += sep4 + match[1];
6055
+ sep4 = " ";
5863
6056
  }
5864
6057
  pos = line.lastIndex;
5865
6058
  }
5866
6059
  const last = /[ \t]*(.*)/sy;
5867
6060
  last.lastIndex = pos;
5868
6061
  match = last.exec(source);
5869
- return res + sep3 + (match?.[1] ?? "");
6062
+ return res + sep4 + (match?.[1] ?? "");
5870
6063
  }
5871
6064
  function doubleQuotedValue(source, onError) {
5872
6065
  let res = "";
@@ -7483,18 +7676,18 @@ var Parser = class {
7483
7676
  if (this.type === "map-value-ind") {
7484
7677
  const prev = getPrevProps(this.peek(2));
7485
7678
  const start = getFirstKeyStartProps(prev);
7486
- let sep3;
7679
+ let sep4;
7487
7680
  if (scalar.end) {
7488
- sep3 = scalar.end;
7489
- sep3.push(this.sourceToken);
7681
+ sep4 = scalar.end;
7682
+ sep4.push(this.sourceToken);
7490
7683
  delete scalar.end;
7491
7684
  } else
7492
- sep3 = [this.sourceToken];
7685
+ sep4 = [this.sourceToken];
7493
7686
  const map2 = {
7494
7687
  type: "block-map",
7495
7688
  offset: scalar.offset,
7496
7689
  indent: scalar.indent,
7497
- items: [{ start, key: scalar, sep: sep3 }]
7690
+ items: [{ start, key: scalar, sep: sep4 }]
7498
7691
  };
7499
7692
  this.onKeyLine = true;
7500
7693
  this.stack[this.stack.length - 1] = map2;
@@ -7647,15 +7840,15 @@ var Parser = class {
7647
7840
  } else if (isFlowToken(it.key) && !includesToken(it.sep, "newline")) {
7648
7841
  const start2 = getFirstKeyStartProps(it.start);
7649
7842
  const key = it.key;
7650
- const sep3 = it.sep;
7651
- sep3.push(this.sourceToken);
7843
+ const sep4 = it.sep;
7844
+ sep4.push(this.sourceToken);
7652
7845
  delete it.key;
7653
7846
  delete it.sep;
7654
7847
  this.stack.push({
7655
7848
  type: "block-map",
7656
7849
  offset: this.offset,
7657
7850
  indent: this.indent,
7658
- items: [{ start: start2, key, sep: sep3 }]
7851
+ items: [{ start: start2, key, sep: sep4 }]
7659
7852
  });
7660
7853
  } else if (start.length > 0) {
7661
7854
  it.sep = it.sep.concat(start, this.sourceToken);
@@ -7849,13 +8042,13 @@ var Parser = class {
7849
8042
  const prev = getPrevProps(parent);
7850
8043
  const start = getFirstKeyStartProps(prev);
7851
8044
  fixFlowSeqItems(fc);
7852
- const sep3 = fc.end.splice(1, fc.end.length);
7853
- sep3.push(this.sourceToken);
8045
+ const sep4 = fc.end.splice(1, fc.end.length);
8046
+ sep4.push(this.sourceToken);
7854
8047
  const map2 = {
7855
8048
  type: "block-map",
7856
8049
  offset: fc.offset,
7857
8050
  indent: fc.indent,
7858
- items: [{ start, key: fc, sep: sep3 }]
8051
+ items: [{ start, key: fc, sep: sep4 }]
7859
8052
  };
7860
8053
  this.onKeyLine = true;
7861
8054
  this.stack[this.stack.length - 1] = map2;
@@ -8761,7 +8954,7 @@ function daemonRequest(method, params = {}, opts = {}) {
8761
8954
  ...caller ? { caller } : {}
8762
8955
  };
8763
8956
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
8764
- return new Promise((resolve9) => {
8957
+ return new Promise((resolve10) => {
8765
8958
  let settled = false;
8766
8959
  let socket;
8767
8960
  let buffer = Buffer.alloc(0);
@@ -8776,7 +8969,7 @@ function daemonRequest(method, params = {}, opts = {}) {
8776
8969
  } catch {
8777
8970
  }
8778
8971
  }
8779
- resolve9(value);
8972
+ resolve10(value);
8780
8973
  };
8781
8974
  const timer = setTimeout(() => settle(null), timeoutMs);
8782
8975
  timer.unref();
@@ -8837,6 +9030,14 @@ function daemonRequest(method, params = {}, opts = {}) {
8837
9030
 
8838
9031
  // src/daemon/self-heal.ts
8839
9032
  import { spawn } from "child_process";
9033
+
9034
+ // src/lib/unattended.ts
9035
+ var UNATTENDED_ENV = "PRIM_UNATTENDED";
9036
+ function unattendedEnv(env = process.env) {
9037
+ return { ...env, [UNATTENDED_ENV]: "1" };
9038
+ }
9039
+
9040
+ // src/daemon/self-heal.ts
8840
9041
  function kickDaemonEnsure(options = {}) {
8841
9042
  const primEntry = options.primEntry === void 0 ? binFile("prim") : options.primEntry;
8842
9043
  if (!primEntry) {
@@ -8844,14 +9045,18 @@ function kickDaemonEnsure(options = {}) {
8844
9045
  }
8845
9046
  try {
8846
9047
  const args = [primEntry, "daemon", "ensure"];
8847
- if ((options.platform ?? process.platform) === "darwin") {
9048
+ if (options.latestBootstrap ?? (options.platform ?? process.platform) === "darwin") {
8848
9049
  args.push("--latest-bootstrap");
8849
9050
  }
8850
9051
  const child = (options.spawnProcess ?? spawn)(
8851
9052
  options.nodeEntry ?? process.execPath,
8852
9053
  args,
8853
- { detached: true, stdio: "ignore" }
9054
+ // Unattended: neither SessionStart's repair nor a drift heal is a person
9055
+ // running prim, so the child (and its descendants) never heal in turn.
9056
+ { detached: true, stdio: "ignore", env: unattendedEnv() }
8854
9057
  );
9058
+ child.once("error", () => {
9059
+ });
8855
9060
  child.unref();
8856
9061
  return true;
8857
9062
  } catch {
@@ -27077,8 +27282,59 @@ async function fetchAndCacheCollectScope(cwd, dependencies = defaultDependencies
27077
27282
  return dependencies.writeCached(cwd, response);
27078
27283
  }
27079
27284
 
27080
- // src/lib/post-commit-hook.ts
27285
+ // src/lib/commit-heartbeat.ts
27081
27286
  import { execFileSync as execFileSync3 } from "child_process";
27287
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync11, readdirSync as readdirSync4, unlinkSync as unlinkSync2 } from "fs";
27288
+ import { dirname as dirname10, join as join15, resolve as resolve6 } from "path";
27289
+ var WIRED_PATH = "prim/git-hooks-wired";
27290
+ var GIT_TIMEOUT_MS3 = 1e3;
27291
+ var MAX_STAMP_BYTES = 1024;
27292
+ function commonPath(cwd, relative4) {
27293
+ const value = execFileSync3("git", ["rev-parse", "--git-common-dir"], {
27294
+ cwd,
27295
+ encoding: "utf8",
27296
+ stdio: ["ignore", "pipe", "ignore"],
27297
+ timeout: GIT_TIMEOUT_MS3
27298
+ }).replace(/\r?\n$/u, "");
27299
+ if (value === "" || /[\0\r\n]/u.test(value)) throw new Error("Git returned an unsafe path");
27300
+ return join15(resolve6(cwd, value), relative4);
27301
+ }
27302
+ function ensurePrivateParent(path) {
27303
+ mkdirSync7(dirname10(dirname10(path)), { recursive: true, mode: 448 });
27304
+ mkdirSync7(dirname10(path), { recursive: true, mode: 448 });
27305
+ }
27306
+ function writeStamp(path, at) {
27307
+ ensurePrivateParent(path);
27308
+ atomicWriteFile(path, `${JSON.stringify({ at })}
27309
+ `, { mode: 384 });
27310
+ }
27311
+ function readStampAt(path) {
27312
+ try {
27313
+ const raw = readFileSync11(path, "utf8");
27314
+ if (raw.length > MAX_STAMP_BYTES) return void 0;
27315
+ const parsed = JSON.parse(raw);
27316
+ return typeof parsed.at === "number" && Number.isSafeInteger(parsed.at) && parsed.at > 0 ? parsed.at : void 0;
27317
+ } catch {
27318
+ return void 0;
27319
+ }
27320
+ }
27321
+ function recordHooksWired(cwd, options = {}) {
27322
+ try {
27323
+ const path = commonPath(cwd, WIRED_PATH);
27324
+ if (options.onlyIfAbsent && readStampAt(path) !== void 0) return;
27325
+ writeStamp(path, options.now ?? Date.now());
27326
+ } catch {
27327
+ }
27328
+ }
27329
+ function clearHooksWired(cwd) {
27330
+ try {
27331
+ unlinkSync2(commonPath(cwd, WIRED_PATH));
27332
+ } catch {
27333
+ }
27334
+ }
27335
+
27336
+ // src/lib/git-hooks.ts
27337
+ import { execFileSync as execFileSync4 } from "child_process";
27082
27338
  import { createHash as createHash8, randomUUID as randomUUID2 } from "crypto";
27083
27339
  import {
27084
27340
  constants as constants5,
@@ -27087,50 +27343,28 @@ import {
27087
27343
  fchmodSync as fchmodSync2,
27088
27344
  fsyncSync as fsyncSync2,
27089
27345
  lstatSync as lstatSync5,
27090
- mkdirSync as mkdirSync7,
27346
+ mkdirSync as mkdirSync8,
27091
27347
  openSync as openSync4,
27092
- readFileSync as readFileSync11,
27348
+ readFileSync as readFileSync12,
27349
+ realpathSync as realpathSync4,
27093
27350
  renameSync as renameSync3,
27094
- unlinkSync as unlinkSync2,
27351
+ unlinkSync as unlinkSync3,
27095
27352
  writeFileSync as writeFileSync4
27096
27353
  } from "fs";
27097
- import { basename as basename2, dirname as dirname10, isAbsolute as isAbsolute7, resolve as resolve6 } from "path";
27098
- var PRIM_POST_COMMIT_BLOCK_START = "# >>> prim post-commit hook >>>";
27099
- var PRIM_POST_COMMIT_BLOCK_END = "# <<< prim post-commit hook <<<";
27100
- var PRIM_CREATED_MARK = "prim-created-post-commit-hook";
27101
- var PRIM_POST_REWRITE_BLOCK_START = "# >>> prim post-rewrite hook >>>";
27102
- var PRIM_POST_REWRITE_BLOCK_END = "# <<< prim post-rewrite hook <<<";
27103
- var PRIM_POST_REWRITE_CREATED_MARK = "prim-created-post-rewrite-hook";
27104
- var LEGACY_PRIM_CREATED_MARK = "prim-created-hook";
27105
- var LEGACY_PRIM_OWNED_HEADER = "# prim post-commit hook \u2014 installed by: prim hooks install (prim-managed-hook)";
27106
- var LEGACY_GLOBAL_OWNED_HEADER = "# prim global post-commit hook (core.hooksPath) \u2014 managed by prim; do not edit.";
27107
- var MAX_HOOK_BYTES = 1048576;
27108
- var GIT_TIMEOUT_MS3 = 1e3;
27109
- var SUPPORTED_SHEBANGS = /* @__PURE__ */ new Set([
27110
- "#!/bin/sh",
27111
- "#!/usr/bin/env sh",
27112
- "#!/bin/bash",
27113
- "#!/usr/bin/env bash",
27114
- "#!/bin/zsh",
27115
- "#!/usr/bin/env zsh"
27116
- ]);
27117
- var HUSKY_V9_DISPATCHERS = /* @__PURE__ */ new Set([
27118
- '#!/usr/bin/env sh\n. "${0%/*}/h"',
27119
- '#!/usr/bin/env sh\n. "$(dirname "$0")/h"'
27120
- ]);
27121
- var HUSKY_DIRECT_DISPATCHER = '#!/bin/sh\nexec sh "$(dirname "$0")/../post-commit"\n';
27122
- var HUSKY_V9_RUNTIME_HASHES = /* @__PURE__ */ new Set([
27123
- "d6aaa6f3f7c11008c525e649161df4a93152f1e55d0887fa1d7b8d622735d380",
27124
- "b02cb91cab7125a9e906be723a1b85297fc435a8307d429ae845546b4ee0ecde",
27125
- "9db5d129c4eb822a773157d98cd8fb8f1b156b85e3193117fefe527ca6d45e91",
27126
- "3b2cb21335e544b4ebce658ca47af4a69d2c77026cc4d3f8b2227c34d062b207",
27127
- "548990e1c11285694096184993d907b258d41235461a7ca2551e3b53ff9d3a38",
27128
- "c029a943acea5d6e2ddbaa271a1fb22c2da55d56b5c6fb59ca804ac9a4018708",
27129
- "ae3413a8fe2b39372de48bdc9691f42055f089c0c7529834b2fb07a131585b6a",
27130
- "70200b200ca709b0622784f93839a5b2872333a917a09afddefd7dc2d8cdc680"
27131
- ]);
27132
- function currentPostCommitBlock() {
27133
- return `${PRIM_POST_COMMIT_BLOCK_START}
27354
+ import { basename as basename2, dirname as dirname11, isAbsolute as isAbsolute8, join as join16, relative as relative3, resolve as resolve7, sep as sep3 } from "path";
27355
+
27356
+ // src/lib/git-hook-legacy.ts
27357
+ import { isAbsolute as isAbsolute7 } from "path";
27358
+ var PRIM_MANAGED_MARK = "prim-managed-hook";
27359
+ var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
27360
+ function blockMarkers2(spec) {
27361
+ return {
27362
+ start: `# >>> prim ${spec.hookName} hook >>>`,
27363
+ end: `# <<< prim ${spec.hookName} hook <<<`
27364
+ };
27365
+ }
27366
+ function legacyPostCommitBlock() {
27367
+ return `# >>> prim post-commit hook >>>
27134
27368
  if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
27135
27369
  prim_commit_sha=$(git rev-parse --verify HEAD 2>/dev/null) || prim_commit_sha=
27136
27370
  case "$prim_commit_sha" in *[!0-9a-f]*|"") prim_commit_sha= ;; esac
@@ -27159,13 +27393,10 @@ if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
27159
27393
  fi
27160
27394
  unset prim_commit_sha
27161
27395
  fi
27162
- ${PRIM_POST_COMMIT_BLOCK_END}`;
27396
+ # <<< prim post-commit hook <<<`;
27163
27397
  }
27164
- function postCommitHookBlock() {
27165
- return currentPostCommitBlock();
27166
- }
27167
- function postRewriteHookBlock() {
27168
- return `${PRIM_POST_REWRITE_BLOCK_START}
27398
+ function legacyPostRewriteBlock() {
27399
+ return `# >>> prim post-rewrite hook >>>
27169
27400
  if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
27170
27401
  case "$1" in amend|rebase) prim_rewrite_source="$1" ;; *) prim_rewrite_source= ;; esac
27171
27402
  if [ -n "$prim_rewrite_source" ]; then
@@ -27204,22 +27435,159 @@ if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
27204
27435
  fi
27205
27436
  unset prim_rewrite_source
27206
27437
  fi
27207
- ${PRIM_POST_REWRITE_BLOCK_END}`;
27208
- }
27209
- var POST_COMMIT_MANAGED_HOOK = {
27210
- hookName: "post-commit",
27211
- blockStart: PRIM_POST_COMMIT_BLOCK_START,
27212
- blockEnd: PRIM_POST_COMMIT_BLOCK_END,
27213
- createdMark: PRIM_CREATED_MARK,
27214
- block: postCommitHookBlock
27215
- };
27216
- var POST_REWRITE_MANAGED_HOOK = {
27217
- hookName: "post-rewrite",
27218
- blockStart: PRIM_POST_REWRITE_BLOCK_START,
27219
- blockEnd: PRIM_POST_REWRITE_BLOCK_END,
27220
- createdMark: PRIM_POST_REWRITE_CREATED_MARK,
27221
- block: postRewriteHookBlock
27438
+ # <<< prim post-rewrite hook <<<`;
27439
+ }
27440
+ var PRE_COMMIT_GATE = 'if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then';
27441
+ function preCommitLadder(fallback, failSoft) {
27442
+ const soft = failSoft ? " || true" : "";
27443
+ return `if command -v prim-pre-commit >/dev/null 2>&1; then
27444
+ prim-pre-commit${soft}
27445
+ elif [ -f "./node_modules/.bin/prim-pre-commit" ]; then
27446
+ ./node_modules/.bin/prim-pre-commit${soft}
27447
+ else
27448
+ ${fallback} 2>/dev/null || true
27449
+ fi`;
27450
+ }
27451
+ var LADDER_ALPHA_2 = preCommitLadder("npx --yes @primitive.ai/prim pre-commit-hook", false);
27452
+ var LADDER_ALPHA_9 = preCommitLadder("npx --yes -p @primitive.ai/prim prim-pre-commit", false);
27453
+ var LADDER_ALPHA_35 = preCommitLadder("npx --yes -p @primitive.ai/prim prim-pre-commit", true);
27454
+ var PINNED_NPX_PRE_COMMIT = String.raw`npx --yes(?: --ignore-scripts)? -p @primitive\.ai/prim@[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)? prim-pre-commit`;
27455
+ var PINNED_PRE_COMMIT_RE = new RegExp(
27456
+ String.raw`^\{ (?:${PINNED_NPX_PRE_COMMIT}|if \[ -x (?<node>'.*') \] && \[ -f (?<entry>'.*') \]; then \k<node> \k<entry>; else ${PINNED_NPX_PRE_COMMIT}; fi); \} \|\| true$`,
27457
+ "u"
27458
+ );
27459
+ var SHELL_QUOTED_RE = /^'(?:[^']|'"'"')*'$/u;
27460
+ function decodePinnedPath(token) {
27461
+ if (!token || !SHELL_QUOTED_RE.test(token)) return void 0;
27462
+ const value = token.slice(1, -1).replaceAll(`'"'"'`, "'");
27463
+ if (!isAbsolute7(value) || value.includes("\0") || value.includes("\n") || value.includes("\r")) {
27464
+ return void 0;
27465
+ }
27466
+ return value;
27467
+ }
27468
+ function isPinnedPreCommitInvocation(line) {
27469
+ const match = PINNED_PRE_COMMIT_RE.exec(line);
27470
+ if (!match) return false;
27471
+ if (match.groups?.node === void 0) return true;
27472
+ return Boolean(
27473
+ decodePinnedPath(match.groups.node) && decodePinnedPath(match.groups.entry)?.endsWith("/dist/hooks/pre-commit.js")
27474
+ );
27475
+ }
27476
+ function isLegacyPreCommitBody(body) {
27477
+ if (body === LADDER_ALPHA_2 || body === LADDER_ALPHA_9 || body === LADDER_ALPHA_35) return true;
27478
+ if (isPinnedPreCommitInvocation(body)) return true;
27479
+ const prefix = `${PRE_COMMIT_GATE}
27480
+ `;
27481
+ if (!body.startsWith(prefix) || !body.endsWith("\nfi")) return false;
27482
+ const inner = body.slice(prefix.length, -"\nfi".length);
27483
+ return inner === LADDER_ALPHA_35 || isPinnedPreCommitInvocation(inner);
27484
+ }
27485
+ function isLegacyOwnedPreCommitScript(content) {
27486
+ const original = (ladder) => `#!/bin/sh
27487
+ # prim pre-commit hook \u2014 auto-syncs affected specs on commit
27488
+ # Installed by: prim hooks install
27489
+
27490
+ # Find the nearest node_modules/.bin with prim, or use npx
27491
+ ${ladder}
27492
+ `;
27493
+ if (content === original(LADDER_ALPHA_2) || content === original(LADDER_ALPHA_9)) return true;
27494
+ if (content === `#!/bin/sh
27495
+ # prim pre-commit hook \u2014 installed by: prim hooks install
27496
+
27497
+ ${LADDER_ALPHA_9}
27498
+ `) {
27499
+ return true;
27500
+ }
27501
+ const prefix = `#!/bin/sh
27502
+ # prim pre-commit hook \u2014 installed by: prim hooks install (${PRIM_MANAGED_MARK})
27503
+
27504
+ `;
27505
+ if (!content.startsWith(prefix) || !content.endsWith("\n")) return false;
27506
+ const body = content.slice(prefix.length, -1);
27507
+ return body === LADDER_ALPHA_35 || isPinnedPreCommitInvocation(body);
27508
+ }
27509
+ var PINNED_PACKAGE_VERSION_RE = /@primitive\.ai\/prim@[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?/gu;
27510
+ function legacyInlineHookBlock(hookName) {
27511
+ return hookName === "post-commit" ? legacyPostCommitBlock() : legacyPostRewriteBlock();
27512
+ }
27513
+ function isRecognizedLegacyBlock(hookName, block) {
27514
+ if (hookName === "pre-commit") {
27515
+ const { start, end } = blockMarkers2(PRE_COMMIT);
27516
+ const prefix = `${start}
27517
+ `;
27518
+ const suffix = `
27519
+ ${end}`;
27520
+ if (!block.startsWith(prefix) || !block.endsWith(suffix)) return false;
27521
+ return isLegacyPreCommitBody(block.slice(prefix.length, -suffix.length));
27522
+ }
27523
+ const expected = legacyInlineHookBlock(hookName);
27524
+ const normalize2 = (text) => text.replace(PINNED_PACKAGE_VERSION_RE, "@primitive.ai/prim@<version>");
27525
+ return normalize2(block) === normalize2(expected);
27526
+ }
27527
+
27528
+ // src/lib/git-hooks.ts
27529
+ var LEGACY_PRIM_CREATED_MARK = "prim-created-hook";
27530
+ var LEGACY_PRIM_OWNED_HEADER = "# prim post-commit hook \u2014 installed by: prim hooks install (prim-managed-hook)";
27531
+ var LEGACY_GLOBAL_OWNED_HEADER = "# prim global post-commit hook (core.hooksPath) \u2014 managed by prim; do not edit.";
27532
+ var MAX_HOOK_BYTES = 1048576;
27533
+ var GIT_TIMEOUT_MS4 = 1e3;
27534
+ var SUPPORTED_SHEBANGS = /* @__PURE__ */ new Set([
27535
+ "#!/bin/sh",
27536
+ "#!/usr/bin/env sh",
27537
+ "#!/bin/bash",
27538
+ "#!/usr/bin/env bash",
27539
+ "#!/bin/zsh",
27540
+ "#!/usr/bin/env zsh"
27541
+ ]);
27542
+ var HUSKY_V9_DISPATCHERS = /* @__PURE__ */ new Set([
27543
+ '#!/usr/bin/env sh\n. "${0%/*}/h"',
27544
+ '#!/usr/bin/env sh\n. "$(dirname "$0")/h"'
27545
+ ]);
27546
+ var HUSKY_DIRECT_DISPATCHER = '#!/bin/sh\nexec sh "$(dirname "$0")/../post-commit"\n';
27547
+ var HUSKY_V9_RUNTIME_HASHES = /* @__PURE__ */ new Set([
27548
+ "d6aaa6f3f7c11008c525e649161df4a93152f1e55d0887fa1d7b8d622735d380",
27549
+ "b02cb91cab7125a9e906be723a1b85297fc435a8307d429ae845546b4ee0ecde",
27550
+ "9db5d129c4eb822a773157d98cd8fb8f1b156b85e3193117fefe527ca6d45e91",
27551
+ "3b2cb21335e544b4ebce658ca47af4a69d2c77026cc4d3f8b2227c34d062b207",
27552
+ "548990e1c11285694096184993d907b258d41235461a7ca2551e3b53ff9d3a38",
27553
+ "c029a943acea5d6e2ddbaa271a1fb22c2da55d56b5c6fb59ca804ac9a4018708",
27554
+ "ae3413a8fe2b39372de48bdc9691f42055f089c0c7529834b2fb07a131585b6a",
27555
+ "70200b200ca709b0622784f93839a5b2872333a917a09afddefd7dc2d8cdc680"
27556
+ ]);
27557
+ function managedHookSpecFor(hookName) {
27558
+ const { start, end } = blockMarkers(hookName);
27559
+ return {
27560
+ hookName,
27561
+ blockStart: start,
27562
+ blockEnd: end,
27563
+ createdMark: `prim-created-${hookName}-hook`
27564
+ };
27565
+ }
27566
+ var MANAGED_HOOK_SPECS = {
27567
+ "pre-commit": managedHookSpecFor("pre-commit"),
27568
+ "post-commit": managedHookSpecFor("post-commit"),
27569
+ "post-rewrite": managedHookSpecFor("post-rewrite")
27222
27570
  };
27571
+ function managedHookSpec(hookName) {
27572
+ return MANAGED_HOOK_SPECS[hookName];
27573
+ }
27574
+ function gitHooksMode(options = {}) {
27575
+ try {
27576
+ const value = execFileSync4(
27577
+ "git",
27578
+ ["config", ...options.global ? ["--global"] : [], "--get", "prim.gitHooks"],
27579
+ {
27580
+ cwd: options.cwd,
27581
+ encoding: "utf8",
27582
+ stdio: ["ignore", "pipe", "ignore"],
27583
+ timeout: GIT_TIMEOUT_MS4
27584
+ }
27585
+ ).trim();
27586
+ return value === "manual" ? "manual" : "auto";
27587
+ } catch {
27588
+ return "auto";
27589
+ }
27590
+ }
27223
27591
  function legacyFloatingInvocation() {
27224
27592
  return `if command -v prim-post-commit >/dev/null 2>&1; then
27225
27593
  prim-post-commit || true
@@ -27236,15 +27604,6 @@ fi
27236
27604
  `);
27237
27605
  }
27238
27606
  var PINNED_INVOCATION_RE = /^\{ if \[ -x (?<node>'.*') \] && \[ -f (?<entry>'.*') \]; then \k<node> \k<entry>; else npx --yes -p @primitive\.ai\/prim@(?<version>[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?) prim-post-commit; fi; \} \|\| true$/u;
27239
- var SHELL_QUOTED_RE = /^'(?:[^']|'"'"')*'$/u;
27240
- function decodePinnedPath(token) {
27241
- if (!token || !SHELL_QUOTED_RE.test(token)) return void 0;
27242
- const value = token.slice(1, -1).replaceAll(`'"'"'`, "'");
27243
- if (!isAbsolute7(value) || value.includes("\0") || value.includes("\n") || value.includes("\r")) {
27244
- return void 0;
27245
- }
27246
- return value;
27247
- }
27248
27607
  function isPinnedInvocation(value) {
27249
27608
  const match = PINNED_INVOCATION_RE.exec(value);
27250
27609
  const node = decodePinnedPath(match?.groups?.node);
@@ -27277,48 +27636,122 @@ function isKnownLegacyGlobalGate(value) {
27277
27636
  }
27278
27637
  function createdScaffold(spec) {
27279
27638
  return Buffer.from(`#!/bin/sh
27280
- ${spec.block()}
27639
+ ${managedHookBlock(spec.hookName)}
27281
27640
  # ${spec.createdMark}
27282
27641
  `);
27283
27642
  }
27643
+ function oldCreatedPrefixes(spec) {
27644
+ return [
27645
+ Buffer.from(`#!/bin/sh
27646
+ # ${spec.createdMark}
27647
+
27648
+ `),
27649
+ Buffer.from(`#!/bin/sh
27650
+ # ${LEGACY_PRIM_CREATED_MARK}
27651
+
27652
+ `)
27653
+ ];
27654
+ }
27655
+ function isOwnedStandalonePreCommit(content) {
27656
+ return content === "#!/bin/sh\nprim-pre-commit\n" || isLegacyOwnedPreCommitScript(content);
27657
+ }
27658
+ function realpathLoose(path) {
27659
+ const missing = [];
27660
+ let current = resolve7(path);
27661
+ for (; ; ) {
27662
+ try {
27663
+ return join16(realpathSync4.native(current), ...missing);
27664
+ } catch {
27665
+ const parent = dirname11(current);
27666
+ if (parent === current) return resolve7(path);
27667
+ missing.unshift(basename2(current));
27668
+ current = parent;
27669
+ }
27670
+ }
27671
+ }
27672
+ function isWithin(path, directory) {
27673
+ const rel = relative3(directory, path);
27674
+ return rel === "" || !isAbsolute8(rel) && rel !== ".." && !rel.startsWith(`..${sep3}`);
27675
+ }
27676
+ function primGitHooksDirectory() {
27677
+ try {
27678
+ return join16(primConfigDirectory(), "git-hooks");
27679
+ } catch {
27680
+ return void 0;
27681
+ }
27682
+ }
27683
+ function otherWorktreeRoots(gitRoot) {
27684
+ try {
27685
+ const self = realpathLoose(gitRoot);
27686
+ return execFileSync4("git", ["worktree", "list", "--porcelain"], {
27687
+ cwd: gitRoot,
27688
+ encoding: "utf8",
27689
+ stdio: ["ignore", "pipe", "ignore"],
27690
+ timeout: GIT_TIMEOUT_MS4
27691
+ }).split("\n").flatMap((line) => line.startsWith("worktree ") ? [realpathLoose(line.slice(9))] : []).filter((root) => root !== self);
27692
+ } catch {
27693
+ return [];
27694
+ }
27695
+ }
27696
+ function hookLocation(hookPath, dirs) {
27697
+ const path = realpathLoose(hookPath);
27698
+ if (isWithin(path, realpathLoose(dirs.gitDir)) || isWithin(path, realpathLoose(dirs.commonDir))) {
27699
+ return "repository";
27700
+ }
27701
+ const primDir = primGitHooksDirectory();
27702
+ if (primDir !== void 0 && isWithin(path, realpathLoose(primDir))) return "prim";
27703
+ const root = realpathLoose(dirs.gitRoot);
27704
+ if (!isWithin(path, root)) return "external";
27705
+ const nested = otherWorktreeRoots(dirs.gitRoot).filter((other) => isWithin(other, root));
27706
+ return nested.some((other) => isWithin(path, other)) ? "external" : "worktree";
27707
+ }
27284
27708
  function safeGitPath(root, value) {
27285
27709
  if (value.length === 0 || value !== value.trim() || value.includes("\0") || value.includes("\n") || value.includes("\r")) {
27286
27710
  throw new Error("Git returned an unsafe hooks path");
27287
27711
  }
27288
- return isAbsolute7(value) ? resolve6(value) : resolve6(root, value);
27712
+ return isAbsolute8(value) ? resolve7(value) : resolve7(root, value);
27289
27713
  }
27290
- function resolveEffectiveManagedHook(spec, cwd = process.cwd()) {
27714
+ function resolveEffectiveGitHook(hookName, cwd = process.cwd()) {
27291
27715
  const gitRoot = gitToplevel(cwd);
27292
27716
  if (!gitRoot) throw new Error("not a git repository");
27293
- const value = execFileSync3("git", ["rev-parse", "--git-path", "hooks"], {
27294
- cwd: gitRoot,
27295
- encoding: "utf8",
27296
- stdio: ["ignore", "pipe", "ignore"],
27297
- timeout: GIT_TIMEOUT_MS3
27298
- }).replace(/\r?\n$/u, "");
27299
- const hooksDir = safeGitPath(gitRoot, value);
27300
- if (basename2(hooksDir) === "_" && basename2(dirname10(hooksDir)) === ".husky") {
27717
+ const lines = execFileSync4(
27718
+ "git",
27719
+ ["rev-parse", "--git-path", "hooks", "--absolute-git-dir", "--git-common-dir"],
27720
+ {
27721
+ cwd: gitRoot,
27722
+ encoding: "utf8",
27723
+ stdio: ["ignore", "pipe", "ignore"],
27724
+ timeout: GIT_TIMEOUT_MS4
27725
+ }
27726
+ ).replace(/\r?\n$/u, "").split(/\r?\n/u);
27727
+ if (lines.length !== 3) throw new Error("Git returned unexpected repository paths");
27728
+ const [hooksValue, gitDirValue, commonDirValue] = lines;
27729
+ const hooksDir = safeGitPath(gitRoot, hooksValue);
27730
+ const dirs = {
27731
+ gitRoot,
27732
+ gitDir: safeGitPath(gitRoot, gitDirValue),
27733
+ commonDir: safeGitPath(gitRoot, commonDirValue)
27734
+ };
27735
+ if (basename2(hooksDir) === "_" && basename2(dirname11(hooksDir)) === ".husky") {
27736
+ const hookPath2 = resolve7(dirname11(hooksDir), hookName);
27301
27737
  return {
27302
27738
  gitRoot,
27303
27739
  hooksDir,
27304
- hookPath: resolve6(dirname10(hooksDir), spec.hookName),
27740
+ hookPath: hookPath2,
27305
27741
  kind: "husky_v9",
27306
- dispatcherPath: resolve6(hooksDir, spec.hookName)
27742
+ dispatcherPath: resolve7(hooksDir, hookName),
27743
+ location: hookLocation(hookPath2, dirs)
27307
27744
  };
27308
27745
  }
27746
+ const hookPath = resolve7(hooksDir, hookName);
27309
27747
  return {
27310
27748
  gitRoot,
27311
27749
  hooksDir,
27312
- hookPath: resolve6(hooksDir, spec.hookName),
27313
- kind: "direct"
27750
+ hookPath,
27751
+ kind: "direct",
27752
+ location: hookLocation(hookPath, dirs)
27314
27753
  };
27315
27754
  }
27316
- function resolveEffectivePostCommitHook(cwd = process.cwd()) {
27317
- return resolveEffectiveManagedHook(POST_COMMIT_MANAGED_HOOK, cwd);
27318
- }
27319
- function resolveEffectivePostRewriteHook(cwd = process.cwd()) {
27320
- return resolveEffectiveManagedHook(POST_REWRITE_MANAGED_HOOK, cwd);
27321
- }
27322
27755
  function assertSafeFile(path, spec) {
27323
27756
  if (!existsSync10(path)) return void 0;
27324
27757
  const stat = lstatSync5(path);
@@ -27342,7 +27775,7 @@ function assertUsableHuskyDispatcher(target, spec) {
27342
27775
  if (!HUSKY_V9_DISPATCHERS.has(normalizedDispatcher)) {
27343
27776
  throw new Error(`unrecognized Husky ${spec.hookName} dispatcher: ${target.dispatcherPath}`);
27344
27777
  }
27345
- const huskyRuntime = resolve6(dirname10(target.dispatcherPath), "h");
27778
+ const huskyRuntime = resolve7(dirname11(target.dispatcherPath), "h");
27346
27779
  const runtimeStat = assertSafeFile(huskyRuntime, spec);
27347
27780
  const runtime = runtimeStat ? readHookFile(huskyRuntime) : void 0;
27348
27781
  const runtimeHash = runtime ? createHash8("sha256").update(runtime).digest("hex") : void 0;
@@ -27351,7 +27784,7 @@ function assertUsableHuskyDispatcher(target, spec) {
27351
27784
  }
27352
27785
  }
27353
27786
  function readHookFile(path) {
27354
- const value = readFileSync11(path);
27787
+ const value = readFileSync12(path);
27355
27788
  return Buffer.isBuffer(value) ? value : Buffer.from(value);
27356
27789
  }
27357
27790
  function decodeHookText(content, spec) {
@@ -27400,16 +27833,107 @@ function blockRange(content, spec) {
27400
27833
  }
27401
27834
  const start = starts[0];
27402
27835
  const end = ends[0] + endMarker.length;
27403
- const current = Buffer.from(spec.block());
27404
- return content.subarray(start, end).equals(current) ? { kind: "current", start, end } : { kind: "stale", start, end };
27836
+ const text = content.subarray(start, end).toString("utf8");
27837
+ if (canonicalHookBlock(text) === canonicalHookBlock(managedHookBlock(spec.hookName))) {
27838
+ return { kind: "current", start, end };
27839
+ }
27840
+ const version = hookBlockContractVersion(text);
27841
+ if (version !== void 0 && version > GIT_HOOK_CONTRACT_VERSION) {
27842
+ return { kind: "newer", start, end };
27843
+ }
27844
+ return { kind: "stale", start, end };
27845
+ }
27846
+ var HUSKY_V8_SOURCE_RE = /^\.[ \t]+"\$\(dirname(?:[ \t]+--)?[ \t]+"\$0"\)\/_\/husky\.sh"[ \t]*$/mu;
27847
+ function blockInsertionPoint(content, allowShebangless, spec, huskyV8) {
27848
+ const shebangEnd = shellInsertionPoint(content, allowShebangless, spec);
27849
+ if (!huskyV8) return shebangEnd;
27850
+ const rest = content.subarray(shebangEnd).toString("utf8");
27851
+ const match = HUSKY_V8_SOURCE_RE.exec(rest);
27852
+ if (!match) return shebangEnd;
27853
+ const lineEnd = match.index + match[0].length;
27854
+ const afterLine = rest[lineEnd] === "\n" ? lineEnd + 1 : lineEnd;
27855
+ return shebangEnd + Buffer.byteLength(rest.slice(0, afterLine), "utf8");
27856
+ }
27857
+ var HUSKY_SPEC_MAJOR_RE = /^\s*[~^]?=?\s*v?([0-9]+)(?:\.(?:[0-9]+|[xX*])){0,2}(?:-[0-9A-Za-z.-]+)?\s*$/u;
27858
+ function declaredHuskyMajor(dir) {
27859
+ for (let current = resolve7(dir); ; ) {
27860
+ const manifest = join16(current, "package.json");
27861
+ if (existsSync10(manifest)) {
27862
+ try {
27863
+ if (lstatSync5(manifest).size > MAX_HOOK_BYTES) return void 0;
27864
+ const parsed = JSON.parse(readFileSync12(manifest, "utf8"));
27865
+ const range = parsed.devDependencies?.husky ?? parsed.dependencies?.husky;
27866
+ const major = typeof range === "string" ? HUSKY_SPEC_MAJOR_RE.exec(range)?.[1] : void 0;
27867
+ return major === void 0 ? void 0 : Number(major);
27868
+ } catch {
27869
+ return void 0;
27870
+ }
27871
+ }
27872
+ const parent = dirname11(current);
27873
+ if (parent === current || existsSync10(join16(current, ".git"))) return void 0;
27874
+ current = parent;
27875
+ }
27876
+ }
27877
+ function isHuskyV8Hook(hookPath) {
27878
+ const runtime = join16(dirname11(hookPath), "_", "husky.sh");
27879
+ try {
27880
+ const stat = lstatSync5(runtime);
27881
+ return stat.isFile() && stat.size <= MAX_HOOK_BYTES && readFileSync12(runtime, "utf8").includes("husky_skip_init");
27882
+ } catch (error) {
27883
+ if (errorCode(error) !== "ENOENT") return false;
27884
+ }
27885
+ const major = declaredHuskyMajor(dirname11(dirname11(hookPath)));
27886
+ return major !== void 0 && major <= 8;
27887
+ }
27888
+ var TERMINAL_LINE_RE = /^(?:exit|return)(?:\s|;|$)/u;
27889
+ var REDIRECT_OPERATOR_RE = /^[0-9]*(?:<<?|>>?|<&|>&|<>|>\|)$/u;
27890
+ var REDIRECT_WORD_RE = /^[0-9]*(?:<|>)/u;
27891
+ function isReplacingExec(line) {
27892
+ if (!/^exec(?:\s|$)/u.test(line)) return false;
27893
+ const words = line.slice(4).trim().split(/\s+/u).filter(Boolean);
27894
+ for (let index = 0; index < words.length; index += 1) {
27895
+ const word = words[index];
27896
+ if (word.startsWith("#")) break;
27897
+ if (REDIRECT_OPERATOR_RE.test(word)) {
27898
+ index += 1;
27899
+ continue;
27900
+ }
27901
+ if (!REDIRECT_WORD_RE.test(word)) return true;
27902
+ }
27903
+ return false;
27904
+ }
27905
+ function terminatesBefore(content, from, offset) {
27906
+ const lines = content.subarray(from, offset).toString("utf8").split("\n");
27907
+ return lines.some((line) => TERMINAL_LINE_RE.test(line) || isReplacingExec(line));
27908
+ }
27909
+ function mayTerminateBefore(content, from, offset) {
27910
+ return content.subarray(from, offset).toString("utf8").split(/\n|;|&&|\|\||\||[{()]|\b(?:then|do|else)\b/u).some((part) => {
27911
+ const command = part.trim();
27912
+ return TERMINAL_LINE_RE.test(command) || isReplacingExec(command);
27913
+ });
27914
+ }
27915
+ function withoutRange(content, start, end) {
27916
+ const suffix = content.subarray(end);
27917
+ return Buffer.concat([
27918
+ content.subarray(0, start),
27919
+ suffix.at(0) === 10 ? suffix.subarray(1) : suffix
27920
+ ]);
27921
+ }
27922
+ function insertBlock(content, at, spec) {
27923
+ return Buffer.concat([
27924
+ content.subarray(0, at),
27925
+ Buffer.from(managedHookBlock(spec.hookName)),
27926
+ Buffer.from("\n"),
27927
+ content.subarray(at)
27928
+ ]);
27405
27929
  }
27406
- function mergedContent(existing, allowShebangless, spec) {
27407
- const block = Buffer.from(spec.block());
27408
- if (!existing) {
27930
+ function migratedLegacyContent(existing, allowShebangless, spec, huskyV8) {
27931
+ const text = existing.toString("utf8");
27932
+ if (spec.hookName === "pre-commit" && isOwnedStandalonePreCommit(text)) {
27409
27933
  return createdScaffold(spec);
27410
27934
  }
27411
- const range = blockRange(existing, spec);
27412
- if (spec.hookName === POST_COMMIT_MANAGED_HOOK.hookName && range.kind === "absent" && existing.toString("utf8").startsWith(`#!/bin/sh
27935
+ if (spec.hookName !== "post-commit") return void 0;
27936
+ if (text.startsWith(`#!/bin/sh
27413
27937
  ${LEGACY_GLOBAL_OWNED_HEADER}
27414
27938
  `)) {
27415
27939
  const gateAt = existing.indexOf(Buffer.from('if [ "$(git config --get prim.active'));
@@ -27424,15 +27948,13 @@ ${LEGACY_GLOBAL_OWNED_HEADER}
27424
27948
  existing.subarray(0, gateAt),
27425
27949
  existing.subarray(chainAt)
27426
27950
  ]);
27427
- const shebangEnd2 = shellInsertionPoint(withoutLegacyGate, allowShebangless, spec);
27428
- return Buffer.concat([
27429
- withoutLegacyGate.subarray(0, shebangEnd2),
27430
- block,
27431
- Buffer.from("\n"),
27432
- withoutLegacyGate.subarray(shebangEnd2)
27433
- ]);
27951
+ return insertBlock(
27952
+ withoutLegacyGate,
27953
+ blockInsertionPoint(withoutLegacyGate, allowShebangless, spec, huskyV8),
27954
+ spec
27955
+ );
27434
27956
  }
27435
- if (spec.hookName === POST_COMMIT_MANAGED_HOOK.hookName && range.kind === "absent" && existing.toString("utf8").startsWith(`#!/bin/sh
27957
+ if (text.startsWith(`#!/bin/sh
27436
27958
  ${LEGACY_PRIM_OWNED_HEADER}
27437
27959
  `)) {
27438
27960
  const prefixLength = legacyProjectPrefixLength(existing);
@@ -27441,42 +27963,59 @@ ${LEGACY_PRIM_OWNED_HEADER}
27441
27963
  }
27442
27964
  return Buffer.concat([createdScaffold(spec), existing.subarray(prefixLength)]);
27443
27965
  }
27444
- if (range.kind !== "absent") {
27445
- const oldCreatedPrefixes = [
27446
- Buffer.from(`#!/bin/sh
27447
- # ${spec.createdMark}
27448
-
27449
- `),
27450
- ...spec.hookName === POST_COMMIT_MANAGED_HOOK.hookName ? [Buffer.from(`#!/bin/sh
27451
- # ${LEGACY_PRIM_CREATED_MARK}
27452
-
27453
- `)] : []
27454
- ];
27455
- const oldCreated = oldCreatedPrefixes.some(
27966
+ return void 0;
27967
+ }
27968
+ function mergedContent(existing, allowShebangless, spec, relocate, huskyV8) {
27969
+ if (!existing) return { content: createdScaffold(spec), migrates: false };
27970
+ const range = blockRange(existing, spec);
27971
+ if (range.kind === "absent") {
27972
+ const migrated = migratedLegacyContent(existing, allowShebangless, spec, huskyV8);
27973
+ if (migrated) return { content: migrated, migrates: true };
27974
+ } else if (range.kind !== "newer") {
27975
+ const oldCreated = oldCreatedPrefixes(spec).some(
27456
27976
  (prefix) => range.start === prefix.length && existing.subarray(0, prefix.length).equals(prefix)
27457
27977
  );
27458
27978
  if (oldCreated) {
27459
27979
  const suffix = existing.subarray(range.end);
27460
27980
  const tail = suffix.at(0) === 10 ? suffix.subarray(1) : suffix;
27461
- return Buffer.concat([createdScaffold(spec), tail]);
27981
+ return {
27982
+ content: Buffer.concat([createdScaffold(spec), tail]),
27983
+ migrates: range.kind === "stale"
27984
+ };
27462
27985
  }
27463
27986
  }
27464
27987
  const shebangEnd = shellInsertionPoint(existing, allowShebangless, spec);
27465
- if (range.kind === "current" && range.start === shebangEnd) return existing;
27466
- let withoutBlock = existing;
27467
- if (range.kind !== "absent") {
27468
- const suffix = existing.subarray(range.end);
27469
- withoutBlock = Buffer.concat([
27470
- existing.subarray(0, range.start),
27471
- range.start === shebangEnd && suffix.at(0) === 10 ? suffix.subarray(1) : suffix
27472
- ]);
27988
+ const insertion = blockInsertionPoint(existing, allowShebangless, spec, huskyV8);
27989
+ switch (range.kind) {
27990
+ case "newer":
27991
+ return { content: existing, migrates: false };
27992
+ case "absent":
27993
+ return { content: insertBlock(existing, insertion, spec), migrates: false };
27994
+ case "current":
27995
+ case "stale": {
27996
+ const misplaced = terminatesBefore(existing, shebangEnd, range.start) || range.start < insertion;
27997
+ if (relocate && misplaced) {
27998
+ const without = withoutRange(existing, range.start, range.end);
27999
+ return {
28000
+ content: insertBlock(
28001
+ without,
28002
+ blockInsertionPoint(without, allowShebangless, spec, huskyV8),
28003
+ spec
28004
+ ),
28005
+ migrates: range.kind === "stale"
28006
+ };
28007
+ }
28008
+ if (range.kind === "current") return { content: existing, migrates: false };
28009
+ return {
28010
+ content: Buffer.concat([
28011
+ existing.subarray(0, range.start),
28012
+ Buffer.from(managedHookBlock(spec.hookName)),
28013
+ existing.subarray(range.end)
28014
+ ]),
28015
+ migrates: true
28016
+ };
28017
+ }
27473
28018
  }
27474
- return Buffer.concat([
27475
- withoutBlock.subarray(0, shebangEnd),
27476
- block,
27477
- Buffer.from("\n"),
27478
- withoutBlock.subarray(shebangEnd)
27479
- ]);
27480
28019
  }
27481
28020
  function errorCode(error) {
27482
28021
  return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
@@ -27497,9 +28036,9 @@ function assertTargetUnchanged(path, expected, spec, stat) {
27497
28036
  }
27498
28037
  }
27499
28038
  function rewriteHookAtomically(path, next, expected, spec, stat) {
27500
- const parent = dirname10(path);
27501
- mkdirSync7(parent, { recursive: true });
27502
- const temporary = resolve6(
28039
+ const parent = dirname11(path);
28040
+ mkdirSync8(parent, { recursive: true });
28041
+ const temporary = resolve7(
27503
28042
  parent,
27504
28043
  `.${basename2(path)}.prim-${String(process.pid)}-${randomUUID2()}.tmp`
27505
28044
  );
@@ -27525,41 +28064,71 @@ function rewriteHookAtomically(path, next, expected, spec, stat) {
27525
28064
  }
27526
28065
  }
27527
28066
  try {
27528
- unlinkSync2(temporary);
28067
+ unlinkSync3(temporary);
27529
28068
  } catch (error) {
27530
28069
  if (errorCode(error) !== "ENOENT") {
27531
28070
  }
27532
28071
  }
27533
28072
  }
27534
28073
  }
27535
- function ensureEffectivePostCommitHook(cwd = process.cwd()) {
27536
- const target = resolveEffectivePostCommitHook(cwd);
27537
- return ensureTarget(target, POST_COMMIT_MANAGED_HOOK);
27538
- }
27539
- function ensureEffectivePostRewriteHook(cwd = process.cwd()) {
27540
- const target = resolveEffectivePostRewriteHook(cwd);
27541
- return ensureTarget(target, POST_REWRITE_MANAGED_HOOK);
28074
+ function workingLegacyBlock(content, target, spec) {
28075
+ try {
28076
+ const range = blockRange(content, spec);
28077
+ if (range.kind !== "stale") return false;
28078
+ const block = content.subarray(range.start, range.end).toString("utf8");
28079
+ const from = shellInsertionPoint(content, target.kind === "husky_v9", spec);
28080
+ return isRecognizedLegacyBlock(spec.hookName, block) && !mayTerminateBefore(content, from, range.start);
28081
+ } catch {
28082
+ return false;
28083
+ }
27542
28084
  }
27543
- function ensureTarget(target, spec, initialContent) {
28085
+ var AMBIENT_LOCATIONS = ["repository", "prim"];
28086
+ var EXPLICIT_LOCATIONS = ["repository", "prim", "worktree"];
28087
+ function ensureTarget(target, spec, policy) {
28088
+ const result = { hookName: spec.hookName, path: target.hookPath, kind: target.kind };
28089
+ const skip = (outcome) => ({
28090
+ ...result,
28091
+ changed: false,
28092
+ outcome
28093
+ });
27544
28094
  assertUsableHuskyDispatcher(target, spec);
27545
28095
  const stat = assertSafeFile(target.hookPath, spec);
27546
28096
  if (stat && target.kind === "direct" && (stat.mode & 64) === 0) {
27547
28097
  throw new Error(`existing ${spec.hookName} hook is not executable: ${target.hookPath}`);
27548
28098
  }
27549
28099
  const existing = stat ? readHookFile(target.hookPath) : void 0;
27550
- const next = !existing && initialContent !== void 0 ? Buffer.from(initialContent) : mergedContent(existing, target.kind === "husky_v9", spec);
27551
- if (!existing && initialContent !== void 0) {
27552
- const shebangEnd = shellInsertionPoint(next, target.kind === "husky_v9", spec);
27553
- const range = blockRange(next, spec);
27554
- if (range.kind !== "current" || range.start !== shebangEnd) {
27555
- throw new Error(`invalid initial Prim ${spec.hookName} scaffold`);
27556
- }
27557
- }
27558
- const contentChanged = !existing || !next.equals(existing);
27559
- if (contentChanged) {
27560
- rewriteHookAtomically(target.hookPath, next, existing, spec, stat);
28100
+ if (policy.repairOnly && (!existing || blockRange(existing, spec).kind === "absent")) {
28101
+ return skip("skipped");
28102
+ }
28103
+ const huskyV8 = isHuskyV8Hook(target.hookPath);
28104
+ const next = mergedContent(existing, target.kind === "husky_v9", spec, policy.relocate, huskyV8);
28105
+ if (existing && next.content.equals(existing)) return skip("unchanged");
28106
+ if (!policy.writable(target.location)) {
28107
+ if (target.location === "worktree") return skip("deferred");
28108
+ return skip(existing && workingLegacyBlock(existing, target, spec) ? "kept" : "external");
28109
+ }
28110
+ if (next.migrates && inspectGitHookEntrypoint() !== "ready") return skip("runtime_missing");
28111
+ rewriteHookAtomically(target.hookPath, next.content, existing, spec, stat);
28112
+ return { ...result, changed: true, outcome: existing ? "updated" : "created" };
28113
+ }
28114
+ function ensureEffectiveGitHook(hookName, cwd = process.cwd(), options = {}) {
28115
+ const target = resolveEffectiveGitHook(hookName, cwd);
28116
+ if (gitHooksMode({ cwd: target.gitRoot }) === "manual") {
28117
+ return {
28118
+ hookName,
28119
+ path: target.hookPath,
28120
+ kind: target.kind,
28121
+ changed: false,
28122
+ outcome: "manual"
28123
+ };
27561
28124
  }
27562
- return { path: target.hookPath, changed: contentChanged || !stat, kind: target.kind };
28125
+ const explicit = (options.context ?? "explicit") === "explicit";
28126
+ const allowed = explicit ? EXPLICIT_LOCATIONS : AMBIENT_LOCATIONS;
28127
+ return ensureTarget(target, managedHookSpec(hookName), {
28128
+ writable: (location) => allowed.includes(location),
28129
+ relocate: explicit,
28130
+ repairOnly: options.repairOnly === true
28131
+ });
27563
28132
  }
27564
28133
 
27565
28134
  // src/lib/repository-binding.ts
@@ -27619,7 +28188,7 @@ async function bindRepository(root, options) {
27619
28188
  }
27620
28189
 
27621
28190
  // src/lib/workspace-id.ts
27622
- import { execFileSync as execFileSync4 } from "child_process";
28191
+ import { execFileSync as execFileSync5 } from "child_process";
27623
28192
  import { randomUUID as randomUUID3 } from "crypto";
27624
28193
  import {
27625
28194
  constants as constants6,
@@ -27629,15 +28198,15 @@ import {
27629
28198
  fsyncSync as fsyncSync3,
27630
28199
  linkSync,
27631
28200
  lstatSync as lstatSync6,
27632
- mkdirSync as mkdirSync8,
28201
+ mkdirSync as mkdirSync9,
27633
28202
  openSync as openSync5,
27634
- readFileSync as readFileSync12,
28203
+ readFileSync as readFileSync13,
27635
28204
  statSync as statSync3,
27636
- unlinkSync as unlinkSync3,
28205
+ unlinkSync as unlinkSync4,
27637
28206
  writeFileSync as writeFileSync5
27638
28207
  } from "fs";
27639
- import { dirname as dirname11, isAbsolute as isAbsolute8, join as join15, resolve as resolve7 } from "path";
27640
- var WORKSPACE_ID_RELATIVE_PATH = join15("prim", "workspace-id");
28208
+ import { dirname as dirname12, isAbsolute as isAbsolute9, join as join17, resolve as resolve8 } from "path";
28209
+ var WORKSPACE_ID_RELATIVE_PATH = join17("prim", "workspace-id");
27641
28210
  var MAX_ANCESTORS = 64;
27642
28211
  var MAX_GIT_FILE_BYTES = 4096;
27643
28212
  var MAX_ID_FILE_BYTES = 128;
@@ -27665,7 +28234,7 @@ function resolveWorkspaceIdPath(cwd) {
27665
28234
  if (fromFilesystem) return { status: "resolved", path: fromFilesystem };
27666
28235
  }
27667
28236
  try {
27668
- const output = execFileSync4(
28237
+ const output = execFileSync5(
27669
28238
  "git",
27670
28239
  ["rev-parse", "--is-inside-work-tree", "--git-path", WORKSPACE_ID_RELATIVE_PATH],
27671
28240
  {
@@ -27683,7 +28252,7 @@ function resolveWorkspaceIdPath(cwd) {
27683
28252
  }
27684
28253
  return {
27685
28254
  status: "resolved",
27686
- path: isAbsolute8(gitPath) ? resolve7(gitPath) : resolve7(cwd, gitPath)
28255
+ path: isAbsolute9(gitPath) ? resolve8(gitPath) : resolve8(cwd, gitPath)
27687
28256
  };
27688
28257
  } catch (error) {
27689
28258
  const code = errorCode2(error);
@@ -27694,21 +28263,21 @@ function resolveWorkspaceIdPath(cwd) {
27694
28263
  }
27695
28264
  }
27696
28265
  function resolveFromFilesystem(cwd) {
27697
- let current = resolve7(cwd);
28266
+ let current = resolve8(cwd);
27698
28267
  for (let depth = 0; depth < MAX_ANCESTORS; depth += 1) {
27699
- const dotGit = join15(current, ".git");
28268
+ const dotGit = join17(current, ".git");
27700
28269
  try {
27701
28270
  const stat = lstatSync6(dotGit);
27702
28271
  if (stat.isDirectory() && isGitDirectory(dotGit)) {
27703
- return join15(dotGit, WORKSPACE_ID_RELATIVE_PATH);
28272
+ return join17(dotGit, WORKSPACE_ID_RELATIVE_PATH);
27704
28273
  }
27705
28274
  if (stat.isFile() && stat.size <= MAX_GIT_FILE_BYTES) {
27706
- const contents = readFileSync12(dotGit, "utf8");
28275
+ const contents = readFileSync13(dotGit, "utf8");
27707
28276
  const match = /^gitdir: (.+?)(?:\r?\n)?$/.exec(contents);
27708
28277
  if (match?.[1]) {
27709
- const gitDirectory = isAbsolute8(match[1]) ? resolve7(match[1]) : resolve7(current, match[1]);
28278
+ const gitDirectory = isAbsolute9(match[1]) ? resolve8(match[1]) : resolve8(current, match[1]);
27710
28279
  if (isGitDirectory(gitDirectory)) {
27711
- return join15(gitDirectory, WORKSPACE_ID_RELATIVE_PATH);
28280
+ return join17(gitDirectory, WORKSPACE_ID_RELATIVE_PATH);
27712
28281
  }
27713
28282
  }
27714
28283
  }
@@ -27719,7 +28288,7 @@ function resolveFromFilesystem(cwd) {
27719
28288
  return null;
27720
28289
  }
27721
28290
  }
27722
- const parent = dirname11(current);
28291
+ const parent = dirname12(current);
27723
28292
  if (parent === current) break;
27724
28293
  current = parent;
27725
28294
  }
@@ -27727,7 +28296,7 @@ function resolveFromFilesystem(cwd) {
27727
28296
  }
27728
28297
  function isGitDirectory(path) {
27729
28298
  try {
27730
- return statSync3(join15(path, "HEAD")).isFile();
28299
+ return statSync3(join17(path, "HEAD")).isFile();
27731
28300
  } catch {
27732
28301
  return false;
27733
28302
  }
@@ -27746,7 +28315,7 @@ function inspectPath(path) {
27746
28315
  return { status: "corrupt", path, reason: "invalid_content" };
27747
28316
  }
27748
28317
  try {
27749
- const contents = readFileSync12(path, "utf8");
28318
+ const contents = readFileSync13(path, "utf8");
27750
28319
  const workspaceId = contents.endsWith("\n") ? contents.slice(0, -1) : contents;
27751
28320
  if ((contents === workspaceId || contents === `${workspaceId}
27752
28321
  `) && isCanonicalWorkspaceId(workspaceId)) {
@@ -27763,13 +28332,13 @@ function inspectPath(path) {
27763
28332
  }
27764
28333
  }
27765
28334
  function createWorkspaceId(path) {
27766
- const parent = dirname11(path);
28335
+ const parent = dirname12(path);
27767
28336
  const workspaceId = randomUUID3();
27768
- const temporaryPath = join15(parent, `.workspace-id-${process.pid}-${randomUUID3()}.tmp`);
28337
+ const temporaryPath = join17(parent, `.workspace-id-${process.pid}-${randomUUID3()}.tmp`);
27769
28338
  let temporaryCreated = false;
27770
28339
  let publishing = false;
27771
28340
  try {
27772
- mkdirSync8(parent, { recursive: true, mode: 448 });
28341
+ mkdirSync9(parent, { recursive: true, mode: 448 });
27773
28342
  chmodSync4(parent, 448);
27774
28343
  const fd = openSync5(
27775
28344
  temporaryPath,
@@ -27799,7 +28368,7 @@ function createWorkspaceId(path) {
27799
28368
  } finally {
27800
28369
  if (temporaryCreated) {
27801
28370
  try {
27802
- unlinkSync3(temporaryPath);
28371
+ unlinkSync4(temporaryPath);
27803
28372
  } catch {
27804
28373
  }
27805
28374
  }
@@ -27829,16 +28398,16 @@ import { createHash as createHash9, randomBytes as randomBytes4 } from "crypto";
27829
28398
  import {
27830
28399
  chmodSync as chmodSync5,
27831
28400
  existsSync as existsSync11,
27832
- mkdirSync as mkdirSync9,
27833
- readFileSync as readFileSync13,
27834
- readdirSync as readdirSync4,
28401
+ mkdirSync as mkdirSync10,
28402
+ readFileSync as readFileSync14,
28403
+ readdirSync as readdirSync5,
27835
28404
  renameSync as renameSync4,
27836
28405
  rmSync as rmSync6,
27837
28406
  statSync as statSync4,
27838
- unlinkSync as unlinkSync4,
28407
+ unlinkSync as unlinkSync5,
27839
28408
  writeFileSync as writeFileSync6
27840
28409
  } from "fs";
27841
- import { join as join16 } from "path";
28410
+ import { join as join18 } from "path";
27842
28411
 
27843
28412
  // src/daemon/decision-digest-cache.ts
27844
28413
  import { randomBytes as randomBytes3 } from "crypto";
@@ -27853,6 +28422,27 @@ function isActionablePrivateDraft(row) {
27853
28422
  return typeof candidate.id === "string" && SAFE_PRIVATE_DRAFT_ID.test(candidate.id) && typeof candidate.intent === "string" && candidate.authorIsSelf === true && candidate.stage === "draft" && (candidate.intentKind === void 0 || candidate.intentKind === "change");
27854
28423
  }
27855
28424
 
28425
+ // src/lib/delivery-backlog.ts
28426
+ var MS_PER_SECOND = 1e3;
28427
+ var MS_PER_MINUTE = 60 * MS_PER_SECOND;
28428
+ var MS_PER_HOUR = 60 * MS_PER_MINUTE;
28429
+ var MS_PER_DAY = 24 * MS_PER_HOUR;
28430
+ function isNonNegativeInteger(value) {
28431
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
28432
+ }
28433
+ function isPositiveInteger(value) {
28434
+ return isNonNegativeInteger(value) && value > 0;
28435
+ }
28436
+ function deliveryBacklogState(ingestion) {
28437
+ if (ingestion?.healthy !== false) return void 0;
28438
+ const failures = ingestion.consecutiveFailures;
28439
+ const failedWithoutProgress = failures !== 0 && !(isPositiveInteger(failures) && isPositiveInteger(ingestion.lastFailedDrainAcknowledgedCount));
28440
+ if (failedWithoutProgress) return "failing";
28441
+ const retained = ingestion.lastRetainedBucketCount;
28442
+ if (!isNonNegativeInteger(retained)) return "failing";
28443
+ return retained > 0 ? "retained" : "draining";
28444
+ }
28445
+
27856
28446
  // src/lib/presence.ts
27857
28447
  var DECISION_ORIGIN = "https://app.getprimitive.ai";
27858
28448
  function validDecisionUrl(value) {
@@ -27927,7 +28517,8 @@ function formatStatusline(version, snapshot, resolveIngestionStatus, options = {
27927
28517
  if (snapshot.ingestion?.healthy === false) {
27928
28518
  const pending = snapshot.ingestion.pendingCount;
27929
28519
  const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
27930
- return `primitive ${version} (daemon: degraded \xB7 delivery: stalled${typeof pending === "number" ? ` \xB7 ${qualifier}${String(pending)} pending` : ""}${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
28520
+ const delivery = deliveryBacklogState(snapshot.ingestion) === "draining" && snapshot.heartbeat?.healthy === true ? "draining" : "stalled";
28521
+ return `primitive ${version} (daemon: degraded \xB7 delivery: ${delivery}${typeof pending === "number" ? ` \xB7 ${qualifier}${String(pending)} pending` : ""}${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
27931
28522
  }
27932
28523
  if (snapshot.heartbeat?.healthy === false) {
27933
28524
  return `primitive ${version} (daemon: degraded \xB7 presence: unavailable${repositorySuffix}${ingestionSuffix(ingestionStatus)})`;
@@ -27969,10 +28560,10 @@ var STATE_VERSION = 1;
27969
28560
  var DRAFT_STATE_VERSION = 2;
27970
28561
  var NO_CURSOR = -1;
27971
28562
  function stateRoot(namespace) {
27972
- return join16(primConfigDirectory(), namespace, "decision-digests");
28563
+ return join18(primConfigDirectory(), namespace, "decision-digests");
27973
28564
  }
27974
28565
  function draftStateRoot(namespace) {
27975
- return join16(primConfigDirectory(), namespace, "decision-draft-deliveries");
28566
+ return join18(primConfigDirectory(), namespace, "decision-draft-deliveries");
27976
28567
  }
27977
28568
  function workspaceFor(cwd) {
27978
28569
  try {
@@ -27984,12 +28575,12 @@ function workspaceFor(cwd) {
27984
28575
  function statePath(args) {
27985
28576
  const identity = `${args.siteUrl}\0${workspaceFor(args.cwd)}\0${args.sessionId}`;
27986
28577
  const key = createHash9("sha256").update(identity).digest("hex");
27987
- return join16(stateRoot(args.namespace), `${key}.json`);
28578
+ return join18(stateRoot(args.namespace), `${key}.json`);
27988
28579
  }
27989
28580
  function draftStatePath(args) {
27990
28581
  const identity = `${args.siteUrl}\0${workspaceFor(args.cwd)}\0${args.sessionId}\0${args.principalId}\0${args.organizationId}\0${args.credentialFingerprint}`;
27991
28582
  const key = createHash9("sha256").update(identity).digest("hex");
27992
- return join16(draftStateRoot(args.namespace), `${key}.json`);
28583
+ return join18(draftStateRoot(args.namespace), `${key}.json`);
27993
28584
  }
27994
28585
  function parseState(value) {
27995
28586
  if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
@@ -28030,7 +28621,7 @@ function parseDraftState(value) {
28030
28621
  function readState(path) {
28031
28622
  if (!existsSync11(path)) return void 0;
28032
28623
  try {
28033
- return parseState(JSON.parse(readFileSync13(path, "utf8")));
28624
+ return parseState(JSON.parse(readFileSync14(path, "utf8")));
28034
28625
  } catch {
28035
28626
  return void 0;
28036
28627
  }
@@ -28038,15 +28629,15 @@ function readState(path) {
28038
28629
  function readDraftState(path) {
28039
28630
  if (!existsSync11(path)) return void 0;
28040
28631
  try {
28041
- return parseDraftState(JSON.parse(readFileSync13(path, "utf8")));
28632
+ return parseDraftState(JSON.parse(readFileSync14(path, "utf8")));
28042
28633
  } catch {
28043
28634
  return void 0;
28044
28635
  }
28045
28636
  }
28046
28637
  function writeStateFile(path, directory, state) {
28047
- mkdirSync9(directory, { recursive: true, mode: 448 });
28638
+ mkdirSync10(directory, { recursive: true, mode: 448 });
28048
28639
  chmodSync5(directory, 448);
28049
- const temporary = join16(
28640
+ const temporary = join18(
28050
28641
  directory,
28051
28642
  `.${path.slice(directory.length + 1)}.tmp-${process.pid}-${randomBytes4(8).toString("hex")}`
28052
28643
  );
@@ -28061,7 +28652,7 @@ function writeStateFile(path, directory, state) {
28061
28652
  renameSync4(temporary, path);
28062
28653
  } finally {
28063
28654
  try {
28064
- unlinkSync4(temporary);
28655
+ unlinkSync5(temporary);
28065
28656
  } catch {
28066
28657
  }
28067
28658
  }
@@ -28070,14 +28661,14 @@ function cleanupStateFiles(directory, now) {
28070
28661
  if (!existsSync11(directory)) return;
28071
28662
  let names;
28072
28663
  try {
28073
- names = readdirSync4(directory);
28664
+ names = readdirSync5(directory);
28074
28665
  } catch {
28075
28666
  return;
28076
28667
  }
28077
28668
  const files = [];
28078
28669
  const residue = [];
28079
28670
  for (const name of names) {
28080
- const path = join16(directory, name);
28671
+ const path = join18(directory, name);
28081
28672
  try {
28082
28673
  const mtimeMs = statSync4(path).mtimeMs;
28083
28674
  if (name.endsWith(".json")) {
@@ -28092,7 +28683,7 @@ function cleanupStateFiles(directory, now) {
28092
28683
  const excess = files.filter((file) => !stale.some((candidate) => candidate.path === file.path)).sort((a, b) => a.mtimeMs - b.mtimeMs).slice(0, Math.max(0, files.length - CODEX_DIGEST_STATE_MAX_FILES));
28093
28684
  for (const file of [...stale, ...excess]) {
28094
28685
  try {
28095
- unlinkSync4(file.path);
28686
+ unlinkSync5(file.path);
28096
28687
  } catch {
28097
28688
  }
28098
28689
  }
@@ -28383,8 +28974,8 @@ async function prepareCodexContext(options) {
28383
28974
 
28384
28975
  // src/hooks/normalize.ts
28385
28976
  import { createHash as createHash10 } from "crypto";
28386
- import { realpathSync as realpathSync4 } from "fs";
28387
- import { isAbsolute as isAbsolute9, resolve as resolve8 } from "path";
28977
+ import { realpathSync as realpathSync5 } from "fs";
28978
+ import { isAbsolute as isAbsolute10, resolve as resolve9 } from "path";
28388
28979
  var HERMES_EVENT_MAP = {
28389
28980
  on_session_start: "SessionStart",
28390
28981
  on_session_end: "SessionEnd",
@@ -28432,16 +29023,16 @@ function cursorToolUseId(value) {
28432
29023
  function workspaceCandidates(value) {
28433
29024
  if (!Array.isArray(value)) return [];
28434
29025
  const roots = value.filter((item) => typeof item === "string" && item.length > 0);
28435
- if (roots.some((root) => !isAbsolute9(root))) {
29026
+ if (roots.some((root) => !isAbsolute10(root))) {
28436
29027
  throw new TypeError("Cursor workspace roots must be absolute");
28437
29028
  }
28438
29029
  return roots;
28439
29030
  }
28440
29031
  function pathIdentity(path) {
28441
29032
  try {
28442
- return realpathSync4.native(path);
29033
+ return realpathSync5.native(path);
28443
29034
  } catch {
28444
- return resolve8(path);
29035
+ return resolve9(path);
28445
29036
  }
28446
29037
  }
28447
29038
  function resolveCursorCwd(parsed) {
@@ -28456,17 +29047,17 @@ function resolveCursorCwd(parsed) {
28456
29047
  }
28457
29048
  const expectedRepo = [...repositoryRoots][0];
28458
29049
  const rawCwd = typeof parsed.cwd === "string" && parsed.cwd.length > 0 ? parsed.cwd : void 0;
28459
- if (rawCwd && !isAbsolute9(rawCwd)) {
29050
+ if (rawCwd && !isAbsolute10(rawCwd)) {
28460
29051
  throw new TypeError("Cursor hook cwd must be absolute");
28461
29052
  }
28462
29053
  const envelopeCwd = rawCwd ? pathIdentity(rawCwd) : void 0;
28463
29054
  const input = typeof parsed.tool_input === "object" && parsed.tool_input !== null && !Array.isArray(parsed.tool_input) ? parsed.tool_input : void 0;
28464
29055
  const rawToolCwd = parsed.tool_name === "Shell" && typeof input?.working_directory === "string" ? input.working_directory : void 0;
28465
29056
  const base = envelopeCwd ?? (roots.length === 1 ? roots[0] : expectedRepo);
28466
- if (rawToolCwd && !isAbsolute9(rawToolCwd) && !base) {
29057
+ if (rawToolCwd && !isAbsolute10(rawToolCwd) && !base) {
28467
29058
  throw new TypeError("relative Cursor tool cwd has no workspace base");
28468
29059
  }
28469
- const toolCwd = rawToolCwd ? pathIdentity(isAbsolute9(rawToolCwd) ? rawToolCwd : resolve8(base, rawToolCwd)) : void 0;
29060
+ const toolCwd = rawToolCwd ? pathIdentity(isAbsolute10(rawToolCwd) ? rawToolCwd : resolve9(base, rawToolCwd)) : void 0;
28470
29061
  const selected = toolCwd ?? envelopeCwd ?? (roots.length === 1 ? roots[0] : expectedRepo);
28471
29062
  if (!selected) {
28472
29063
  throw new TypeError("Cursor hook envelope has no unambiguous workspace");
@@ -28560,7 +29151,11 @@ var CODEX_PRIM_REMINDER = primReminder(CODEX_PRIM_ACTION, CODEX_PRIM_ACTION);
28560
29151
  async function activeProjectRoot(cwd) {
28561
29152
  try {
28562
29153
  const root = gitToplevel(cwd);
28563
- if (!root || !isRepoActiveForCapture(cwd)) return null;
29154
+ if (!root) return null;
29155
+ if (!isRepoActiveForCapture(cwd)) {
29156
+ if (repoActiveFlag(root) === "false") clearHooksWired(root);
29157
+ return null;
29158
+ }
28564
29159
  let shellGateActive = repoActiveFlag(root) === "true";
28565
29160
  if (!shellGateActive) {
28566
29161
  try {
@@ -28570,13 +29165,18 @@ async function activeProjectRoot(cwd) {
28570
29165
  }
28571
29166
  }
28572
29167
  if (shellGateActive) {
28573
- try {
28574
- ensureEffectivePostCommitHook(root);
28575
- } catch {
28576
- }
28577
- try {
28578
- ensureEffectivePostRewriteHook(root);
28579
- } catch {
29168
+ for (const hookName of MANAGED_GIT_HOOK_NAMES) {
29169
+ try {
29170
+ const result = ensureEffectiveGitHook(hookName, root, {
29171
+ context: "ambient",
29172
+ repairOnly: hookName === "pre-commit"
29173
+ });
29174
+ if (hookName === "post-commit" && // Manual mode: the user's own wiring is expected to reach prim too.
29175
+ ["created", "updated", "unchanged", "manual"].includes(result.outcome)) {
29176
+ recordHooksWired(root, { onlyIfAbsent: true });
29177
+ }
29178
+ } catch {
29179
+ }
28580
29180
  }
28581
29181
  }
28582
29182
  try {