create-egregore 0.19.33 → 0.20.0

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 (171) hide show
  1. package/assets/egregore-launcher.js +118 -13
  2. package/bin/cli.js +19 -3
  3. package/lib/connect-runtime.js +4 -2
  4. package/lib/local.js +15 -1
  5. package/lib/prime-runtime.js +208 -0
  6. package/lib/setup.js +45 -3
  7. package/package.json +4 -3
  8. package/runtime/codex/.codex/skills/ingest-notion/SKILL.md +29 -0
  9. package/runtime/codex/.codex/skills/notion-connect/SKILL.md +29 -0
  10. package/runtime/codex/.codex/skills/teams-connect/SKILL.md +29 -0
  11. package/runtime/codex/.codex/spec-manifest.json +3 -3
  12. package/runtime/codex/AGENTS.md +11 -4
  13. package/runtime/codex/bin/activity-data.sh +4 -8
  14. package/runtime/codex/bin/attendant.sh +13 -0
  15. package/runtime/codex/bin/autosave.sh +27 -1
  16. package/runtime/codex/bin/codex-render-spec.mjs +6 -0
  17. package/runtime/codex/bin/codex-session-start.sh +3 -1
  18. package/runtime/codex/bin/graph-batch.sh +25 -6
  19. package/runtime/codex/bin/graph-op.sh +5 -4
  20. package/runtime/codex/bin/graph.sh +31 -5
  21. package/runtime/codex/bin/ingest.py +81 -5
  22. package/runtime/codex/bin/lib/context.sh +26 -1
  23. package/runtime/codex/bin/lib/greeting.sh +4 -1
  24. package/runtime/codex/bin/lib/worktree-links.sh +25 -0
  25. package/runtime/codex/manifest.json +7 -1
  26. package/runtime/pi/.codex/skills/ingest-notion/SKILL.md +29 -0
  27. package/runtime/pi/.codex/skills/notion-connect/SKILL.md +29 -0
  28. package/runtime/pi/.codex/skills/teams-connect/SKILL.md +29 -0
  29. package/runtime/pi/.codex/spec-manifest.json +3 -3
  30. package/runtime/pi/.pi/APPEND_SYSTEM.md +3 -2
  31. package/runtime/pi/.pi/extensions/egregore.ts +17 -6
  32. package/runtime/pi/.pi/spec-manifest.json +2 -2
  33. package/runtime/pi/AGENTS.md +11 -4
  34. package/runtime/pi/bin/activity-data.sh +4 -8
  35. package/runtime/pi/bin/attendant.sh +13 -0
  36. package/runtime/pi/bin/autosave.sh +27 -1
  37. package/runtime/pi/bin/codex-render-spec.mjs +6 -0
  38. package/runtime/pi/bin/codex-session-start.sh +3 -1
  39. package/runtime/pi/bin/graph-batch.sh +25 -6
  40. package/runtime/pi/bin/graph-op.sh +5 -4
  41. package/runtime/pi/bin/graph.sh +31 -5
  42. package/runtime/pi/bin/ingest.py +81 -5
  43. package/runtime/pi/bin/lib/context.sh +26 -1
  44. package/runtime/pi/bin/lib/greeting.sh +4 -1
  45. package/runtime/pi/bin/lib/worktree-links.sh +25 -0
  46. package/runtime/pi/bin/pi-branch-policy.mjs +138 -1
  47. package/runtime/pi/bin/pi-product-workflows.mjs +35 -7
  48. package/runtime/pi/manifest.json +7 -1
  49. package/runtime/prime/.claude/context/notification-consent.md +66 -0
  50. package/runtime/prime/.codex/config.toml +17 -0
  51. package/runtime/prime/.codex/hooks/branch-guard.js +461 -0
  52. package/runtime/prime/.codex/hooks/search-hint.js +67 -0
  53. package/runtime/prime/.codex/hooks.json +30 -0
  54. package/runtime/prime/.codex/skills/activity/SKILL.md +82 -0
  55. package/runtime/prime/.codex/skills/announce/SKILL.md +67 -0
  56. package/runtime/prime/.codex/skills/archive/SKILL.md +45 -0
  57. package/runtime/prime/.codex/skills/branch/SKILL.md +29 -0
  58. package/runtime/prime/.codex/skills/checkup/SKILL.md +45 -0
  59. package/runtime/prime/.codex/skills/commit/SKILL.md +29 -0
  60. package/runtime/prime/.codex/skills/contribute/SKILL.md +29 -0
  61. package/runtime/prime/.codex/skills/create-skill/SKILL.md +29 -0
  62. package/runtime/prime/.codex/skills/dark-mode/SKILL.md +29 -0
  63. package/runtime/prime/.codex/skills/dashboard/SKILL.md +59 -0
  64. package/runtime/prime/.codex/skills/deep-reflect/SKILL.md +109 -0
  65. package/runtime/prime/.codex/skills/delete-user/SKILL.md +29 -0
  66. package/runtime/prime/.codex/skills/emissary/SKILL.md +45 -0
  67. package/runtime/prime/.codex/skills/handoff/SKILL.md +166 -0
  68. package/runtime/prime/.codex/skills/harvest/QUESTION_PALETTE.md +76 -0
  69. package/runtime/prime/.codex/skills/harvest/SKILL.md +131 -0
  70. package/runtime/prime/.codex/skills/ingest/SKILL.md +29 -0
  71. package/runtime/prime/.codex/skills/ingest-notion/SKILL.md +29 -0
  72. package/runtime/prime/.codex/skills/ingest-user-interview/SKILL.md +45 -0
  73. package/runtime/prime/.codex/skills/invite/SKILL.md +44 -0
  74. package/runtime/prime/.codex/skills/issue/SKILL.md +45 -0
  75. package/runtime/prime/.codex/skills/me/SKILL.md +29 -0
  76. package/runtime/prime/.codex/skills/meeting/SKILL.md +45 -0
  77. package/runtime/prime/.codex/skills/note/SKILL.md +29 -0
  78. package/runtime/prime/.codex/skills/notion-connect/SKILL.md +29 -0
  79. package/runtime/prime/.codex/skills/onboarding/SKILL.md +45 -0
  80. package/runtime/prime/.codex/skills/pr/SKILL.md +29 -0
  81. package/runtime/prime/.codex/skills/pull/SKILL.md +29 -0
  82. package/runtime/prime/.codex/skills/push/SKILL.md +29 -0
  83. package/runtime/prime/.codex/skills/quest/SKILL.md +80 -0
  84. package/runtime/prime/.codex/skills/reflect/SKILL.md +45 -0
  85. package/runtime/prime/.codex/skills/review-pr/SKILL.md +45 -0
  86. package/runtime/prime/.codex/skills/save/SKILL.md +125 -0
  87. package/runtime/prime/.codex/skills/slack-connect/SKILL.md +29 -0
  88. package/runtime/prime/.codex/skills/summon/SKILL.md +45 -0
  89. package/runtime/prime/.codex/skills/sync-repos/SKILL.md +29 -0
  90. package/runtime/prime/.codex/skills/teams-connect/SKILL.md +29 -0
  91. package/runtime/prime/.codex/skills/telegram-connect/SKILL.md +29 -0
  92. package/runtime/prime/.codex/skills/telemetry/SKILL.md +29 -0
  93. package/runtime/prime/.codex/skills/test/SKILL.md +45 -0
  94. package/runtime/prime/.codex/skills/the-spiral/SKILL.md +84 -0
  95. package/runtime/prime/.codex/skills/tui-design/SKILL.md +45 -0
  96. package/runtime/prime/.codex/skills/tutorial/SKILL.md +45 -0
  97. package/runtime/prime/.codex/skills/update/SKILL.md +29 -0
  98. package/runtime/prime/.codex/skills/view/SKILL.md +156 -0
  99. package/runtime/prime/.codex/skills/view/agents/openai.yaml +4 -0
  100. package/runtime/prime/.codex/skills/view/assets/compose-scaffold.html +221 -0
  101. package/runtime/prime/.codex/skills/visual-explain/SKILL.md +45 -0
  102. package/runtime/prime/.codex/skills/wrap/SKILL.md +58 -0
  103. package/runtime/prime/.codex/spec-manifest.json +132 -0
  104. package/runtime/prime/.prime/agent/APPEND_SYSTEM.md +393 -0
  105. package/runtime/prime/.prime/agent/extensions/egregore.ts +460 -0
  106. package/runtime/prime/.prime/agent/settings.json +4 -0
  107. package/runtime/prime/.prime/agent/skills/egregore/SKILL.md +34 -0
  108. package/runtime/prime/.prime/agent/skills/egregore/pyproject.toml +13 -0
  109. package/runtime/prime/.prime/agent/skills/egregore/src/egregore/__init__.py +156 -0
  110. package/runtime/prime/.prime/agent/spec-manifest.json +12 -0
  111. package/runtime/prime/AGENTS.md +489 -0
  112. package/runtime/prime/bin/activity-data.sh +238 -0
  113. package/runtime/prime/bin/agent.sh +775 -0
  114. package/runtime/prime/bin/attendant.sh +707 -0
  115. package/runtime/prime/bin/autosave.sh +112 -0
  116. package/runtime/prime/bin/capture-reconcile.sh +69 -0
  117. package/runtime/prime/bin/capture-run.sh +341 -0
  118. package/runtime/prime/bin/codex-install-skills.sh +78 -0
  119. package/runtime/prime/bin/codex-render-spec.mjs +440 -0
  120. package/runtime/prime/bin/codex-session-start.sh +265 -0
  121. package/runtime/prime/bin/codex-skill-render.mjs +347 -0
  122. package/runtime/prime/bin/codex-sync-skills.sh +232 -0
  123. package/runtime/prime/bin/connect-refresh.sh +33 -0
  124. package/runtime/prime/bin/dashboard-data.sh +178 -0
  125. package/runtime/prime/bin/graph-batch.sh +79 -0
  126. package/runtime/prime/bin/graph-op.sh +1102 -0
  127. package/runtime/prime/bin/graph-wal.sh +229 -0
  128. package/runtime/prime/bin/graph.sh +223 -0
  129. package/runtime/prime/bin/handoff-pr-backfill.sh +86 -0
  130. package/runtime/prime/bin/handoff-run.sh +671 -0
  131. package/runtime/prime/bin/handoff-save-egregore.sh +212 -0
  132. package/runtime/prime/bin/index-handoff.sh +322 -0
  133. package/runtime/prime/bin/ingest-graph.sh +80 -0
  134. package/runtime/prime/bin/ingest-surface.html +495 -0
  135. package/runtime/prime/bin/ingest.py +756 -0
  136. package/runtime/prime/bin/ingest.sh +279 -0
  137. package/runtime/prime/bin/ingest_graph.py +856 -0
  138. package/runtime/prime/bin/ingest_surface.py +544 -0
  139. package/runtime/prime/bin/lib/artifact-id.sh +46 -0
  140. package/runtime/prime/bin/lib/config.sh +116 -0
  141. package/runtime/prime/bin/lib/context.sh +621 -0
  142. package/runtime/prime/bin/lib/dashboard-artifact.sh +242 -0
  143. package/runtime/prime/bin/lib/ensure-develop.sh +183 -0
  144. package/runtime/prime/bin/lib/git-sync.sh +406 -0
  145. package/runtime/prime/bin/lib/greeting.sh +519 -0
  146. package/runtime/prime/bin/lib/hash.sh +13 -0
  147. package/runtime/prime/bin/lib/identity.sh +213 -0
  148. package/runtime/prime/bin/lib/metrics.sh +374 -0
  149. package/runtime/prime/bin/lib/noncode.sh +31 -0
  150. package/runtime/prime/bin/lib/permissions.sh +106 -0
  151. package/runtime/prime/bin/lib/time.sh +56 -0
  152. package/runtime/prime/bin/lib/worktree-links.sh +79 -0
  153. package/runtime/prime/bin/notify.sh +636 -0
  154. package/runtime/prime/bin/person.py +630 -0
  155. package/runtime/prime/bin/person.sh +448 -0
  156. package/runtime/prime/bin/pi-branch-policy.mjs +312 -0
  157. package/runtime/prime/bin/pi-product-workflows.mjs +394 -0
  158. package/runtime/prime/bin/prime-render-spec.mjs +123 -0
  159. package/runtime/prime/bin/prime-session-start.sh +24 -0
  160. package/runtime/prime/bin/publish-artifact.sh +271 -0
  161. package/runtime/prime/bin/publish-references.sh +144 -0
  162. package/runtime/prime/bin/repo-state.sh +96 -0
  163. package/runtime/prime/bin/search.sh +240 -0
  164. package/runtime/prime/bin/session-autosave.sh +209 -0
  165. package/runtime/prime/bin/session-log.sh +199 -0
  166. package/runtime/prime/bin/settings.sh +304 -0
  167. package/runtime/prime/bin/telemetry.sh +391 -0
  168. package/runtime/prime/bin/worktree-create.sh +128 -0
  169. package/runtime/prime/bin/worktree-remove.sh +50 -0
  170. package/runtime/prime/bin/worktree.sh +198 -0
  171. package/runtime/prime/manifest.json +178 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "source": "egregore-pi-meta-harness",
