create-egregore 0.21.0-runtime-mvp.9 → 0.21.0-runtime-mvp.11
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 +3 -3
- package/lib/claude-runtime.js +23 -1
- package/package.json +1 -1
- package/runtime/claude/bin/lib/git-sync.sh +11 -1
- package/runtime/claude/bin/lib/greeting.sh +8 -0
- package/runtime/claude/bin/lib/settings-drift.sh +116 -0
- package/runtime/claude/bin/search.sh +15 -0
- package/runtime/claude/bin/settings.sh +3 -3
- package/runtime/claude/bin/telemetry.sh +36 -23
- package/runtime/claude/egregore_runtime/issue_cli.py +3 -3
- package/runtime/claude/manifest.json +1 -0
- package/runtime/codex/bin/lib/git-sync.sh +11 -1
- package/runtime/codex/bin/lib/greeting.sh +8 -0
- package/runtime/codex/bin/lib/settings-drift.sh +116 -0
- package/runtime/codex/bin/search.sh +15 -0
- package/runtime/codex/bin/settings.sh +3 -3
- package/runtime/codex/bin/telemetry.sh +36 -23
- package/runtime/codex/egregore_runtime/issue_cli.py +3 -3
- package/runtime/codex/manifest.json +1 -0
- package/runtime/pi/bin/lib/git-sync.sh +11 -1
- package/runtime/pi/bin/lib/greeting.sh +8 -0
- package/runtime/pi/bin/lib/settings-drift.sh +116 -0
- package/runtime/pi/bin/search.sh +15 -0
- package/runtime/pi/bin/settings.sh +3 -3
- package/runtime/pi/bin/telemetry.sh +36 -23
- package/runtime/pi/egregore_runtime/issue_cli.py +3 -3
- package/runtime/pi/manifest.json +1 -0
- package/runtime/prime/bin/lib/git-sync.sh +11 -1
- package/runtime/prime/bin/lib/greeting.sh +8 -0
- package/runtime/prime/bin/lib/settings-drift.sh +116 -0
- package/runtime/prime/bin/search.sh +15 -0
- package/runtime/prime/bin/settings.sh +3 -3
- package/runtime/prime/bin/telemetry.sh +36 -23
- package/runtime/prime/egregore_runtime/issue_cli.py +3 -3
- 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.11
|
|
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.11";
|
|
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");
|
|
@@ -4046,7 +4046,7 @@ async function browser(args = {}) {
|
|
|
4046
4046
|
}
|
|
4047
4047
|
if (chunk === "s" && proposal && proposal.token) {
|
|
4048
4048
|
S.settingsAction = "tele-share";
|
|
4049
|
-
S.settingsInput = `Send ${proposal.events} event(s)
|
|
4049
|
+
S.settingsInput = `Send ${proposal.events} event(s) now? Local events are retained. [y/N]`;
|
|
4050
4050
|
render(); return;
|
|
4051
4051
|
}
|
|
4052
4052
|
if (key.name === "escape" || key.name === "left") {
|
package/lib/claude-runtime.js
CHANGED
|
@@ -76,6 +76,24 @@ function copyFile(src, dest) {
|
|
|
76
76
|
try { fs.chmodSync(dest, fs.statSync(src).mode & 0o777); } catch {}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
// The generated settings adapter may replace a stale template settings.json —
|
|
80
|
+
// one carrying nothing the adapter lacks — but an org's customized settings
|
|
81
|
+
// (extra hook events or permission allows, e.g. PreCompact/Stop) are
|
|
82
|
+
// authoritative and are never adopted, even when clean and tracked.
|
|
83
|
+
function settingsSafeToAdopt(destFile, srcFile) {
|
|
84
|
+
try {
|
|
85
|
+
const local = JSON.parse(fs.readFileSync(destFile, "utf8"));
|
|
86
|
+
const bundle = JSON.parse(fs.readFileSync(srcFile, "utf8"));
|
|
87
|
+
const bundleHooks = bundle.hooks || {};
|
|
88
|
+
const extraHooks = Object.keys(local.hooks || {}).filter((k) => !(k in bundleHooks));
|
|
89
|
+
const bundleAllow = new Set(bundle.permissions?.allow || []);
|
|
90
|
+
const extraAllow = (local.permissions?.allow || []).filter((entry) => !bundleAllow.has(entry));
|
|
91
|
+
return extraHooks.length === 0 && extraAllow.length === 0;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
79
97
|
// One-time ownership migration for pre-bundle Claude installs. A clean,
|
|
80
98
|
// tracked path is part of the cloned framework template and may be adopted by
|
|
81
99
|
// the package. Dirty/untracked paths are user work and remain untouched.
|
|
@@ -153,7 +171,11 @@ function installClaudeRuntime(egregoreDir) {
|
|
|
153
171
|
summary.skipped += 1; record();
|
|
154
172
|
} else if (previousHash && currentHash === previousHash) {
|
|
155
173
|
copyFile(src, dest); summary.updated += 1; record();
|
|
156
|
-
} else if (
|
|
174
|
+
} else if (
|
|
175
|
+
!previousHash
|
|
176
|
+
&& (rel.replaceAll("\\", "/") !== ".claude/settings.json" || settingsSafeToAdopt(dest, src))
|
|
177
|
+
&& isCleanTrackedFrameworkFile(egregoreDir, rel)
|
|
178
|
+
) {
|
|
157
179
|
copyFile(src, dest); summary.updated += 1; record();
|
|
158
180
|
} else {
|
|
159
181
|
summary.conflicted += 1; summary.conflicts.push(rel);
|
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.11",
|
|
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": {
|
|
@@ -24,6 +24,10 @@ FRAMEWORK_UPDATED_FILES=""
|
|
|
24
24
|
HEALTH_RETRIEVAL="skip"
|
|
25
25
|
SYNC_RECEIPT=""
|
|
26
26
|
|
|
27
|
+
# Settings drift healer + unshared-settings reporting (see the lib header).
|
|
28
|
+
# shellcheck source=bin/lib/settings-drift.sh
|
|
29
|
+
source "$SCRIPT_DIR/bin/lib/settings-drift.sh"
|
|
30
|
+
|
|
27
31
|
# The core repo's integration branch: "develop" unless egregore.json sets
|
|
28
32
|
# base_branch. session-start.sh resolves this before sourcing; resolve it here
|
|
29
33
|
# too so the lib stands alone for other callers.
|
|
@@ -324,8 +328,14 @@ setup_develop() {
|
|
|
324
328
|
return 1
|
|
325
329
|
fi
|
|
326
330
|
else
|
|
327
|
-
# Already on it — fetch couldn't update a checked-out branch, so pull
|
|
331
|
+
# Already on it — fetch couldn't update a checked-out branch, so pull.
|
|
332
|
+
# Launcher/installer settings drift blocks this merge and the refusal
|
|
333
|
+
# is otherwise silent; heal it first, re-apply the launcher's intent
|
|
334
|
+
# after (bin/lib/settings-drift.sh).
|
|
335
|
+
settings_drift_heal
|
|
328
336
|
git merge --ff-only "origin/$BASE_BRANCH" --quiet 2>/dev/null || true
|
|
337
|
+
settings_drift_reapply
|
|
338
|
+
settings_drift_report
|
|
329
339
|
fi
|
|
330
340
|
|
|
331
341
|
# Never report the intended branch as the actual branch. A checkout can fail
|
|
@@ -95,6 +95,14 @@ if [ -n "$SAVED_BRANCH" ]; then
|
|
|
95
95
|
echo " ✓ Auto-saved uncommitted work on $SAVED_BRANCH"
|
|
96
96
|
fi
|
|
97
97
|
|
|
98
|
+
# --- Unshared org-settings drift (launcher writes not yet shared) ---
|
|
99
|
+
if [ "${SETTINGS_ADAPTER_RESTORED:-false}" = "true" ]; then
|
|
100
|
+
echo " ◇ restored org .claude/settings.json — a generated adapter had replaced it locally"
|
|
101
|
+
fi
|
|
102
|
+
if [ -n "${UNSHARED_SETTINGS_KEYS:-}" ]; then
|
|
103
|
+
echo " ◇ org settings changed on this machine (${UNSHARED_SETTINGS_KEYS}) — say \"share settings\" to make them org-wide"
|
|
104
|
+
fi
|
|
105
|
+
|
|
98
106
|
# --- Footer: health + repos + memory (tertiary) ---
|
|
99
107
|
echo "$SEPARATOR"
|
|
100
108
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# settings-drift.sh — heal launcher/installer settings drift around the
|
|
3
|
+
# base-branch fast-forward, and surface unshared org-settings changes.
|
|
4
|
+
#
|
|
5
|
+
# Two org-config surfaces accumulate legitimate local modifications outside
|
|
6
|
+
# any session: .claude/settings.json (the installer's generated public-safe
|
|
7
|
+
# adapter can replace the org's richer committed file) and egregore.json
|
|
8
|
+
# (launcher settings verbs: posture, workflow, people removals). Local
|
|
9
|
+
# modifications to files a fast-forward also changes make git refuse the
|
|
10
|
+
# merge — and that refusal used to be silently swallowed, leaving the
|
|
11
|
+
# checkout behind with no explanation.
|
|
12
|
+
#
|
|
13
|
+
# Contract (sourced; caller provides $BASE_BRANCH and runs from the repo
|
|
14
|
+
# root):
|
|
15
|
+
# settings_drift_heal — before the ff-merge. Restores settings.json
|
|
16
|
+
# only when the local copy is a reduced
|
|
17
|
+
# generated adapter (its hook events and
|
|
18
|
+
# permission allows are a subset of the
|
|
19
|
+
# committed file's). Snapshots egregore.json
|
|
20
|
+
# and returns it to HEAD so the merge can run.
|
|
21
|
+
# settings_drift_reapply — after the ff-merge. Re-applies the snapshot's
|
|
22
|
+
# changed settings-owned keys on top of the
|
|
23
|
+
# incoming egregore.json.
|
|
24
|
+
# settings_drift_report — sets UNSHARED_SETTINGS_KEYS to the
|
|
25
|
+
# settings-owned keys where the working file
|
|
26
|
+
# differs from HEAD, for the greeting.
|
|
27
|
+
#
|
|
28
|
+
# Anything dirty beyond these two files is user work and is never touched.
|
|
29
|
+
|
|
30
|
+
_SETTINGS_OWNED_KEYS='["boundary","base_branch","people_removed","features","repos","admins","people"]'
|
|
31
|
+
_SETTINGS_DRIFT_SNAPSHOT=""
|
|
32
|
+
_SETTINGS_DRIFT_BASE=""
|
|
33
|
+
# Read by bin/lib/greeting.sh after git-sync completes.
|
|
34
|
+
export SETTINGS_ADAPTER_RESTORED="false"
|
|
35
|
+
export UNSHARED_SETTINGS_KEYS=""
|
|
36
|
+
|
|
37
|
+
# The generated adapter only ever removes relative to the org file: fewer
|
|
38
|
+
# hook events, fewer permission allows. A local settings.json that carries
|
|
39
|
+
# anything the committed file lacks is user work and stays untouched.
|
|
40
|
+
_settings_is_reduced_adapter() {
|
|
41
|
+
local tmp_head
|
|
42
|
+
tmp_head=$(mktemp) || return 1
|
|
43
|
+
if ! git show "HEAD:.claude/settings.json" > "$tmp_head" 2>/dev/null; then
|
|
44
|
+
rm -f "$tmp_head"
|
|
45
|
+
return 1
|
|
46
|
+
fi
|
|
47
|
+
jq -es '
|
|
48
|
+
.[0] as $local | .[1] as $head |
|
|
49
|
+
($local != $head) and
|
|
50
|
+
((($local.hooks // {}) | keys) - (($head.hooks // {}) | keys) | length == 0) and
|
|
51
|
+
((($local.permissions.allow // []) - ($head.permissions.allow // [])) | length == 0)
|
|
52
|
+
' .claude/settings.json "$tmp_head" >/dev/null 2>&1
|
|
53
|
+
local verdict=$?
|
|
54
|
+
rm -f "$tmp_head"
|
|
55
|
+
return "$verdict"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
settings_drift_heal() {
|
|
59
|
+
git rev-parse -q --verify "origin/$BASE_BRANCH" >/dev/null 2>&1 || return 0
|
|
60
|
+
[ "$(git rev-parse HEAD 2>/dev/null)" = "$(git rev-parse "origin/$BASE_BRANCH" 2>/dev/null)" ] && return 0
|
|
61
|
+
|
|
62
|
+
if ! git diff --quiet -- .claude/settings.json 2>/dev/null; then
|
|
63
|
+
if _settings_is_reduced_adapter; then
|
|
64
|
+
git checkout -- .claude/settings.json 2>/dev/null && SETTINGS_ADAPTER_RESTORED="true"
|
|
65
|
+
fi
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
if ! git diff --quiet -- egregore.json 2>/dev/null; then
|
|
69
|
+
_SETTINGS_DRIFT_SNAPSHOT=$(cat egregore.json 2>/dev/null || true)
|
|
70
|
+
_SETTINGS_DRIFT_BASE=$(git show "HEAD:egregore.json" 2>/dev/null || true)
|
|
71
|
+
if [ -n "$_SETTINGS_DRIFT_SNAPSHOT" ] && [ -n "$_SETTINGS_DRIFT_BASE" ] \
|
|
72
|
+
&& printf '%s' "$_SETTINGS_DRIFT_SNAPSHOT" | jq -e . >/dev/null 2>&1; then
|
|
73
|
+
git checkout -- egregore.json 2>/dev/null || _SETTINGS_DRIFT_SNAPSHOT=""
|
|
74
|
+
else
|
|
75
|
+
_SETTINGS_DRIFT_SNAPSHOT=""
|
|
76
|
+
fi
|
|
77
|
+
fi
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
settings_drift_reapply() {
|
|
81
|
+
[ -n "$_SETTINGS_DRIFT_SNAPSHOT" ] || return 0
|
|
82
|
+
local tmp_local tmp_base merged
|
|
83
|
+
tmp_local=$(mktemp) && tmp_base=$(mktemp) || return 0
|
|
84
|
+
printf '%s' "$_SETTINGS_DRIFT_SNAPSHOT" > "$tmp_local"
|
|
85
|
+
printf '%s' "$_SETTINGS_DRIFT_BASE" > "$tmp_base"
|
|
86
|
+
merged=$(jq -s --argjson keys "$_SETTINGS_OWNED_KEYS" '
|
|
87
|
+
.[0] as $incoming | .[1] as $local | .[2] as $base |
|
|
88
|
+
reduce $keys[] as $k ($incoming;
|
|
89
|
+
if ($local[$k] // null) != ($base[$k] // null) then
|
|
90
|
+
if ($local | has($k)) then .[$k] = $local[$k] else del(.[$k]) end
|
|
91
|
+
else . end)
|
|
92
|
+
' egregore.json "$tmp_local" "$tmp_base" 2>/dev/null || true)
|
|
93
|
+
rm -f "$tmp_local" "$tmp_base"
|
|
94
|
+
if [ -n "$merged" ] && printf '%s' "$merged" | jq -e . >/dev/null 2>&1; then
|
|
95
|
+
printf '%s\n' "$merged" > egregore.json
|
|
96
|
+
fi
|
|
97
|
+
_SETTINGS_DRIFT_SNAPSHOT=""
|
|
98
|
+
_SETTINGS_DRIFT_BASE=""
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
settings_drift_report() {
|
|
102
|
+
UNSHARED_SETTINGS_KEYS=""
|
|
103
|
+
git diff --quiet -- egregore.json 2>/dev/null && return 0
|
|
104
|
+
local tmp_head
|
|
105
|
+
tmp_head=$(mktemp) || return 0
|
|
106
|
+
if ! git show "HEAD:egregore.json" > "$tmp_head" 2>/dev/null; then
|
|
107
|
+
rm -f "$tmp_head"
|
|
108
|
+
return 0
|
|
109
|
+
fi
|
|
110
|
+
UNSHARED_SETTINGS_KEYS=$(jq -rs --argjson keys "$_SETTINGS_OWNED_KEYS" '
|
|
111
|
+
.[0] as $local | .[1] as $head |
|
|
112
|
+
[$keys[] | select(($local[.] // null) != ($head[.] // null))] | join(", ")
|
|
113
|
+
' egregore.json "$tmp_head" 2>/dev/null || true)
|
|
114
|
+
rm -f "$tmp_head"
|
|
115
|
+
export UNSHARED_SETTINGS_KEYS
|
|
116
|
+
}
|
|
@@ -235,6 +235,21 @@ cmd_query() {
|
|
|
235
235
|
} >&2
|
|
236
236
|
return "$query_rc"
|
|
237
237
|
fi
|
|
238
|
+
# Content-free query telemetry: mode, duration, result count. Never the
|
|
239
|
+
# query text. This is what makes Runtime/QMD retrieval visible in the
|
|
240
|
+
# telemetry event list instead of only legacy graph_query rows.
|
|
241
|
+
local _tele_count _tele_ms
|
|
242
|
+
_tele_ms=$(( ($(date +%s) - t0) * 1000 ))
|
|
243
|
+
_tele_count=$(printf '%s' "$out" | jq 'if type == "array" then length elif type == "object" then (.results // [] | length) else empty end' 2>/dev/null || true)
|
|
244
|
+
if [[ "$_tele_count" =~ ^[0-9]+$ ]]; then
|
|
245
|
+
bash "$SCRIPT_DIR/bin/telemetry.sh" emit "query" \
|
|
246
|
+
"$(jq -n --arg mode "$label" --argjson d "$_tele_ms" --argjson c "$_tele_count" \
|
|
247
|
+
'{mode:$mode, duration_ms:$d, count:$c}')" 2>/dev/null &
|
|
248
|
+
else
|
|
249
|
+
bash "$SCRIPT_DIR/bin/telemetry.sh" emit "query" \
|
|
250
|
+
"$(jq -n --arg mode "$label" --argjson d "$_tele_ms" '{mode:$mode, duration_ms:$d}')" 2>/dev/null &
|
|
251
|
+
fi
|
|
252
|
+
|
|
238
253
|
[ "$enrich" = 1 ] && emit="_enrich"
|
|
239
254
|
if [ "$context_packet" = 1 ]; then
|
|
240
255
|
local packet
|
|
@@ -146,7 +146,7 @@ cmd_workflow() {
|
|
|
146
146
|
|| _die "cannot switch workflow: origin/main does not exist or could not be reached" 1
|
|
147
147
|
_save '.base_branch = "main"'
|
|
148
148
|
fi
|
|
149
|
-
echo "Git workflow: SIMPLE — pull requests now target main"
|
|
149
|
+
echo "Git workflow: SIMPLE — pull requests now target main · /save shares it with the org"
|
|
150
150
|
;;
|
|
151
151
|
staged)
|
|
152
152
|
if [ "$base" != "develop" ]; then
|
|
@@ -160,7 +160,7 @@ cmd_workflow() {
|
|
|
160
160
|
elif ! jq -e 'has("base_branch")' "$CONFIG" >/dev/null 2>&1; then
|
|
161
161
|
_save '.base_branch = "develop"'
|
|
162
162
|
fi
|
|
163
|
-
echo "Git workflow: STAGED — pull requests now target develop; releases promote to main"
|
|
163
|
+
echo "Git workflow: STAGED — pull requests now target develop; releases promote to main · /save shares it with the org"
|
|
164
164
|
;;
|
|
165
165
|
*) _die "usage: settings.sh workflow status|simple|staged" 1 ;;
|
|
166
166
|
esac
|
|
@@ -468,7 +468,7 @@ cmd_posture() {
|
|
|
468
468
|
if [ "$action" != "$current" ]; then
|
|
469
469
|
_save '.boundary = ((.boundary // {}) + {posture: $p})' --arg p "$action"
|
|
470
470
|
fi
|
|
471
|
-
echo "Boundary posture: $action"
|
|
471
|
+
echo "Boundary posture: $action · applies to new sessions · /save shares it with the org"
|
|
472
472
|
;;
|
|
473
473
|
*) _die "usage: settings.sh posture status|strict|standard|open" 1 ;;
|
|
474
474
|
esac
|
|
@@ -27,6 +27,10 @@ fi
|
|
|
27
27
|
BUFFER_FILE="$BUFFER_DIR/telemetry.jsonl"
|
|
28
28
|
LOCK_DIR="$BUFFER_DIR/telemetry.lock"
|
|
29
29
|
SHARE_CONSENT_FILE="$BUFFER_DIR/telemetry-share-consent.json"
|
|
30
|
+
# Exact dataset pinned at proposal time. The confirm step sends this snapshot,
|
|
31
|
+
# so events emitted between proposal and confirm (attendant, other sessions)
|
|
32
|
+
# can never invalidate — or silently join — a disclosed share.
|
|
33
|
+
SHARE_STAGED_FILE="$BUFFER_DIR/telemetry.share-staged.jsonl"
|
|
30
34
|
STATE_FILE="${EGREGORE_STATE_FILE:-$SCRIPT_DIR/.egregore-state.json}"
|
|
31
35
|
CONFIG="${EGREGORE_CONFIG_FILE:-$SCRIPT_DIR/egregore.json}"
|
|
32
36
|
ENV_FILE="${EGREGORE_ENV_FILE:-$SCRIPT_DIR/.env}"
|
|
@@ -230,7 +234,8 @@ _validate_metrics() {
|
|
|
230
234
|
}
|
|
231
235
|
|
|
232
236
|
_validate_buffer() {
|
|
233
|
-
|
|
237
|
+
local target="${1:-$BUFFER_FILE}"
|
|
238
|
+
[ -s "$target" ] || return 0
|
|
234
239
|
jq -e --arg schema "$SCHEMA_VERSION" --argjson allowed "$ALLOWED_METRICS_JSON" '
|
|
235
240
|
def safe_id:
|
|
236
241
|
type == "string" and length > 0 and length <= 256 and
|
|
@@ -264,7 +269,7 @@ _validate_buffer() {
|
|
|
264
269
|
(.data | safe_metrics) and (.ts | type == "string") and
|
|
265
270
|
(.type | safe_id) and (.sid | safe_id) and (.org | safe_id) and (.user | safe_id)
|
|
266
271
|
end
|
|
267
|
-
' "$
|
|
272
|
+
' "$target" >/dev/null 2>&1
|
|
268
273
|
}
|
|
269
274
|
|
|
270
275
|
cmd_emit() {
|
|
@@ -422,7 +427,7 @@ cmd_clear() {
|
|
|
422
427
|
echo "Could not acquire the telemetry lock; nothing was cleared." >&2
|
|
423
428
|
return 1
|
|
424
429
|
fi
|
|
425
|
-
rm -f "$BUFFER_FILE" "$SHARE_CONSENT_FILE"
|
|
430
|
+
rm -f "$BUFFER_FILE" "$SHARE_CONSENT_FILE" "$SHARE_STAGED_FILE"
|
|
426
431
|
_unlock
|
|
427
432
|
echo "Local telemetry buffer cleared."
|
|
428
433
|
}
|
|
@@ -525,31 +530,35 @@ cmd_share() {
|
|
|
525
530
|
https://*|http://127.0.0.1*|http://localhost*|http://\[::1\]*) ;;
|
|
526
531
|
*) echo "Share endpoint must use https (or loopback http)." >&2; return 2 ;;
|
|
527
532
|
esac
|
|
528
|
-
|
|
533
|
+
# Proposal validates the live buffer it is about to stage; confirm validates
|
|
534
|
+
# the staged snapshot it is about to send — never the moving buffer.
|
|
535
|
+
if [ -z "$confirmation" ] && ! _validate_buffer; then
|
|
529
536
|
echo "Share blocked: the local buffer contains an invalid or content-bearing event." >&2
|
|
530
537
|
return 2
|
|
531
538
|
fi
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
count=$(grep -c '^{' "$BUFFER_FILE" 2>/dev/null || true)
|
|
536
|
-
size=$(wc -c < "$BUFFER_FILE" 2>/dev/null | tr -d ' ')
|
|
537
|
-
dataset_sha256=$(_sha256_file "$BUFFER_FILE")
|
|
538
|
-
else
|
|
539
|
-
local empty_file="$BUFFER_DIR/telemetry.empty.$$"
|
|
540
|
-
mkdir -p "$BUFFER_DIR"
|
|
541
|
-
: > "$empty_file"
|
|
542
|
-
dataset_sha256=$(_sha256_file "$empty_file")
|
|
543
|
-
rm -f "$empty_file"
|
|
539
|
+
if [ -n "$confirmation" ] && ! _validate_buffer "$SHARE_STAGED_FILE"; then
|
|
540
|
+
echo "Share blocked: the staged dataset contains an invalid or content-bearing event." >&2
|
|
541
|
+
return 2
|
|
544
542
|
fi
|
|
545
543
|
|
|
544
|
+
local count=0 size=0 dataset_sha256=""
|
|
546
545
|
if [ -z "$confirmation" ]; then
|
|
547
|
-
|
|
546
|
+
mkdir -p "$BUFFER_DIR"
|
|
548
547
|
if [ -s "$BUFFER_FILE" ]; then
|
|
548
|
+
cp "$BUFFER_FILE" "$SHARE_STAGED_FILE"
|
|
549
|
+
else
|
|
550
|
+
: > "$SHARE_STAGED_FILE"
|
|
551
|
+
fi
|
|
552
|
+
chmod 600 "$SHARE_STAGED_FILE"
|
|
553
|
+
count=$(grep -c '^{' "$SHARE_STAGED_FILE" 2>/dev/null || true)
|
|
554
|
+
size=$(wc -c < "$SHARE_STAGED_FILE" 2>/dev/null | tr -d ' ')
|
|
555
|
+
dataset_sha256=$(_sha256_file "$SHARE_STAGED_FILE")
|
|
556
|
+
local fields="none"
|
|
557
|
+
if [ -s "$SHARE_STAGED_FILE" ]; then
|
|
549
558
|
fields=$(jq -rs '
|
|
550
559
|
[.[].metrics // .[].data // {} | keys[]] | unique |
|
|
551
560
|
if length == 0 then "none" else map("metrics." + .) | join(", ") end
|
|
552
|
-
' "$
|
|
561
|
+
' "$SHARE_STAGED_FILE" 2>/dev/null || echo "unable to inspect")
|
|
553
562
|
fi
|
|
554
563
|
local token token_sha256 expires_at
|
|
555
564
|
token=$(_random_token)
|
|
@@ -600,10 +609,14 @@ cmd_share() {
|
|
|
600
609
|
return 0
|
|
601
610
|
fi
|
|
602
611
|
|
|
603
|
-
if [ ! -f "$SHARE_CONSENT_FILE" ]; then
|
|
612
|
+
if [ ! -f "$SHARE_CONSENT_FILE" ] || [ ! -f "$SHARE_STAGED_FILE" ]; then
|
|
604
613
|
echo "Share blocked: run share without --confirm to inspect and prepare this dataset first." >&2
|
|
605
614
|
return 2
|
|
606
615
|
fi
|
|
616
|
+
# The staged snapshot is the disclosed dataset: exactly what the proposal
|
|
617
|
+
# named is what ships, and later buffer growth cannot invalidate it.
|
|
618
|
+
count=$(grep -c '^{' "$SHARE_STAGED_FILE" 2>/dev/null || true)
|
|
619
|
+
dataset_sha256=$(_sha256_file "$SHARE_STAGED_FILE")
|
|
607
620
|
local expected_target expected_dataset expected_token expires_at actual_token
|
|
608
621
|
expected_target=$(jq -r '.target // empty' "$SHARE_CONSENT_FILE")
|
|
609
622
|
expected_dataset=$(jq -r '.dataset_sha256 // empty' "$SHARE_CONSENT_FILE")
|
|
@@ -611,7 +624,7 @@ cmd_share() {
|
|
|
611
624
|
expires_at=$(jq -r '.expires_at // 0' "$SHARE_CONSENT_FILE")
|
|
612
625
|
actual_token=$(_sha256_text "$confirmation")
|
|
613
626
|
if [ "$(date +%s)" -gt "$expires_at" ] 2>/dev/null; then
|
|
614
|
-
rm -f "$SHARE_CONSENT_FILE"
|
|
627
|
+
rm -f "$SHARE_CONSENT_FILE" "$SHARE_STAGED_FILE"
|
|
615
628
|
echo "Share blocked: confirmation expired; prepare a new share." >&2
|
|
616
629
|
return 2
|
|
617
630
|
fi
|
|
@@ -621,7 +634,7 @@ cmd_share() {
|
|
|
621
634
|
fi
|
|
622
635
|
|
|
623
636
|
local share_file="$BUFFER_DIR/telemetry.share.$$"
|
|
624
|
-
if [ -s "$
|
|
637
|
+
if [ -s "$SHARE_STAGED_FILE" ]; then
|
|
625
638
|
jq -c --arg schema "$SCHEMA_VERSION" '
|
|
626
639
|
if .schema_version == $schema then
|
|
627
640
|
{ts:.occurred_at,type:.event_type,sid:.session_id,org:.org_id,user:.actor_id,
|
|
@@ -629,7 +642,7 @@ cmd_share() {
|
|
|
629
642
|
artifact_ids:.artifact_ids,task_id:.task_id,org_revision:.org_revision,
|
|
630
643
|
index_spec_version:.index_spec_version,shared:true})}
|
|
631
644
|
else . end
|
|
632
|
-
' "$
|
|
645
|
+
' "$SHARE_STAGED_FILE" > "$share_file"
|
|
633
646
|
else
|
|
634
647
|
: > "$share_file"
|
|
635
648
|
fi
|
|
@@ -646,7 +659,7 @@ cmd_share() {
|
|
|
646
659
|
http_code=$(curl "${curl_args[@]}" 2>/dev/null || echo "000")
|
|
647
660
|
rm -f "$share_file"
|
|
648
661
|
if [ "$http_code" -ge 200 ] 2>/dev/null && [ "$http_code" -lt 300 ] 2>/dev/null; then
|
|
649
|
-
rm -f "$SHARE_CONSENT_FILE"
|
|
662
|
+
rm -f "$SHARE_CONSENT_FILE" "$SHARE_STAGED_FILE"
|
|
650
663
|
echo "Shared $count telemetry events with $endpoint. Local events were retained."
|
|
651
664
|
else
|
|
652
665
|
echo "Share failed (HTTP $http_code). Nothing was removed locally." >&2
|
|
@@ -61,7 +61,7 @@ def _snapshot(runtime, actor, service, permission: Permission = Permission.READ)
|
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
def _resolve(runtime, actor, service, reference: str) -> IssueSnapshot:
|
|
64
|
-
rows, _ = _snapshot(runtime, actor)
|
|
64
|
+
rows, _ = _snapshot(runtime, actor, service)
|
|
65
65
|
return service.resolve(rows, reference)
|
|
66
66
|
|
|
67
67
|
|
|
@@ -72,7 +72,7 @@ def _write_result(issue: IssueSnapshot, receipt) -> int:
|
|
|
72
72
|
|
|
73
73
|
def _list(args):
|
|
74
74
|
_, runtime, actor, service = _context()
|
|
75
|
-
rows, _ = _snapshot(runtime, actor)
|
|
75
|
+
rows, _ = _snapshot(runtime, actor, service)
|
|
76
76
|
selected = rows if args.status == "all" else tuple(
|
|
77
77
|
row for row in rows if row.status == args.status
|
|
78
78
|
)
|
|
@@ -89,7 +89,7 @@ def _show(args):
|
|
|
89
89
|
|
|
90
90
|
def _search(args):
|
|
91
91
|
_, runtime, actor, service = _context()
|
|
92
|
-
rows, _ = _snapshot(runtime, actor)
|
|
92
|
+
rows, _ = _snapshot(runtime, actor, service)
|
|
93
93
|
terms = tuple(part for part in args.term.casefold().split() if part)
|
|
94
94
|
|
|
95
95
|
def matches(row: IssueSnapshot) -> bool:
|
|
@@ -24,6 +24,10 @@ FRAMEWORK_UPDATED_FILES=""
|
|
|
24
24
|
HEALTH_RETRIEVAL="skip"
|
|
25
25
|
SYNC_RECEIPT=""
|
|
26
26
|
|
|
27
|
+
# Settings drift healer + unshared-settings reporting (see the lib header).
|
|
28
|
+
# shellcheck source=bin/lib/settings-drift.sh
|
|
29
|
+
source "$SCRIPT_DIR/bin/lib/settings-drift.sh"
|
|
30
|
+
|
|
27
31
|
# The core repo's integration branch: "develop" unless egregore.json sets
|
|
28
32
|
# base_branch. session-start.sh resolves this before sourcing; resolve it here
|
|
29
33
|
# too so the lib stands alone for other callers.
|
|
@@ -324,8 +328,14 @@ setup_develop() {
|
|
|
324
328
|
return 1
|
|
325
329
|
fi
|
|
326
330
|
else
|
|
327
|
-
# Already on it — fetch couldn't update a checked-out branch, so pull
|
|
331
|
+
# Already on it — fetch couldn't update a checked-out branch, so pull.
|
|
332
|
+
# Launcher/installer settings drift blocks this merge and the refusal
|
|
333
|
+
# is otherwise silent; heal it first, re-apply the launcher's intent
|
|
334
|
+
# after (bin/lib/settings-drift.sh).
|
|
335
|
+
settings_drift_heal
|
|
328
336
|
git merge --ff-only "origin/$BASE_BRANCH" --quiet 2>/dev/null || true
|
|
337
|
+
settings_drift_reapply
|
|
338
|
+
settings_drift_report
|
|
329
339
|
fi
|
|
330
340
|
|
|
331
341
|
# Never report the intended branch as the actual branch. A checkout can fail
|
|
@@ -95,6 +95,14 @@ if [ -n "$SAVED_BRANCH" ]; then
|
|
|
95
95
|
echo " ✓ Auto-saved uncommitted work on $SAVED_BRANCH"
|
|
96
96
|
fi
|
|
97
97
|
|
|
98
|
+
# --- Unshared org-settings drift (launcher writes not yet shared) ---
|
|
99
|
+
if [ "${SETTINGS_ADAPTER_RESTORED:-false}" = "true" ]; then
|
|
100
|
+
echo " ◇ restored org .claude/settings.json — a generated adapter had replaced it locally"
|
|
101
|
+
fi
|
|
102
|
+
if [ -n "${UNSHARED_SETTINGS_KEYS:-}" ]; then
|
|
103
|
+
echo " ◇ org settings changed on this machine (${UNSHARED_SETTINGS_KEYS}) — say \"share settings\" to make them org-wide"
|
|
104
|
+
fi
|
|
105
|
+
|
|
98
106
|
# --- Footer: health + repos + memory (tertiary) ---
|
|
99
107
|
echo "$SEPARATOR"
|
|
100
108
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# settings-drift.sh — heal launcher/installer settings drift around the
|
|
3
|
+
# base-branch fast-forward, and surface unshared org-settings changes.
|
|
4
|
+
#
|
|
5
|
+
# Two org-config surfaces accumulate legitimate local modifications outside
|
|
6
|
+
# any session: .claude/settings.json (the installer's generated public-safe
|
|
7
|
+
# adapter can replace the org's richer committed file) and egregore.json
|
|
8
|
+
# (launcher settings verbs: posture, workflow, people removals). Local
|
|
9
|
+
# modifications to files a fast-forward also changes make git refuse the
|
|
10
|
+
# merge — and that refusal used to be silently swallowed, leaving the
|
|
11
|
+
# checkout behind with no explanation.
|
|
12
|
+
#
|
|
13
|
+
# Contract (sourced; caller provides $BASE_BRANCH and runs from the repo
|
|
14
|
+
# root):
|
|
15
|
+
# settings_drift_heal — before the ff-merge. Restores settings.json
|
|
16
|
+
# only when the local copy is a reduced
|
|
17
|
+
# generated adapter (its hook events and
|
|
18
|
+
# permission allows are a subset of the
|
|
19
|
+
# committed file's). Snapshots egregore.json
|
|
20
|
+
# and returns it to HEAD so the merge can run.
|
|
21
|
+
# settings_drift_reapply — after the ff-merge. Re-applies the snapshot's
|
|
22
|
+
# changed settings-owned keys on top of the
|
|
23
|
+
# incoming egregore.json.
|
|
24
|
+
# settings_drift_report — sets UNSHARED_SETTINGS_KEYS to the
|
|
25
|
+
# settings-owned keys where the working file
|
|
26
|
+
# differs from HEAD, for the greeting.
|
|
27
|
+
#
|
|
28
|
+
# Anything dirty beyond these two files is user work and is never touched.
|
|
29
|
+
|
|
30
|
+
_SETTINGS_OWNED_KEYS='["boundary","base_branch","people_removed","features","repos","admins","people"]'
|
|
31
|
+
_SETTINGS_DRIFT_SNAPSHOT=""
|
|
32
|
+
_SETTINGS_DRIFT_BASE=""
|
|
33
|
+
# Read by bin/lib/greeting.sh after git-sync completes.
|
|
34
|
+
export SETTINGS_ADAPTER_RESTORED="false"
|
|
35
|
+
export UNSHARED_SETTINGS_KEYS=""
|
|
36
|
+
|
|
37
|
+
# The generated adapter only ever removes relative to the org file: fewer
|
|
38
|
+
# hook events, fewer permission allows. A local settings.json that carries
|
|
39
|
+
# anything the committed file lacks is user work and stays untouched.
|
|
40
|
+
_settings_is_reduced_adapter() {
|
|
41
|
+
local tmp_head
|
|
42
|
+
tmp_head=$(mktemp) || return 1
|
|
43
|
+
if ! git show "HEAD:.claude/settings.json" > "$tmp_head" 2>/dev/null; then
|
|
44
|
+
rm -f "$tmp_head"
|
|
45
|
+
return 1
|
|
46
|
+
fi
|
|
47
|
+
jq -es '
|
|
48
|
+
.[0] as $local | .[1] as $head |
|
|
49
|
+
($local != $head) and
|
|
50
|
+
((($local.hooks // {}) | keys) - (($head.hooks // {}) | keys) | length == 0) and
|
|
51
|
+
((($local.permissions.allow // []) - ($head.permissions.allow // [])) | length == 0)
|
|
52
|
+
' .claude/settings.json "$tmp_head" >/dev/null 2>&1
|
|
53
|
+
local verdict=$?
|
|
54
|
+
rm -f "$tmp_head"
|
|
55
|
+
return "$verdict"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
settings_drift_heal() {
|
|
59
|
+
git rev-parse -q --verify "origin/$BASE_BRANCH" >/dev/null 2>&1 || return 0
|
|
60
|
+
[ "$(git rev-parse HEAD 2>/dev/null)" = "$(git rev-parse "origin/$BASE_BRANCH" 2>/dev/null)" ] && return 0
|
|
61
|
+
|
|
62
|
+
if ! git diff --quiet -- .claude/settings.json 2>/dev/null; then
|
|
63
|
+
if _settings_is_reduced_adapter; then
|
|
64
|
+
git checkout -- .claude/settings.json 2>/dev/null && SETTINGS_ADAPTER_RESTORED="true"
|
|
65
|
+
fi
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
if ! git diff --quiet -- egregore.json 2>/dev/null; then
|
|
69
|
+
_SETTINGS_DRIFT_SNAPSHOT=$(cat egregore.json 2>/dev/null || true)
|
|
70
|
+
_SETTINGS_DRIFT_BASE=$(git show "HEAD:egregore.json" 2>/dev/null || true)
|
|
71
|
+
if [ -n "$_SETTINGS_DRIFT_SNAPSHOT" ] && [ -n "$_SETTINGS_DRIFT_BASE" ] \
|
|
72
|
+
&& printf '%s' "$_SETTINGS_DRIFT_SNAPSHOT" | jq -e . >/dev/null 2>&1; then
|
|
73
|
+
git checkout -- egregore.json 2>/dev/null || _SETTINGS_DRIFT_SNAPSHOT=""
|
|
74
|
+
else
|
|
75
|
+
_SETTINGS_DRIFT_SNAPSHOT=""
|
|
76
|
+
fi
|
|
77
|
+
fi
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
settings_drift_reapply() {
|
|
81
|
+
[ -n "$_SETTINGS_DRIFT_SNAPSHOT" ] || return 0
|
|
82
|
+
local tmp_local tmp_base merged
|
|
83
|
+
tmp_local=$(mktemp) && tmp_base=$(mktemp) || return 0
|
|
84
|
+
printf '%s' "$_SETTINGS_DRIFT_SNAPSHOT" > "$tmp_local"
|
|
85
|
+
printf '%s' "$_SETTINGS_DRIFT_BASE" > "$tmp_base"
|
|
86
|
+
merged=$(jq -s --argjson keys "$_SETTINGS_OWNED_KEYS" '
|
|
87
|
+
.[0] as $incoming | .[1] as $local | .[2] as $base |
|
|
88
|
+
reduce $keys[] as $k ($incoming;
|
|
89
|
+
if ($local[$k] // null) != ($base[$k] // null) then
|
|
90
|
+
if ($local | has($k)) then .[$k] = $local[$k] else del(.[$k]) end
|
|
91
|
+
else . end)
|
|
92
|
+
' egregore.json "$tmp_local" "$tmp_base" 2>/dev/null || true)
|
|
93
|
+
rm -f "$tmp_local" "$tmp_base"
|
|
94
|
+
if [ -n "$merged" ] && printf '%s' "$merged" | jq -e . >/dev/null 2>&1; then
|
|
95
|
+
printf '%s\n' "$merged" > egregore.json
|
|
96
|
+
fi
|
|
97
|
+
_SETTINGS_DRIFT_SNAPSHOT=""
|
|
98
|
+
_SETTINGS_DRIFT_BASE=""
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
settings_drift_report() {
|
|
102
|
+
UNSHARED_SETTINGS_KEYS=""
|
|
103
|
+
git diff --quiet -- egregore.json 2>/dev/null && return 0
|
|
104
|
+
local tmp_head
|
|
105
|
+
tmp_head=$(mktemp) || return 0
|
|
106
|
+
if ! git show "HEAD:egregore.json" > "$tmp_head" 2>/dev/null; then
|
|
107
|
+
rm -f "$tmp_head"
|
|
108
|
+
return 0
|
|
109
|
+
fi
|
|
110
|
+
UNSHARED_SETTINGS_KEYS=$(jq -rs --argjson keys "$_SETTINGS_OWNED_KEYS" '
|
|
111
|
+
.[0] as $local | .[1] as $head |
|
|
112
|
+
[$keys[] | select(($local[.] // null) != ($head[.] // null))] | join(", ")
|
|
113
|
+
' egregore.json "$tmp_head" 2>/dev/null || true)
|
|
114
|
+
rm -f "$tmp_head"
|
|
115
|
+
export UNSHARED_SETTINGS_KEYS
|
|
116
|
+
}
|
|
@@ -235,6 +235,21 @@ cmd_query() {
|
|
|
235
235
|
} >&2
|
|
236
236
|
return "$query_rc"
|
|
237
237
|
fi
|
|
238
|
+
# Content-free query telemetry: mode, duration, result count. Never the
|
|
239
|
+
# query text. This is what makes Runtime/QMD retrieval visible in the
|
|
240
|
+
# telemetry event list instead of only legacy graph_query rows.
|
|
241
|
+
local _tele_count _tele_ms
|
|
242
|
+
_tele_ms=$(( ($(date +%s) - t0) * 1000 ))
|
|
243
|
+
_tele_count=$(printf '%s' "$out" | jq 'if type == "array" then length elif type == "object" then (.results // [] | length) else empty end' 2>/dev/null || true)
|
|
244
|
+
if [[ "$_tele_count" =~ ^[0-9]+$ ]]; then
|
|
245
|
+
bash "$SCRIPT_DIR/bin/telemetry.sh" emit "query" \
|
|
246
|
+
"$(jq -n --arg mode "$label" --argjson d "$_tele_ms" --argjson c "$_tele_count" \
|
|
247
|
+
'{mode:$mode, duration_ms:$d, count:$c}')" 2>/dev/null &
|
|
248
|
+
else
|
|
249
|
+
bash "$SCRIPT_DIR/bin/telemetry.sh" emit "query" \
|
|
250
|
+
"$(jq -n --arg mode "$label" --argjson d "$_tele_ms" '{mode:$mode, duration_ms:$d}')" 2>/dev/null &
|
|
251
|
+
fi
|
|
252
|
+
|
|
238
253
|
[ "$enrich" = 1 ] && emit="_enrich"
|
|
239
254
|
if [ "$context_packet" = 1 ]; then
|
|
240
255
|
local packet
|