create-egregore 0.19.20 → 0.19.22

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 (226) hide show
  1. package/assets/egregore-launcher.js +124 -12
  2. package/bin/cli.js +3 -4
  3. package/lib/auth.js +3 -4
  4. package/lib/path-lookup.js +59 -0
  5. package/lib/setup.js +52 -6
  6. package/package.json +2 -2
  7. package/runtime/codex/.claude/context/notification-consent.md +66 -0
  8. package/runtime/codex/.codex/hooks/branch-guard.js +14 -3
  9. package/runtime/codex/.codex/skills/add/SKILL.md +8 -4
  10. package/runtime/codex/.codex/skills/announce/SKILL.md +21 -8
  11. package/runtime/codex/.codex/skills/answer/SKILL.md +8 -4
  12. package/runtime/codex/.codex/skills/archive/SKILL.md +8 -4
  13. package/runtime/codex/.codex/skills/ask/SKILL.md +14 -6
  14. package/runtime/codex/.codex/skills/audit/SKILL.md +29 -0
  15. package/runtime/codex/.codex/skills/autosave/SKILL.md +29 -0
  16. package/runtime/codex/.codex/skills/branch/SKILL.md +8 -4
  17. package/runtime/codex/.codex/skills/checkup/SKILL.md +8 -4
  18. package/runtime/codex/.codex/skills/commit/SKILL.md +8 -4
  19. package/runtime/codex/.codex/skills/contribute/SKILL.md +8 -4
  20. package/runtime/codex/.codex/skills/dark-mode/SKILL.md +8 -4
  21. package/runtime/codex/.codex/skills/delete-user/SKILL.md +8 -4
  22. package/runtime/codex/.codex/skills/demo-mode/SKILL.md +29 -0
  23. package/runtime/codex/.codex/skills/deploy-bridge/SKILL.md +8 -4
  24. package/runtime/codex/.codex/skills/design/SKILL.md +29 -0
  25. package/runtime/codex/.codex/skills/egregore-voice/SKILL.md +8 -4
  26. package/runtime/codex/.codex/skills/emissary/SKILL.md +8 -4
  27. package/runtime/codex/.codex/skills/env/SKILL.md +8 -4
  28. package/runtime/codex/.codex/skills/handoff/SKILL.md +15 -6
  29. package/runtime/codex/.codex/skills/harvest/QUESTION_PALETTE.md +76 -0
  30. package/runtime/codex/.codex/skills/harvest/SKILL.md +30 -6
  31. package/runtime/codex/.codex/skills/ingest/SKILL.md +8 -4
  32. package/runtime/codex/.codex/skills/ingest-google/SKILL.md +8 -4
  33. package/runtime/codex/.codex/skills/ingest-user-interview/SKILL.md +8 -4
  34. package/runtime/codex/.codex/skills/invite/SKILL.md +6 -2
  35. package/runtime/codex/.codex/skills/issue/SKILL.md +8 -4
  36. package/runtime/codex/.codex/skills/me/SKILL.md +8 -4
  37. package/runtime/codex/.codex/skills/meeting/SKILL.md +8 -4
  38. package/runtime/codex/.codex/skills/minion/SKILL.md +8 -4
  39. package/runtime/codex/.codex/skills/mock/SKILL.md +29 -0
  40. package/runtime/codex/.codex/skills/note/SKILL.md +8 -4
  41. package/runtime/codex/.codex/skills/onboarding/SKILL.md +8 -4
  42. package/runtime/codex/.codex/skills/pr/SKILL.md +8 -4
  43. package/runtime/codex/.codex/skills/product-voice/SKILL.md +29 -0
  44. package/runtime/codex/.codex/skills/project/SKILL.md +8 -4
  45. package/runtime/codex/.codex/skills/pull/SKILL.md +8 -4
  46. package/runtime/codex/.codex/skills/push/SKILL.md +8 -4
  47. package/runtime/codex/.codex/skills/quest/SKILL.md +6 -3
  48. package/runtime/codex/.codex/skills/quest-suggest/SKILL.md +8 -4
  49. package/runtime/codex/.codex/skills/reflect/SKILL.md +8 -4
  50. package/runtime/codex/.codex/skills/review-pr/SKILL.md +8 -4
  51. package/runtime/codex/.codex/skills/save/SKILL.md +6 -4
  52. package/runtime/codex/.codex/skills/scroll/SKILL.md +92 -23
  53. package/runtime/codex/.codex/skills/scroll/assets/shell.html +381 -52
  54. package/runtime/codex/.codex/skills/session-view/SKILL.md +8 -4
  55. package/runtime/codex/.codex/skills/setup/SKILL.md +8 -4
  56. package/runtime/codex/.codex/skills/summon/SKILL.md +8 -4
  57. package/runtime/codex/.codex/skills/sync-repos/SKILL.md +8 -4
  58. package/runtime/codex/.codex/skills/task/SKILL.md +9 -5
  59. package/runtime/codex/.codex/skills/teams-connect/SKILL.md +29 -0
  60. package/runtime/codex/.codex/skills/telegram-connect/SKILL.md +8 -4
  61. package/runtime/codex/.codex/skills/telemetry/SKILL.md +8 -4
  62. package/runtime/codex/.codex/skills/test/SKILL.md +8 -4
  63. package/runtime/codex/.codex/skills/thread/SKILL.md +8 -4
  64. package/runtime/codex/.codex/skills/todo/SKILL.md +8 -4
  65. package/runtime/codex/.codex/skills/tui-design/SKILL.md +8 -4
  66. package/runtime/codex/.codex/skills/tutorial/SKILL.md +8 -4
  67. package/runtime/codex/.codex/skills/update/SKILL.md +8 -4
  68. package/runtime/codex/.codex/skills/visual-explain/SKILL.md +8 -4
  69. package/runtime/codex/.codex/spec-manifest.json +4 -4
  70. package/runtime/codex/AGENTS.md +36 -14
  71. package/runtime/codex/bin/activity-data.sh +1 -1
  72. package/runtime/codex/bin/agent.sh +38 -19
  73. package/runtime/codex/bin/attendant.sh +8 -2
  74. package/runtime/codex/bin/autosave.sh +1 -1
  75. package/runtime/codex/bin/capture-reconcile.sh +8 -6
  76. package/runtime/codex/bin/capture-run.sh +1 -1
  77. package/runtime/codex/bin/codex-install-skills.sh +1 -1
  78. package/runtime/codex/bin/codex-render-spec.mjs +12 -5
  79. package/runtime/codex/bin/codex-session-start.sh +1 -1
  80. package/runtime/codex/bin/codex-sync-skills.sh +9 -5
  81. package/runtime/codex/bin/dashboard-data.sh +1 -1
  82. package/runtime/codex/bin/graph-batch.sh +1 -1
  83. package/runtime/codex/bin/graph-op.sh +1 -1
  84. package/runtime/codex/bin/graph-wal.sh +1 -1
  85. package/runtime/codex/bin/graph.sh +1 -1
  86. package/runtime/codex/bin/handoff-pr-backfill.sh +1 -1
  87. package/runtime/codex/bin/handoff-run.sh +53 -39
  88. package/runtime/codex/bin/handoff-save-egregore.sh +62 -32
  89. package/runtime/codex/bin/index-handoff.sh +1 -1
  90. package/runtime/codex/bin/ingest-graph.sh +1 -1
  91. package/runtime/codex/bin/ingest.sh +1 -1
  92. package/runtime/codex/bin/lib/artifact-id.sh +1 -1
  93. package/runtime/codex/bin/lib/config.sh +56 -20
  94. package/runtime/codex/bin/lib/context.sh +7 -1
  95. package/runtime/codex/bin/lib/ensure-develop.sh +1 -1
  96. package/runtime/codex/bin/lib/git-sync.sh +145 -59
  97. package/runtime/codex/bin/lib/greeting.sh +14 -0
  98. package/runtime/codex/bin/lib/hash.sh +1 -1
  99. package/runtime/codex/bin/lib/noncode.sh +1 -1
  100. package/runtime/codex/bin/lib/time.sh +1 -1
  101. package/runtime/codex/bin/notify.sh +492 -153
  102. package/runtime/codex/bin/person.sh +1 -1
  103. package/runtime/codex/bin/publish-artifact.sh +39 -4
  104. package/runtime/codex/bin/publish-references.sh +1 -1
  105. package/runtime/codex/bin/repo-state.sh +7 -10
  106. package/runtime/codex/bin/search.sh +1 -1
  107. package/runtime/codex/bin/session-autosave.sh +1 -1
  108. package/runtime/codex/bin/session-log.sh +1 -1
  109. package/runtime/codex/bin/settings.sh +32 -2
  110. package/runtime/codex/bin/telemetry.sh +1 -1
  111. package/runtime/codex/bin/worktree-create.sh +14 -4
  112. package/runtime/codex/bin/worktree-remove.sh +1 -1
  113. package/runtime/codex/bin/worktree.sh +1 -1
  114. package/runtime/codex/manifest.json +18 -1
  115. package/runtime/pi/.claude/context/notification-consent.md +66 -0
  116. package/runtime/pi/.codex/hooks/branch-guard.js +14 -3
  117. package/runtime/pi/.codex/skills/add/SKILL.md +8 -4
  118. package/runtime/pi/.codex/skills/announce/SKILL.md +21 -8
  119. package/runtime/pi/.codex/skills/answer/SKILL.md +8 -4
  120. package/runtime/pi/.codex/skills/archive/SKILL.md +8 -4
  121. package/runtime/pi/.codex/skills/ask/SKILL.md +14 -6
  122. package/runtime/pi/.codex/skills/audit/SKILL.md +29 -0
  123. package/runtime/pi/.codex/skills/autosave/SKILL.md +29 -0
  124. package/runtime/pi/.codex/skills/branch/SKILL.md +8 -4
  125. package/runtime/pi/.codex/skills/checkup/SKILL.md +8 -4
  126. package/runtime/pi/.codex/skills/commit/SKILL.md +8 -4
  127. package/runtime/pi/.codex/skills/contribute/SKILL.md +8 -4
  128. package/runtime/pi/.codex/skills/dark-mode/SKILL.md +8 -4
  129. package/runtime/pi/.codex/skills/delete-user/SKILL.md +8 -4
  130. package/runtime/pi/.codex/skills/demo-mode/SKILL.md +29 -0
  131. package/runtime/pi/.codex/skills/deploy-bridge/SKILL.md +8 -4
  132. package/runtime/pi/.codex/skills/design/SKILL.md +29 -0
  133. package/runtime/pi/.codex/skills/egregore-voice/SKILL.md +8 -4
  134. package/runtime/pi/.codex/skills/emissary/SKILL.md +8 -4
  135. package/runtime/pi/.codex/skills/env/SKILL.md +8 -4
  136. package/runtime/pi/.codex/skills/handoff/SKILL.md +15 -6
  137. package/runtime/pi/.codex/skills/harvest/QUESTION_PALETTE.md +76 -0
  138. package/runtime/pi/.codex/skills/harvest/SKILL.md +30 -6
  139. package/runtime/pi/.codex/skills/ingest/SKILL.md +8 -4
  140. package/runtime/pi/.codex/skills/ingest-google/SKILL.md +8 -4
  141. package/runtime/pi/.codex/skills/ingest-user-interview/SKILL.md +8 -4
  142. package/runtime/pi/.codex/skills/invite/SKILL.md +6 -2
  143. package/runtime/pi/.codex/skills/issue/SKILL.md +8 -4
  144. package/runtime/pi/.codex/skills/me/SKILL.md +8 -4
  145. package/runtime/pi/.codex/skills/meeting/SKILL.md +8 -4
  146. package/runtime/pi/.codex/skills/minion/SKILL.md +8 -4
  147. package/runtime/pi/.codex/skills/mock/SKILL.md +29 -0
  148. package/runtime/pi/.codex/skills/note/SKILL.md +8 -4
  149. package/runtime/pi/.codex/skills/onboarding/SKILL.md +8 -4
  150. package/runtime/pi/.codex/skills/pr/SKILL.md +8 -4
  151. package/runtime/pi/.codex/skills/product-voice/SKILL.md +29 -0
  152. package/runtime/pi/.codex/skills/project/SKILL.md +8 -4
  153. package/runtime/pi/.codex/skills/pull/SKILL.md +8 -4
  154. package/runtime/pi/.codex/skills/push/SKILL.md +8 -4
  155. package/runtime/pi/.codex/skills/quest/SKILL.md +6 -3
  156. package/runtime/pi/.codex/skills/quest-suggest/SKILL.md +8 -4
  157. package/runtime/pi/.codex/skills/reflect/SKILL.md +8 -4
  158. package/runtime/pi/.codex/skills/review-pr/SKILL.md +8 -4
  159. package/runtime/pi/.codex/skills/save/SKILL.md +6 -4
  160. package/runtime/pi/.codex/skills/scroll/SKILL.md +92 -23
  161. package/runtime/pi/.codex/skills/scroll/assets/shell.html +381 -52
  162. package/runtime/pi/.codex/skills/session-view/SKILL.md +8 -4
  163. package/runtime/pi/.codex/skills/setup/SKILL.md +8 -4
  164. package/runtime/pi/.codex/skills/summon/SKILL.md +8 -4
  165. package/runtime/pi/.codex/skills/sync-repos/SKILL.md +8 -4
  166. package/runtime/pi/.codex/skills/task/SKILL.md +9 -5
  167. package/runtime/pi/.codex/skills/teams-connect/SKILL.md +29 -0
  168. package/runtime/pi/.codex/skills/telegram-connect/SKILL.md +8 -4
  169. package/runtime/pi/.codex/skills/telemetry/SKILL.md +8 -4
  170. package/runtime/pi/.codex/skills/test/SKILL.md +8 -4
  171. package/runtime/pi/.codex/skills/thread/SKILL.md +8 -4
  172. package/runtime/pi/.codex/skills/todo/SKILL.md +8 -4
  173. package/runtime/pi/.codex/skills/tui-design/SKILL.md +8 -4
  174. package/runtime/pi/.codex/skills/tutorial/SKILL.md +8 -4
  175. package/runtime/pi/.codex/skills/update/SKILL.md +8 -4
  176. package/runtime/pi/.codex/skills/visual-explain/SKILL.md +8 -4
  177. package/runtime/pi/.codex/spec-manifest.json +4 -4
  178. package/runtime/pi/.pi/APPEND_SYSTEM.md +34 -12
  179. package/runtime/pi/.pi/extensions/egregore.ts +8 -1
  180. package/runtime/pi/.pi/spec-manifest.json +2 -2
  181. package/runtime/pi/AGENTS.md +36 -14
  182. package/runtime/pi/bin/activity-data.sh +1 -1
  183. package/runtime/pi/bin/agent.sh +38 -19
  184. package/runtime/pi/bin/attendant.sh +8 -2
  185. package/runtime/pi/bin/autosave.sh +1 -1
  186. package/runtime/pi/bin/capture-reconcile.sh +8 -6
  187. package/runtime/pi/bin/capture-run.sh +1 -1
  188. package/runtime/pi/bin/codex-install-skills.sh +1 -1
  189. package/runtime/pi/bin/codex-render-spec.mjs +12 -5
  190. package/runtime/pi/bin/codex-session-start.sh +1 -1
  191. package/runtime/pi/bin/codex-sync-skills.sh +9 -5
  192. package/runtime/pi/bin/dashboard-data.sh +1 -1
  193. package/runtime/pi/bin/graph-batch.sh +1 -1
  194. package/runtime/pi/bin/graph-op.sh +1 -1
  195. package/runtime/pi/bin/graph-wal.sh +1 -1
  196. package/runtime/pi/bin/graph.sh +1 -1
  197. package/runtime/pi/bin/handoff-pr-backfill.sh +1 -1
  198. package/runtime/pi/bin/handoff-run.sh +53 -39
  199. package/runtime/pi/bin/handoff-save-egregore.sh +62 -32
  200. package/runtime/pi/bin/index-handoff.sh +1 -1
  201. package/runtime/pi/bin/ingest-graph.sh +1 -1
  202. package/runtime/pi/bin/ingest.sh +1 -1
  203. package/runtime/pi/bin/lib/artifact-id.sh +1 -1
  204. package/runtime/pi/bin/lib/config.sh +56 -20
  205. package/runtime/pi/bin/lib/context.sh +7 -1
  206. package/runtime/pi/bin/lib/ensure-develop.sh +1 -1
  207. package/runtime/pi/bin/lib/git-sync.sh +145 -59
  208. package/runtime/pi/bin/lib/greeting.sh +14 -0
  209. package/runtime/pi/bin/lib/hash.sh +1 -1
  210. package/runtime/pi/bin/lib/noncode.sh +1 -1
  211. package/runtime/pi/bin/lib/time.sh +1 -1
  212. package/runtime/pi/bin/notify.sh +492 -153
  213. package/runtime/pi/bin/person.sh +1 -1
  214. package/runtime/pi/bin/pi-session-start.sh +1 -1
  215. package/runtime/pi/bin/publish-artifact.sh +39 -4
  216. package/runtime/pi/bin/publish-references.sh +1 -1
  217. package/runtime/pi/bin/repo-state.sh +7 -10
  218. package/runtime/pi/bin/search.sh +1 -1
  219. package/runtime/pi/bin/session-autosave.sh +1 -1
  220. package/runtime/pi/bin/session-log.sh +1 -1
  221. package/runtime/pi/bin/settings.sh +32 -2
  222. package/runtime/pi/bin/telemetry.sh +1 -1
  223. package/runtime/pi/bin/worktree-create.sh +14 -4
  224. package/runtime/pi/bin/worktree-remove.sh +1 -1
  225. package/runtime/pi/bin/worktree.sh +1 -1
  226. package/runtime/pi/manifest.json +18 -1
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // egregore-launcher-version: 0.19.20
2
+ // egregore-launcher-version: 0.19.22
3
3
  //
