create-egregore 0.21.0-runtime-mvp.8 → 0.21.0-runtime-mvp.10

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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // egregore-launcher-version: 0.21.0-runtime-mvp.8
2
+ // egregore-launcher-version: 0.21.0-runtime-mvp.10
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.8";
38
+ const VERSION = "0.21.0-runtime-mvp.10";
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");
@@ -2787,13 +2787,13 @@ async function cmdSettingsRuntime(inst, args) {
2787
2787
  async function cmdSettings(args) {
2788
2788
  const registry = readRegistry();
2789
2789
  if (registry.length === 0) die("no egregores installed yet.\n run: npx -y create-egregore@latest");
2790
- const DOMAINS = new Set(["hosting", "workflow", "repo", "admin", "people", "dump", "runtime", "privacy", "telemetry", "boundary"]);
2790
+ const DOMAINS = new Set(["hosting", "workflow", "repo", "admin", "people", "dump", "runtime", "privacy", "telemetry", "boundary", "posture"]);
2791
2791
  let rest = args.slice();
2792
2792
  let slug = "";
2793
2793
  if (rest.length && !DOMAINS.has(rest[0])) { slug = rest[0]; rest = rest.slice(1); }
2794
2794
  const inst = resolveInviteInstance(registry, slug);
2795
2795
  if (!inst) {
2796
- die(`multiple egregores installed; choose one:\n egregore settings <slug> [runtime|hosting|workflow|repo|admin|people|privacy|telemetry]\n run: egregore ls`);
2796
+ die(`multiple egregores installed; choose one:\n egregore settings <slug> [runtime|hosting|workflow|repo|admin|people|privacy|posture|telemetry]\n run: egregore ls`);
2797
2797
  }
2798
2798
  // Runtime update is Runtime-owned state, not org config — it routes to the
2799
2799
  // upgrade bridge instead of bin/settings.sh.
@@ -3208,14 +3208,58 @@ function renderSettingsPanel({ inst, data, cursor, view, domain, items, listCurs
3208
3208
  lines.push(boxLine(`${BOLD}${view === "privacy" ? "Privacy & access" : "Telemetry"}${RESET}`, innerWidth, margin));
3209
3209
  lines.push(emptyBoxLine(innerWidth, margin));
3210
3210
  if (mode === "shareinfo") {
3211
- const shareLines = (data && data.shareLines) || ["share proposal unavailable"];
3211
+ const proposal = data && data.shareProposal;
3212
3212
  lines.push(boxLine(`${BOLD}Share proposal${RESET} ${DIM}(nothing has been sent)${RESET}`, innerWidth, margin));
3213
3213
  lines.push(emptyBoxLine(innerWidth, margin));
3214
- for (const line of shareLines.slice(0, 16)) {
3215
- lines.push(boxLine(`${line.includes("--confirm") ? "" : DIM}${line}${RESET}`, innerWidth, margin));
3214
+ if (proposal && proposal.token) {
3215
+ lines.push(boxLine(`${DIM}Target: ${proposal.target}${RESET}`, innerWidth, margin));
3216
+ lines.push(boxLine(`${DIM}Events: ${proposal.events} · Bytes: ${proposal.bytes}${RESET}`, innerWidth, margin));
3217
+ lines.push(boxLine(`${DIM}Dataset SHA-256: ${proposal.dataset_sha256}${RESET}`, innerWidth, margin));
3218
+ lines.push(boxLine(`${DIM}Metric fields: ${proposal.metric_fields}${RESET}`, innerWidth, margin));
3219
+ lines.push(boxLine(`${DIM}Excluded by validation: ${proposal.excluded}${RESET}`, innerWidth, margin));
3220
+ lines.push(emptyBoxLine(innerWidth, margin));
3221
+ if (input) {
3222
+ lines.push(boxLine(`${YELLOW}${input}${RESET}`, innerWidth, margin));
3223
+ } else {
3224
+ lines.push(boxLine(`${DIM}Nothing is sent until you confirm here. ${RESET}${BOLD}s${RESET}${DIM} send now · esc back${RESET}`, innerWidth, margin));
3225
+ }
3226
+ } else {
3227
+ // Older adapter without a machine-readable proposal: show its prose
3228
+ // and fall back to the terminal confirm instruction it printed.
3229
+ const shareLines = (data && data.shareLines) || ["share proposal unavailable"];
3230
+ for (const line of shareLines.slice(0, 16)) {
3231
+ lines.push(boxLine(`${line.includes("--confirm") ? "" : DIM}${line}${RESET}`, innerWidth, margin));
3232
+ }
3233
+ lines.push(emptyBoxLine(innerWidth, margin));
3234
+ lines.push(boxLine(`${DIM}Sharing completes only by running the confirm command above in a terminal. ← back${RESET}`, innerWidth, margin));
3216
3235
  }
3217
3236
  lines.push(emptyBoxLine(innerWidth, margin));
3218
- lines.push(boxLine(`${DIM}Sharing completes only by running the confirm command above in a terminal. ← back${RESET}`, innerWidth, margin));
3237
+ lines.push(`${margin}${DIM}└${"─".repeat(innerWidth)}┘${RESET}`);
3238
+ return lines;
3239
+ }
3240
+ if (mode === "posture") {
3241
+ const current = (data && data.privacySnapshot && data.privacySnapshot.boundary && data.privacySnapshot.boundary.posture) || "standard";
3242
+ lines.push(boxLine(`${BOLD}Boundary posture${RESET} ${DIM}— how far reads may reach outside this egregore${RESET}`, innerWidth, margin));
3243
+ lines.push(emptyBoxLine(innerWidth, margin));
3244
+ const options = [
3245
+ ["strict", "reads outside the project, memory, and managed repos always ask first"],
3246
+ ["standard", "inbox folders (Downloads, Desktop) readable without asking; everything else asks first"],
3247
+ ["open", "reads outside the boundary allowed without asking; writes outside still ask"],
3248
+ ];
3249
+ options.forEach(([name, why], i) => {
3250
+ const sel = i === (listCursor || 0);
3251
+ const mark = name === current ? `${GREEN}● ${RESET}` : " ";
3252
+ lines.push(boxLine(`${sel ? `${CYAN}❯${RESET} ` : " "}${mark}${sel ? BOLD : ""}${name}${RESET}`, innerWidth, margin));
3253
+ lines.push(boxLine(` ${DIM}${why}${RESET}`, innerWidth, margin));
3254
+ });
3255
+ lines.push(boxLine(` ${DIM}other Egregore instances stay isolated under every posture${RESET}`, innerWidth, margin));
3256
+ lines.push(emptyBoxLine(innerWidth, margin));
3257
+ if (input) {
3258
+ lines.push(boxLine(`${YELLOW}${input}${RESET}`, innerWidth, margin));
3259
+ } else {
3260
+ if (notice) lines.push(boxLine(notice, innerWidth, margin));
3261
+ lines.push(boxLine(`${DIM}↑↓ nav · ${RESET}${BOLD}enter${RESET}${DIM} select · esc cancel${RESET}`, innerWidth, margin));
3262
+ }
3219
3263
  lines.push(emptyBoxLine(innerWidth, margin));
3220
3264
  lines.push(`${margin}${DIM}└${"─".repeat(innerWidth)}┘${RESET}`);
3221
3265
  return lines;
@@ -3300,7 +3344,7 @@ function renderSettingsPanel({ inst, data, cursor, view, domain, items, listCurs
3300
3344
  lines.push(boxLine(
3301
3345
  locked
3302
3346
  ? `${DIM}Boundary policy is managed by your organization. ← back · q quit${RESET}`
3303
- : `${DIM}${RESET}${BOLD}a${RESET}${DIM} add personal read root · ${RESET}${BOLD}r${RESET}${DIM} remove · ← back · q quit${RESET}`,
3347
+ : `${DIM}${RESET}${BOLD}a${RESET}${DIM} add personal read root · ${RESET}${BOLD}r${RESET}${DIM} remove · ${RESET}${BOLD}p${RESET}${DIM} boundary posture · ← back · q quit${RESET}`,
3304
3348
  innerWidth, margin,
3305
3349
  ));
3306
3350
  } else {
@@ -3966,16 +4010,49 @@ async function browser(args = {}) {
3966
4010
  render(); return;
3967
4011
  }
3968
4012
  if (chunk === "s") {
3969
- const r = runTelemetry(S.settingsInst, ["share"]);
3970
- const shareLines = ((r && (r.stdout || r.stderr)) || "share preview unavailable").split("\n").filter(Boolean);
3971
- S.settingsData = { ...(S.settingsData || {}), shareLines };
3972
- S.settingsMode = "shareinfo"; S.settingsNotice = "";
4013
+ // Prefer the machine-readable proposal so the send can complete
4014
+ // here; fall back to the adapter's prose for older runtimes.
4015
+ let proposal = null;
4016
+ const rj = runTelemetry(S.settingsInst, ["share", "--json"]);
4017
+ if (rj && rj.status === 0 && rj.stdout) {
4018
+ try { proposal = JSON.parse(rj.stdout); } catch { proposal = null; }
4019
+ }
4020
+ if (proposal && proposal.token) {
4021
+ S.settingsData = { ...(S.settingsData || {}), shareProposal: proposal, shareLines: null };
4022
+ } else {
4023
+ const r = runTelemetry(S.settingsInst, ["share"]);
4024
+ const shareLines = ((r && (r.stdout || r.stderr)) || "share preview unavailable").split("\n").filter(Boolean);
4025
+ S.settingsData = { ...(S.settingsData || {}), shareProposal: null, shareLines };
4026
+ }
4027
+ S.settingsMode = "shareinfo"; S.settingsNotice = ""; S.settingsInput = ""; S.settingsAction = "";
3973
4028
  render(); return;
3974
4029
  }
3975
4030
  return;
3976
4031
  }
3977
4032
  if (S.settingsMode === "shareinfo") {
3978
- if (key.name === "escape" || key.name === "left") { S.settingsMode = "events"; render(); return; }
4033
+ const proposal = S.settingsData && S.settingsData.shareProposal;
4034
+ if (S.settingsAction === "tele-share" && S.settingsInput) {
4035
+ if (chunk === "y" || chunk === "Y") {
4036
+ const r = runTelemetry(S.settingsInst, ["share", "--confirm", proposal.token]);
4037
+ S.settingsNotice = r && r.status === 0
4038
+ ? `${GREEN}${((r.stdout || "shared").split("\n")[0])}${RESET}`
4039
+ : `${YELLOW}${(((r && (r.stderr || r.stdout)) || "share failed").split("\n")[0])}${RESET}`;
4040
+ S.settingsAction = ""; S.settingsInput = "";
4041
+ S.settingsData = { ...(S.settingsData || {}), shareProposal: null, telemetryEvents: telemetryEvents(S.settingsInst, 100) };
4042
+ S.settingsMode = "events"; S.settingsListCursor = 0;
4043
+ refreshPanel(); render(); return;
4044
+ }
4045
+ S.settingsAction = ""; S.settingsInput = ""; render(); return;
4046
+ }
4047
+ if (chunk === "s" && proposal && proposal.token) {
4048
+ S.settingsAction = "tele-share";
4049
+ S.settingsInput = `Send ${proposal.events} event(s) now? Local events are retained. [y/N]`;
4050
+ render(); return;
4051
+ }
4052
+ if (key.name === "escape" || key.name === "left") {
4053
+ S.settingsAction = ""; S.settingsInput = "";
4054
+ S.settingsMode = "events"; render(); return;
4055
+ }
3979
4056
  return;
3980
4057
  }
3981
4058
  if (S.settingsMode === "pickdir") {
@@ -4003,6 +4080,34 @@ async function browser(args = {}) {
4003
4080
  }
4004
4081
  return;
4005
4082
  }
4083
+ if (S.settingsMode === "posture") {
4084
+ const POSTURES = ["strict", "standard", "open"];
4085
+ const current = (S.settingsData.privacySnapshot && S.settingsData.privacySnapshot.boundary && S.settingsData.privacySnapshot.boundary.posture) || "standard";
4086
+ if (S.settingsAction === "posture" && S.settingsInput) {
4087
+ if (chunk === "y" || chunk === "Y") {
4088
+ const r = runSettings(S.settingsInst, ["posture", S.settingsPendingDir]);
4089
+ S.settingsNotice = r && r.status === 0
4090
+ ? `${GREEN}${((r.stdout || "done").split("\n")[0])}${RESET}`
4091
+ : `${YELLOW}${(((r && (r.stderr || r.stdout)) || "posture change failed").split("\n")[0])}${RESET}`;
4092
+ S.settingsAction = ""; S.settingsInput = ""; S.settingsPendingDir = "";
4093
+ S.settingsMode = "browse";
4094
+ refreshPanel(); render(); return;
4095
+ }
4096
+ S.settingsAction = ""; S.settingsInput = ""; S.settingsPendingDir = ""; render(); return;
4097
+ }
4098
+ if (key.name === "escape" || key.name === "left") { S.settingsMode = "browse"; S.settingsListCursor = 0; S.settingsNotice = ""; render(); return; }
4099
+ if (key.name === "up" || key.name === "k") { S.settingsListCursor = ((S.settingsListCursor || 0) + 2) % 3; render(); return; }
4100
+ if (key.name === "down" || key.name === "j") { S.settingsListCursor = ((S.settingsListCursor || 0) + 1) % 3; render(); return; }
4101
+ if (key.name === "return") {
4102
+ const chosen = POSTURES[Math.min(S.settingsListCursor || 0, 2)];
4103
+ if (chosen === current) { S.settingsNotice = `${DIM}already on ${chosen}${RESET}`; render(); return; }
4104
+ S.settingsAction = "posture";
4105
+ S.settingsPendingDir = chosen;
4106
+ S.settingsInput = `Switch boundary posture to ${chosen.toUpperCase()} for the whole organization? This edits the org's committed egregore.json for everyone. [y/N]`;
4107
+ render(); return;
4108
+ }
4109
+ return;
4110
+ }
4006
4111
  if (S.settingsMode === "pickroot") {
4007
4112
  const roots = (S.settingsData.privacySnapshot && S.settingsData.privacySnapshot.boundary && S.settingsData.privacySnapshot.boundary.personal_read_roots) || [];
4008
4113
  if (key.name === "escape" || key.name === "left") { S.settingsMode = "browse"; S.settingsListCursor = 0; render(); return; }
@@ -4050,6 +4155,13 @@ async function browser(args = {}) {
4050
4155
  if (!locked && chunk === "r") {
4051
4156
  S.settingsMode = "pickroot"; S.settingsListCursor = 0; S.settingsNotice = ""; render(); return;
4052
4157
  }
4158
+ if (!locked && chunk === "p") {
4159
+ const current = (S.settingsData && S.settingsData.privacySnapshot && S.settingsData.privacySnapshot.boundary && S.settingsData.privacySnapshot.boundary.posture) || "standard";
4160
+ S.settingsMode = "posture";
4161
+ S.settingsListCursor = Math.max(0, ["strict", "standard", "open"].indexOf(current));
4162
+ S.settingsNotice = ""; S.settingsInput = ""; S.settingsAction = "";
4163
+ render(); return;
4164
+ }
4053
4165
  return;
4054
4166
  }
4055
4167
  // telemetry view
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-egregore",
3
- "version": "0.21.0-runtime-mvp.8",
3
+ "version": "0.21.0-runtime-mvp.10",
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": {
@@ -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
@@ -59,6 +59,7 @@ settings.sh — Egregore instance settings
59
59
  settings.sh repo list | add <name> [description] | remove <name>
60
60
  settings.sh admin list | add <github-handle> | remove <github-handle>
61
61
  settings.sh people list | add <github-handle> | remove <github-handle>
62
+ settings.sh posture status|strict|standard|open org-wide boundary posture
62
63
  settings.sh dump full snapshot (JSON)
63
64
 
64
65
  --json machine-readable output for status/list
@@ -145,7 +146,7 @@ cmd_workflow() {
145
146
  || _die "cannot switch workflow: origin/main does not exist or could not be reached" 1
146
147
  _save '.base_branch = "main"'
147
148
  fi
148
- 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"
149
150
  ;;
150
151
  staged)
151
152
  if [ "$base" != "develop" ]; then
@@ -159,7 +160,7 @@ cmd_workflow() {
159
160
  elif ! jq -e 'has("base_branch")' "$CONFIG" >/dev/null 2>&1; then
160
161
  _save '.base_branch = "develop"'
161
162
  fi
162
- 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"
163
164
  ;;
164
165
  *) _die "usage: settings.sh workflow status|simple|staged" 1 ;;
165
166
  esac
@@ -441,6 +442,38 @@ cmd_privacy() {
441
442
  }'
442
443
  }
443
444
 
445
+ # ── boundary posture (org-wide) ──────────────────────────────────────────
446
+ # Edits committed egregore.json for the whole organization. `locked: true`
447
+ # removes the change path entirely — posture is then org-policy managed.
448
+
449
+ cmd_posture() {
450
+ local action="${1:-status}" current
451
+ current="$(jq -r '.boundary.posture // "standard"' "$CONFIG" 2>/dev/null || echo standard)"
452
+ case "$current" in strict|standard|open) ;; *) current="standard" ;; esac
453
+ case "$action" in
454
+ status)
455
+ if [ "$JSON" = 1 ]; then
456
+ jq -n --arg posture "$current" \
457
+ --argjson locked "$(_boundary_locked && echo true || echo false)" \
458
+ '{domain:"posture", posture:$posture, locked:$locked}'
459
+ else
460
+ echo "Boundary posture: $current"
461
+ _boundary_locked && echo "(organization boundary policy is locked — posture cannot change)"
462
+ fi
463
+ ;;
464
+ strict|standard|open)
465
+ if _boundary_locked; then
466
+ _die "boundary policy is managed by your organization and locked; posture cannot change" 1
467
+ fi
468
+ if [ "$action" != "$current" ]; then
469
+ _save '.boundary = ((.boundary // {}) + {posture: $p})' --arg p "$action"
470
+ fi
471
+ echo "Boundary posture: $action · applies to new sessions · /save shares it with the org"
472
+ ;;
473
+ *) _die "usage: settings.sh posture status|strict|standard|open" 1 ;;
474
+ esac
475
+ }
476
+
444
477
  # ── personal boundary read roots ─────────────────────────────────────────
445
478
  # Personal-only: edits .egregore-boundary.local.json for the current member.
446
479
  # Respects org `locked: true` (no personal expansion), and never accepts a
@@ -525,8 +558,9 @@ case "$DOMAIN" in
525
558
  admin) cmd_admin "$@" ;;
526
559
  people) cmd_people "$@" ;;
