create-egregore 0.17.4 → 0.18.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 (110) hide show
  1. package/assets/egregore-launcher.js +1965 -0
  2. package/assets/egregore-launcher.sh +385 -0
  3. package/bin/cli.js +472 -81
  4. package/lib/api.js +2 -2
  5. package/lib/auth.js +255 -20
  6. package/lib/codex-runtime.js +268 -0
  7. package/lib/commands.js +31 -0
  8. package/lib/context.js +1 -1
  9. package/lib/local.js +616 -189
  10. package/lib/setup.js +822 -84
  11. package/lib/ui.js +780 -84
  12. package/package.json +11 -3
  13. package/runtime/codex/.codex/config.toml +15 -0
  14. package/runtime/codex/.codex/hooks/branch-guard.js +234 -0
  15. package/runtime/codex/.codex/hooks.json +17 -0
  16. package/runtime/codex/.codex/skills/activity/SKILL.md +70 -0
  17. package/runtime/codex/.codex/skills/add/SKILL.md +25 -0
  18. package/runtime/codex/.codex/skills/announce/SKILL.md +54 -0
  19. package/runtime/codex/.codex/skills/answer/SKILL.md +25 -0
  20. package/runtime/codex/.codex/skills/archive/SKILL.md +41 -0
  21. package/runtime/codex/.codex/skills/ask/SKILL.md +59 -0
  22. package/runtime/codex/.codex/skills/branch/SKILL.md +25 -0
  23. package/runtime/codex/.codex/skills/checkup/SKILL.md +41 -0
  24. package/runtime/codex/.codex/skills/comment/SKILL.md +25 -0
  25. package/runtime/codex/.codex/skills/commit/SKILL.md +25 -0
  26. package/runtime/codex/.codex/skills/contribute/SKILL.md +25 -0
  27. package/runtime/codex/.codex/skills/dark-mode/SKILL.md +25 -0
  28. package/runtime/codex/.codex/skills/dashboard/SKILL.md +59 -0
  29. package/runtime/codex/.codex/skills/deep-reflect/SKILL.md +70 -0
  30. package/runtime/codex/.codex/skills/delete-user/SKILL.md +25 -0
  31. package/runtime/codex/.codex/skills/edit/SKILL.md +25 -0
  32. package/runtime/codex/.codex/skills/edit-process/SKILL.md +25 -0
  33. package/runtime/codex/.codex/skills/egregore-voice/SKILL.md +25 -0
  34. package/runtime/codex/.codex/skills/emissary/SKILL.md +41 -0
  35. package/runtime/codex/.codex/skills/env/SKILL.md +25 -0
  36. package/runtime/codex/.codex/skills/handoff/SKILL.md +67 -0
  37. package/runtime/codex/.codex/skills/harvest/SKILL.md +107 -0
  38. package/runtime/codex/.codex/skills/infra/SKILL.md +41 -0
  39. package/runtime/codex/.codex/skills/ingest/SKILL.md +25 -0
  40. package/runtime/codex/.codex/skills/ingest-google/SKILL.md +25 -0
  41. package/runtime/codex/.codex/skills/ingest-user-interview/SKILL.md +41 -0
  42. package/runtime/codex/.codex/skills/invite/SKILL.md +40 -0
  43. package/runtime/codex/.codex/skills/issue/SKILL.md +41 -0
  44. package/runtime/codex/.codex/skills/me/SKILL.md +25 -0
  45. package/runtime/codex/.codex/skills/meeting/SKILL.md +41 -0
  46. package/runtime/codex/.codex/skills/note/SKILL.md +25 -0
  47. package/runtime/codex/.codex/skills/onboarding/SKILL.md +41 -0
  48. package/runtime/codex/.codex/skills/pr/SKILL.md +25 -0
  49. package/runtime/codex/.codex/skills/project/SKILL.md +41 -0
  50. package/runtime/codex/.codex/skills/pull/SKILL.md +25 -0
  51. package/runtime/codex/.codex/skills/push/SKILL.md +25 -0
  52. package/runtime/codex/.codex/skills/quest/SKILL.md +77 -0
  53. package/runtime/codex/.codex/skills/quest-suggest/SKILL.md +25 -0
  54. package/runtime/codex/.codex/skills/reflect/SKILL.md +41 -0
  55. package/runtime/codex/.codex/skills/review-pr/SKILL.md +41 -0
  56. package/runtime/codex/.codex/skills/save/SKILL.md +62 -0
  57. package/runtime/codex/.codex/skills/setup/SKILL.md +25 -0
  58. package/runtime/codex/.codex/skills/summon/SKILL.md +41 -0
  59. package/runtime/codex/.codex/skills/sync-repos/SKILL.md +25 -0
  60. package/runtime/codex/.codex/skills/telegram-connect/SKILL.md +25 -0
  61. package/runtime/codex/.codex/skills/telemetry/SKILL.md +25 -0
  62. package/runtime/codex/.codex/skills/test/SKILL.md +41 -0
  63. package/runtime/codex/.codex/skills/the-spiral/SKILL.md +84 -0
  64. package/runtime/codex/.codex/skills/todo/SKILL.md +41 -0
  65. package/runtime/codex/.codex/skills/tui-design/SKILL.md +41 -0
  66. package/runtime/codex/.codex/skills/tutorial/SKILL.md +41 -0
  67. package/runtime/codex/.codex/skills/update/SKILL.md +25 -0
  68. package/runtime/codex/.codex/skills/view/SKILL.md +41 -0
  69. package/runtime/codex/.codex/skills/visual-explain/SKILL.md +41 -0
  70. package/runtime/codex/.codex/skills/wrap/SKILL.md +55 -0
  71. package/runtime/codex/.codex/spec-manifest.json +108 -0
  72. package/runtime/codex/AGENTS.md +328 -0
  73. package/runtime/codex/bin/activity-data.sh +180 -0
  74. package/runtime/codex/bin/agent.sh +669 -0
  75. package/runtime/codex/bin/codex-install-skills.sh +78 -0
  76. package/runtime/codex/bin/codex-render-spec.mjs +308 -0
  77. package/runtime/codex/bin/codex-session-start.sh +238 -0
  78. package/runtime/codex/bin/codex-skill-render.mjs +315 -0
  79. package/runtime/codex/bin/codex-sync-skills.sh +223 -0
  80. package/runtime/codex/bin/dashboard-data.sh +178 -0
  81. package/runtime/codex/bin/graph-batch.sh +60 -0
  82. package/runtime/codex/bin/graph-op.sh +397 -0
  83. package/runtime/codex/bin/graph-wal.sh +228 -0
  84. package/runtime/codex/bin/graph.sh +141 -0
  85. package/runtime/codex/bin/handoff-pr-backfill.sh +86 -0
  86. package/runtime/codex/bin/handoff-run.sh +569 -0
  87. package/runtime/codex/bin/handoff-save-egregore.sh +131 -0
  88. package/runtime/codex/bin/index-handoff.sh +306 -0
  89. package/runtime/codex/bin/lib/artifact-id.sh +46 -0
  90. package/runtime/codex/bin/lib/config.sh +71 -0
  91. package/runtime/codex/bin/lib/context.sh +511 -0
  92. package/runtime/codex/bin/lib/dashboard-artifact.sh +142 -0
  93. package/runtime/codex/bin/lib/edit-loopback.mjs +652 -0
  94. package/runtime/codex/bin/lib/ensure-develop.sh +183 -0
  95. package/runtime/codex/bin/lib/git-sync.sh +262 -0
  96. package/runtime/codex/bin/lib/greeting.sh +432 -0
  97. package/runtime/codex/bin/lib/hash.sh +13 -0
  98. package/runtime/codex/bin/lib/identity.sh +164 -0
  99. package/runtime/codex/bin/lib/permissions.sh +106 -0
  100. package/runtime/codex/bin/lib/time.sh +56 -0
  101. package/runtime/codex/bin/lib/worktree-links.sh +54 -0
  102. package/runtime/codex/bin/notify.sh +207 -0
  103. package/runtime/codex/bin/publish-artifact.sh +187 -0
  104. package/runtime/codex/bin/publish-references.sh +144 -0
  105. package/runtime/codex/bin/repo-state.sh +99 -0
  106. package/runtime/codex/bin/telemetry.sh +350 -0
  107. package/runtime/codex/bin/worktree-create.sh +115 -0
  108. package/runtime/codex/bin/worktree-remove.sh +50 -0
  109. package/runtime/codex/bin/worktree.sh +199 -0
  110. package/runtime/codex/manifest.json +162 -0
