create-egregore 0.21.0-runtime-mvp.14 → 0.21.0-runtime-mvp.16

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.
Files changed (29) hide show
  1. package/assets/egregore-launcher.js +2 -2
  2. package/package.json +1 -1
  3. package/runtime/claude/CLAUDE.md +19 -19
  4. package/runtime/claude/bin/codex-render-spec.mjs +2 -3
  5. package/runtime/claude/bin/search.sh +37 -10
  6. package/runtime/claude/egregore_runtime/adapters/qmd.py +42 -6
  7. package/runtime/claude/egregore_runtime/harness_cli.py +94 -3
  8. package/runtime/codex/.codex/spec-manifest.json +1 -1
  9. package/runtime/codex/AGENTS.md +17 -16
  10. package/runtime/codex/bin/codex-render-spec.mjs +2 -3
  11. package/runtime/codex/bin/search.sh +37 -10
  12. package/runtime/codex/egregore_runtime/adapters/qmd.py +42 -6
  13. package/runtime/codex/egregore_runtime/harness_cli.py +94 -3
  14. package/runtime/pi/.codex/spec-manifest.json +1 -1
  15. package/runtime/pi/.pi/APPEND_SYSTEM.md +17 -16
  16. package/runtime/pi/.pi/spec-manifest.json +2 -2
  17. package/runtime/pi/AGENTS.md +17 -16
  18. package/runtime/pi/bin/codex-render-spec.mjs +2 -3
  19. package/runtime/pi/bin/search.sh +37 -10
  20. package/runtime/pi/egregore_runtime/adapters/qmd.py +42 -6
  21. package/runtime/pi/egregore_runtime/harness_cli.py +94 -3
  22. package/runtime/prime/.codex/spec-manifest.json +1 -1
  23. package/runtime/prime/.prime/agent/APPEND_SYSTEM.md +17 -16
  24. package/runtime/prime/.prime/agent/spec-manifest.json +2 -2
  25. package/runtime/prime/AGENTS.md +17 -16
  26. package/runtime/prime/bin/codex-render-spec.mjs +2 -3
  27. package/runtime/prime/bin/search.sh +37 -10
  28. package/runtime/prime/egregore_runtime/adapters/qmd.py +42 -6
  29. package/runtime/prime/egregore_runtime/harness_cli.py +94 -3
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // egregore-launcher-version: 0.21.0-runtime-mvp.14
2
+ // egregore-launcher-version: 0.21.0-runtime-mvp.16
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.14";
38
+ const VERSION = "0.21.0-runtime-mvp.16";
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-egregore",
3
- "version": "0.21.0-runtime-mvp.14",
3
+ "version": "0.21.0-runtime-mvp.16",
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": {
@@ -228,19 +228,21 @@ For each user-directed organizational lookup, show exactly one visible line:
228
228
  - retrieval that actually queries the connected graph:
229
229
  `⌕ Egregore Connect · searching your organization’s memory and relationships`
230
230
 
231
- Runtime owns retrieval; no agent feature flag does. `EGREGORE_ORG_CONTEXT_V1`
232
- is only the schema label. When attached, authorized retrieval is complete:
231
+ When `EGREGORE_ORG_CONTEXT_V1` is attached, authorized retrieval is complete:
233
232
  never query again that turn, and state any evidence gap. `OPENED_BY_OBSERVE`
234
233
  means the evidence is open. A later explicit search starts a new turn.
235
234
 
236
- **Visibility is the contract.** Emit it verbatim as a
237
- standalone assistant message before the first Bash, Grep, Glob, Read, or other
238
- retrieval tool call.
239
- Shell/tool output does not satisfy this requirement. Do not paraphrase the line.
235
+ **Scope.** Egregore Runtime is for organizational recall only: team memory,
236
+ decisions, handoffs, prior work. Code, files, Git, and tests use the harness's
237
+ normal tools, with no beat and no Runtime call.
240
238
 
241
- Use the memory form normally; use Connect only for an explicit graph traversal
242
- that actually runs. Emit once per episode. A `bin/search.sh` banner is only a
243
- shell fallback and does not replace this visible beat.
239
+ **Visibility is the contract.** Emit it verbatim as a standalone assistant
240
+ message before the first tool call that performs the organizational recall
241
+ (the `bin/search.sh` query or a graph read). Tool output does not satisfy it.
242
+ Do not paraphrase the line.
243
+
244
+ Use Connect only for a graph traversal that actually runs. Emit once per
245
+ episode.
244
246
 
