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.
- package/lib/local.js +11 -1
- package/lib/setup.js +33 -0
- package/package.json +1 -1
- package/runtime/codex/.codex/hooks/branch-guard.js +242 -34
- package/runtime/codex/.codex/skills/activity/SKILL.md +18 -6
- package/runtime/codex/.codex/skills/deep-reflect/SKILL.md +90 -55
- package/runtime/codex/.codex/skills/deploy-bridge/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/handoff/SKILL.md +6 -2
- package/runtime/codex/.codex/skills/save/SKILL.md +21 -3
- package/runtime/codex/.codex/skills/scroll/SKILL.md +54 -3
- package/runtime/codex/.codex/skills/task/SKILL.md +25 -0
- package/runtime/codex/.codex/skills/tui-design/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/wrap/SKILL.md +4 -1
- package/runtime/codex/.codex/spec-manifest.json +15 -3
- package/runtime/codex/AGENTS.md +45 -9
- package/runtime/codex/bin/activity-data.sh +1 -1
- package/runtime/codex/bin/agent.sh +66 -23
- package/runtime/codex/bin/attendant.sh +525 -0
- package/runtime/codex/bin/capture-reconcile.sh +67 -0
- package/runtime/codex/bin/capture-run.sh +341 -0
- package/runtime/codex/bin/codex-render-spec.mjs +15 -4
- package/runtime/codex/bin/codex-session-start.sh +45 -3
- package/runtime/codex/bin/codex-skill-render.mjs +23 -4
- package/runtime/codex/bin/graph-op.sh +719 -49
- package/runtime/codex/bin/graph-wal.sh +3 -2
- package/runtime/codex/bin/handoff-run.sh +72 -0
- package/runtime/codex/bin/handoff-save-egregore.sh +51 -11
- package/runtime/codex/bin/index-handoff.sh +26 -17
- package/runtime/codex/bin/ingest-graph.sh +80 -0
- package/runtime/codex/bin/ingest-surface.html +429 -0
- package/runtime/codex/bin/ingest.py +614 -0
- package/runtime/codex/bin/ingest.sh +231 -0
- package/runtime/codex/bin/ingest_graph.py +776 -0
- package/runtime/codex/bin/ingest_surface.py +464 -0
- package/runtime/codex/bin/lib/git-sync.sh +2 -6
- package/runtime/codex/bin/lib/greeting.sh +157 -22
- package/runtime/codex/bin/lib/identity.sh +50 -29
- package/runtime/codex/bin/lib/noncode.sh +31 -0
- package/runtime/codex/bin/person.py +610 -0
- package/runtime/codex/bin/person.sh +448 -0
- package/runtime/codex/bin/session-autosave.sh +184 -0
- package/runtime/codex/bin/session-log.sh +199 -0
- package/runtime/codex/bin/settings.sh +1 -1
- package/runtime/codex/manifest.json +19 -1
- package/runtime/pi/.codex/hooks/branch-guard.js +242 -34
- package/runtime/pi/.codex/skills/activity/SKILL.md +18 -6
- package/runtime/pi/.codex/skills/deep-reflect/SKILL.md +90 -55
- package/runtime/pi/.codex/skills/deploy-bridge/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/handoff/SKILL.md +6 -2
- package/runtime/pi/.codex/skills/save/SKILL.md +21 -3
- package/runtime/pi/.codex/skills/scroll/SKILL.md +54 -3
- package/runtime/pi/.codex/skills/task/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/tui-design/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/wrap/SKILL.md +4 -1
- package/runtime/pi/.codex/spec-manifest.json +15 -3
- package/runtime/pi/.pi/APPEND_SYSTEM.md +41 -5
- package/runtime/pi/.pi/extensions/egregore.ts +98 -8
- package/runtime/pi/.pi/spec-manifest.json +2 -2
- package/runtime/pi/AGENTS.md +45 -9
- package/runtime/pi/bin/activity-data.sh +1 -1
- package/runtime/pi/bin/agent.sh +66 -23
- package/runtime/pi/bin/attendant.sh +525 -0
- package/runtime/pi/bin/capture-reconcile.sh +67 -0
- package/runtime/pi/bin/capture-run.sh +341 -0
- package/runtime/pi/bin/codex-render-spec.mjs +15 -4
- package/runtime/pi/bin/codex-session-start.sh +45 -3
- package/runtime/pi/bin/codex-skill-render.mjs +23 -4
- package/runtime/pi/bin/graph-op.sh +719 -49
- package/runtime/pi/bin/graph-wal.sh +3 -2
- package/runtime/pi/bin/handoff-run.sh +72 -0
- package/runtime/pi/bin/handoff-save-egregore.sh +51 -11
- package/runtime/pi/bin/index-handoff.sh +26 -17
- package/runtime/pi/bin/ingest-graph.sh +80 -0
- package/runtime/pi/bin/ingest-surface.html +429 -0
- package/runtime/pi/bin/ingest.py +614 -0
- package/runtime/pi/bin/ingest.sh +231 -0
- package/runtime/pi/bin/ingest_graph.py +776 -0
- package/runtime/pi/bin/ingest_surface.py +464 -0
- package/runtime/pi/bin/lib/git-sync.sh +2 -6
- package/runtime/pi/bin/lib/greeting.sh +157 -22
- package/runtime/pi/bin/lib/identity.sh +50 -29
- package/runtime/pi/bin/lib/noncode.sh +31 -0
- package/runtime/pi/bin/person.py +610 -0
- package/runtime/pi/bin/person.sh +448 -0
- package/runtime/pi/bin/pi-branch-policy.mjs +164 -4
- package/runtime/pi/bin/pi-product-workflows.mjs +31 -2
- package/runtime/pi/bin/publish-artifact.sh +14 -0
- package/runtime/pi/bin/session-autosave.sh +184 -0
- package/runtime/pi/bin/session-log.sh +199 -0
- package/runtime/pi/bin/settings.sh +274 -0
- 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 {
|
|
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
|
@@ -48,22 +48,86 @@ function inside(child, parent) {
|
|
|
48
48
|
return rel === "" || (!!rel && !rel.startsWith("..") && !path.isAbsolute(rel));
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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(
|
|
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
|
-
|
|
363
|
+
const visible = stripQuotedSegments(command);
|
|
364
|
+
if (isBranchSetupCommand(visible)) return false;
|
|
172
365
|
|
|
173
366
|
const mutatingPatterns = [
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
|
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}).
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
`
|
|
218
|
-
"
|
|
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.
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|