527
560
  privacy) cmd_privacy ;;
561
+ posture) cmd_posture "$@" ;;
528
562
  boundary) cmd_boundary "$@" ;;
529
563
  dump) cmd_dump ;;
530
564
  ""|-h|--help|help) usage 0 ;;
531
- *) _die "unknown settings domain: '$DOMAIN' (try: hosting, relay, workflow, repo, admin, people, privacy, boundary, dump)" 1 ;;
565
+ *) _die "unknown settings domain: '$DOMAIN' (try: hosting, relay, workflow, repo, admin, people, privacy, posture, boundary, dump)" 1 ;;
532
566
  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
- [ -s "$BUFFER_FILE" ] || return 0
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
- ' "$BUFFER_FILE" >/dev/null 2>&1
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
  }
@@ -484,7 +489,7 @@ _resolve_share_target() {
484
489
  }
485
490
 
486
491
  cmd_share() {
487
- local confirmation="" requested_endpoint="" allow_legacy_relay="false"
492
+ local confirmation="" requested_endpoint="" allow_legacy_relay="false" json_output="false"
488
493
  while [ "$#" -gt 0 ]; do
489
494
  case "$1" in
490
495
  --confirm)
@@ -499,6 +504,10 @@ cmd_share() {
499
504
  allow_legacy_relay="true"
500
505
  shift
501
506
  ;;
507
+ --json)
508
+ json_output="true"
509
+ shift
510
+ ;;
502
511
  *)
