create-egregore 0.19.11 → 0.19.12
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/assets/egregore-launcher.js +266 -4
- package/package.json +2 -2
- package/runtime/codex/.codex/skills/save/SKILL.md +8 -2
- package/runtime/codex/.codex/spec-manifest.json +4 -4
- package/runtime/codex/AGENTS.md +18 -10
- package/runtime/codex/bin/agent.sh +23 -3
- package/runtime/codex/bin/codex-render-spec.mjs +14 -10
- package/runtime/codex/bin/codex-session-start.sh +4 -1
- package/runtime/pi/.codex/skills/save/SKILL.md +8 -2
- package/runtime/pi/.codex/spec-manifest.json +4 -4
- package/runtime/pi/.pi/APPEND_SYSTEM.md +16 -9
- package/runtime/pi/.pi/spec-manifest.json +2 -2
- package/runtime/pi/AGENTS.md +18 -10
- package/runtime/pi/bin/agent.sh +23 -3
- package/runtime/pi/bin/codex-render-spec.mjs +14 -10
- package/runtime/pi/bin/codex-session-start.sh +4 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// egregore-launcher-version: 0.
|
|
2
|
+
// egregore-launcher-version: 0.8.0
|
|
3
3
|
//
|
|
4
4
|
// egregore — launch and browse Egregore instances from your terminal.
|
|
5
5
|
// Installed by create-egregore. Reads ~/.egregore/instances.json.
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// egregore <slug|name> pick a harness for the matching instance
|
|
14
14
|
// egregore ls | list list instances (non-interactive table)
|
|
15
15
|
// egregore use <slug> set the default for bare `egregore`
|
|
16
|
+
// egregore connect [slug] activate Connected for an existing instance
|
|
16
17
|
// egregore /invite <user> invite a GitHub user to an instance
|
|
17
18
|
// egregore settings [slug] hosting on/off, managed repos, admins (s in browser)
|
|
18
19
|
// egregore prune [--dry-run] remove entries whose path is gone
|
|
@@ -28,13 +29,18 @@ const path = require("node:path");
|
|
|
28
29
|
const os = require("node:os");
|
|
29
30
|
const http = require("node:http");
|
|
30
31
|
const https = require("node:https");
|
|
32
|
+
const crypto = require("node:crypto");
|
|
31
33
|
const { spawnSync, spawn } = require("node:child_process");
|
|
32
34
|
const readline = require("node:readline");
|
|
33
35
|
|
|
34
|
-
const VERSION = "0.
|
|
36
|
+
const VERSION = "0.8.0";
|
|
35
37
|
const REGISTRY = path.join(os.homedir(), ".egregore", "instances.json");
|
|
36
38
|
const DEFAULT_FILE = path.join(os.homedir(), ".egregore", "default");
|
|
37
39
|
const PUBLIC_NPX_PACKAGE = "create-egregore";
|
|
40
|
+
const CONNECT_API_URL = (
|
|
41
|
+
process.env.EGREGORE_CONNECT_API_URL ||
|
|
42
|
+
"https://egregore-production-55f2.up.railway.app"
|
|
43
|
+
).replace(/\/+$/, "");
|
|
38
44
|
|
|
39
45
|
const NATIVE_CODEX_SKILLS = [
|
|
40
46
|
"activity",
|
|
@@ -864,6 +870,7 @@ function cmdHelp() {
|
|
|
864
870
|
` ${CYAN}egregore browse${RESET} force the full-screen browser`,
|
|
865
871
|
` ${CYAN}egregore ls${RESET} ${DIM}| list${RESET} list installed instances`,
|
|
866
872
|
` ${CYAN}egregore use${RESET} ${DIM}<slug>${RESET} set the default for bare ${BOLD}egregore${RESET}`,
|
|
873
|
+
` ${CYAN}egregore connect${RESET} ${DIM}[slug]${RESET} activate Connected for an existing instance`,
|
|
867
874
|
` ${CYAN}egregore /invite${RESET} ${DIM}<user> [slug]${RESET} invite a GitHub user`,
|
|
868
875
|
` ${CYAN}egregore settings${RESET} ${DIM}[slug]${RESET} hosting on/off, repos, admins`,
|
|
869
876
|
` ${CYAN}egregore prune${RESET} ${DIM}[--dry-run]${RESET} remove entries with missing paths`,
|
|
@@ -874,6 +881,7 @@ function cmdHelp() {
|
|
|
874
881
|
` ${DIM}enter${RESET} choose selected instance`,
|
|
875
882
|
` ${DIM}/${RESET} filter (type to narrow; esc to clear)`,
|
|
876
883
|
` ${DIM}n / +${RESET} create a new Egregore`,
|
|
884
|
+
` ${DIM}c${RESET} connect selected instance`,
|
|
877
885
|
` ${DIM}u${RESET} set selected as default`,
|
|
878
886
|
` ${DIM}i${RESET} invite a GitHub user to selected instance`,
|
|
879
887
|
` ${DIM}s${RESET} settings (hosting on/off, repos, admins)`,
|
|
@@ -994,6 +1002,252 @@ function apiRequest(apiUrl, apiKey, apiPath, body) {
|
|
|
994
1002
|
}, body);
|
|
995
1003
|
}
|
|
996
1004
|
|
|
1005
|
+
function connectedApiRequest(githubToken, method, apiPath, body, headers = {}) {
|
|
1006
|
+
const url = new URL(apiPath, CONNECT_API_URL + "/");
|
|
1007
|
+
return requestJson({
|
|
1008
|
+
hostname: url.hostname,
|
|
1009
|
+
port: url.port || undefined,
|
|
1010
|
+
path: `${url.pathname}${url.search}`,
|
|
1011
|
+
method,
|
|
1012
|
+
protocol: url.protocol,
|
|
1013
|
+
headers: {
|
|
1014
|
+
Authorization: `Bearer ${githubToken}`,
|
|
1015
|
+
...headers,
|
|
1016
|
+
},
|
|
1017
|
+
}, body);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function openExternalUrl(url) {
|
|
1021
|
+
try {
|
|
1022
|
+
if (process.platform === "darwin") {
|
|
1023
|
+
spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
1024
|
+
} else if (process.platform === "win32") {
|
|
1025
|
+
spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
|
|
1026
|
+
} else {
|
|
1027
|
+
spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
1028
|
+
}
|
|
1029
|
+
return true;
|
|
1030
|
+
} catch {
|
|
1031
|
+
return false;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function atomicWrite(file, content, mode) {
|
|
1036
|
+
const temp = path.join(
|
|
1037
|
+
path.dirname(file),
|
|
1038
|
+
`.${path.basename(file)}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`,
|
|
1039
|
+
);
|
|
1040
|
+
fs.writeFileSync(temp, content, { mode });
|
|
1041
|
+
fs.renameSync(temp, file);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function writeJsonFileAtomic(file, value) {
|
|
1045
|
+
atomicWrite(file, JSON.stringify(value, null, 2) + "\n", 0o644);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function writeEnvValueAtomic(file, key, value) {
|
|
1049
|
+
const lines = fs.existsSync(file)
|
|
1050
|
+
? fs.readFileSync(file, "utf-8").split(/\r?\n/)
|
|
1051
|
+
: [];
|
|
1052
|
+
let replaced = false;
|
|
1053
|
+
const next = lines
|
|
1054
|
+
.filter((line, index) => !(index === lines.length - 1 && line === ""))
|
|
1055
|
+
.map((line) => {
|
|
1056
|
+
if (!line.startsWith(`${key}=`)) return line;
|
|
1057
|
+
replaced = true;
|
|
1058
|
+
return `${key}=${value}`;
|
|
1059
|
+
});
|
|
1060
|
+
if (!replaced) next.push(`${key}=${value}`);
|
|
1061
|
+
atomicWrite(file, next.join("\n") + "\n", 0o600);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function persistConnectState(inst, fields) {
|
|
1065
|
+
const file = path.join(inst.path, ".egregore-state.json");
|
|
1066
|
+
const state = readJsonFile(file);
|
|
1067
|
+
state.connected_activation = {
|
|
1068
|
+
...(state.connected_activation || {}),
|
|
1069
|
+
...fields,
|
|
1070
|
+
updated_at: new Date().toISOString(),
|
|
1071
|
+
};
|
|
1072
|
+
writeJsonFileAtomic(file, state);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function connectError(result, fallback) {
|
|
1076
|
+
if (!result) return fallback;
|
|
1077
|
+
const data = result.data || {};
|
|
1078
|
+
return data.detail || data.message || (typeof data === "string" ? data : fallback);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function sleep(ms) {
|
|
1082
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
async function cmdConnect(args) {
|
|
1086
|
+
const registry = readRegistry();
|
|
1087
|
+
if (registry.length === 0) die("no egregores installed yet.\n run: npx -y create-egregore@latest");
|
|
1088
|
+
const inst = resolveInviteInstance(registry, args[0] || "");
|
|
1089
|
+
if (!inst) {
|
|
1090
|
+
die("multiple egregores installed; choose one:\n egregore connect <slug>\n run: egregore ls");
|
|
1091
|
+
}
|
|
1092
|
+
if (!fs.existsSync(inst.path)) die(`instance path is missing: ${inst.path}`);
|
|
1093
|
+
|
|
1094
|
+
const { cfg, state, env } = configForInstance(inst);
|
|
1095
|
+
const githubToken = env.GITHUB_TOKEN || "";
|
|
1096
|
+
if (!githubToken) {
|
|
1097
|
+
die(`GITHUB_TOKEN is missing for ${inst.slug}.\n Add it to ${path.join(inst.path, ".env")} and retry.`);
|
|
1098
|
+
}
|
|
1099
|
+
const activation = state.connected_activation || {};
|
|
1100
|
+
if (cfg.mode === "connected" && activation.status === "connected") {
|
|
1101
|
+
console.log(`\n ${GREEN}✓${RESET} ${BOLD}${inst.slug}${RESET} is already connected.\n`);
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
console.log("");
|
|
1106
|
+
console.log(` ${BOLD}CONNECT${RESET} ${DIM}${inst.slug}${RESET}`);
|
|
1107
|
+
console.log(` ${DIM}€500/month · Stripe checkout · existing Markdown and Git stay in place${RESET}`);
|
|
1108
|
+
console.log("");
|
|
1109
|
+
|
|
1110
|
+
let intentId = activation.intent_id || "";
|
|
1111
|
+
let connectUrl = activation.connect_url || "";
|
|
1112
|
+
if (!intentId || !["checkout_pending", "entitled", "backfilling"].includes(activation.status)) {
|
|
1113
|
+
console.log(` [1/5] ${DIM}Creating connection intent…${RESET}`);
|
|
1114
|
+
const checkout = await connectedApiRequest(
|
|
1115
|
+
githubToken,
|
|
1116
|
+
"POST",
|
|
1117
|
+
"/api/billing/checkout",
|
|
1118
|
+
{
|
|
1119
|
+
org_slug: cfg.slug || inst.slug,
|
|
1120
|
+
plan: "connect",
|
|
1121
|
+
instance_metadata: {
|
|
1122
|
+
launcher_version: VERSION,
|
|
1123
|
+
mode: cfg.mode || "local",
|
|
1124
|
+
repo_name: cfg.repo_name || "",
|
|
1125
|
+
managed_repo_count: Array.isArray(cfg.repos) ? cfg.repos.length : 0,
|
|
1126
|
+
},
|
|
1127
|
+
},
|
|
1128
|
+
{ "X-Idempotency-Key": `cli:${inst.slug}:${crypto.randomUUID()}` },
|
|
1129
|
+
);
|
|
1130
|
+
if (checkout.status !== 200) {
|
|
1131
|
+
die(`could not start Connected checkout: ${connectError(checkout, `HTTP ${checkout.status}`)}`);
|
|
1132
|
+
}
|
|
1133
|
+
intentId = checkout.data.connection_intent_id;
|
|
1134
|
+
connectUrl = checkout.data.connect_url;
|
|
1135
|
+
persistConnectState(inst, {
|
|
1136
|
+
intent_id: intentId,
|
|
1137
|
+
checkout_session_id: checkout.data.checkout_session_id,
|
|
1138
|
+
connect_url: connectUrl,
|
|
1139
|
+
status: "checkout_pending",
|
|
1140
|
+
});
|
|
1141
|
+
console.log(` [1/5] ${GREEN}✓${RESET} Connection intent`);
|
|
1142
|
+
} else {
|
|
1143
|
+
console.log(` [1/5] ${GREEN}✓${RESET} Resuming connection intent`);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
if (activation.status !== "backfilling") {
|
|
1147
|
+
if (connectUrl) {
|
|
1148
|
+
const opened = openExternalUrl(connectUrl);
|
|
1149
|
+
const mark = opened ? `${GREEN}✓${RESET}` : `${YELLOW}!${RESET}`;
|
|
1150
|
+
console.log(` [2/5] ${mark} ${opened ? "Opened" : "Open"} Egregore Connect`);
|
|
1151
|
+
console.log(` ${CYAN}${connectUrl}${RESET}`);
|
|
1152
|
+
}
|
|
1153
|
+
console.log(` ${DIM}Complete checkout in the browser. Promotion codes are entered in Stripe.${RESET}`);
|
|
1154
|
+
} else {
|
|
1155
|
+
console.log(` [2/5] ${GREEN}✓${RESET} Subscription already confirmed`);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
const pollMs = Math.max(100, Number(process.env.EGREGORE_CONNECT_POLL_INTERVAL_MS || 2000));
|
|
1159
|
+
const timeoutMs = Math.max(pollMs, Number(process.env.EGREGORE_CONNECT_TIMEOUT_MS || 30 * 60 * 1000));
|
|
1160
|
+
const deadline = Date.now() + timeoutMs;
|
|
1161
|
+
let status;
|
|
1162
|
+
while (Date.now() < deadline) {
|
|
1163
|
+
const result = await connectedApiRequest(
|
|
1164
|
+
githubToken,
|
|
1165
|
+
"GET",
|
|
1166
|
+
`/api/billing/connect/${encodeURIComponent(intentId)}`,
|
|
1167
|
+
);
|
|
1168
|
+
if (result.status === 200) {
|
|
1169
|
+
status = result.data;
|
|
1170
|
+
if (
|
|
1171
|
+
status.entitled ||
|
|
1172
|
+
["entitled", "provisioning", "backfilling", "connected"].includes(status.status)
|
|
1173
|
+
) break;
|
|
1174
|
+
if (["canceled", "failed"].includes(status.status)) {
|
|
1175
|
+
persistConnectState(inst, { status: status.status });
|
|
1176
|
+
die(`Connected checkout ${status.status}. Your local Egregore was not changed.`);
|
|
1177
|
+
}
|
|
1178
|
+
} else if (result.status !== 0) {
|
|
1179
|
+
die(`could not read Connected status: ${connectError(result, `HTTP ${result.status}`)}`);
|
|
1180
|
+
}
|
|
1181
|
+
await sleep(pollMs);
|
|
1182
|
+
}
|
|
1183
|
+
if (!status || !(status.entitled || ["entitled", "provisioning", "backfilling", "connected"].includes(status.status))) {
|
|
1184
|
+
die("timed out waiting for checkout. Run the same command to resume.");
|
|
1185
|
+
}
|
|
1186
|
+
console.log(` [3/5] ${GREEN}✓${RESET} Subscription confirmed`);
|
|
1187
|
+
persistConnectState(inst, { status: "entitled" });
|
|
1188
|
+
|
|
1189
|
+
const claim = await connectedApiRequest(
|
|
1190
|
+
githubToken,
|
|
1191
|
+
"POST",
|
|
1192
|
+
`/api/billing/connect/${encodeURIComponent(intentId)}/claim`,
|
|
1193
|
+
{},
|
|
1194
|
+
);
|
|
1195
|
+
if (claim.status !== 200) {
|
|
1196
|
+
die(`could not activate Connected infrastructure: ${connectError(claim, `HTTP ${claim.status}`)}`);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
const configFile = path.join(inst.path, "egregore.json");
|
|
1200
|
+
const envFile = path.join(inst.path, ".env");
|
|
1201
|
+
const currentConfig = readJsonFile(configFile);
|
|
1202
|
+
writeJsonFileAtomic(configFile, {
|
|
1203
|
+
...currentConfig,
|
|
1204
|
+
mode: "connected",
|
|
1205
|
+
api_url: claim.data.api_url,
|
|
1206
|
+
});
|
|
1207
|
+
writeEnvValueAtomic(envFile, "EGREGORE_API_KEY", claim.data.api_key);
|
|
1208
|
+
persistConnectState(inst, {
|
|
1209
|
+
status: "backfilling",
|
|
1210
|
+
activation_protocol: claim.data.activation_protocol,
|
|
1211
|
+
});
|
|
1212
|
+
console.log(` [4/5] ${GREEN}✓${RESET} Connected credential and graph node`);
|
|
1213
|
+
|
|
1214
|
+
const syncScript = path.join(inst.path, "bin", "sync-graph.sh");
|
|
1215
|
+
if (!fs.existsSync(syncScript)) {
|
|
1216
|
+
die("bin/sync-graph.sh is missing. Update the installed Egregore version, then run egregore connect again.");
|
|
1217
|
+
}
|
|
1218
|
+
const sync = spawnSync("bash", [syncScript], {
|
|
1219
|
+
cwd: inst.path,
|
|
1220
|
+
encoding: "utf-8",
|
|
1221
|
+
stdio: "pipe",
|
|
1222
|
+
timeout: 10 * 60 * 1000,
|
|
1223
|
+
});
|
|
1224
|
+
if (sync.status !== 0) {
|
|
1225
|
+
const detail = (sync.stderr || sync.stdout || "graph replay failed").trim();
|
|
1226
|
+
die(`Connected is configured, but graph replay did not finish.\n ${detail}\n Run egregore connect ${inst.slug} to retry; existing data is unchanged.`);
|
|
1227
|
+
}
|
|
1228
|
+
let backfillSummary = { replayed: true };
|
|
1229
|
+
try { backfillSummary = JSON.parse((sync.stdout || "").trim()); } catch {}
|
|
1230
|
+
|
|
1231
|
+
const complete = await connectedApiRequest(
|
|
1232
|
+
githubToken,
|
|
1233
|
+
"POST",
|
|
1234
|
+
`/api/billing/connect/${encodeURIComponent(intentId)}/complete`,
|
|
1235
|
+
{ backfill_summary: backfillSummary },
|
|
1236
|
+
);
|
|
1237
|
+
if (complete.status !== 200) {
|
|
1238
|
+
die(`graph replay finished, but activation could not be finalized: ${connectError(complete, `HTTP ${complete.status}`)}`);
|
|
1239
|
+
}
|
|
1240
|
+
persistConnectState(inst, {
|
|
1241
|
+
status: "connected",
|
|
1242
|
+
connected_at: new Date().toISOString(),
|
|
1243
|
+
backfill_summary: backfillSummary,
|
|
1244
|
+
});
|
|
1245
|
+
console.log(` [5/5] ${GREEN}✓${RESET} Existing memory projected into the graph`);
|
|
1246
|
+
console.log("");
|
|
1247
|
+
console.log(` ${GREEN}✓${RESET} ${BOLD}${inst.slug}${RESET} is connected. Local files remain the source of truth.`);
|
|
1248
|
+
console.log("");
|
|
1249
|
+
}
|
|
1250
|
+
|
|
997
1251
|
function accessLabel(status) {
|
|
998
1252
|
if (status === 201) return `${GREEN}sent${RESET}`;
|
|
999
1253
|
if (status === 204) return `${GREEN}already added${RESET}`;
|
|
@@ -1952,8 +2206,8 @@ async function browser(args = {}) {
|
|
|
1952
2206
|
footer = band(`● ${stripAnsi(state.message)}`, bg);
|
|
1953
2207
|
} else {
|
|
1954
2208
|
footer = blockWidth < 96
|
|
1955
|
-
? band(`${key("↑↓", "nav")} ${key("enter", "select")} ${key("n/+", "new")} ${key("
|
|
1956
|
-
: band(`${key("↑↓", "nav")} ${key("enter", "select")} ${key("n/+", "new")} ${key("/", "filter")} ${key("i", "invite")} ${key("s", "settings")} ${key("u", "default")} ${key("d", "delete")} ${key("?", "help")} ${key("q", "quit")}`, 46);
|
|
2209
|
+
? band(`${key("↑↓", "nav")} ${key("enter", "select")} ${key("n/+", "new")} ${key("c", "connect")} ${key("i", "invite")} ${key("d", "delete")} ${key("?", "help")} ${key("q", "quit")}`, 46)
|
|
2210
|
+
: band(`${key("↑↓", "nav")} ${key("enter", "select")} ${key("n/+", "new")} ${key("/", "filter")} ${key("c", "connect")} ${key("i", "invite")} ${key("s", "settings")} ${key("u", "default")} ${key("d", "delete")} ${key("?", "help")} ${key("q", "quit")}`, 46);
|
|
1957
2211
|
}
|
|
1958
2212
|
out.push(M + footer + "\n");
|
|
1959
2213
|
if (state.showHelp) {
|
|
@@ -2310,6 +2564,13 @@ async function browser(args = {}) {
|
|
|
2310
2564
|
resolve(setupNewEgregore(args.runtimeOverride));
|
|
2311
2565
|
return;
|
|
2312
2566
|
}
|
|
2567
|
+
if (chunk === "c") {
|
|
2568
|
+
const pick = visible()[state.cursor]?.e;
|
|
2569
|
+
if (!pick) return;
|
|
2570
|
+
cleanup();
|
|
2571
|
+
resolve(cmdConnect([pick.slug]));
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2313
2574
|
if (chunk === "i") {
|
|
2314
2575
|
const pick = visible()[state.cursor]?.e;
|
|
2315
2576
|
if (!pick) return;
|
|
@@ -2451,6 +2712,7 @@ function main() {
|
|
|
2451
2712
|
// Non-interactive subcommands
|
|
2452
2713
|
if (cmd === "ls" || cmd === "list") return cmdLs();
|
|
2453
2714
|
if (cmd === "use") return cmdUse(args[1]);
|
|
2715
|
+
if (cmd === "connect") return cmdConnect(args.slice(1));
|
|
2454
2716
|
if (cmd === "/invite" || cmd === "invite") return cmdInvite(args.slice(1));
|
|
2455
2717
|
if (cmd === "settings" || cmd === "config") return cmdSettings(args.slice(1));
|
|
2456
2718
|
if (cmd === "prune") return cmdPruneNonInteractive(args[1] === "--dry-run" || args[1] === "-n");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-egregore",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.12",
|
|
4
4
|
"description": "Set up Egregore for your team in one command — installs the egregore launcher and your choice of Claude Code, Codex, or Pi.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build:runtime": "node scripts/build-runtime-bundle.mjs && node scripts/build-pi-runtime-bundle.mjs",
|
|
14
14
|
"build:pi-runtime": "node scripts/build-pi-runtime-bundle.mjs",
|
|
15
|
-
"test": "node scripts/build-runtime-bundle.mjs --check && node scripts/build-pi-runtime-bundle.mjs --check && node --test test/context.test.js test/parse-join-target.test.js test/skip-repos.test.js test/onboarding-shortcircuit.test.js test/welcome-screen.test.js test/runtime-selection.test.js test/command-generation.test.js test/auth-gh.test.js test/gauntlet-fixes.test.js test/codex-runtime.test.js test/pi-runtime.test.js test/pi-product-workflows.test.js test/installer-runtime-source.test.js test/scroll-repro.test.js test/launcher-refocus-redraw.test.js test/multiselect-remote-search.test.js test/back-navigation.test.js test/manifest-form.test.js test/launcher-delete.test.js",
|
|
15
|
+
"test": "node scripts/build-runtime-bundle.mjs --check && node scripts/build-pi-runtime-bundle.mjs --check && node --test test/context.test.js test/parse-join-target.test.js test/skip-repos.test.js test/onboarding-shortcircuit.test.js test/welcome-screen.test.js test/runtime-selection.test.js test/command-generation.test.js test/auth-gh.test.js test/gauntlet-fixes.test.js test/codex-runtime.test.js test/pi-runtime.test.js test/pi-product-workflows.test.js test/installer-runtime-source.test.js test/scroll-repro.test.js test/launcher-refocus-redraw.test.js test/multiselect-remote-search.test.js test/back-navigation.test.js test/manifest-form.test.js test/launcher-delete.test.js test/launcher-connect.test.js",
|
|
16
16
|
"test:e2e": "node --test test/e2e-local.test.js",
|
|
17
17
|
"test:all": "node scripts/build-runtime-bundle.mjs --check && node scripts/build-pi-runtime-bundle.mjs --check && node --test test/"
|
|
18
18
|
},
|
|
@@ -43,7 +43,11 @@ esac
|
|
|
43
43
|
4. Synthesize:
|
|
44
44
|
- a short topic from the work,
|
|
45
45
|
- a clear commit message,
|
|
46
|
-
- a one-line user-facing scope summary
|
|
46
|
+
- a one-line user-facing scope summary,
|
|
47
|
+
- a PR description following `.claude/context/pr-format.md`:
|
|
48
|
+
`## What` (1–4 bullets), `## Why` (1–3 sentences), and
|
|
49
|
+
`## Verification` (how the change was checked — required when the
|
|
50
|
+
diff touches non-markdown files; be honest if unverified).
|
|
47
51
|
5. If the scope is ambiguous or includes unrelated changes, ask for one compact
|
|
48
52
|
confirmation. Use structured Codex question tooling when available;
|
|
49
53
|
otherwise render:
|
|
@@ -58,11 +62,13 @@ Other:
|
|
|
58
62
|
6. Run the bridge command:
|
|
59
63
|
|
|
60
64
|
```bash
|
|
61
|
-
bin/agent.sh save --message "$MESSAGE" --topic "$TOPIC"
|
|
65
|
+
bin/agent.sh save --message "$MESSAGE" --topic "$TOPIC" --pr-body "$PR_BODY"
|
|
62
66
|
```
|
|
63
67
|
|
|
64
68
|
The bridge owns the mechanical workflow: sync memory, ensure a task branch,
|
|
65
69
|
commit repo changes, push, and create or reuse a pull request when available.
|
|
70
|
+
Always pass the synthesized `--pr-body` — the bridge only auto-generates a
|
|
71
|
+
skeleton body as a last resort, and the CI `pr-format` check gates every PR.
|
|
66
72
|
|
|
67
73
|
7. Parse the output and report only useful user information:
|
|
68
74
|
- branch name,
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
{
|
|
42
42
|
"section": "Config Files",
|
|
43
43
|
"action": "keep",
|
|
44
|
-
"sourceHash": "
|
|
44
|
+
"sourceHash": "8d7f1bd0f2d9",
|
|
45
45
|
"note": "Kept verbatim — shell facts, runtime-neutral."
|
|
46
46
|
},
|
|
47
47
|
{
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
{
|
|
84
84
|
"section": "Git Workflow",
|
|
85
85
|
"action": "keep",
|
|
86
|
-
"sourceHash": "
|
|
86
|
+
"sourceHash": "49fd4625fa1e",
|
|
87
87
|
"note": "Kept; slash tokens rendered as $skill tokens. Branch diagram unchanged (code fence)."
|
|
88
88
|
},
|
|
89
89
|
{
|
|
@@ -119,8 +119,8 @@
|
|
|
119
119
|
{
|
|
120
120
|
"section": "Environment Isolation",
|
|
121
121
|
"action": "replace",
|
|
122
|
-
"sourceHash": "
|
|
123
|
-
"note": "boundary-check is a Claude PreToolUse hook with no .codex equivalent —
|
|
122
|
+
"sourceHash": "086e2c1d9019",
|
|
123
|
+
"note": "boundary-check is a Claude PreToolUse hook with no .codex equivalent — two-tier model stated as a standing instruction; AskUserQuestion remediation -> numbered options, now including the two consent-grant options (session grant -> .egregore-boundary-consent, instance grant -> .egregore-boundary.local.json read[])."
|
|
124
124
|
}
|
|
125
125
|
]
|
|
126
126
|
}
|
package/runtime/codex/AGENTS.md
CHANGED
|
@@ -35,7 +35,8 @@ Use the runtime-neutral bridge instead of Claude Code slash commands:
|
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
37
|
bin/agent.sh branch --topic "auth review"
|
|
38
|
-
bin/agent.sh save --message "Save: auth review" --topic "auth review"
|
|
38
|
+
bin/agent.sh save --message "Save: auth review" --topic "auth review" \
|
|
39
|
+
--pr-body "$PR_BODY" # body per .claude/context/pr-format.md (auto-skeleton if omitted)
|
|
39
40
|
bin/agent.sh wrap --from alice --topic "auth review" \
|
|
40
41
|
--summary "Implemented the OAuth callback parser and documented follow-ups." \
|
|
41
42
|
--body "Open threads: review error cases and browser redirects."
|
|
@@ -191,7 +192,7 @@ If the startup card output contains `onboarding_needed`, invoke the `$onboarding
|
|
|
191
192
|
|
|
192
193
|
## Config Files
|
|
193
194
|
|
|
194
|
-
- **`egregore.json`** — committed. Non-secret org config: `org_name`, `github_org`, `memory_repo`, `slug`, `mode`. `api_url` is connected-mode only. **Never put secrets here.**
|
|
195
|
+
- **`egregore.json`** — committed. Non-secret org config: `org_name`, `github_org`, `memory_repo`, `slug`, `mode`. `api_url` is connected-mode only. Optional `boundary { posture, read[], locked }` sets the org's isolation posture (see Environment Isolation). **Never put secrets here.**
|
|
195
196
|
- **`.env`** — gitignored. Personal secrets. Local mode: `GITHUB_TOKEN` only. Connected mode: `GITHUB_TOKEN` + `EGREGORE_API_KEY`. **Never use `source .env`** — use `grep '^KEY=' .env | cut -d'=' -f2-`.
|
|
196
197
|
|
|
197
198
|
In connected mode, infrastructure credentials (Neo4j, Telegram) live on the API server only — `bin/graph.sh` and `bin/notify.sh` route through the API gateway.
|
|
@@ -280,6 +281,10 @@ main ← stable (/release)
|
|
|
280
281
|
- **Memory repo**: stays on main (separate repo, auto-merge).
|
|
281
282
|
- **Never push directly to main or develop.** All changes flow through PRs.
|
|
282
283
|
|
|
284
|
+
### Pull request format (all harnesses)
|
|
285
|
+
|
|
286
|
+
Every PR body follows `.claude/context/pr-format.md`, enforced by the `pr-format` CI check regardless of which harness opened it: `## What` (1–4 bullets) + `## Why` (1–3 sentences) always; `## Verification` when the diff touches non-markdown files (how it was checked, or an honest `Not verified — <reason>`); `## Risk`/`## Links` when real; title `type(scope): imperative summary` (advisory). **Never create a PR with an empty body or `--fill`** — write the body and pass it explicitly (`gh pr create --body`, or `bin/agent.sh save --pr-body` for shell agents; the bridge auto-generates a compliant skeleton only as a last resort).
|
|
287
|
+
|
|
283
288
|
### Managed Repos
|
|
284
289
|
|
|
285
290
|
Repos in `egregore.json` → `repos[]` are cloned as siblings (`../{repo}/`). Each entry can be a string or `{"name": "...", "description": "..."}`. Match user intent to the right repo using `description`. Same branching strategy. Use `git -C` with absolute paths — never `cd` into repos. `$save` scans all managed repos for uncommitted changes.
|
|
@@ -368,19 +373,22 @@ Egregore runs in one of two configurations, set by `mode` in `egregore.json`. De
|
|
|
368
373
|
|
|
369
374
|
## Environment Isolation
|
|
370
375
|
|
|
371
|
-
Sessions are confined to this project + memory + managed repos. On Codex
|
|
376
|
+
Sessions are confined to this project + memory + managed repos, with a **two-tier boundary** — a hard wall between Egregore instances, a consent gate for everything else. On Codex the boundary is a standing instruction, not an enforced hook — hold it yourself.
|
|
372
377
|
|
|
373
|
-
- **Never modify `~/.egregore/instances.json
|
|
374
|
-
- **
|
|
375
|
-
- When a request
|
|
378
|
+
- **Hard tier — other Egregore instances.** Denied for every tool, always. There is no consent path. Never access another instance's files — refuse even if asked. Never modify `~/.egregore/instances.json` (managed by session-start.sh). When a request points at another instance's files there is nothing to ask — refuse and explain.
|
|
379
|
+
- **Soft tier — paths outside the boundary.** Consent-gated. Inbox dirs (`~/Downloads`, `~/Desktop`) are readable without consent under the default posture; writes outside the project always need consent. Posture (`strict | standard | open`) and extra read roots come from `egregore.json` -> `boundary { posture, read[], locked }` (org, committed) merged with `.egregore-boundary.local.json` (personal, gitignored). `locked: true` removes the consent path entirely.
|
|
380
|
+
- When a request needs soft-tier consent, do not improvise a workaround. Ask in plain text with numbered options:
|
|
376
381
|
|
|
377
382
|
```text
|
|
378
383
|
That path is outside this Egregore's boundary. How should we proceed?
|
|
379
|
-
1.
|
|
380
|
-
2.
|
|
381
|
-
3.
|
|
384
|
+
1. Allow its directory for this session (recorded in .egregore-boundary-consent)
|
|
385
|
+
2. Always allow on this instance (added to read[] in .egregore-boundary.local.json)
|
|
386
|
+
3. Paste the contents inline
|
|
387
|
+
4. Cancel
|
|
382
388
|
```
|
|
383
389
|
|
|
384
|
-
|
|
390
|
+
Never record a consent grant without the user's explicit choice of option 1 or 2 in that exchange. Session grants are cleared on session start; `locked: true` removes options 1 and 2.
|
|
391
|
+
|
|
392
|
+
See DEVELOPMENT.md §3 for boundary details and `memory/knowledge/decisions/2026-07-08-boundary-hook-consent-design.md` for the design decisions.
|
|
385
393
|
|
|
386
394
|
<!-- END GENERATED EGREGORE CODEX SPEC -->
|
|
@@ -32,7 +32,10 @@ Runtime-neutral commands:
|
|
|
32
32
|
Hybrid recall over shared memory (qmd)
|
|
33
33
|
people List known people from memory/people
|
|
34
34
|
branch --topic TEXT Create or reuse a task branch/worktree
|
|
35
|
-
save [--message TEXT]
|
|
35
|
+
save [--message TEXT] [--pr-body TEXT|--pr-body-file PATH]
|
|
36
|
+
Commit and push current repo + memory changes;
|
|
37
|
+
PR body follows .claude/context/pr-format.md
|
|
38
|
+
(auto-generated skeleton if not provided)
|
|
36
39
|
wrap --topic T --summary TEXT Write a session wrap to memory/wraps
|
|
37
40
|
handoff --from A --to B --topic T [--intent action|feedback|fyi]
|
|
38
41
|
[--body TEXT|--body-file PATH] [--no-push] [--no-publish] [--no-notify] [--json]
|
|
@@ -332,13 +335,26 @@ ensure_working_branch() {
|
|
|
332
335
|
echo "$branch"
|
|
333
336
|
}
|
|
334
337
|
|
|
338
|
+
build_pr_body() {
|
|
339
|
+
# Compliant fallback PR body (spec: .claude/context/pr-format.md) built
|
|
340
|
+
# from the commit log + diff when the calling agent supplies none.
|
|
341
|
+
local topic="${1:-portable save}" commits stat
|
|
342
|
+
commits="$(git -C "$SCRIPT_DIR" log --pretty='- %s' "origin/develop..HEAD" 2>/dev/null | head -8)"
|
|
343
|
+
[ -n "$commits" ] || commits="- (commit list unavailable — see the Files tab)"
|
|
344
|
+
stat="$(git -C "$SCRIPT_DIR" diff --shortstat "origin/develop...HEAD" 2>/dev/null | sed 's/^ *//')"
|
|
345
|
+
printf '## What\n%s\n\n## Why\nShell-agent session save — topic: %s. Body auto-generated by bin/agent.sh; pass --pr-body for a hand-written description.\n\n## Verification\nNot verified — auto-generated skeleton; review the diff before merging.%s\n\n🤖 Saved via bin/agent.sh\n' \
|
|
346
|
+
"$commits" "$topic" "${stat:+ ($stat)}"
|
|
347
|
+
}
|
|
348
|
+
|
|
335
349
|
cmd_save() {
|
|
336
|
-
local message="" topic="" no_pr=0 branch pushed="false" committed="false" pr_url="" existing_pr="" pr_number="" non_md="" merged="false"
|
|
350
|
+
local message="" topic="" no_pr=0 branch pushed="false" committed="false" pr_url="" existing_pr="" pr_number="" non_md="" merged="false" pr_body="" pr_body_file=""
|
|
337
351
|
|
|
338
352
|
while [ $# -gt 0 ]; do
|
|
339
353
|
case "$1" in
|
|
340
354
|
--message|-m) message="${2:-}"; shift 2 ;;
|
|
341
355
|
--topic) topic="${2:-}"; shift 2 ;;
|
|
356
|
+
--pr-body) pr_body="${2:-}"; shift 2 ;;
|
|
357
|
+
--pr-body-file) pr_body_file="${2:-}"; shift 2 ;;
|
|
342
358
|
--no-pr) no_pr=1; shift ;;
|
|
343
359
|
*) die "unknown save option: $1" ;;
|
|
344
360
|
esac
|
|
@@ -376,7 +392,11 @@ cmd_save() {
|
|
|
376
392
|
if [ -n "$existing_pr" ]; then
|
|
377
393
|
pr_url="$existing_pr"
|
|
378
394
|
else
|
|
379
|
-
|
|
395
|
+
if [ -z "$pr_body" ] && [ -n "$pr_body_file" ] && [ -f "$pr_body_file" ]; then
|
|
396
|
+
pr_body="$(cat "$pr_body_file")"
|
|
397
|
+
fi
|
|
398
|
+
[ -n "$pr_body" ] || pr_body="$(build_pr_body "$topic")"
|
|
399
|
+
pr_url="$(gh pr create --base develop --head "$branch" --title "$message" --body "$pr_body" 2>/dev/null || true)"
|
|
380
400
|
fi
|
|
381
401
|
fi
|
|
382
402
|
|
|
@@ -79,7 +79,8 @@ Use the runtime-neutral bridge instead of Claude Code slash commands:
|
|
|
79
79
|
|
|
80
80
|
\`\`\`bash
|
|
81
81
|
bin/agent.sh branch --topic "auth review"
|
|
82
|
-
bin/agent.sh save --message "Save: auth review" --topic "auth review"
|
|
82
|
+
bin/agent.sh save --message "Save: auth review" --topic "auth review" \\
|
|
83
|
+
--pr-body "$PR_BODY" # body per .claude/context/pr-format.md (auto-skeleton if omitted)
|
|
83
84
|
bin/agent.sh wrap --from alice --topic "auth review" \\
|
|
84
85
|
--summary "Implemented the OAuth callback parser and documented follow-ups." \\
|
|
85
86
|
--body "Open threads: review error cases and browser redirects."
|
|
@@ -251,20 +252,23 @@ Codex has no structured question tool — render each batch as compact numbered
|
|
|
251
252
|
|
|
252
253
|
const LOOM_ROUTING_BODY = `Loom routes commands across model tiers on the Claude Code runtime (\`loom/routes.json\` + \`bin/loom.sh\` + a model-pinned executor subagent). Codex has no subagent delegation — every command runs inline in the current session. Ignore "Loom routing" preambles if you encounter them in a skill spec, and skip \`bin/loom.sh\` calls; \`loom/\` and \`.claude/agents/\` are framework files synced for the Claude runtime.`;
|
|
253
254
|
|
|
254
|
-
const ISOLATION_BODY = `Sessions are confined to this project + memory + managed repos. On Codex
|
|
255
|
+
const ISOLATION_BODY = `Sessions are confined to this project + memory + managed repos, with a **two-tier boundary** — a hard wall between Egregore instances, a consent gate for everything else. On Codex the boundary is a standing instruction, not an enforced hook — hold it yourself.
|
|
255
256
|
|
|
256
|
-
- **Never modify \`~/.egregore/instances.json
|
|
257
|
-
- **
|
|
258
|
-
- When a request
|
|
257
|
+
- **Hard tier — other Egregore instances.** Denied for every tool, always. There is no consent path. Never access another instance's files — refuse even if asked. Never modify \`~/.egregore/instances.json\` (managed by session-start.sh). When a request points at another instance's files there is nothing to ask — refuse and explain.
|
|
258
|
+
- **Soft tier — paths outside the boundary.** Consent-gated. Inbox dirs (\`~/Downloads\`, \`~/Desktop\`) are readable without consent under the default posture; writes outside the project always need consent. Posture (\`strict | standard | open\`) and extra read roots come from \`egregore.json\` -> \`boundary { posture, read[], locked }\` (org, committed) merged with \`.egregore-boundary.local.json\` (personal, gitignored). \`locked: true\` removes the consent path entirely.
|
|
259
|
+
- When a request needs soft-tier consent, do not improvise a workaround. Ask in plain text with numbered options:
|
|
259
260
|
|
|
260
261
|
\`\`\`text
|
|
261
262
|
That path is outside this Egregore's boundary. How should we proceed?
|
|
262
|
-
1.
|
|
263
|
-
2.
|
|
264
|
-
3.
|
|
263
|
+
1. Allow its directory for this session (recorded in .egregore-boundary-consent)
|
|
264
|
+
2. Always allow on this instance (added to read[] in .egregore-boundary.local.json)
|
|
265
|
+
3. Paste the contents inline
|
|
266
|
+
4. Cancel
|
|
265
267
|
\`\`\`
|
|
266
268
|
|
|
267
|
-
|
|
269
|
+
Never record a consent grant without the user's explicit choice of option 1 or 2 in that exchange. Session grants are cleared on session start; \`locked: true\` removes options 1 and 2.
|
|
270
|
+
|
|
271
|
+
See DEVELOPMENT.md §3 for boundary details and \`memory/knowledge/decisions/2026-07-08-boundary-hook-consent-design.md\` for the design decisions.`;
|
|
268
272
|
|
|
269
273
|
// ── per-section rules ────────────────────────────────────────────────────
|
|
270
274
|
const RULES = {
|
|
@@ -326,7 +330,7 @@ const RULES = {
|
|
|
326
330
|
'Environment Isolation': {
|
|
327
331
|
action: 'replace',
|
|
328
332
|
body: ISOLATION_BODY,
|
|
329
|
-
note: 'boundary-check is a Claude PreToolUse hook with no .codex equivalent —
|
|
333
|
+
note: 'boundary-check is a Claude PreToolUse hook with no .codex equivalent — two-tier model stated as a standing instruction; AskUserQuestion remediation -> numbered options, now including the two consent-grant options (session grant -> .egregore-boundary-consent, instance grant -> .egregore-boundary.local.json read[]).',
|
|
330
334
|
},
|
|
331
335
|
};
|
|
332
336
|
|
|
@@ -6,7 +6,7 @@ set -o pipefail
|
|
|
6
6
|
# libraries as Claude's SessionStart, then renders a terminal greeting before
|
|
7
7
|
# Codex itself starts.
|
|
8
8
|
|
|
9
|
-
FRAMEWORK_VERSION="
|
|
9
|
+
FRAMEWORK_VERSION="7"
|
|
10
10
|
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
11
11
|
cd "$SCRIPT_DIR" || exit 1
|
|
12
12
|
|
|
@@ -21,6 +21,9 @@ fi
|
|
|
21
21
|
# Clear any branch-guard consent from a previous session — consent to write on
|
|
22
22
|
# a protected branch is asked fresh each session (see AGENTS.md branch-guard protocol).
|
|
23
23
|
rm -f "$SCRIPT_DIR/.egregore-branch-consent" "$MAIN_PROJECT_DIR/.egregore-branch-consent" 2>/dev/null
|
|
24
|
+
# Same for boundary-crossing consent — grants are session-scoped (AGENTS.md
|
|
25
|
+
# environment-isolation protocol); durable grants belong in .egregore-boundary.local.json.
|
|
26
|
+
rm -f "$SCRIPT_DIR/.egregore-boundary-consent" "$MAIN_PROJECT_DIR/.egregore-boundary-consent" 2>/dev/null
|
|
24
27
|
|
|
25
28
|
if [ -f "$SCRIPT_DIR/bin/lib/worktree-links.sh" ]; then
|
|
26
29
|
source "$SCRIPT_DIR/bin/lib/worktree-links.sh" >/dev/null 2>/dev/null || true
|
|
@@ -43,7 +43,11 @@ esac
|
|
|
43
43
|
4. Synthesize:
|
|
44
44
|
- a short topic from the work,
|
|
45
45
|
- a clear commit message,
|
|
46
|
-
- a one-line user-facing scope summary
|
|
46
|
+
- a one-line user-facing scope summary,
|
|
47
|
+
- a PR description following `.claude/context/pr-format.md`:
|
|
48
|
+
`## What` (1–4 bullets), `## Why` (1–3 sentences), and
|
|
49
|
+
`## Verification` (how the change was checked — required when the
|
|
50
|
+
diff touches non-markdown files; be honest if unverified).
|
|
47
51
|
5. If the scope is ambiguous or includes unrelated changes, ask for one compact
|
|
48
52
|
confirmation. Use structured Codex question tooling when available;
|
|
49
53
|
otherwise render:
|
|
@@ -58,11 +62,13 @@ Other:
|
|
|
58
62
|
6. Run the bridge command:
|
|
59
63
|
|
|
60
64
|
```bash
|
|
61
|
-
bin/agent.sh save --message "$MESSAGE" --topic "$TOPIC"
|
|
65
|
+
bin/agent.sh save --message "$MESSAGE" --topic "$TOPIC" --pr-body "$PR_BODY"
|
|
62
66
|
```
|
|
63
67
|
|
|
64
68
|
The bridge owns the mechanical workflow: sync memory, ensure a task branch,
|
|
65
69
|
commit repo changes, push, and create or reuse a pull request when available.
|
|
70
|
+
Always pass the synthesized `--pr-body` — the bridge only auto-generates a
|
|
71
|
+
skeleton body as a last resort, and the CI `pr-format` check gates every PR.
|
|
66
72
|
|
|
67
73
|
7. Parse the output and report only useful user information:
|
|
68
74
|
- branch name,
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
{
|
|
42
42
|
"section": "Config Files",
|
|
43
43
|
"action": "keep",
|
|
44
|
-
"sourceHash": "
|
|
44
|
+
"sourceHash": "8d7f1bd0f2d9",
|
|
45
45
|
"note": "Kept verbatim — shell facts, runtime-neutral."
|
|
46
46
|
},
|
|
47
47
|
{
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
{
|
|
84
84
|
"section": "Git Workflow",
|
|
85
85
|
"action": "keep",
|
|
86
|
-
"sourceHash": "
|
|
86
|
+
"sourceHash": "49fd4625fa1e",
|
|
87
87
|
"note": "Kept; slash tokens rendered as $skill tokens. Branch diagram unchanged (code fence)."
|
|
88
88
|
},
|
|
89
89
|
{
|
|
@@ -119,8 +119,8 @@
|
|
|
119
119
|
{
|
|
120
120
|
"section": "Environment Isolation",
|
|
121
121
|
"action": "replace",
|
|
122
|
-
"sourceHash": "
|
|
123
|
-
"note": "boundary-check is a Claude PreToolUse hook with no .codex equivalent —
|
|
122
|
+
"sourceHash": "086e2c1d9019",
|
|
123
|
+
"note": "boundary-check is a Claude PreToolUse hook with no .codex equivalent — two-tier model stated as a standing instruction; AskUserQuestion remediation -> numbered options, now including the two consent-grant options (session grant -> .egregore-boundary-consent, instance grant -> .egregore-boundary.local.json read[])."
|
|
124
124
|
}
|
|
125
125
|
]
|
|
126
126
|
}
|
|
@@ -118,7 +118,7 @@ If the startup card output contains `onboarding_needed`, invoke the `/onboarding
|
|
|
118
118
|
|
|
119
119
|
## Config Files
|
|
120
120
|
|
|
121
|
-
- **`egregore.json`** — committed. Non-secret org config: `org_name`, `github_org`, `memory_repo`, `slug`, `mode`. `api_url` is connected-mode only. **Never put secrets here.**
|
|
121
|
+
- **`egregore.json`** — committed. Non-secret org config: `org_name`, `github_org`, `memory_repo`, `slug`, `mode`. `api_url` is connected-mode only. Optional `boundary { posture, read[], locked }` sets the org's isolation posture (see Environment Isolation). **Never put secrets here.**
|
|
122
122
|
- **`.env`** — gitignored. Personal secrets. Local mode: `GITHUB_TOKEN` only. Connected mode: `GITHUB_TOKEN` + `EGREGORE_API_KEY`. **Never use `source .env`** — use `grep '^KEY=' .env | cut -d'=' -f2-`.
|
|
123
123
|
|
|
124
124
|
In connected mode, infrastructure credentials (Neo4j, Telegram) live on the API server only — `bin/graph.sh` and `bin/notify.sh` route through the API gateway.
|
|
@@ -207,6 +207,10 @@ main ← stable (/release)
|
|
|
207
207
|
- **Memory repo**: stays on main (separate repo, auto-merge).
|
|
208
208
|
- **Never push directly to main or develop.** All changes flow through PRs.
|
|
209
209
|
|
|
210
|
+
### Pull request format (all harnesses)
|
|
211
|
+
|
|
212
|
+
Every PR body follows `.claude/context/pr-format.md`, enforced by the `pr-format` CI check regardless of which harness opened it: `## What` (1–4 bullets) + `## Why` (1–3 sentences) always; `## Verification` when the diff touches non-markdown files (how it was checked, or an honest `Not verified — <reason>`); `## Risk`/`## Links` when real; title `type(scope): imperative summary` (advisory). **Never create a PR with an empty body or `--fill`** — write the body and pass it explicitly (`gh pr create --body`, or `bin/agent.sh save --pr-body` for shell agents; the bridge auto-generates a compliant skeleton only as a last resort).
|
|
213
|
+
|
|
210
214
|
### Managed Repos
|
|
211
215
|
|
|
212
216
|
Repos in `egregore.json` → `repos[]` are cloned as siblings (`../{repo}/`). Each entry can be a string or `{"name": "...", "description": "..."}`. Match user intent to the right repo using `description`. Same branching strategy. Use `git -C` with absolute paths — never `cd` into repos. `/save` scans all managed repos for uncommitted changes.
|
|
@@ -275,17 +279,20 @@ Egregore runs in one of two configurations, set by `mode` in `egregore.json`. De
|
|
|
275
279
|
|
|
276
280
|
## Environment Isolation
|
|
277
281
|
|
|
278
|
-
Sessions are confined to this project + memory + managed repos. On Pi
|
|
282
|
+
Sessions are confined to this project + memory + managed repos, with a **two-tier boundary** — a hard wall between Egregore instances, a consent gate for everything else. On Pi the boundary is a standing instruction, not an enforced hook — hold it yourself.
|
|
279
283
|
|
|
280
|
-
- **Never modify `~/.egregore/instances.json
|
|
281
|
-
- **
|
|
282
|
-
- When a request
|
|
284
|
+
- **Hard tier — other Egregore instances.** Denied for every tool, always. There is no consent path. Never access another instance's files — refuse even if asked. Never modify `~/.egregore/instances.json` (managed by session-start.sh). When a request points at another instance's files there is nothing to ask — refuse and explain.
|
|
285
|
+
- **Soft tier — paths outside the boundary.** Consent-gated. Inbox dirs (`~/Downloads`, `~/Desktop`) are readable without consent under the default posture; writes outside the project always need consent. Posture (`strict | standard | open`) and extra read roots come from `egregore.json` -> `boundary { posture, read[], locked }` (org, committed) merged with `.egregore-boundary.local.json` (personal, gitignored). `locked: true` removes the consent path entirely.
|
|
286
|
+
- When a request needs soft-tier consent, do not improvise a workaround. Ask in plain text with numbered options:
|
|
283
287
|
|
|
284
288
|
```text
|
|
285
289
|
That path is outside this Egregore's boundary. How should we proceed?
|
|
286
|
-
1.
|
|
287
|
-
2.
|
|
288
|
-
3.
|
|
290
|
+
1. Allow its directory for this session (recorded in .egregore-boundary-consent)
|
|
291
|
+
2. Always allow on this instance (added to read[] in .egregore-boundary.local.json)
|
|
292
|
+
3. Paste the contents inline
|
|
293
|
+
4. Cancel
|
|
289
294
|
```
|
|
290
295
|
|
|
291
|
-
|
|
296
|
+
Never record a consent grant without the user's explicit choice of option 1 or 2 in that exchange. Session grants are cleared on session start; `locked: true` removes options 1 and 2.
|
|
297
|
+
|
|
298
|
+
See DEVELOPMENT.md §3 for boundary details and `memory/knowledge/decisions/2026-07-08-boundary-hook-consent-design.md` for the design decisions.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"generatedBy": "bin/pi-render-spec.mjs",
|
|
3
3
|
"source": "AGENTS.md generated Codex spec",
|
|
4
|
-
"sourceHash": "
|
|
5
|
-
"outputHash": "
|
|
4
|
+
"sourceHash": "6ad94ccc38d8",
|
|
5
|
+
"outputHash": "7a3db92a611e",
|
|
6
6
|
"adaptations": [
|
|
7
7
|
"Pi project trust and APPEND_SYSTEM delivery",
|
|
8
8
|
"Pi tool_call branch gate",
|
package/runtime/pi/AGENTS.md
CHANGED
|
@@ -35,7 +35,8 @@ Use the runtime-neutral bridge instead of Claude Code slash commands:
|
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
37
|
bin/agent.sh branch --topic "auth review"
|
|
38
|
-
bin/agent.sh save --message "Save: auth review" --topic "auth review"
|
|
38
|
+
bin/agent.sh save --message "Save: auth review" --topic "auth review" \
|
|
39
|
+
--pr-body "$PR_BODY" # body per .claude/context/pr-format.md (auto-skeleton if omitted)
|
|
39
40
|
bin/agent.sh wrap --from alice --topic "auth review" \
|
|
40
41
|
--summary "Implemented the OAuth callback parser and documented follow-ups." \
|
|
41
42
|
--body "Open threads: review error cases and browser redirects."
|
|
@@ -191,7 +192,7 @@ If the startup card output contains `onboarding_needed`, invoke the `$onboarding
|
|
|
191
192
|
|
|
192
193
|
## Config Files
|
|
193
194
|
|
|
194
|
-
- **`egregore.json`** — committed. Non-secret org config: `org_name`, `github_org`, `memory_repo`, `slug`, `mode`. `api_url` is connected-mode only. **Never put secrets here.**
|
|
195
|
+
- **`egregore.json`** — committed. Non-secret org config: `org_name`, `github_org`, `memory_repo`, `slug`, `mode`. `api_url` is connected-mode only. Optional `boundary { posture, read[], locked }` sets the org's isolation posture (see Environment Isolation). **Never put secrets here.**
|
|
195
196
|
- **`.env`** — gitignored. Personal secrets. Local mode: `GITHUB_TOKEN` only. Connected mode: `GITHUB_TOKEN` + `EGREGORE_API_KEY`. **Never use `source .env`** — use `grep '^KEY=' .env | cut -d'=' -f2-`.
|
|
196
197
|
|
|
197
198
|
In connected mode, infrastructure credentials (Neo4j, Telegram) live on the API server only — `bin/graph.sh` and `bin/notify.sh` route through the API gateway.
|
|
@@ -280,6 +281,10 @@ main ← stable (/release)
|
|
|
280
281
|
- **Memory repo**: stays on main (separate repo, auto-merge).
|
|
281
282
|
- **Never push directly to main or develop.** All changes flow through PRs.
|
|
282
283
|
|
|
284
|
+
### Pull request format (all harnesses)
|
|
285
|
+
|
|
286
|
+
Every PR body follows `.claude/context/pr-format.md`, enforced by the `pr-format` CI check regardless of which harness opened it: `## What` (1–4 bullets) + `## Why` (1–3 sentences) always; `## Verification` when the diff touches non-markdown files (how it was checked, or an honest `Not verified — <reason>`); `## Risk`/`## Links` when real; title `type(scope): imperative summary` (advisory). **Never create a PR with an empty body or `--fill`** — write the body and pass it explicitly (`gh pr create --body`, or `bin/agent.sh save --pr-body` for shell agents; the bridge auto-generates a compliant skeleton only as a last resort).
|
|
287
|
+
|
|
283
288
|
### Managed Repos
|
|
284
289
|
|
|
285
290
|
Repos in `egregore.json` → `repos[]` are cloned as siblings (`../{repo}/`). Each entry can be a string or `{"name": "...", "description": "..."}`. Match user intent to the right repo using `description`. Same branching strategy. Use `git -C` with absolute paths — never `cd` into repos. `$save` scans all managed repos for uncommitted changes.
|
|
@@ -368,19 +373,22 @@ Egregore runs in one of two configurations, set by `mode` in `egregore.json`. De
|
|
|
368
373
|
|
|
369
374
|
## Environment Isolation
|
|
370
375
|
|
|
371
|
-
Sessions are confined to this project + memory + managed repos. On Codex
|
|
376
|
+
Sessions are confined to this project + memory + managed repos, with a **two-tier boundary** — a hard wall between Egregore instances, a consent gate for everything else. On Codex the boundary is a standing instruction, not an enforced hook — hold it yourself.
|
|
372
377
|
|
|
373
|
-
- **Never modify `~/.egregore/instances.json
|
|
374
|
-
- **
|
|
375
|
-
- When a request
|
|
378
|
+
- **Hard tier — other Egregore instances.** Denied for every tool, always. There is no consent path. Never access another instance's files — refuse even if asked. Never modify `~/.egregore/instances.json` (managed by session-start.sh). When a request points at another instance's files there is nothing to ask — refuse and explain.
|
|
379
|
+
- **Soft tier — paths outside the boundary.** Consent-gated. Inbox dirs (`~/Downloads`, `~/Desktop`) are readable without consent under the default posture; writes outside the project always need consent. Posture (`strict | standard | open`) and extra read roots come from `egregore.json` -> `boundary { posture, read[], locked }` (org, committed) merged with `.egregore-boundary.local.json` (personal, gitignored). `locked: true` removes the consent path entirely.
|
|
380
|
+
- When a request needs soft-tier consent, do not improvise a workaround. Ask in plain text with numbered options:
|
|
376
381
|
|
|
377
382
|
```text
|
|
378
383
|
That path is outside this Egregore's boundary. How should we proceed?
|
|
379
|
-
1.
|
|
380
|
-
2.
|
|
381
|
-
3.
|
|
384
|
+
1. Allow its directory for this session (recorded in .egregore-boundary-consent)
|
|
385
|
+
2. Always allow on this instance (added to read[] in .egregore-boundary.local.json)
|
|
386
|
+
3. Paste the contents inline
|
|
387
|
+
4. Cancel
|
|
382
388
|
```
|
|
383
389
|
|
|
384
|
-
|
|
390
|
+
Never record a consent grant without the user's explicit choice of option 1 or 2 in that exchange. Session grants are cleared on session start; `locked: true` removes options 1 and 2.
|
|
391
|
+
|
|
392
|
+
See DEVELOPMENT.md §3 for boundary details and `memory/knowledge/decisions/2026-07-08-boundary-hook-consent-design.md` for the design decisions.
|
|
385
393
|
|
|
386
394
|
<!-- END GENERATED EGREGORE CODEX SPEC -->
|
package/runtime/pi/bin/agent.sh
CHANGED
|
@@ -32,7 +32,10 @@ Runtime-neutral commands:
|
|
|
32
32
|
Hybrid recall over shared memory (qmd)
|
|
33
33
|
people List known people from memory/people
|
|
34
34
|
branch --topic TEXT Create or reuse a task branch/worktree
|
|
35
|
-
save [--message TEXT]
|
|
35
|
+
save [--message TEXT] [--pr-body TEXT|--pr-body-file PATH]
|
|
36
|
+
Commit and push current repo + memory changes;
|
|
37
|
+
PR body follows .claude/context/pr-format.md
|
|
38
|
+
(auto-generated skeleton if not provided)
|
|
36
39
|
wrap --topic T --summary TEXT Write a session wrap to memory/wraps
|
|
37
40
|
handoff --from A --to B --topic T [--intent action|feedback|fyi]
|
|
38
41
|
[--body TEXT|--body-file PATH] [--no-push] [--no-publish] [--no-notify] [--json]
|
|
@@ -332,13 +335,26 @@ ensure_working_branch() {
|
|
|
332
335
|
echo "$branch"
|
|
333
336
|
}
|
|
334
337
|
|
|
338
|
+
build_pr_body() {
|
|
339
|
+
# Compliant fallback PR body (spec: .claude/context/pr-format.md) built
|
|
340
|
+
# from the commit log + diff when the calling agent supplies none.
|
|
341
|
+
local topic="${1:-portable save}" commits stat
|
|
342
|
+
commits="$(git -C "$SCRIPT_DIR" log --pretty='- %s' "origin/develop..HEAD" 2>/dev/null | head -8)"
|
|
343
|
+
[ -n "$commits" ] || commits="- (commit list unavailable — see the Files tab)"
|
|
344
|
+
stat="$(git -C "$SCRIPT_DIR" diff --shortstat "origin/develop...HEAD" 2>/dev/null | sed 's/^ *//')"
|
|
345
|
+
printf '## What\n%s\n\n## Why\nShell-agent session save — topic: %s. Body auto-generated by bin/agent.sh; pass --pr-body for a hand-written description.\n\n## Verification\nNot verified — auto-generated skeleton; review the diff before merging.%s\n\n🤖 Saved via bin/agent.sh\n' \
|
|
346
|
+
"$commits" "$topic" "${stat:+ ($stat)}"
|
|
347
|
+
}
|
|
348
|
+
|
|
335
349
|
cmd_save() {
|
|
336
|
-
local message="" topic="" no_pr=0 branch pushed="false" committed="false" pr_url="" existing_pr="" pr_number="" non_md="" merged="false"
|
|
350
|
+
local message="" topic="" no_pr=0 branch pushed="false" committed="false" pr_url="" existing_pr="" pr_number="" non_md="" merged="false" pr_body="" pr_body_file=""
|
|
337
351
|
|
|
338
352
|
while [ $# -gt 0 ]; do
|
|
339
353
|
case "$1" in
|
|
340
354
|
--message|-m) message="${2:-}"; shift 2 ;;
|
|
341
355
|
--topic) topic="${2:-}"; shift 2 ;;
|
|
356
|
+
--pr-body) pr_body="${2:-}"; shift 2 ;;
|
|
357
|
+
--pr-body-file) pr_body_file="${2:-}"; shift 2 ;;
|
|
342
358
|
--no-pr) no_pr=1; shift ;;
|
|
343
359
|
*) die "unknown save option: $1" ;;
|
|
344
360
|
esac
|
|
@@ -376,7 +392,11 @@ cmd_save() {
|
|
|
376
392
|
if [ -n "$existing_pr" ]; then
|
|
377
393
|
pr_url="$existing_pr"
|
|
378
394
|
else
|
|
379
|
-
|
|
395
|
+
if [ -z "$pr_body" ] && [ -n "$pr_body_file" ] && [ -f "$pr_body_file" ]; then
|
|
396
|
+
pr_body="$(cat "$pr_body_file")"
|
|
397
|
+
fi
|
|
398
|
+
[ -n "$pr_body" ] || pr_body="$(build_pr_body "$topic")"
|
|
399
|
+
pr_url="$(gh pr create --base develop --head "$branch" --title "$message" --body "$pr_body" 2>/dev/null || true)"
|
|
380
400
|
fi
|
|
381
401
|
fi
|
|
382
402
|
|
|
@@ -79,7 +79,8 @@ Use the runtime-neutral bridge instead of Claude Code slash commands:
|
|
|
79
79
|
|
|
80
80
|
\`\`\`bash
|
|
81
81
|
bin/agent.sh branch --topic "auth review"
|
|
82
|
-
bin/agent.sh save --message "Save: auth review" --topic "auth review"
|
|
82
|
+
bin/agent.sh save --message "Save: auth review" --topic "auth review" \\
|
|
83
|
+
--pr-body "$PR_BODY" # body per .claude/context/pr-format.md (auto-skeleton if omitted)
|
|
83
84
|
bin/agent.sh wrap --from alice --topic "auth review" \\
|
|
84
85
|
--summary "Implemented the OAuth callback parser and documented follow-ups." \\
|
|
85
86
|
--body "Open threads: review error cases and browser redirects."
|
|
@@ -251,20 +252,23 @@ Codex has no structured question tool — render each batch as compact numbered
|
|
|
251
252
|
|
|
252
253
|
const LOOM_ROUTING_BODY = `Loom routes commands across model tiers on the Claude Code runtime (\`loom/routes.json\` + \`bin/loom.sh\` + a model-pinned executor subagent). Codex has no subagent delegation — every command runs inline in the current session. Ignore "Loom routing" preambles if you encounter them in a skill spec, and skip \`bin/loom.sh\` calls; \`loom/\` and \`.claude/agents/\` are framework files synced for the Claude runtime.`;
|
|
253
254
|
|
|
254
|
-
const ISOLATION_BODY = `Sessions are confined to this project + memory + managed repos. On Codex
|
|
255
|
+
const ISOLATION_BODY = `Sessions are confined to this project + memory + managed repos, with a **two-tier boundary** — a hard wall between Egregore instances, a consent gate for everything else. On Codex the boundary is a standing instruction, not an enforced hook — hold it yourself.
|
|
255
256
|
|
|
256
|
-
- **Never modify \`~/.egregore/instances.json
|
|
257
|
-
- **
|
|
258
|
-
- When a request
|
|
257
|
+
- **Hard tier — other Egregore instances.** Denied for every tool, always. There is no consent path. Never access another instance's files — refuse even if asked. Never modify \`~/.egregore/instances.json\` (managed by session-start.sh). When a request points at another instance's files there is nothing to ask — refuse and explain.
|
|
258
|
+
- **Soft tier — paths outside the boundary.** Consent-gated. Inbox dirs (\`~/Downloads\`, \`~/Desktop\`) are readable without consent under the default posture; writes outside the project always need consent. Posture (\`strict | standard | open\`) and extra read roots come from \`egregore.json\` -> \`boundary { posture, read[], locked }\` (org, committed) merged with \`.egregore-boundary.local.json\` (personal, gitignored). \`locked: true\` removes the consent path entirely.
|
|
259
|
+
- When a request needs soft-tier consent, do not improvise a workaround. Ask in plain text with numbered options:
|
|
259
260
|
|
|
260
261
|
\`\`\`text
|
|
261
262
|
That path is outside this Egregore's boundary. How should we proceed?
|
|
262
|
-
1.
|
|
263
|
-
2.
|
|
264
|
-
3.
|
|
263
|
+
1. Allow its directory for this session (recorded in .egregore-boundary-consent)
|
|
264
|
+
2. Always allow on this instance (added to read[] in .egregore-boundary.local.json)
|
|
265
|
+
3. Paste the contents inline
|
|
266
|
+
4. Cancel
|
|
265
267
|
\`\`\`
|
|
266
268
|
|
|
267
|
-
|
|
269
|
+
Never record a consent grant without the user's explicit choice of option 1 or 2 in that exchange. Session grants are cleared on session start; \`locked: true\` removes options 1 and 2.
|
|
270
|
+
|
|
271
|
+
See DEVELOPMENT.md §3 for boundary details and \`memory/knowledge/decisions/2026-07-08-boundary-hook-consent-design.md\` for the design decisions.`;
|
|
268
272
|
|
|
269
273
|
// ── per-section rules ────────────────────────────────────────────────────
|
|
270
274
|
const RULES = {
|
|
@@ -326,7 +330,7 @@ const RULES = {
|
|
|
326
330
|
'Environment Isolation': {
|
|
327
331
|
action: 'replace',
|
|
328
332
|
body: ISOLATION_BODY,
|
|
329
|
-
note: 'boundary-check is a Claude PreToolUse hook with no .codex equivalent —
|
|
333
|
+
note: 'boundary-check is a Claude PreToolUse hook with no .codex equivalent — two-tier model stated as a standing instruction; AskUserQuestion remediation -> numbered options, now including the two consent-grant options (session grant -> .egregore-boundary-consent, instance grant -> .egregore-boundary.local.json read[]).',
|
|
330
334
|
},
|
|
331
335
|
};
|
|
332
336
|
|
|
@@ -6,7 +6,7 @@ set -o pipefail
|
|
|
6
6
|
# libraries as Claude's SessionStart, then renders a terminal greeting before
|
|
7
7
|
# Codex itself starts.
|
|
8
8
|
|
|
9
|
-
FRAMEWORK_VERSION="
|
|
9
|
+
FRAMEWORK_VERSION="7"
|
|
10
10
|
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
11
11
|
cd "$SCRIPT_DIR" || exit 1
|
|
12
12
|
|
|
@@ -21,6 +21,9 @@ fi
|
|
|
21
21
|
# Clear any branch-guard consent from a previous session — consent to write on
|
|
22
22
|
# a protected branch is asked fresh each session (see AGENTS.md branch-guard protocol).
|
|
23
23
|
rm -f "$SCRIPT_DIR/.egregore-branch-consent" "$MAIN_PROJECT_DIR/.egregore-branch-consent" 2>/dev/null
|
|
24
|
+
# Same for boundary-crossing consent — grants are session-scoped (AGENTS.md
|
|
25
|
+
# environment-isolation protocol); durable grants belong in .egregore-boundary.local.json.
|
|
26
|
+
rm -f "$SCRIPT_DIR/.egregore-boundary-consent" "$MAIN_PROJECT_DIR/.egregore-boundary-consent" 2>/dev/null
|
|
24
27
|
|
|
25
28
|
if [ -f "$SCRIPT_DIR/bin/lib/worktree-links.sh" ]; then
|
|
26
29
|
source "$SCRIPT_DIR/bin/lib/worktree-links.sh" >/dev/null 2>/dev/null || true
|