245
247
  **Routing:** Organizational recall always enters Egregore Runtime. The model is
246
248
  the semantic intent authority, deciding from meaning rather than fixed phrases.
@@ -251,20 +253,18 @@ Without sufficient attached context, make exactly one Runtime/QMD call:
251
253
  bash bin/search.sh query "<concept>" -n 6 --compact --context-packet
252
254
  ```
253
255
 
254
- `--context-packet` keeps the evidence out of the visible transcript and
255
- attaches it privately; the visible output is the attribution line alone.
256
- Never re-print ranked results, file/rank/excerpt metadata, or raw JSON.
256
+ `--context-packet` attaches the evidence privately; the visible output is the
257
+ attribution line alone. Never re-print ranked results or raw JSON.
257
258
 
258
259
  Do not resolve `memory/` to its sibling repo or start with raw `grep`/`ls`.
259
- Search owns lexical/semantic selection and attaches no graph by default. Open a
260
- source only when its excerpt cannot support a necessary claim. For current
260
+ Search attaches no graph by default. Open a source only when its excerpt
261
+ cannot support a necessary claim. For current
261
262
  team/person synthesis, retain known organization/member names and choose
262
- `--recent-days` plus `--min-evidence`. Use native
263
- Grep/Glob for code and exact errors. `/activity`, `/dashboard`, and `/project`
263
+ `--recent-days` plus `--min-evidence`. `/activity`, `/dashboard`, and `/project`
264
264
  display bounded status surfaces only when named or directly requested as a
265
- card. Questions asking for an explained synthesis of current organizational
266
- work are Runtime/QMD recall, as is prior-work discovery. Do not run full
267
- hybrid retrieval on unrelated prompts or infer graph from recall intent.
265
+ card. Explained syntheses of current organizational work and prior-work
266
+ discovery are Runtime/QMD recall. Do not run retrieval on unrelated prompts or
267
+ infer graph from recall intent.
268
268
 
269
269
  Do not emit the beat for code search, Git inspection, startup hydration, graph
270
270
  writes, ingestion, background sync, maintenance, or internal queries.
@@ -157,9 +157,8 @@ const NON_SKILL_TOKENS = new Set(['loop', 'fast', 'config', 'clear', 'help']);
157
157
  // hook delivers there. Rewrite the fallback command to the stdout form and
158
158
  // state the same hygiene contract in shell terms: search output is
159
159
  // model-internal; the visible reply carries only the retrieval line.
160
- const CLAUDE_PACKET_NOTE = `\`--context-packet\` keeps the evidence out of the visible transcript and
161
- attaches it privately; the visible output is the attribution line alone.
162
- Never re-print ranked results, file/rank/excerpt metadata, or raw JSON.`;
160
+ const CLAUDE_PACKET_NOTE = `\`--context-packet\` attaches the evidence privately; the visible output is the
161
+ attribution line alone. Never re-print ranked results or raw JSON.`;
163
162
  const SHELL_PACKET_NOTE = `Search output is model-internal: show only the retrieval line and receipts —
164
163
  never ranked blocks, file/rank/excerpt metadata, or raw JSON.`;
165
164
 
@@ -32,6 +32,19 @@ _maintenance_runtime() {
32
32
  python3 -m egregore_runtime.adapters.qmd_cli "$@"
33
33
  }
34
34
 
