create-egregore 0.19.32 → 0.20.0

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 (173) hide show
  1. package/assets/egregore-launcher.js +118 -13
  2. package/bin/cli.js +48 -3
  3. package/lib/connect-runtime.js +4 -2
  4. package/lib/local.js +15 -1
  5. package/lib/prime-runtime.js +208 -0
  6. package/lib/setup.js +45 -3
  7. package/package.json +5 -4
  8. package/runtime/codex/.codex/skills/ingest-notion/SKILL.md +29 -0
  9. package/runtime/codex/.codex/skills/notion-connect/SKILL.md +29 -0
  10. package/runtime/codex/.codex/skills/teams-connect/SKILL.md +29 -0
  11. package/runtime/codex/.codex/spec-manifest.json +3 -3
  12. package/runtime/codex/AGENTS.md +11 -4
  13. package/runtime/codex/bin/activity-data.sh +4 -8
  14. package/runtime/codex/bin/attendant.sh +13 -0
  15. package/runtime/codex/bin/autosave.sh +27 -1
  16. package/runtime/codex/bin/codex-render-spec.mjs +6 -0
  17. package/runtime/codex/bin/codex-session-start.sh +8 -1
  18. package/runtime/codex/bin/connect-refresh.sh +33 -0
  19. package/runtime/codex/bin/graph-batch.sh +25 -6
  20. package/runtime/codex/bin/graph-op.sh +5 -4
  21. package/runtime/codex/bin/graph.sh +31 -5
  22. package/runtime/codex/bin/ingest.py +81 -5
  23. package/runtime/codex/bin/lib/context.sh +26 -1
  24. package/runtime/codex/bin/lib/greeting.sh +4 -1
  25. package/runtime/codex/bin/lib/worktree-links.sh +25 -0
  26. package/runtime/codex/manifest.json +8 -1
  27. package/runtime/pi/.codex/skills/ingest-notion/SKILL.md +29 -0
  28. package/runtime/pi/.codex/skills/notion-connect/SKILL.md +29 -0
  29. package/runtime/pi/.codex/skills/teams-connect/SKILL.md +29 -0
  30. package/runtime/pi/.codex/spec-manifest.json +3 -3
  31. package/runtime/pi/.pi/APPEND_SYSTEM.md +3 -2
  32. package/runtime/pi/.pi/extensions/egregore.ts +17 -6
  33. package/runtime/pi/.pi/spec-manifest.json +2 -2
  34. package/runtime/pi/AGENTS.md +11 -4
  35. package/runtime/pi/bin/activity-data.sh +4 -8
  36. package/runtime/pi/bin/attendant.sh +13 -0
  37. package/runtime/pi/bin/autosave.sh +27 -1
  38. package/runtime/pi/bin/codex-render-spec.mjs +6 -0
  39. package/runtime/pi/bin/codex-session-start.sh +8 -1
  40. package/runtime/pi/bin/connect-refresh.sh +33 -0
  41. package/runtime/pi/bin/graph-batch.sh +25 -6
  42. package/runtime/pi/bin/graph-op.sh +5 -4
  43. package/runtime/pi/bin/graph.sh +31 -5
  44. package/runtime/pi/bin/ingest.py +81 -5
  45. package/runtime/pi/bin/lib/context.sh +26 -1
  46. package/runtime/pi/bin/lib/greeting.sh +4 -1
  47. package/runtime/pi/bin/lib/worktree-links.sh +25 -0
  48. package/runtime/pi/bin/pi-branch-policy.mjs +138 -1
  49. package/runtime/pi/bin/pi-product-workflows.mjs +35 -7
  50. package/runtime/pi/manifest.json +8 -1
  51. package/runtime/prime/.claude/context/notification-consent.md +66 -0
  52. package/runtime/prime/.codex/config.toml +17 -0
  53. package/runtime/prime/.codex/hooks/branch-guard.js +461 -0
  54. package/runtime/prime/.codex/hooks/search-hint.js +67 -0
  55. package/runtime/prime/.codex/hooks.json +30 -0
  56. package/runtime/prime/.codex/skills/activity/SKILL.md +82 -0
  57. package/runtime/prime/.codex/skills/announce/SKILL.md +67 -0
  58. package/runtime/prime/.codex/skills/archive/SKILL.md +45 -0
  59. package/runtime/prime/.codex/skills/branch/SKILL.md +29 -0
  60. package/runtime/prime/.codex/skills/checkup/SKILL.md +45 -0
  61. package/runtime/prime/.codex/skills/commit/SKILL.md +29 -0
  62. package/runtime/prime/.codex/skills/contribute/SKILL.md +29 -0
  63. package/runtime/prime/.codex/skills/create-skill/SKILL.md +29 -0
  64. package/runtime/prime/.codex/skills/dark-mode/SKILL.md +29 -0
  65. package/runtime/prime/.codex/skills/dashboard/SKILL.md +59 -0
  66. package/runtime/prime/.codex/skills/deep-reflect/SKILL.md +109 -0
  67. package/runtime/prime/.codex/skills/delete-user/SKILL.md +29 -0
  68. package/runtime/prime/.codex/skills/emissary/SKILL.md +45 -0
  69. package/runtime/prime/.codex/skills/handoff/SKILL.md +166 -0
  70. package/runtime/prime/.codex/skills/harvest/QUESTION_PALETTE.md +76 -0
  71. package/runtime/prime/.codex/skills/harvest/SKILL.md +131 -0
  72. package/runtime/prime/.codex/skills/ingest/SKILL.md +29 -0
  73. package/runtime/prime/.codex/skills/ingest-notion/SKILL.md +29 -0
  74. package/runtime/prime/.codex/skills/ingest-user-interview/SKILL.md +45 -0
  75. package/runtime/prime/.codex/skills/invite/SKILL.md +44 -0
  76. package/runtime/prime/.codex/skills/issue/SKILL.md +45 -0
  77. package/runtime/prime/.codex/skills/me/SKILL.md +29 -0
  78. package/runtime/prime/.codex/skills/meeting/SKILL.md +45 -0
  79. package/runtime/prime/.codex/skills/note/SKILL.md +29 -0
  80. package/runtime/prime/.codex/skills/notion-connect/SKILL.md +29 -0
  81. package/runtime/prime/.codex/skills/onboarding/SKILL.md +45 -0
  82. package/runtime/prime/.codex/skills/pr/SKILL.md +29 -0
  83. package/runtime/prime/.codex/skills/pull/SKILL.md +29 -0
  84. package/runtime/prime/.codex/skills/push/SKILL.md +29 -0
  85. package/runtime/prime/.codex/skills/quest/SKILL.md +80 -0
  86. package/runtime/prime/.codex/skills/reflect/SKILL.md +45 -0
  87. package/runtime/prime/.codex/skills/review-pr/SKILL.md +45 -0
  88. package/runtime/prime/.codex/skills/save/SKILL.md +125 -0
  89. package/runtime/prime/.codex/skills/slack-connect/SKILL.md +29 -0
  90. package/runtime/prime/.codex/skills/summon/SKILL.md +45 -0
  91. package/runtime/prime/.codex/skills/sync-repos/SKILL.md +29 -0
  92. package/runtime/prime/.codex/skills/teams-connect/SKILL.md +29 -0
  93. package/runtime/prime/.codex/skills/telegram-connect/SKILL.md +29 -0
  94. package/runtime/prime/.codex/skills/telemetry/SKILL.md +29 -0
  95. package/runtime/prime/.codex/skills/test/SKILL.md +45 -0
  96. package/runtime/prime/.codex/skills/the-spiral/SKILL.md +84 -0
  97. package/runtime/prime/.codex/skills/tui-design/SKILL.md +45 -0
  98. package/runtime/prime/.codex/skills/tutorial/SKILL.md +45 -0
  99. package/runtime/prime/.codex/skills/update/SKILL.md +29 -0
  100. package/runtime/prime/.codex/skills/view/SKILL.md +156 -0
  101. package/runtime/prime/.codex/skills/view/agents/openai.yaml +4 -0
  102. package/runtime/prime/.codex/skills/view/assets/compose-scaffold.html +221 -0
  103. package/runtime/prime/.codex/skills/visual-explain/SKILL.md +45 -0
  104. package/runtime/prime/.codex/skills/wrap/SKILL.md +58 -0
  105. package/runtime/prime/.codex/spec-manifest.json +132 -0
  106. package/runtime/prime/.prime/agent/APPEND_SYSTEM.md +393 -0
  107. package/runtime/prime/.prime/agent/extensions/egregore.ts +460 -0
  108. package/runtime/prime/.prime/agent/settings.json +4 -0
  109. package/runtime/prime/.prime/agent/skills/egregore/SKILL.md +34 -0
  110. package/runtime/prime/.prime/agent/skills/egregore/pyproject.toml +13 -0
  111. package/runtime/prime/.prime/agent/skills/egregore/src/egregore/__init__.py +156 -0
  112. package/runtime/prime/.prime/agent/spec-manifest.json +12 -0
  113. package/runtime/prime/AGENTS.md +489 -0
  114. package/runtime/prime/bin/activity-data.sh +238 -0
  115. package/runtime/prime/bin/agent.sh +775 -0
  116. package/runtime/prime/bin/attendant.sh +707 -0
  117. package/runtime/prime/bin/autosave.sh +112 -0
  118. package/runtime/prime/bin/capture-reconcile.sh +69 -0
  119. package/runtime/prime/bin/capture-run.sh +341 -0
  120. package/runtime/prime/bin/codex-install-skills.sh +78 -0
  121. package/runtime/prime/bin/codex-render-spec.mjs +440 -0
  122. package/runtime/prime/bin/codex-session-start.sh +265 -0
  123. package/runtime/prime/bin/codex-skill-render.mjs +347 -0
  124. package/runtime/prime/bin/codex-sync-skills.sh +232 -0
  125. package/runtime/prime/bin/connect-refresh.sh +33 -0
  126. package/runtime/prime/bin/dashboard-data.sh +178 -0
  127. package/runtime/prime/bin/graph-batch.sh +79 -0
  128. package/runtime/prime/bin/graph-op.sh +1102 -0
  129. package/runtime/prime/bin/graph-wal.sh +229 -0
  130. package/runtime/prime/bin/graph.sh +223 -0
  131. package/runtime/prime/bin/handoff-pr-backfill.sh +86 -0
  132. package/runtime/prime/bin/handoff-run.sh +671 -0
  133. package/runtime/prime/bin/handoff-save-egregore.sh +212 -0
  134. package/runtime/prime/bin/index-handoff.sh +322 -0
  135. package/runtime/prime/bin/ingest-graph.sh +80 -0
  136. package/runtime/prime/bin/ingest-surface.html +495 -0
  137. package/runtime/prime/bin/ingest.py +756 -0
  138. package/runtime/prime/bin/ingest.sh +279 -0
  139. package/runtime/prime/bin/ingest_graph.py +856 -0
  140. package/runtime/prime/bin/ingest_surface.py +544 -0
  141. package/runtime/prime/bin/lib/artifact-id.sh +46 -0
  142. package/runtime/prime/bin/lib/config.sh +116 -0
  143. package/runtime/prime/bin/lib/context.sh +621 -0
  144. package/runtime/prime/bin/lib/dashboard-artifact.sh +242 -0
  145. package/runtime/prime/bin/lib/ensure-develop.sh +183 -0
  146. package/runtime/prime/bin/lib/git-sync.sh +406 -0
  147. package/runtime/prime/bin/lib/greeting.sh +519 -0
  148. package/runtime/prime/bin/lib/hash.sh +13 -0
  149. package/runtime/prime/bin/lib/identity.sh +213 -0
  150. package/runtime/prime/bin/lib/metrics.sh +374 -0
  151. package/runtime/prime/bin/lib/noncode.sh +31 -0
  152. package/runtime/prime/bin/lib/permissions.sh +106 -0
  153. package/runtime/prime/bin/lib/time.sh +56 -0
  154. package/runtime/prime/bin/lib/worktree-links.sh +79 -0
  155. package/runtime/prime/bin/notify.sh +636 -0
  156. package/runtime/prime/bin/person.py +630 -0
  157. package/runtime/prime/bin/person.sh +448 -0
  158. package/runtime/prime/bin/pi-branch-policy.mjs +312 -0
  159. package/runtime/prime/bin/pi-product-workflows.mjs +394 -0
  160. package/runtime/prime/bin/prime-render-spec.mjs +123 -0
  161. package/runtime/prime/bin/prime-session-start.sh +24 -0
  162. package/runtime/prime/bin/publish-artifact.sh +271 -0
  163. package/runtime/prime/bin/publish-references.sh +144 -0
  164. package/runtime/prime/bin/repo-state.sh +96 -0
  165. package/runtime/prime/bin/search.sh +240 -0
  166. package/runtime/prime/bin/session-autosave.sh +209 -0
  167. package/runtime/prime/bin/session-log.sh +199 -0
  168. package/runtime/prime/bin/settings.sh +304 -0
  169. package/runtime/prime/bin/telemetry.sh +391 -0
  170. package/runtime/prime/bin/worktree-create.sh +128 -0
  171. package/runtime/prime/bin/worktree-remove.sh +50 -0
  172. package/runtime/prime/bin/worktree.sh +198 -0
  173. package/runtime/prime/manifest.json +178 -0
