create-egregore 0.19.33 → 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 (171) hide show
  1. package/assets/egregore-launcher.js +118 -13
  2. package/bin/cli.js +19 -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 +4 -3
  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 +3 -1
  18. package/runtime/codex/bin/graph-batch.sh +25 -6
  19. package/runtime/codex/bin/graph-op.sh +5 -4
  20. package/runtime/codex/bin/graph.sh +31 -5
  21. package/runtime/codex/bin/ingest.py +81 -5
  22. package/runtime/codex/bin/lib/context.sh +26 -1
  23. package/runtime/codex/bin/lib/greeting.sh +4 -1
  24. package/runtime/codex/bin/lib/worktree-links.sh +25 -0
  25. package/runtime/codex/manifest.json +7 -1
  26. package/runtime/pi/.codex/skills/ingest-notion/SKILL.md +29 -0
  27. package/runtime/pi/.codex/skills/notion-connect/SKILL.md +29 -0
  28. package/runtime/pi/.codex/skills/teams-connect/SKILL.md +29 -0
  29. package/runtime/pi/.codex/spec-manifest.json +3 -3
  30. package/runtime/pi/.pi/APPEND_SYSTEM.md +3 -2
  31. package/runtime/pi/.pi/extensions/egregore.ts +17 -6
  32. package/runtime/pi/.pi/spec-manifest.json +2 -2
  33. package/runtime/pi/AGENTS.md +11 -4
  34. package/runtime/pi/bin/activity-data.sh +4 -8
  35. package/runtime/pi/bin/attendant.sh +13 -0
  36. package/runtime/pi/bin/autosave.sh +27 -1
  37. package/runtime/pi/bin/codex-render-spec.mjs +6 -0
  38. package/runtime/pi/bin/codex-session-start.sh +3 -1
  39. package/runtime/pi/bin/graph-batch.sh +25 -6
  40. package/runtime/pi/bin/graph-op.sh +5 -4
  41. package/runtime/pi/bin/graph.sh +31 -5
  42. package/runtime/pi/bin/ingest.py +81 -5
  43. package/runtime/pi/bin/lib/context.sh +26 -1
  44. package/runtime/pi/bin/lib/greeting.sh +4 -1
  45. package/runtime/pi/bin/lib/worktree-links.sh +25 -0
  46. package/runtime/pi/bin/pi-branch-policy.mjs +138 -1
  47. package/runtime/pi/bin/pi-product-workflows.mjs +35 -7
  48. package/runtime/pi/manifest.json +7 -1
  49. package/runtime/prime/.claude/context/notification-consent.md +66 -0
  50. package/runtime/prime/.codex/config.toml +17 -0
  51. package/runtime/prime/.codex/hooks/branch-guard.js +461 -0
  52. package/runtime/prime/.codex/hooks/search-hint.js +67 -0
  53. package/runtime/prime/.codex/hooks.json +30 -0
  54. package/runtime/prime/.codex/skills/activity/SKILL.md +82 -0
  55. package/runtime/prime/.codex/skills/announce/SKILL.md +67 -0
  56. package/runtime/prime/.codex/skills/archive/SKILL.md +45 -0
  57. package/runtime/prime/.codex/skills/branch/SKILL.md +29 -0
  58. package/runtime/prime/.codex/skills/checkup/SKILL.md +45 -0
  59. package/runtime/prime/.codex/skills/commit/SKILL.md +29 -0
  60. package/runtime/prime/.codex/skills/contribute/SKILL.md +29 -0
  61. package/runtime/prime/.codex/skills/create-skill/SKILL.md +29 -0
  62. package/runtime/prime/.codex/skills/dark-mode/SKILL.md +29 -0
  63. package/runtime/prime/.codex/skills/dashboard/SKILL.md +59 -0
  64. package/runtime/prime/.codex/skills/deep-reflect/SKILL.md +109 -0
  65. package/runtime/prime/.codex/skills/delete-user/SKILL.md +29 -0
  66. package/runtime/prime/.codex/skills/emissary/SKILL.md +45 -0
  67. package/runtime/prime/.codex/skills/handoff/SKILL.md +166 -0
  68. package/runtime/prime/.codex/skills/harvest/QUESTION_PALETTE.md +76 -0
  69. package/runtime/prime/.codex/skills/harvest/SKILL.md +131 -0
  70. package/runtime/prime/.codex/skills/ingest/SKILL.md +29 -0
  71. package/runtime/prime/.codex/skills/ingest-notion/SKILL.md +29 -0
  72. package/runtime/prime/.codex/skills/ingest-user-interview/SKILL.md +45 -0
  73. package/runtime/prime/.codex/skills/invite/SKILL.md +44 -0
  74. package/runtime/prime/.codex/skills/issue/SKILL.md +45 -0
  75. package/runtime/prime/.codex/skills/me/SKILL.md +29 -0
  76. package/runtime/prime/.codex/skills/meeting/SKILL.md +45 -0
  77. package/runtime/prime/.codex/skills/note/SKILL.md +29 -0
  78. package/runtime/prime/.codex/skills/notion-connect/SKILL.md +29 -0
  79. package/runtime/prime/.codex/skills/onboarding/SKILL.md +45 -0
  80. package/runtime/prime/.codex/skills/pr/SKILL.md +29 -0
  81. package/runtime/prime/.codex/skills/pull/SKILL.md +29 -0
  82. package/runtime/prime/.codex/skills/push/SKILL.md +29 -0
  83. package/runtime/prime/.codex/skills/quest/SKILL.md +80 -0
  84. package/runtime/prime/.codex/skills/reflect/SKILL.md +45 -0
  85. package/runtime/prime/.codex/skills/review-pr/SKILL.md +45 -0
  86. package/runtime/prime/.codex/skills/save/SKILL.md +125 -0
  87. package/runtime/prime/.codex/skills/slack-connect/SKILL.md +29 -0
  88. package/runtime/prime/.codex/skills/summon/SKILL.md +45 -0
  89. package/runtime/prime/.codex/skills/sync-repos/SKILL.md +29 -0
  90. package/runtime/prime/.codex/skills/teams-connect/SKILL.md +29 -0
  91. package/runtime/prime/.codex/skills/telegram-connect/SKILL.md +29 -0
  92. package/runtime/prime/.codex/skills/telemetry/SKILL.md +29 -0
  93. package/runtime/prime/.codex/skills/test/SKILL.md +45 -0
  94. package/runtime/prime/.codex/skills/the-spiral/SKILL.md +84 -0
  95. package/runtime/prime/.codex/skills/tui-design/SKILL.md +45 -0
  96. package/runtime/prime/.codex/skills/tutorial/SKILL.md +45 -0
  97. package/runtime/prime/.codex/skills/update/SKILL.md +29 -0
  98. package/runtime/prime/.codex/skills/view/SKILL.md +156 -0
  99. package/runtime/prime/.codex/skills/view/agents/openai.yaml +4 -0
  100. package/runtime/prime/.codex/skills/view/assets/compose-scaffold.html +221 -0
  101. package/runtime/prime/.codex/skills/visual-explain/SKILL.md +45 -0
  102. package/runtime/prime/.codex/skills/wrap/SKILL.md +58 -0
  103. package/runtime/prime/.codex/spec-manifest.json +132 -0
  104. package/runtime/prime/.prime/agent/APPEND_SYSTEM.md +393 -0
  105. package/runtime/prime/.prime/agent/extensions/egregore.ts +460 -0
  106. package/runtime/prime/.prime/agent/settings.json +4 -0
  107. package/runtime/prime/.prime/agent/skills/egregore/SKILL.md +34 -0
  108. package/runtime/prime/.prime/agent/skills/egregore/pyproject.toml +13 -0
  109. package/runtime/prime/.prime/agent/skills/egregore/src/egregore/__init__.py +156 -0
  110. package/runtime/prime/.prime/agent/spec-manifest.json +12 -0
  111. package/runtime/prime/AGENTS.md +489 -0
  112. package/runtime/prime/bin/activity-data.sh +238 -0
  113. package/runtime/prime/bin/agent.sh +775 -0
  114. package/runtime/prime/bin/attendant.sh +707 -0
  115. package/runtime/prime/bin/autosave.sh +112 -0
  116. package/runtime/prime/bin/capture-reconcile.sh +69 -0
  117. package/runtime/prime/bin/capture-run.sh +341 -0
  118. package/runtime/prime/bin/codex-install-skills.sh +78 -0
  119. package/runtime/prime/bin/codex-render-spec.mjs +440 -0
  120. package/runtime/prime/bin/codex-session-start.sh +265 -0
  121. package/runtime/prime/bin/codex-skill-render.mjs +347 -0
  122. package/runtime/prime/bin/codex-sync-skills.sh +232 -0
  123. package/runtime/prime/bin/connect-refresh.sh +33 -0
  124. package/runtime/prime/bin/dashboard-data.sh +178 -0
  125. package/runtime/prime/bin/graph-batch.sh +79 -0
  126. package/runtime/prime/bin/graph-op.sh +1102 -0
  127. package/runtime/prime/bin/graph-wal.sh +229 -0
  128. package/runtime/prime/bin/graph.sh +223 -0
  129. package/runtime/prime/bin/handoff-pr-backfill.sh +86 -0
  130. package/runtime/prime/bin/handoff-run.sh +671 -0
  131. package/runtime/prime/bin/handoff-save-egregore.sh +212 -0
  132. package/runtime/prime/bin/index-handoff.sh +322 -0
  133. package/runtime/prime/bin/ingest-graph.sh +80 -0
  134. package/runtime/prime/bin/ingest-surface.html +495 -0
  135. package/runtime/prime/bin/ingest.py +756 -0
  136. package/runtime/prime/bin/ingest.sh +279 -0
  137. package/runtime/prime/bin/ingest_graph.py +856 -0
  138. package/runtime/prime/bin/ingest_surface.py +544 -0
  139. package/runtime/prime/bin/lib/artifact-id.sh +46 -0
  140. package/runtime/prime/bin/lib/config.sh +116 -0
  141. package/runtime/prime/bin/lib/context.sh +621 -0
  142. package/runtime/prime/bin/lib/dashboard-artifact.sh +242 -0
  143. package/runtime/prime/bin/lib/ensure-develop.sh +183 -0
  144. package/runtime/prime/bin/lib/git-sync.sh +406 -0
  145. package/runtime/prime/bin/lib/greeting.sh +519 -0
  146. package/runtime/prime/bin/lib/hash.sh +13 -0
  147. package/runtime/prime/bin/lib/identity.sh +213 -0
  148. package/runtime/prime/bin/lib/metrics.sh +374 -0
  149. package/runtime/prime/bin/lib/noncode.sh +31 -0
  150. package/runtime/prime/bin/lib/permissions.sh +106 -0
  151. package/runtime/prime/bin/lib/time.sh +56 -0
  152. package/runtime/prime/bin/lib/worktree-links.sh +79 -0
  153. package/runtime/prime/bin/notify.sh +636 -0
  154. package/runtime/prime/bin/person.py +630 -0
  155. package/runtime/prime/bin/person.sh +448 -0
  156. package/runtime/prime/bin/pi-branch-policy.mjs +312 -0
  157. package/runtime/prime/bin/pi-product-workflows.mjs +394 -0
  158. package/runtime/prime/bin/prime-render-spec.mjs +123 -0
  159. package/runtime/prime/bin/prime-session-start.sh +24 -0
  160. package/runtime/prime/bin/publish-artifact.sh +271 -0
  161. package/runtime/prime/bin/publish-references.sh +144 -0
  162. package/runtime/prime/bin/repo-state.sh +96 -0
  163. package/runtime/prime/bin/search.sh +240 -0
  164. package/runtime/prime/bin/session-autosave.sh +209 -0
  165. package/runtime/prime/bin/session-log.sh +199 -0
  166. package/runtime/prime/bin/settings.sh +304 -0
  167. package/runtime/prime/bin/telemetry.sh +391 -0
  168. package/runtime/prime/bin/worktree-create.sh +128 -0
  169. package/runtime/prime/bin/worktree-remove.sh +50 -0
  170. package/runtime/prime/bin/worktree.sh +198 -0
  171. package/runtime/prime/manifest.json +178 -0
