create-egregore 0.21.0-runtime-mvp.12 → 0.21.0-runtime-mvp.14
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 +7 -3
- package/bin/cli.js +13 -72
- package/lib/setup.js +18 -46
- package/package.json +1 -1
- package/runtime/claude/bin/lib/git-sync.sh +12 -0
- package/runtime/claude/bin/lib/greeting.sh +3 -7
- package/runtime/claude/bin/lib/runtime-owned.sh +30 -0
- package/runtime/claude/bin/lib/settings-drift.sh +5 -1
- package/runtime/claude/egregore_runtime/harness_cli.py +3 -0
- package/runtime/claude/egregore_runtime/sync.py +74 -11
- package/runtime/claude/egregore_runtime/upgrade.py +264 -60
- package/runtime/claude/manifest.json +1 -0
- package/runtime/codex/bin/lib/git-sync.sh +12 -0
- package/runtime/codex/bin/lib/greeting.sh +3 -7
- package/runtime/codex/bin/lib/runtime-owned.sh +30 -0
- package/runtime/codex/bin/lib/settings-drift.sh +5 -1
- package/runtime/codex/egregore_runtime/harness_cli.py +3 -0
- package/runtime/codex/egregore_runtime/sync.py +74 -11
- package/runtime/codex/egregore_runtime/upgrade.py +264 -60
- package/runtime/codex/manifest.json +1 -0
- package/runtime/pi/bin/lib/git-sync.sh +12 -0
- package/runtime/pi/bin/lib/greeting.sh +3 -7
- package/runtime/pi/bin/lib/runtime-owned.sh +30 -0
- package/runtime/pi/bin/lib/settings-drift.sh +5 -1
- package/runtime/pi/egregore_runtime/harness_cli.py +3 -0
- package/runtime/pi/egregore_runtime/sync.py +74 -11
- package/runtime/pi/egregore_runtime/upgrade.py +264 -60
- package/runtime/pi/manifest.json +1 -0
- package/runtime/prime/bin/lib/git-sync.sh +12 -0
- package/runtime/prime/bin/lib/greeting.sh +3 -7
- package/runtime/prime/bin/lib/runtime-owned.sh +30 -0
- package/runtime/prime/bin/lib/settings-drift.sh +5 -1
- package/runtime/prime/egregore_runtime/harness_cli.py +3 -0
- package/runtime/prime/egregore_runtime/sync.py +74 -11
- package/runtime/prime/egregore_runtime/upgrade.py +264 -60
- package/runtime/prime/manifest.json +1 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// egregore-launcher-version: 0.21.0-runtime-mvp.
|
|
2
|
+
// egregore-launcher-version: 0.21.0-runtime-mvp.14
|
|
3
3
|
//
|
|
4
4
|
// egregore — launch and browse Egregore instances from your terminal.
|
|
5
5
|
// Installed by create-egregore. Reads ~/.egregore/instances.json.
|
|
@@ -35,7 +35,7 @@ const crypto = require("node:crypto");
|
|
|
35
35
|
const { spawnSync, spawn } = require("node:child_process");
|
|
36
36
|
const readline = require("node:readline");
|
|
37
37
|
|
|
38
|
-
const VERSION = "0.21.0-runtime-mvp.
|
|
38
|
+
const VERSION = "0.21.0-runtime-mvp.14";
|
|
39
39
|
const REGISTRY = path.join(os.homedir(), ".egregore", "instances.json");
|
|
40
40
|
const DEFAULT_FILE = path.join(os.homedir(), ".egregore", "default");
|
|
41
41
|
const UPDATE_CACHE_FILE = path.join(os.homedir(), ".egregore", "update-check.json");
|
|
@@ -2673,7 +2673,11 @@ function formatUpgradeLines(inst, st, { candidate = "", retrieval = null } = {})
|
|
|
2673
2673
|
+ (d.candidate_version ? ` → candidate ${d.candidate_version}` : candidate ? ` → candidate ${candidate} available` : ""));
|
|
2674
2674
|
lines.push(`Status: ${UPGRADE_STATUS_LABEL[d.status] || d.status}`);
|
|
2675
2675
|
if (d.candidate_digest) lines.push(`Package digest: sha256 ${String(d.candidate_digest).slice(0, 16)}…`);
|
|
2676
|
-
|
|
2676
|
+
// A recorded failure only needs attention while the transaction is stuck
|
|
2677
|
+
// on it; once a later prepare succeeds the line would contradict "ready".
|
|
2678
|
+
if (d.failure && d.failure.message && (d.status === "failed" || d.status === "interrupted")) {
|
|
2679
|
+
lines.push(`Attention: ${d.failure.message}`);
|
|
2680
|
+
}
|
|
2677
2681
|
if (d.rollback_version && d.status !== "none") lines.push(`Rollback target: ${d.rollback_version}`);
|
|
2678
2682
|
const ts = d.team_sync;
|
|
2679
2683
|
if (ts && ts.label) lines.push(`Team memory: ${ts.label}`);
|
package/bin/cli.js
CHANGED
|
@@ -25,32 +25,10 @@ const {
|
|
|
25
25
|
ghCliExists,
|
|
26
26
|
tryGhAuthOrLogin,
|
|
27
27
|
} = require("../lib/auth");
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
} = require("../lib/codex-runtime");
|
|
33
|
-
const {
|
|
34
|
-
hasClaudeRuntimeSupport,
|
|
35
|
-
installClaudeRuntime,
|
|
36
|
-
reportClaudeRuntimeSummary,
|
|
37
|
-
} = require("../lib/claude-runtime");
|
|
38
|
-
const {
|
|
39
|
-
hasPiRuntimeSupport,
|
|
40
|
-
installPiRuntime,
|
|
41
|
-
reportPiRuntimeSummary,
|
|
42
|
-
} = require("../lib/pi-runtime");
|
|
43
|
-
const {
|
|
44
|
-
hasPrimeRuntimeSupport,
|
|
45
|
-
installPrimeRuntime,
|
|
46
|
-
reportPrimeRuntimeSummary,
|
|
47
|
-
} = require("../lib/prime-runtime");
|
|
48
|
-
const {
|
|
49
|
-
connectEligibility,
|
|
50
|
-
fetchConnectRuntimePack,
|
|
51
|
-
installConnectRuntime,
|
|
52
|
-
reportConnectRuntimeSummary,
|
|
53
|
-
} = require("../lib/connect-runtime");
|
|
28
|
+
// Runtime adapter installers are deliberately NOT imported here: the global
|
|
29
|
+
// CLI never writes framework/Runtime files into registered instances. Fresh
|
|
30
|
+
// installs get their files in lib/setup.js and lib/local.js; existing
|
|
31
|
+
// instances upgrade only through their own Settings prepare → activate flow.
|
|
54
32
|
const {
|
|
55
33
|
install,
|
|
56
34
|
ghApi,
|
|
@@ -125,50 +103,15 @@ function assistantLabel(runtime) {
|
|
|
125
103
|
return "Claude Code";
|
|
126
104
|
}
|
|
127
105
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
? await fetchConnectRuntimePack(inst.path)
|
|
138
|
-
: null;
|
|
139
|
-
const claudeSummary = installClaudeRuntime(inst.path);
|
|
140
|
-
const claudeReady = hasClaudeRuntimeSupport(inst.path);
|
|
141
|
-
reportClaudeRuntimeSummary(claudeSummary, ui, {
|
|
142
|
-
label: `Claude Code runtime for ${inst.slug || inst.name || path.basename(inst.path)}`,
|
|
143
|
-
includeConflicts: !claudeReady,
|
|
144
|
-
});
|
|
145
|
-
const summary = installCodexRuntime(inst.path);
|
|
146
|
-
const ready = hasCodexRuntimeSupport(inst.path);
|
|
147
|
-
reportCodexRuntimeSummary(summary, ui, {
|
|
148
|
-
label: `Codex runtime for ${inst.slug || inst.name || path.basename(inst.path)}`,
|
|
149
|
-
includeConflicts: !ready,
|
|
150
|
-
});
|
|
151
|
-
const piSummary = installPiRuntime(inst.path);
|
|
152
|
-
const piReady = hasPiRuntimeSupport(inst.path);
|
|
153
|
-
reportPiRuntimeSummary(piSummary, ui, {
|
|
154
|
-
label: `Pi meta-harness for ${inst.slug || inst.name || path.basename(inst.path)}`,
|
|
155
|
-
includeConflicts: !piReady,
|
|
156
|
-
});
|
|
157
|
-
const primeSummary = installPrimeRuntime(inst.path);
|
|
158
|
-
const primeReady = hasPrimeRuntimeSupport(inst.path);
|
|
159
|
-
reportPrimeRuntimeSummary(primeSummary, ui, {
|
|
160
|
-
label: `Prime Agent runtime for ${inst.slug || inst.name || path.basename(inst.path)}`,
|
|
161
|
-
includeConflicts: !primeReady,
|
|
162
|
-
});
|
|
163
|
-
const connectSummary = await installConnectRuntime(inst.path, { validatedPack: connectPack });
|
|
164
|
-
reportConnectRuntimeSummary(connectSummary, ui, {
|
|
165
|
-
label: `Connect features for ${inst.slug || inst.name || path.basename(inst.path)}`,
|
|
166
|
-
});
|
|
167
|
-
} catch (err) {
|
|
168
|
-
ui.warn(`Could not refresh shell-runtime support for ${inst.path}: ${err.message}`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
106
|
+
// Runtime ownership contract: refreshing the global CLI updates ONLY the
|
|
107
|
+
// global CLI. Registered instances are never installed into, updated,
|
|
108
|
+
// removed from, or reconciled here — an existing instance crosses into the
|
|
109
|
+
// new Runtime exclusively through its own Settings flow
|
|
110
|
+
// (prepare → confirm → activate), which applies engine and all harness
|
|
111
|
+
// adapters transactionally from one verified candidate. A CLI refresh that
|
|
112
|
+
// sprayed adapters into every registered instance is exactly how a legacy
|
|
113
|
+
// instance became an incoherent mixture of old framework and new hooks
|
|
114
|
+
// (its own upstream sync then reverted the tracked half).
|
|
172
115
|
|
|
173
116
|
async function chooseRuntime(args) {
|
|
174
117
|
if (args.runtime) {
|
|
@@ -210,7 +153,6 @@ async function chooseRuntime(args) {
|
|
|
210
153
|
}
|
|
211
154
|
|
|
212
155
|
async function upgradeLauncherFromExisting(instances) {
|
|
213
|
-
await upgradePortableRuntimesForExisting(instances);
|
|
214
156
|
const inst = instances.find((entry) => entry && entry.path && fs.existsSync(entry.path));
|
|
215
157
|
if (!inst) return null;
|
|
216
158
|
try {
|
|
@@ -524,7 +466,6 @@ async function main() {
|
|
|
524
466
|
ui.error("No installed Egregore instance was found. Run create-egregore without --refresh-launcher.");
|
|
525
467
|
process.exit(1);
|
|
526
468
|
}
|
|
527
|
-
await upgradePortableRuntimesForExisting(instances);
|
|
528
469
|
const alias = await installLauncherBinary(instances[0].path, ui);
|
|
529
470
|
if (!alias || !alias.binaryPath) {
|
|
530
471
|
ui.error("Could not refresh the Egregore CLI.");
|
package/lib/setup.js
CHANGED
|
@@ -577,14 +577,20 @@ async function install(data, ui, targetDir, opts = {}) {
|
|
|
577
577
|
try { ensurePersonOnboardingWitness(memoryDir, state); } catch {}
|
|
578
578
|
|
|
579
579
|
// 4c. Overlay the selected harness adapter from the installed npm artifact.
|
|
580
|
-
//
|
|
581
|
-
//
|
|
580
|
+
// This is the one-time explicit setup write; launches never install
|
|
581
|
+
// framework files (see the ownership contract at the launch path), and
|
|
582
|
+
// existing instances change only through their Settings
|
|
583
|
+
// prepare → confirm → activate flow — which applies ALL FOUR harness
|
|
584
|
+
// trees from one verified candidate.
|
|
582
585
|
if (preferredRuntime === "claude-code") {
|
|
583
586
|
const claudeRuntime = installClaudeRuntime(egregoreDir);
|
|
584
587
|
reportClaudeRuntimeSummary(claudeRuntime, ui);
|
|
585
588
|
} else if (preferredRuntime === "codex") {
|
|
586
589
|
const codexRuntime = installCodexRuntime(egregoreDir);
|
|
587
590
|
reportCodexRuntimeSummary(codexRuntime, ui);
|
|
591
|
+
// Codex-native skills are framework code, so they render at setup —
|
|
592
|
+
// previously they were sprayed at every launch.
|
|
593
|
+
try { installCodexSkills(egregoreDir); } catch {}
|
|
588
594
|
} else if (preferredRuntime === "pi") {
|
|
589
595
|
const piRuntime = installPiRuntime(egregoreDir);
|
|
590
596
|
reportPiRuntimeSummary(piRuntime, ui);
|
|
@@ -1251,50 +1257,16 @@ async function launchOrInstruct(egregoreDir, alias, ui, ctx = {}) {
|
|
|
1251
1257
|
ui.info(`Next time, ${launcherNextStep(ui, aliasName)}`);
|
|
1252
1258
|
console.log("");
|
|
1253
1259
|
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
const codexRuntime = installCodexRuntime(egregoreDir);
|
|
1265
|
-
reportCodexRuntimeSummary(codexRuntime, ui);
|
|
1266
|
-
if (!hasCodexCoreSupport(egregoreDir)) {
|
|
1267
|
-
ui.warn(codexSupportMessage(egregoreDir));
|
|
1268
|
-
ui.info(`Run ${ui.bold(aliasName)} after updating the checkout.`);
|
|
1269
|
-
console.log("");
|
|
1270
|
-
return;
|
|
1271
|
-
}
|
|
1272
|
-
const skillsInstalled = installCodexSkills(egregoreDir);
|
|
1273
|
-
const cardRendered = await renderCodexStartupCard(egregoreDir);
|
|
1274
|
-
if (!skillsInstalled || !cardRendered) {
|
|
1275
|
-
ui.warn(codexSupportMessage(egregoreDir));
|
|
1276
|
-
ui.info(`Run ${ui.bold(aliasName)} after updating the checkout.`);
|
|
1277
|
-
console.log("");
|
|
1278
|
-
return;
|
|
1279
|
-
}
|
|
1280
|
-
} else if (runtime === "pi") {
|
|
1281
|
-
const piRuntime = installPiRuntime(egregoreDir);
|
|
1282
|
-
reportPiRuntimeSummary(piRuntime, ui);
|
|
1283
|
-
if (!hasPiRuntimeSupport(egregoreDir)) {
|
|
1284
|
-
ui.warn(piSupportMessage(egregoreDir));
|
|
1285
|
-
ui.info(`Run ${ui.bold(aliasName)} after updating the checkout.`);
|
|
1286
|
-
console.log("");
|
|
1287
|
-
return;
|
|
1288
|
-
}
|
|
1289
|
-
} else if (runtime === "prime") {
|
|
1290
|
-
const primeRuntime = installPrimeRuntime(egregoreDir);
|
|
1291
|
-
reportPrimeRuntimeSummary(primeRuntime, ui);
|
|
1292
|
-
if (!hasPrimeRuntimeSupport(egregoreDir)) {
|
|
1293
|
-
ui.warn(primeSupportMessage(egregoreDir));
|
|
1294
|
-
ui.info(`Run ${ui.bold(aliasName)} after updating the checkout.`);
|
|
1295
|
-
console.log("");
|
|
1296
|
-
return;
|
|
1297
|
-
}
|
|
1260
|
+
// Runtime ownership contract: launching NEVER adds, replaces, or removes
|
|
1261
|
+
// framework/Runtime code. Fresh installs received their files during
|
|
1262
|
+
// explicit setup; existing instances change only through their own
|
|
1263
|
+
// Settings prepare → confirm → activate flow. A legacy instance opens
|
|
1264
|
+
// exactly as it is — old framework, old graph retrieval, no commentary.
|
|
1265
|
+
// The one launch-time write kept below is the Codex startup card, which is
|
|
1266
|
+
// rendered presentation state, not framework code; a legacy instance
|
|
1267
|
+
// without the renderer skips it silently.
|
|
1268
|
+
if (runtime === "codex") {
|
|
1269
|
+
try { await renderCodexStartupCard(egregoreDir); } catch {}
|
|
1298
1270
|
}
|
|
1299
1271
|
|
|
1300
1272
|
const result = spawnSync(binary, runtimeLaunchArgs(runtime, egregoreDir), {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-egregore",
|
|
3
|
-
"version": "0.21.0-runtime-mvp.
|
|
3
|
+
"version": "0.21.0-runtime-mvp.14",
|
|
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": {
|
|
@@ -28,6 +28,10 @@ SYNC_RECEIPT=""
|
|
|
28
28
|
# shellcheck source=bin/lib/settings-drift.sh
|
|
29
29
|
source "$SCRIPT_DIR/bin/lib/settings-drift.sh"
|
|
30
30
|
|
|
31
|
+
# Runtime framework ownership (activation record gate for legacy sync).
|
|
32
|
+
# shellcheck source=bin/lib/runtime-owned.sh
|
|
33
|
+
source "$SCRIPT_DIR/bin/lib/runtime-owned.sh"
|
|
34
|
+
|
|
31
35
|
# The core repo's integration branch: "develop" unless egregore.json sets
|
|
32
36
|
# base_branch. session-start.sh resolves this before sourcing; resolve it here
|
|
33
37
|
# too so the lib stands alone for other callers.
|
|
@@ -181,6 +185,14 @@ _apply_framework_update() {
|
|
|
181
185
|
if [ "$_UPSTREAM_URL" = "none" ] || [ "$_AUTO_UPDATE" != "true" ]; then
|
|
182
186
|
return 0
|
|
183
187
|
fi
|
|
188
|
+
# Runtime ownership: once this exact instance activated the new Runtime
|
|
189
|
+
# (activation record with retrieval runtime-qmd), the legacy upstream sync
|
|
190
|
+
# must never overwrite Runtime-owned framework paths — that fight is how a
|
|
191
|
+
# launched-then-synced instance became a broken old/new mixture. Rollback
|
|
192
|
+
# flips the record off runtime-qmd and legacy ownership resumes here.
|
|
193
|
+
if runtime_owns_framework 2>/dev/null; then
|
|
194
|
+
return 0
|
|
195
|
+
fi
|
|
184
196
|
if ! git show-ref --verify --quiet refs/remotes/upstream/main 2>/dev/null; then
|
|
185
197
|
return 0
|
|
186
198
|
fi
|
|
@@ -95,19 +95,15 @@ if [ -n "$SAVED_BRANCH" ]; then
|
|
|
95
95
|
echo " ✓ Auto-saved uncommitted work on $SAVED_BRANCH"
|
|
96
96
|
fi
|
|
97
97
|
|
|
98
|
-
# ---
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
fi
|
|
98
|
+
# --- Org-settings sync receipts. A settings.json heal is logged to
|
|
99
|
+
# .egregore/settings-drift.log, never announced; local settings changes reach
|
|
100
|
+
# the control plane through bin/settings.sh, so there is nothing to ask. ---
|
|
102
101
|
if [ -n "${SETTINGS_SYNCED_FROM_ORG:-}" ]; then
|
|
103
102
|
echo " ◇ org settings synced from the control plane (${SETTINGS_SYNCED_FROM_ORG})"
|
|
104
103
|
fi
|
|
105
104
|
if [ -n "${SETTINGS_POSTURE_HELD:-}" ]; then
|
|
106
105
|
echo " ◇ boundary posture was set to ${SETTINGS_POSTURE_HELD} elsewhere — relaxing access needs your confirm: bash bin/settings.sh posture ${SETTINGS_POSTURE_HELD}"
|
|
107
106
|
fi
|
|
108
|
-
if [ -n "${UNSHARED_SETTINGS_KEYS:-}" ]; then
|
|
109
|
-
echo " ◇ org settings changed on this machine (${UNSHARED_SETTINGS_KEYS}) — say \"share settings\" to make them org-wide"
|
|
110
|
-
fi
|
|
111
107
|
|
|
112
108
|
# --- Footer: health + repos + memory (tertiary) ---
|
|
113
109
|
echo "$SEPARATOR"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# runtime-owned.sh — does the Runtime own this instance's framework paths?
|
|
3
|
+
#
|
|
4
|
+
# True exactly when THIS instance has an activation record whose retrieval
|
|
5
|
+
# mode is runtime-qmd. Uses the same stable identity as the Python upgrade
|
|
6
|
+
# store and the graph cutover gate: sha256(org_id | main-checkout path),
|
|
7
|
+
# first 16 hex chars, under EGREGORE_UPGRADE_ROOT (default
|
|
8
|
+
# ~/.egregore/runtime/upgrade). No global flag, no pathname guess, no slug,
|
|
9
|
+
# no branch name, no hardcoded organization or version.
|
|
10
|
+
#
|
|
11
|
+
# After a rollback the record's retrieval flips to previous-runtime, so this
|
|
12
|
+
# returns false again and legacy framework ownership resumes automatically.
|
|
13
|
+
#
|
|
14
|
+
# Contract: sourced with SCRIPT_DIR set to the instance root.
|
|
15
|
+
|
|
16
|
+
runtime_owns_framework() {
|
|
17
|
+
local org main gd key rec
|
|
18
|
+
org=$(jq -r '.org_id // empty' "$SCRIPT_DIR/egregore.json" 2>/dev/null)
|
|
19
|
+
[ -n "$org" ] || return 1
|
|
20
|
+
main="$SCRIPT_DIR"
|
|
21
|
+
if [ -f "$SCRIPT_DIR/.git" ]; then
|
|
22
|
+
gd=$(sed -n 's/^gitdir: //p' "$SCRIPT_DIR/.git" 2>/dev/null)
|
|
23
|
+
case "$gd" in */.git/worktrees/*) main="${gd%/.git/worktrees/*}" ;; esac
|
|
24
|
+
fi
|
|
25
|
+
main=$(cd "$main" 2>/dev/null && pwd -P || printf '%s' "$SCRIPT_DIR")
|
|
26
|
+
key=$(printf '%s|%s' "$org" "$main" | shasum -a 256 2>/dev/null | cut -c1-16)
|
|
27
|
+
[ -n "$key" ] || return 1
|
|
28
|
+
rec="${EGREGORE_UPGRADE_ROOT:-$HOME/.egregore/runtime/upgrade}/${key}/active.json"
|
|
29
|
+
[ -f "$rec" ] && [ "$(jq -r '.retrieval // empty' "$rec" 2>/dev/null)" = "runtime-qmd" ]
|
|
30
|
+
}
|
|
@@ -61,7 +61,11 @@ settings_drift_heal() {
|
|
|
61
61
|
|
|
62
62
|
if ! git diff --quiet -- .claude/settings.json 2>/dev/null; then
|
|
63
63
|
if _settings_is_reduced_adapter; then
|
|
64
|
-
git checkout -- .claude/settings.json 2>/dev/null
|
|
64
|
+
if git checkout -- .claude/settings.json 2>/dev/null; then
|
|
65
|
+
SETTINGS_ADAPTER_RESTORED="true"
|
|
66
|
+
mkdir -p .egregore 2>/dev/null && printf '%s restored .claude/settings.json: a generated adapter had replaced the org file\n' \
|
|
67
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> .egregore/settings-drift.log 2>/dev/null || true
|
|
68
|
+
fi
|
|
65
69
|
fi
|
|
66
70
|
fi
|
|
67
71
|
|
|
@@ -897,6 +897,9 @@ def _upgrade(args: argparse.Namespace) -> int:
|
|
|
897
897
|
state = upgrade_module.describe(store.load())
|
|
898
898
|
state["active_version"] = _active_runtime_version(root, store)
|
|
899
899
|
state["instance_hash"] = upgrade_module.instance_key(root)
|
|
900
|
+
# A live apply journal means a previous candidate application died
|
|
901
|
+
# mid-transaction; the next activate recovers deterministically.
|
|
902
|
+
state["interrupted_apply"] = store.load_apply_journal() is not None
|
|
900
903
|
memory_root = (root / "memory").resolve()
|
|
901
904
|
if memory_root.is_dir():
|
|
902
905
|
state["team_sync"] = team_sync(memory_root, fetch=bool(args.fetch)).to_dict()
|
|
@@ -170,14 +170,80 @@ class LocalGitSyncTransport:
|
|
|
170
170
|
remote_is_ancestor = self._run(
|
|
171
171
|
"merge-base", "--is-ancestor", remote_revision, "HEAD"
|
|
172
172
|
).returncode == 0
|
|
173
|
+
warnings: list[str] = []
|
|
173
174
|
if not remote_is_ancestor:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
175
|
+
warnings.extend(self._integrate_remote(remote_revision))
|
|
176
|
+
return self._status(tuple(warnings), changed=self._revision() != before)
|
|
177
|
+
|
|
178
|
+
def _integrate_remote(self, remote_revision: str) -> list[str]:
|
|
179
|
+
"""Integrate fetched history into a possibly-dirty canonical checkout.
|
|
180
|
+
|
|
181
|
+
Canonical memory has many concurrent writers (sessions, background
|
|
182
|
+
capture, hosted agents) appending distinct files — incoming history
|
|
183
|
+
plus local edits is almost never a content conflict, so it must
|
|
184
|
+
self-heal instead of failing closed. The rebase autostashes local
|
|
185
|
+
edits and re-applies them; edits git cannot re-apply are parked in
|
|
186
|
+
the stash (content preserved, surfaced as a warning). Only a rebase
|
|
187
|
+
that itself conflicts — a genuine same-line collision with local
|
|
188
|
+
commits — fails closed.
|
|
189
|
+
"""
|
|
190
|
+
warnings: list[str] = []
|
|
191
|
+
dirty = bool(self._required("status", "--porcelain"))
|
|
192
|
+
stashes_before = self._run("stash", "list").stdout.count("\n")
|
|
193
|
+
arguments = ["rebase"]
|
|
194
|
+
if dirty:
|
|
195
|
+
arguments.append("--autostash")
|
|
196
|
+
result = self._run(*arguments, remote_revision, timeout=180)
|
|
197
|
+
if result.returncode != 0:
|
|
198
|
+
self._run("rebase", "--abort")
|
|
199
|
+
detail = (result.stderr or result.stdout).strip().splitlines()
|
|
200
|
+
raise GitSyncError(
|
|
201
|
+
"canonical sync hit a real content conflict while integrating "
|
|
202
|
+
"incoming changes; resolve it in the memory repository"
|
|
203
|
+
+ (f" ({detail[-1]})" if detail else "")
|
|
204
|
+
)
|
|
205
|
+
if dirty and self._run("stash", "list").stdout.count("\n") > stashes_before:
|
|
206
|
+
# Git kept the whole autostash entry AND left a conflicted
|
|
207
|
+
# application in the tree. Every local edit is safe inside the
|
|
208
|
+
# kept stash, so the tree resets to the clean rebased HEAD —
|
|
209
|
+
# parked edits, never conflict markers in canonical files.
|
|
210
|
+
self._run("reset", "--hard", "HEAD")
|
|
211
|
+
warnings.append(
|
|
212
|
+
"local memory edits could not be re-applied over incoming "
|
|
213
|
+
"changes and were parked in git stash — recover with "
|
|
214
|
+
"`git stash pop` in the memory repository"
|
|
215
|
+
)
|
|
216
|
+
return warnings
|
|
217
|
+
|
|
218
|
+
def _push_with_retry(self, attempts: int = 3) -> list[str]:
|
|
219
|
+
"""Push canonical history, absorbing the many-writer race.
|
|
220
|
+
|
|
221
|
+
The memory remote moves constantly (teammates' sessions, background
|
|
222
|
+
capture, hosted agents). A rejected push is re-integrated with an
|
|
223
|
+
autostashed rebase and retried — the exact manual recovery this
|
|
224
|
+
failure class always needed — before anything is reported as failed.
|
|
225
|
+
"""
|
|
226
|
+
detail = ""
|
|
227
|
+
for attempt in range(attempts):
|
|
228
|
+
pushed = self._run("push", "origin", "HEAD", timeout=180)
|
|
229
|
+
if pushed.returncode == 0:
|
|
230
|
+
return []
|
|
231
|
+
detail = (pushed.stderr or pushed.stdout).strip()
|
|
232
|
+
if attempt == attempts - 1:
|
|
233
|
+
break
|
|
234
|
+
branch = self._required("branch", "--show-current")
|
|
235
|
+
if not branch:
|
|
236
|
+
break
|
|
237
|
+
if self._run("fetch", "origin", branch, timeout=180).returncode != 0:
|
|
238
|
+
break
|
|
239
|
+
remote_revision = self._remote_revision()
|
|
240
|
+
if remote_revision is None:
|
|
241
|
+
break
|
|
242
|
+
try:
|
|
243
|
+
self._integrate_remote(remote_revision)
|
|
244
|
+
except GitSyncError as exc:
|
|
245
|
+
return [f"remote push failed: {exc}"]
|
|
246
|
+
return [f"remote push failed: {detail or 'git push failed'}"]
|
|
181
247
|
|
|
182
248
|
def push(self, *, message: str, paths: Sequence[str] = ()) -> SyncStatus:
|
|
183
249
|
selected = self._relative_paths(paths)
|
|
@@ -209,10 +275,7 @@ class LocalGitSyncTransport:
|
|
|
209
275
|
if warning := self._push_remote_background():
|
|
210
276
|
warnings.append(warning)
|
|
211
277
|
else:
|
|
212
|
-
|
|
213
|
-
if pushed.returncode != 0:
|
|
214
|
-
detail = (pushed.stderr or pushed.stdout).strip()
|
|
215
|
-
warnings.append(f"remote push failed: {detail or 'git push failed'}")
|
|
278
|
+
warnings.extend(self._push_with_retry())
|
|
216
279
|
else:
|
|
217
280
|
warnings.append("no origin remote configured; canonical history is local")
|
|
218
281
|
return self._status(warnings)
|