create-egregore 0.19.8 → 0.19.11

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 (91) hide show
  1. package/lib/local.js +11 -1
  2. package/lib/setup.js +33 -0
  3. package/package.json +1 -1
  4. package/runtime/codex/.codex/hooks/branch-guard.js +242 -34
  5. package/runtime/codex/.codex/skills/activity/SKILL.md +18 -6
  6. package/runtime/codex/.codex/skills/deep-reflect/SKILL.md +90 -55
  7. package/runtime/codex/.codex/skills/deploy-bridge/SKILL.md +25 -0
  8. package/runtime/codex/.codex/skills/handoff/SKILL.md +6 -2
  9. package/runtime/codex/.codex/skills/save/SKILL.md +21 -3
  10. package/runtime/codex/.codex/skills/scroll/SKILL.md +54 -3
  11. package/runtime/codex/.codex/skills/task/SKILL.md +25 -0
  12. package/runtime/codex/.codex/skills/tui-design/SKILL.md +1 -1
  13. package/runtime/codex/.codex/skills/wrap/SKILL.md +4 -1
  14. package/runtime/codex/.codex/spec-manifest.json +15 -3
  15. package/runtime/codex/AGENTS.md +45 -9
  16. package/runtime/codex/bin/activity-data.sh +1 -1
  17. package/runtime/codex/bin/agent.sh +66 -23
  18. package/runtime/codex/bin/attendant.sh +525 -0
  19. package/runtime/codex/bin/capture-reconcile.sh +67 -0
  20. package/runtime/codex/bin/capture-run.sh +341 -0
  21. package/runtime/codex/bin/codex-render-spec.mjs +15 -4
  22. package/runtime/codex/bin/codex-session-start.sh +45 -3
  23. package/runtime/codex/bin/codex-skill-render.mjs +23 -4
  24. package/runtime/codex/bin/graph-op.sh +719 -49
  25. package/runtime/codex/bin/graph-wal.sh +3 -2
  26. package/runtime/codex/bin/handoff-run.sh +72 -0
  27. package/runtime/codex/bin/handoff-save-egregore.sh +51 -11
  28. package/runtime/codex/bin/index-handoff.sh +26 -17
  29. package/runtime/codex/bin/ingest-graph.sh +80 -0
  30. package/runtime/codex/bin/ingest-surface.html +429 -0
  31. package/runtime/codex/bin/ingest.py +614 -0
  32. package/runtime/codex/bin/ingest.sh +231 -0
  33. package/runtime/codex/bin/ingest_graph.py +776 -0
  34. package/runtime/codex/bin/ingest_surface.py +464 -0
  35. package/runtime/codex/bin/lib/git-sync.sh +2 -6
  36. package/runtime/codex/bin/lib/greeting.sh +157 -22
  37. package/runtime/codex/bin/lib/identity.sh +50 -29
  38. package/runtime/codex/bin/lib/noncode.sh +31 -0
  39. package/runtime/codex/bin/person.py +610 -0
  40. package/runtime/codex/bin/person.sh +448 -0
  41. package/runtime/codex/bin/session-autosave.sh +184 -0
  42. package/runtime/codex/bin/session-log.sh +199 -0
  43. package/runtime/codex/bin/settings.sh +1 -1
  44. package/runtime/codex/manifest.json +19 -1
  45. package/runtime/pi/.codex/hooks/branch-guard.js +242 -34
  46. package/runtime/pi/.codex/skills/activity/SKILL.md +18 -6
  47. package/runtime/pi/.codex/skills/deep-reflect/SKILL.md +90 -55
  48. package/runtime/pi/.codex/skills/deploy-bridge/SKILL.md +25 -0
  49. package/runtime/pi/.codex/skills/handoff/SKILL.md +6 -2
  50. package/runtime/pi/.codex/skills/save/SKILL.md +21 -3
  51. package/runtime/pi/.codex/skills/scroll/SKILL.md +54 -3
  52. package/runtime/pi/.codex/skills/task/SKILL.md +25 -0
  53. package/runtime/pi/.codex/skills/tui-design/SKILL.md +1 -1
  54. package/runtime/pi/.codex/skills/wrap/SKILL.md +4 -1
  55. package/runtime/pi/.codex/spec-manifest.json +15 -3
  56. package/runtime/pi/.pi/APPEND_SYSTEM.md +41 -5
  57. package/runtime/pi/.pi/extensions/egregore.ts +98 -8
  58. package/runtime/pi/.pi/spec-manifest.json +2 -2
  59. package/runtime/pi/AGENTS.md +45 -9
  60. package/runtime/pi/bin/activity-data.sh +1 -1
  61. package/runtime/pi/bin/agent.sh +66 -23
  62. package/runtime/pi/bin/attendant.sh +525 -0
  63. package/runtime/pi/bin/capture-reconcile.sh +67 -0
  64. package/runtime/pi/bin/capture-run.sh +341 -0
  65. package/runtime/pi/bin/codex-render-spec.mjs +15 -4
  66. package/runtime/pi/bin/codex-session-start.sh +45 -3
  67. package/runtime/pi/bin/codex-skill-render.mjs +23 -4
  68. package/runtime/pi/bin/graph-op.sh +719 -49
  69. package/runtime/pi/bin/graph-wal.sh +3 -2
  70. package/runtime/pi/bin/handoff-run.sh +72 -0
  71. package/runtime/pi/bin/handoff-save-egregore.sh +51 -11
  72. package/runtime/pi/bin/index-handoff.sh +26 -17
  73. package/runtime/pi/bin/ingest-graph.sh +80 -0
  74. package/runtime/pi/bin/ingest-surface.html +429 -0
  75. package/runtime/pi/bin/ingest.py +614 -0
  76. package/runtime/pi/bin/ingest.sh +231 -0
  77. package/runtime/pi/bin/ingest_graph.py +776 -0
  78. package/runtime/pi/bin/ingest_surface.py +464 -0
  79. package/runtime/pi/bin/lib/git-sync.sh +2 -6
  80. package/runtime/pi/bin/lib/greeting.sh +157 -22
  81. package/runtime/pi/bin/lib/identity.sh +50 -29
  82. package/runtime/pi/bin/lib/noncode.sh +31 -0
  83. package/runtime/pi/bin/person.py +610 -0
  84. package/runtime/pi/bin/person.sh +448 -0
  85. package/runtime/pi/bin/pi-branch-policy.mjs +164 -4
  86. package/runtime/pi/bin/pi-product-workflows.mjs +31 -2
  87. package/runtime/pi/bin/publish-artifact.sh +14 -0
  88. package/runtime/pi/bin/session-autosave.sh +184 -0
  89. package/runtime/pi/bin/session-log.sh +199 -0
  90. package/runtime/pi/bin/settings.sh +274 -0
  91. package/runtime/pi/manifest.json +20 -1