@@ -0,0 +1,669 @@
1
+ #!/bin/bash
2
+ # Runtime-neutral Egregore agent bridge.
3
+ #
4
+ # This script exposes the Git-backed memory protocol without requiring
5
+ # Claude Code hooks or slash-command skills. Any agent runtime that can run
6
+ # shell commands inside an Egregore checkout can use it to sync memory,
7
+ # create handoffs, ask questions, answer questions, and inspect activity.
8
+ set -euo pipefail
9
+
10
+ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
11
+ MAIN_PROJECT_DIR="$SCRIPT_DIR"
12
+ if [ -f "$SCRIPT_DIR/.git" ]; then
13
+ WT_GITDIR=$(sed 's/^gitdir: //' "$SCRIPT_DIR/.git" 2>/dev/null || true)
14
+ [ -n "$WT_GITDIR" ] && MAIN_PROJECT_DIR=$(cd "$WT_GITDIR/../../.." 2>/dev/null && pwd || echo "$SCRIPT_DIR")
15
+ fi
16
+ if [ -f "$SCRIPT_DIR/bin/lib/worktree-links.sh" ]; then
17
+ source "$SCRIPT_DIR/bin/lib/worktree-links.sh" >/dev/null 2>/dev/null || true
18
+ egregore_link_shared_state "$SCRIPT_DIR" "$MAIN_PROJECT_DIR" >/dev/null 2>/dev/null || true
19
+ fi
20
+ CONFIG="$SCRIPT_DIR/egregore.json"
21
+ MEMORY_DIR="${EGREGORE_MEMORY_DIR:-$SCRIPT_DIR/memory}"
22
+
23
+ usage() {
24
+ cat <<'EOF'
25
+ Usage: agent.sh <command> [options]
26
+
27
+ Runtime-neutral commands:
28
+ protocol Print the portable agent protocol
29
+ sync Pull latest shared memory
30
+ activity [--for <person>] Show recent handoffs and questions
31
+ people List known people from memory/people
32
+ branch --topic TEXT Create or reuse a task branch/worktree
33
+ save [--message TEXT] Commit and push current repo + memory changes
34
+ wrap --topic T --summary TEXT Write a session wrap to memory/wraps
35
+ handoff --from A --to B --topic T [--body TEXT|--body-file PATH] [--no-push] [--no-publish] [--no-notify] [--json]
36
+ ask --from A --to B --topic T --question TEXT [--no-push]
37
+ [--harvest-id ID --harvest-session-id ID --turn N
38
+ --question-intent TEXT --context-mode blind|disclosed|comparative]
39
+ answer --from A --question PATH --body TEXT [--no-push]
40
+
41
+ The script uses the existing memory/ repository and keeps Claude Code
42
+ behavior unchanged.
43
+ EOF
44
+ }
45
+
46
+ die() {
47
+ echo "agent.sh: $*" >&2
48
+ exit 1
49
+ }
50
+
51
+ slugify() {
52
+ echo "$1" \
53
+ | tr '[:upper:]' '[:lower:]' \
54
+ | sed -E 's/[^a-z0-9]+/-/g' \
55
+ | sed -E 's/^-+|-+$//g' \
56
+ | cut -c1-60
57
+ }
58
+
59
+ # Reject control characters that would break YAML frontmatter when interpolated.
60
+ reject_newlines() {
61
+ case "$2" in
62
+ *$'\n'*|*$'\r'*) die "invalid $1: must not contain newlines" ;;
63
+ esac
64
+ }
65
+
66
+ # Emit a value as a JSON-encoded string (valid YAML), so colons, quotes,
67
+ # and other YAML metacharacters in user-supplied values cannot inject keys.
68
+ yaml_str() {
69
+ jq -n --arg v "$1" '$v'
70
+ }
71
+
72
+ ensure_memory() {
73
+ [ -d "$MEMORY_DIR" ] || die "memory directory not found: $MEMORY_DIR"
74
+ }
75
+
76
+ is_memory_git() {
77
+ git -C "$MEMORY_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1
78
+ }
79
+
80
+ memory_branch() {
81
+ git -C "$MEMORY_DIR" branch --show-current 2>/dev/null || echo "main"
82
+ }
83
+
84
+ sync_memory() {
85
+ ensure_memory
86
+ if is_memory_git && git -C "$MEMORY_DIR" remote get-url origin >/dev/null 2>&1; then
87
+ local branch
88
+ branch="$(memory_branch)"
89
+ [ -z "$branch" ] && branch="main"
90
+ git -C "$MEMORY_DIR" pull --rebase origin "$branch" --quiet 2>/dev/null || true
91
+ fi
92
+ }
93
+
94
+ save_memory() {
95
+ local message="$1"
96
+ shift
97
+ local no_push="${NO_PUSH:-0}"
98
+
99
+ ensure_memory
100
+ if ! is_memory_git; then
101
+ return 0
102
+ fi
103
+
104
+ git -C "$MEMORY_DIR" add "$@" >/dev/null 2>&1 || true
105
+ if ! git -C "$MEMORY_DIR" diff --cached --quiet 2>/dev/null; then
106
+ git -C "$MEMORY_DIR" commit -m "$message" --quiet 2>/dev/null || true
107
+ fi
108
+
109
+ [ "$no_push" = "1" ] && return 0
110
+ git -C "$MEMORY_DIR" remote get-url origin >/dev/null 2>&1 || return 0
111
+
112
+ local branch
113
+ branch="$(memory_branch)"
114
+ [ -z "$branch" ] && branch="main"
115
+ for _try in 1 2 3; do
116
+ if git -C "$MEMORY_DIR" pull --rebase origin "$branch" --quiet 2>/dev/null \
117
+ && git -C "$MEMORY_DIR" push origin "$branch" --quiet 2>/dev/null; then
118
+ return 0
119
+ fi
120
+ sleep 1
121
+ done
122
+ die "could not push memory after 3 attempts"
123
+ }
124
+
125
+ person_from_state() {
126
+ if [ -f "$SCRIPT_DIR/.egregore-state.json" ]; then
127
+ jq -r '.github_username // .name // empty' "$SCRIPT_DIR/.egregore-state.json" 2>/dev/null || true
128
+ fi
129
+ }
130
+
131
+ current_author() {
132
+ local author
133
+ author="$(person_from_state)"
134
+ [ -n "$author" ] || author="$(git -C "$SCRIPT_DIR" config user.name 2>/dev/null | awk '{print tolower($1)}')"
135
+ [ -n "$author" ] || author="agent"
136
+ echo "$author"
137
+ }
138
+
139
+ worktree_for_branch() {
140
+ local branch="$1"
141
+ git -C "$MAIN_PROJECT_DIR" worktree list --porcelain 2>/dev/null | awk -v ref="refs/heads/$branch" '
142
+ $1 == "worktree" { path = $2 }
143
+ $1 == "branch" && $2 == ref { print path; exit }
144
+ '
145
+ }
146
+
147
+ base_ref() {
148
+ git -C "$MAIN_PROJECT_DIR" fetch origin develop --quiet 2>/dev/null || true
149
+ if git -C "$MAIN_PROJECT_DIR" show-ref --verify --quiet refs/remotes/origin/develop 2>/dev/null; then
150
+ echo "origin/develop"
151
+ elif git -C "$MAIN_PROJECT_DIR" show-ref --verify --quiet refs/heads/develop 2>/dev/null; then
152
+ echo "develop"
153
+ else
154
+ echo "HEAD"
155
+ fi
156
+ }
157
+
158
+ cmd_protocol() {
159
+ cat <<'EOF'
160
+ Egregore portable agent protocol
161
+
162
+ 1. Work inside an Egregore checkout with a linked memory/ repository.
163
+ 2. Run `bin/agent.sh sync` before reading shared state.
164
+ 3. Read `memory/people/` for collaborators.
165
+ 4. Use `bin/agent.sh handoff` to leave structured context for another agent.
166
+ 5. Use `bin/agent.sh ask` and `bin/agent.sh answer` for asynchronous questions.
167
+ 6. Run `bin/agent.sh activity --for <person>` to see pending work.
168
+ 7. Run `bin/agent.sh branch --topic "<what you are working on>"` before code changes when you need a task branch/worktree.
169
+ 8. Run `bin/agent.sh save` and `bin/agent.sh wrap` for portable save/wrap flows.
170
+
171
+ This protocol is runtime-neutral. Claude Code may still use .claude hooks and
172
+ skills, while Codex or another agent can call this script directly.
173
+ EOF
174
+ }
175
+
176
+ cmd_people() {
177
+ ensure_memory
178
+ local people_dir="$MEMORY_DIR/people"
179
+ [ -d "$people_dir" ] || return 0
180
+ local f github display
181
+ for f in "$people_dir"/*.md; do
182
+ [ -f "$f" ] || continue
183
+ github="$(basename "$f" .md)"
184
+ display="$(sed -nE 's/^name:[[:space:]]*(.*)$/\1/p; s/^# (.*)$/\1/p' "$f" | head -1)"
185
+ [ -z "$display" ] && display="$github"
186
+ printf '%s\t%s\n' "$github" "$display"
187
+ done
188
+ }
189
+
190
+ cmd_sync() {
191
+ sync_memory
192
+ echo "synced: $MEMORY_DIR"
193
+ }
194
+
195
+ cmd_activity() {
196
+ ensure_memory
197
+ local for_person=""
198
+ local limit=12
199
+ while [ $# -gt 0 ]; do
200
+ case "$1" in
201
+ --for) for_person="${2:-}"; shift 2 ;;
202
+ --limit) limit="${2:-12}"; shift 2 ;;
203
+ *) die "unknown activity option: $1" ;;
204
+ esac
205
+ done
206
+
207
+ echo "Handoffs"
208
+ local index="$MEMORY_DIR/handoffs/index.md"
209
+ if [ -f "$index" ]; then
210
+ if [ -n "$for_person" ]; then
211
+ grep -i "handoff to ${for_person}" "$index" 2>/dev/null | head -n "$limit" || true
212
+ else
213
+ grep '^- ' "$index" 2>/dev/null | head -n "$limit" || true
214
+ fi
215
+ fi
216
+
217
+ echo
218
+ echo "Questions"
219
+ local qdir="$MEMORY_DIR/knowledge/questions"
220
+ [ -d "$qdir" ] || return 0
221
+ local q from to topic status
222
+ while IFS= read -r q; do
223
+ from="$(sed -nE 's/^from:[[:space:]]*(.*)$/\1/p' "$q" | head -1)"
224
+ to="$(sed -nE 's/^to:[[:space:]]*(.*)$/\1/p' "$q" | head -1)"
225
+ topic="$(sed -nE 's/^topic:[[:space:]]*(.*)$/\1/p' "$q" | head -1)"
226
+ status="$(sed -nE 's/^status:[[:space:]]*(.*)$/\1/p' "$q" | head -1)"
227
+ [ -z "$status" ] && status="pending"
228
+ if [ -n "$for_person" ] && [ "$(echo "$to" | tr '[:upper:]' '[:lower:]')" != "$(echo "$for_person" | tr '[:upper:]' '[:lower:]')" ]; then
229
+ continue
230
+ fi
231
+ printf '%s\n' "- ${from} -> ${to} [${status}]: ${topic} (${q#$MEMORY_DIR/})"
232
+ done < <(find "$qdir" -type f -name '*.md' | sort -r | head -n "$limit")
233
+ }
234
+
235
+ cmd_branch() {
236
+ local topic="" author="" branch_type="" slug="" branch="" wt_path="" existing_wt="" base=""
237
+
238
+ while [ $# -gt 0 ]; do
239
+ case "$1" in
240
+ --topic|--description) topic="${2:-}"; shift 2 ;;
241
+ --from|--author) author="${2:-}"; shift 2 ;;
242
+ --type) branch_type="${2:-}"; shift 2 ;;
243
+ *) die "unknown branch option: $1" ;;
244
+ esac
245
+ done
246
+
247
+ [ -n "$topic" ] || topic="$(date +%Y-%m-%d)"
248
+ [ -n "$author" ] || author="$(current_author)"
249
+
250
+ if [ -z "$branch_type" ]; then
251
+ case "$(echo "$topic" | tr '[:upper:]' '[:lower:]')" in
252
+ *fix*|*bug*|*broken*|*crash*) branch_type="bugfix" ;;
253
+ *feature*|*add*|*implement*|*new*) branch_type="feature" ;;
254
+ *) branch_type="dev" ;;
255
+ esac
256
+ fi
257
+ case "$branch_type" in
258
+ dev|feature|bugfix) ;;
259
+ *) die "invalid --type: $branch_type (valid: dev|feature|bugfix)" ;;
260
+ esac
261
+
262
+ slug="$(slugify "$topic")"
263
+ [ -n "$slug" ] || slug="$(date +%Y-%m-%d)"
264
+ case "$branch_type" in
265
+ dev) branch="dev/${author}/${slug}" ;;
266
+ feature) branch="feature/${slug}" ;;
267
+ bugfix) branch="bugfix/${slug}" ;;
268
+ esac
269
+
270
+ base="$(base_ref)"
271
+
272
+ if [ -f "$SCRIPT_DIR/.git" ]; then
273
+ if git -C "$SCRIPT_DIR" show-ref --verify --quiet "refs/heads/$branch" 2>/dev/null; then
274
+ git -C "$SCRIPT_DIR" checkout "$branch" --quiet
275
+ else
276
+ git -C "$SCRIPT_DIR" checkout -b "$branch" "$base" --quiet
277
+ fi
278
+ egregore_link_shared_state "$SCRIPT_DIR" "$MAIN_PROJECT_DIR" >/dev/null 2>/dev/null || true
279
+ echo "branch: $branch"
280
+ echo "worktree: $SCRIPT_DIR"
281
+ return 0
282
+ fi
283
+
284
+ existing_wt="$(worktree_for_branch "$branch")"
285
+ if [ -n "$existing_wt" ] && [ -d "$existing_wt" ]; then
286
+ bash "$MAIN_PROJECT_DIR/bin/worktree.sh" setup "$existing_wt" "$MAIN_PROJECT_DIR" >/dev/null 2>/dev/null || true
287
+ echo "branch: $branch"
288
+ echo "worktree: $existing_wt"
289
+ return 0
290
+ fi
291
+
292
+ if ! git -C "$MAIN_PROJECT_DIR" show-ref --verify --quiet "refs/heads/$branch" 2>/dev/null; then
293
+ git -C "$MAIN_PROJECT_DIR" branch "$branch" "$base" >/dev/null 2>&1 || die "failed to create branch $branch"
294
+ fi
295
+
296
+ wt_path="$MAIN_PROJECT_DIR/.claude/worktrees/$slug"
297
+ if [ -e "$wt_path" ]; then
298
+ die "worktree path already exists: $wt_path"
299
+ fi
300
+
301
+ mkdir -p "$MAIN_PROJECT_DIR/.claude/worktrees"
302
+ git -C "$MAIN_PROJECT_DIR" worktree add "$wt_path" "$branch" --quiet 2>/dev/null || die "failed to create worktree at $wt_path"
303
+ bash "$MAIN_PROJECT_DIR/bin/worktree.sh" setup "$wt_path" "$MAIN_PROJECT_DIR" >/dev/null 2>/dev/null || true
304
+
305
+ echo "branch: $branch"
306
+ echo "worktree: $wt_path"
307
+ }
308
+
309
+ ensure_working_branch() {
310
+ local topic="${1:-$(date +%Y-%m-%d)}"
311
+ local author branch slug base
312
+ branch="$(git -C "$SCRIPT_DIR" branch --show-current 2>/dev/null || echo "")"
313
+ case "$branch" in
314
+ dev/*|feature/*|bugfix/*) echo "$branch"; return 0 ;;
315
+ esac
316
+
317
+ author="$(current_author)"
318
+ slug="$(slugify "$topic")"
319
+ [ -n "$slug" ] || slug="$(date +%Y-%m-%d)"
320
+ branch="dev/${author}/${slug}"
321
+ base="$(base_ref)"
322
+
323
+ if git -C "$SCRIPT_DIR" show-ref --verify --quiet "refs/heads/$branch" 2>/dev/null; then
324
+ git -C "$SCRIPT_DIR" checkout "$branch" --quiet || die "failed to checkout $branch"
325
+ else
326
+ git -C "$SCRIPT_DIR" checkout -b "$branch" "$base" --quiet || die "failed to create $branch"
327
+ fi
328
+ echo "$branch"
329
+ }
330
+
331
+ cmd_save() {
332
+ local message="" topic="" no_pr=0 branch pushed="false" committed="false" pr_url="" existing_pr=""
333
+
334
+ while [ $# -gt 0 ]; do
335
+ case "$1" in
336
+ --message|-m) message="${2:-}"; shift 2 ;;
337
+ --topic) topic="${2:-}"; shift 2 ;;
338
+ --no-pr) no_pr=1; shift ;;
339
+ *) die "unknown save option: $1" ;;
340
+ esac
341
+ done
342
+
343
+ [ -n "$topic" ] || topic="portable save"
344
+ [ -n "$message" ] || message="Save: $topic"
345
+
346
+ if [ -d "$MEMORY_DIR" ]; then
347
+ save_memory "$message" "."
348
+ echo "memory: synced"
349
+ fi
350
+
351
+ branch="$(git -C "$SCRIPT_DIR" branch --show-current 2>/dev/null || echo "")"
352
+ if [ -n "$(git -C "$SCRIPT_DIR" status --porcelain 2>/dev/null | head -1)" ]; then
353
+ branch="$(ensure_working_branch "$topic")"
354
+ git -C "$SCRIPT_DIR" add -A >/dev/null 2>&1 || die "git add failed"
355
+ if ! git -C "$SCRIPT_DIR" diff --cached --quiet 2>/dev/null; then
356
+ git -C "$SCRIPT_DIR" commit -m "$message" --quiet || die "git commit failed"
357
+ committed="true"
358
+ fi
359
+ fi
360
+ [ -n "$branch" ] || branch="$(git -C "$SCRIPT_DIR" branch --show-current 2>/dev/null || echo "")"
361
+
362
+ if [ -n "$branch" ] && git -C "$SCRIPT_DIR" remote get-url origin >/dev/null 2>&1; then
363
+ if git -C "$SCRIPT_DIR" push -u origin "$branch" --quiet 2>/dev/null; then
364
+ pushed="true"
365
+ else
366
+ die "push failed for $branch; commits are safe locally"
367
+ fi
368
+ fi
369
+
370
+ if [ "$no_pr" = "0" ] && [ "$pushed" = "true" ] && command -v gh >/dev/null 2>&1; then
371
+ existing_pr="$(gh pr list --head "$branch" --state open --json url --jq '.[0].url // empty' 2>/dev/null || true)"
372
+ if [ -n "$existing_pr" ]; then
373
+ pr_url="$existing_pr"
374
+ else
375
+ pr_url="$(gh pr create --base develop --head "$branch" --title "$message" --body "Saved via bin/agent.sh save." 2>/dev/null || true)"
376
+ fi
377
+ fi
378
+
379
+ echo "branch: ${branch:-none}"
380
+ echo "commit: $committed"
381
+ echo "push: $pushed"
382
+ [ -n "$pr_url" ] && echo "pr: $pr_url"
383
+ return 0
384
+ }
385
+
386
+ cmd_wrap() {
387
+ ensure_memory
388
+ local from="" topic="" summary="" body="" body_file="" no_push=0
389
+
390
+ while [ $# -gt 0 ]; do
391
+ case "$1" in
392
+ --from) from="${2:-}"; shift 2 ;;
393
+ --topic) topic="${2:-}"; shift 2 ;;
394
+ --summary) summary="${2:-}"; shift 2 ;;
395
+ --body) body="${2:-}"; shift 2 ;;
396
+ --body-file) body_file="${2:-}"; shift 2 ;;
397
+ --no-push) no_push=1; shift ;;
398
+ *) die "unknown wrap option: $1" ;;
399
+ esac
400
+ done
401
+
402
+ [ -n "$from" ] || from="$(current_author)"
403
+ [ -n "$topic" ] || die "wrap requires --topic"
404
+ [ -n "$summary" ] || die "wrap requires --summary"
405
+ if [ -n "$body_file" ]; then
406
+ [ -f "$body_file" ] || die "body file not found: $body_file"
407
+ body="$(cat "$body_file")"
408
+ elif [ -z "$body" ] && [ ! -t 0 ]; then
409
+ body="$(cat)"
410
+ fi
411
+ [ -n "$body" ] || body="$summary"
412
+
413
+ local month rel abs slug now
414
+ month="$(date +%Y-%m)"
415
+ slug="$(slugify "$topic")"
416
+ [ -n "$slug" ] || slug="session"
417
+ rel="wraps/$month/$(date +%d)-${from}-${slug}.md"
418
+ abs="$MEMORY_DIR/$rel"
419
+ now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
420
+ mkdir -p "$(dirname "$abs")"
421
+
422
+ {
423
+ printf '# Wrap: %s\n\n' "$topic"
424
+ printf '**Date**: %s\n' "$(date +%Y-%m-%d)"
425
+ printf '**Author**: %s\n' "$from"
426
+ printf '**Wrapped At**: %s\n\n' "$now"
427
+ printf '## Summary\n\n%s\n\n' "$summary"
428
+ printf '## Notes\n\n%s\n' "$body"
429
+ } > "$abs"
430
+
431
+ NO_PUSH="$no_push" save_memory "Wrap: $topic" "$rel"
432
+ echo "wrap: memory/$rel"
433
+ }
434
+
435
+ cmd_handoff() {
436
+ ensure_memory
437
+ local from="" to="" topic="" body="" body_file="" project=""
438
+ local no_publish=0 no_notify=0 json=0
439
+ NO_PUSH=0
440
+
441
+ while [ $# -gt 0 ]; do
442
+ case "$1" in
443
+ --from) from="${2:-}"; shift 2 ;;
444
+ --to) to="${2:-}"; shift 2 ;;
445
+ --topic) topic="${2:-}"; shift 2 ;;
446
+ --body) body="${2:-}"; shift 2 ;;
447
+ --body-file) body_file="${2:-}"; shift 2 ;;
448
+ --project) project="${2:-}"; shift 2 ;;
449
+ --no-push) NO_PUSH=1; shift ;;
450
+ --no-publish) no_publish=1; shift ;;
451
+ --no-notify) no_notify=1; shift ;;
452
+ --json) json=1; shift ;;
453
+ *) die "unknown handoff option: $1" ;;
454
+ esac
455
+ done
456
+
457
+ [ -z "$from" ] && from="$(person_from_state)"
458
+ [ -n "$from" ] || die "handoff requires --from"
459
+ [ -n "$topic" ] || die "handoff requires --topic"
460
+ if [ -n "$body_file" ]; then
461
+ [ -f "$body_file" ] || die "body file not found: $body_file"
462
+ body="$(cat "$body_file")"
463
+ elif [ -z "$body" ] && [ ! -t 0 ]; then
464
+ body="$(cat)"
465
+ fi
466
+ [ -n "$body" ] || body="No briefing provided."
467
+
468
+ local today tmpd result result_out rel project_line stdout_file
469
+ local handoff_args=()
470
+ today="$(date +%Y-%m-%d)"
471
+ tmpd="$(mktemp -d -t egregore-agent-handoff-XXXXXX)"
472
+ trap 'rm -rf "$tmpd"' RETURN
473
+ result_out="$(mktemp -t egregore-agent-handoff-result-XXXXXX.json)"
474
+ stdout_file="$tmpd/stdout"
475
+
476
+ project_line=""
477
+ [ -n "$project" ] && project_line="**Project**: $project"
478
+ [ -n "$to" ] && handoff_args+=(--recipient "$to")
479
+ [ -n "$project" ] && handoff_args+=(--project "$project")
480
+ [ "$NO_PUSH" = "1" ] && handoff_args+=(--no-push)
481
+ [ "$no_publish" = "1" ] && handoff_args+=(--no-publish)
482
+ [ "$no_notify" = "1" ] && handoff_args+=(--no-notify)
483
+
484
+ {
485
+ printf '# Handoff: %s\n\n' "$topic"
486
+ printf '**Date**: %s\n' "$today"
487
+ printf '**Author**: %s\n' "$from"
488
+ [ -n "$to" ] && printf '**To**: %s\n' "$to"
489
+ [ -n "$project_line" ] && printf '%s\n' "$project_line"
490
+ printf '\n## Briefing\n\n%s\n\n' "$body"
491
+ printf '## Next Steps\n\n- Continue from this handoff using the shared Egregore memory protocol.\n'
492
+ } | TMPDIR="$tmpd" bash "$SCRIPT_DIR/bin/handoff-run.sh" \
493
+ --author "$from" \
494
+ --topic "$topic" \
495
+ "${handoff_args[@]}" \
496
+ > "$stdout_file"
497
+
498
+ result="$tmpd/handoff-run-result.json"
499
+ rel="$(jq -r '.file // empty' "$result" 2>/dev/null || true)"
500
+ [ -n "$rel" ] || die "handoff was not created"
501
+ cp "$result" "$result_out"
502
+ if [ "$json" = "1" ]; then
503
+ cat "$result_out"
504
+ return 0
505
+ fi
506
+ [ -s "$stdout_file" ] && sed 's/^/status: /' "$stdout_file"
507
+ echo "handoff: memory/$rel"
508
+ echo "result: $result_out"
509
+ }
510
+
511
+ cmd_ask() {
512
+ ensure_memory
513
+ local from="" to="" topic="" question=""
514
+ local harvest_id="" harvest_session_id="" turn="" question_intent="" context_mode=""
515
+ NO_PUSH=0
516
+
517
+ while [ $# -gt 0 ]; do
518
+ case "$1" in
519
+ --from) from="${2:-}"; shift 2 ;;
520
+ --to) to="${2:-}"; shift 2 ;;
521
+ --topic) topic="${2:-}"; shift 2 ;;
522
+ --question) question="${2:-}"; shift 2 ;;
523
+ --harvest-id) harvest_id="${2:-}"; shift 2 ;;
524
+ --harvest-session-id) harvest_session_id="${2:-}"; shift 2 ;;
525
+ --turn) turn="${2:-}"; shift 2 ;;
526
+ --question-intent) question_intent="${2:-}"; shift 2 ;;
527
+ --context-mode) context_mode="${2:-}"; shift 2 ;;
528
+ --no-push) NO_PUSH=1; shift ;;
529
+ *) die "unknown ask option: $1" ;;
530
+ esac
531
+ done
532
+
533
+ [ -z "$from" ] && from="$(person_from_state)"
534
+ [ -n "$from" ] || die "ask requires --from"
535
+ [ -n "$to" ] || die "ask requires --to"
536
+ [ -n "$topic" ] || die "ask requires --topic"
537
+ [ -n "$question" ] || die "ask requires --question"
538
+
539
+ if [ -n "$context_mode" ]; then
540
+ case "$context_mode" in
541
+ blind|disclosed|comparative) ;;
542
+ *) die "invalid --context-mode: $context_mode (valid: blind|disclosed|comparative)" ;;
543
+ esac
544
+ fi
545
+
546
+ if [ -n "$turn" ]; then
547
+ case "$turn" in
548
+ ''|*[!0-9]*) die "invalid --turn: must be a non-negative integer" ;;
549
+ esac
550
+ fi
551
+
552
+ reject_newlines "--from" "$from"
553
+ reject_newlines "--to" "$to"
554
+ reject_newlines "--topic" "$topic"
555
+ reject_newlines "--question" "$question"
556
+ reject_newlines "--harvest-id" "$harvest_id"
557
+ reject_newlines "--harvest-session-id" "$harvest_session_id"
558
+ reject_newlines "--question-intent" "$question_intent"
559
+
560
+ local now slug rel abs n
561
+ now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
562
+ slug="$(slugify "$topic")"
563
+ mkdir -p "$MEMORY_DIR/knowledge/questions"
564
+ rel="knowledge/questions/$(date +%Y-%m-%d)-${from}-to-${to}-${slug}.md"
565
+ abs="$MEMORY_DIR/$rel"
566
+ n=2
567
+ while [ -e "$abs" ]; do
568
+ rel="knowledge/questions/$(date +%Y-%m-%d)-${from}-to-${to}-${slug}-${n}.md"
569
+ abs="$MEMORY_DIR/$rel"
570
+ n=$((n + 1))
571
+ done
572
+
573
+ {
574
+ echo "---"
575
+ echo "from: $from"
576
+ echo "to: $to"
577
+ echo "topic: $topic"
578
+ echo "status: pending"
579
+ echo "created: $now"
580
+ [ -n "$harvest_id" ] && echo "harvest_id: $(yaml_str "$harvest_id")"
581
+ [ -n "$harvest_session_id" ] && echo "harvest_session_id: $(yaml_str "$harvest_session_id")"
582
+ [ -n "$turn" ] && echo "turn: $turn"
583
+ [ -n "$question_intent" ] && echo "question_intent: $(yaml_str "$question_intent")"
584
+ [ -n "$context_mode" ] && echo "context_mode: $context_mode"
585
+ echo "---"
586
+ echo ""
587
+ echo "## Questions"
588
+ echo ""
589
+ echo "- $question"
590
+ } > "$abs"
591
+
592
+ save_memory "Question: ${topic} (to ${to})" "$rel"
593
+ echo "question: memory/$rel"
594
+ }
595
+
596
+ cmd_answer() {
597
+ ensure_memory
598
+ local from="" question_path="" body=""
599
+ NO_PUSH=0
600
+
601
+ while [ $# -gt 0 ]; do
602
+ case "$1" in
603
+ --from) from="${2:-}"; shift 2 ;;
604
+ --question) question_path="${2:-}"; shift 2 ;;
605
+ --body) body="${2:-}"; shift 2 ;;
606
+ --no-push) NO_PUSH=1; shift ;;
607
+ *) die "unknown answer option: $1" ;;
608
+ esac
609
+ done
610
+
611
+ [ -z "$from" ] && from="$(person_from_state)"
612
+ [ -n "$from" ] || die "answer requires --from"
613
+ [ -n "$question_path" ] || die "answer requires --question"
614
+ [ -n "$body" ] || die "answer requires --body"
615
+
616
+ local abs rel now tmp
617
+ case "$question_path" in
618
+ "$MEMORY_DIR"/*) abs="$question_path" ;;
619
+ memory/*) abs="$MEMORY_DIR/${question_path#memory/}" ;;
620
+ *) abs="$MEMORY_DIR/$question_path" ;;
621
+ esac
622
+ [ -f "$abs" ] || die "question file not found: $question_path"
623
+ rel="${abs#$MEMORY_DIR/}"
624
+ now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
625
+ tmp="$abs.tmp.$$"
626
+
627
+ awk -v from="$from" -v now="$now" '
628
+ BEGIN { in_fm = 0; added = 0; saw_status = 0 }
629
+ NR == 1 && $0 == "---" { in_fm = 1; print; next }
630
+ in_fm && /^status:/ { print "status: answered"; saw_status = 1; next }
631
+ in_fm && $0 == "---" {
632
+ if (!saw_status) print "status: answered"
633
+ if (!added) {
634
+ print "answered_by: " from
635
+ print "answered_at: " now
636
+ added = 1
637
+ }
638
+ in_fm = 0
639
+ print
640
+ next
641
+ }
642
+ { print }
643
+ ' "$abs" > "$tmp" && mv "$tmp" "$abs"
644
+
645
+ {
646
+ printf '\n## Answer\n\n'
647
+ printf '**From**: %s\n' "$from"
648
+ printf '**At**: %s\n\n' "$now"
649
+ printf '%s\n' "$body"
650
+ } >> "$abs"
651
+
652
+ save_memory "Answer question: ${rel}" "$rel"
653
+ echo "answered: memory/$rel"
654
+ }
655
+
656
+ case "${1:-}" in
657
+ --help|-h|help|"") usage ;;
658
+ protocol) shift; cmd_protocol "$@" ;;
659
+ sync) shift; cmd_sync "$@" ;;
660
+ activity) shift; cmd_activity "$@" ;;
661
+ people) shift; cmd_people "$@" ;;
662
+ branch) shift; cmd_branch "$@" ;;
663
+ save) shift; cmd_save "$@" ;;
664
+ wrap) shift; cmd_wrap "$@" ;;
665
+ handoff) shift; cmd_handoff "$@" ;;
666
+ ask) shift; cmd_ask "$@" ;;
667
+ answer) shift; cmd_answer "$@" ;;
668
+ *) die "unknown command: $1" ;;
669
+ esac