create-egregore 0.19.19 → 0.19.21
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 +110 -17
- package/package.json +2 -2
- package/runtime/codex/bin/agent.sh +2 -0
- package/runtime/codex/bin/autosave.sh +86 -0
- package/runtime/codex/bin/codex-session-start.sh +15 -4
- package/runtime/codex/bin/handoff-save-egregore.sh +8 -2
- package/runtime/codex/bin/lib/greeting.sh +16 -4
- package/runtime/codex/bin/session-autosave.sh +26 -1
- package/runtime/pi/bin/agent.sh +2 -0
- package/runtime/pi/bin/autosave.sh +86 -0
- package/runtime/pi/bin/codex-session-start.sh +15 -4
- package/runtime/pi/bin/handoff-save-egregore.sh +8 -2
- package/runtime/pi/bin/lib/greeting.sh +16 -4
- package/runtime/pi/bin/session-autosave.sh +26 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// egregore-launcher-version: 0.19.
|
|
2
|
+
// egregore-launcher-version: 0.19.21
|
|
3
3
|
//
|
|
4
4
|
// egregore — launch and browse Egregore instances from your terminal.
|
|
5
5
|
// Installed by create-egregore. Reads ~/.egregore/instances.json.
|
|
@@ -33,7 +33,7 @@ const crypto = require("node:crypto");
|
|
|
33
33
|
const { spawnSync, spawn } = require("node:child_process");
|
|
34
34
|
const readline = require("node:readline");
|
|
35
35
|
|
|
36
|
-
const VERSION = "0.19.
|
|
36
|
+
const VERSION = "0.19.21";
|
|
37
37
|
const REGISTRY = path.join(os.homedir(), ".egregore", "instances.json");
|
|
38
38
|
const DEFAULT_FILE = path.join(os.homedir(), ".egregore", "default");
|
|
39
39
|
const UPDATE_CACHE_FILE = path.join(os.homedir(), ".egregore", "update-check.json");
|
|
@@ -629,7 +629,14 @@ async function renderCodexStartupCard(egregoreDir) {
|
|
|
629
629
|
const result = await runCapturedWithSpinner("bash", [script], {
|
|
630
630
|
cwd: egregoreDir,
|
|
631
631
|
}, "Preparing Codex startup");
|
|
632
|
-
|
|
632
|
+
let output = result.stdout || "";
|
|
633
|
+
if (isConnectedPath(egregoreDir) && !output.includes("◆ CONNECTED")) {
|
|
634
|
+
const connectedLine = " ◆ CONNECTED · hosted graph active\n";
|
|
635
|
+
output = output.includes("\nWhat are you working on?")
|
|
636
|
+
? output.replace("\nWhat are you working on?", `\n${connectedLine}\nWhat are you working on?`)
|
|
637
|
+
: `${output}${output.endsWith("\n") || !output ? "" : "\n"}${connectedLine}`;
|
|
638
|
+
}
|
|
639
|
+
if (output) process.stdout.write(output);
|
|
633
640
|
if (result.status !== 0 && result.stderr) process.stderr.write(result.stderr);
|
|
634
641
|
if (result.status === 0) return true;
|
|
635
642
|
} catch {}
|
|
@@ -749,6 +756,10 @@ async function launch(inst, runtimeOverride) {
|
|
|
749
756
|
}
|
|
750
757
|
const runtime = await chooseRuntimeForLaunch(inst, runtimeOverride);
|
|
751
758
|
if (!runtime) return;
|
|
759
|
+
if (runtime === "claude-code") {
|
|
760
|
+
normalizeClaudeDenyRules(inst.path);
|
|
761
|
+
ensureClaudeModeStatusLine(inst.path);
|
|
762
|
+
}
|
|
752
763
|
const binary = runtimeBinary(runtime);
|
|
753
764
|
if (!commandExists(binary)) {
|
|
754
765
|
die(`${runtimeLabel(runtime)} is not installed.\n Install it with:\n ${assistantInstallCommand(runtime)}\n Then run: egregore`);
|
|
@@ -813,13 +824,17 @@ function cmdLs() {
|
|
|
813
824
|
const slugW = Math.min(24, Math.max(...registry.map((r) => (r.slug || "").length)));
|
|
814
825
|
const nameW = Math.min(24, Math.max(...registry.map((r) => (r.name || "").length)));
|
|
815
826
|
const runtimeW = Math.max(9, ...registry.map((r) => runtimeShort(runtimeFor(r)).length));
|
|
827
|
+
const modeW = "◆ CONNECTED".length;
|
|
816
828
|
console.log(`\n ${BOLD}${registry.length}${RESET} egregore${registry.length === 1 ? "" : "s"} registered\n`);
|
|
817
|
-
console.log(` ${DIM}${pad("SLUG", slugW)} ${pad("NAME", nameW)} ${pad("ASSISTANT", runtimeW)} PATH${RESET}`);
|
|
829
|
+
console.log(` ${DIM}${pad("SLUG", slugW)} ${pad("NAME", nameW)} ${pad("MODE", modeW)} ${pad("ASSISTANT", runtimeW)} PATH${RESET}`);
|
|
818
830
|
for (const inst of registry) {
|
|
819
831
|
const marker = inst.slug === def ? `${GREEN}●${RESET}` : " ";
|
|
820
832
|
const slug = inst.slug === def ? `${BOLD}${pad(inst.slug, slugW)}${RESET}` : pad(inst.slug, slugW);
|
|
821
833
|
const runtime = pad(runtimeShort(runtimeFor(inst)), runtimeW);
|
|
822
|
-
|
|
834
|
+
const mode = isConnectedInstance(inst)
|
|
835
|
+
? `${CYAN}${pad("◆ CONNECTED", modeW)}${RESET}`
|
|
836
|
+
: `${DIM}${pad("◇ LOCAL", modeW)}${RESET}`;
|
|
837
|
+
console.log(` ${marker} ${slug} ${pad(inst.name || "", nameW)} ${mode} ${runtime} ${DIM}${inst.path}${RESET}`);
|
|
823
838
|
}
|
|
824
839
|
if (def) console.log(`\n ${GREEN}●${RESET} = default marker (used by non-interactive ${BOLD}egregore${RESET})\n`);
|
|
825
840
|
else console.log("");
|
|
@@ -933,6 +948,22 @@ function readJsonFile(file) {
|
|
|
933
948
|
}
|
|
934
949
|
}
|
|
935
950
|
|
|
951
|
+
function isConnectedConfig(config) {
|
|
952
|
+
return (
|
|
953
|
+
config?.mode === "connected" &&
|
|
954
|
+
typeof config.api_url === "string" &&
|
|
955
|
+
config.api_url.trim().length > 0
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function isConnectedPath(instancePath) {
|
|
960
|
+
return isConnectedConfig(readJsonFile(path.join(instancePath, "egregore.json")));
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function isConnectedInstance(inst) {
|
|
964
|
+
return !!inst?.path && isConnectedPath(inst.path);
|
|
965
|
+
}
|
|
966
|
+
|
|
936
967
|
function readEnvValues(file) {
|
|
937
968
|
const values = {};
|
|
938
969
|
if (!fs.existsSync(file)) return values;
|
|
@@ -1213,19 +1244,64 @@ function writeJsonFileAtomic(file, value) {
|
|
|
1213
1244
|
atomicWrite(file, JSON.stringify(value, null, 2) + "\n", 0o644);
|
|
1214
1245
|
}
|
|
1215
1246
|
|
|
1216
|
-
function
|
|
1217
|
-
const stale = "Write(~/.egregore/instances.json)";
|
|
1218
|
-
const supported = "Edit(~/.egregore/instances.json)";
|
|
1247
|
+
function normalizeClaudeDenyRules(instancePath) {
|
|
1219
1248
|
for (const relative of [".claude/settings.json", ".claude/settings.local.json"]) {
|
|
1220
1249
|
const file = path.join(instancePath, relative);
|
|
1221
1250
|
const settings = readJsonFile(file);
|
|
1222
1251
|
const deny = settings?.permissions?.deny;
|
|
1223
|
-
if (!Array.isArray(deny)
|
|
1224
|
-
|
|
1252
|
+
if (!Array.isArray(deny)) continue;
|
|
1253
|
+
const normalized = deny.map((rule) => (
|
|
1254
|
+
typeof rule === "string" && /^Write\(.+\)$/.test(rule)
|
|
1255
|
+
? `Edit${rule.slice("Write".length)}`
|
|
1256
|
+
: rule
|
|
1257
|
+
));
|
|
1258
|
+
if (normalized.every((rule, index) => rule === deny[index])) continue;
|
|
1259
|
+
settings.permissions.deny = [...new Set(normalized)];
|
|
1225
1260
|
writeJsonFileAtomic(file, settings);
|
|
1226
1261
|
}
|
|
1227
1262
|
}
|
|
1228
1263
|
|
|
1264
|
+
const CLAUDE_MODE_STATUS_MARKER = "# egregore-connected-status-v1";
|
|
1265
|
+
const CLAUDE_DEFAULT_STATUS_COMMAND = "\"${CLAUDE_PROJECT_DIR}/bin/statusline.sh\"";
|
|
1266
|
+
|
|
1267
|
+
function claudeModeStatusCommand(baseCommand) {
|
|
1268
|
+
return [
|
|
1269
|
+
CLAUDE_MODE_STATUS_MARKER,
|
|
1270
|
+
"MODE=$(jq -r 'if (.mode == \"connected\" and ((.api_url // \"\") | length > 0)) then \"connected\" else \"local\" end' \"${CLAUDE_PROJECT_DIR}/egregore.json\" 2>/dev/null)",
|
|
1271
|
+
`BASE=$( ( ${baseCommand} ) 2>/dev/null || true)`,
|
|
1272
|
+
"if [ \"$MODE\" = \"connected\" ]; then",
|
|
1273
|
+
" case \"$BASE\" in",
|
|
1274
|
+
" *\"◆ CONNECTED\"*) printf '%s\\n' \"$BASE\" ;;",
|
|
1275
|
+
" \"\") printf '◆ CONNECTED\\n' ;;",
|
|
1276
|
+
" *) printf '◆ CONNECTED · %s\\n' \"$BASE\" ;;",
|
|
1277
|
+
" esac",
|
|
1278
|
+
"else",
|
|
1279
|
+
" printf '%s\\n' \"$BASE\"",
|
|
1280
|
+
"fi",
|
|
1281
|
+
].join("\n");
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function ensureClaudeModeStatusLine(instancePath) {
|
|
1285
|
+
if (!isConnectedPath(instancePath)) return false;
|
|
1286
|
+
const file = path.join(instancePath, ".claude", "settings.local.json");
|
|
1287
|
+
const settings = readJsonFile(file);
|
|
1288
|
+
const current = settings?.statusLine;
|
|
1289
|
+
if (current?.type === "command" && current?.command?.includes(CLAUDE_MODE_STATUS_MARKER)) {
|
|
1290
|
+
return false;
|
|
1291
|
+
}
|
|
1292
|
+
const baseCommand = current?.type === "command" && current?.command?.trim()
|
|
1293
|
+
? current.command
|
|
1294
|
+
: CLAUDE_DEFAULT_STATUS_COMMAND;
|
|
1295
|
+
settings.statusLine = {
|
|
1296
|
+
...(current || {}),
|
|
1297
|
+
type: "command",
|
|
1298
|
+
command: claudeModeStatusCommand(baseCommand),
|
|
1299
|
+
};
|
|
1300
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1301
|
+
writeJsonFileAtomic(file, settings);
|
|
1302
|
+
return true;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1229
1305
|
function writeEnvValueAtomic(file, key, value) {
|
|
1230
1306
|
const lines = fs.existsSync(file)
|
|
1231
1307
|
? fs.readFileSync(file, "utf-8").split(/\r?\n/)
|
|
@@ -1316,6 +1392,8 @@ async function cmdConnect(args) {
|
|
|
1316
1392
|
}
|
|
1317
1393
|
const activation = state.connected_activation || {};
|
|
1318
1394
|
if (cfg.mode === "connected" && activation.status === "connected") {
|
|
1395
|
+
normalizeClaudeDenyRules(inst.path);
|
|
1396
|
+
ensureClaudeModeStatusLine(inst.path);
|
|
1319
1397
|
console.log(`\n ${GREEN}✓${RESET} ${BOLD}${inst.slug}${RESET} is already connected.\n`);
|
|
1320
1398
|
return;
|
|
1321
1399
|
}
|
|
@@ -1454,7 +1532,8 @@ async function cmdConnect(args) {
|
|
|
1454
1532
|
api_url: claim.data.api_url,
|
|
1455
1533
|
});
|
|
1456
1534
|
writeEnvValueAtomic(envFile, "EGREGORE_API_KEY", claim.data.api_key);
|
|
1457
|
-
|
|
1535
|
+
normalizeClaudeDenyRules(inst.path);
|
|
1536
|
+
ensureClaudeModeStatusLine(inst.path);
|
|
1458
1537
|
persistConnectState(inst, {
|
|
1459
1538
|
status: "backfilling",
|
|
1460
1539
|
activation_protocol: claim.data.activation_protocol,
|
|
@@ -1931,7 +2010,8 @@ function loadPreview(inst) {
|
|
|
1931
2010
|
data.telegram = !!(cfg.telegram_chat_id || cfg.telegram_group_link);
|
|
1932
2011
|
data.org = cfg.org_name;
|
|
1933
2012
|
data.githubOrg = cfg.github_org;
|
|
1934
|
-
data.
|
|
2013
|
+
data.connected = isConnectedConfig(cfg);
|
|
2014
|
+
data.mode = data.connected ? "connected" : "local";
|
|
1935
2015
|
data.flowVariant = cfg.flow_variant;
|
|
1936
2016
|
}
|
|
1937
2017
|
} catch {}
|
|
@@ -2004,7 +2084,8 @@ function renderHeroPanel({ selected, pv, width, height, countSuffix, filter, fil
|
|
|
2004
2084
|
}
|
|
2005
2085
|
lines.push(emptyBoxLine(innerWidth, margin));
|
|
2006
2086
|
lines.push(updateBand || mid);
|
|
2007
|
-
|
|
2087
|
+
const connectedHero = pv?.connected ? ` · ${CYAN}◆ CONNECTED${RESET}` : "";
|
|
2088
|
+
lines.push(boxLine(`${BOLD}${title}${RESET} ${DIM}· ${org} · ${runtime}${RESET}${connectedHero}${DIM} · ${countSuffix} · ${filterText}${RESET}`, innerWidth, margin));
|
|
2008
2089
|
lines.push(boxLine(`${DIM}skills${RESET} ${skillStatus}`, innerWidth, margin));
|
|
2009
2090
|
lines.push(emptyBoxLine(innerWidth, margin));
|
|
2010
2091
|
|
|
@@ -2058,6 +2139,13 @@ function renderRuntimePanel({ inst, cursor, width, height }) {
|
|
|
2058
2139
|
emptyBoxLine(innerWidth, margin),
|
|
2059
2140
|
boxLine(`${BOLD}${CYAN}Launch ${title}${RESET}`, innerWidth, margin),
|
|
2060
2141
|
boxLine(`${DIM}${inst.path}${RESET}`, innerWidth, margin),
|
|
2142
|
+
boxLine(
|
|
2143
|
+
isConnectedInstance(inst)
|
|
2144
|
+
? `${CYAN}◆ CONNECTED${RESET} ${DIM}· hosted graph active${RESET}`
|
|
2145
|
+
: `${DIM}◇ LOCAL · Markdown + Git${RESET}`,
|
|
2146
|
+
innerWidth,
|
|
2147
|
+
margin,
|
|
2148
|
+
),
|
|
2061
2149
|
emptyBoxLine(innerWidth, margin),
|
|
2062
2150
|
];
|
|
2063
2151
|
|
|
@@ -2374,10 +2462,13 @@ async function browser(args = {}) {
|
|
|
2374
2462
|
? `${DIM}${pad(e.name || "", nameW)}${RESET}`
|
|
2375
2463
|
: pad(e.name || "", nameW);
|
|
2376
2464
|
const pathAvail = Math.max(0, listAvail - slugW - nameW - 10);
|
|
2465
|
+
const connected = isConnectedInstance(e);
|
|
2466
|
+
const modeBadge = connected ? `${CYAN}◆ CONNECTED${RESET}` : "";
|
|
2467
|
+
const adjustedPathAvail = Math.max(0, pathAvail - (connected ? "◆ CONNECTED ".length : 0));
|
|
2377
2468
|
const pathDim = isMissing
|
|
2378
|
-
? `${RED}✗${RESET} ${DIM}${truncate(e.path, Math.max(8,
|
|
2379
|
-
: `${DIM}${truncate(e.path,
|
|
2380
|
-
const line = ` ${cursorMark} ${defMark} ${slug} ${name} ${pathDim}`;
|
|
2469
|
+
? `${RED}✗${RESET} ${DIM}${truncate(e.path, Math.max(8, adjustedPathAvail - 12))}${RESET} ${RED}(missing)${RESET}`
|
|
2470
|
+
: `${DIM}${truncate(e.path, adjustedPathAvail)}${RESET}`;
|
|
2471
|
+
const line = ` ${cursorMark} ${defMark} ${slug} ${name} ${modeBadge}${modeBadge ? " " : ""}${pathDim}`;
|
|
2381
2472
|
out.push(M + " ".repeat(listPad - 3) + line + ANSI.clearLine + "\n");
|
|
2382
2473
|
}
|
|
2383
2474
|
|
|
@@ -2404,6 +2495,9 @@ async function browser(args = {}) {
|
|
|
2404
2495
|
? ` ${label("path")} ${RED}✗${RESET} ${truncate(selected.path, Math.max(8, valW - 12))} ${RED}(missing)${RESET}`
|
|
2405
2496
|
: ` ${label("path")} ${truncate(selected.path, valW)}`);
|
|
2406
2497
|
if (pv.org) previewLines.push(` ${label("org")} ${truncate(`${pv.org} ${DIM}(${pv.githubOrg || "?"})${RESET}`, valW)}`);
|
|
2498
|
+
previewLines.push(pv.connected
|
|
2499
|
+
? ` ${label("mode")} ${CYAN}◆ CONNECTED${RESET} ${DIM}· hosted graph active${RESET}`
|
|
2500
|
+
: ` ${label("mode")} ${DIM}◇ LOCAL · Markdown + Git${RESET}`);
|
|
2407
2501
|
if (pv.branch) previewLines.push(` ${label("branch")} ${BOLD}${pv.branch}${RESET}`);
|
|
2408
2502
|
if (pv.lastCommit) previewLines.push(` ${label("last")} ${DIM}${truncate(pv.lastCommit, valW)}${RESET}`);
|
|
2409
2503
|
if (pv.repos && pv.repos.length) {
|
|
@@ -2417,7 +2511,6 @@ async function browser(args = {}) {
|
|
|
2417
2511
|
}
|
|
2418
2512
|
const modeBits = [];
|
|
2419
2513
|
modeBits.push(runtimeShort(runtimeFor(selected, args.runtimeOverride)));
|
|
2420
|
-
if (pv.mode) modeBits.push(pv.mode);
|
|
2421
2514
|
if (pv.flowVariant && pv.flowVariant !== "legacy") modeBits.push(pv.flowVariant);
|
|
2422
2515
|
if (pv.telegram) modeBits.push("telegram");
|
|
2423
2516
|
if (selected.slug === def) modeBits.push("default");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-egregore",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.21",
|
|
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 test/launcher-connect.test.js test/launcher-update.test.js test/connected-config.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-connected-status.test.js test/launcher-permissions.test.js test/launcher-update.test.js test/connected-config.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
|
},
|
|
@@ -36,6 +36,7 @@ Runtime-neutral commands:
|
|
|
36
36
|
Commit and push current repo + memory changes;
|
|
37
37
|
PR body follows .claude/context/pr-format.md
|
|
38
38
|
(auto-generated skeleton if not provided)
|
|
39
|
+
autosave [status|on|off|scope|publish|merge] Personal ambient-save settings + consent
|
|
39
40
|
wrap --topic T --summary TEXT Write a session wrap to memory/wraps
|
|
40
41
|
handoff --from A --to B --topic T [--intent action|feedback|fyi]
|
|
41
42
|
[--body TEXT|--body-file PATH] [--no-push] [--no-publish] [--no-notify] [--json]
|
|
@@ -732,6 +733,7 @@ case "${1:-}" in
|
|
|
732
733
|
people) shift; cmd_people "$@" ;;
|
|
733
734
|
branch) shift; cmd_branch "$@" ;;
|
|
734
735
|
save) shift; cmd_save "$@" ;;
|
|
736
|
+
autosave) shift; bash "$SCRIPT_DIR/bin/autosave.sh" "$@" ;;
|
|
735
737
|
wrap) shift; cmd_wrap "$@" ;;
|
|
736
738
|
handoff) shift; cmd_handoff "$@" ;;
|
|
737
739
|
ask) shift; cmd_ask "$@" ;;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# autosave.sh — per-user autosave settings. The consent surface for ambient
|
|
3
|
+
# session capture (see bin/session-autosave.sh, docs/autosave.md).
|
|
4
|
+
#
|
|
5
|
+
# Settings live in .egregore-state.json (gitignored, personal — same pattern
|
|
6
|
+
# as the transcript_sharing consent flag):
|
|
7
|
+
# autosave_enabled true|false ambient capture on/off (default FALSE — opt-in experiment)
|
|
8
|
+
# autosave_scope all|handoffs what it touches (default all)
|
|
9
|
+
# all = memory repo + core-repo non-coding work
|
|
10
|
+
# handoffs = memory repo only; core repo never touched
|
|
11
|
+
# ambiently
|
|
12
|
+
# autosave_publish gate|auto what happens to the PR (default gate)
|
|
13
|
+
# gate = PR opened, merge waits for your explicit word
|
|
14
|
+
# auto = PR merged to develop automatically
|
|
15
|
+
#
|
|
16
|
+
# Usage:
|
|
17
|
+
# autosave.sh status
|
|
18
|
+
# autosave.sh on | off
|
|
19
|
+
# autosave.sh scope all|handoffs
|
|
20
|
+
# autosave.sh publish gate|auto
|
|
21
|
+
# autosave.sh merge # the consent act: merge YOUR staged auto-save PRs
|
|
22
|
+
#
|
|
23
|
+
# Exposed to Claude via the /autosave skill and to Codex/Pi via
|
|
24
|
+
# `bin/agent.sh autosave ...`.
|
|
25
|
+
|
|
26
|
+
set -uo pipefail
|
|
27
|
+
|
|
28
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
29
|
+
STATE_FILE="$SCRIPT_DIR/.egregore-state.json"
|
|
30
|
+
|
|
31
|
+
[ -f "$STATE_FILE" ] || echo '{}' > "$STATE_FILE"
|
|
32
|
+
|
|
33
|
+
# NB: jq's `//` swallows false — status would show "on" for enabled:false.
|
|
34
|
+
# Test key presence instead.
|
|
35
|
+
get() {
|
|
36
|
+
local v
|
|
37
|
+
v=$(jq -r "if ($1 != null) then ($1|tostring) else empty end" "$STATE_FILE" 2>/dev/null)
|
|
38
|
+
[ -n "$v" ] && echo "$v" || echo "$2"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
set_key() { # key value(json-encoded)
|
|
42
|
+
jq ".$1 = $2" "$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
status() {
|
|
46
|
+
local enabled scope publish
|
|
47
|
+
enabled=$(get '.autosave_enabled' 'false')
|
|
48
|
+
scope=$(get '.autosave_scope' 'all')
|
|
49
|
+
publish=$(get '.autosave_publish' 'gate')
|
|
50
|
+
echo "autosave: $([ "$enabled" = "true" ] && echo on || echo "off (opt-in — 'autosave on' to join the experiment)")"
|
|
51
|
+
echo "scope: $scope (all = handoffs + artifacts + docs · handoffs = memory repo only)"
|
|
52
|
+
echo "publish: $publish (gate = PRs wait for your merge · auto = merge to develop automatically)"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case "${1:-status}" in
|
|
56
|
+
status) status ;;
|
|
57
|
+
on) set_key autosave_enabled true && echo "autosave: on" ;;
|
|
58
|
+
off) set_key autosave_enabled false && echo "autosave: off — nothing is captured ambiently; /save and /handoff still work" ;;
|
|
59
|
+
scope)
|
|
60
|
+
case "${2:-}" in
|
|
61
|
+
all|handoffs) set_key autosave_scope "\"$2\"" && echo "scope: $2" ;;
|
|
62
|
+
*) echo "usage: autosave.sh scope all|handoffs" >&2; exit 2 ;;
|
|
63
|
+
esac ;;
|
|
64
|
+
publish)
|
|
65
|
+
case "${2:-}" in
|
|
66
|
+
gate|auto) set_key autosave_publish "\"$2\"" && echo "publish: $2" ;;
|
|
67
|
+
*) echo "usage: autosave.sh publish gate|auto" >&2; exit 2 ;;
|
|
68
|
+
esac ;;
|
|
69
|
+
merge)
|
|
70
|
+
# The consent act under publish=gate: merge this user's own staged
|
|
71
|
+
# Auto-save PRs. Never touches anyone else's.
|
|
72
|
+
command -v gh >/dev/null 2>&1 || { echo "gh not available" >&2; exit 1; }
|
|
73
|
+
GH_SELF=$(get '.github_username' '')
|
|
74
|
+
[ -n "$GH_SELF" ] || { echo "no github_username in .egregore-state.json" >&2; exit 1; }
|
|
75
|
+
PRS=$(gh pr list --state open --author "$GH_SELF" --limit 20 \
|
|
76
|
+
--json number,title --jq '.[] | select(.title | startswith("Auto-save:")) | .number' 2>/dev/null)
|
|
77
|
+
[ -n "$PRS" ] || { echo "no auto-saves waiting"; exit 0; }
|
|
78
|
+
for n in $PRS; do
|
|
79
|
+
if gh pr merge "$n" --merge >/dev/null 2>&1; then
|
|
80
|
+
echo "✓ merged auto-save PR #$n → develop"
|
|
81
|
+
else
|
|
82
|
+
echo "✗ PR #$n did not merge (conflict with develop?) — it stays open"
|
|
83
|
+
fi
|
|
84
|
+
done ;;
|
|
85
|
+
*) echo "usage: autosave.sh [status|on|off|scope all|handoffs|publish gate|auto|merge]" >&2; exit 2 ;;
|
|
86
|
+
esac
|
|
@@ -183,15 +183,26 @@ render_card() {
|
|
|
183
183
|
if [ "$total_as" -gt "$seen_as" ]; then
|
|
184
184
|
new_as=$((total_as - seen_as))
|
|
185
185
|
merged_as=$(grep '|autosave|' "$autosave_log" 2>/dev/null | tail -n "$new_as" | grep -c '|merged|' || echo 0)
|
|
186
|
-
if [ "$merged_as" -
|
|
187
|
-
printf ' ⟲ auto-saved %s non-coding save(s) → develop while you were away\n' "$
|
|
188
|
-
else
|
|
189
|
-
printf ' ⟲ auto-saved %s non-coding save(s) — %s merged, rest in open PRs\n' "$new_as" "$merged_as"
|
|
186
|
+
if [ "$merged_as" -gt 0 ]; then
|
|
187
|
+
printf ' ⟲ auto-saved %s non-coding save(s) → develop while you were away\n' "$merged_as"
|
|
190
188
|
fi
|
|
191
189
|
fi
|
|
192
190
|
printf '%s' "$total_as" > "$autosave_seen" 2>/dev/null || true
|
|
193
191
|
fi
|
|
194
192
|
|
|
193
|
+
# Staged auto-saves awaiting the user's merge (the autosave consent gate).
|
|
194
|
+
# Live gh query so the line persists until the PRs actually merge.
|
|
195
|
+
if command -v gh >/dev/null 2>&1; then
|
|
196
|
+
gh_self=$(jq -r '.github_username // empty' "$STATE_FILE" 2>/dev/null)
|
|
197
|
+
if [ -n "$gh_self" ]; then
|
|
198
|
+
waiting_as=$(gh pr list --state open --author "$gh_self" --limit 20 \
|
|
199
|
+
--json number,title --jq '[.[] | select(.title | startswith("Auto-save:")) | .number] | join(", #")' 2>/dev/null || true)
|
|
200
|
+
if [ -n "$waiting_as" ]; then
|
|
201
|
+
printf ' ⟲ auto-saves awaiting your merge: PR #%s — \$autosave merge (or bin/agent.sh autosave merge)\n' "$waiting_as"
|
|
202
|
+
fi
|
|
203
|
+
fi
|
|
204
|
+
fi
|
|
205
|
+
|
|
195
206
|
# Greeting links. Org config `pinned_links` in egregore.json replaces the
|
|
196
207
|
# default board link — same contract as bin/lib/greeting.sh.
|
|
197
208
|
pinned_links=$(jq -c '.pinned_links // []' "$CONFIG" 2>/dev/null || echo "[]")
|
|
@@ -33,11 +33,13 @@ shift 2 2>/dev/null || true
|
|
|
33
33
|
REPO_DIR=""
|
|
34
34
|
KIND="handoff"
|
|
35
35
|
BRANCH_SUFFIX=""
|
|
36
|
+
PUBLISH="auto"
|
|
36
37
|
while [ $# -gt 0 ]; do
|
|
37
38
|
case "$1" in
|
|
38
39
|
--repo-dir) REPO_DIR="${2:-}"; shift 2 ;;
|
|
39
40
|
--kind) KIND="${2:-handoff}"; shift 2 ;;
|
|
40
41
|
--branch-suffix) BRANCH_SUFFIX="${2:-}"; shift 2 ;;
|
|
42
|
+
--publish) PUBLISH="${2:-auto}"; shift 2 ;;
|
|
41
43
|
*) shift ;;
|
|
42
44
|
esac
|
|
43
45
|
done
|
|
@@ -158,14 +160,18 @@ fi
|
|
|
158
160
|
|
|
159
161
|
[ -n "$PR_NUMBER" ] || { ledger_write "-" "pushed"; exit 0; }
|
|
160
162
|
|
|
161
|
-
# ---
|
|
163
|
+
# --- Merge decision -------------------------------------------------------
|
|
164
|
+
# Non-coding + publish=auto → merge now. publish=gate → the PR is staged and
|
|
165
|
+
# the merge waits for the user's explicit word (the /autosave consent gate).
|
|
162
166
|
# `--auto` only works when the repo allows auto-merge AND required checks are
|
|
163
167
|
# pending; on a repo with no branch protection it always errors. Fall back to
|
|
164
168
|
# an immediate merge so non-coding PRs never sit open.
|
|
165
169
|
OUTCOME="open"
|
|
166
170
|
NON_CODE_BLOCKER=$(noncode_blockers origin/develop .)
|
|
167
171
|
if [ -z "$NON_CODE_BLOCKER" ]; then
|
|
168
|
-
if
|
|
172
|
+
if [ "$PUBLISH" = "gate" ]; then
|
|
173
|
+
OUTCOME="staged"
|
|
174
|
+
elif gh pr merge "$PR_NUMBER" --auto --merge >/dev/null 2>&1 \
|
|
169
175
|
|| gh pr merge "$PR_NUMBER" --merge >/dev/null 2>&1; then
|
|
170
176
|
OUTCOME="merged"
|
|
171
177
|
fi
|
|
@@ -164,15 +164,27 @@ if [ -f "$AUTOSAVE_LOG" ]; then
|
|
|
164
164
|
if [ "$TOTAL_AS" -gt "$SEEN_AS" ]; then
|
|
165
165
|
NEW_AS=$((TOTAL_AS - SEEN_AS))
|
|
166
166
|
MERGED_AS=$(grep '|autosave|' "$AUTOSAVE_LOG" 2>/dev/null | tail -n "$NEW_AS" | grep -c '|merged|' || echo 0)
|
|
167
|
-
if [ "$MERGED_AS" -
|
|
168
|
-
printf " ⟲ auto-saved %s non-coding save(s) → develop while you were away\n" "$
|
|
169
|
-
else
|
|
170
|
-
printf " ⟲ auto-saved %s non-coding save(s) — %s merged, rest in open PRs\n" "$NEW_AS" "$MERGED_AS"
|
|
167
|
+
if [ "$MERGED_AS" -gt 0 ]; then
|
|
168
|
+
printf " ⟲ auto-saved %s non-coding save(s) → develop while you were away\n" "$MERGED_AS"
|
|
171
169
|
fi
|
|
172
170
|
fi
|
|
173
171
|
printf '%s' "$TOTAL_AS" > "$AUTOSAVE_SEEN" 2>/dev/null || true
|
|
174
172
|
fi
|
|
175
173
|
|
|
174
|
+
# Staged auto-saves awaiting the user's merge (the /autosave consent gate).
|
|
175
|
+
# Live gh query so the line persists until the PRs actually merge — the
|
|
176
|
+
# ledger cursor above only reports once. Fail-soft when gh/network is out.
|
|
177
|
+
if command -v gh >/dev/null 2>&1; then
|
|
178
|
+
GH_SELF=$(jq -r '.github_username // empty' "$STATE_FILE" 2>/dev/null)
|
|
179
|
+
if [ -n "$GH_SELF" ]; then
|
|
180
|
+
WAITING_AS=$(gh pr list --state open --author "$GH_SELF" --limit 20 \
|
|
181
|
+
--json number,title --jq '[.[] | select(.title | startswith("Auto-save:")) | .number] | join(", #")' 2>/dev/null || true)
|
|
182
|
+
if [ -n "$WAITING_AS" ]; then
|
|
183
|
+
printf " ⟲ auto-saves awaiting your merge: PR #%s — say \"merge my auto-saves\"\n" "$WAITING_AS"
|
|
184
|
+
fi
|
|
185
|
+
fi
|
|
186
|
+
fi
|
|
187
|
+
|
|
176
188
|
# Greeting links. Org config `pinned_links` in egregore.json replaces the
|
|
177
189
|
# default dashboard + board links. Entries are strings or {url, label}.
|
|
178
190
|
# (OSC 8 hyperlink — clickable text in supported terminals)
|
|
@@ -87,6 +87,26 @@ touch "$STAMP" 2>/dev/null || true
|
|
|
87
87
|
# shellcheck source=bin/lib/noncode.sh
|
|
88
88
|
. "$SCRIPT_ROOT/bin/lib/noncode.sh" 2>/dev/null || exit 0
|
|
89
89
|
|
|
90
|
+
# --- Per-user consent settings (bin/autosave.sh / the /autosave skill) ----
|
|
91
|
+
# enabled=false → fully off, nothing captured ambiently
|
|
92
|
+
# scope=handoffs → memory repo only; the core repo is never touched ambiently
|
|
93
|
+
# publish=gate → PR is opened but NEVER merged without the user's word
|
|
94
|
+
# NB: jq's `//` swallows false (falsy) — `.k // empty` on k:false yields the
|
|
95
|
+
# default, silently re-enabling autosave. Test key presence instead (this is
|
|
96
|
+
# the same trap that broke auto_update:false).
|
|
97
|
+
_as_conf() { # key default
|
|
98
|
+
local v
|
|
99
|
+
v=$(jq -r "if (.$1 != null) then (.$1|tostring) else empty end" "$DIR/.egregore-state.json" 2>/dev/null)
|
|
100
|
+
[ -n "$v" ] || v=$(jq -r "if (.$1 != null) then (.$1|tostring) else empty end" "$SCRIPT_ROOT/.egregore-state.json" 2>/dev/null)
|
|
101
|
+
[ -n "$v" ] && echo "$v" || echo "$2"
|
|
102
|
+
}
|
|
103
|
+
# Autosave is an opt-in experiment: enabled defaults to FALSE. Nothing is
|
|
104
|
+
# captured ambiently until the user runs `/autosave on` (or autosave.sh on).
|
|
105
|
+
AS_ENABLED=$(_as_conf autosave_enabled false)
|
|
106
|
+
AS_SCOPE=$(_as_conf autosave_scope all)
|
|
107
|
+
AS_PUBLISH=$(_as_conf autosave_publish gate)
|
|
108
|
+
[ "$AS_ENABLED" = "true" ] || exit 0
|
|
109
|
+
|
|
90
110
|
if [ -z "$AUTHOR" ]; then
|
|
91
111
|
AUTHOR=$(jq -r '.github_username // .name // empty' "$DIR/.egregore-state.json" 2>/dev/null || true)
|
|
92
112
|
[ -n "$AUTHOR" ] || AUTHOR=$(jq -r '.github_username // .name // empty' "$SCRIPT_ROOT/.egregore-state.json" 2>/dev/null || true)
|
|
@@ -111,6 +131,10 @@ if git -C "$DIR/memory" rev-parse --git-dir >/dev/null 2>&1; then
|
|
|
111
131
|
fi
|
|
112
132
|
fi
|
|
113
133
|
|
|
134
|
+
# scope=handoffs: memory (above) is the handoff channel; stop before the
|
|
135
|
+
# core repo — it is never captured ambiently under this scope.
|
|
136
|
+
[ "$AS_SCOPE" = "handoffs" ] && exit 0
|
|
137
|
+
|
|
114
138
|
# --- Core repo gate: everything pending must be non-coding ---------------
|
|
115
139
|
# -uall expands untracked dirs to individual files: a collapsed "docs/" entry
|
|
116
140
|
# would defeat both the classifier (code nested under a content dir) and the
|
|
@@ -144,7 +168,7 @@ ORIG_BRANCH=$(git branch --show-current 2>/dev/null || echo "")
|
|
|
144
168
|
|
|
145
169
|
bash "$SCRIPT_ROOT/bin/handoff-save-egregore.sh" "$AUTHOR" \
|
|
146
170
|
"session content $(date +%Y-%m-%d)" --kind autosave --repo-dir "$DIR" \
|
|
147
|
-
--branch-suffix "$DIR_HASH" \
|
|
171
|
+
--branch-suffix "$DIR_HASH" --publish "$AS_PUBLISH" \
|
|
148
172
|
>/dev/null 2>&1 || true
|
|
149
173
|
|
|
150
174
|
# Tell the user what just happened. Under the Stop hook this line surfaces in
|
|
@@ -162,6 +186,7 @@ if [ -n "$LAST" ]; then
|
|
|
162
186
|
if [ $(( NOW_EPOCH - TS_EPOCH )) -lt 120 ]; then
|
|
163
187
|
case "$RESULT" in
|
|
164
188
|
merged) echo "⟲ auto-saved: $NF non-coding file(s) → develop (PR #$PR merged)" ;;
|
|
189
|
+
staged) echo "⟲ auto-saved: $NF non-coding file(s) → PR #$PR — yours to merge (say \"merge my auto-saves\" or /autosave)" ;;
|
|
165
190
|
open) echo "⟲ auto-saved: $NF non-coding file(s) → PR #$PR (merge pending)" ;;
|
|
166
191
|
*) echo "⟲ auto-saved: $NF non-coding file(s) → branch pushed (PR pending)" ;;
|
|
167
192
|
esac
|
package/runtime/pi/bin/agent.sh
CHANGED
|
@@ -36,6 +36,7 @@ Runtime-neutral commands:
|
|
|
36
36
|
Commit and push current repo + memory changes;
|
|
37
37
|
PR body follows .claude/context/pr-format.md
|
|
38
38
|
(auto-generated skeleton if not provided)
|
|
39
|
+
autosave [status|on|off|scope|publish|merge] Personal ambient-save settings + consent
|
|
39
40
|
wrap --topic T --summary TEXT Write a session wrap to memory/wraps
|
|
40
41
|
handoff --from A --to B --topic T [--intent action|feedback|fyi]
|
|
41
42
|
[--body TEXT|--body-file PATH] [--no-push] [--no-publish] [--no-notify] [--json]
|
|
@@ -732,6 +733,7 @@ case "${1:-}" in
|
|
|
732
733
|
people) shift; cmd_people "$@" ;;
|
|
733
734
|
branch) shift; cmd_branch "$@" ;;
|
|
734
735
|
save) shift; cmd_save "$@" ;;
|
|
736
|
+
autosave) shift; bash "$SCRIPT_DIR/bin/autosave.sh" "$@" ;;
|
|
735
737
|
wrap) shift; cmd_wrap "$@" ;;
|
|
736
738
|
handoff) shift; cmd_handoff "$@" ;;
|
|
737
739
|
ask) shift; cmd_ask "$@" ;;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# autosave.sh — per-user autosave settings. The consent surface for ambient
|
|
3
|
+
# session capture (see bin/session-autosave.sh, docs/autosave.md).
|
|
4
|
+
#
|
|
5
|
+
# Settings live in .egregore-state.json (gitignored, personal — same pattern
|
|
6
|
+
# as the transcript_sharing consent flag):
|
|
7
|
+
# autosave_enabled true|false ambient capture on/off (default FALSE — opt-in experiment)
|
|
8
|
+
# autosave_scope all|handoffs what it touches (default all)
|
|
9
|
+
# all = memory repo + core-repo non-coding work
|
|
10
|
+
# handoffs = memory repo only; core repo never touched
|
|
11
|
+
# ambiently
|
|
12
|
+
# autosave_publish gate|auto what happens to the PR (default gate)
|
|
13
|
+
# gate = PR opened, merge waits for your explicit word
|
|
14
|
+
# auto = PR merged to develop automatically
|
|
15
|
+
#
|
|
16
|
+
# Usage:
|
|
17
|
+
# autosave.sh status
|
|
18
|
+
# autosave.sh on | off
|
|
19
|
+
# autosave.sh scope all|handoffs
|
|
20
|
+
# autosave.sh publish gate|auto
|
|
21
|
+
# autosave.sh merge # the consent act: merge YOUR staged auto-save PRs
|
|
22
|
+
#
|
|
23
|
+
# Exposed to Claude via the /autosave skill and to Codex/Pi via
|
|
24
|
+
# `bin/agent.sh autosave ...`.
|
|
25
|
+
|
|
26
|
+
set -uo pipefail
|
|
27
|
+
|
|
28
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
29
|
+
STATE_FILE="$SCRIPT_DIR/.egregore-state.json"
|
|
30
|
+
|
|
31
|
+
[ -f "$STATE_FILE" ] || echo '{}' > "$STATE_FILE"
|
|
32
|
+
|
|
33
|
+
# NB: jq's `//` swallows false — status would show "on" for enabled:false.
|
|
34
|
+
# Test key presence instead.
|
|
35
|
+
get() {
|
|
36
|
+
local v
|
|
37
|
+
v=$(jq -r "if ($1 != null) then ($1|tostring) else empty end" "$STATE_FILE" 2>/dev/null)
|
|
38
|
+
[ -n "$v" ] && echo "$v" || echo "$2"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
set_key() { # key value(json-encoded)
|
|
42
|
+
jq ".$1 = $2" "$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
status() {
|
|
46
|
+
local enabled scope publish
|
|
47
|
+
enabled=$(get '.autosave_enabled' 'false')
|
|
48
|
+
scope=$(get '.autosave_scope' 'all')
|
|
49
|
+
publish=$(get '.autosave_publish' 'gate')
|
|
50
|
+
echo "autosave: $([ "$enabled" = "true" ] && echo on || echo "off (opt-in — 'autosave on' to join the experiment)")"
|
|
51
|
+
echo "scope: $scope (all = handoffs + artifacts + docs · handoffs = memory repo only)"
|
|
52
|
+
echo "publish: $publish (gate = PRs wait for your merge · auto = merge to develop automatically)"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case "${1:-status}" in
|
|
56
|
+
status) status ;;
|
|
57
|
+
on) set_key autosave_enabled true && echo "autosave: on" ;;
|
|
58
|
+
off) set_key autosave_enabled false && echo "autosave: off — nothing is captured ambiently; /save and /handoff still work" ;;
|
|
59
|
+
scope)
|
|
60
|
+
case "${2:-}" in
|
|
61
|
+
all|handoffs) set_key autosave_scope "\"$2\"" && echo "scope: $2" ;;
|
|
62
|
+
*) echo "usage: autosave.sh scope all|handoffs" >&2; exit 2 ;;
|
|
63
|
+
esac ;;
|
|
64
|
+
publish)
|
|
65
|
+
case "${2:-}" in
|
|
66
|
+
gate|auto) set_key autosave_publish "\"$2\"" && echo "publish: $2" ;;
|
|
67
|
+
*) echo "usage: autosave.sh publish gate|auto" >&2; exit 2 ;;
|
|
68
|
+
esac ;;
|
|
69
|
+
merge)
|
|
70
|
+
# The consent act under publish=gate: merge this user's own staged
|
|
71
|
+
# Auto-save PRs. Never touches anyone else's.
|
|
72
|
+
command -v gh >/dev/null 2>&1 || { echo "gh not available" >&2; exit 1; }
|
|
73
|
+
GH_SELF=$(get '.github_username' '')
|
|
74
|
+
[ -n "$GH_SELF" ] || { echo "no github_username in .egregore-state.json" >&2; exit 1; }
|
|
75
|
+
PRS=$(gh pr list --state open --author "$GH_SELF" --limit 20 \
|
|
76
|
+
--json number,title --jq '.[] | select(.title | startswith("Auto-save:")) | .number' 2>/dev/null)
|
|
77
|
+
[ -n "$PRS" ] || { echo "no auto-saves waiting"; exit 0; }
|
|
78
|
+
for n in $PRS; do
|
|
79
|
+
if gh pr merge "$n" --merge >/dev/null 2>&1; then
|
|
80
|
+
echo "✓ merged auto-save PR #$n → develop"
|
|
81
|
+
else
|
|
82
|
+
echo "✗ PR #$n did not merge (conflict with develop?) — it stays open"
|
|
83
|
+
fi
|
|
84
|
+
done ;;
|
|
85
|
+
*) echo "usage: autosave.sh [status|on|off|scope all|handoffs|publish gate|auto|merge]" >&2; exit 2 ;;
|
|
86
|
+
esac
|
|
@@ -183,15 +183,26 @@ render_card() {
|
|
|
183
183
|
if [ "$total_as" -gt "$seen_as" ]; then
|
|
184
184
|
new_as=$((total_as - seen_as))
|
|
185
185
|
merged_as=$(grep '|autosave|' "$autosave_log" 2>/dev/null | tail -n "$new_as" | grep -c '|merged|' || echo 0)
|
|
186
|
-
if [ "$merged_as" -
|
|
187
|
-
printf ' ⟲ auto-saved %s non-coding save(s) → develop while you were away\n' "$
|
|
188
|
-
else
|
|
189
|
-
printf ' ⟲ auto-saved %s non-coding save(s) — %s merged, rest in open PRs\n' "$new_as" "$merged_as"
|
|
186
|
+
if [ "$merged_as" -gt 0 ]; then
|
|
187
|
+
printf ' ⟲ auto-saved %s non-coding save(s) → develop while you were away\n' "$merged_as"
|
|
190
188
|
fi
|
|
191
189
|
fi
|
|
192
190
|
printf '%s' "$total_as" > "$autosave_seen" 2>/dev/null || true
|
|
193
191
|
fi
|
|
194
192
|
|
|
193
|
+
# Staged auto-saves awaiting the user's merge (the autosave consent gate).
|
|
194
|
+
# Live gh query so the line persists until the PRs actually merge.
|
|
195
|
+
if command -v gh >/dev/null 2>&1; then
|
|
196
|
+
gh_self=$(jq -r '.github_username // empty' "$STATE_FILE" 2>/dev/null)
|
|
197
|
+
if [ -n "$gh_self" ]; then
|
|
198
|
+
waiting_as=$(gh pr list --state open --author "$gh_self" --limit 20 \
|
|
199
|
+
--json number,title --jq '[.[] | select(.title | startswith("Auto-save:")) | .number] | join(", #")' 2>/dev/null || true)
|
|
200
|
+
if [ -n "$waiting_as" ]; then
|
|
201
|
+
printf ' ⟲ auto-saves awaiting your merge: PR #%s — \$autosave merge (or bin/agent.sh autosave merge)\n' "$waiting_as"
|
|
202
|
+
fi
|
|
203
|
+
fi
|
|
204
|
+
fi
|
|
205
|
+
|
|
195
206
|
# Greeting links. Org config `pinned_links` in egregore.json replaces the
|
|
196
207
|
# default board link — same contract as bin/lib/greeting.sh.
|
|
197
208
|
pinned_links=$(jq -c '.pinned_links // []' "$CONFIG" 2>/dev/null || echo "[]")
|
|
@@ -33,11 +33,13 @@ shift 2 2>/dev/null || true
|
|
|
33
33
|
REPO_DIR=""
|
|
34
34
|
KIND="handoff"
|
|
35
35
|
BRANCH_SUFFIX=""
|
|
36
|
+
PUBLISH="auto"
|
|
36
37
|
while [ $# -gt 0 ]; do
|
|
37
38
|
case "$1" in
|
|
38
39
|
--repo-dir) REPO_DIR="${2:-}"; shift 2 ;;
|
|
39
40
|
--kind) KIND="${2:-handoff}"; shift 2 ;;
|
|
40
41
|
--branch-suffix) BRANCH_SUFFIX="${2:-}"; shift 2 ;;
|
|
42
|
+
--publish) PUBLISH="${2:-auto}"; shift 2 ;;
|
|
41
43
|
*) shift ;;
|
|
42
44
|
esac
|
|
43
45
|
done
|
|
@@ -158,14 +160,18 @@ fi
|
|
|
158
160
|
|
|
159
161
|
[ -n "$PR_NUMBER" ] || { ledger_write "-" "pushed"; exit 0; }
|
|
160
162
|
|
|
161
|
-
# ---
|
|
163
|
+
# --- Merge decision -------------------------------------------------------
|
|
164
|
+
# Non-coding + publish=auto → merge now. publish=gate → the PR is staged and
|
|
165
|
+
# the merge waits for the user's explicit word (the /autosave consent gate).
|
|
162
166
|
# `--auto` only works when the repo allows auto-merge AND required checks are
|
|
163
167
|
# pending; on a repo with no branch protection it always errors. Fall back to
|
|
164
168
|
# an immediate merge so non-coding PRs never sit open.
|
|
165
169
|
OUTCOME="open"
|
|
166
170
|
NON_CODE_BLOCKER=$(noncode_blockers origin/develop .)
|
|
167
171
|
if [ -z "$NON_CODE_BLOCKER" ]; then
|
|
168
|
-
if
|
|
172
|
+
if [ "$PUBLISH" = "gate" ]; then
|
|
173
|
+
OUTCOME="staged"
|
|
174
|
+
elif gh pr merge "$PR_NUMBER" --auto --merge >/dev/null 2>&1 \
|
|
169
175
|
|| gh pr merge "$PR_NUMBER" --merge >/dev/null 2>&1; then
|
|
170
176
|
OUTCOME="merged"
|
|
171
177
|
fi
|
|
@@ -164,15 +164,27 @@ if [ -f "$AUTOSAVE_LOG" ]; then
|
|
|
164
164
|
if [ "$TOTAL_AS" -gt "$SEEN_AS" ]; then
|
|
165
165
|
NEW_AS=$((TOTAL_AS - SEEN_AS))
|
|
166
166
|
MERGED_AS=$(grep '|autosave|' "$AUTOSAVE_LOG" 2>/dev/null | tail -n "$NEW_AS" | grep -c '|merged|' || echo 0)
|
|
167
|
-
if [ "$MERGED_AS" -
|
|
168
|
-
printf " ⟲ auto-saved %s non-coding save(s) → develop while you were away\n" "$
|
|
169
|
-
else
|
|
170
|
-
printf " ⟲ auto-saved %s non-coding save(s) — %s merged, rest in open PRs\n" "$NEW_AS" "$MERGED_AS"
|
|
167
|
+
if [ "$MERGED_AS" -gt 0 ]; then
|
|
168
|
+
printf " ⟲ auto-saved %s non-coding save(s) → develop while you were away\n" "$MERGED_AS"
|
|
171
169
|
fi
|
|
172
170
|
fi
|
|
173
171
|
printf '%s' "$TOTAL_AS" > "$AUTOSAVE_SEEN" 2>/dev/null || true
|
|
174
172
|
fi
|
|
175
173
|
|
|
174
|
+
# Staged auto-saves awaiting the user's merge (the /autosave consent gate).
|
|
175
|
+
# Live gh query so the line persists until the PRs actually merge — the
|
|
176
|
+
# ledger cursor above only reports once. Fail-soft when gh/network is out.
|
|
177
|
+
if command -v gh >/dev/null 2>&1; then
|
|
178
|
+
GH_SELF=$(jq -r '.github_username // empty' "$STATE_FILE" 2>/dev/null)
|
|
179
|
+
if [ -n "$GH_SELF" ]; then
|
|
180
|
+
WAITING_AS=$(gh pr list --state open --author "$GH_SELF" --limit 20 \
|
|
181
|
+
--json number,title --jq '[.[] | select(.title | startswith("Auto-save:")) | .number] | join(", #")' 2>/dev/null || true)
|
|
182
|
+
if [ -n "$WAITING_AS" ]; then
|
|
183
|
+
printf " ⟲ auto-saves awaiting your merge: PR #%s — say \"merge my auto-saves\"\n" "$WAITING_AS"
|
|
184
|
+
fi
|
|
185
|
+
fi
|
|
186
|
+
fi
|
|
187
|
+
|
|
176
188
|
# Greeting links. Org config `pinned_links` in egregore.json replaces the
|
|
177
189
|
# default dashboard + board links. Entries are strings or {url, label}.
|
|
178
190
|
# (OSC 8 hyperlink — clickable text in supported terminals)
|
|
@@ -87,6 +87,26 @@ touch "$STAMP" 2>/dev/null || true
|
|
|
87
87
|
# shellcheck source=bin/lib/noncode.sh
|
|
88
88
|
. "$SCRIPT_ROOT/bin/lib/noncode.sh" 2>/dev/null || exit 0
|
|
89
89
|
|
|
90
|
+
# --- Per-user consent settings (bin/autosave.sh / the /autosave skill) ----
|
|
91
|
+
# enabled=false → fully off, nothing captured ambiently
|
|
92
|
+
# scope=handoffs → memory repo only; the core repo is never touched ambiently
|
|
93
|
+
# publish=gate → PR is opened but NEVER merged without the user's word
|
|
94
|
+
# NB: jq's `//` swallows false (falsy) — `.k // empty` on k:false yields the
|
|
95
|
+
# default, silently re-enabling autosave. Test key presence instead (this is
|
|
96
|
+
# the same trap that broke auto_update:false).
|
|
97
|
+
_as_conf() { # key default
|
|
98
|
+
local v
|
|
99
|
+
v=$(jq -r "if (.$1 != null) then (.$1|tostring) else empty end" "$DIR/.egregore-state.json" 2>/dev/null)
|
|
100
|
+
[ -n "$v" ] || v=$(jq -r "if (.$1 != null) then (.$1|tostring) else empty end" "$SCRIPT_ROOT/.egregore-state.json" 2>/dev/null)
|
|
101
|
+
[ -n "$v" ] && echo "$v" || echo "$2"
|
|
102
|
+
}
|
|
103
|
+
# Autosave is an opt-in experiment: enabled defaults to FALSE. Nothing is
|
|
104
|
+
# captured ambiently until the user runs `/autosave on` (or autosave.sh on).
|
|
105
|
+
AS_ENABLED=$(_as_conf autosave_enabled false)
|
|
106
|
+
AS_SCOPE=$(_as_conf autosave_scope all)
|
|
107
|
+
AS_PUBLISH=$(_as_conf autosave_publish gate)
|
|
108
|
+
[ "$AS_ENABLED" = "true" ] || exit 0
|
|
109
|
+
|
|
90
110
|
if [ -z "$AUTHOR" ]; then
|
|
91
111
|
AUTHOR=$(jq -r '.github_username // .name // empty' "$DIR/.egregore-state.json" 2>/dev/null || true)
|
|
92
112
|
[ -n "$AUTHOR" ] || AUTHOR=$(jq -r '.github_username // .name // empty' "$SCRIPT_ROOT/.egregore-state.json" 2>/dev/null || true)
|
|
@@ -111,6 +131,10 @@ if git -C "$DIR/memory" rev-parse --git-dir >/dev/null 2>&1; then
|
|
|
111
131
|
fi
|
|
112
132
|
fi
|
|
113
133
|
|
|
134
|
+
# scope=handoffs: memory (above) is the handoff channel; stop before the
|
|
135
|
+
# core repo — it is never captured ambiently under this scope.
|
|
136
|
+
[ "$AS_SCOPE" = "handoffs" ] && exit 0
|
|
137
|
+
|
|
114
138
|
# --- Core repo gate: everything pending must be non-coding ---------------
|
|
115
139
|
# -uall expands untracked dirs to individual files: a collapsed "docs/" entry
|
|
116
140
|
# would defeat both the classifier (code nested under a content dir) and the
|
|
@@ -144,7 +168,7 @@ ORIG_BRANCH=$(git branch --show-current 2>/dev/null || echo "")
|
|
|
144
168
|
|
|
145
169
|
bash "$SCRIPT_ROOT/bin/handoff-save-egregore.sh" "$AUTHOR" \
|
|
146
170
|
"session content $(date +%Y-%m-%d)" --kind autosave --repo-dir "$DIR" \
|
|
147
|
-
--branch-suffix "$DIR_HASH" \
|
|
171
|
+
--branch-suffix "$DIR_HASH" --publish "$AS_PUBLISH" \
|
|
148
172
|
>/dev/null 2>&1 || true
|
|
149
173
|
|
|
150
174
|
# Tell the user what just happened. Under the Stop hook this line surfaces in
|
|
@@ -162,6 +186,7 @@ if [ -n "$LAST" ]; then
|
|
|
162
186
|
if [ $(( NOW_EPOCH - TS_EPOCH )) -lt 120 ]; then
|
|
163
187
|
case "$RESULT" in
|
|
164
188
|
merged) echo "⟲ auto-saved: $NF non-coding file(s) → develop (PR #$PR merged)" ;;
|
|
189
|
+
staged) echo "⟲ auto-saved: $NF non-coding file(s) → PR #$PR — yours to merge (say \"merge my auto-saves\" or /autosave)" ;;
|
|
165
190
|
open) echo "⟲ auto-saved: $NF non-coding file(s) → PR #$PR (merge pending)" ;;
|
|
166
191
|
*) echo "⟲ auto-saved: $NF non-coding file(s) → branch pushed (PR pending)" ;;
|
|
167
192
|
esac
|