package/lib/local.js CHANGED
@@ -47,7 +47,12 @@ const TEMPLATE_REPO = "egregore";
47
47
  async function getUser(token) {
48
48
  const { status, data } = await ghApi("GET", "/user", token);
49
49
  if (status !== 200) throw new Error("GitHub auth failed — could not fetch user");
50
- return { login: data.login, name: data.name || data.login, email: data.email || null };
50
+ return {
51
+ login: data.login,
52
+ id: data.id || null,
53
+ name: data.name || data.login,
54
+ email: data.email || null,
55
+ };
51
56
  }
52
57
 
53
58
  async function listOrgs(token) {
@@ -1286,11 +1291,14 @@ async function localFounderFlow(ui, ctx = {}) {
1286
1291
  const slugifiedName = (displayName || user.login).toLowerCase().replace(/\s+/g, "-");
1287
1292
  const state = {
1288
1293
  github_username: user.login,
1294
+ github_id: user.id,
1295
+ person_id: user.id ? `github:${user.id}` : `github-login:${user.login.toLowerCase()}`,
1289
1296
  github_name: user.name,
1290
1297
  display_name: displayName || user.login,
1291
1298
  name: slugifiedName,
1292
1299
  member_role: memberRole,
1293
1300
  email: user.email,
1301
+ emails: user.email ? [user.email.toLowerCase()] : [],
1294
1302
  onboarding_complete: false,
1295
1303
  usage_type: "founder_group",
1296
1304
  org_setup: true,
@@ -1430,6 +1438,7 @@ async function localFounderFlow(ui, ctx = {}) {
1430
1438
  const invPersonContent = [
1431
1439
  "---",
1432
1440
  `name: ${invUsername}`,
1441
+ `person_id: github-login:${invUsername.toLowerCase()}`,
1433
1442
  `github: ${invUsername}`,
1434
1443
  `invited_by: ${user.login}`,
1435
1444
  `joined: ${today}`,
@@ -1755,6 +1764,7 @@ async function localJoinFlow(orgArg, ui, ctx = {}) {
1755
1764
  org_name: orgName,
1756
1765
  github_org: orgLogin,
1757
1766
  github_username: user.login,
1767
+ github_id: user.id,
1758
1768
  github_name: user.name,
1759
1769
  github_email: user.email,
1760
1770
  slug: config.slug || orgLogin,
package/lib/setup.js CHANGED
@@ -174,12 +174,24 @@ function mergeProfileFields(existing, additions = []) {
174
174
  function personProfileContent(state = {}, opts = {}) {
175
175
  const now = opts.onboardedAt || new Date().toISOString();
176
176
  const username = state.github_username || opts.githubUsername || "unknown";
177
+ const githubId = state.github_id || opts.githubId || "";
178
+ const personId = state.person_id || (githubId ? `github:${githubId}` : `github-login:${username.toLowerCase()}`);
177
179
  const displayName = state.display_name || state.github_name || username;
178
180
  const role = state.member_role || state.onboarding?.member_role || opts.memberRole || "Member";
181
+ const emails = [...new Set(
182
+ [state.email, ...(Array.isArray(state.emails) ? state.emails : [])]
183
+ .filter(Boolean)
184
+ .map((value) => String(value).trim().toLowerCase()),
185
+ )];
179
186
  return [
180
187
  `# ${displayName}`,
181
188
  "",
189
+ `Person-ID: ${personId}`,
182
190
  `GitHub: ${username}`,
191
+ `GitHub-ID: ${githubId}`,
192
+ "GitHub-Aliases: ",
193
+ `Email: ${emails[0] || ""}`,
194
+ `Emails: ${emails.join(", ")}`,
183
195
  `Role: ${role}`,
184
196
  `Joined: ${now.split("T")[0]}`,
185
197
  `Onboarded: ${now}`,
@@ -234,6 +246,7 @@ function buildOnboardingState(existing = {}, data = {}, opts = {}) {
234
246
  const state = { ...existing };
235
247
  const preferredRuntime = normalizeRuntime(opts.runtime);
236
248
  const githubUsername = data.github_username || state.github_username || "";
249
+ const githubId = data.github_id || state.github_id || null;
237
250
  const githubName = data.github_name || state.github_name || githubUsername;
238
251
  const displayName = data.display_name || state.display_name || githubName || githubUsername;
239
252
  const usageType = normalizeUsageType(data);
@@ -248,6 +261,12 @@ function buildOnboardingState(existing = {}, data = {}, opts = {}) {
248
261
  const shouldComplete = alreadyComplete || (requestedComplete && !!memberRole);
249
262
 
250
263
  if (githubUsername) state.github_username = githubUsername;
264
+ if (githubId) {
265
+ state.github_id = githubId;
266
+ state.person_id = `github:${githubId}`;
267
+ } else if (githubUsername && !state.person_id) {
268
+ state.person_id = `github-login:${githubUsername.toLowerCase()}`;
269
+ }
251
270
  if (githubName) state.github_name = githubName;
252
271
  if (displayName) {
253
272
  state.display_name = displayName;
@@ -457,6 +476,20 @@ async function install(data, ui, targetDir, opts = {}) {
457
476
  reportPiRuntimeSummary(piRuntime, ui);
458
477
  }
459
478
 
479
+ // Reconcile the installer-captured identity through the same portable writer
480
+ // used by /me and onboarding. Failure is recoverable: the state + person
481
+ // profile already exist and the next session retries `bin/person.sh sync`.
482
+ const personScript = path.join(egregoreDir, "bin", "person.sh");
483
+ if (fs.existsSync(personScript)) {
484
+ try {
485
+ spawnSync("bash", [personScript, "onboard"], {
486
+ cwd: egregoreDir,
487
+ stdio: "ignore",
488
+ timeout: 15000,
489
+ });
490
+ } catch {}
491
+ }
492
+
460
493
  // 5. Register instance + install launcher binary
461
494
  ui.step(5, totalSteps, "Registering instance...");
462
495
  registerInstance(forkDirName, org_name, egregoreDir, preferredRuntime);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-egregore",
3
- "version": "0.19.8",
3
+ "version": "0.19.11",
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": {
@@ -48,22 +48,86 @@ function inside(child, parent) {
48
48
  return rel === "" || (!!rel && !rel.startsWith("..") && !path.isAbsolute(rel));
49
49
  }
50
50
 
51
- function findProjectDir(input) {
52
- // Tool payload cwd is the operation location. Codex launchers may keep
53
- // EGREGORE_CODEX_PROJECT_DIR pointed at the main checkout even after the
54
- // model continues in a task worktree, so using the env var first can make
55
- // legitimate worktree writes look like writes on protected develop.
56
- const cwd = input.cwd || process.cwd() || process.env.EGREGORE_CODEX_PROJECT_DIR;
57
- const top = run("git", ["-C", cwd, "rev-parse", "--show-toplevel"], cwd);
58
- if (top) return realish(top);
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;
59
94
 
60
- const envDir = process.env.EGREGORE_CODEX_PROJECT_DIR || "";
61
- if (envDir) {
62
- const envTop = run("git", ["-C", envDir, "rev-parse", "--show-toplevel"], envDir);
63
- return realish(envTop || envDir);
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 };
64
124
  }
65
125
 
66
- return realish(cwd);
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 };
67
131
  }
68
132
 
69
133
  function currentBranch(projectDir) {
@@ -95,7 +159,7 @@ function isExemptPath(rawPath, projectDir, memoryDir) {
95
159
  if (inside(resolved, target)) return true;
96
160
  }
97
161
 
98
- for (const file of [".egregore-state.json", ".env"]) {
162
+ for (const file of [".egregore-state.json", ".egregore-branch-consent", ".env"]) {
99
163
  if (resolved === realish(path.join(projectDir, file))) return true;
100
164
  }
101
165
 
@@ -133,12 +197,13 @@ function commandTargetsOtherRepo(command, projectDir, memoryDir) {
133
197
  }
134
198
 
135
199
  function isBranchSetupCommand(command) {
200
+ if (shellSegments(command).length !== 1) return false;
136
201
  return (
137
- /\bbin\/agent\.sh\s+branch\b/.test(command) ||
138
- /\bgit\s+(checkout|switch)\s+(-b|-c)\b/.test(command) ||
139
- /\bgit\s+branch\s+(dev|feature|bugfix)\//.test(command) ||
140
- /\bgit\s+worktree\s+add\b/.test(command) ||
141
- /\bbin\/worktree\.sh\s+setup\b/.test(command)
202
+ /^\s*bin\/agent\.sh\s+branch\b/.test(command) ||
203
+ /^\s*git\s+(checkout|switch)\s+(-b|-c)\b/.test(command) ||
204
+ /^\s*git\s+branch\s+(dev|feature|bugfix)\//.test(command) ||
205
+ /^\s*git\s+worktree\s+add\b/.test(command) ||
206
+ /^\s*bin\/worktree\.sh\s+setup\b/.test(command)
142
207
  );
143
208
  }
144
209
 
@@ -149,7 +214,134 @@ function stagedOnlyFrameworkPaths(projectDir) {
149
214
  return files.length > 0 && files.every((file) => /^(bin\/|\.claude\/|\.codex\/|CLAUDE\.md$|AGENTS\.md$|skills\/)/.test(file));
150
215
  }
151
216
 
217
+ function stripQuotedSegments(command) {
218
+ let out = "";
219
+ let quote = null;
220
+ for (let i = 0; i < String(command || "").length; i++) {
221
+ const ch = command[i];
222
+ if (quote) {
223
+ if (quote === '"' && ch === "\\") i++;
224
+ else if (ch === quote) quote = null;
225
+ continue;
226
+ }
227
+ if (ch === "'" || ch === '"') {
228
+ quote = ch;
229
+ out += "x";
230
+ continue;
231
+ }
232
+ out += ch;
233
+ }
234
+ return out;
235
+ }
236
+
237
+ function maskQuotedOperators(command) {
238
+ let out = "";
239
+ let quote = null;
240
+ for (let i = 0; i < String(command || "").length; i++) {
241
+ const ch = command[i];
242
+ if (quote) {
243
+ if (quote === '"' && ch === "\\") {
244
+ out += ch;
245
+ if (i + 1 < command.length) out += command[++i];
246
+ continue;
247
+ }
248
+ if (ch === quote) quote = null;
249
+ out += /[><|;&]/.test(ch) ? " " : ch;
250
+ continue;
251
+ }
252
+ if (ch === "'" || ch === '"') quote = ch;
253
+ out += ch;
254
+ }
255
+ return out;
256
+ }
257
+
258
+ function shellSegments(command) {
259
+ const segments = [];
260
+ let current = "";
261
+ let quote = null;
262
+ for (let i = 0; i < String(command || "").length; i++) {
263
+ const ch = command[i];
264
+ if (quote) {
265
+ current += ch;
266
+ if (quote === '"' && ch === "\\" && i + 1 < command.length) current += command[++i];
267
+ else if (ch === quote) quote = null;
268
+ continue;
269
+ }
270
+ if (ch === "'" || ch === '"') {
271
+ quote = ch;
272
+ current += ch;
273
+ continue;
274
+ }
275
+ if (ch === ";" || ch === "|" || ch === "&" || ch === "\n") {
276
+ if (current.trim()) segments.push(current.trim());
277
+ current = "";
278
+ continue;
279
+ }
280
+ current += ch;
281
+ }
282
+ if (current.trim()) segments.push(current.trim());
283
+ return segments;
284
+ }
285
+
286
+ function shellWords(segment) {
287
+ const words = [];
288
+ let current = "";
289
+ let quote = null;
290
+ for (let i = 0; i < String(segment || "").length; i++) {
291
+ const ch = segment[i];
292
+ if (quote) {
293
+ if (quote === '"' && ch === "\\" && i + 1 < segment.length) current += segment[++i];
294
+ else if (ch === quote) quote = null;
295
+ else current += ch;
296
+ continue;
297
+ }
298
+ if (ch === "'" || ch === '"') {
299
+ quote = ch;
300
+ continue;
301
+ }
302
+ if (/\s/.test(ch)) {
303
+ if (current) words.push(current);
304
+ current = "";
305
+ continue;
306
+ }
307
+ if (ch === "\\") {
308
+ if (i + 1 < segment.length) current += segment[++i];
309
+ continue;
310
+ }
311
+ current += ch;
312
+ }
313
+ if (current) words.push(current);
314
+ return words;
315
+ }
316
+
317
+ function filesystemMutationStatus(command, projectDir, memoryDir) {
318
+ const commands = new Set(["rm", "mv", "cp", "mkdir", "rmdir", "touch", "chmod", "chown", "ln", "tee"]);
319
+ let unsafe = false;
320
+
321
+ for (const segment of shellSegments(command)) {
322
+ const words = shellWords(segment);
323
+ while (words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0])) words.shift();
324
+ while (words[0] === "sudo" || words[0] === "command" || words[0] === "env") words.shift();
325
+ if (!words.length) continue;
326
+
327
+ const name = path.basename(words.shift());
328
+ if (!commands.has(name)) continue;
329
+
330
+ const args = words.filter((word) => !word.startsWith("-"));
331
+ let targets = args;
332
+ if (name === "cp" || name === "ln") targets = args.length ? [args[args.length - 1]] : [];
333
+ if (name === "chmod" || name === "chown") targets = args.slice(1);
334
+
335
+ if (!targets.length || targets.some((target) =>
336
+ /[$`*?{}]/.test(target) || !isExemptPath(target, projectDir, memoryDir)
337
+ )) unsafe = true;
338
+ }
339
+
340
+ return { unsafe };
341
+ }
342
+
152
343
  function redirectLooksMutating(command, projectDir, memoryDir) {
344
+ const visible = maskQuotedOperators(command);
153
345
  const redirectPatterns = [
154
346
  /(?:^|[\s;|&])(?:\d*)>>?\s*([^&\s;|]+)/g,
155
347
  /\|\s*tee\s+(?:-a\s+)?([^&\s;|]+)/g,
@@ -157,7 +349,7 @@ function redirectLooksMutating(command, projectDir, memoryDir) {
157
349
 
158
350
  for (const pattern of redirectPatterns) {
159
351
  let match;
160
- while ((match = pattern.exec(command))) {
352
+ while ((match = pattern.exec(visible))) {
161
353
  const target = String(match[1] || "").replace(/^["']|["']$/g, "");
162
354
  if (!target || target === "/dev/null" || target.startsWith("$")) continue;
163
355
  if (!isExemptPath(target, projectDir, memoryDir)) return true;
@@ -168,18 +360,31 @@ function redirectLooksMutating(command, projectDir, memoryDir) {
168
360
 
169
361
  function shellLooksMutating(command, projectDir, memoryDir) {
170
362
  if (!command.trim()) return false;
171
- if (isBranchSetupCommand(command)) return false;
363
+ const visible = stripQuotedSegments(command);
364
+ if (isBranchSetupCommand(visible)) return false;
172
365
 
173
366
  const mutatingPatterns = [
174
- /\bgit\s+(add|commit|push|rm|mv|reset|restore|checkout\s+--|clean|rebase|merge|cherry-pick)\b/,
175
- /\b(apply_patch|touch|mkdir|rm|mv|cp|chmod|chown|ln)\b/,
176
- /\b(sed|perl)\b[^;&|]*\s-i(\s|$)/,
177
- /\b(npm|pnpm|yarn|bun)\s+(install|add|remove|update|dedupe|ci)\b/,
178
- /\b(go\s+mod\s+tidy|cargo\s+(fmt|fix|update)|rustfmt|prettier\s+--write|eslint\s+--fix)\b/,
367
+ /(?:^|[;&|(\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/,
368
+ /(?:^|[;&|(\n]\s*)(?:sudo\s+)?apply_patch\b/,
369
+ /(?:^|[;&|(\n]\s*)(?:sudo\s+)?(sed|perl)\b[^;&|]*\s-i(\s|$)/,
370
+ /(?:^|[;&|(\n]\s*)(?:sudo\s+)?(npm|pnpm|yarn|bun)\s+(install|add|remove|update|dedupe|ci)\b/,
371
+ /(?:^|[;&|(\n]\s*)(?:sudo\s+)?(go\s+mod\s+tidy|cargo\s+(fmt|fix|update)|rustfmt|prettier\s+--write|eslint\s+--fix)\b/,
179
372
  ];
180
373
 
181
- if (/\bgit\s+commit\b/.test(command) && stagedOnlyFrameworkPaths(projectDir)) return false;
182
- return mutatingPatterns.some((pattern) => pattern.test(command)) || redirectLooksMutating(command, projectDir, memoryDir);
374
+ const gitCommit = /(?:^|[;&|(\n]\s*)(?:sudo\s+)?git(?:\s+-C\s+\S+)?\s+commit\b/.test(visible);
375
+ if (gitCommit && stagedOnlyFrameworkPaths(projectDir)) return false;
376
+
377
+ const filesystem = filesystemMutationStatus(command, projectDir, memoryDir);
378
+ return mutatingPatterns.some((pattern) => pattern.test(visible)) ||
379
+ filesystem.unsafe ||
380
+ redirectLooksMutating(command, projectDir, memoryDir);
381
+ }
382
+
383
+ function isConsentCommand(command, branch) {
384
+ const escaped = branch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
385
+ return new RegExp(
386
+ `^\\s*echo\\s+['"]?${escaped}['"]?\\s*>\\s*(?:\\./)?\\.egregore-branch-consent\\s*$`
387
+ ).test(String(command || ""));
183
388
  }
184
389
 
185
390
  function deny(reason) {
@@ -194,7 +399,9 @@ function deny(reason) {
194
399
 
195
400
  function main() {
196
401
  const input = parseJson(readStdin());
197
- const projectDir = findProjectDir(input);
402
+ const context = findProjectContext(input);
403
+ if (context.external) return;
404
+ const projectDir = context.projectDir;
198
405
  const branch = currentBranch(projectDir);
199
406
  if (!isProtectedBranch(branch)) return;
200
407
 
@@ -211,11 +418,11 @@ function main() {
211
418
  const memoryDir = realish(path.join(projectDir, "memory"));
212
419
  const author = currentAuthor(projectDir);
213
420
  const reason = [
214
- `Protected branch (${branch}). Ask the user how to proceed — do NOT auto-branch silently.`,
215
- "Offer a choice (numbered or structured): Branch off (default) / Proceed on this branch / Cancel.",
216
- ` • Branch off: bin/agent.sh branch --topic "<work topic>" (prefix dev/${author}/...)`,
217
- ` • Proceed on ${branch}: echo '${branch}' > .egregore-branch-consent then retry`,
218
- "Proceeding records consent for this branch for the rest of the session (cleared on next session start).",
421
+ `Protected branch (${branch}). Move project work to a task branch before writing.`,
422
+ `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.`,
423
+ "Ask only when the work topic is genuinely ambiguous; ask for the topic, not for routine Git permission.",
424
+ `Only when the user explicitly requested writing on ${branch}, record consent with: echo '${branch}' > .egregore-branch-consent`,
425
+ "Memory, framework-runtime, and managed-repo writes are exempt and should not trigger this guard.",
219
426
  ].join("\n");
220
427
 
221
428
  if (toolName === "apply_patch") {
@@ -234,6 +441,7 @@ function main() {
234
441
 
235
442
  if (toolName === "Bash") {
236
443
  const command = String(toolInput.command || "");
444
+ if (isConsentCommand(command, branch)) return;
237
445
  if (commandTargetsOtherRepo(command, projectDir, memoryDir)) return;
238
446
  if (shellLooksMutating(command, projectDir, memoryDir)) deny(reason);
239
447
  }
@@ -15,15 +15,27 @@ remain enough to render a useful answer.
15
15
 
16
16
  1. Verify `bin/agent.sh` exists. If it does not, say this is not an Egregore
17
17
  checkout.
18
- 2. Run `bin/agent.sh sync`.
19
- 3. Resolve the requested handle:
18
+ 2. If the request is `activity done N`, `activity expire N`, or
19
+ `activity reopen N`, fetch `bin/activity-data.sh` to a temporary JSON file,
20
+ map `N` to the corresponding `handoffs_to_me[N-1].sessionId`, and run:
21
+
22
+ ```bash
23
+ bash bin/activity-action.sh <done|expire|reopen> "<session-id>"
24
+ ```
25
+
26
+ Report the resulting topic and state. Never accept a raw session ID from the
27
+ visible command when a numbered activity item is available; the number keeps
28
+ the mutation tied to what the user just saw. In local mode, preserve the
29
+ script's explicit unavailable result.
30
+ 3. Run `bin/agent.sh sync`.
31
+ 4. Resolve the requested handle:
20
32
  - If the user named a person, match it against `memory/people/*.md`.
21
33
  - Otherwise use `.egregore-state.json` `github_username`, then git user
22
34
  fallback.
23
- 4. Run `bash bin/activity-data.sh [handle]` with network escalation and capture
35
+ 5. Run `bash bin/activity-data.sh [handle]` with network escalation and capture
24
36
  stdout as JSON. This command performs the graph API call internally; a
25
37
  normal sandboxed Codex shell can make a healthy graph look unreachable.
26
- 5. Classify the graph status:
38
+ 6. Classify the graph status:
27
39
 
28
40
  ```bash
29
41
  node bin/codex-skill-render.mjs classify-graph --mode connected
@@ -37,7 +49,7 @@ say Codex graph network access was not granted; do not claim the graph itself
37
49
  was unreachable. Do not retry for `missing_config`, `auth_error`, or
38
50
  `server_error`; render the clear reason instead.
39
51
 
40
- 6. Render the card with:
52
+ 7. Render the card with:
41
53
 
42
54
  ```bash
43
55
  node bin/codex-skill-render.mjs activity-card <json-file>
@@ -47,7 +59,7 @@ Paste the rendered card exactly in the visible response, preferably in a
47
59
  `text` fenced block. Do not paraphrase the card into bullets unless the user
48
60
  asks for a summary.
49
61
 
50
- 7. Offer one compact focus prompt:
62
+ 8. Offer one compact focus prompt:
51
63
  - `Handoffs` - open pending handoffs.
52
64
  - `Questions` - answer or inspect pending questions.
53
65
  - `Recent work` - inspect sessions and wraps.