@@ -0,0 +1,265 @@
1
+ #!/usr/bin/env bash
2
+ set -o pipefail
3
+
4
+ # Codex does not run Claude Code SessionStart hooks. This is the runtime-neutral
5
+ # startup card: it reuses the same identity, git sync, and context-gathering
6
+ # libraries as Claude's SessionStart, then renders a terminal greeting before
7
+ # Codex itself starts.
8
+
9
+ FRAMEWORK_VERSION="7"
10
+ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
11
+ cd "$SCRIPT_DIR" || exit 1
12
+
13
+ IS_WORKTREE="false"
14
+ MAIN_PROJECT_DIR="$SCRIPT_DIR"
15
+ if [ -f "$SCRIPT_DIR/.git" ]; then
16
+ IS_WORKTREE="true"
17
+ WT_GITDIR=$(sed 's/^gitdir: //' "$SCRIPT_DIR/.git" 2>/dev/null)
18
+ MAIN_PROJECT_DIR=$(cd "$WT_GITDIR/../../.." 2>/dev/null && pwd)
19
+ fi
20
+
21
+ # Clear any branch-guard consent from a previous session — consent to write on
22
+ # a protected branch is asked fresh each session (see AGENTS.md branch-guard protocol).
23
+ rm -f "$SCRIPT_DIR/.egregore-branch-consent" "$MAIN_PROJECT_DIR/.egregore-branch-consent" 2>/dev/null
24
+ # Same for boundary-crossing consent — grants are session-scoped (AGENTS.md
25
+ # environment-isolation protocol); durable grants belong in .egregore-boundary.local.json.
26
+ rm -f "$SCRIPT_DIR/.egregore-boundary-consent" "$MAIN_PROJECT_DIR/.egregore-boundary-consent" 2>/dev/null
27
+
28
+ if [ -f "$SCRIPT_DIR/bin/lib/worktree-links.sh" ]; then
29
+ source "$SCRIPT_DIR/bin/lib/worktree-links.sh" >/dev/null 2>/dev/null || true
30
+ egregore_link_shared_state "$SCRIPT_DIR" "$MAIN_PROJECT_DIR" >/dev/null 2>/dev/null || true
31
+ fi
32
+
33
+ HEALTH_GITHUB="skip"
34
+ HEALTH_GIT="skip"
35
+ HEALTH_APIKEY="skip"
36
+ HEALTH_GRAPH="skip"
37
+ HEALTH_TELEGRAM="skip"
38
+
39
+ STATE_FILE="$SCRIPT_DIR/.egregore-state.json"
40
+ ENV_FILE="$SCRIPT_DIR/.env"
41
+ CONFIG="$SCRIPT_DIR/egregore.json"
42
+ DATE=$(date +%Y-%m-%d)
43
+
44
+ source "$SCRIPT_DIR/bin/lib/config.sh"
45
+ source "$SCRIPT_DIR/bin/lib/hash.sh"
46
+ source "$SCRIPT_DIR/bin/lib/time.sh"
47
+ source "$SCRIPT_DIR/bin/lib/identity.sh" >/dev/null 2>/dev/null
48
+
49
+ # Keep the durable local identity current before gathering context, then project
50
+ # it to Supabase + Neo4j in the background so startup is never network-blocked.
51
+ if [ -f "$STATE_FILE" ] && [ -f "$SCRIPT_DIR/bin/person.py" ]; then
52
+ python3 "$SCRIPT_DIR/bin/person.py" sync-local >/dev/null 2>&1 || true
53
+ fi
54
+ if [ -f "$SCRIPT_DIR/bin/person.sh" ]; then
55
+ ( bash "$SCRIPT_DIR/bin/person.sh" sync >/dev/null 2>&1 & ) 2>/dev/null
56
+ fi
57
+
58
+ LOCAL_MODE="false"
59
+ [ "$(_detect_mode)" = "local" ] && LOCAL_MODE="true"
60
+ MODE_BADGE="$(_runtime_mode_badge)"
61
+
62
+ if [ "$LOCAL_MODE" != "true" ]; then
63
+ if [ -f "$ENV_FILE" ] && grep -q '^EGREGORE_API_KEY=.' "$ENV_FILE" 2>/dev/null; then
64
+ HEALTH_APIKEY="ok"
65
+ else
66
+ HEALTH_APIKEY="fail"
67
+ fi
68
+ fi
69
+
70
+ source "$SCRIPT_DIR/bin/lib/git-sync.sh" >/dev/null 2>/dev/null
71
+ # Warm-state parity with the Claude SessionStart path: hand context.sh the
72
+ # pre-baked snapshot and route greeting reads through the warm graph cache.
73
+ # The attendant daemon is deliberately NOT started from the Codex/Pi path —
74
+ # its dead-session detection reads Claude transcripts only, so a live Codex
75
+ # or Pi session with 12 quiet minutes would look dead and could have its
76
+ # unfinished work ripcorded/auto-pushed. Codex sessions still benefit from
77
+ # any snapshot a Claude session's attendant maintains; runtime-neutral
78
+ # liveness tracking is the prerequisite for starting the daemon here.
79
+ if [ -n "${_ATT_KEY:-}" ]; then
80
+ export CTX_SEED_TAR="${ATTENDANT_HOME:-$HOME/.egregore/attendant}/${_ATT_KEY}.ctx.tar"
81
+ fi
82
+ export EGREGORE_GRAPH_CACHE_TTL=600
83
+ source "$SCRIPT_DIR/bin/lib/context.sh" >/dev/null 2>/dev/null
84
+ unset EGREGORE_GRAPH_CACHE_TTL CTX_SEED_TAR
85
+ source "$SCRIPT_DIR/bin/lib/metrics.sh" >/dev/null 2>/dev/null
86
+ trap 'rm -rf "${CTX_DIR:-}"' EXIT
87
+
88
+ # Rescue non-coding leftovers from sessions that died without ceremony
89
+ # (terminal closed). Gated + idle-guarded inside the script; fire-and-forget.
90
+ ( bash "$SCRIPT_DIR/bin/session-autosave.sh" --sweep >/dev/null 2>&1 & ) 2>/dev/null
91
+
92
+ # Connected instances pick up newly released Connect skills without
93
+ # re-running the launcher. Local mode exits instantly inside the script.
94
+ # Pi shares this path (pi-session-start.sh delegates here).
95
+ ( bash "$SCRIPT_DIR/bin/connect-refresh.sh" >/dev/null 2>&1 & ) 2>/dev/null
96
+
97
+ read_json() {
98
+ local file="$1"
99
+ local expr="$2"
100
+ local fallback="${3:-}"
101
+ if [ -f "$file" ] && command -v jq >/dev/null 2>&1; then
102
+ jq -r "$expr // empty" "$file" 2>/dev/null | head -1
103
+ else
104
+ printf '%s\n' "$fallback"
105
+ fi
106
+ }
107
+
108
+ ORG_NAME="$(read_json "$CONFIG" '.org_name' '')"
109
+ [ -z "$ORG_NAME" ] && ORG_NAME="$(read_json "$CONFIG" '.slug' 'Egregore')"
110
+ GITHUB_ORG="$(read_json "$CONFIG" '.github_org' '')"
111
+ REPO_NAME="$(read_json "$CONFIG" '.repo_name' 'egregore')"
112
+ SLUG="$(read_json "$CONFIG" '.slug' '')"
113
+ DISPLAY_NAME="$(read_json "$STATE_FILE" '.display_name' '')"
114
+ [ -z "$DISPLAY_NAME" ] && DISPLAY_NAME="${DISPLAY_NAME_STATE:-$AUTHOR}"
115
+ ONBOARDING_COMPLETE="$(read_json "$STATE_FILE" '.onboarding_complete' '')"
116
+ [ -z "$ONBOARDING_COMPLETE" ] && ONBOARDING_COMPLETE="unknown"
117
+
118
+ ADDRESSED_RICH="$(cat "$CTX_DIR/addressed_rich" 2>/dev/null || echo "[]")"
119
+ PENDING_Q="$(cat "$CTX_DIR/pending_questions" 2>/dev/null || echo "[]")"
120
+ ADDRESSED_COUNT="$(echo "$ADDRESSED_RICH" | jq 'length' 2>/dev/null || echo "0")"
121
+ PENDING_Q_COUNT="$(echo "$PENDING_Q" | jq 'length' 2>/dev/null || echo "0")"
122
+
123
+ render_pending_line() {
124
+ if [ "$PENDING_Q_COUNT" -gt 0 ] 2>/dev/null; then
125
+ local senders noun
126
+ senders="$(echo "$PENDING_Q" | jq -r '[.[].from] | unique | join(", ")' 2>/dev/null)"
127
+ [ "$PENDING_Q_COUNT" = "1" ] && noun="question" || noun="questions"
128
+ if [ -n "$senders" ]; then
129
+ printf ' ◐ %s pending %s from %s - bin/agent.sh answer\n' "$PENDING_Q_COUNT" "$noun" "$senders"
130
+ else
131
+ printf ' ◐ %s pending %s - bin/agent.sh answer\n' "$PENDING_Q_COUNT" "$noun"
132
+ fi
133
+ fi
134
+ if [ "$ADDRESSED_COUNT" -gt 0 ] 2>/dev/null; then
135
+ printf ' ◇ %s handoffs for %s - say "show my handoffs" to review or close\n' "$ADDRESSED_COUNT" "$AUTHOR"
136
+ fi
137
+ }
138
+
139
+ banner() {
140
+ cat <<'EOF'
141
+
142
+ ███████╗ ██████╗ ██████╗ ███████╗ ██████╗ ██████╗ ██████╗ ███████╗
143
+ ██╔════╝██╔════╝ ██╔══██╗██╔════╝██╔════╝ ██╔═══██╗██╔══██╗██╔════╝
144
+ █████╗ ██║ ███╗██████╔╝█████╗ ██║ ███╗██║ ██║██████╔╝█████╗
145
+ ██╔══╝ ██║ ██║██╔══██╗██╔══╝ ██║ ██║██║ ██║██╔══██╗██╔══╝
146
+ ███████╗╚██████╔╝██║ ██║███████╗╚██████╔╝╚██████╔╝██║ ██║███████╗
147
+ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚══════╝
148
+
149
+ EOF
150
+ }
151
+
152
+ render_card() {
153
+ local separator identity_left identity_right line_width pad footer_left footer_right board_url has_failure failed_services
154
+ separator=" ..................................................................."
155
+ identity_left=" ${GITHUB_ORG:-$ORG_NAME}/${REPO_NAME} · ${MODE_BADGE}"
156
+ identity_right="${DISPLAY_NAME} · ${BRANCH:-?}"
157
+ line_width=67
158
+ pad=$((line_width - ${#identity_left} - ${#identity_right}))
159
+ [ "$pad" -lt 1 ] && pad=1
160
+
161
+ banner
162
+ printf '%s%*s%s\n' "$identity_left" "$pad" "" "$identity_right"
163
+ printf '%s\n' "$separator"
164
+ SEPARATOR="$separator" LINE_WIDTH=$line_width _render_momentum_board
165
+ render_pending_line
166
+ printf '%s\n' "$separator"
167
+
168
+ has_failure="false"
169
+ failed_services=""
170
+ for pair in "github:$HEALTH_GITHUB" "git:$HEALTH_GIT" "api-key:$HEALTH_APIKEY" "graph:$HEALTH_GRAPH" "telegram:$HEALTH_TELEGRAM"; do
171
+ svc="${pair%%:*}"
172
+ svc_status="${pair#*:}"
173
+ if [ "$svc_status" = "fail" ]; then
174
+ has_failure="true"
175
+ failed_services="${failed_services} ${svc} ✗"
176
+ fi
177
+ done
178
+
179
+ if [ "$has_failure" = "true" ]; then
180
+ printf ' ⚠%s\n' "$failed_services"
181
+ else
182
+ footer_left=" ✓ ready"
183
+ if [ "${MEMORY_SYNCED:-false}" = "true" ]; then
184
+ footer_right="◆ memory synced"
185
+ else
186
+ footer_right="◆ memory not synced"
187
+ fi
188
+ pad=$((line_width - ${#footer_left} - ${#footer_right}))
189
+ [ "$pad" -lt 1 ] && pad=1
190
+ printf '%s%*s%s\n' "$footer_left" "$pad" "" "$footer_right"
191
+ fi
192
+
193
+ # Autosave trail — background saves since the last greeting (same ledger
194
+ # the Claude greeting reads; sweep rescues become visible here).
195
+ autosave_log="${EGREGORE_AUTOSAVE_LOG:-$HOME/.egregore/autosave.log}"
196
+ autosave_seen="${autosave_log}.seen"
197
+ if [ -f "$autosave_log" ]; then
198
+ total_as=$(grep -c '|autosave|' "$autosave_log" 2>/dev/null || echo 0)
199
+ seen_as=$(cat "$autosave_seen" 2>/dev/null || echo 0)
200
+ case "$seen_as" in ''|*[!0-9]*) seen_as=0 ;; esac
201
+ if [ "$total_as" -gt "$seen_as" ]; then
202
+ new_as=$((total_as - seen_as))
203
+ merged_as=$(grep '|autosave|' "$autosave_log" 2>/dev/null | tail -n "$new_as" | grep -c '|merged|' || echo 0)
204
+ if [ "$merged_as" -gt 0 ]; then
205
+ printf ' ⟲ auto-saved %s non-coding save(s) → develop while you were away\n' "$merged_as"
206
+ fi
207
+ fi
208
+ printf '%s' "$total_as" > "$autosave_seen" 2>/dev/null || true
209
+ fi
210
+
211
+ # Staged auto-saves awaiting the user's merge (the autosave consent gate).
212
+ # Live gh query so the line persists until the PRs actually merge.
213
+ # Shown only while the autosave experiment is enabled.
214
+ autosave_on=$(jq -r '.autosave_enabled // false' "$STATE_FILE" 2>/dev/null)
215
+ if [ "$autosave_on" = "true" ] && command -v gh >/dev/null 2>&1; then
216
+ gh_self=$(jq -r '.github_username // empty' "$STATE_FILE" 2>/dev/null)
217
+ if [ -n "$gh_self" ]; then
218
+ waiting_as=$(gh pr list --state open --author "$gh_self" --limit 20 \
219
+ --json number,title --jq '[.[] | select(.title | startswith("Auto-save:")) | .number] | join(", #")' 2>/dev/null || true)
220
+ if [ -n "$waiting_as" ]; then
221
+ printf ' ⟲ auto-saves awaiting your merge: PR #%s — \$autosave merge (or bin/agent.sh autosave merge)\n' "$waiting_as"
222
+ fi
223
+ fi
224
+ fi
225
+
226
+ # Greeting links. Org config `pinned_links` in egregore.json replaces the
227
+ # default board link — same contract as bin/lib/greeting.sh.
228
+ pinned_links=$(jq -c '.pinned_links // []' "$CONFIG" 2>/dev/null || echo "[]")
229
+ if [ "$(printf '%s' "$pinned_links" | jq 'length' 2>/dev/null || echo 0)" -gt 0 ]; then
230
+ printf '%s' "$pinned_links" | jq -r '.[] |
231
+ if type == "string" then " ◆ \(.)"
232
+ elif (.label // "") != "" then " ◆ \(.label): \(.url)"
233
+ else " ◆ \(.url)" end' 2>/dev/null
234
+ elif [ -n "$SLUG" ]; then
235
+ board_url="https://egregore.xyz/view/${SLUG}/board"
236
+ printf ' ◆ %s\n' "$board_url"
237
+ fi
238
+ printf '\nWhat are you working on?\n'
239
+ }
240
+
241
+ case "${1:---card}" in
242
+ --card|card)
243
+ render_card
244
+ ;;
245
+ --prompt|prompt)
246
+ cat <<EOF
247
+ You are Codex running inside Egregore for ${ORG_NAME} as ${AUTHOR}.
248
+ The launcher already rendered the Egregore startup card using the shared identity, git-sync, graph, and filesystem context pipeline.
249
+ This session is Trusted Codex: full shell/network access is enabled and approval prompts are disabled by the launcher.
250
+ Do not rerun startup checks or narrate startup unless the user asks.
251
+ Follow AGENTS.md before code changes.
252
+ After the user names what they are working on, use bin/agent.sh branch --topic "<work topic>" if you are not already in an appropriate task worktree, then continue file work from the printed worktree path.
253
+ Use bin/agent.sh to update shared activity: handoff, ask, and answer write to memory/ and push when possible.
254
+ Use bin/agent.sh handoff for internal team session-handoffs. Portable external capsules are emissaries; use egregore-emissary when installed, not the deprecated egregore-handoff CLI.
255
+ Codex reserves leading slash commands for built-ins, so custom Egregore /activity-style commands will not reach the agent. Egregore native Codex skills are: \$activity, \$handoff, \$wrap, \$announce, \$harvest, \$the-spiral, \$dashboard, \$deep-reflect, \$quest, \$invite, \$ask, \$save, \$view, and \$scroll. Natural-language requests such as "show activity", "make a handoff", or "render this as a scroll" work too. Remaining mirrored workflows are adapter skills for long-tail coverage.
256
+ \$save is the user-facing abstraction for committing, pushing, opening or reusing pull requests, and syncing memory; do not make users manage the git workflow by hand.
257
+ Egregore Codex workflows are skill invocations, not Claude Code slash commands. Do not rely on Claude Code-only command machinery.
258
+ If onboarding_complete is false, use the memory protocol directly and make sure memory/people/${AUTHOR}.md exists before the first handoff.
259
+ EOF
260
+ ;;
261
+ *)
262
+ echo "usage: bin/codex-session-start.sh [--card|--prompt]" >&2
263
+ exit 2
264
+ ;;
265
+ esac
@@ -0,0 +1,347 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+
4
+ const WIDTH = 72;
5
+
6
+ function usage() {
7
+ console.error(`usage:
8
+ node bin/codex-skill-render.mjs classify-graph [--mode connected|local] [--attempt initial|retry] [file|-]
9
+ node bin/codex-skill-render.mjs activity-card [file|-]
10
+ node bin/codex-skill-render.mjs dashboard-card [file|-]
11
+ node bin/codex-skill-render.mjs handoff-card [file|-]`);
12
+ process.exit(2);
13
+ }
14
+
15
+ function readInput(file) {
16
+ if (!file || file === '-') {
17
+ return fs.readFileSync(0, 'utf8');
18
+ }
19
+ return fs.readFileSync(file, 'utf8');
20
+ }
21
+
22
+ function readJson(file) {
23
+ const raw = readInput(file).trim() || '{}';
24
+ try {
25
+ return JSON.parse(raw);
26
+ } catch (error) {
27
+ console.error(`invalid JSON${file ? ` in ${file}` : ''}: ${error.message}`);
28
+ process.exit(1);
29
+ }
30
+ }
31
+
32
+ function parseFlags(args) {
33
+ const flags = {};
34
+ const rest = [];
35
+ for (let i = 0; i < args.length; i += 1) {
36
+ const arg = args[i];
37
+ if (arg === '--mode' || arg === '--attempt') {
38
+ flags[arg.slice(2)] = args[i + 1];
39
+ i += 1;
40
+ } else {
41
+ rest.push(arg);
42
+ }
43
+ }
44
+ return { flags, rest };
45
+ }
46
+
47
+ function asArray(value) {
48
+ return Array.isArray(value) ? value : [];
49
+ }
50
+
51
+ function firstArray(data, paths) {
52
+ for (const p of paths) {
53
+ const value = p.split('.').reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : undefined), data);
54
+ if (Array.isArray(value)) return value;
55
+ }
56
+ return [];
57
+ }
58
+
59
+ function valueOf(item, keys, fallback = '') {
60
+ for (const key of keys) {
61
+ if (item && item[key] !== undefined && item[key] !== null && String(item[key]).trim() !== '') {
62
+ return String(item[key]);
63
+ }
64
+ }
65
+ return fallback;
66
+ }
67
+
68
+ function truncate(value, max) {
69
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
70
+ if (text.length <= max) return text;
71
+ return `${text.slice(0, Math.max(0, max - 3))}...`;
72
+ }
73
+
74
+ function padLine(left, right = '') {
75
+ const inner = WIDTH - 4;
76
+ const r = right ? truncate(right, Math.min(18, inner - 1)) : '';
77
+ const l = truncate(left, inner - (r ? r.length + 1 : 0));
78
+ const space = Math.max(0, inner - l.length - r.length);
79
+ return `│ ${l}${' '.repeat(space)}${r} │`;
80
+ }
81
+
82
+ function topRule() {
83
+ return `┌${'─'.repeat(WIDTH - 2)}┐`;
84
+ }
85
+
86
+ function rule() {
87
+ return `├${'─'.repeat(WIDTH - 2)}┤`;
88
+ }
89
+
90
+ function bottomRule() {
91
+ return `└${'─'.repeat(WIDTH - 2)}┘`;
92
+ }
93
+
94
+ function graphFields(data) {
95
+ return {
96
+ status: data.graph_status || data.graphStatus || data.graph?.status || '',
97
+ reason: data.graph_reason || data.graphReason || data.graph?.reason || '',
98
+ };
99
+ }
100
+
101
+ function classifyGraph(data, options = {}) {
102
+ const mode = options.mode || data.mode || 'connected';
103
+ const attempt = options.attempt || 'initial';
104
+ const { status, reason } = graphFields(data);
105
+
106
+ if (mode === 'local') {
107
+ return {
108
+ status: 'local',
109
+ retry: false,
110
+ reason: reason || 'local_mode',
111
+ message: 'Local mode uses memory files as the source of truth.',
112
+ };
113
+ }
114
+
115
+ if (status === 'connected') {
116
+ return {
117
+ status: 'connected',
118
+ retry: false,
119
+ reason: '',
120
+ message: 'Graph connected.',
121
+ };
122
+ }
123
+
124
+ if (reason === 'unreachable' && attempt !== 'retry') {
125
+ return {
126
+ status: 'retry',
127
+ retry: true,
128
+ reason,
129
+ message: 'Connected-mode graph was unreachable; retry with network escalation.',
130
+ };
131
+ }
132
+
133
+ const finalReason = reason || status || 'offline';
134
+ return {
135
+ status: 'offline',
136
+ retry: false,
137
+ reason: finalReason,
138
+ message: `Graph offline (${finalReason}). Render filesystem fallback.`,
139
+ };
140
+ }
141
+
142
+ function renderActivity(data) {
143
+ const graph = classifyGraph(data, { mode: data.mode || 'connected', attempt: 'retry' });
144
+ const org = data.org || data.organization || 'Egregore';
145
+ const date = data.date || '';
146
+ const me = typeof data.me === 'string'
147
+ ? data.me
148
+ : data.me?.github || data.me?.name || data.github_username || '';
149
+ const handoffs = firstArray(data, ['handoffs_to_me', 'handoffs', 'pending_handoffs', 'me.handoffs', 'addressed_handoffs']);
150
+ const questions = firstArray(data, ['questions', 'pending_questions', 'me.questions']);
151
+ const prs = asArray(data.prs);
152
+ const mine = firstArray(data, ['local_sessions.my_sessions', 'my_sessions', 'sessions']);
153
+ const team = firstArray(data, ['local_sessions.team_sessions', 'team_sessions', 'team.sessions']);
154
+
155
+ const brand = `${truncate(org, 20).toUpperCase()} EGREGORE ✦ ACTIVITY DASHBOARD`;
156
+ const lines = [topRule(), padLine(brand), padLine([me, date].filter(Boolean).join(' · ')), rule()];
157
+ if (graph.status !== 'connected') {
158
+ lines.push(padLine(`graph: ${graph.status}`, graph.reason));
159
+ lines.push(rule());
160
+ }
161
+
162
+ lines.push(padLine('FOR YOU'));
163
+ if (questions.length === 0 && handoffs.length === 0) {
164
+ lines.push(padLine('nothing pending'));
165
+ } else {
166
+ if (questions.length > 0) {
167
+ lines.push(padLine('QUESTIONS'));
168
+ questions.slice(0, 4).forEach((q) => {
169
+ lines.push(padLine(`● ${valueOf(q, ['from', 'author', 'asker'], 'someone')}: ${valueOf(q, ['topic', 'title', 'question'], 'question')}`));
170
+ });
171
+ }
172
+ if (handoffs.length > 0) {
173
+ lines.push(padLine('HANDOFFS'));
174
+ const glyph = { pending: '●', read: '◐', claimed: '◆', done: '✓', completed: '✓', expired: '×' };
175
+ handoffs.slice(0, 6).forEach((h, index) => {
176
+ const status = valueOf(h, ['status'], 'pending');
177
+ const age = valueOf(h, ['ageDays', 'age_days'], '');
178
+ const intent = valueOf(h, ['intent'], '');
179
+ const meta = [
180
+ age === '' ? '' : `${age}d`,
181
+ intent && intent !== 'unclassified' ? intent : '',
182
+ status,
183
+ ].filter(Boolean).join(' · ');
184
+ lines.push(padLine(
185
+ `[${index + 1}] ${glyph[status] || '○'} ⇌ ${valueOf(h, ['author', 'from'], 'someone')}: ${valueOf(h, ['topic', 'title'], 'handoff')}`,
186
+ meta,
187
+ ));
188
+ });
189
+ }
190
+ }
191
+
192
+ lines.push(rule());
193
+ lines.push(padLine('RECENT WORK'));
194
+ if (mine.length === 0) {
195
+ lines.push(padLine('no recent local sessions found'));
196
+ } else {
197
+ mine.slice(0, 5).forEach((s) => {
198
+ lines.push(padLine(`${valueOf(s, ['date', 'when'], '')} ${valueOf(s, ['topic', 'title', 'branch'], 'session')}`, valueOf(s, ['type', 'status', 'duration'], '')));
199
+ });
200
+ }
201
+
202
+ if (team.length > 0 || prs.length > 0) {
203
+ lines.push(rule());
204
+ lines.push(padLine('AROUND'));
205
+ team.slice(0, 3).forEach((s) => {
206
+ lines.push(padLine(`${valueOf(s, ['author', 'name'], 'teammate')}: ${valueOf(s, ['topic', 'title', 'branch'], 'session')}`));
207
+ });
208
+ prs.slice(0, 3).forEach((pr) => {
209
+ lines.push(padLine(`#${valueOf(pr, ['number'], '')} ${valueOf(pr, ['title'], 'pull request')}`, valueOf(pr.author || {}, ['login', 'name'], '')));
210
+ });
211
+ }
212
+
213
+ lines.push(rule());
214
+ if (handoffs.length > 0) {
215
+ lines.push(padLine('Handoff actions: done N · expire N · reopen N'));
216
+ lines.push(padLine('Type a number to act, or keep working.'));
217
+ } else {
218
+ lines.push(padLine('Focus choices: questions, recent work, dashboard, done'));
219
+ }
220
+ lines.push(bottomRule());
221
+ return lines.join('\n');
222
+ }
223
+
224
+ function humanTopic(session) {
225
+ const topic = valueOf(session, ['topic', 'title'], '');
226
+ if (topic) return topic;
227
+ const branch = valueOf(session, ['branch'], '');
228
+ if (branch && !['develop', 'main', 'master'].includes(branch)) {
229
+ return branch.replace(/^dev\/[^/]+\//, '').replace(/[-_]+/g, ' ');
230
+ }
231
+ return valueOf(session, ['id', 'date'], 'session');
232
+ }
233
+
234
+ function renderDashboard(data) {
235
+ const graph = classifyGraph(data, { mode: data.mode || 'connected', attempt: 'retry' });
236
+ const org = data.org || 'Egregore';
237
+ const date = data.date || '';
238
+ const me = data.me?.github || data.me?.name || data.github_username || '';
239
+ const current = data.current_session || {};
240
+ const sessions = firstArray(data, ['sessions', 'local_sessions.sessions']);
241
+ const todos = firstArray(data, ['todos', 'open_todos']);
242
+ const quests = firstArray(data, ['quests', 'active_quests']);
243
+ const handoffs = firstArray(data, ['handoffs', 'pending_handoffs']);
244
+ const threads = firstArray(data, ['open_threads', 'threads']);
245
+ const stats = data.stats || {};
246
+
247
+ const lines = [topRule(), padLine('DASHBOARD', [org, me, date].filter(Boolean).join(' · ')), rule()];
248
+ if (graph.status !== 'connected') {
249
+ lines.push(padLine(`graph: ${graph.status}`, graph.reason));
250
+ lines.push(rule());
251
+ }
252
+
253
+ lines.push(padLine(`* ${humanTopic(current) || '(starting...)'}`, 'active'));
254
+
255
+ if (threads.length > 0) {
256
+ lines.push(rule());
257
+ lines.push(padLine('PICK UP WHERE YOU LEFT OFF'));
258
+ threads.slice(0, 5).forEach((thread) => {
259
+ lines.push(padLine(`- ${valueOf(thread, ['text', 'thread', 'title', 'topic'], 'open thread')}`));
260
+ });
261
+ }
262
+
263
+ if (todos.length > 0) {
264
+ lines.push(rule());
265
+ lines.push(padLine(`OPEN TODOS (${todos.length})`));
266
+ todos.slice(0, 3).forEach((todo, index) => {
267
+ lines.push(padLine(`[${index + 1}] ${valueOf(todo, ['text', 'title'], 'todo')}`, valueOf(todo, ['status'], '')));
268
+ });
269
+ }
270
+
271
+ if (handoffs.length > 0) {
272
+ lines.push(rule());
273
+ lines.push(padLine(`HANDOFFS (${handoffs.length} pending)`));
274
+ handoffs.slice(0, 3).forEach((handoff) => {
275
+ lines.push(padLine(`${valueOf(handoff, ['author', 'from'], 'someone')}: ${valueOf(handoff, ['topic', 'title'], 'handoff')}`, valueOf(handoff, ['when', 'date'], '')));
276
+ });
277
+ }
278
+
279
+ lines.push(rule());
280
+ lines.push(padLine(`SESSIONS (${data.range_label || 'recent'})`));
281
+ if (sessions.length === 0) {
282
+ lines.push(padLine('No activity recorded yet'));
283
+ } else {
284
+ sessions.slice(0, 5).forEach((session) => {
285
+ lines.push(padLine(`${valueOf(session, ['date', 'when'], '')} ${humanTopic(session)}`, valueOf(session, ['status', 'type'], '')));
286
+ });
287
+ }
288
+
289
+ if (quests.length > 0) {
290
+ lines.push(rule());
291
+ lines.push(padLine(`QUESTS (${quests.length} active)`));
292
+ quests.slice(0, 3).forEach((quest) => {
293
+ lines.push(padLine(valueOf(quest, ['id', 'quest', 'slug', 'title'], 'quest'), valueOf(quest, ['status', 'summary'], '')));
294
+ });
295
+ }
296
+
297
+ lines.push(rule());
298
+ const total = stats.totalSessions ?? data.local_sessions?.session_count ?? sessions.length;
299
+ const wrapped = stats.wrappedSessions ?? sessions.filter((s) => valueOf(s, ['status', 'type'], '').includes('wrap')).length;
300
+ lines.push(padLine(`${total} sessions · ${wrapped}/${total} wrapped · ${stats.openTodos ?? todos.length} open todos`));
301
+ lines.push(padLine("What's next?"));
302
+ lines.push(bottomRule());
303
+ return lines.join('\n');
304
+ }
305
+
306
+ function renderHandoff(data) {
307
+ const file = data.file ? `memory/${data.file}` : '';
308
+ const statuses = [
309
+ data.graphStatus ? `graph=${data.graphStatus}` : '',
310
+ data.memoryStatus ? `memory=${data.memoryStatus}` : '',
311
+ data.notifyStatus ? `notify=${data.notifyStatus}` : '',
312
+ data.publishStatus ? `publish=${data.publishStatus}` : '',
313
+ ].filter(Boolean).join(' · ');
314
+
315
+ const lines = [topRule(), padLine('⇌ HANDOFF', data.author || ''), rule()];
316
+ lines.push(padLine(`Topic: ${data.topic || '(untitled)'}`));
317
+ if (data.recipient) lines.push(padLine(`To: ${data.recipient}`));
318
+ if (file) lines.push(padLine(`Path: ${file}`));
319
+ if (data.artifactUrl) lines.push(padLine(`Link: ${data.artifactUrl}`));
320
+ if (statuses) lines.push(padLine(statuses));
321
+ if (data.publishStatus === 'fidelity-failed') {
322
+ lines.push(rule());
323
+ lines.push(padLine('Artifact not published — restore missing content and preview again.'));
324
+ }
325
+ lines.push(bottomRule());
326
+ return lines.join('\n');
327
+ }
328
+
329
+ const [command, ...rawArgs] = process.argv.slice(2);
330
+ if (!command) usage();
331
+
332
+ if (command === 'classify-graph') {
333
+ const { flags, rest } = parseFlags(rawArgs);
334
+ const data = readJson(rest[0]);
335
+ console.log(JSON.stringify(classifyGraph(data, flags)));
336
+ } else if (command === 'activity-card') {
337
+ const data = readJson(rawArgs[0]);
338
+ console.log(renderActivity(data));
339
+ } else if (command === 'dashboard-card') {
340
+ const data = readJson(rawArgs[0]);
341
+ console.log(renderDashboard(data));
342
+ } else if (command === 'handoff-card') {
343
+ const data = readJson(rawArgs[0]);
344
+ console.log(renderHandoff(data));
345
+ } else {
346
+ usage();
347
+ }