@@ -23,10 +23,12 @@ from datetime import datetime, timezone
23
23
  from pathlib import Path
24
24
  from xml.etree import ElementTree
25
25
 
26
- sys.path.insert(0, str(Path(__file__).resolve().parent))
26
+ sys.path.append(str(Path(__file__).resolve().parent))
27
+ import catalogue as catalogue_module # noqa: E402
27
28
  import doc_segment # noqa: E402 (same-directory module, path set above)
28
29
  import pdf_extract # noqa: E402
29
30
  import source_profile # noqa: E402
31
+ import text_repair # noqa: E402
30
32
 
31
33
 
32
34
  ROOT = Path(__file__).resolve().parent.parent
@@ -152,6 +154,35 @@ def chunks(text: str, limit: int = 7000) -> list[str]:
152
154
  return result
153
155
 
154
156
 
157
+ def find_publisher_profile() -> Path | None:
158
+ """Where this instance's publisher profile is, if it has one.
159
+
160
+ The profile is what assigns a zone and a trust tier to each document, and
161
+ zone is a hard retrieval boundary. Ingesting without it produces a corpus
162
+ where every document is unplaced, so every later question is refused — a
163
+ failure that looks like an empty archive rather than a missing setting.
164
+
165
+ So this looks in the places a profile actually lives rather than in one
166
+ fixed path. An instance keeps its ontology under memory/knowledge/tools/,
167
+ not beside its intake metadata, and requiring an environment variable to
168
+ bridge that gap means the common case is the broken one.
169
+ """
170
+ override = os.environ.get("EGREGORE_PUBLISHER_PROFILE")
171
+ if override:
172
+ path = Path(override)
173
+ return path if path.is_file() else None
174
+
175
+ candidates = [metadata_root() / "publisher-profile.json"]
176
+ memory = Path("memory")
177
+ if memory.is_dir():
178
+ candidates.extend(sorted(memory.glob("knowledge/tools/*/publisher-profile.json")))
179
+ candidates.append(memory / "publisher-profile.json")
180
+ for path in candidates:
181
+ if path.is_file():
182
+ return path
183
+ return None
184
+
185
+
155
186
  def publisher_profile() -> dict | None:
156
187
  """Load the instance's publisher profile, if one is configured.
157
188
 
@@ -159,9 +190,8 @@ def publisher_profile() -> dict | None:
159
190
  operator set: deriving a region is an inference, and an inference must not
160
191
  silently replace a stated fact.
161
192
  """
162
- override = os.environ.get("EGREGORE_PUBLISHER_PROFILE")
163
- path = Path(override) if override else (metadata_root() / "publisher-profile.json")
164
- if not path.is_file():
193
+ path = find_publisher_profile()
194
+ if path is None:
165
195
  return None
166
196
  try:
167
197
  return json.loads(path.read_text(encoding="utf-8"))
@@ -223,7 +253,19 @@ def add_path(args: argparse.Namespace) -> dict:
223
253
  existing_manifest = load_manifest(directory)
224
254
  previous = {d["id"]: d for d in existing_manifest.get("documents", [])}
225
255
  now = datetime.now(timezone.utc).isoformat()
256
+ profile_path = find_publisher_profile()
226
257
  profile = publisher_profile()
258
+ if profile is None:
259
+ # Said once, plainly, because the alternative is a corpus that ingests
260
+ # cleanly and then refuses every question with no indication why.
261
+ print("ingest: no publisher-profile.json found — documents will carry no zone "
262
+ "or trust tier, and a zoneless document can never be served. Put one "
263
+ "under memory/knowledge/tools/<ontology>/ or set "
264
+ "EGREGORE_PUBLISHER_PROFILE.", file=sys.stderr)
265
+ catalogue = catalogue_module.load(
266
+ profile or {}, base=profile_path.parent if profile_path else None
267
+ )
268
+ repair_map = text_repair.load_mapping(profile or {})
227
269
  ingested = unchanged = metadata_updated = skipped = pruned = 0
228
270
  errors: list[dict] = []
229
271
  seen_paths: set[str] = set()
@@ -252,6 +294,15 @@ def add_path(args: argparse.Namespace) -> dict:
252
294
  extraction = {"kind": "local", "extractor": extractor} if extractor else None
253
295
  if extraction and detail:
254
296
  extraction["pages"] = detail
297
+ # Fix characters the document's own font encoding got wrong, before
298
+ # anything downstream indexes or quotes them.
299
+ repaired = None
300
+ if text and repair_map:
301
+ outcome = text_repair.repair(text, repair_map)
302
+ if outcome.replaced:
303
+ text = outcome.text
304
+ repaired = outcome.as_report()
305
+
255
306
  if error or text is None or not text.strip():
256
307
  skipped += 1
257
308
  errors.append({"path": relative, "error": error or "empty extracted text"})
@@ -283,6 +334,9 @@ def add_path(args: argparse.Namespace) -> dict:
283
334
  continue
284
335
  # Segment before chunking so no passage straddles two works. A document
285
336
  # holding a single work yields one segment, and chunking is unchanged.
337
+ if repaired and extraction:
338
+ extraction["repair"] = repaired
339
+
286
340
  segments = doc_segment.segment(text)
287
341
  document_chunks: list[tuple[str, doc_segment.Segment]] = []
288
342
  for seg in segments:
@@ -314,12 +368,34 @@ def add_path(args: argparse.Namespace) -> dict:
314
368
  # is absent filters nothing. A derived value is marked as derived: anything
315
369
  # downstream can tell it apart from one a person stated, and weigh it by the
316
370
  # confidence recorded alongside. An operator's value is never overwritten.
371
+ # Evidence strongest first: what the operator stated, then what the
372
+ # organisation's own catalogue records, then what can be inferred from
373
+ # the document's origin. A weaker source never overwrites a stronger one.
317
374
  provenance = None
318
375
  doc_boundaries = dict(boundaries)
376
+ catalogued = catalogue_module.lookup(catalogue, relative)
377
+
378
+ if catalogued:
379
+ provenance = {"from_catalogue": {k: v for k, v in catalogued.items()}}
380
+ for field_name, value in catalogued.items():
381
+ if field_name in ("zone", "crop", "topic", "customer", "jurisdiction", "project"):
382
+ if field_name not in doc_boundaries:
383
+ doc_boundaries[field_name] = value
384
+ provenance["applied_boundary"] = field_name
385
+ if catalogued.get("year"):
386
+ structure.setdefault("publication_year", catalogued["year"])
387
+ structure["year_source"] = structure.get("year_source") or "catalogue"
388
+ if catalogued.get("tier"):
389
+ provenance["tier"] = catalogued["tier"]
390
+ provenance["confidence"] = "high"
391
+
319
392
  if profile:
320
393
  resolved = source_profile.resolve(profile, filename=relative)
321
394
  if resolved.publisher_class or resolved.notes:
322
- provenance = resolved.as_dict()
395
+ inferred = resolved.as_dict()
396
+ # Keep the inference visible even when the catalogue answered,
397
+ # so a disagreement between the two is inspectable.
398
+ provenance = {**inferred, **(provenance or {})} if provenance else inferred
323
399
  key = source_profile.boundary_key(profile)
324
400
  if key and resolved.zone and key not in doc_boundaries:
325
401
  doc_boundaries[key] = resolved.zone
@@ -370,6 +370,29 @@ fi
370
370
  JSON="[]"
371
371
  RICH="[]"
372
372
  if [ -d "$SCRIPT_DIR/memory/handoffs" ]; then
373
+ # One definition everywhere: in connected mode the list comes from the
374
+ # graph's open-handoffs named read (open = pending/unread/read/claimed,
375
+ # matched by name/github/aliases) — the same definition the dashboard
376
+ # uses. The file grep below stays as the local-mode/offline fallback.
377
+ ADDRESSED=""
378
+ if [ "${LOCAL_MODE:-false}" != "true" ]; then
379
+ _GRAPH_LIST=$(bash "$SCRIPT_DIR/bin/graph-op.sh" open-handoffs "$AUTHOR" 5 2>/dev/null || true)
380
+ if echo "$_GRAPH_LIST" | jq -e '.values | length > 0' >/dev/null 2>&1; then
381
+ ADDRESSED=$(echo "$_GRAPH_LIST" | jq -r '.values[][0]' 2>/dev/null | awk '!seen[$0]++' | while IFS= read -r _SID; do
382
+ # Two on-disk layouts: dated subdirs (YYYY-MM/DD-slug.md, the id's
383
+ # own shape) and flat files at the handoffs root. Probe both, else
384
+ # a graph-listed handoff silently vanishes from the greeting.
385
+ _MONTH="${_SID:0:7}"
386
+ _REST="${_SID:8}"
387
+ _F="$SCRIPT_DIR/memory/handoffs/$_MONTH/$_REST.md"
388
+ if [ -f "$_F" ]; then
389
+ echo "$_F"
390
+ elif [ -f "$SCRIPT_DIR/memory/handoffs/$_SID.md" ]; then
391
+ echo "$SCRIPT_DIR/memory/handoffs/$_SID.md"
392
+ fi
393
+ done | head -5)
394
+ fi
395
+ fi
373
396
  # Match github username, display name, github_name first word, and people-file name
374
397
  # Handoff files may use "to: name", "**To**: name", or "To: name"
375
398
  _DISPLAY=$(jq -r '.display_name // empty' "$STATE_FILE" 2>/dev/null)
@@ -387,7 +410,9 @@ fi
387
410
  echo "$_GREP_PAT" | grep -qF "$_VARIANT" && continue
388
411
  _GREP_PAT="$_GREP_PAT\|[Tt]o[*]*: *$_VARIANT\|[Tt]o[*]*:$_VARIANT"
389
412
  done
390
- ADDRESSED=$(grep -rli "$_GREP_PAT" "$SCRIPT_DIR/memory/handoffs/" 2>/dev/null | sort -r | head -5 || true)
413
+ if [ -z "$ADDRESSED" ]; then
414
+ ADDRESSED=$(grep -rli "$_GREP_PAT" "$SCRIPT_DIR/memory/handoffs/" 2>/dev/null | sort -r | head -5 || true)
415
+ fi
391
416
  JSON="["
392
417
  RICH="["
393
418
  FIRST=true
@@ -188,7 +188,10 @@ fi
188
188
  # Staged auto-saves awaiting the user's merge (the /autosave consent gate).
189
189
  # Live gh query so the line persists until the PRs actually merge — the
190
190
  # ledger cursor above only reports once. Fail-soft when gh/network is out.
191
- if command -v gh >/dev/null 2>&1; then
191
+ # Shown only while the autosave experiment is enabled: stale PRs from a
192
+ # disabled experiment should not nag every greeting.
193
+ AUTOSAVE_ON=$(jq -r '.autosave_enabled // false' "$STATE_FILE" 2>/dev/null)
194
+ if [ "$AUTOSAVE_ON" = "true" ] && command -v gh >/dev/null 2>&1; then
192
195
  GH_SELF=$(jq -r '.github_username // empty' "$STATE_FILE" 2>/dev/null)
193
196
  if [ -n "$GH_SELF" ]; then