503
512
  echo "Unknown share option: $1" >&2
504
513
  return 2
@@ -521,31 +530,35 @@ cmd_share() {
521
530
  https://*|http://127.0.0.1*|http://localhost*|http://\[::1\]*) ;;
522
531
  *) echo "Share endpoint must use https (or loopback http)." >&2; return 2 ;;
523
532
  esac
524
- if ! _validate_buffer; then
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
525
536
  echo "Share blocked: the local buffer contains an invalid or content-bearing event." >&2
526
537
  return 2
527
538
  fi
528
-
529
- local count=0 size=0 dataset_sha256=""
530
- if [ -s "$BUFFER_FILE" ]; then
531
- count=$(grep -c '^{' "$BUFFER_FILE" 2>/dev/null || true)
532
- size=$(wc -c < "$BUFFER_FILE" 2>/dev/null | tr -d ' ')
533
- dataset_sha256=$(_sha256_file "$BUFFER_FILE")
534
- else
535
- local empty_file="$BUFFER_DIR/telemetry.empty.$$"
536
- mkdir -p "$BUFFER_DIR"
537
- : > "$empty_file"
538
- dataset_sha256=$(_sha256_file "$empty_file")
539
- 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
540
542
  fi
541
543
 
544
+ local count=0 size=0 dataset_sha256=""
542
545
  if [ -z "$confirmation" ]; then
