create-egregore 0.19.11 → 0.19.13
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 +432 -6
- package/bin/cli.js +18 -0
- 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.19.13
|
|
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,25 @@ 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.19.13";
|
|
35
37
|
const REGISTRY = path.join(os.homedir(), ".egregore", "instances.json");
|
|
36
38
|
const DEFAULT_FILE = path.join(os.homedir(), ".egregore", "default");
|
|
39
|
+
const UPDATE_CACHE_FILE = path.join(os.homedir(), ".egregore", "update-check.json");
|
|
37
40
|
const PUBLIC_NPX_PACKAGE = "create-egregore";
|
|
41
|
+
const UPDATE_REGISTRY_URL =
|
|
42
|
+
process.env.EGREGORE_UPDATE_REGISTRY_URL ||
|
|
43
|
+
`https://registry.npmjs.org/${PUBLIC_NPX_PACKAGE}/latest`;
|
|
44
|
+
const UPDATE_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
45
|
+
const UPDATE_FAILURE_TTL_MS = 30 * 60 * 1000;
|
|
46
|
+
const UPDATE_CHECK_TIMEOUT_MS = 1500;
|
|
47
|
+
const CONNECT_API_URL = (
|
|
48
|
+
process.env.EGREGORE_CONNECT_API_URL ||
|
|
49
|
+
"https://egregore-production-55f2.up.railway.app"
|
|
50
|
+
).replace(/\/+$/, "");
|
|
38
51
|
|
|
39
52
|
const NATIVE_CODEX_SKILLS = [
|
|
40
53
|
"activity",
|
|
@@ -864,6 +877,7 @@ function cmdHelp() {
|
|
|
864
877
|
` ${CYAN}egregore browse${RESET} force the full-screen browser`,
|
|
865
878
|
` ${CYAN}egregore ls${RESET} ${DIM}| list${RESET} list installed instances`,
|
|
866
879
|
` ${CYAN}egregore use${RESET} ${DIM}<slug>${RESET} set the default for bare ${BOLD}egregore${RESET}`,
|
|
880
|
+
` ${CYAN}egregore connect${RESET} ${DIM}[slug]${RESET} activate Connected for an existing instance`,
|
|
867
881
|
` ${CYAN}egregore /invite${RESET} ${DIM}<user> [slug]${RESET} invite a GitHub user`,
|
|
868
882
|
` ${CYAN}egregore settings${RESET} ${DIM}[slug]${RESET} hosting on/off, repos, admins`,
|
|
869
883
|
` ${CYAN}egregore prune${RESET} ${DIM}[--dry-run]${RESET} remove entries with missing paths`,
|
|
@@ -874,6 +888,7 @@ function cmdHelp() {
|
|
|
874
888
|
` ${DIM}enter${RESET} choose selected instance`,
|
|
875
889
|
` ${DIM}/${RESET} filter (type to narrow; esc to clear)`,
|
|
876
890
|
` ${DIM}n / +${RESET} create a new Egregore`,
|
|
891
|
+
` ${DIM}c${RESET} connect selected instance`,
|
|
877
892
|
` ${DIM}u${RESET} set selected as default`,
|
|
878
893
|
` ${DIM}i${RESET} invite a GitHub user to selected instance`,
|
|
879
894
|
` ${DIM}s${RESET} settings (hosting on/off, repos, admins)`,
|
|
@@ -968,11 +983,145 @@ function requestJson(options, body) {
|
|
|
968
983
|
});
|
|
969
984
|
});
|
|
970
985
|
req.on("error", (err) => resolve({ status: 0, data: { message: err.message } }));
|
|
986
|
+
if (options.unref) {
|
|
987
|
+
req.on("socket", (socket) => socket.unref());
|
|
988
|
+
}
|
|
989
|
+
if (options.timeoutMs) {
|
|
990
|
+
req.setTimeout(options.timeoutMs, () => req.destroy(new Error("request timed out")));
|
|
991
|
+
}
|
|
971
992
|
if (payload) req.write(payload);
|
|
972
993
|
req.end();
|
|
973
994
|
});
|
|
974
995
|
}
|
|
975
996
|
|
|
997
|
+
function parseReleaseVersion(value) {
|
|
998
|
+
const match = String(value || "").trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
|
|
999
|
+
if (!match) return null;
|
|
1000
|
+
return {
|
|
1001
|
+
parts: match.slice(1, 4).map(Number),
|
|
1002
|
+
prerelease: match[4] || "",
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function compareReleaseVersions(a, b) {
|
|
1007
|
+
const left = parseReleaseVersion(a);
|
|
1008
|
+
const right = parseReleaseVersion(b);
|
|
1009
|
+
if (!left || !right) return 0;
|
|
1010
|
+
for (let i = 0; i < 3; i++) {
|
|
1011
|
+
if (left.parts[i] !== right.parts[i]) return left.parts[i] > right.parts[i] ? 1 : -1;
|
|
1012
|
+
}
|
|
1013
|
+
if (left.prerelease === right.prerelease) return 0;
|
|
1014
|
+
if (!left.prerelease) return 1;
|
|
1015
|
+
if (!right.prerelease) return -1;
|
|
1016
|
+
return left.prerelease.localeCompare(right.prerelease);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function cliUpdateState(latest) {
|
|
1020
|
+
if (!parseReleaseVersion(latest)) return null;
|
|
1021
|
+
return {
|
|
1022
|
+
current: VERSION,
|
|
1023
|
+
latest,
|
|
1024
|
+
available: compareReleaseVersions(latest, VERSION) > 0,
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function readUpdateCache() {
|
|
1029
|
+
try {
|
|
1030
|
+
const cache = JSON.parse(fs.readFileSync(UPDATE_CACHE_FILE, "utf-8"));
|
|
1031
|
+
if (!cache || !cache.checked_at) return null;
|
|
1032
|
+
const age = Date.now() - new Date(cache.checked_at).getTime();
|
|
1033
|
+
const ttl = cache.latest ? UPDATE_CACHE_TTL_MS : UPDATE_FAILURE_TTL_MS;
|
|
1034
|
+
if (!Number.isFinite(age) || age < 0 || age > ttl) return null;
|
|
1035
|
+
if (!cache.latest) return { current: VERSION, latest: null, available: false };
|
|
1036
|
+
return cliUpdateState(cache.latest);
|
|
1037
|
+
} catch {
|
|
1038
|
+
return null;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function writeUpdateCache(latest) {
|
|
1043
|
+
try {
|
|
1044
|
+
fs.mkdirSync(path.dirname(UPDATE_CACHE_FILE), { recursive: true });
|
|
1045
|
+
atomicWrite(
|
|
1046
|
+
UPDATE_CACHE_FILE,
|
|
1047
|
+
JSON.stringify({ checked_at: new Date().toISOString(), latest: latest || null }, null, 2) + "\n",
|
|
1048
|
+
0o600,
|
|
1049
|
+
);
|
|
1050
|
+
} catch {}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
async function checkForCliUpdate() {
|
|
1054
|
+
if (process.env.EGREGORE_DISABLE_UPDATE_CHECK === "1") return null;
|
|
1055
|
+
if (process.env.EGREGORE_UPDATE_LATEST_VERSION) {
|
|
1056
|
+
return cliUpdateState(process.env.EGREGORE_UPDATE_LATEST_VERSION);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
const cached = readUpdateCache();
|
|
1060
|
+
if (cached) return cached;
|
|
1061
|
+
|
|
1062
|
+
try {
|
|
1063
|
+
const url = new URL(UPDATE_REGISTRY_URL);
|
|
1064
|
+
const result = await requestJson({
|
|
1065
|
+
hostname: url.hostname,
|
|
1066
|
+
port: url.port || undefined,
|
|
1067
|
+
path: `${url.pathname}${url.search}`,
|
|
1068
|
+
method: "GET",
|
|
1069
|
+
protocol: url.protocol,
|
|
1070
|
+
timeoutMs: UPDATE_CHECK_TIMEOUT_MS,
|
|
1071
|
+
unref: true,
|
|
1072
|
+
headers: { Accept: "application/json" },
|
|
1073
|
+
});
|
|
1074
|
+
const latest = result.status === 200 && result.data && result.data.version;
|
|
1075
|
+
const update = cliUpdateState(latest);
|
|
1076
|
+
writeUpdateCache(update?.latest || null);
|
|
1077
|
+
return update;
|
|
1078
|
+
} catch {
|
|
1079
|
+
return null;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function cmdCliUpdate(latest) {
|
|
1084
|
+
if (!parseReleaseVersion(latest)) {
|
|
1085
|
+
console.error(`\n ${YELLOW}!${RESET} Could not verify the CLI release version.\n`);
|
|
1086
|
+
process.exitCode = 1;
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
console.log("");
|
|
1091
|
+
console.log(` ${BOLD}${CYAN}↑ CLI UPDATE${RESET}`);
|
|
1092
|
+
console.log(` ${DIM}${VERSION} → ${latest}${RESET}`);
|
|
1093
|
+
console.log("");
|
|
1094
|
+
process.stdout.write(` [1/2] Installing ${PUBLIC_NPX_PACKAGE}@${latest}…`);
|
|
1095
|
+
const result = spawnSync(
|
|
1096
|
+
"npx",
|
|
1097
|
+
["-y", `${PUBLIC_NPX_PACKAGE}@${latest}`, "--refresh-launcher"],
|
|
1098
|
+
{ encoding: "utf-8", stdio: ["inherit", "pipe", "pipe"], env: { ...process.env } },
|
|
1099
|
+
);
|
|
1100
|
+
if (result.status !== 0) {
|
|
1101
|
+
console.log(`\r [1/2] ${YELLOW}!${RESET} Install failed`);
|
|
1102
|
+
const detail = String(result.stderr || result.stdout || "").trim().split("\n").filter(Boolean).pop();
|
|
1103
|
+
if (detail) console.error(` ${DIM}${detail}${RESET}`);
|
|
1104
|
+
console.error(` Retry: npx -y ${PUBLIC_NPX_PACKAGE}@${latest} --refresh-launcher\n`);
|
|
1105
|
+
process.exitCode = 1;
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
console.log(`\r [1/2] ${GREEN}✓${RESET} CLI ${latest} installed`);
|
|
1109
|
+
|
|
1110
|
+
if (process.env.EGREGORE_UPDATE_NO_RELAUNCH === "1") {
|
|
1111
|
+
console.log(` [2/2] ${GREEN}✓${RESET} Ready for the next launch\n`);
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
console.log(` [2/2] ${GREEN}✓${RESET} Reopening Egregore\n`);
|
|
1116
|
+
const relaunched = spawnSync(process.execPath, [process.argv[1], "browse"], {
|
|
1117
|
+
stdio: "inherit",
|
|
1118
|
+
env: { ...process.env },
|
|
1119
|
+
});
|
|
1120
|
+
if (relaunched.error) {
|
|
1121
|
+
console.error(` ${YELLOW}!${RESET} Updated successfully. Run ${BOLD}egregore${RESET} to reopen.\n`);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
976
1125
|
function githubRequest(token, method, apiPath, body) {
|
|
977
1126
|
return requestJson({
|
|
978
1127
|
hostname: "api.github.com",
|
|
@@ -994,6 +1143,252 @@ function apiRequest(apiUrl, apiKey, apiPath, body) {
|
|
|
994
1143
|
}, body);
|
|
995
1144
|
}
|
|
996
1145
|
|
|
1146
|
+
function connectedApiRequest(githubToken, method, apiPath, body, headers = {}) {
|
|
1147
|
+
const url = new URL(apiPath, CONNECT_API_URL + "/");
|
|
1148
|
+
return requestJson({
|
|
1149
|
+
hostname: url.hostname,
|
|
1150
|
+
port: url.port || undefined,
|
|
1151
|
+
path: `${url.pathname}${url.search}`,
|
|
1152
|
+
method,
|
|
1153
|
+
protocol: url.protocol,
|
|
1154
|
+
headers: {
|
|
1155
|
+
Authorization: `Bearer ${githubToken}`,
|
|
1156
|
+
...headers,
|
|
1157
|
+
},
|
|
1158
|
+
}, body);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function openExternalUrl(url) {
|
|
1162
|
+
try {
|
|
1163
|
+
if (process.platform === "darwin") {
|
|
1164
|
+
spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
1165
|
+
} else if (process.platform === "win32") {
|
|
1166
|
+
spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref();
|
|
1167
|
+
} else {
|
|
1168
|
+
spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
1169
|
+
}
|
|
1170
|
+
return true;
|
|
1171
|
+
} catch {
|
|
1172
|
+
return false;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function atomicWrite(file, content, mode) {
|
|
1177
|
+
const temp = path.join(
|
|
1178
|
+
path.dirname(file),
|
|
1179
|
+
`.${path.basename(file)}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`,
|
|
1180
|
+
);
|
|
1181
|
+
fs.writeFileSync(temp, content, { mode });
|
|
1182
|
+
fs.renameSync(temp, file);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function writeJsonFileAtomic(file, value) {
|
|
1186
|
+
atomicWrite(file, JSON.stringify(value, null, 2) + "\n", 0o644);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function writeEnvValueAtomic(file, key, value) {
|
|
1190
|
+
const lines = fs.existsSync(file)
|
|
1191
|
+
? fs.readFileSync(file, "utf-8").split(/\r?\n/)
|
|
1192
|
+
: [];
|
|
1193
|
+
let replaced = false;
|
|
1194
|
+
const next = lines
|
|
1195
|
+
.filter((line, index) => !(index === lines.length - 1 && line === ""))
|
|
1196
|
+
.map((line) => {
|
|
1197
|
+
if (!line.startsWith(`${key}=`)) return line;
|
|
1198
|
+
replaced = true;
|
|
1199
|
+
return `${key}=${value}`;
|
|
1200
|
+
});
|
|
1201
|
+
if (!replaced) next.push(`${key}=${value}`);
|
|
1202
|
+
atomicWrite(file, next.join("\n") + "\n", 0o600);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function persistConnectState(inst, fields) {
|
|
1206
|
+
const file = path.join(inst.path, ".egregore-state.json");
|
|
1207
|
+
const state = readJsonFile(file);
|
|
1208
|
+
state.connected_activation = {
|
|
1209
|
+
...(state.connected_activation || {}),
|
|
1210
|
+
...fields,
|
|
1211
|
+
updated_at: new Date().toISOString(),
|
|
1212
|
+
};
|
|
1213
|
+
writeJsonFileAtomic(file, state);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function connectError(result, fallback) {
|
|
1217
|
+
if (!result) return fallback;
|
|
1218
|
+
const data = result.data || {};
|
|
1219
|
+
return data.detail || data.message || (typeof data === "string" ? data : fallback);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function sleep(ms) {
|
|
1223
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
async function cmdConnect(args) {
|
|
1227
|
+
const registry = readRegistry();
|
|
1228
|
+
if (registry.length === 0) die("no egregores installed yet.\n run: npx -y create-egregore@latest");
|
|
1229
|
+
const inst = resolveInviteInstance(registry, args[0] || "");
|
|
1230
|
+
if (!inst) {
|
|
1231
|
+
die("multiple egregores installed; choose one:\n egregore connect <slug>\n run: egregore ls");
|
|
1232
|
+
}
|
|
1233
|
+
if (!fs.existsSync(inst.path)) die(`instance path is missing: ${inst.path}`);
|
|
1234
|
+
|
|
1235
|
+
const { cfg, state, env } = configForInstance(inst);
|
|
1236
|
+
const githubToken = env.GITHUB_TOKEN || "";
|
|
1237
|
+
if (!githubToken) {
|
|
1238
|
+
die(`GITHUB_TOKEN is missing for ${inst.slug}.\n Add it to ${path.join(inst.path, ".env")} and retry.`);
|
|
1239
|
+
}
|
|
1240
|
+
const activation = state.connected_activation || {};
|
|
1241
|
+
if (cfg.mode === "connected" && activation.status === "connected") {
|
|
1242
|
+
console.log(`\n ${GREEN}✓${RESET} ${BOLD}${inst.slug}${RESET} is already connected.\n`);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
console.log("");
|
|
1247
|
+
console.log(` ${BOLD}CONNECT${RESET} ${DIM}${inst.slug}${RESET}`);
|
|
1248
|
+
console.log(` ${DIM}€500/month · Stripe checkout · existing Markdown and Git stay in place${RESET}`);
|
|
1249
|
+
console.log("");
|
|
1250
|
+
|
|
1251
|
+
let intentId = activation.intent_id || "";
|
|
1252
|
+
let connectUrl = activation.connect_url || "";
|
|
1253
|
+
if (!intentId || !["checkout_pending", "entitled", "backfilling"].includes(activation.status)) {
|
|
1254
|
+
console.log(` [1/5] ${DIM}Creating connection intent…${RESET}`);
|
|
1255
|
+
const checkout = await connectedApiRequest(
|
|
1256
|
+
githubToken,
|
|
1257
|
+
"POST",
|
|
1258
|
+
"/api/billing/checkout",
|
|
1259
|
+
{
|
|
1260
|
+
org_slug: cfg.slug || inst.slug,
|
|
1261
|
+
plan: "connect",
|
|
1262
|
+
instance_metadata: {
|
|
1263
|
+
launcher_version: VERSION,
|
|
1264
|
+
mode: cfg.mode || "local",
|
|
1265
|
+
repo_name: cfg.repo_name || "",
|
|
1266
|
+
managed_repo_count: Array.isArray(cfg.repos) ? cfg.repos.length : 0,
|
|
1267
|
+
},
|
|
1268
|
+
},
|
|
1269
|
+
{ "X-Idempotency-Key": `cli:${inst.slug}:${crypto.randomUUID()}` },
|
|
1270
|
+
);
|
|
1271
|
+
if (checkout.status !== 200) {
|
|
1272
|
+
die(`could not start Connected checkout: ${connectError(checkout, `HTTP ${checkout.status}`)}`);
|
|
1273
|
+
}
|
|
1274
|
+
intentId = checkout.data.connection_intent_id;
|
|
1275
|
+
connectUrl = checkout.data.connect_url;
|
|
1276
|
+
persistConnectState(inst, {
|
|
1277
|
+
intent_id: intentId,
|
|
1278
|
+
checkout_session_id: checkout.data.checkout_session_id,
|
|
1279
|
+
connect_url: connectUrl,
|
|
1280
|
+
status: "checkout_pending",
|
|
1281
|
+
});
|
|
1282
|
+
console.log(` [1/5] ${GREEN}✓${RESET} Connection intent`);
|
|
1283
|
+
} else {
|
|
1284
|
+
console.log(` [1/5] ${GREEN}✓${RESET} Resuming connection intent`);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
if (activation.status !== "backfilling") {
|
|
1288
|
+
if (connectUrl) {
|
|
1289
|
+
const opened = openExternalUrl(connectUrl);
|
|
1290
|
+
const mark = opened ? `${GREEN}✓${RESET}` : `${YELLOW}!${RESET}`;
|
|
1291
|
+
console.log(` [2/5] ${mark} ${opened ? "Opened" : "Open"} Egregore Connect`);
|
|
1292
|
+
console.log(` ${CYAN}${connectUrl}${RESET}`);
|
|
1293
|
+
}
|
|
1294
|
+
console.log(` ${DIM}Complete checkout in the browser. Promotion codes are entered in Stripe.${RESET}`);
|
|
1295
|
+
} else {
|
|
1296
|
+
console.log(` [2/5] ${GREEN}✓${RESET} Subscription already confirmed`);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
const pollMs = Math.max(100, Number(process.env.EGREGORE_CONNECT_POLL_INTERVAL_MS || 2000));
|
|
1300
|
+
const timeoutMs = Math.max(pollMs, Number(process.env.EGREGORE_CONNECT_TIMEOUT_MS || 30 * 60 * 1000));
|
|
1301
|
+
const deadline = Date.now() + timeoutMs;
|
|
1302
|
+
let status;
|
|
1303
|
+
while (Date.now() < deadline) {
|
|
1304
|
+
const result = await connectedApiRequest(
|
|
1305
|
+
githubToken,
|
|
1306
|
+
"GET",
|
|
1307
|
+
`/api/billing/connect/${encodeURIComponent(intentId)}`,
|
|
1308
|
+
);
|
|
1309
|
+
if (result.status === 200) {
|
|
1310
|
+
status = result.data;
|
|
1311
|
+
if (
|
|
1312
|
+
status.entitled ||
|
|
1313
|
+
["entitled", "provisioning", "backfilling", "connected"].includes(status.status)
|
|
1314
|
+
) break;
|
|
1315
|
+
if (["canceled", "failed"].includes(status.status)) {
|
|
1316
|
+
persistConnectState(inst, { status: status.status });
|
|
1317
|
+
die(`Connected checkout ${status.status}. Your local Egregore was not changed.`);
|
|
1318
|
+
}
|
|
1319
|
+
} else if (result.status !== 0) {
|
|
1320
|
+
die(`could not read Connected status: ${connectError(result, `HTTP ${result.status}`)}`);
|
|
1321
|
+
}
|
|
1322
|
+
await sleep(pollMs);
|
|
1323
|
+
}
|
|
1324
|
+
if (!status || !(status.entitled || ["entitled", "provisioning", "backfilling", "connected"].includes(status.status))) {
|
|
1325
|
+
die("timed out waiting for checkout. Run the same command to resume.");
|
|
1326
|
+
}
|
|
1327
|
+
console.log(` [3/5] ${GREEN}✓${RESET} Subscription confirmed`);
|
|
1328
|
+
persistConnectState(inst, { status: "entitled" });
|
|
1329
|
+
|
|
1330
|
+
const claim = await connectedApiRequest(
|
|
1331
|
+
githubToken,
|
|
1332
|
+
"POST",
|
|
1333
|
+
`/api/billing/connect/${encodeURIComponent(intentId)}/claim`,
|
|
1334
|
+
{},
|
|
1335
|
+
);
|
|
1336
|
+
if (claim.status !== 200) {
|
|
1337
|
+
die(`could not activate Connected infrastructure: ${connectError(claim, `HTTP ${claim.status}`)}`);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
const configFile = path.join(inst.path, "egregore.json");
|
|
1341
|
+
const envFile = path.join(inst.path, ".env");
|
|
1342
|
+
const currentConfig = readJsonFile(configFile);
|
|
1343
|
+
writeJsonFileAtomic(configFile, {
|
|
1344
|
+
...currentConfig,
|
|
1345
|
+
mode: "connected",
|
|
1346
|
+
api_url: claim.data.api_url,
|
|
1347
|
+
});
|
|
1348
|
+
writeEnvValueAtomic(envFile, "EGREGORE_API_KEY", claim.data.api_key);
|
|
1349
|
+
persistConnectState(inst, {
|
|
1350
|
+
status: "backfilling",
|
|
1351
|
+
activation_protocol: claim.data.activation_protocol,
|
|
1352
|
+
});
|
|
1353
|
+
console.log(` [4/5] ${GREEN}✓${RESET} Connected credential and graph node`);
|
|
1354
|
+
|
|
1355
|
+
const syncScript = path.join(inst.path, "bin", "sync-graph.sh");
|
|
1356
|
+
if (!fs.existsSync(syncScript)) {
|
|
1357
|
+
die("bin/sync-graph.sh is missing. Update the installed Egregore version, then run egregore connect again.");
|
|
1358
|
+
}
|
|
1359
|
+
const sync = spawnSync("bash", [syncScript], {
|
|
1360
|
+
cwd: inst.path,
|
|
1361
|
+
encoding: "utf-8",
|
|
1362
|
+
stdio: "pipe",
|
|
1363
|
+
timeout: 10 * 60 * 1000,
|
|
1364
|
+
});
|
|
1365
|
+
if (sync.status !== 0) {
|
|
1366
|
+
const detail = (sync.stderr || sync.stdout || "graph replay failed").trim();
|
|
1367
|
+
die(`Connected is configured, but graph replay did not finish.\n ${detail}\n Run egregore connect ${inst.slug} to retry; existing data is unchanged.`);
|
|
1368
|
+
}
|
|
1369
|
+
let backfillSummary = { replayed: true };
|
|
1370
|
+
try { backfillSummary = JSON.parse((sync.stdout || "").trim()); } catch {}
|
|
1371
|
+
|
|
1372
|
+
const complete = await connectedApiRequest(
|
|
1373
|
+
githubToken,
|
|
1374
|
+
"POST",
|
|
1375
|
+
`/api/billing/connect/${encodeURIComponent(intentId)}/complete`,
|
|
1376
|
+
{ backfill_summary: backfillSummary },
|
|
1377
|
+
);
|
|
1378
|
+
if (complete.status !== 200) {
|
|
1379
|
+
die(`graph replay finished, but activation could not be finalized: ${connectError(complete, `HTTP ${complete.status}`)}`);
|
|
1380
|
+
}
|
|
1381
|
+
persistConnectState(inst, {
|
|
1382
|
+
status: "connected",
|
|
1383
|
+
connected_at: new Date().toISOString(),
|
|
1384
|
+
backfill_summary: backfillSummary,
|
|
1385
|
+
});
|
|
1386
|
+
console.log(` [5/5] ${GREEN}✓${RESET} Existing memory projected into the graph`);
|
|
1387
|
+
console.log("");
|
|
1388
|
+
console.log(` ${GREEN}✓${RESET} ${BOLD}${inst.slug}${RESET} is connected. Local files remain the source of truth.`);
|
|
1389
|
+
console.log("");
|
|
1390
|
+
}
|
|
1391
|
+
|
|
997
1392
|
function accessLabel(status) {
|
|
998
1393
|
if (status === 201) return `${GREEN}sent${RESET}`;
|
|
999
1394
|
if (status === 204) return `${GREEN}already added${RESET}`;
|
|
@@ -1435,7 +1830,7 @@ function selectedWorkflowLine(pv, width) {
|
|
|
1435
1830
|
return joinShort(available.map(skillToken), Math.max(12, width));
|
|
1436
1831
|
}
|
|
1437
1832
|
|
|
1438
|
-
function renderHeroPanel({ selected, pv, width, height, countSuffix, filter, filterMode }) {
|
|
1833
|
+
function renderHeroPanel({ selected, pv, width, height, countSuffix, filter, filterMode, cliUpdate }) {
|
|
1439
1834
|
const outerWidth = Math.min(Math.max(50, width - 2), 126);
|
|
1440
1835
|
const innerWidth = Math.max(48, outerWidth - 2);
|
|
1441
1836
|
const bodyWidth = Math.max(8, innerWidth - 2);
|
|
@@ -1487,7 +1882,17 @@ function renderHeroPanel({ selected, pv, width, height, countSuffix, filter, fil
|
|
|
1487
1882
|
lines.push(mid);
|
|
1488
1883
|
lines.push(boxLine(`${BOLD}${title}${RESET} ${DIM}· ${org} · ${runtime} · ${countSuffix} · ${filterText}${RESET}`, innerWidth, margin));
|
|
1489
1884
|
lines.push(boxLine(`${DIM}skills${RESET} ${skillStatus}`, innerWidth, margin));
|
|
1490
|
-
|
|
1885
|
+
if (cliUpdate?.available) {
|
|
1886
|
+
lines.push(boxLine(
|
|
1887
|
+
`${YELLOW}→ CLI update ${cliUpdate.latest} available${RESET} ${DIM}· installed ${cliUpdate.current} · ${RESET}${BOLD}U${RESET}${DIM} install${RESET}`,
|
|
1888
|
+
innerWidth,
|
|
1889
|
+
margin,
|
|
1890
|
+
));
|
|
1891
|
+
} else {
|
|
1892
|
+
// Reserve the status row so the list never jumps when the async version
|
|
1893
|
+
// check resolves and reveals an available release.
|
|
1894
|
+
lines.push(emptyBoxLine(innerWidth, margin));
|
|
1895
|
+
}
|
|
1491
1896
|
|
|
1492
1897
|
const sigilWidth = Math.max(...EGREGORE_SIGIL.map((line) => line.length));
|
|
1493
1898
|
const twoColumn = bodyWidth >= 68;
|
|
@@ -1684,6 +2089,7 @@ async function browser(args = {}) {
|
|
|
1684
2089
|
message: null, // ephemeral info line (e.g. "default set to X")
|
|
1685
2090
|
messageKind: "info", // "info" | "success" | "warn"
|
|
1686
2091
|
messageUntil: 0,
|
|
2092
|
+
cliUpdate: null,
|
|
1687
2093
|
};
|
|
1688
2094
|
|
|
1689
2095
|
const newAction = {
|
|
@@ -1787,6 +2193,7 @@ async function browser(args = {}) {
|
|
|
1787
2193
|
countSuffix,
|
|
1788
2194
|
filter: state.filter,
|
|
1789
2195
|
filterMode: state.filterMode,
|
|
2196
|
+
cliUpdate: state.cliUpdate,
|
|
1790
2197
|
});
|
|
1791
2198
|
for (const line of heroLines) out.push(line + ANSI.clearLine + "\n");
|
|
1792
2199
|
out.push(ANSI.clearLine + "\n");
|
|
@@ -1952,8 +2359,8 @@ async function browser(args = {}) {
|
|
|
1952
2359
|
footer = band(`● ${stripAnsi(state.message)}`, bg);
|
|
1953
2360
|
} else {
|
|
1954
2361
|
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);
|
|
2362
|
+
? band(`${key("↑↓", "nav")} ${key("enter", "select")} ${key("n/+", "new")} ${key("c", "connect")} ${key("i", "invite")} ${key("d", "delete")} ${key("?", "help")} ${key("q", "quit")}`, 46)
|
|
2363
|
+
: 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
2364
|
}
|
|
1958
2365
|
out.push(M + footer + "\n");
|
|
1959
2366
|
if (state.showHelp) {
|
|
@@ -2310,6 +2717,19 @@ async function browser(args = {}) {
|
|
|
2310
2717
|
resolve(setupNewEgregore(args.runtimeOverride));
|
|
2311
2718
|
return;
|
|
2312
2719
|
}
|
|
2720
|
+
if (chunk === "c") {
|
|
2721
|
+
const pick = visible()[state.cursor]?.e;
|
|
2722
|
+
if (!pick) return;
|
|
2723
|
+
cleanup();
|
|
2724
|
+
resolve(cmdConnect([pick.slug]));
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
if (chunk === "U" && state.cliUpdate?.available) {
|
|
2728
|
+
const latest = state.cliUpdate.latest;
|
|
2729
|
+
cleanup();
|
|
2730
|
+
resolve(cmdCliUpdate(latest));
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2313
2733
|
if (chunk === "i") {
|
|
2314
2734
|
const pick = visible()[state.cursor]?.e;
|
|
2315
2735
|
if (!pick) return;
|
|
@@ -2396,6 +2816,11 @@ async function browser(args = {}) {
|
|
|
2396
2816
|
});
|
|
2397
2817
|
|
|
2398
2818
|
render();
|
|
2819
|
+
checkForCliUpdate().then((update) => {
|
|
2820
|
+
if (done || !update) return;
|
|
2821
|
+
state.cliUpdate = update;
|
|
2822
|
+
if (update.available) render();
|
|
2823
|
+
});
|
|
2399
2824
|
});
|
|
2400
2825
|
}
|
|
2401
2826
|
|
|
@@ -2451,6 +2876,7 @@ function main() {
|
|
|
2451
2876
|
// Non-interactive subcommands
|
|
2452
2877
|
if (cmd === "ls" || cmd === "list") return cmdLs();
|
|
2453
2878
|
if (cmd === "use") return cmdUse(args[1]);
|
|
2879
|
+
if (cmd === "connect") return cmdConnect(args.slice(1));
|
|
2454
2880
|
if (cmd === "/invite" || cmd === "invite") return cmdInvite(args.slice(1));
|
|
2455
2881
|
if (cmd === "settings" || cmd === "config") return cmdSettings(args.slice(1));
|
|
2456
2882
|
if (cmd === "prune") return cmdPruneNonInteractive(args[1] === "--dry-run" || args[1] === "-n");
|
package/bin/cli.js
CHANGED
|
@@ -223,6 +223,8 @@ function parseArgs(argv) {
|
|
|
223
223
|
// already served as the entry screen — a second alt-screen welcome
|
|
224
224
|
// would otherwise demand a redundant Enter and churn the alt buffer.
|
|
225
225
|
args.noWelcome = true;
|
|
226
|
+
} else if (argv[i] === "--refresh-launcher") {
|
|
227
|
+
args.refreshLauncher = true;
|
|
226
228
|
} else if (argv[i] === "--codex") {
|
|
227
229
|
args.runtime = "codex";
|
|
228
230
|
} else if (argv[i] === "--pi") {
|
|
@@ -395,6 +397,7 @@ function showHelp() {
|
|
|
395
397
|
ui.info(" --force Skip the in-repo detection check");
|
|
396
398
|
ui.info(" --no-autolaunch Don't launch the harness at the end (for CI)");
|
|
397
399
|
ui.info(" --no-screens Skip the full-screen welcome/ready bookends");
|
|
400
|
+
ui.info(" --refresh-launcher Refresh only the Egregore CLI, then exit");
|
|
398
401
|
ui.info(" -h, --help Show this help");
|
|
399
402
|
ui.info("");
|
|
400
403
|
}
|
|
@@ -443,6 +446,21 @@ async function main() {
|
|
|
443
446
|
}
|
|
444
447
|
}
|
|
445
448
|
|
|
449
|
+
if (args.refreshLauncher) {
|
|
450
|
+
const instances = readLocalInstances();
|
|
451
|
+
if (instances.length === 0) {
|
|
452
|
+
ui.error("No installed Egregore instance was found. Run create-egregore without --refresh-launcher.");
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
const alias = await installLauncherBinary(instances[0].path, ui);
|
|
456
|
+
if (!alias || !alias.binaryPath) {
|
|
457
|
+
ui.error("Could not refresh the Egregore CLI.");
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
ui.success(`Egregore CLI is current at ${alias.binaryPath}`);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
446
464
|
// Local-first product surface: if the user already has egregores installed
|
|
447
465
|
// and didn't pass an explicit mode flag, refresh the launcher and open the
|
|
448
466
|
// same TUI as bare `egregore`. With no installed instances, make bare
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-egregore",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.13",
|
|
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 test/launcher-update.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
|