35
+ _now_ms() {
36
+ if command -v perl >/dev/null 2>&1; then
37
+ perl -MTime::HiRes=time -e 'printf("%d\n", time() * 1000)'
38
+ else
39
+ echo $(( $(date +%s) * 1000 ))
40
+ fi
41
+ }
42
+
43
+ _elapsed_s() {
44
+ # One-decimal seconds since a _now_ms() timestamp.
45
+ awk -v ms="$(( $(_now_ms) - $1 ))" 'BEGIN { printf "%.1f", ms / 1000 }'
46
+ }
47
+
35
48
  _result_paths() {
36
49
  local line path canonical_prefix
37
50
  canonical_prefix="**file:** \`memory/"
@@ -125,14 +138,14 @@ _banner_and_emit() {
125
138
  actual="${actual%% *}"
126
139
  required="${gap#*required=}"
127
140
  required="${required%% *}"
128
- dur=$(( $(date +%s) - t0 ))
141
+ dur=$(_elapsed_s "$t0")
129
142
  printf '⌕ Egregore · searching your organization’s memory · %s · evidence gap · %s/%s sources · %ss\n\n' \
130
143
  "$label" "$actual" "$required" "$dur"
131
144
  [ "$hide_results" = 1 ] || printf '%s\n' "$out" | tail -n +2
132
145
  return 0
133
146
  fi
134
147
  hits=$(printf '%s\n' "$out" | _result_paths | wc -l | tr -d ' ')
135
- dur=$(( $(date +%s) - t0 ))
148
+ dur=$(_elapsed_s "$t0")
136
149
  product="Egregore"
137
150
  surface="searching your organization’s memory"
138
151
  if [ "$emit" = "_enrich" ] && [ "$hits" -gt 0 ]; then
@@ -223,7 +236,7 @@ cmd_query() {
223
236
  esac
224
237
 
225
238
  local t0 out emit="cat" query_rc
226
- t0=$(date +%s)
239
+ t0=$(_now_ms)
227
240
  out=$(_runtime "${args[@]}" 2>&1)
228
241
  query_rc=$?
229
242
  if [ "$query_rc" -ne 0 ]; then
@@ -235,20 +248,31 @@ cmd_query() {
235
248
  } >&2
236
249
  return "$query_rc"
237
250
  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.
251
+ # The runtime marks a query that ran without its persistent worker (the
252
+ # pinned CLI started cold). Strip the marker from the evidence and carry
253
+ # it into the banner, the telemetry, and a one-line repair hint.
254
+ local cold=0 cold_json=false
255
+ if printf '%s\n' "$out" | grep -qx 'EGREGORE_RETRIEVAL_COLD_START'; then
256
+ cold=1
257
+ cold_json=true
258
+ out=$(printf '%s\n' "$out" | grep -vx 'EGREGORE_RETRIEVAL_COLD_START')
259
+ fi
260
+ # Content-free query telemetry: mode, duration, result count, degraded
261
+ # path. Never the query text. This is what makes Runtime/QMD retrieval
262
+ # visible in the telemetry event list instead of only legacy graph_query rows.
241
263
  local _tele_count _tele_ms
242
- _tele_ms=$(( ($(date +%s) - t0) * 1000 ))
264
+ _tele_ms=$(( $(_now_ms) - t0 ))
243
265
  _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
266
  if [[ "$_tele_count" =~ ^[0-9]+$ ]]; then
245
267
  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 &
268
+ "$(jq -n --arg mode "$label" --argjson d "$_tele_ms" --argjson c "$_tele_count" --argjson cold "$cold_json" \
269
+ '{mode:$mode, duration_ms:$d, count:$c, degraded:$cold}')" 2>/dev/null &
248
270
  else
249
271
  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 &
272
+ "$(jq -n --arg mode "$label" --argjson d "$_tele_ms" --argjson cold "$cold_json" \
273
+ '{mode:$mode, duration_ms:$d, degraded:$cold}')" 2>/dev/null &
251
274
  fi
275
+ [ "$cold" = 1 ] && label="${label} · cold start"
252
276
 
253
277
  [ "$enrich" = 1 ] && emit="_enrich"
254
278
  if [ "$context_packet" = 1 ]; then
@@ -263,6 +287,9 @@ cmd_query() {
263
287
  else
264
288
  _banner_and_emit "$label" "$t0" "$emit" "$out"
265
289
  fi
290
+ if [ "$cold" = 1 ]; then
291
+ echo " ↳ the retrieval server was not running, so this query started QMD cold. Repair: bash bin/search.sh start" >&2
292
+ fi
266
293
  }
267
294
 
268
295
  cmd_reindex() {
@@ -157,6 +157,11 @@ class QmdLocalRetriever:
157
157
  self._resolved_command: tuple[str, ...] | None = None
158
158
  self._version_checked = False
159
159
  self._daemon_available = False
160
+ # Outcome of the most recent typed query: True when the owned worker
161
+ # answered, False when the pinned CLI ran cold, None when no query
162
+ # ran or persistence is disabled. Read by the harness so a slow
163
+ # retrieval is never silent.
164
+ self._last_query_used_daemon: bool | None = None
160
165
  self._source_revision_cache: str | None = None
161
166
  self._configure_isolated_environment()
162
167
  self._prefer_usable_node_runtime()
@@ -726,7 +731,10 @@ class QmdLocalRetriever:
726
731
  limit: int,
727
732
  intent: str,
728
733
  ) -> list[dict[str, Any]]:
729
- if self._persistent_enabled() and self._ensure_daemon():
734
+ self._last_query_used_daemon = None
735
+ if not self._persistent_enabled():
736
+ daemon_error = "daemon unavailable"
737
+ elif self._ensure_daemon():
730
738
  payload = {
731
739
  "searches": searches,
732
740
  "collections": [self.collection],
@@ -746,6 +754,7 @@ class QmdLocalRetriever:
746
754
  body = json.loads(response.read().decode("utf-8"))
747
755
  results = body.get("results") if isinstance(body, dict) else None
748
756
  if isinstance(results, list):
757
+ self._last_query_used_daemon = True
749
758
  return [item for item in results if isinstance(item, dict)]
750
759
  except (OSError, ValueError, urllib.error.URLError) as exc:
751
760
  self._daemon_available = False
@@ -754,8 +763,13 @@ class QmdLocalRetriever:
754
763
  daemon_error = str(exc)
755
764
  else:
756
765
  daemon_error = "daemon returned an invalid result payload"
766
+ self._last_query_used_daemon = False
757
767
  else:
768
+ # The owned worker could not be started or reused. The pinned CLI
769
+ # answers cold; the harness surfaces that instead of a silent
770
+ # multi-second query.
758
771
  daemon_error = "daemon unavailable"
772
+ self._last_query_used_daemon = False
759
773
 
760
774
  query_document = "\n".join(
761
775
  f"{item['type']}: {self._single_line(item['query'])}" for item in searches
@@ -1009,6 +1023,13 @@ class QmdLocalRetriever:
1009
1023
  def _persistent_enabled(self) -> bool:
1010
1024
  return self.environment.get("EGREGORE_QMD_PERSISTENT", "1") != "0"
1011
1025
 
1026
+ @property
1027
+ def last_query_used_daemon(self) -> bool | None:
1028
+ """True when the last typed query answered through the owned worker,
1029
+ False when it fell back to a cold CLI run, None when no query ran or
1030
+ the persistent worker is disabled for this instance."""
1031
+ return self._last_query_used_daemon
1032
+
1012
1033
  def _ensure_daemon(self) -> bool:
1013
1034
  if not self._persistent_enabled():
1014
1035
  return False
@@ -1153,15 +1174,30 @@ class QmdLocalRetriever:
1153
1174
 
1154
1175
  @staticmethod
1155
1176
  def _port_available(port: int) -> bool:
1177
+ """A port is free only when both loopback families can bind it.
1178
+
1179
+ The worker binds "localhost", which resolves to ::1 as well as
1180
+ 127.0.0.1. Probing IPv4 alone reported a port free while a foreign
1181
+ server held [::1] on it; the owned worker then died on bind and every
1182
+ query ran the CLI cold.
1183
+ """
1156
1184
  if port == 8181:
1157
1185
  return False
1158
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
1159
- probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1186
+ for family, address in (
1187
+ (socket.AF_INET, "127.0.0.1"),
1188
+ (socket.AF_INET6, "::1"),
1189
+ ):
1160
1190
  try:
1161
- probe.bind(("127.0.0.1", port))
1162
- return True
1191
+ probe = socket.socket(family, socket.SOCK_STREAM)
1163
1192
  except OSError:
1164
- return False
1193
+ continue # family not available on this host
1194
+ with probe:
1195
+ probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1196
+ try:
1197
+ probe.bind((address, port))
1198
+ except OSError:
1199
+ return False
1200
+ return True
1165
1201
 
1166
1202
  def _allocate_port(self) -> int:
1167
1203
  for _ in range(20):
@@ -38,7 +38,7 @@ from .runtime import EgregoreRuntime, local_runtime
38
38
  _ORG_CONTEXT_PREFETCH_PATTERN = re.compile(
39
39
  "|".join(
40
40
  (
41
- r"\b(find|locate|dig up|look up|pull up)\b.*\b"
41
+ r"\b(find|locate|dig up|look up|pull up)\b[^\n]*\b"
42
42
  r"(handoff|decision|notes?|docs?|meeting|harvest|quest|reflection)s?\b",
43
43
  r"\bdid we (decide|discuss|agree|say|talk about|cover)\b",
44
44
  r"\bdo we have (any )?(notes?|docs?|anything|something)( on| about)?\b",
@@ -161,6 +161,19 @@ def _clear_observe_receipt(root: Path, harness: str) -> None:
161
161
  pass
162
162
 
163
163
 
164
+ def _actor_contract_marker_path(root: Path, harness: str) -> Path:
165
+ """Gitignored per-session marker: the full actor/retrieval contract was
166
+ already attached once. Later prompts receive a short reminder instead of
167
+ the ~4 KB block. bin/pre-compact.sh removes the marker so the first prompt
168
+ after a compaction re-attaches the full contract."""
169
+
170
+ session = re.sub(r"[^A-Za-z0-9_.-]", "-", _session_id(root))[:120]
171
+ return (
172
+ root / ".egregore" / "runtime" / "observe"
173
+ / f"actor-contract-{harness}-{session}.sent"
174
+ )
175
+
176
+
164
177
  def _write_observe_receipt(root: Path, harness: str, prompt: str, context) -> None:
165
178
  """Record that this prompt already received useful authorized evidence.
166
179
 
@@ -369,9 +382,46 @@ def _query(args: argparse.Namespace) -> int:
369
382
  )
370
383
  else:
371
384
  print("No results found.")
385
+ retriever = getattr(runtime, "retriever", None)
386
+ if not args.json and getattr(retriever, "last_query_used_daemon", None) is False:
387
+ # The owned QMD worker was unavailable and the pinned CLI answered
388
+ # cold. bin/search.sh turns this marker into a visible "cold start"
389
+ # note so a slow query is never silent.
390
+ print("EGREGORE_RETRIEVAL_COLD_START")
372
391
  return 0
373
392
 
374
393
 
394
+ _PASTED_OUTPUT_LINE = re.compile(
395
+ r"^(?:"
396
+ r"[ MADRCU?!]{2}\s+\S" # git status --short / porcelain rows
397
+ r"|[▶❯»]\s" # terminal prompt markers
398
+ r"|\$\s\S" # shell prompt
399
+ r"|(?:\+\+\+|---|@@)\s" # unified diff
400
+ r"|diff --git\b"
401
+ r"|Traceback \(most recent call last\)"
402
+ r"|\s+File \"[^\"]+\", line \d+"
403
+ r"|\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}" # log timestamps
404
+ r"|[A-Za-z0-9_./-]+/[A-Za-z0-9_.-]+\.[a-z]{1,6}$" # a bare path
405
+ r")"
406
+ )
407
+
408
+
409
+ def _looks_like_pasted_output(prompt: str) -> bool:
410
+ """True for terminal output, diffs, logs, and code blocks pasted into a prompt.
411
+
412
+ Those carry words like "handoff", "notes", or "docs" by accident; running a
413
+ retrieval on them attaches unrelated evidence and costs seconds. The actor
414
+ context still tells the model to recall once if the prompt truly needs it.
415
+ """
416
+
417
+ if "```" in prompt:
418
+ return True
419
+ lines = [line for line in prompt.splitlines() if line.strip()]
420
+ if len(lines) >= 8:
421
+ return True
422
+ return sum(1 for line in lines if _PASTED_OUTPUT_LINE.match(line)) >= 2
423
+
424
+
375
425
  def prompt_prefetches_org_context(prompt: str) -> bool:
376
426
  """Return whether Observe should prefetch likely organizational context.
377
427
 
@@ -384,6 +434,7 @@ def prompt_prefetches_org_context(prompt: str) -> bool:
384
434
  return bool(
385
435
  prompt
386
436
  and not _EXPLICIT_SEARCH.search(prompt)
437
+ and not _looks_like_pasted_output(prompt)
387
438
  and (
388
439
  _ORG_CONTEXT_PREFETCH_PATTERN.search(prompt)
389
440
  or _ORG_RITUAL_CONTEXT_PREFETCH_PATTERN.search(prompt)
@@ -408,8 +459,22 @@ def prompt_requests_actor_context(prompt: str) -> bool:
408
459
  return bool(prompt and prompt.strip())
409
460
 
410
461
 
411
- def _actor_context_block(runtime: EgregoreRuntime, root: Path, harness: str) -> str:
412
- """Render the minimum stable identity needed by harness rituals."""
462
+ _RECALL_ONLY_SCOPE = (
463
+ "Egregore Runtime is for organizational recall and memory only: team memory, "
464
+ "decisions, handoffs, meetings, prior work. Code search, file reads, Git, tests, "
465
+ "and every other task use the harness's normal tools (Grep, Glob, Read, Bash) "
466
+ "with no retrieval ritual and no Runtime call."
467
+ )
468
+
469
+
470
+ def _actor_context_block(
471
+ runtime: EgregoreRuntime, root: Path, harness: str, *, full: bool = True
472
+ ) -> str:
473
+ """Render the stable identity and the recall contract for harness rituals.
474
+
475
+ ``full`` renders the complete contract (once per session); otherwise a
476
+ short reminder that keeps identity and the recall-only scope in view.
477
+ """
413
478
 
414
479
  actor_context = _actor(runtime, root, harness)
415
480
  actor = actor_context.actor
@@ -429,6 +494,22 @@ def _actor_context_block(runtime: EgregoreRuntime, root: Path, harness: str) ->
429
494
  search_command = 'bash bin/search.sh query "<concept>" -n 6 --compact'
430
495
  if harness == "claude":
431
496
  search_command += " --context-packet"
497
+ if not full:
498
+ return "\n".join(
499
+ (
500
+ "EGREGORE_ACTOR_CONTEXT_V1",
501
+ "Reminder — the full contract was attached earlier in this session.",
502
+ f"Actor: {actor.display_name} · Actor ID: {actor.actor_id} · "
503
+ f"Organization ID: {actor_context.profile.org_id} · Runtime root: {root}",
504
+ _RECALL_ONLY_SCOPE,
505
+ "When a prompt needs that recall and no EGREGORE_ORG_CONTEXT_V1 block is "
506
+ f"attached, run exactly one `{search_command}`; exact latest/oldest/open "
507
+ "handoff questions use `bash bin/search.sh handoffs …` instead.",
508
+ "`me` and `myself` mean this Actor ID and display name; never infer identity "
509
+ "from the OS user, Git, or GitHub names.",
510
+ "END_EGREGORE_ACTOR_CONTEXT_V1",
511
+ )
512
+ )
432
513
  return "\n".join(
433
514
  (
434
515
  "EGREGORE_ACTOR_CONTEXT_V1",
@@ -445,6 +526,7 @@ def _actor_context_block(runtime: EgregoreRuntime, root: Path, harness: str) ->
445
526
  f"Runtime root: {root}",
446
527
  "Run Egregore retrieval and workflow commands from this Runtime root. "
447
528
  "Do not fall back to a launch checkout named elsewhere in the session.",
529
+ _RECALL_ONLY_SCOPE,
448
530
  "Organizational retrieval status: NOT_PERFORMED. This actor-only block "
449
531
  "does not mean organizational recall is unnecessary.",
450
532
  "Interpret the user's intent semantically, without requiring trigger words. "
@@ -681,11 +763,20 @@ def _prompt_hook(args: argparse.Namespace) -> int:
681
763
  args.limit,
682
764
  )
683
765
  else:
766
+ marker = _actor_contract_marker_path(root, args.harness)
767
+ full = not marker.exists()
684
768
  additional_context = _actor_context_block(
685
769
  runtime,
686
770
  root,
687
771
  args.harness,
772
+ full=full,
688
773
  )
774
+ if full:
775
+ try:
776
+ marker.parent.mkdir(parents=True, exist_ok=True)
777
+ marker.write_text(datetime.now(UTC).isoformat(), encoding="utf-8")
778
+ except OSError:
779
+ pass
689
780
  except Exception as exc: # Hooks are advisory and must never block a prompt.
690
781
  additional_context = (
691
782
  "Egregore Runtime context was requested but the Local Runtime is unavailable "
@@ -53,7 +53,7 @@
53
53
  {
54
54
  "section": "Egregore Retrieval Beat",
55
55
  "action": "keep",
56
- "sourceHash": "6c5cbb0d1c9a",
56
+ "sourceHash": "bb2f214689cb",
57
57
  "note": "Kept verbatim — product attribution applies across runtimes."
58
58
  },
59
59
  {
@@ -258,19 +258,21 @@ For each user-directed organizational lookup, show exactly one visible line:
258
258
  - retrieval that actually queries the connected graph:
259
259
  `⌕ Egregore Connect · searching your organization’s memory and relationships`
260
260
 
261
- Runtime owns retrieval; no agent feature flag does. `EGREGORE_ORG_CONTEXT_V1`
262
- is only the schema label. When attached, authorized retrieval is complete:
261
+ When `EGREGORE_ORG_CONTEXT_V1` is attached, authorized retrieval is complete:
263
262
  never query again that turn, and state any evidence gap. `OPENED_BY_OBSERVE`
264
263
  means the evidence is open. A later explicit search starts a new turn.
265
264
 
266
- **Visibility is the contract.** Emit it verbatim as a
267
- standalone assistant message before the first Bash, Grep, Glob, Read, or other
268
- retrieval tool call.
269
- Shell/tool output does not satisfy this requirement. Do not paraphrase the line.
265
+ **Scope.** Egregore Runtime is for organizational recall only: team memory,
266
+ decisions, handoffs, prior work. Code, files, Git, and tests use the harness's
267
+ normal tools, with no beat and no Runtime call.
270
268
 
271
- Use the memory form normally; use Connect only for an explicit graph traversal
272
- that actually runs. Emit once per episode. A `bin/search.sh` banner is only a
273
- shell fallback and does not replace this visible beat.
269
+ **Visibility is the contract.** Emit it verbatim as a standalone assistant
270
+ message before the first tool call that performs the organizational recall
271
+ (the `bin/search.sh` query or a graph read). Tool output does not satisfy it.
272
+ Do not paraphrase the line.
273
+
274
+ Use Connect only for a graph traversal that actually runs. Emit once per
275
+ episode.
274
276
 
275
277
  **Routing:** Organizational recall always enters Egregore Runtime. The model is
276
278
  the semantic intent authority, deciding from meaning rather than fixed phrases.
@@ -285,15 +287,14 @@ Search output is model-internal: show only the retrieval line and receipts —
285
287
  never ranked blocks, file/rank/excerpt metadata, or raw JSON.
286
288
 
287
289
  Do not resolve `memory/` to its sibling repo or start with raw `grep`/`ls`.
288
- Search owns lexical/semantic selection and attaches no graph by default. Open a
289
- source only when its excerpt cannot support a necessary claim. For current
290
+ Search attaches no graph by default. Open a source only when its excerpt
291
+ cannot support a necessary claim. For current
290
292
  team/person synthesis, retain known organization/member names and choose
291
- `--recent-days` plus `--min-evidence`. Use native
292
- Grep/Glob for code and exact errors. `$activity`, `$dashboard`, and `$project`
293
+ `--recent-days` plus `--min-evidence`. `$activity`, `$dashboard`, and `$project`
293
294
  display bounded status surfaces only when named or directly requested as a
294
- card. Questions asking for an explained synthesis of current organizational
295
- work are Runtime/QMD recall, as is prior-work discovery. Do not run full
296
- hybrid retrieval on unrelated prompts or infer graph from recall intent.
295
+ card. Explained syntheses of current organizational work and prior-work
296
+ discovery are Runtime/QMD recall. Do not run retrieval on unrelated prompts or
297
+ infer graph from recall intent.
297
298
 
298
299
  Do not emit the beat for code search, Git inspection, startup hydration, graph
299
300
  writes, ingestion, background sync, maintenance, or internal queries.
@@ -157,9 +157,8 @@ const NON_SKILL_TOKENS = new Set(['loop', 'fast', 'config', 'clear', 'help']);
157
157
  // hook delivers there. Rewrite the fallback command to the stdout form and
158
158
  // state the same hygiene contract in shell terms: search output is
159
159
  // model-internal; the visible reply carries only the retrieval line.
160
- const CLAUDE_PACKET_NOTE = `\`--context-packet\` keeps the evidence out of the visible transcript and
161
- attaches it privately; the visible output is the attribution line alone.
162
- Never re-print ranked results, file/rank/excerpt metadata, or raw JSON.`;
160
+ const CLAUDE_PACKET_NOTE = `\`--context-packet\` attaches the evidence privately; the visible output is the
161
+ attribution line alone. Never re-print ranked results or raw JSON.`;
163
162
  const SHELL_PACKET_NOTE = `Search output is model-internal: show only the retrieval line and receipts —
164
163
  never ranked blocks, file/rank/excerpt metadata, or raw JSON.`;
165
164
 
@@ -32,6 +32,19 @@ _maintenance_runtime() {
32
32
  python3 -m egregore_runtime.adapters.qmd_cli "$@"
33
33
  }
34
34
 
35
+ _now_ms() {
36
+ if command -v perl >/dev/null 2>&1; then
37
+ perl -MTime::HiRes=time -e 'printf("%d\n", time() * 1000)'
38
+ else
39
+ echo $(( $(date +%s) * 1000 ))
40
+ fi
41
+ }
42
+
43
+ _elapsed_s() {
44
+ # One-decimal seconds since a _now_ms() timestamp.
45
+ awk -v ms="$(( $(_now_ms) - $1 ))" 'BEGIN { printf "%.1f", ms / 1000 }'
46
+ }
47
+
35
48
  _result_paths() {
36
49
  local line path canonical_prefix
37
50
  canonical_prefix="**file:** \`memory/"
@@ -125,14 +138,14 @@ _banner_and_emit() {
125
138
  actual="${actual%% *}"
126
139
  required="${gap#*required=}"
127
140
  required="${required%% *}"
128
- dur=$(( $(date +%s) - t0 ))
141
+ dur=$(_elapsed_s "$t0")
129
142
  printf '⌕ Egregore · searching your organization’s memory · %s · evidence gap · %s/%s sources · %ss\n\n' \
130
143
  "$label" "$actual" "$required" "$dur"
131
144
  [ "$hide_results" = 1 ] || printf '%s\n' "$out" | tail -n +2
132
145
  return 0
133
146
  fi
134
147
  hits=$(printf '%s\n' "$out" | _result_paths | wc -l | tr -d ' ')
135
- dur=$(( $(date +%s) - t0 ))
148
+ dur=$(_elapsed_s "$t0")
136
149
  product="Egregore"
137
150
  surface="searching your organization’s memory"
138
151
  if [ "$emit" = "_enrich" ] && [ "$hits" -gt 0 ]; then
@@ -223,7 +236,7 @@ cmd_query() {
223
236
  esac
224
237
 
225
238
  local t0 out emit="cat" query_rc
226
- t0=$(date +%s)
239
+ t0=$(_now_ms)
227
240
  out=$(_runtime "${args[@]}" 2>&1)
228
241
  query_rc=$?
229
242
  if [ "$query_rc" -ne 0 ]; then
@@ -235,20 +248,31 @@ cmd_query() {
235
248
  } >&2
236
249
  return "$query_rc"
237
250
  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.
251
+ # The runtime marks a query that ran without its persistent worker (the
252
+ # pinned CLI started cold). Strip the marker from the evidence and carry
253
+ # it into the banner, the telemetry, and a one-line repair hint.
254
+ local cold=0 cold_json=false
255
+ if printf '%s\n' "$out" | grep -qx 'EGREGORE_RETRIEVAL_COLD_START'; then
256
+ cold=1
257
+ cold_json=true
258
+ out=$(printf '%s\n' "$out" | grep -vx 'EGREGORE_RETRIEVAL_COLD_START')
259
+ fi
260
+ # Content-free query telemetry: mode, duration, result count, degraded
261
+ # path. Never the query text. This is what makes Runtime/QMD retrieval
262
+ # visible in the telemetry event list instead of only legacy graph_query rows.
241
263
  local _tele_count _tele_ms
242
- _tele_ms=$(( ($(date +%s) - t0) * 1000 ))
264
+ _tele_ms=$(( $(_now_ms) - t0 ))
243
265
  _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
266
  if [[ "$_tele_count" =~ ^[0-9]+$ ]]; then
245
267
  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 &
268
+ "$(jq -n --arg mode "$label" --argjson d "$_tele_ms" --argjson c "$_tele_count" --argjson cold "$cold_json" \
269
+ '{mode:$mode, duration_ms:$d, count:$c, degraded:$cold}')" 2>/dev/null &
248
270
  else
249
271
  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 &
272
+ "$(jq -n --arg mode "$label" --argjson d "$_tele_ms" --argjson cold "$cold_json" \
273
+ '{mode:$mode, duration_ms:$d, degraded:$cold}')" 2>/dev/null &
251
274
  fi
275
+ [ "$cold" = 1 ] && label="${label} · cold start"
252
276
 
253
277
  [ "$enrich" = 1 ] && emit="_enrich"
254
278
  if [ "$context_packet" = 1 ]; then
@@ -263,6 +287,9 @@ cmd_query() {
263
287
  else
264
288
  _banner_and_emit "$label" "$t0" "$emit" "$out"
265
289
  fi
290
+ if [ "$cold" = 1 ]; then
291
+ echo " ↳ the retrieval server was not running, so this query started QMD cold. Repair: bash bin/search.sh start" >&2
292
+ fi
266
293
  }
267
294
 
268
295
  cmd_reindex() {