194
197
  WAITING_AS=$(gh pr list --state open --author "$GH_SELF" --limit 20 \
@@ -52,3 +52,28 @@ egregore_link_shared_state() {
52
52
  fi
53
53
  done
54
54
  }
55
+
56
+ # Resolve the .env that governs a checkout. A worktree can be missing its .env
57
+ # symlink (created before .env existed, or by a runtime that skipped worktree
58
+ # setup) — self-heal the links, then fall back to the main checkout's .env so
59
+ # credential loading never silently comes up empty. Prints the usable path, or
60
+ # nothing when neither checkout has one. Always returns 0.
61
+ egregore_env_file() {
62
+ local project_dir="${1:?project dir required}"
63
+ if [ ! -f "$project_dir/.env" ] && [ -f "$project_dir/.git" ]; then
64
+ egregore_link_shared_state "$project_dir" 2>/dev/null || true
65
+ fi
66
+ if [ -f "$project_dir/.env" ]; then
67
+ echo "$project_dir/.env"
68
+ return 0
69
+ fi
70
+ if [ -f "$project_dir/.git" ]; then
71
+ local main_dir
72
+ main_dir="$(egregore_main_project_dir "$project_dir" 2>/dev/null || true)"
73
+ if [ -n "$main_dir" ] && [ -f "$main_dir/.env" ]; then
74
+ echo "$main_dir/.env"
75
+ return 0
76
+ fi
77
+ fi
78
+ return 0
79
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "source": "egregore-runtime",
3
3
  "policy": "available OSS components selected by capability-distribution.json; Connect is an authenticated additive overlay",
4
- "skillCount": 43,
4
+ "skillCount": 46,
5
5
  "skills": [
6
6
  "activity",
7
7
  "announce",
@@ -19,12 +19,14 @@
19
19
  "handoff",
20
20
  "harvest",
21
21
  "ingest",
22
+ "ingest-notion",
22
23
  "ingest-user-interview",
23
24
  "invite",
24
25
  "issue",
25
26
  "me",
26
27
  "meeting",
27
28
  "note",
29
+ "notion-connect",
28
30
  "onboarding",
29
31
  "pr",
30
32
  "pull",
@@ -36,6 +38,7 @@
36
38
  "slack-connect",
37
39
  "summon",
38
40
  "sync-repos",
41
+ "teams-connect",
39
42
  "telegram-connect",
40
43
  "telemetry",
41
44
  "test",
@@ -71,6 +74,7 @@
71
74
  ".codex/skills/handoff/SKILL.md",
72
75
  ".codex/skills/harvest/QUESTION_PALETTE.md",
73
76
  ".codex/skills/harvest/SKILL.md",
77
+ ".codex/skills/ingest-notion/SKILL.md",
74
78
  ".codex/skills/ingest-user-interview/SKILL.md",
75
79
  ".codex/skills/ingest/SKILL.md",
76
80
  ".codex/skills/invite/SKILL.md",
@@ -78,6 +82,7 @@
78
82
  ".codex/skills/me/SKILL.md",
79
83
  ".codex/skills/meeting/SKILL.md",
80
84
  ".codex/skills/note/SKILL.md",
85
+ ".codex/skills/notion-connect/SKILL.md",
81
86
  ".codex/skills/onboarding/SKILL.md",
82
87
  ".codex/skills/pr/SKILL.md",
83
88
  ".codex/skills/pull/SKILL.md",
@@ -89,6 +94,7 @@
89
94
  ".codex/skills/slack-connect/SKILL.md",
90
95
  ".codex/skills/summon/SKILL.md",
91
96
  ".codex/skills/sync-repos/SKILL.md",
97
+ ".codex/skills/teams-connect/SKILL.md",
92
98
  ".codex/skills/telegram-connect/SKILL.md",
93
99
  ".codex/skills/telemetry/SKILL.md",
94
100
  ".codex/skills/test/SKILL.md",
@@ -114,6 +120,7 @@
114
120
  "bin/codex-session-start.sh",
115
121
  "bin/codex-skill-render.mjs",
116
122
  "bin/codex-sync-skills.sh",
123
+ "bin/connect-refresh.sh",
117
124
  "bin/dashboard-data.sh",
118
125
  "bin/graph-batch.sh",
119
126
  "bin/graph-op.sh",
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: ingest-notion
3
+ description: 'Fallback Codex adapter for the Egregore ingest-notion workflow.'
4
+ ---
5
+
6
+ <!-- generated-by: bin/codex-sync-skills.sh -->
7
+
8
+ # Egregore ingest-notion Adapter
9
+
10
+ This is fallback coverage for a long-tail Egregore workflow that has not been
11
+ ported to a hand-written Codex-native skill yet.
12
+
13
+ Use the project shell and filesystem directly. Do not invoke Claude Code
14
+ commands. Translate interactive choices to structured Codex question tooling
15
+ when it is available; otherwise render compact numbered choices with an
16
+ `Other:` option and wait for the user.
17
+
18
+ 1. Read `.claude/skills/ingest-notion/SKILL.md` for the workflow details.
19
+ 2. Run the referenced `bin/` scripts directly from Codex.
20
+ 3. Treat graph and publish steps as best-effort unless that workflow explicitly
21
+ says they are required.
22
+ 4. For every external notification, follow
23
+ `.claude/context/notification-consent.md`: plan without sending, then show
24
+ a separate exact Send / Edit / Cancel checkpoint. Never infer notification
25
+ consent from the workflow request or a batch approval.
26
+ 5. Keep local-mode behavior filesystem-first and avoid graph or notification
27
+ calls when `egregore.json` declares `"mode": "local"`.
28
+ 6. Never call the deprecated `egregore-handoff` CLI for Egregore project
29
+ handoffs.
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: notion-connect
3
+ description: 'Fallback Codex adapter for the Egregore notion-connect workflow.'
4
+ ---
5
+
6
+ <!-- generated-by: bin/codex-sync-skills.sh -->
7
+
8
+ # Egregore notion-connect Adapter
9
+
10
+ This is fallback coverage for a long-tail Egregore workflow that has not been
11
+ ported to a hand-written Codex-native skill yet.
12
+
13
+ Use the project shell and filesystem directly. Do not invoke Claude Code
14
+ commands. Translate interactive choices to structured Codex question tooling
15
+ when it is available; otherwise render compact numbered choices with an
16
+ `Other:` option and wait for the user.
17
+
18
+ 1. Read `.claude/skills/notion-connect/SKILL.md` for the workflow details.
19
+ 2. Run the referenced `bin/` scripts directly from Codex.
20
+ 3. Treat graph and publish steps as best-effort unless that workflow explicitly
21
+ says they are required.
22
+ 4. For every external notification, follow
23
+ `.claude/context/notification-consent.md`: plan without sending, then show
24
+ a separate exact Send / Edit / Cancel checkpoint. Never infer notification
25
+ consent from the workflow request or a batch approval.
26
+ 5. Keep local-mode behavior filesystem-first and avoid graph or notification
27
+ calls when `egregore.json` declares `"mode": "local"`.
28
+ 6. Never call the deprecated `egregore-handoff` CLI for Egregore project
29
+ handoffs.
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: teams-connect
3
+ description: 'Fallback Codex adapter for the Egregore teams-connect workflow.'
4
+ ---
5
+
6
+ <!-- generated-by: bin/codex-sync-skills.sh -->
7
+
8
+ # Egregore teams-connect Adapter
9
+
10
+ This is fallback coverage for a long-tail Egregore workflow that has not been
11
+ ported to a hand-written Codex-native skill yet.
12
+
13
+ Use the project shell and filesystem directly. Do not invoke Claude Code
14
+ commands. Translate interactive choices to structured Codex question tooling
15
+ when it is available; otherwise render compact numbered choices with an
16
+ `Other:` option and wait for the user.
17
+
18
+ 1. Read `.claude/skills/teams-connect/SKILL.md` for the workflow details.
19
+ 2. Run the referenced `bin/` scripts directly from Codex.
20
+ 3. Treat graph and publish steps as best-effort unless that workflow explicitly
21
+ says they are required.
22
+ 4. For every external notification, follow
23
+ `.claude/context/notification-consent.md`: plan without sending, then show
24
+ a separate exact Send / Edit / Cancel checkpoint. Never infer notification
25
+ consent from the workflow request or a batch approval.
26
+ 5. Keep local-mode behavior filesystem-first and avoid graph or notification
27
+ calls when `egregore.json` declares `"mode": "local"`.
28
+ 6. Never call the deprecated `egregore-handoff` CLI for Egregore project
29
+ handoffs.
@@ -35,7 +35,7 @@
35
35
  {
36
36
  "section": "After Greeting — BRANCH ON FIRST RESPONSE",
37
37
  "action": "replace",
38
- "sourceHash": "88410d7c6c00",
38
+ "sourceHash": "d28a5c162ab4",
39
39
  "note": "EnterWorktree -> bin/agent.sh branch with git checkout fallback; AskUserQuestion -> numbered options in plain text; branch-guard mapped to .codex/hooks/branch-guard.js with instruction fallback; plan-mode note dropped (no plan mode on Codex); /branch + /onboarding -> skill tokens. Handoff claiming and repoState auto-checkout kept verbatim (pure git)."
40
40
  },
41
41
  {
@@ -101,7 +101,7 @@
101
101
  {
102
102
  "section": "Command Awareness",
103
103
  "action": "keep",
104
- "sourceHash": "905ad739e18c",
104
+ "sourceHash": "be00d1fc715f",
105
105
  "note": "Prepended the Codex command surface (skill tokens, native vs adapter split, $save abstraction); disambiguation map kept with /x -> $x token rendering. Tokens with no Codex skill (e.g. /loop, a Claude Code harness feature) are left as-is."
106
106
  },
107
107
  {
@@ -119,7 +119,7 @@
119
119
  {
120
120
  "section": "Mode",
121
121
  "action": "keep",
122
- "sourceHash": "ae5cbb9c5d30",
122
+ "sourceHash": "0f74e18fd1b4",
123
123
  "note": "Kept; /env and /checkup rendered as $skill tokens."
124
124
  },
125
125
  {
@@ -355,8 +355,9 @@ Egregore runs in one of two configurations, set by `mode` in `egregore.json`. De
355
355
 
356
356
  **Hard rules in local mode:**
357
357
  - Never tell the user to "ask their admin" for credentials. The user IS the admin.
358
- - Never surface `api_url`, `EGREGORE_API_KEY`, or "connected mode" as an upgrade path. Hosted Egregore is a separate service, not something a user can turn on by adding fields to `egregore.json`.
359
- - If a feature genuinely requires the hosted service, say so plainly ("this isn't available in this configuration") and stop. Do not improvise a path to enabling it.
358
+ - Never surface `api_url`, `EGREGORE_API_KEY`, or config edits as an upgrade path. Hosted Egregore is not something a user can turn on by adding fields to `egregore.json`.
359
+ - **The one sanctioned upgrade path is `egregore connect`** (the launcher flow: registers the org, provisions the key, replays the graph). Connect-tier connector skills carry an explicit upsell gate for local instances — deliver their message verbatim, offer the upgrade via `egregore connect` or a clean "not now", and stop on decline. Beyond that gate, do not improvise: no hand-set `api_url`, no partial flows.
360
+ - If a feature requires the hosted service and has no upsell gate, say so plainly ("this isn't available in this configuration") and stop.
360
361
  - Calling `bin/graph.sh` or `bin/notify.sh` in local mode is harmless — they fail soft and return empty results — but there is no need to call them; prefer reading `memory/` directly.
361
362
 
362
363
  **Connected mode** (`"mode": "connected"`, `api_url` set) — the hosted configuration, used by organizations on the hosted service. Full feature set: Neo4j knowledge graph via `bin/graph.sh`, Telegram notifications via `bin/notify.sh`, dashboard publication, API-backed context gathering on session start. Use `/env` to check API key, `/checkup` for diagnostics. If the graph is offline, show troubleshooting.
@@ -2,10 +2,12 @@ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
2
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { Text } from "@earendil-works/pi-tui";
4
4
  import { Type } from "typebox";
5
- import { isMutatingBash, isRuntimeStatePath, mutatesOnlyRuntimeState } from "../../bin/pi-branch-policy.mjs";
5
+ import { isConsentCommand, isMutatingBash, isRuntimeStatePath, mutatesOnlyRuntimeState } from "../../bin/pi-branch-policy.mjs";
6
6
  import {
7
+ activityDataPath,
7
8
  captureSessionEnd,
8
9
  createHandoff,
10
+ lastActivityJson,
9
11
  parseViewRequest,
10
12
  publishScroll,
11
13
  renderActivity,
@@ -220,9 +222,21 @@ export default function egregore(pi) {
220
222
  const focus = await ctx.ui.select("Focus", ["Handoffs", "Questions", "Recent work", "Dashboard", "Done"]);
221
223
  if (!focus || focus === "Done") return;
222
224
  const command = focus === "Dashboard" ? "dashboard" : "activity";
225
+ // Hand the model the complete data the extension already fetched, as a
226
+ // file: one fast read, never truncated, no reason to re-run the data
227
+ // script or spelunk renderer sources.
228
+ const cached = lastActivityJson();
223
229
  const prompt = focus === "Dashboard"
224
230
  ? workflowPrompt("dashboard", "")
225
- : `Continue the Egregore /activity workflow with focus: ${focus.toLowerCase()}. Work quietly and show only the designed result.`;
231
+ : [
232
+ `Continue the Egregore /activity workflow with focus: ${focus.toLowerCase()}.`,
233
+ "Output nothing except the final rendered result — no prose, planning, or narration between commands.",
234
+ cached
235
+ ? `The complete activity data is already fresh at ${relative(root, activityDataPath(root))} — read that file in one call; do NOT re-run bin/activity-data.sh, read renderer sources, or run other data commands.`
236
+ : "",
237
+ "If required data is missing or a command fails, render the single line '◦ activity data unavailable' and stop — never debug data plumbing inside this workflow.",
238
+ "Rows without a topic: show the branch name instead; with neither, omit the row — never print (untitled).",
239
+ ].filter(Boolean).join("\n");
226
240
  sendQuietWorkflow(command, "", ctx, { prompt, working: `Opening ${focus.toLowerCase()}…` });
227
241
  },
228
242
  });
@@ -398,10 +412,7 @@ export default function egregore(pi) {
398
412
  // (echo 'develop' into .egregore-branch-consent) contains a redirect and
399
413
  // would otherwise be blocked by this very gate.
400
414
  if (event.toolName === "bash") {
401
- const consentCmd = new RegExp(
402
- "^\\s*echo\\s+['\"]?" + branch + "['\"]?\\s*>\\s*\\.egregore-branch-consent\\s*$"
403
- );
404
- if (consentCmd.test(String(event.input?.command || "").trim())) return;
415
+ if (isConsentCommand(String(event.input?.command || "").trim(), branch)) return;
405
416
  }
406
417
 
407
418
  const consentFile = join(gitDir, ".egregore-branch-consent");
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "generatedBy": "bin/pi-render-spec.mjs",
3
3
  "source": "AGENTS.md generated Codex spec",
4
- "sourceHash": "02c4d2d275db",
5
- "outputHash": "dc9d961b8ce2",
4
+ "sourceHash": "72d7c3986402",
5
+ "outputHash": "ac3aab4c039b",
6
6
  "adaptations": [
7
7
  "Pi project trust and APPEND_SYSTEM delivery",
8
8
  "Pi tool_call branch gate",
@@ -14,6 +14,12 @@
14
14
  > `.pi/APPEND_SYSTEM.md`, and your workflows are exposed through the
15
15
  > project-local `.pi/` runtime. Do not follow the Codex-specific block below.
16
16
  >
17
+ > **If you are Prime Agent** (the `prime-agent` harness): **stop here.** Your
18
+ > authoritative Egregore instructions are loaded from
19
+ > `.prime/agent/APPEND_SYSTEM.md`, and your workflows are exposed through the
20
+ > project-local `.prime/agent/` runtime. Do not follow the Codex-specific
21
+ > block below.
22
+ >
17
23
  > **If you are Codex or another shell-only agent:** this file is yours — continue.
18
24
 
19
25
  Egregore is no longer only a Claude Code workspace. Claude Code remains the
@@ -395,7 +401,7 @@ Invoke commands from user intent — don't wait for the slash. Each command file
395
401
  **Knowledge** — `$search` `$deep-reflect` `$archive` `$note` `$add` `$meeting` `$ingest` `$scroll` `$mock` `$audit`
396
402
  **Identity** — `$me` (view profile or set display name)
397
403
  **Coordination** — `$ask` `$quest` `$issue` `$invite` `$delete-user` `$announce`
398
- **Connectors** — `$notion-connect` (Notion workspace) `$telegram-connect` (Telegram group setup) `$teams-connect` (Microsoft Teams channel setup)
404
+ **Connectors** — `$notion-connect` (Notion workspace) `$telegram-connect` (Telegram group setup) `$teams-connect` (Microsoft Teams channel setup) `$slack-connect` (Slack channel setup)
399
405
  **Git** — `$branch` `$commit` `$push` `$pr` `$save` `$review-pr` `$contribute`
400
406
  **Spirits** — `$summon` (persistent agent processes)
401
407
  **Skills** — `$create-skill` (org-owned skill: scaffold, protect from updates, share)
@@ -411,7 +417,7 @@ Invoke commands from user intent — don't wait for the slash. Each command file
411
417
  - Tasks: `$todo` (personal) · `$quest` (team exploration) · `$issue` (something broken)
412
418
  - Questions: `$ask [person]` (async) · just ask (agent answers from context)
413
419
  - Ingestion: `$ingest <file-or-folder>` (org-scoped corpus intake) · `$ingest meeting` · `$ingest user-interview` · `$ingest google` · ambiguous → ask which type
414
- - Connectors: `$notion-connect` (Notion workspace) · `$telegram-connect` (Telegram group) · `$teams-connect` (MS Teams channel) · `$ingest` (bring content in) · `$ingest notion` (promote Notion docs)
420
+ - Connectors: `$notion-connect` (Notion workspace) · `$telegram-connect` (Telegram group) · `$teams-connect` (MS Teams channel) · `$slack-connect` (Slack channel) · `$ingest` (bring content in) · `$ingest notion` (promote Notion docs)
415
421
  - Identity: `$me` — "who am I", "call me oz"
416
422
  - People: `$invite` (add) · `$delete-user` (remove)
417
423
  - PRs: `$pr` (create) · `$review-pr` (review)
@@ -451,8 +457,9 @@ Egregore runs in one of two configurations, set by `mode` in `egregore.json`. De
451
457
 
452
458
  **Hard rules in local mode:**
453
459
  - Never tell the user to "ask their admin" for credentials. The user IS the admin.
454
- - Never surface `api_url`, `EGREGORE_API_KEY`, or "connected mode" as an upgrade path. Hosted Egregore is a separate service, not something a user can turn on by adding fields to `egregore.json`.
455
- - If a feature genuinely requires the hosted service, say so plainly ("this isn't available in this configuration") and stop. Do not improvise a path to enabling it.
460
+ - Never surface `api_url`, `EGREGORE_API_KEY`, or config edits as an upgrade path. Hosted Egregore is not something a user can turn on by adding fields to `egregore.json`.
461
+ - **The one sanctioned upgrade path is `egregore connect`** (the launcher flow: registers the org, provisions the key, replays the graph). Connect-tier connector skills carry an explicit upsell gate for local instances — deliver their message verbatim, offer the upgrade via `egregore connect` or a clean "not now", and stop on decline. Beyond that gate, do not improvise: no hand-set `api_url`, no partial flows.
462
+ - If a feature requires the hosted service and has no upsell gate, say so plainly ("this isn't available in this configuration") and stop.
456
463
  - Calling `bin/graph.sh` or `bin/notify.sh` in local mode is harmless — they fail soft and return empty results — but there is no need to call them; prefer reading `memory/` directly.
457
464
 
458
465
  **Connected mode** (`"mode": "connected"`, `api_url` set) — the hosted configuration, used by organizations on the hosted service. Full feature set: Neo4j knowledge graph via `bin/graph.sh`, Telegram notifications via `bin/notify.sh`, dashboard publication, API-backed context gathering on session start. Use `$env` to check API key, `$checkup` for diagnostics. If the graph is offline, show troubleshooting.
@@ -76,15 +76,11 @@ API_PID=$!
76
76
 
77
77
  # Git/disk: runs in parallel (stays client-side)
78
78
  (
79
- # Sync
79
+ # Refresh remote refs only. A data read must never mutate the working
80
+ # branch: the rebase this block used to run rewrote users' task branches as
81
+ # a side effect of /activity (observed rewinding a pushed branch mid-session).
82
+ # Workspace sync belongs to agent.sh sync / the session-start path.
80
83
  git -C "$SCRIPT_DIR" fetch origin --quiet 2>/dev/null || true
81
- CURRENT=$(git -C "$SCRIPT_DIR" branch --show-current 2>/dev/null || echo "unknown")
82
- if [ "$CURRENT" != "develop" ]; then
83
- git -C "$SCRIPT_DIR" fetch origin develop:develop --quiet 2>/dev/null || true
84
- fi
85
- if [[ "$CURRENT" == dev/* ]]; then
86
- git -C "$SCRIPT_DIR" rebase develop --quiet >/dev/null 2>&1 || git -C "$SCRIPT_DIR" rebase --abort >/dev/null 2>&1 || true
87
- fi
88
84
 
89
85
  # Memory sync
90
86
  if [ -d "$SCRIPT_DIR/memory/.git" ] || [ -L "$SCRIPT_DIR/memory" ]; then
@@ -99,6 +99,16 @@ _trigger_auto_thread() {
99
99
  ) >> "$LOGFILE" 2>&1 &
100
100
  }
101
101
 
102
+ # Personal off switch for ambient capture. ripcord_enabled=false disables BOTH
103
+ # ripcord notes and auto-push of dirty worktrees — a user who opts out of
104
+ # ambient capture must not find silent commits either. The attendant's other
105
+ # duties (graph cache, context seed, handoff lifecycle) are unaffected.
106
+ # NOTE: jq's `//` treats false as empty (the auto_update:false gotcha), so the
107
+ # test compares the raw value, never `.key // true`.
108
+ _capture_enabled() {
109
+ [ "$(jq -r '.ripcord_enabled' "$MAIN_DIR/.egregore-state.json" 2>/dev/null)" != "false" ]
110
+ }
111
+
102
112
  _author() {
103
113
  local a
104
114
  a=$(jq -r '.github_username // empty' "$MAIN_DIR/.egregore-state.json" 2>/dev/null)
@@ -249,6 +259,8 @@ _ripcord() {
249
259
  local transcript="$1" base author date_ym date_d slug topic file prompts gitsum
250
260
  base=$(basename "$transcript" .jsonl)
251
261
 
262
+ _capture_enabled || return 0
263
+
252
264
  grep -q "$base" "$JOURNAL" 2>/dev/null && return 0
253
265
  # Session already ended with ceremony? Then the attendant has no business
254
266
  # here. Scan only the tail — a session that merely *read* the /handoff or
@@ -336,6 +348,7 @@ _ripcord() {
336
348
  # --- Auto-push -------------------------------------------------------------
337
349
 
338
350
  _auto_push() {
351
+ _capture_enabled || return 0
339
352
  local wt branch dirty ahead upstream
340
353
  git -C "$MAIN_DIR" worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2}' | while read -r wt; do
341
354
  branch=$(git -C "$wt" branch --show-current 2>/dev/null)
@@ -28,6 +28,17 @@ set -uo pipefail
28
28
  SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
29
29
  STATE_FILE="$SCRIPT_DIR/.egregore-state.json"
30
30
 
31
+ # The attendant runs against the MAIN checkout and reads its state file, so
32
+ # ambient-capture settings must be written there — a worktree keeps its own
33
+ # copy, and writing the flag into it would silently do nothing.
34
+ MAIN_STATE_DIR="$SCRIPT_DIR"
35
+ if [ -f "$SCRIPT_DIR/.git" ]; then
36
+ _WT_GITDIR=$(sed 's/^gitdir: //' "$SCRIPT_DIR/.git" 2>/dev/null || true)
37
+ [ -n "$_WT_GITDIR" ] && MAIN_STATE_DIR=$(cd "$_WT_GITDIR/../../.." 2>/dev/null && pwd || echo "$SCRIPT_DIR")
38
+ fi
39
+ MAIN_STATE_FILE="$MAIN_STATE_DIR/.egregore-state.json"
40
+ [ -f "$MAIN_STATE_FILE" ] || MAIN_STATE_FILE="$STATE_FILE"
41
+
31
42
  [ -f "$STATE_FILE" ] || echo '{}' > "$STATE_FILE"
32
43
 
33
44
  # NB: jq's `//` swallows false — status would show "on" for enabled:false.
@@ -42,6 +53,11 @@ set_key() { # key value(json-encoded)
42
53
  jq ".$1 = $2" "$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
43
54
  }
44
55
 
56
+ _set_main() { # key value(json-encoded) — writes where the attendant reads
57
+ jq ".$1 = $2" "$MAIN_STATE_FILE" > "$MAIN_STATE_FILE.tmp" \
58
+ && mv "$MAIN_STATE_FILE.tmp" "$MAIN_STATE_FILE"
59
+ }
60
+
45
61
  status() {
46
62
  local enabled scope publish
47
63
  enabled=$(get '.autosave_enabled' 'false')
@@ -50,6 +66,7 @@ status() {
50
66
  echo "autosave: $([ "$enabled" = "true" ] && echo on || echo "off (opt-in — 'autosave on' to join the experiment)")"
51
67
  echo "scope: $scope (all = handoffs + artifacts + docs · handoffs = memory repo only)"
52
68
  echo "publish: $publish (gate = PRs wait for your merge · auto = merge to develop automatically)"
69
+ echo "ripcord: $(jq -r 'if (.ripcord_enabled != null) then (.ripcord_enabled|tostring) else "true" end' "$MAIN_STATE_FILE" 2>/dev/null || echo true) (crash-recovery notes + branch auto-push when a session dies without /wrap)"
53
70
  }
54
71
 
55
72
  case "${1:-status}" in
@@ -61,6 +78,15 @@ case "${1:-status}" in
61
78
  all|handoffs) set_key autosave_scope "\"$2\"" && echo "scope: $2" ;;
62
79
  *) echo "usage: autosave.sh scope all|handoffs" >&2; exit 2 ;;
63
80
  esac ;;
81
+ ripcord)
82
+ # Ambient crash-recovery capture (the attendant): ripcord notes AND
83
+ # auto-push of dirty task branches. Separate from the autosave
84
+ # experiment above — this one defaults ON.
85
+ case "${2:-}" in
86
+ on) _set_main ripcord_enabled true && echo "ripcord: on — dead sessions leave a recovery note" ;;
87
+ off) _set_main ripcord_enabled false && echo "ripcord: off — no ambient notes, no branch auto-push" ;;
88
+ *) echo "usage: autosave.sh ripcord on|off" >&2; exit 2 ;;
89
+ esac ;;
64
90
  publish)
65
91
  case "${2:-}" in
66
92
  gate|auto) set_key autosave_publish "\"$2\"" && echo "publish: $2" ;;
@@ -82,5 +108,5 @@ case "${1:-status}" in
82
108
  echo "✗ PR #$n did not merge (conflict with develop?) — it stays open"
83
109
  fi
84
110
  done ;;
85
- *) echo "usage: autosave.sh [status|on|off|scope all|handoffs|publish gate|auto|merge]" >&2; exit 2 ;;
111
+ *) echo "usage: autosave.sh [status|on|off|scope all|handoffs|publish gate|auto|merge|ripcord on|off]" >&2; exit 2 ;;
86
112
  esac