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
@@ -0,0 +1,312 @@
1
+ import { isAbsolute, relative, resolve } from "node:path";
2
+
3
+ const MUTATING_COMMANDS = /(^|[;&|]\s*)(?:sudo\s+)?(rm|mv|cp|mkdir|rmdir|touch|chmod|chown|install|tee)\b/;
4
+ // Global options (-C <path>, -c k=v, --git-dir=…) may sit between `git` and
5
+ // the subcommand. The repo convention actively teaches `git -C <abs-path>`,
6
+ // so a classifier that only matches bare `git commit` misses the shape the
7
+ // model is most likely to emit.
8
+ const MUTATING_GIT = /(^|[;&|]\s*)(?:sudo\s+)?git\s+(?:(?:-C|-c|--git-dir|--work-tree)(?:=\S+|\s+\S+)\s+)*(add|commit|push|merge|rebase|cherry-pick|tag|rm|mv|reset|restore|clean)\b/;
9
+ const MUTATING_PACKAGES = /(^|[;&|]\s*)(?:sudo\s+)?(npm|pnpm|yarn|bun)\s+(install|add|remove|uninstall|update)\b/;
10
+
11
+ // A `>` only acts as a shell redirect when it sits OUTSIDE quotes. Quoted
12
+ // `>` shows up constantly in read-only commands — Cypher comparisons
13
+ // (`WHERE x >= y`), JS/Python formatters (`f"{a} -> {b}"`), jq filters —
14
+ // so the redirect test runs on a quote-stripped copy of the command.
15
+ // Quoted segments collapse to a placeholder char so a redirect with a
16
+ // quoted TARGET (`echo hi > "/tmp/x"`) is still detected.
17
+ function stripQuotedSegments(command) {
18
+ let out = "";
19
+ let quote = null;
20
+ for (let i = 0; i < command.length; i++) {
21
+ const ch = command[i];
22
+ if (quote) {
23
+ if (quote === '"' && ch === "\\") i++; // skip escaped char inside double quotes
24
+ else if (ch === quote) quote = null;
25
+ continue;
26
+ }
27
+ if (ch === "'" || ch === '"') { quote = ch; out += "x"; continue; }
28
+ out += ch;
29
+ }
30
+ return out;
31
+ }
32
+
33
+ function maskQuotedOperators(command) {
34
+ let out = "";
35
+ let quote = null;
36
+ for (let i = 0; i < command.length; i++) {
37
+ const ch = command[i];
38
+ if (quote) {
39
+ if (quote === '"' && ch === "\\" && i + 1 < command.length) {
40
+ out += ch + command[++i];
41
+ continue;
42
+ }
43
+ if (ch === quote) quote = null;
44
+ out += /[><|;&]/.test(ch) ? " " : ch;
45
+ continue;
46
+ }
47
+ if (ch === "'" || ch === '"') quote = ch;
48
+ out += ch;
49
+ }
50
+ return out;
51
+ }
52
+
53
+ function shellSegments(command) {
54
+ const segments = [];
55
+ let current = "";
56
+ let quote = null;
57
+ for (let i = 0; i < command.length; i++) {
58
+ const ch = command[i];
59
+ if (quote) {
60
+ current += ch;
61
+ if (quote === '"' && ch === "\\" && i + 1 < command.length) current += command[++i];
62
+ else if (ch === quote) quote = null;
63
+ continue;
64
+ }
65
+ if (ch === "'" || ch === '"') {
66
+ quote = ch;
67
+ current += ch;
68
+ continue;
69
+ }
70
+ if (ch === ";" || ch === "|" || ch === "&" || ch === "\n") {
71
+ if (current.trim()) segments.push(current.trim());
72
+ current = "";
73
+ continue;
74
+ }
75
+ current += ch;
76
+ }
77
+ if (current.trim()) segments.push(current.trim());
78
+ return segments;
79
+ }
80
+
81
+ function shellWords(segment) {
82
+ const words = [];
83
+ let current = "";
84
+ let quote = null;
85
+ for (let i = 0; i < segment.length; i++) {
86
+ const ch = segment[i];
87
+ if (quote) {
88
+ if (quote === '"' && ch === "\\" && i + 1 < segment.length) current += segment[++i];
89
+ else if (ch === quote) quote = null;
90
+ else current += ch;
91
+ continue;
92
+ }
93
+ if (ch === "'" || ch === '"') {
94
+ quote = ch;
95
+ continue;
96
+ }
97
+ if (/\s/.test(ch)) {
98
+ if (current) words.push(current);
99
+ current = "";
100
+ continue;
101
+ }
102
+ if (ch === "\\" && i + 1 < segment.length) current += segment[++i];
103
+ else current += ch;
104
+ }
105
+ if (current) words.push(current);
106
+ return words;
107
+ }
108
+
109
+ export function isRuntimeStatePath(target, projectDir) {
110
+ if (!target || /[$`*?{}]/.test(target)) return false;
111
+ const absolute = isAbsolute(target) ? resolve(target) : resolve(projectDir, target);
112
+ const rel = relative(resolve(projectDir), absolute);
113
+ if (rel.startsWith("..") || isAbsolute(rel)) return true;
114
+ if (rel === ".claude/worktrees" || rel.startsWith(".claude/worktrees/")) return false;
115
+ return rel === "memory" || rel.startsWith("memory/") ||
116
+ rel === ".claude" || rel.startsWith(".claude/") ||
117
+ rel === ".codex" || rel.startsWith(".codex/") ||
118
+ rel === ".pi" || rel.startsWith(".pi/") ||
119
+ rel === ".prime" || rel.startsWith(".prime/") ||
120
+ rel === ".egregore-state.json" ||
121
+ rel === ".egregore-branch-consent" ||
122
+ rel === ".env";
123
+ }
124
+
125
+ export function mutatesOnlyRuntimeState(command, projectDir) {
126
+ if (!isMutatingBash(command)) return false;
127
+ const visible = stripQuotedSegments(String(command));
128
+ if (MUTATING_GIT.test(visible) || MUTATING_PACKAGES.test(visible) ||
129
+ /\bsed\s+[^\n]*\s-i(?:\s|$)/.test(visible)) return false;
130
+
131
+ let foundTarget = false;
132
+ const pathCommands = new Set(["rm", "mv", "cp", "mkdir", "rmdir", "touch", "chmod", "chown", "install", "tee"]);
133
+ for (const segment of shellSegments(String(command))) {
134
+ const words = shellWords(segment);
135
+ while (words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0])) words.shift();
136
+ while (words[0] === "sudo" || words[0] === "command" || words[0] === "env") words.shift();
137
+ if (!words.length) continue;
138
+ const name = words.shift().split("/").pop();
139
+ if (!pathCommands.has(name)) continue;
140
+
141
+ const args = words.filter((word) => !word.startsWith("-"));
142
+ let targets = args;
143
+ if (name === "cp" || name === "install") targets = args.length ? [args[args.length - 1]] : [];
144
+ if (name === "chmod" || name === "chown") targets = args.slice(1);
145
+ if (!targets.length || targets.some((target) => !isRuntimeStatePath(target, projectDir))) return false;
146
+ foundTarget = true;
147
+ }
148
+
149
+ const redirectSource = maskQuotedOperators(String(command));
150
+ const redirects = /(?:^|[\s;|&])(?:\d*)>>?\s*([^&\s;|]+)/g;
151
+ let redirect;
152
+ while ((redirect = redirects.exec(redirectSource))) {
153
+ const target = String(redirect[1] || "").replace(/^["']|["']$/g, "");
154
+ if (!target || target === "/dev/null") continue;
155
+ if (!isRuntimeStatePath(target, projectDir)) return false;
156
+ foundTarget = true;
157
+ }
158
+
159
+ const tees = /\|\s*tee\s+(?:-a\s+)?([^&\s;|]+)/g;
160
+ let tee;
161
+ while ((tee = tees.exec(redirectSource))) {
162
+ const target = String(tee[1] || "").replace(/^["']|["']$/g, "");
163
+ if (!isRuntimeStatePath(target, projectDir)) return false;
164
+ foundTarget = true;
165
+ }
166
+
167
+ return foundTarget;
168
+ }
169
+
170
+ // The documented escape hatch must itself be runnable, but ONLY as the whole
171
+ // command: an unanchored match lets `git push origin develop && echo 'develop'
172
+ // > .egregore-branch-consent` pass the gate in one call, and the block message
173
+ // teaches that exact echo string. Canonical form mirrors
174
+ // .codex/hooks/branch-guard.js isConsentCommand — anchored, branch escaped,
175
+ // optional ./ prefix — and is shared so the three runtimes cannot drift.
176
+ export function isConsentCommand(command, branch) {
177
+ if (!branch) return false;
178
+ const escaped = String(branch).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
179
+ return new RegExp(
180
+ `^\\s*echo\\s+['"]?${escaped}['"]?\\s*>\\s*(?:\\./)?\\.egregore-branch-consent\\s*$`
181
+ ).test(String(command || ""));
182
+ }
183
+
184
+ export function isMutatingBash(command) {
185
+ if (!command) return false;
186
+ const meaningfulRedirects = stripQuotedSegments(String(command))
187
+ .replace(/(?:^|\s)\d*>>?\s*\/dev\/null\b/g, " ")
188
+ .replace(/(?:^|\s)\d*>\s*&\d+\b/g, " ");
189
+ return MUTATING_COMMANDS.test(command) ||
190
+ MUTATING_GIT.test(command) ||
191
+ MUTATING_PACKAGES.test(command) ||
192
+ /(^|[^<])>>?\s*[^&]/.test(meaningfulRedirects) ||
193
+ /\bsed\s+[^\n]*\s-i(?:\s|$)/.test(command);
194
+ }
195
+
196
+ // ---- IPython code classification (Prime Agent) -----------------------------
197
+ // Prime Agent's default tool surface is a persistent IPython kernel: file
198
+ // writes and shell escapes arrive as Python code, not bash tool calls. This
199
+ // classifier is deliberately static and therefore porous (exec/eval and
200
+ // string-built commands pass); the generated runtime spec carries the
201
+ // standing-instruction discipline as the backstop, and the adversarial test
202
+ // fixtures pin what this actually catches.
203
+
204
+ // Strip Python string literals so call-name detection ignores docstrings and
205
+ // string content. Triple quotes first, then single-line quotes; f-string
206
+ // prefixes fall out naturally because only the quoted part is replaced.
207
+ function stripPythonStrings(code) {
208
+ return String(code)
209
+ .replace(/[rbuf]{0,2}("""|''')[\s\S]*?\1/gi, '""')
210
+ .replace(/[rbuf]{0,2}"(?:\\.|[^"\\\n])*"/gi, '""')
211
+ .replace(/[rbuf]{0,2}'(?:\\.|[^'\\\n])*'/gi, "''");
212
+ }
213
+
214
+ function pythonShellCommands(code) {
215
+ const shell = [];
216
+ const lines = String(code).split("\n");
217
+ const first = (lines[0] || "").trim();
218
+ if (/^%%(bash|sh|script\s+(ba)?sh)\b/.test(first)) {
219
+ shell.push(lines.slice(1).join("\n"));
220
+ return { shell, rest: "" };
221
+ }
222
+ if (/^%%writefile\b/.test(first)) {
223
+ const target = first.replace(/^%%writefile\s+(-a\s+)?/, "").trim();
224
+ return { shell: [`tee ${target || "unknown-target"}`], rest: "" };
225
+ }
226
+ const rest = [];
227
+ for (const line of lines) {
228
+ const bang = line.match(/^\s*!(.+)$/);
229
+ const sysMagic = line.match(/^\s*%(?:system|sx)\s+(.+)$/);
230
+ if (bang) shell.push(bang[1]);
231
+ else if (sysMagic) shell.push(sysMagic[1]);
232
+ else rest.push(line);
233
+ }
234
+ return { shell, rest: rest.join("\n") };
235
+ }
236
+
237
+ const PY_MUTATING_CALLS =
238
+ /\b(?:os\.(?:remove|unlink|rename|replace|rmdir|removedirs|makedirs|mkdir|symlink|link|chmod|chown|truncate)|shutil\.(?:copy|copy2|copyfile|copytree|move|rmtree|make_archive|unpack_archive))\s*\(/;
239
+ const PY_WRITE_METHODS = /\.\s*(?:write_text|write_bytes|unlink|touch|mkdir|rmdir|hardlink_to|symlink_to)\s*\(/;
240
+ const PY_OPEN_WRITE =
241
+ /\bopen\s*\(\s*[^,]+,\s*(?:mode\s*=\s*)?['"](?:[wax]|r\+)[rwxab+]*['"]/;
242
+ const PY_OPEN_MODE_KWARG = /\bopen\s*\([^)]*mode\s*=\s*['"](?:[wax]|r\+)[rwxab+]*['"]/;
243
+
244
+ function pythonSubprocessArgv(code) {
245
+ const argvs = [];
246
+ const calls =
247
+ /\b(?:subprocess\.(?:run|call|check_call|check_output|Popen)|os\.system|os\.popen)\s*\(\s*(\[[^\]]*\]|[rbuf]{0,2}["'](?:\\.|[^"'\\\n])*["'])/g;
248
+ let match;
249
+ while ((match = calls.exec(String(code)))) {
250
+ const arg = match[1];
251
+ if (arg.startsWith("[")) {
252
+ const words = [...arg.matchAll(/["']((?:\\.|[^"'\\])*)["']/g)].map((m) => m[1]);
253
+ if (words.length) argvs.push(words.join(" "));
254
+ } else {
255
+ argvs.push(arg.replace(/^[rbuf]{0,2}["']/i, "").replace(/["']$/, ""));
256
+ }
257
+ }
258
+ return argvs;
259
+ }
260
+
261
+ // Classify IPython cell code. Returns true when the code statically performs a
262
+ // repo mutation that should meet the protected-branch gate. Mutations that
263
+ // target only Egregore runtime state (memory/, dot-dirs, consent files) are
264
+ // exempt when the target is statically extractable, mirroring
265
+ // mutatesOnlyRuntimeState for bash.
266
+ export function isMutatingIpython(code, projectDir) {
267
+ if (!code) return false;
268
+ const { shell, rest } = pythonShellCommands(code);
269
+ for (const cmd of shell) {
270
+ if (isMutatingBash(cmd) && !(projectDir && mutatesOnlyRuntimeState(cmd, projectDir))) return true;
271
+ }
272
+ if (!rest) return false;
273
+
274
+ for (const argv of pythonSubprocessArgv(rest)) {
275
+ if (isMutatingBash(argv) && !(projectDir && mutatesOnlyRuntimeState(argv, projectDir))) return true;
276
+ }
277
+
278
+ const stripped = stripPythonStrings(rest);
279
+ if (PY_MUTATING_CALLS.test(stripped) || PY_WRITE_METHODS.test(stripped)) {
280
+ // Exempt when every extractable string target is runtime state. The
281
+ // mutated location differs by call: copy/move/rename mutate their LAST
282
+ // argument (the destination); remove/rmtree/mkdir mutate their first.
283
+ if (projectDir) {
284
+ const DEST_LAST = /^(?:copy|copy2|copyfile|copytree|move|rename|replace|symlink|link)$/;
285
+ const targets = [...String(rest).matchAll(
286
+ /\b(?:os|shutil)\.(\w+)\s*\(([^)]*)\)/g
287
+ )].flatMap((m) => {
288
+ const args = [...m[2].matchAll(/["']((?:\\.|[^"'\\])*)["']/g)].map((s) => s[1]);
289
+ if (!args.length) return [];
290
+ return [DEST_LAST.test(m[1]) ? args[args.length - 1] : args[0]];
291
+ });
292
+ const pathTargets = [...String(rest).matchAll(
293
+ /Path\s*\(\s*[rbuf]{0,2}["']((?:\\.|[^"'\\\n])*)["']\s*\)\s*\.\s*(?:write_text|write_bytes|unlink|touch|mkdir|rmdir)/g
294
+ )].map((m) => m[1]);
295
+ const all = [...targets, ...pathTargets];
296
+ if (all.length && all.every((t) => isRuntimeStatePath(t, projectDir))) return false;
297
+ }
298
+ return true;
299
+ }
300
+
301
+ if (PY_OPEN_WRITE.test(rest) || PY_OPEN_MODE_KWARG.test(rest)) {
302
+ if (projectDir) {
303
+ const openTargets = [...String(rest).matchAll(
304
+ /\bopen\s*\(\s*[rbuf]{0,2}["']((?:\\.|[^"'\\\n])*)["']\s*,\s*(?:mode\s*=\s*)?["'](?:[wax]|r\+)/g
305
+ )].map((m) => m[1]);
306
+ if (openTargets.length && openTargets.every((t) => isRuntimeStatePath(t, projectDir))) return false;
307
+ }
308
+ return true;
309
+ }
310
+
311
+ return false;
312
+ }