create-egregore 0.21.0-runtime-mvp.13 → 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/package.json +1 -1
- package/runtime/claude/bin/lib/greeting.sh +3 -7
- package/runtime/claude/bin/lib/settings-drift.sh +5 -1
- package/runtime/claude/egregore_runtime/upgrade.py +64 -2
- package/runtime/codex/bin/lib/greeting.sh +3 -7
- package/runtime/codex/bin/lib/settings-drift.sh +5 -1
- package/runtime/codex/egregore_runtime/upgrade.py +64 -2
- package/runtime/pi/bin/lib/greeting.sh +3 -7
- package/runtime/pi/bin/lib/settings-drift.sh +5 -1
- package/runtime/pi/egregore_runtime/upgrade.py +64 -2
- package/runtime/prime/bin/lib/greeting.sh +3 -7
- package/runtime/prime/bin/lib/settings-drift.sh +5 -1
- package/runtime/prime/egregore_runtime/upgrade.py +64 -2
|
@@ -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/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": {
|
|
@@ -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"
|
|
@@ -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
|
|
|
@@ -53,6 +53,40 @@ BUNDLE_TREES = ("claude", "codex", "pi", "prime")
|
|
|
53
53
|
# wins and duplicates are skipped.
|
|
54
54
|
APPLY_TREES = ("claude", "codex", "pi", "prime")
|
|
55
55
|
APPLY_TREE = APPLY_TREES[0]
|
|
56
|
+
# Bundle metadata at the root of each adapter tree; never instance content.
|
|
57
|
+
BUNDLE_MANIFEST = "manifest.json"
|
|
58
|
+
# The harness settings adapter the bundle generates; an instance's richer
|
|
59
|
+
# committed copy is preserved on activation.
|
|
60
|
+
SETTINGS_ADAPTER = ".claude/settings.json"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _settings_carries_more(existing: Path, candidate: Path) -> bool:
|
|
64
|
+
"""True when the instance's settings file has hook events or permission
|
|
65
|
+
allows the candidate's generated adapter lacks. An unreadable candidate
|
|
66
|
+
keeps the existing file; an unreadable or missing existing file does not."""
|
|
67
|
+
if not existing.is_file():
|
|
68
|
+
return False
|
|
69
|
+
try:
|
|
70
|
+
current = json.loads(existing.read_text(encoding="utf-8"))
|
|
71
|
+
except (OSError, ValueError):
|
|
72
|
+
return False
|
|
73
|
+
try:
|
|
74
|
+
incoming = json.loads(candidate.read_text(encoding="utf-8"))
|
|
75
|
+
except (OSError, ValueError):
|
|
76
|
+
return True
|
|
77
|
+
if not isinstance(current, dict) or not isinstance(incoming, dict):
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
def hooks(doc: dict) -> set[str]:
|
|
81
|
+
value = doc.get("hooks")
|
|
82
|
+
return set(value.keys()) if isinstance(value, dict) else set()
|
|
83
|
+
|
|
84
|
+
def allows(doc: dict) -> set[str]:
|
|
85
|
+
perms = doc.get("permissions")
|
|
86
|
+
value = perms.get("allow") if isinstance(perms, dict) else None
|
|
87
|
+
return {str(v) for v in value} if isinstance(value, list) else set()
|
|
88
|
+
|
|
89
|
+
return bool(hooks(current) - hooks(incoming)) or bool(allows(current) - allows(incoming))
|
|
56
90
|
|
|
57
91
|
STATUS_PENDING = "pending"
|
|
58
92
|
STATUS_PREPARING = "preparing"
|
|
@@ -744,6 +778,22 @@ class UpgradeEngine:
|
|
|
744
778
|
continue
|
|
745
779
|
relative = str(source_file.relative_to(tree))
|
|
746
780
|
selected.setdefault(relative, source_file)
|
|
781
|
+
# The bundle's own manifest describes the package, not the instance;
|
|
782
|
+
# nothing in an instance reads it and it only shows up as an untracked
|
|
783
|
+
# file in the instance's repository.
|
|
784
|
+
selected.pop(BUNDLE_MANIFEST, None)
|
|
785
|
+
# The instance's own settings adapter may carry hook events or
|
|
786
|
+
# permission allows the generated public-safe adapter lacks (an org's
|
|
787
|
+
# committed .claude/settings.json). Activation never reduces it — the
|
|
788
|
+
# same rule the installer applies. A stale generated adapter (equal or
|
|
789
|
+
# a subset) still upgrades.
|
|
790
|
+
preserved: list[str] = []
|
|
791
|
+
candidate_settings = selected.get(SETTINGS_ADAPTER)
|
|
792
|
+
if candidate_settings is not None and _settings_carries_more(
|
|
793
|
+
instance / SETTINGS_ADAPTER, candidate_settings
|
|
794
|
+
):
|
|
795
|
+
selected.pop(SETTINGS_ADAPTER, None)
|
|
796
|
+
preserved.append(SETTINGS_ADAPTER)
|
|
747
797
|
# Journal opens the transaction BEFORE the first byte moves. From here
|
|
748
798
|
# until the caller clears it, a crash is recoverable from the backup.
|
|
749
799
|
self.store.write_apply_journal({
|
|
@@ -794,7 +844,12 @@ class UpgradeEngine:
|
|
|
794
844
|
(self.store.root / "rollback-files.tar.gz").unlink(missing_ok=True)
|
|
795
845
|
(self.store.root / "rollback-manifest.json").unlink(missing_ok=True)
|
|
796
846
|
raise
|
|
797
|
-
return {
|
|
847
|
+
return {
|
|
848
|
+
"applied_files": len(selected),
|
|
849
|
+
"replaced": len(replaced),
|
|
850
|
+
"added": len(added),
|
|
851
|
+
"preserved": preserved,
|
|
852
|
+
}
|
|
798
853
|
|
|
799
854
|
def activate(self) -> dict[str, Any]:
|
|
800
855
|
self._acquire_apply_lock()
|
|
@@ -886,7 +941,14 @@ class UpgradeEngine:
|
|
|
886
941
|
self.store.request_cancel()
|
|
887
942
|
worker = state.get("worker") or {}
|
|
888
943
|
pid = worker.get("pid")
|
|
889
|
-
|
|
944
|
+
status = state.get("status")
|
|
945
|
+
if status in (STATUS_PENDING, STATUS_READY, STATUS_FAILED):
|
|
946
|
+
# Nothing is running: release the staged candidate outright so a
|
|
947
|
+
# newer one can be staged. A ready or failed candidate used to be
|
|
948
|
+
# unreleasable — cancel ignored it and init refused "already staged".
|
|
949
|
+
state["status"] = STATUS_CANCELLED
|
|
950
|
+
self.store.write(state)
|
|
951
|
+
elif status == STATUS_PREPARING and not (pid and _pid_alive(pid)):
|
|
890
952
|
# No live worker will observe the flag — record the cancel now.
|
|
891
953
|
state["status"] = STATUS_CANCELLED
|
|
892
954
|
self.store.write(state)
|
|
@@ -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"
|
|
@@ -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
|
|
|
@@ -53,6 +53,40 @@ BUNDLE_TREES = ("claude", "codex", "pi", "prime")
|
|
|
53
53
|
# wins and duplicates are skipped.
|
|
54
54
|
APPLY_TREES = ("claude", "codex", "pi", "prime")
|
|
55
55
|
APPLY_TREE = APPLY_TREES[0]
|
|
56
|
+
# Bundle metadata at the root of each adapter tree; never instance content.
|
|
57
|
+
BUNDLE_MANIFEST = "manifest.json"
|
|
58
|
+
# The harness settings adapter the bundle generates; an instance's richer
|
|
59
|
+
# committed copy is preserved on activation.
|
|
60
|
+
SETTINGS_ADAPTER = ".claude/settings.json"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _settings_carries_more(existing: Path, candidate: Path) -> bool:
|
|
64
|
+
"""True when the instance's settings file has hook events or permission
|
|
65
|
+
allows the candidate's generated adapter lacks. An unreadable candidate
|
|
66
|
+
keeps the existing file; an unreadable or missing existing file does not."""
|
|
67
|
+
if not existing.is_file():
|
|
68
|
+
return False
|
|
69
|
+
try:
|
|
70
|
+
current = json.loads(existing.read_text(encoding="utf-8"))
|
|
71
|
+
except (OSError, ValueError):
|
|
72
|
+
return False
|
|
73
|
+
try:
|
|
74
|
+
incoming = json.loads(candidate.read_text(encoding="utf-8"))
|
|
75
|
+
except (OSError, ValueError):
|
|
76
|
+
return True
|
|
77
|
+
if not isinstance(current, dict) or not isinstance(incoming, dict):
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
def hooks(doc: dict) -> set[str]:
|
|
81
|
+
value = doc.get("hooks")
|
|
82
|
+
return set(value.keys()) if isinstance(value, dict) else set()
|
|
83
|
+
|
|
84
|
+
def allows(doc: dict) -> set[str]:
|
|
85
|
+
perms = doc.get("permissions")
|
|
86
|
+
value = perms.get("allow") if isinstance(perms, dict) else None
|
|
87
|
+
return {str(v) for v in value} if isinstance(value, list) else set()
|
|
88
|
+
|
|
89
|
+
return bool(hooks(current) - hooks(incoming)) or bool(allows(current) - allows(incoming))
|
|
56
90
|
|
|
57
91
|
STATUS_PENDING = "pending"
|
|
58
92
|
STATUS_PREPARING = "preparing"
|
|
@@ -744,6 +778,22 @@ class UpgradeEngine:
|
|
|
744
778
|
continue
|
|
745
779
|
relative = str(source_file.relative_to(tree))
|
|
746
780
|
selected.setdefault(relative, source_file)
|
|
781
|
+
# The bundle's own manifest describes the package, not the instance;
|
|
782
|
+
# nothing in an instance reads it and it only shows up as an untracked
|
|
783
|
+
# file in the instance's repository.
|
|
784
|
+
selected.pop(BUNDLE_MANIFEST, None)
|
|
785
|
+
# The instance's own settings adapter may carry hook events or
|
|
786
|
+
# permission allows the generated public-safe adapter lacks (an org's
|
|
787
|
+
# committed .claude/settings.json). Activation never reduces it — the
|
|
788
|
+
# same rule the installer applies. A stale generated adapter (equal or
|
|
789
|
+
# a subset) still upgrades.
|
|
790
|
+
preserved: list[str] = []
|
|
791
|
+
candidate_settings = selected.get(SETTINGS_ADAPTER)
|
|
792
|
+
if candidate_settings is not None and _settings_carries_more(
|
|
793
|
+
instance / SETTINGS_ADAPTER, candidate_settings
|
|
794
|
+
):
|
|
795
|
+
selected.pop(SETTINGS_ADAPTER, None)
|
|
796
|
+
preserved.append(SETTINGS_ADAPTER)
|
|
747
797
|
# Journal opens the transaction BEFORE the first byte moves. From here
|
|
748
798
|
# until the caller clears it, a crash is recoverable from the backup.
|
|
749
799
|
self.store.write_apply_journal({
|
|
@@ -794,7 +844,12 @@ class UpgradeEngine:
|
|
|
794
844
|
(self.store.root / "rollback-files.tar.gz").unlink(missing_ok=True)
|
|
795
845
|
(self.store.root / "rollback-manifest.json").unlink(missing_ok=True)
|
|
796
846
|
raise
|
|
797
|
-
return {
|
|
847
|
+
return {
|
|
848
|
+
"applied_files": len(selected),
|
|
849
|
+
"replaced": len(replaced),
|
|
850
|
+
"added": len(added),
|
|
851
|
+
"preserved": preserved,
|
|
852
|
+
}
|
|
798
853
|
|
|
799
854
|
def activate(self) -> dict[str, Any]:
|
|
800
855
|
self._acquire_apply_lock()
|
|
@@ -886,7 +941,14 @@ class UpgradeEngine:
|
|
|
886
941
|
self.store.request_cancel()
|
|
887
942
|
worker = state.get("worker") or {}
|
|
888
943
|
pid = worker.get("pid")
|
|
889
|
-
|
|
944
|
+
status = state.get("status")
|
|
945
|
+
if status in (STATUS_PENDING, STATUS_READY, STATUS_FAILED):
|
|
946
|
+
# Nothing is running: release the staged candidate outright so a
|
|
947
|
+
# newer one can be staged. A ready or failed candidate used to be
|
|
948
|
+
# unreleasable — cancel ignored it and init refused "already staged".
|
|
949
|
+
state["status"] = STATUS_CANCELLED
|
|
950
|
+
self.store.write(state)
|
|
951
|
+
elif status == STATUS_PREPARING and not (pid and _pid_alive(pid)):
|
|
890
952
|
# No live worker will observe the flag — record the cancel now.
|
|
891
953
|
state["status"] = STATUS_CANCELLED
|
|
892
954
|
self.store.write(state)
|
|
@@ -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"
|
|
@@ -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
|
|
|
@@ -53,6 +53,40 @@ BUNDLE_TREES = ("claude", "codex", "pi", "prime")
|
|
|
53
53
|
# wins and duplicates are skipped.
|
|
54
54
|
APPLY_TREES = ("claude", "codex", "pi", "prime")
|
|
55
55
|
APPLY_TREE = APPLY_TREES[0]
|
|
56
|
+
# Bundle metadata at the root of each adapter tree; never instance content.
|
|
57
|
+
BUNDLE_MANIFEST = "manifest.json"
|
|
58
|
+
# The harness settings adapter the bundle generates; an instance's richer
|
|
59
|
+
# committed copy is preserved on activation.
|
|
60
|
+
SETTINGS_ADAPTER = ".claude/settings.json"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _settings_carries_more(existing: Path, candidate: Path) -> bool:
|
|
64
|
+
"""True when the instance's settings file has hook events or permission
|
|
65
|
+
allows the candidate's generated adapter lacks. An unreadable candidate
|
|
66
|
+
keeps the existing file; an unreadable or missing existing file does not."""
|
|
67
|
+
if not existing.is_file():
|
|
68
|
+
return False
|
|
69
|
+
try:
|
|
70
|
+
current = json.loads(existing.read_text(encoding="utf-8"))
|
|
71
|
+
except (OSError, ValueError):
|
|
72
|
+
return False
|
|
73
|
+
try:
|
|
74
|
+
incoming = json.loads(candidate.read_text(encoding="utf-8"))
|
|
75
|
+
except (OSError, ValueError):
|
|
76
|
+
return True
|
|
77
|
+
if not isinstance(current, dict) or not isinstance(incoming, dict):
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
def hooks(doc: dict) -> set[str]:
|
|
81
|
+
value = doc.get("hooks")
|
|
82
|
+
return set(value.keys()) if isinstance(value, dict) else set()
|
|
83
|
+
|
|
84
|
+
def allows(doc: dict) -> set[str]:
|
|
85
|
+
perms = doc.get("permissions")
|
|
86
|
+
value = perms.get("allow") if isinstance(perms, dict) else None
|
|
87
|
+
return {str(v) for v in value} if isinstance(value, list) else set()
|
|
88
|
+
|
|
89
|
+
return bool(hooks(current) - hooks(incoming)) or bool(allows(current) - allows(incoming))
|
|
56
90
|
|
|
57
91
|
STATUS_PENDING = "pending"
|
|
58
92
|
STATUS_PREPARING = "preparing"
|
|
@@ -744,6 +778,22 @@ class UpgradeEngine:
|
|
|
744
778
|
continue
|
|
745
779
|
relative = str(source_file.relative_to(tree))
|
|
746
780
|
selected.setdefault(relative, source_file)
|
|
781
|
+
# The bundle's own manifest describes the package, not the instance;
|
|
782
|
+
# nothing in an instance reads it and it only shows up as an untracked
|
|
783
|
+
# file in the instance's repository.
|
|
784
|
+
selected.pop(BUNDLE_MANIFEST, None)
|
|
785
|
+
# The instance's own settings adapter may carry hook events or
|
|
786
|
+
# permission allows the generated public-safe adapter lacks (an org's
|
|
787
|
+
# committed .claude/settings.json). Activation never reduces it — the
|
|
788
|
+
# same rule the installer applies. A stale generated adapter (equal or
|
|
789
|
+
# a subset) still upgrades.
|
|
790
|
+
preserved: list[str] = []
|
|
791
|
+
candidate_settings = selected.get(SETTINGS_ADAPTER)
|
|
792
|
+
if candidate_settings is not None and _settings_carries_more(
|
|
793
|
+
instance / SETTINGS_ADAPTER, candidate_settings
|
|
794
|
+
):
|
|
795
|
+
selected.pop(SETTINGS_ADAPTER, None)
|
|
796
|
+
preserved.append(SETTINGS_ADAPTER)
|
|
747
797
|
# Journal opens the transaction BEFORE the first byte moves. From here
|
|
748
798
|
# until the caller clears it, a crash is recoverable from the backup.
|
|
749
799
|
self.store.write_apply_journal({
|
|
@@ -794,7 +844,12 @@ class UpgradeEngine:
|
|
|
794
844
|
(self.store.root / "rollback-files.tar.gz").unlink(missing_ok=True)
|
|
795
845
|
(self.store.root / "rollback-manifest.json").unlink(missing_ok=True)
|
|
796
846
|
raise
|
|
797
|
-
return {
|
|
847
|
+
return {
|
|
848
|
+
"applied_files": len(selected),
|
|
849
|
+
"replaced": len(replaced),
|
|
850
|
+
"added": len(added),
|
|
851
|
+
"preserved": preserved,
|
|
852
|
+
}
|
|
798
853
|
|
|
799
854
|
def activate(self) -> dict[str, Any]:
|
|
800
855
|
self._acquire_apply_lock()
|
|
@@ -886,7 +941,14 @@ class UpgradeEngine:
|
|
|
886
941
|
self.store.request_cancel()
|
|
887
942
|
worker = state.get("worker") or {}
|
|
888
943
|
pid = worker.get("pid")
|
|
889
|
-
|
|
944
|
+
status = state.get("status")
|
|
945
|
+
if status in (STATUS_PENDING, STATUS_READY, STATUS_FAILED):
|
|
946
|
+
# Nothing is running: release the staged candidate outright so a
|
|
947
|
+
# newer one can be staged. A ready or failed candidate used to be
|
|
948
|
+
# unreleasable — cancel ignored it and init refused "already staged".
|
|
949
|
+
state["status"] = STATUS_CANCELLED
|
|
950
|
+
self.store.write(state)
|
|
951
|
+
elif status == STATUS_PREPARING and not (pid and _pid_alive(pid)):
|
|
890
952
|
# No live worker will observe the flag — record the cancel now.
|
|
891
953
|
state["status"] = STATUS_CANCELLED
|
|
892
954
|
self.store.write(state)
|
|
@@ -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"
|
|
@@ -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
|
|
|
@@ -53,6 +53,40 @@ BUNDLE_TREES = ("claude", "codex", "pi", "prime")
|
|
|
53
53
|
# wins and duplicates are skipped.
|
|
54
54
|
APPLY_TREES = ("claude", "codex", "pi", "prime")
|
|
55
55
|
APPLY_TREE = APPLY_TREES[0]
|
|
56
|
+
# Bundle metadata at the root of each adapter tree; never instance content.
|
|
57
|
+
BUNDLE_MANIFEST = "manifest.json"
|
|
58
|
+
# The harness settings adapter the bundle generates; an instance's richer
|
|
59
|
+
# committed copy is preserved on activation.
|
|
60
|
+
SETTINGS_ADAPTER = ".claude/settings.json"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _settings_carries_more(existing: Path, candidate: Path) -> bool:
|
|
64
|
+
"""True when the instance's settings file has hook events or permission
|
|
65
|
+
allows the candidate's generated adapter lacks. An unreadable candidate
|
|
66
|
+
keeps the existing file; an unreadable or missing existing file does not."""
|
|
67
|
+
if not existing.is_file():
|
|
68
|
+
return False
|
|
69
|
+
try:
|
|
70
|
+
current = json.loads(existing.read_text(encoding="utf-8"))
|
|
71
|
+
except (OSError, ValueError):
|
|
72
|
+
return False
|
|
73
|
+
try:
|
|
74
|
+
incoming = json.loads(candidate.read_text(encoding="utf-8"))
|
|
75
|
+
except (OSError, ValueError):
|
|
76
|
+
return True
|
|
77
|
+
if not isinstance(current, dict) or not isinstance(incoming, dict):
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
def hooks(doc: dict) -> set[str]:
|
|
81
|
+
value = doc.get("hooks")
|
|
82
|
+
return set(value.keys()) if isinstance(value, dict) else set()
|
|
83
|
+
|
|
84
|
+
def allows(doc: dict) -> set[str]:
|
|
85
|
+
perms = doc.get("permissions")
|
|
86
|
+
value = perms.get("allow") if isinstance(perms, dict) else None
|
|
87
|
+
return {str(v) for v in value} if isinstance(value, list) else set()
|
|
88
|
+
|
|
89
|
+
return bool(hooks(current) - hooks(incoming)) or bool(allows(current) - allows(incoming))
|
|
56
90
|
|
|
57
91
|
STATUS_PENDING = "pending"
|
|
58
92
|
STATUS_PREPARING = "preparing"
|
|
@@ -744,6 +778,22 @@ class UpgradeEngine:
|
|
|
744
778
|
continue
|
|
745
779
|
relative = str(source_file.relative_to(tree))
|
|
746
780
|
selected.setdefault(relative, source_file)
|
|
781
|
+
# The bundle's own manifest describes the package, not the instance;
|
|
782
|
+
# nothing in an instance reads it and it only shows up as an untracked
|
|
783
|
+
# file in the instance's repository.
|
|
784
|
+
selected.pop(BUNDLE_MANIFEST, None)
|
|
785
|
+
# The instance's own settings adapter may carry hook events or
|
|
786
|
+
# permission allows the generated public-safe adapter lacks (an org's
|
|
787
|
+
# committed .claude/settings.json). Activation never reduces it — the
|
|
788
|
+
# same rule the installer applies. A stale generated adapter (equal or
|
|
789
|
+
# a subset) still upgrades.
|
|
790
|
+
preserved: list[str] = []
|
|
791
|
+
candidate_settings = selected.get(SETTINGS_ADAPTER)
|
|
792
|
+
if candidate_settings is not None and _settings_carries_more(
|
|
793
|
+
instance / SETTINGS_ADAPTER, candidate_settings
|
|
794
|
+
):
|
|
795
|
+
selected.pop(SETTINGS_ADAPTER, None)
|
|
796
|
+
preserved.append(SETTINGS_ADAPTER)
|
|
747
797
|
# Journal opens the transaction BEFORE the first byte moves. From here
|
|
748
798
|
# until the caller clears it, a crash is recoverable from the backup.
|
|
749
799
|
self.store.write_apply_journal({
|
|
@@ -794,7 +844,12 @@ class UpgradeEngine:
|
|
|
794
844
|
(self.store.root / "rollback-files.tar.gz").unlink(missing_ok=True)
|
|
795
845
|
(self.store.root / "rollback-manifest.json").unlink(missing_ok=True)
|
|
796
846
|
raise
|
|
797
|
-
return {
|
|
847
|
+
return {
|
|
848
|
+
"applied_files": len(selected),
|
|
849
|
+
"replaced": len(replaced),
|
|
850
|
+
"added": len(added),
|
|
851
|
+
"preserved": preserved,
|
|
852
|
+
}
|
|
798
853
|
|
|
799
854
|
def activate(self) -> dict[str, Any]:
|
|
800
855
|
self._acquire_apply_lock()
|
|
@@ -886,7 +941,14 @@ class UpgradeEngine:
|
|
|
886
941
|
self.store.request_cancel()
|
|
887
942
|
worker = state.get("worker") or {}
|
|
888
943
|
pid = worker.get("pid")
|
|
889
|
-
|
|
944
|
+
status = state.get("status")
|
|
945
|
+
if status in (STATUS_PENDING, STATUS_READY, STATUS_FAILED):
|
|
946
|
+
# Nothing is running: release the staged candidate outright so a
|
|
947
|
+
# newer one can be staged. A ready or failed candidate used to be
|
|
948
|
+
# unreleasable — cancel ignored it and init refused "already staged".
|
|
949
|
+
state["status"] = STATUS_CANCELLED
|
|
950
|
+
self.store.write(state)
|
|
951
|
+
elif status == STATUS_PREPARING and not (pid and _pid_alive(pid)):
|
|
890
952
|
# No live worker will observe the flag — record the cancel now.
|
|
891
953
|
state["status"] = STATUS_CANCELLED
|
|
892
954
|
self.store.write(state)
|