543
- local fields="none"
546
+ mkdir -p "$BUFFER_DIR"
544
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
545
558
  fields=$(jq -rs '
546
559
  [.[].metrics // .[].data // {} | keys[]] | unique |
547
560
  if length == 0 then "none" else map("metrics." + .) | join(", ") end
548
- ' "$BUFFER_FILE" 2>/dev/null || echo "unable to inspect")
561
+ ' "$SHARE_STAGED_FILE" 2>/dev/null || echo "unable to inspect")
549
562
  fi
550
563
  local token token_sha256 expires_at
551
564
  token=$(_random_token)
@@ -561,6 +574,25 @@ cmd_share() {
561
574
  > "$SHARE_CONSENT_FILE"
562
575
  chmod 600 "$SHARE_CONSENT_FILE"
563
576
 
577
+ if [ "$json_output" = "true" ]; then
578
+ # Machine-readable proposal for thin surfaces (the launcher). Same
579
+ # consent protocol: the token here is the only place it exists, and a
580
+ # send still requires a separate explicit `share --confirm`.
581
+ jq -n \
582
+ --arg target "$endpoint" \
583
+ --argjson events "${count:-0}" \
584
+ --argjson bytes "${size:-0}" \
585
+ --arg dataset_sha256 "$dataset_sha256" \
586
+ --arg metric_fields "$fields" \
587
+ --arg token "$token" \
588
+ --argjson expires_at "$expires_at" \
589
+ '{proposal:true, sent:false, target:$target, events:$events, bytes:$bytes,
590
+ dataset_sha256:$dataset_sha256, metric_fields:$metric_fields,
591
+ excluded:"prompts, content, passages, file paths, arguments, secrets, env values",
592
+ token:$token, expires_at:$expires_at}'
593
+ return 0
594
+ fi
595
+
564
596
  echo "Telemetry share proposal (nothing sent):"
565
597
  echo ""
566
598
  echo " Target: $endpoint"
@@ -577,10 +609,14 @@ cmd_share() {
577
609
  return 0
578
610
  fi
579
611
 
580
- if [ ! -f "$SHARE_CONSENT_FILE" ]; then
612
+ if [ ! -f "$SHARE_CONSENT_FILE" ] || [ ! -f "$SHARE_STAGED_FILE" ]; then
581
613
  echo "Share blocked: run share without --confirm to inspect and prepare this dataset first." >&2
582
614
  return 2
583
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")
584
620
  local expected_target expected_dataset expected_token expires_at actual_token
585
621
  expected_target=$(jq -r '.target // empty' "$SHARE_CONSENT_FILE")
586
622
  expected_dataset=$(jq -r '.dataset_sha256 // empty' "$SHARE_CONSENT_FILE")
@@ -588,7 +624,7 @@ cmd_share() {
588
624
  expires_at=$(jq -r '.expires_at // 0' "$SHARE_CONSENT_FILE")
589
625
  actual_token=$(_sha256_text "$confirmation")
590
626
  if [ "$(date +%s)" -gt "$expires_at" ] 2>/dev/null; then
591
- rm -f "$SHARE_CONSENT_FILE"
627
+ rm -f "$SHARE_CONSENT_FILE" "$SHARE_STAGED_FILE"
592
628
  echo "Share blocked: confirmation expired; prepare a new share." >&2
593
629
  return 2
594
630
  fi
@@ -598,7 +634,7 @@ cmd_share() {
598
634
  fi
599
635
 
600
636
  local share_file="$BUFFER_DIR/telemetry.share.$$"
601
- if [ -s "$BUFFER_FILE" ]; then
637
+ if [ -s "$SHARE_STAGED_FILE" ]; then
602
638
  jq -c --arg schema "$SCHEMA_VERSION" '
603
639
  if .schema_version == $schema then
604
640
  {ts:.occurred_at,type:.event_type,sid:.session_id,org:.org_id,user:.actor_id,
@@ -606,7 +642,7 @@ cmd_share() {
606
642
  artifact_ids:.artifact_ids,task_id:.task_id,org_revision:.org_revision,
607
643
  index_spec_version:.index_spec_version,shared:true})}
608
644
  else . end
609
- ' "$BUFFER_FILE" > "$share_file"
645
+ ' "$SHARE_STAGED_FILE" > "$share_file"
610
646
  else
611
647
  : > "$share_file"
612
648
  fi
@@ -623,7 +659,7 @@ cmd_share() {
623
659
  http_code=$(curl "${curl_args[@]}" 2>/dev/null || echo "000")
624
660
  rm -f "$share_file"
625
661
  if [ "$http_code" -ge 200 ] 2>/dev/null && [ "$http_code" -lt 300 ] 2>/dev/null; then
626
- rm -f "$SHARE_CONSENT_FILE"
662
+ rm -f "$SHARE_CONSENT_FILE" "$SHARE_STAGED_FILE"
627
663
  echo "Shared $count telemetry events with $endpoint. Local events were retained."
628
664
  else
629
665
  echo "Share failed (HTTP $http_code). Nothing was removed locally." >&2
@@ -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
@@ -59,6 +59,7 @@ settings.sh — Egregore instance settings
59
59
  settings.sh repo list | add <name> [description] | remove <name>
60
60
  settings.sh admin list | add <github-handle> | remove <github-handle>
61
61
  settings.sh people list | add <github-handle> | remove <github-handle>
62
+ settings.sh posture status|strict|standard|open org-wide boundary posture
62
63
  settings.sh dump full snapshot (JSON)
63
64
 
64
65
  --json machine-readable output for status/list
@@ -145,7 +146,7 @@ cmd_workflow() {
145
146
  || _die "cannot switch workflow: origin/main does not exist or could not be reached" 1
146
147
  _save '.base_branch = "main"'
147
148
  fi
148
- 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"
149
150
  ;;
150
151
  staged)
151
152
  if [ "$base" != "develop" ]; then
@@ -159,7 +160,7 @@ cmd_workflow() {
159
160
  elif ! jq -e 'has("base_branch")' "$CONFIG" >/dev/null 2>&1; then
160
161
  _save '.base_branch = "develop"'
161
162
  fi
162
- 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"
163
164
  ;;
164
165
  *) _die "usage: settings.sh workflow status|simple|staged" 1 ;;
165
166
  esac
@@ -441,6 +442,38 @@ cmd_privacy() {
441
442
  }'
442
443
  }
443
444
 
445
+ # ── boundary posture (org-wide) ──────────────────────────────────────────
446
+ # Edits committed egregore.json for the whole organization. `locked: true`
447
+ # removes the change path entirely — posture is then org-policy managed.
448
+
449
+ cmd_posture() {
450
+ local action="${1:-status}" current
451
+ current="$(jq -r '.boundary.posture // "standard"' "$CONFIG" 2>/dev/null || echo standard)"
452
+ case "$current" in strict|standard|open) ;; *) current="standard" ;; esac
453
+ case "$action" in
454
+ status)
455
+ if [ "$JSON" = 1 ]; then
456
+ jq -n --arg posture "$current" \
457
+ --argjson locked "$(_boundary_locked && echo true || echo false)" \
458
+ '{domain:"posture", posture:$posture, locked:$locked}'
459
+ else
460
+ echo "Boundary posture: $current"
461
+ _boundary_locked && echo "(organization boundary policy is locked — posture cannot change)"
462
+ fi
463
+ ;;
464
+ strict|standard|open)
465
+ if _boundary_locked; then
466
+ _die "boundary policy is managed by your organization and locked; posture cannot change" 1
467
+ fi
468
+ if [ "$action" != "$current" ]; then
469
+ _save '.boundary = ((.boundary // {}) + {posture: $p})' --arg p "$action"
470
+ fi
471
+ echo "Boundary posture: $action · applies to new sessions · /save shares it with the org"
472
+ ;;
473
+ *) _die "usage: settings.sh posture status|strict|standard|open" 1 ;;
474
+ esac
475
+ }
476
+
444
477
  # ── personal boundary read roots ─────────────────────────────────────────
445
478
  # Personal-only: edits .egregore-boundary.local.json for the current member.
446
479
  # Respects org `locked: true` (no personal expansion), and never accepts a
@@ -525,8 +558,9 @@ case "$DOMAIN" in
525
558
  admin) cmd_admin "$@" ;;
526
559
  people) cmd_people "$@" ;;
527
560
  privacy) cmd_privacy ;;
561
+ posture) cmd_posture "$@" ;;
528
562
  boundary) cmd_boundary "$@" ;;
529
563
  dump) cmd_dump ;;
530
564
  ""|-h|--help|help) usage 0 ;;
531
- *) _die "unknown settings domain: '$DOMAIN' (try: hosting, relay, workflow, repo, admin, people, privacy, boundary, dump)" 1 ;;
565
+ *) _die "unknown settings domain: '$DOMAIN' (try: hosting, relay, workflow, repo, admin, people, privacy, posture, boundary, dump)" 1 ;;
532
566
  esac