3
3
  "policy": "Pi-native lifecycle layer over the available OSS Codex skill corpus",
4
- "skillCount": 43,
4
+ "skillCount": 46,
5
5
  "skills": [
6
6
  "activity",
7
7
  "announce",
@@ -19,12 +19,14 @@
19
19
  "handoff",
20
20
  "harvest",
21
21
  "ingest",
22
+ "ingest-notion",
22
23
  "ingest-user-interview",
23
24
  "invite",
24
25
  "issue",
25
26
  "me",
26
27
  "meeting",
27
28
  "note",
29
+ "notion-connect",
28
30
  "onboarding",
29
31
  "pr",
30
32
  "pull",
@@ -36,6 +38,7 @@
36
38
  "slack-connect",
37
39
  "summon",
38
40
  "sync-repos",
41
+ "teams-connect",
39
42
  "telegram-connect",
40
43
  "telemetry",
41
44
  "test",
@@ -70,6 +73,7 @@
70
73
  ".codex/skills/handoff/SKILL.md",
71
74
  ".codex/skills/harvest/QUESTION_PALETTE.md",
72
75
  ".codex/skills/harvest/SKILL.md",
76
+ ".codex/skills/ingest-notion/SKILL.md",
73
77
  ".codex/skills/ingest-user-interview/SKILL.md",
74
78
  ".codex/skills/ingest/SKILL.md",
75
79
  ".codex/skills/invite/SKILL.md",
@@ -77,6 +81,7 @@
77
81
  ".codex/skills/me/SKILL.md",
78
82
  ".codex/skills/meeting/SKILL.md",
79
83
  ".codex/skills/note/SKILL.md",
84
+ ".codex/skills/notion-connect/SKILL.md",
80
85
  ".codex/skills/onboarding/SKILL.md",
81
86
  ".codex/skills/pr/SKILL.md",
82
87
  ".codex/skills/pull/SKILL.md",
@@ -88,6 +93,7 @@
88
93
  ".codex/skills/slack-connect/SKILL.md",
89
94
  ".codex/skills/summon/SKILL.md",
90
95
  ".codex/skills/sync-repos/SKILL.md",
96
+ ".codex/skills/teams-connect/SKILL.md",
91
97
  ".codex/skills/telegram-connect/SKILL.md",
92
98
  ".codex/skills/telemetry/SKILL.md",
93
99
  ".codex/skills/test/SKILL.md",
@@ -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.
@@ -0,0 +1,17 @@
1
+ [features]
2
+ hooks = true
3
+
4
+ [tui]
5
+ # Codex currently accepts built-in footer item IDs only. Egregore's dynamic
6
+ # ◆ CONNECTED / ◇ LOCAL identity is rendered by bin/codex-session-start.sh.
7
+ status_line = [
8
+ "model-with-reasoning",
9
+ "git-branch",
10
+ "current-dir",
11
+ "context-remaining",
12
+ "five-hour-limit",
13
+ "weekly-limit",
14
+ "permissions",
15
+ "approval-mode",
16
+ ]
17
+ status_line_use_colors = true
@@ -0,0 +1,461 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+ const { spawnSync } = require("node:child_process");
7
+
8
+ function readStdin() {
9
+ try {
10
+ return fs.readFileSync(0, "utf-8");
11
+ } catch {
12
+ return "";
13
+ }
14
+ }
15
+
16
+ function parseJson(raw) {
17
+ try {
18
+ return JSON.parse(raw || "{}");
19
+ } catch {
20
+ return {};
21
+ }
22
+ }
23
+
24
+ function run(cmd, args, cwd) {
25
+ const result = spawnSync(cmd, args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
26
+ if (result.status !== 0) return "";
27
+ return String(result.stdout || "").trim();
28
+ }
29
+
30
+ function realish(filePath) {
31
+ const absolute = path.resolve(filePath);
32
+ let cursor = absolute;
33
+ while (!fs.existsSync(cursor)) {
34
+ const parent = path.dirname(cursor);
35
+ if (parent === cursor) return absolute;
36
+ cursor = parent;
37
+ }
38
+ try {
39
+ const resolved = fs.realpathSync(cursor);
40
+ return path.join(resolved, path.relative(cursor, absolute));
41
+ } catch {
42
+ return absolute;
43
+ }
44
+ }
45
+
46
+ function inside(child, parent) {
47
+ const rel = path.relative(parent, child);
48
+ return rel === "" || (!!rel && !rel.startsWith("..") && !path.isAbsolute(rel));
49
+ }
50
+
51
+ function nearestExistingDirectory(candidate) {
52
+ let cursor = path.resolve(candidate);
53
+ try {
54
+ if (fs.existsSync(cursor) && !fs.statSync(cursor).isDirectory()) cursor = path.dirname(cursor);
55
+ } catch {}
56
+ while (!fs.existsSync(cursor)) {
57
+ const parent = path.dirname(cursor);
58
+ if (parent === cursor) break;
59
+ cursor = parent;
60
+ }
61
+ return cursor;
62
+ }
63
+
64
+ function gitContext(candidate) {
65
+ if (!candidate) return null;
66
+ const probe = nearestExistingDirectory(candidate);
67
+ const top = run("git", ["-C", probe, "rev-parse", "--show-toplevel"], probe);
68
+ if (!top) return null;
69
+ const commonRaw = run("git", ["-C", top, "rev-parse", "--git-common-dir"], top);
70
+ const common = commonRaw
71
+ ? realish(path.isAbsolute(commonRaw) ? commonRaw : path.join(top, commonRaw))
72
+ : realish(path.join(top, ".git"));
73
+ return { top: realish(top), common };
74
+ }
75
+
76
+ function shellRetarget(command) {
77
+ // Only retarget a single operation. Mixed command chains are evaluated from
78
+ // the hub context so one external `git -C` cannot mask a later project write.
79
+ if (shellSegments(command).length !== 1) return "";
80
+ const gitC = String(command || "").match(/(?:^|[;&|]\s*)git\s+-C\s+("[^"]*"|'[^']*'|[^\s;&|]+)/);
81
+ const cdAnd = String(command || "").match(/^\s*cd\s+("[^"]*"|'[^']*'|[^\s;&|]+)\s*&&/);
82
+ const raw = (gitC && gitC[1]) || (cdAnd && cdAnd[1]) || "";
83
+ const target = raw.replace(/^['"]|['"]$/g, "");
84
+ return /[$`*?]/.test(target) ? "" : target;
85
+ }
86
+
87
+ function operationCandidate(input, baseDir) {
88
+ const toolName = input.tool_name || "";
89
+ const toolInput = input.tool_input || {};
90
+ const explicitWorkdir = toolInput.workdir || toolInput.cwd || "";
91
+ let candidate = explicitWorkdir
92
+ ? (path.isAbsolute(explicitWorkdir) ? explicitWorkdir : path.join(baseDir, explicitWorkdir))
93
+ : baseDir;
94
+
95
+ if (toolName === "Edit" || toolName === "Write") {
96
+ const target = toolInput.file_path || toolInput.path || "";
97
+ if (target) candidate = path.isAbsolute(target) ? target : path.join(candidate, target);
98
+ } else if (toolName === "Bash") {
99
+ const target = shellRetarget(toolInput.command || "");
100
+ if (target) candidate = path.isAbsolute(target) ? target : path.join(candidate, target);
101
+ }
102
+
103
+ return candidate;
104
+ }
105
+
106
+ function findProjectContext(input) {
107
+ // The launcher keeps EGREGORE_CODEX_PROJECT_DIR on the hub checkout while
108
+ // individual tool calls can target a task worktree, memory, or a managed
109
+ // repo. Compare Git common dirs: a task worktree shares the hub common dir
110
+ // and must use its own branch; memory/managed repos are separate and exempt.
111
+ const fallback = input.cwd || process.cwd();
112
+ const hubSeed = process.env.EGREGORE_CODEX_PROJECT_DIR || fallback;
113
+ const hub = gitContext(hubSeed);
114
+ const baseDir = input.cwd || hub?.top || fallback;
115
+ const candidate = operationCandidate(input, baseDir);
116
+ const operation = gitContext(candidate);
117
+
118
+ if (!hub) return { projectDir: operation?.top || realish(baseDir), external: false };
119
+ if (operation && operation.common === hub.common) {
120
+ return { projectDir: operation.top, external: false };
121
+ }
122
+ if (operation && operation.common !== hub.common) {
123
+ return { projectDir: hub.top, external: true };
124
+ }
125
+
126
+ const resolvedCandidate = realish(candidate);
127
+ if (!inside(resolvedCandidate, hub.top)) {
128
+ return { projectDir: hub.top, external: true };
129
+ }
130
+ return { projectDir: hub.top, external: false };
131
+ }
132
+
133
+ function currentBranch(projectDir) {
134
+ return run("git", ["-C", projectDir, "rev-parse", "--abbrev-ref", "HEAD"], projectDir);
135
+ }
136
+
137
+ function currentAuthor(projectDir) {
138
+ try {
139
+ const raw = fs.readFileSync(path.join(projectDir, ".egregore-state.json"), "utf-8");
140
+ const state = JSON.parse(raw);
141
+ const value = state.handle || state.display_name || state.name || "dev";
142
+ return String(value).trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "dev";
143
+ } catch {
144
+ return "dev";
145
+ }
146
+ }
147
+
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";
161
+ }
162
+
163
+ function isExemptPath(rawPath, projectDir, memoryDir) {
164
+ if (!rawPath) return false;
165
+ const absolute = path.isAbsolute(rawPath) ? rawPath : path.join(projectDir, rawPath);
166
+ const resolved = realish(absolute);
167
+
168
+ for (const dir of [".claude", ".codex", "memory"]) {
169
+ const target = realish(path.join(projectDir, dir));
170
+ if (inside(resolved, target)) return true;
171
+ }
172
+
173
+ for (const file of [".egregore-state.json", ".egregore-branch-consent", ".env"]) {
174
+ if (resolved === realish(path.join(projectDir, file))) return true;
175
+ }
176
+
177
+ if (memoryDir && inside(resolved, memoryDir)) return true;
178
+
179
+ return !inside(resolved, projectDir);
180
+ }
181
+
182
+ function extractApplyPatchPaths(command) {
183
+ const paths = [];
184
+ for (const line of String(command || "").split(/\r?\n/)) {
185
+ const match = line.match(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/) || line.match(/^\*\*\* Move to: (.+)$/);
186
+ if (match && match[1]) paths.push(match[1].trim());
187
+ }
188
+ return paths;
189
+ }
190
+
191
+ function commandTargetsOtherRepo(command, projectDir, memoryDir) {
192
+ const paths = [];
193
+ const patterns = [
194
+ /\bcd\s+["']?([^"';&|]+)["']?/g,
195
+ /\bgit\s+-C\s+["']?([^"';&|]+)["']?/g,
196
+ ];
197
+ for (const pattern of patterns) {
198
+ let match;
199
+ while ((match = pattern.exec(command))) paths.push(match[1]);
200
+ }
201
+
202
+ for (const raw of paths) {
203
+ const resolved = realish(path.isAbsolute(raw) ? raw : path.join(projectDir, raw));
204
+ if (memoryDir && inside(resolved, memoryDir)) return true;
205
+ if (!inside(resolved, projectDir)) return true;
206
+ }
207
+ return false;
208
+ }
209
+
210
+ function isBranchSetupCommand(command) {
211
+ if (shellSegments(command).length !== 1) return false;
212
+ return (
213
+ /^\s*bin\/agent\.sh\s+branch\b/.test(command) ||
214
+ /^\s*git\s+(checkout|switch)\s+(-b|-c)\b/.test(command) ||
215
+ /^\s*git\s+branch\s+(dev|feature|bugfix)\//.test(command) ||
216
+ /^\s*git\s+worktree\s+add\b/.test(command) ||
217
+ /^\s*bin\/worktree\.sh\s+setup\b/.test(command)
218
+ );
219
+ }
220
+
221
+ function stagedOnlyFrameworkPaths(projectDir) {
222
+ const files = run("git", ["-C", projectDir, "diff", "--cached", "--name-only"], projectDir)
223
+ .split(/\r?\n/)
224
+ .filter(Boolean);
225
+ return files.length > 0 && files.every((file) => /^(bin\/|\.claude\/|\.codex\/|CLAUDE\.md$|AGENTS\.md$|skills\/)/.test(file));
226
+ }
227
+
228
+ function stripQuotedSegments(command) {
229
+ let out = "";
230
+ let quote = null;
231
+ for (let i = 0; i < String(command || "").length; i++) {
232
+ const ch = command[i];
233
+ if (quote) {
234
+ if (quote === '"' && ch === "\\") i++;
235
+ else if (ch === quote) quote = null;
236
+ continue;
237
+ }
238
+ if (ch === "'" || ch === '"') {
239
+ quote = ch;
240
+ out += "x";
241
+ continue;
242
+ }
243
+ out += ch;
244
+ }
245
+ return out;
246
+ }
247
+
248
+ function maskQuotedOperators(command) {
249
+ let out = "";
250
+ let quote = null;
251
+ for (let i = 0; i < String(command || "").length; i++) {
252
+ const ch = command[i];
253
+ if (quote) {
254
+ if (quote === '"' && ch === "\\") {
255
+ out += ch;
256
+ if (i + 1 < command.length) out += command[++i];
257
+ continue;
258
+ }
259
+ if (ch === quote) quote = null;
260
+ out += /[><|;&]/.test(ch) ? " " : ch;
261
+ continue;
262
+ }
263
+ if (ch === "'" || ch === '"') quote = ch;
264
+ out += ch;
265
+ }
266
+ return out;
267
+ }
268
+
269
+ function shellSegments(command) {
270
+ const segments = [];
271
+ let current = "";
272
+ let quote = null;
273
+ for (let i = 0; i < String(command || "").length; i++) {
274
+ const ch = command[i];
275
+ if (quote) {
276
+ current += ch;
277
+ if (quote === '"' && ch === "\\" && i + 1 < command.length) current += command[++i];
278
+ else if (ch === quote) quote = null;
279
+ continue;
280
+ }
281
+ if (ch === "'" || ch === '"') {
282
+ quote = ch;
283
+ current += ch;
284
+ continue;
285
+ }
286
+ if (ch === ";" || ch === "|" || ch === "&" || ch === "\n") {
287
+ if (current.trim()) segments.push(current.trim());
288
+ current = "";
289
+ continue;
290
+ }
291
+ current += ch;
292
+ }
293
+ if (current.trim()) segments.push(current.trim());
294
+ return segments;
295
+ }
296
+
297
+ function shellWords(segment) {
298
+ const words = [];
299
+ let current = "";
300
+ let quote = null;
301
+ for (let i = 0; i < String(segment || "").length; i++) {
302
+ const ch = segment[i];
303
+ if (quote) {
304
+ if (quote === '"' && ch === "\\" && i + 1 < segment.length) current += segment[++i];
305
+ else if (ch === quote) quote = null;
306
+ else current += ch;
307
+ continue;
308
+ }
309
+ if (ch === "'" || ch === '"') {
310
+ quote = ch;
311
+ continue;
312
+ }
313
+ if (/\s/.test(ch)) {
314
+ if (current) words.push(current);
315
+ current = "";
316
+ continue;
317
+ }
318
+ if (ch === "\\") {
319
+ if (i + 1 < segment.length) current += segment[++i];
320
+ continue;
321
+ }
322
+ current += ch;
323
+ }
324
+ if (current) words.push(current);
325
+ return words;
326
+ }
327
+
328
+ function filesystemMutationStatus(command, projectDir, memoryDir) {
329
+ const commands = new Set(["rm", "mv", "cp", "mkdir", "rmdir", "touch", "chmod", "chown", "ln", "tee"]);
330
+ let unsafe = false;
331
+
332
+ for (const segment of shellSegments(command)) {
333
+ const words = shellWords(segment);
334
+ while (words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0])) words.shift();
335
+ while (words[0] === "sudo" || words[0] === "command" || words[0] === "env") words.shift();
336
+ if (!words.length) continue;
337
+
338
+ const name = path.basename(words.shift());
339
+ if (!commands.has(name)) continue;
340
+
341
+ const args = words.filter((word) => !word.startsWith("-"));
342
+ let targets = args;
343
+ if (name === "cp" || name === "ln") targets = args.length ? [args[args.length - 1]] : [];
344
+ if (name === "chmod" || name === "chown") targets = args.slice(1);
345
+
346
+ if (!targets.length || targets.some((target) =>
347
+ /[$`*?{}]/.test(target) || !isExemptPath(target, projectDir, memoryDir)
348
+ )) unsafe = true;
349
+ }
350
+
351
+ return { unsafe };
352
+ }
353
+
354
+ function redirectLooksMutating(command, projectDir, memoryDir) {
355
+ const visible = maskQuotedOperators(command);
356
+ const redirectPatterns = [
357
+ /(?:^|[\s;|&])(?:\d*)>>?\s*([^&\s;|]+)/g,
358
+ /\|\s*tee\s+(?:-a\s+)?([^&\s;|]+)/g,
359
+ ];
360
+
361
+ for (const pattern of redirectPatterns) {
362
+ let match;
363
+ while ((match = pattern.exec(visible))) {
364
+ const target = String(match[1] || "").replace(/^["']|["']$/g, "");
365
+ if (!target || target === "/dev/null" || target.startsWith("$")) continue;
366
+ if (!isExemptPath(target, projectDir, memoryDir)) return true;
367
+ }
368
+ }
369
+ return false;
370
+ }
371
+
372
+ function shellLooksMutating(command, projectDir, memoryDir) {
373
+ if (!command.trim()) return false;
374
+ const visible = stripQuotedSegments(command);
375
+ if (isBranchSetupCommand(visible)) return false;
376
+
377
+ const mutatingPatterns = [
378
+ /(?:^|[;&|(\n]\s*)(?:sudo\s+)?git(?:\s+-C\s+\S+)?\s+(add|commit|push|rm|mv|reset|restore|checkout\s+--|clean|rebase|merge|cherry-pick)\b/,
379
+ /(?:^|[;&|(\n]\s*)(?:sudo\s+)?apply_patch\b/,
380
+ /(?:^|[;&|(\n]\s*)(?:sudo\s+)?(sed|perl)\b[^;&|]*\s-i(\s|$)/,
381
+ /(?:^|[;&|(\n]\s*)(?:sudo\s+)?(npm|pnpm|yarn|bun)\s+(install|add|remove|update|dedupe|ci)\b/,
382
+ /(?:^|[;&|(\n]\s*)(?:sudo\s+)?(go\s+mod\s+tidy|cargo\s+(fmt|fix|update)|rustfmt|prettier\s+--write|eslint\s+--fix)\b/,
383
+ ];
384
+
385
+ const gitCommit = /(?:^|[;&|(\n]\s*)(?:sudo\s+)?git(?:\s+-C\s+\S+)?\s+commit\b/.test(visible);
386
+ if (gitCommit && stagedOnlyFrameworkPaths(projectDir)) return false;
387
+
388
+ const filesystem = filesystemMutationStatus(command, projectDir, memoryDir);
389
+ return mutatingPatterns.some((pattern) => pattern.test(visible)) ||
390
+ filesystem.unsafe ||
391
+ redirectLooksMutating(command, projectDir, memoryDir);
392
+ }
393
+
394
+ function isConsentCommand(command, branch) {
395
+ const escaped = branch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
396
+ return new RegExp(
397
+ `^\\s*echo\\s+['"]?${escaped}['"]?\\s*>\\s*(?:\\./)?\\.egregore-branch-consent\\s*$`
398
+ ).test(String(command || ""));
399
+ }
400
+
401
+ function deny(reason) {
402
+ process.stdout.write(JSON.stringify({
403
+ hookSpecificOutput: {
404
+ hookEventName: "PreToolUse",
405
+ permissionDecision: "deny",
406
+ permissionDecisionReason: reason,
407
+ },
408
+ }));
409
+ }
410
+
411
+ function main() {
412
+ const input = parseJson(readStdin());
413
+ const context = findProjectContext(input);
414
+ if (context.external) return;
415
+ const projectDir = context.projectDir;
416
+ const branch = currentBranch(projectDir);
417
+ if (!isProtectedBranch(branch, projectDir)) return;
418
+
419
+ // Consent bypass: the user explicitly chose to work on this protected branch.
420
+ // Written only after they pick "Proceed on <branch>"; cleared on session start,
421
+ // so consent is asked once per session, not per write.
422
+ try {
423
+ const consent = fs.readFileSync(path.join(projectDir, ".egregore-branch-consent"), "utf-8").trim();
424
+ if (consent === branch) return;
425
+ } catch {}
426
+
427
+ const toolName = input.tool_name || "";
428
+ const toolInput = input.tool_input || {};
429
+ const memoryDir = realish(path.join(projectDir, "memory"));
430
+ const author = currentAuthor(projectDir);
431
+ const reason = [
432
+ `Protected branch (${branch}). Move project work to a task branch before writing.`,
433
+ `If the work topic is clear, run bin/agent.sh branch --topic "<work topic>" automatically (prefix dev/${author}/...), continue in the printed worktree, and briefly tell the user.`,
434
+ "Ask only when the work topic is genuinely ambiguous; ask for the topic, not for routine Git permission.",
435
+ `Only when the user explicitly requested writing on ${branch}, record consent with: echo '${branch}' > .egregore-branch-consent`,
436
+ "Memory, framework-runtime, and managed-repo writes are exempt and should not trigger this guard.",
437
+ ].join("\n");
438
+
439
+ if (toolName === "apply_patch") {
440
+ const changedPaths = extractApplyPatchPaths(toolInput.command || "");
441
+ if (changedPaths.length === 0 || changedPaths.some((p) => !isExemptPath(p, projectDir, memoryDir))) {
442
+ deny(reason);
443
+ }
444
+ return;
445
+ }
446
+
447
+ if (toolName === "Edit" || toolName === "Write") {
448
+ const target = toolInput.file_path || toolInput.path || "";
449
+ if (!target || !isExemptPath(target, projectDir, memoryDir)) deny(reason);
450
+ return;
451
+ }
452
+
453
+ if (toolName === "Bash") {
454
+ const command = String(toolInput.command || "");
455
+ if (isConsentCommand(command, branch)) return;
456
+ if (commandTargetsOtherRepo(command, projectDir, memoryDir)) return;
457
+ if (shellLooksMutating(command, projectDir, memoryDir)) deny(reason);
458
+ }
459
+ }
460
+
461
+ main();
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // search-hint.js — UserPromptSubmit hook (Codex runtime).
4
+ //
5
+ // Mirror of .claude/hooks/search-hint.sh: when the user's prompt is
6
+ // recall-shaped ("find the handoff about…", "did we decide…"), inject a
7
+ // routing hint toward `bash bin/search.sh query` — one ranked call over
8
+ // memory/ — instead of improvised ls/grep/graph exploration.
9
+ //
10
+ // Advisory only, never blocks, rate-limited once per session, exits 0.
11
+ // If the Codex runtime does not fire UserPromptSubmit, this file is inert.
12
+
13
+ const fs = require("node:fs");
14
+ const path = require("node:path");
15
+ const os = require("node:os");
16
+
17
+ function readStdin() {
18
+ try { return fs.readFileSync(0, "utf-8"); } catch { return ""; }
19
+ }
20
+ function parseJson(raw) {
21
+ try { return JSON.parse(raw || "{}"); } catch { return {}; }
22
+ }
23
+
24
+ const input = parseJson(readStdin());
25
+ const prompt = String(input.prompt || input.user_prompt || "");
26
+ if (!prompt) process.exit(0);
27
+
28
+ const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
29
+ if (!fs.existsSync(path.join(root, "bin", "search.sh"))) process.exit(0);
30
+
31
+ // Already oriented toward search.
32
+ if (/(\/search\b|search\.sh|\bsearch (the )?memory\b)/i.test(prompt)) process.exit(0);
33
+
34
+ const RECALL = new RegExp(
35
+ [
36
+ "\\b(find|locate|dig up|look up|pull up)\\b.*\\b(handoff|decision|notes?|docs?|meeting|harvest|quest|reflection)s?\\b",
37
+ "\\bdid we (decide|discuss|agree|say|talk about|cover)\\b",
38
+ "\\bdo we have (any )?(notes?|docs?|anything|something)( on| about)?\\b",
39
+ "\\bwhat did we (decide|say|agree|discuss)\\b",
40
+ "\\bhave we (discussed|decided|covered|talked about|looked at)\\b",
41
+ "\\bnotes on\\b",
42
+ "\\bhandoff (from|about)\\b",
43
+ "\\bwho worked on\\b",
44
+ "\\bdidn.?t (someone|we) already\\b",
45
+ "\\bwas there (a|any) (handoff|decision|discussion|meeting)\\b",
46
+ "\\b(our|company|org|organization|team)\\b.*\\b(pricing|price|tiers?|plans?|paid|free|strategy|policy|positioning|ownership|decision)\\b",
47
+ "\\b(pricing|price|tiers?|plans?|paid|free)\\b.*\\b(our|company|org|organization|team)\\b",
48
+ ].join("|"),
49
+ "i"
50
+ );
51
+ if (!RECALL.test(prompt)) process.exit(0);
52
+
53
+ // Rate-limit once per session.
54
+ let sid = "default";
55
+ try { sid = fs.readFileSync(path.join(root, ".egregore-session-id"), "utf-8").trim() || "default"; } catch {}
56
+ const flag = path.join(os.tmpdir(), `egregore-search-hint-${sid}`);
57
+ if (fs.existsSync(flag)) process.exit(0);
58
+ try { fs.writeFileSync(flag, "1"); } catch {}
59
+
60
+ process.stdout.write(JSON.stringify({
61
+ hookSpecificOutput: {
62
+ hookEventName: "UserPromptSubmit",
63
+ additionalContext:
64
+ "Routing hint: this prompt asks for recall from org memory. FIRST action: `bash bin/search.sh query \"<the concept, not the literal sentence>\"` — one ranked call over all of memory/ (hybrid keyword+semantic; graph-state annotations attach automatically in connected mode). Read the top hits and cite paths. Do NOT improvise ls/grep/graph exploration first — hand-written graph queries against invented labels (e.g. :Handoff — handoffs are :Session nodes) return empty. Full spec: the search skill.",
65
+ },
66
+ }) + "\n");
67
+ process.exit(0);