4
4
  // egregore — launch and browse Egregore instances from your terminal.
5
5
  // Installed by create-egregore. Reads ~/.egregore/instances.json.
@@ -33,7 +33,7 @@ const crypto = require("node:crypto");
33
33
  const { spawnSync, spawn } = require("node:child_process");
34
34
  const readline = require("node:readline");
35
35
 
36
- const VERSION = "0.19.20";
36
+ const VERSION = "0.19.22";
37
37
  const REGISTRY = path.join(os.homedir(), ".egregore", "instances.json");
38
38
  const DEFAULT_FILE = path.join(os.homedir(), ".egregore", "default");
39
39
  const UPDATE_CACHE_FILE = path.join(os.homedir(), ".egregore", "update-check.json");
@@ -435,7 +435,33 @@ function piTrustArgs() {
435
435
  }
436
436
 
437
437
  function commandExists(bin) {
438
- return spawnSync("which", [bin], { stdio: "pipe" }).status === 0;
438
+ if (typeof bin !== "string" || bin.length === 0) return false;
439
+ const pathValue = process.env.PATH || process.env.Path || process.env.path || "";
440
+ const separator = process.platform === "win32" ? ";" : ":";
441
+ const names =
442
+ process.platform === "win32" && !path.extname(bin)
443
+ ? (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD")
444
+ .split(";")
445
+ .filter(Boolean)
446
+ .map((extension) => `${bin}${extension.toLowerCase()}`)
447
+ : [bin];
448
+
449
+ return pathValue.split(separator).some((entry) => {
450
+ const directory = entry ? entry.replace(/^"(.*)"$/, "$1") : process.cwd();
451
+ return names.some((name) => {
452
+ const candidate = path.join(directory, name);
453
+ try {
454
+ if (!fs.statSync(candidate).isFile()) return false;
455
+ fs.accessSync(
456
+ candidate,
457
+ process.platform === "win32" ? fs.constants.F_OK : fs.constants.X_OK,
458
+ );
459
+ return true;
460
+ } catch {
461
+ return false;
462
+ }
463
+ });
464
+ });
439
465
  }
440
466
 
441
467
  function runtimeChoiceDescription(runtime, inst, stored) {
@@ -629,7 +655,14 @@ async function renderCodexStartupCard(egregoreDir) {
629
655
  const result = await runCapturedWithSpinner("bash", [script], {
630
656
  cwd: egregoreDir,
631
657
  }, "Preparing Codex startup");
632
- if (result.stdout) process.stdout.write(result.stdout);
658
+ let output = result.stdout || "";
659
+ if (isConnectedPath(egregoreDir) && !output.includes("◆ CONNECTED")) {
660
+ const connectedLine = " ◆ CONNECTED · hosted graph active\n";
661
+ output = output.includes("\nWhat are you working on?")
662
+ ? output.replace("\nWhat are you working on?", `\n${connectedLine}\nWhat are you working on?`)
663
+ : `${output}${output.endsWith("\n") || !output ? "" : "\n"}${connectedLine}`;
664
+ }
665
+ if (output) process.stdout.write(output);
633
666
  if (result.status !== 0 && result.stderr) process.stderr.write(result.stderr);
634
667
  if (result.status === 0) return true;
635
668
  } catch {}
@@ -751,6 +784,7 @@ async function launch(inst, runtimeOverride) {
751
784
  if (!runtime) return;
752
785
  if (runtime === "claude-code") {
753
786
  normalizeClaudeDenyRules(inst.path);
787
+ ensureClaudeModeStatusLine(inst.path);
754
788
  }
755
789
  const binary = runtimeBinary(runtime);
756
790
  if (!commandExists(binary)) {
@@ -816,13 +850,17 @@ function cmdLs() {
816
850
  const slugW = Math.min(24, Math.max(...registry.map((r) => (r.slug || "").length)));
817
851
  const nameW = Math.min(24, Math.max(...registry.map((r) => (r.name || "").length)));
818
852
  const runtimeW = Math.max(9, ...registry.map((r) => runtimeShort(runtimeFor(r)).length));
853
+ const modeW = "◆ CONNECTED".length;
819
854
  console.log(`\n ${BOLD}${registry.length}${RESET} egregore${registry.length === 1 ? "" : "s"} registered\n`);
820
- console.log(` ${DIM}${pad("SLUG", slugW)} ${pad("NAME", nameW)} ${pad("ASSISTANT", runtimeW)} PATH${RESET}`);
855
+ console.log(` ${DIM}${pad("SLUG", slugW)} ${pad("NAME", nameW)} ${pad("MODE", modeW)} ${pad("ASSISTANT", runtimeW)} PATH${RESET}`);
821
856
  for (const inst of registry) {
822
857
  const marker = inst.slug === def ? `${GREEN}●${RESET}` : " ";
823
858
  const slug = inst.slug === def ? `${BOLD}${pad(inst.slug, slugW)}${RESET}` : pad(inst.slug, slugW);
824
859
  const runtime = pad(runtimeShort(runtimeFor(inst)), runtimeW);
825
- console.log(` ${marker} ${slug} ${pad(inst.name || "", nameW)} ${runtime} ${DIM}${inst.path}${RESET}`);
860
+ const mode = isConnectedInstance(inst)
861
+ ? `${CYAN}${pad("◆ CONNECTED", modeW)}${RESET}`
862
+ : `${DIM}${pad("◇ LOCAL", modeW)}${RESET}`;
863
+ console.log(` ${marker} ${slug} ${pad(inst.name || "", nameW)} ${mode} ${runtime} ${DIM}${inst.path}${RESET}`);
826
864
  }
827
865
  if (def) console.log(`\n ${GREEN}●${RESET} = default marker (used by non-interactive ${BOLD}egregore${RESET})\n`);
828
866
  else console.log("");
@@ -936,6 +974,22 @@ function readJsonFile(file) {
936
974
  }
937
975
  }
938
976
 
977
+ function isConnectedConfig(config) {
978
+ return (
979
+ config?.mode === "connected" &&
980
+ typeof config.api_url === "string" &&
981
+ config.api_url.trim().length > 0
982
+ );
983
+ }
984
+
985
+ function isConnectedPath(instancePath) {
986
+ return isConnectedConfig(readJsonFile(path.join(instancePath, "egregore.json")));
987
+ }
988
+
989
+ function isConnectedInstance(inst) {
990
+ return !!inst?.path && isConnectedPath(inst.path);
991
+ }
992
+
939
993
  function readEnvValues(file) {
940
994
  const values = {};
941
995
  if (!fs.existsSync(file)) return values;
@@ -1233,6 +1287,47 @@ function normalizeClaudeDenyRules(instancePath) {
1233
1287
  }
1234
1288
  }
1235
1289
 
1290
+ const CLAUDE_MODE_STATUS_MARKER = "# egregore-connected-status-v1";
1291
+ const CLAUDE_DEFAULT_STATUS_COMMAND = "\"${CLAUDE_PROJECT_DIR}/bin/statusline.sh\"";
1292
+
1293
+ function claudeModeStatusCommand(baseCommand) {
1294
+ return [
1295
+ CLAUDE_MODE_STATUS_MARKER,
1296
+ "MODE=$(jq -r 'if (.mode == \"connected\" and ((.api_url // \"\") | length > 0)) then \"connected\" else \"local\" end' \"${CLAUDE_PROJECT_DIR}/egregore.json\" 2>/dev/null)",
1297
+ `BASE=$( ( ${baseCommand} ) 2>/dev/null || true)`,
1298
+ "if [ \"$MODE\" = \"connected\" ]; then",
1299
+ " case \"$BASE\" in",
1300
+ " *\"◆ CONNECTED\"*) printf '%s\\n' \"$BASE\" ;;",
1301
+ " \"\") printf '◆ CONNECTED\\n' ;;",
1302
+ " *) printf '◆ CONNECTED · %s\\n' \"$BASE\" ;;",
1303
+ " esac",
1304
+ "else",
1305
+ " printf '%s\\n' \"$BASE\"",
1306
+ "fi",
1307
+ ].join("\n");
1308
+ }
1309
+
1310
+ function ensureClaudeModeStatusLine(instancePath) {
1311
+ if (!isConnectedPath(instancePath)) return false;
1312
+ const file = path.join(instancePath, ".claude", "settings.local.json");
1313
+ const settings = readJsonFile(file);
1314
+ const current = settings?.statusLine;
1315
+ if (current?.type === "command" && current?.command?.includes(CLAUDE_MODE_STATUS_MARKER)) {
1316
+ return false;
1317
+ }
1318
+ const baseCommand = current?.type === "command" && current?.command?.trim()
1319
+ ? current.command
1320
+ : CLAUDE_DEFAULT_STATUS_COMMAND;
1321
+ settings.statusLine = {
1322
+ ...(current || {}),
1323
+ type: "command",
1324
+ command: claudeModeStatusCommand(baseCommand),
1325
+ };
1326
+ fs.mkdirSync(path.dirname(file), { recursive: true });
1327
+ writeJsonFileAtomic(file, settings);
1328
+ return true;
1329
+ }
1330
+
1236
1331
  function writeEnvValueAtomic(file, key, value) {
1237
1332
  const lines = fs.existsSync(file)
1238
1333
  ? fs.readFileSync(file, "utf-8").split(/\r?\n/)
@@ -1323,6 +1418,8 @@ async function cmdConnect(args) {
1323
1418
  }
1324
1419
  const activation = state.connected_activation || {};
1325
1420
  if (cfg.mode === "connected" && activation.status === "connected") {
1421
+ normalizeClaudeDenyRules(inst.path);
1422
+ ensureClaudeModeStatusLine(inst.path);
1326
1423
  console.log(`\n ${GREEN}✓${RESET} ${BOLD}${inst.slug}${RESET} is already connected.\n`);
1327
1424
  return;
1328
1425
  }
@@ -1462,6 +1559,7 @@ async function cmdConnect(args) {
1462
1559
  });
1463
1560
  writeEnvValueAtomic(envFile, "EGREGORE_API_KEY", claim.data.api_key);
1464
1561
  normalizeClaudeDenyRules(inst.path);
1562
+ ensureClaudeModeStatusLine(inst.path);
1465
1563
  persistConnectState(inst, {
1466
1564
  status: "backfilling",
1467
1565
  activation_protocol: claim.data.activation_protocol,
@@ -1938,7 +2036,8 @@ function loadPreview(inst) {
1938
2036
  data.telegram = !!(cfg.telegram_chat_id || cfg.telegram_group_link);
1939
2037
  data.org = cfg.org_name;
1940
2038
  data.githubOrg = cfg.github_org;
1941
- data.mode = cfg.mode;
2039
+ data.connected = isConnectedConfig(cfg);
2040
+ data.mode = data.connected ? "connected" : "local";
1942
2041
  data.flowVariant = cfg.flow_variant;
1943
2042
  }
1944
2043
  } catch {}
@@ -2011,7 +2110,8 @@ function renderHeroPanel({ selected, pv, width, height, countSuffix, filter, fil
2011
2110
  }
2012
2111
  lines.push(emptyBoxLine(innerWidth, margin));
2013
2112
  lines.push(updateBand || mid);
2014
- lines.push(boxLine(`${BOLD}${title}${RESET} ${DIM}· ${org} · ${runtime} · ${countSuffix} · ${filterText}${RESET}`, innerWidth, margin));
2113
+ const connectedHero = pv?.connected ? ` · ${CYAN}◆ CONNECTED${RESET}` : "";
2114
+ lines.push(boxLine(`${BOLD}${title}${RESET} ${DIM}· ${org} · ${runtime}${RESET}${connectedHero}${DIM} · ${countSuffix} · ${filterText}${RESET}`, innerWidth, margin));
2015
2115
  lines.push(boxLine(`${DIM}skills${RESET} ${skillStatus}`, innerWidth, margin));
2016
2116
  lines.push(emptyBoxLine(innerWidth, margin));
2017
2117
 
@@ -2065,6 +2165,13 @@ function renderRuntimePanel({ inst, cursor, width, height }) {
2065
2165
  emptyBoxLine(innerWidth, margin),
2066
2166
  boxLine(`${BOLD}${CYAN}Launch ${title}${RESET}`, innerWidth, margin),
2067
2167
  boxLine(`${DIM}${inst.path}${RESET}`, innerWidth, margin),
2168
+ boxLine(
2169
+ isConnectedInstance(inst)
2170
+ ? `${CYAN}◆ CONNECTED${RESET} ${DIM}· hosted graph active${RESET}`
2171
+ : `${DIM}◇ LOCAL · Markdown + Git${RESET}`,
2172
+ innerWidth,
2173
+ margin,
2174
+ ),
2068
2175
  emptyBoxLine(innerWidth, margin),
2069
2176
  ];
2070
2177
 
@@ -2381,10 +2488,13 @@ async function browser(args = {}) {
2381
2488
  ? `${DIM}${pad(e.name || "", nameW)}${RESET}`
2382
2489
  : pad(e.name || "", nameW);
2383
2490
  const pathAvail = Math.max(0, listAvail - slugW - nameW - 10);
2491
+ const connected = isConnectedInstance(e);
2492
+ const modeBadge = connected ? `${CYAN}◆ CONNECTED${RESET}` : "";
2493
+ const adjustedPathAvail = Math.max(0, pathAvail - (connected ? "◆ CONNECTED ".length : 0));
2384
2494
  const pathDim = isMissing
2385
- ? `${RED}✗${RESET} ${DIM}${truncate(e.path, Math.max(8, pathAvail - 12))}${RESET} ${RED}(missing)${RESET}`
2386
- : `${DIM}${truncate(e.path, pathAvail)}${RESET}`;
2387
- const line = ` ${cursorMark} ${defMark} ${slug} ${name} ${pathDim}`;
2495
+ ? `${RED}✗${RESET} ${DIM}${truncate(e.path, Math.max(8, adjustedPathAvail - 12))}${RESET} ${RED}(missing)${RESET}`
2496
+ : `${DIM}${truncate(e.path, adjustedPathAvail)}${RESET}`;
2497
+ const line = ` ${cursorMark} ${defMark} ${slug} ${name} ${modeBadge}${modeBadge ? " " : ""}${pathDim}`;
2388
2498
  out.push(M + " ".repeat(listPad - 3) + line + ANSI.clearLine + "\n");
2389
2499
  }
2390
2500
 
@@ -2411,6 +2521,9 @@ async function browser(args = {}) {
2411
2521
  ? ` ${label("path")} ${RED}✗${RESET} ${truncate(selected.path, Math.max(8, valW - 12))} ${RED}(missing)${RESET}`
2412
2522
  : ` ${label("path")} ${truncate(selected.path, valW)}`);
2413
2523
  if (pv.org) previewLines.push(` ${label("org")} ${truncate(`${pv.org} ${DIM}(${pv.githubOrg || "?"})${RESET}`, valW)}`);
2524
+ previewLines.push(pv.connected
2525
+ ? ` ${label("mode")} ${CYAN}◆ CONNECTED${RESET} ${DIM}· hosted graph active${RESET}`
2526
+ : ` ${label("mode")} ${DIM}◇ LOCAL · Markdown + Git${RESET}`);
2414
2527
  if (pv.branch) previewLines.push(` ${label("branch")} ${BOLD}${pv.branch}${RESET}`);
2415
2528
  if (pv.lastCommit) previewLines.push(` ${label("last")} ${DIM}${truncate(pv.lastCommit, valW)}${RESET}`);
2416
2529
  if (pv.repos && pv.repos.length) {
@@ -2424,7 +2537,6 @@ async function browser(args = {}) {
2424
2537
  }
2425
2538
  const modeBits = [];
2426
2539
  modeBits.push(runtimeShort(runtimeFor(selected, args.runtimeOverride)));
2427
- if (pv.mode) modeBits.push(pv.mode);
2428
2540
  if (pv.flowVariant && pv.flowVariant !== "legacy") modeBits.push(pv.flowVariant);
2429
2541
  if (pv.telegram) modeBits.push("telegram");
2430
2542
  if (selected.slug === def) modeBits.push("default");
package/bin/cli.js CHANGED
@@ -44,6 +44,7 @@ const {
44
44
  redactToken,
45
45
  } = require("../lib/setup");
46
46
  const { detectContext, isWritable, isSystemDir } = require("../lib/context");
47
+ const { commandExists } = require("../lib/path-lookup");
47
48
 
48
49
  const API_URL = process.env.EGREGORE_API_URL || "https://egregore-production-55f2.up.railway.app";
49
50
  const INSTANCES_FILE = path.join(os.homedir(), ".egregore", "instances.json");
@@ -97,7 +98,7 @@ function detectCwdEgregore(cwd) {
97
98
  }
98
99
 
99
100
  function binaryOnPath(bin) {
100
- return spawnSync("which", [bin], { stdio: "pipe" }).status === 0;
101
+ return commandExists(bin);
101
102
  }
102
103
 
103
104
  function assistantLabel(runtime) {
@@ -406,7 +407,6 @@ async function preflight() {
406
407
  // Verify prerequisites before starting. Fast fail with specific fix hints
407
408
  // beats discovering missing tools mid-install. Claude Code is checked
408
409
  // separately at auto-exec time since users can install it after.
409
- const { spawnSync } = require("node:child_process");
410
410
  const checks = [
411
411
  { bin: "git", hint: "install Xcode command-line tools or https://git-scm.com" },
412
412
  { bin: "jq", hint: "brew install jq (or https://stedolan.github.io/jq/)" },
@@ -417,8 +417,7 @@ async function preflight() {
417
417
  missing.push({ bin: "node >=18", hint: `you're on Node ${process.versions.node}; upgrade at https://nodejs.org` });
418
418
  }
419
419
  for (const c of checks) {
420
- const r = spawnSync("which", [c.bin], { stdio: "pipe" });
421
- if (r.status !== 0) missing.push(c);
420
+ if (!commandExists(c.bin)) missing.push(c);
422
421
  }
423
422
  if (missing.length === 0) return;
424
423
 
package/lib/auth.js CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  const https = require("node:https");
7
7
  const { spawnSync } = require("node:child_process");
8
+ const { commandExists } = require("./path-lookup");
8
9
 
9
10
  /**
10
11
  * Try to get a token from the user's `gh` CLI, if installed and authenticated.
@@ -31,8 +32,7 @@ function envGhAuth() {
31
32
 
32
33
  function ghCliExists() {
33
34
  if (process.env.EGREGORE_SKIP_GH === "1") return false;
34
- const which = spawnSync("which", ["gh"], { stdio: "pipe" });
35
- return which.status === 0;
35
+ return commandExists("gh");
36
36
  }
37
37
 
38
38
  function canUseInteractiveBrowserFlow() {
@@ -46,8 +46,7 @@ const GH_INSTALL_URL = "https://cli.github.com";
46
46
  // and repos vary too much for a single command, so we point at the canonical
47
47
  // page there (and always print GH_INSTALL_URL alongside on every platform).
48
48
  function _has(b) {
49
- try { return spawnSync("which", [b], { stdio: "pipe" }).status === 0; }
50
- catch { return false; }
49
+ return commandExists(b);
51
50
  }
52
51
 
53
52
  // True on a machine with no way to open a browser (server / SSH). macOS and
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+
6
+ function executableFile(candidate, platform) {
7
+ try {
8
+ if (!fs.statSync(candidate).isFile()) return false;
9
+ fs.accessSync(
10
+ candidate,
11
+ platform === "win32" ? fs.constants.F_OK : fs.constants.X_OK,
12
+ );
13
+ return true;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ function executableNames(command, env, platform) {
20
+ if (platform !== "win32" || path.extname(command)) return [command];
21
+ const pathExt = env.PATHEXT || ".COM;.EXE;.BAT;.CMD";
22
+ return pathExt
23
+ .split(";")
24
+ .filter(Boolean)
25
+ .map((extension) => `${command}${extension.toLowerCase()}`);
26
+ }
27
+
28
+ /**
29
+ * Resolve a command from PATH without relying on the external `which` utility.
30
+ *
31
+ * NixOS provides declared executables through PATH but does not guarantee
32
+ * `which` is installed. Keeping this lookup in Node also avoids spawning a
33
+ * subprocess for every prerequisite and harness check.
34
+ */
35
+ function commandExists(command, options = {}) {
36
+ if (typeof command !== "string" || command.length === 0) return false;
37
+
38
+ const env = options.env || process.env;
39
+ const platform = options.platform || process.platform;
40
+ const cwd = options.cwd || process.cwd();
41
+ const separator = platform === "win32" ? ";" : ":";
42
+ const pathValue = env.PATH || env.Path || env.path || "";
43
+ const names = executableNames(command, env, platform);
44
+ const hasPathSeparator =
45
+ path.isAbsolute(command) || command.includes("/") || command.includes("\\");
46
+
47
+ if (hasPathSeparator) {
48
+ return names.some((name) =>
49
+ executableFile(path.isAbsolute(name) ? name : path.resolve(cwd, name), platform),
50
+ );
51
+ }
52
+
53
+ return pathValue.split(separator).some((entry) => {
54
+ const directory = entry ? entry.replace(/^"(.*)"$/, "$1") : cwd;
55
+ return names.some((name) => executableFile(path.join(directory, name), platform));
56
+ });
57
+ }
58
+
59
+ module.exports = { commandExists };
package/lib/setup.js CHANGED
@@ -17,6 +17,7 @@ const {
17
17
  installPiRuntime,
18
18
  reportPiRuntimeSummary,
19
19
  } = require("./pi-runtime");
20
+ const { commandExists } = require("./path-lookup");
20
21
 
21
22
  const https = require("node:https");
22
23
 
@@ -154,6 +155,45 @@ function slugifyName(name) {
154
155
  .replace(/^-|-$/g, "");
155
156
  }
156
157
 
158
+ /**
159
+ * Choose an install directory without ever adopting an unrelated checkout.
160
+ *
161
+ * Most Egregore forks are named "egregore", so deriving the target only from
162
+ * the fork basename makes different organizations collide in the same parent
163
+ * directory. If that default path is already a Git checkout with another
164
+ * origin, leave it untouched and use egregore-{org-slug}. The fallback must
165
+ * also be absent or point at the requested fork; otherwise stop rather than
166
+ * guessing at another path.
167
+ */
168
+ function resolveEgregoreInstallDir(base, forkUrl, slug) {
169
+ const forkDirName = forkUrl.split("/").pop().replace(/\.git$/, "");
170
+ const requestedDir = path.join(base, forkDirName);
171
+ const requestedGitPath = path.join(requestedDir, ".git");
172
+
173
+ if (!fs.existsSync(requestedGitPath) || gitRemoteMatches(requestedDir, forkUrl)) {
174
+ return { egregoreDir: requestedDir, redirectedFrom: null };
175
+ }
176
+
177
+ const safeSlug = slugifyName(slug) || "instance";
178
+ const fallbackDir = path.join(base, `egregore-${safeSlug}`);
179
+ if (path.resolve(fallbackDir) === path.resolve(requestedDir)) {
180
+ throw new Error(
181
+ `Refusing to install into ${requestedDir}: its Git origin does not match ${forkUrl}`
182
+ );
183
+ }
184
+
185
+ if (fs.existsSync(fallbackDir)) {
186
+ const fallbackGitPath = path.join(fallbackDir, ".git");
187
+ if (!fs.existsSync(fallbackGitPath) || !gitRemoteMatches(fallbackDir, forkUrl)) {
188
+ throw new Error(
189
+ `Refusing to install into ${fallbackDir}: the fallback path already belongs to another checkout or directory`
190
+ );
191
+ }
192
+ }
193
+
194
+ return { egregoreDir: fallbackDir, redirectedFrom: requestedDir };
195
+ }
196
+
157
197
  function normalizeUsageType(data = {}) {
158
198
  if (data.usage_type === "founder_group" || data.usage_type === "joiner_group") {
159
199
  return data.usage_type;
@@ -341,8 +381,17 @@ async function install(data, ui, targetDir, opts = {}) {
341
381
  const preferredRuntime = normalizeRuntime(opts.runtime);
342
382
 
343
383
  // Directory name from fork URL (what git clone would use), e.g. "egregore"
344
- const forkDirName = fork_url.split("/").pop().replace(/\.git$/, "");
345
- const egregoreDir = path.join(base, forkDirName);
384
+ let forkDirName = fork_url.split("/").pop().replace(/\.git$/, "");
385
+ const {
386
+ egregoreDir,
387
+ redirectedFrom,
388
+ } = resolveEgregoreInstallDir(base, fork_url, slug || github_org || org_name);
389
+ if (redirectedFrom) {
390
+ forkDirName = path.basename(egregoreDir);
391
+ ui.warn(
392
+ `${redirectedFrom} belongs to another Git origin — leaving it untouched and installing at ${egregoreDir}`
393
+ );
394
+ }
346
395
  const memoryDirName = memory_url
347
396
  .split("/")
348
397
  .pop()
@@ -953,10 +1002,6 @@ function installCodexSkills(egregoreDir) {
953
1002
  return false;
954
1003
  }
955
1004
 
956
- function commandExists(bin) {
957
- return spawnSync("which", [bin], { stdio: "pipe" }).status === 0;
958
- }
959
-
960
1005
  function registerInstance(slug, name, egregoreDir, runtime) {
961
1006
  const registryDir = path.join(os.homedir(), ".egregore");
962
1007
  const registryFile = path.join(registryDir, "instances.json");
@@ -1433,5 +1478,6 @@ module.exports = {
1433
1478
  initMemoryDirs,
1434
1479
  ensureDevelop,
1435
1480
  ensureConnectedConfig,
1481
+ resolveEgregoreInstallDir,
1436
1482
  run,
1437
1483
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-egregore",
3
- "version": "0.19.20",
3
+ "version": "0.19.22",
4
4
  "description": "Set up Egregore for your team in one command — installs the egregore launcher and your choice of Claude Code, Codex, or Pi.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -12,7 +12,7 @@
12
12
  "scripts": {
13
13
  "build:runtime": "node scripts/build-runtime-bundle.mjs && node scripts/build-pi-runtime-bundle.mjs",
14
14
  "build:pi-runtime": "node scripts/build-pi-runtime-bundle.mjs",
15
- "test": "node scripts/build-runtime-bundle.mjs --check && node scripts/build-pi-runtime-bundle.mjs --check && node --test test/context.test.js test/parse-join-target.test.js test/skip-repos.test.js test/onboarding-shortcircuit.test.js test/welcome-screen.test.js test/runtime-selection.test.js test/command-generation.test.js test/auth-gh.test.js test/gauntlet-fixes.test.js test/codex-runtime.test.js test/pi-runtime.test.js test/pi-product-workflows.test.js test/installer-runtime-source.test.js test/scroll-repro.test.js test/launcher-refocus-redraw.test.js test/multiselect-remote-search.test.js test/back-navigation.test.js test/manifest-form.test.js test/launcher-delete.test.js test/launcher-connect.test.js test/launcher-permissions.test.js test/launcher-update.test.js test/connected-config.test.js",
15
+ "test": "node scripts/build-runtime-bundle.mjs --check && node scripts/build-pi-runtime-bundle.mjs --check && node --test test/context.test.js test/path-lookup.test.js test/nixos-portability.test.js test/parse-join-target.test.js test/skip-repos.test.js test/install-origin-guard.test.js test/onboarding-shortcircuit.test.js test/welcome-screen.test.js test/runtime-selection.test.js test/command-generation.test.js test/auth-gh.test.js test/gauntlet-fixes.test.js test/codex-runtime.test.js test/pi-runtime.test.js test/pi-product-workflows.test.js test/installer-runtime-source.test.js test/scroll-repro.test.js test/launcher-refocus-redraw.test.js test/multiselect-remote-search.test.js test/back-navigation.test.js test/manifest-form.test.js test/launcher-delete.test.js test/launcher-connect.test.js test/launcher-connected-status.test.js test/launcher-permissions.test.js test/launcher-update.test.js test/connected-config.test.js",
16
16
  "test:e2e": "node --test test/e2e-local.test.js",
17
17
  "test:all": "node scripts/build-runtime-bundle.mjs --check && node scripts/build-pi-runtime-bundle.mjs --check && node --test test/"
18
18
  },
@@ -0,0 +1,66 @@
1
+ # External notification consent
2
+
3
+ This protocol applies to every Telegram, Teams, or other external notification
4
+ in every harness. It applies even when the user asked to announce, notify,
5
+ handoff, invite, or message someone, and even when the harness is running with
6
+ broad tool permissions.
7
+
8
+ ## Invariant
9
+
10
+ Never dispatch an external notification until the human has approved that one
11
+ exact delivery in a dedicated checkpoint. The checkpoint must show:
12
+
13
+ - the organization;
14
+ - the final recipient or group;
15
+ - every channel that will receive it;
16
+ - the exact final message, including links.
17
+
18
+ A workflow request, plan approval, batch approval, prior approval, broad
19
+ permission mode, or approval of a related action is not notification consent.
20
+ There is no standing "always allow" choice. One approval authorizes one attempt
21
+ to send one immutable message.
22
+
23
+ ## Required flow
24
+
25
+ 1. Finish the message first. Publish any artifact and insert its real URL
26
+ before planning; never change the message after preview.
27
+ 2. Resolve the exact delivery without sending:
28
+
29
+ ```bash
30
+ PLAN_JSON=$(bash bin/notify.sh plan send "$RECIPIENT" "$MESSAGE")
31
+ # or
32
+ PLAN_JSON=$(bash bin/notify.sh plan group "$MESSAGE")
33
+ ```
34
+
35
+ 3. Read `plan_id` and `digest` from `PLAN_JSON`. Show the organization,
36
+ recipient, all `channels`/`deliveries`, and exact `message` in a dedicated
37
+ Send / Edit / Cancel question. Use the harness's structured question UI
38
+ when available. Otherwise show the same fields in plain text and stop for
39
+ the user's answer.
40
+ 4. Only if the user selects Send for that exact preview:
41
+
42
+ ```bash
43
+ APPROVAL_JSON=$(bash bin/notify.sh approve \
44
+ "$PLAN_ID" "$DIGEST" APPROVE_EXACT_NOTIFICATION)
45
+ APPROVAL_TOKEN=$(printf '%s' "$APPROVAL_JSON" | jq -r '.approval_token')
46
+ bash bin/notify.sh dispatch "$PLAN_ID" "$APPROVAL_TOKEN"
47
+ ```
48
+
49
+ 5. If the user chooses Edit, cancel the old plan, edit, create a new plan, and
50
+ preview again. If they choose Cancel, cancel it and do not send. If the plan
51
+ expires or dispatch fails, create a new plan and obtain new consent; never
52
+ retry from the old approval.
53
+
54
+ ## Hard rules
55
+
56
+ - Never call notification API endpoints directly. Use `bin/notify.sh`.
57
+ - Never use legacy `notify.sh send` or `notify.sh group`; they only create a
58
+ proposal and intentionally cannot dispatch.
59
+ - Never substitute a group when a direct message cannot be delivered.
60
+ - Never add a channel after preview.
61
+ - Never approve or dispatch from a detached/background process, hook, cron
62
+ job, batch execution, or unattended agent. Those paths may create a plan and
63
+ report that human approval is pending.
64
+ - Multiple direct recipients require a separate preview and approval for each
65
+ exact message. A group fanout may use one approval only when every receiving
66
+ channel is listed in the preview.
@@ -145,8 +145,19 @@ function currentAuthor(projectDir) {
145
145
  }
146
146
  }
147
147
 
148
- function isProtectedBranch(branch) {
149
- return branch === "develop" || branch === "main" || branch === "master";
148
+ function configuredBaseBranch(projectDir) {
149
+ try {
150
+ const raw = fs.readFileSync(path.join(projectDir, "egregore.json"), "utf-8");
151
+ const value = JSON.parse(raw).base_branch;
152
+ return typeof value === "string" && value.trim() ? value.trim() : "develop";
153
+ } catch {
154
+ return "develop";
155
+ }
156
+ }
157
+
158
+ function isProtectedBranch(branch, projectDir) {
159
+ return branch === configuredBaseBranch(projectDir) ||
160
+ branch === "develop" || branch === "main" || branch === "master";
150
161
  }
151
162
 
152
163
  function isExemptPath(rawPath, projectDir, memoryDir) {
@@ -403,7 +414,7 @@ function main() {
403
414
  if (context.external) return;
404
415
  const projectDir = context.projectDir;
405
416
  const branch = currentBranch(projectDir);
406
- if (!isProtectedBranch(branch)) return;
417
+ if (!isProtectedBranch(branch, projectDir)) return;
407
418
 
408
419
  // Consent bypass: the user explicitly chose to work on this protected branch.
409
420
  // Written only after they pick "Proceed on <branch>"; cleared on session start,
@@ -17,9 +17,13 @@ when it is available; otherwise render compact numbered choices with an
17
17
 
18
18
  1. Read `.claude/skills/add/SKILL.md` for the workflow details.
19
19
  2. Run the referenced `bin/` scripts directly from Codex.
20
- 3. Treat graph, publish, and notify steps as best-effort unless that workflow
21
- explicitly says they are required.
22
- 4. Keep local-mode behavior filesystem-first and avoid graph or notification
20
+ 3. Treat graph and publish steps as best-effort unless that workflow explicitly
21
+ says they are required.
22
+ 4. For every external notification, follow
23
+ `.claude/context/notification-consent.md`: plan without sending, then show
24
+ a separate exact Send / Edit / Cancel checkpoint. Never infer notification
25
+ consent from the workflow request or a batch approval.
26
+ 5. Keep local-mode behavior filesystem-first and avoid graph or notification
23
27
  calls when `egregore.json` declares `"mode": "local"`.
24
- 5. Never call the deprecated `egregore-handoff` CLI for Egregore project
28
+ 6. Never call the deprecated `egregore-handoff` CLI for Egregore project
25
29
  handoffs.
@@ -22,33 +22,46 @@ bash bin/publish-artifact.sh document "$FILE_PATH" --title "$TITLE" --author "$A
22
22
  Use `handoff`, `quest`, or `document` as the artifact type when appropriate.
23
23
  Never fabricate a URL.
24
24
 
25
- 3. Preview the exact message. Confirm with structured Codex question tooling
26
- when available; otherwise render:
25
+ 3. Follow `.claude/context/notification-consent.md`. Create a plan without
26
+ sending:
27
+
28
+ ```bash
29
+ PLAN_JSON=$(bash bin/notify.sh plan group "$MESSAGE")
30
+ ```
31
+
32
+ 4. Preview the plan's exact organization, all deliveries/channels, and exact
33
+ final message. Confirm in a separate checkpoint with structured Codex
34
+ question tooling when available; otherwise render those fields followed by:
27
35
 
28
36
  ```text
29
37
  Send this announcement?
30
38
  1. Send
31
39
  2. Edit
32
- Other:
40
+ 3. Cancel
33
41
  ```
34
42
 
35
- 4. If the user edits, redraft and preview again. If they approve, run:
43
+ 5. If the user edits, cancel the old plan, redraft, plan, and preview again.
44
+ If they cancel, cancel the plan and stop. Only after they select Send for
45
+ that exact preview, run:
36
46
 
37
47
  ```bash
38
- bash bin/notify.sh group "$MESSAGE"
48
+ APPROVAL_JSON=$(bash bin/notify.sh approve "$PLAN_ID" "$DIGEST" APPROVE_EXACT_NOTIFICATION)
49
+ APPROVAL_TOKEN=$(printf '%s' "$APPROVAL_JSON" | jq -r '.approval_token')
50
+ bash bin/notify.sh dispatch "$PLAN_ID" "$APPROVAL_TOKEN"
39
51
  ```
40
52
 
41
- 5. Emit telemetry in the background:
53
+ 6. Emit telemetry in the background:
42
54
 
43
55
  ```bash
44
56
  bash bin/telemetry.sh emit "command" '{"command":"announce"}' >/dev/null 2>&1 &
45
57
  ```
46
58
 
47
- 6. Confirm whether the group send succeeded.
59
+ 7. Confirm whether the exact approved group send succeeded.
48
60
 
49
61
  ## Rules
50
62
 
51
- - Always preview before sending.
63
+ - The request to announce is not dispatch consent. Always use a dedicated
64
+ exact-delivery checkpoint.
52
65
  - Do not expose notification credentials.
53
66
  - Use `bin/notify.sh`; do not call Telegram directly.
54
67
  - Do not use Claude Code commands.