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
@@ -0,0 +1,775 @@
1
+ #!/usr/bin/env 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
+ if [ -f "$SCRIPT_DIR/bin/lib/config.sh" ]; then
23
+ # shellcheck source=bin/lib/config.sh
24
+ source "$SCRIPT_DIR/bin/lib/config.sh"
25
+ fi
26
+
27
+ usage() {
28
+ cat <<'EOF'
29
+ Usage: agent.sh <command> [options]
30
+
31
+ Runtime-neutral commands:
32
+ protocol Print the portable agent protocol
33
+ sync Pull latest shared memory
34
+ activity [--for <person>] Show recent handoffs and questions
35
+ search "query" [-n N] [--fast|--semantic]
36
+ Hybrid recall over shared memory (qmd)
37
+ people List known people from memory/people
38
+ branch --topic TEXT Create or reuse a task branch/worktree
39
+ save [--message TEXT] [--pr-body TEXT|--pr-body-file PATH]
40
+ Commit and push current repo + memory changes;
41
+ PR body follows .claude/context/pr-format.md
42
+ (auto-generated skeleton if not provided)
43
+ autosave [status|on|off|scope|publish|merge] Personal ambient-save settings + consent
44
+ wrap --topic T --summary TEXT Write a session wrap to memory/wraps
45
+ handoff --from A --to B --topic T [--intent action|feedback|fyi]
46
+ [--body TEXT|--body-file PATH] [--no-push] [--no-publish] [--no-notify] [--json]
47
+ ask --from A --to B --topic T --question TEXT [--no-push]
48
+ [--harvest-id ID --harvest-session-id ID --turn N
49
+ --question-intent TEXT --context-mode blind|disclosed|comparative]
50
+ answer --from A --question PATH --body TEXT [--no-push]
51
+
52
+ The script uses the existing memory/ repository and keeps Claude Code
53
+ behavior unchanged.
54
+ EOF
55
+ }
56
+
57
+ die() {
58
+ echo "agent.sh: $*" >&2
59
+ exit 1
60
+ }
61
+
62
+ resolve_base_branch() {
63
+ local base
64
+ if ! base=$(_get_base_branch); then
65
+ echo "agent.sh: could not resolve the configured base branch" >&2
66
+ return 1
67
+ fi
68
+ echo "$base"
69
+ }
70
+
71
+ slugify() {
72
+ echo "$1" \
73
+ | tr '[:upper:]' '[:lower:]' \
74
+ | sed -E 's/[^a-z0-9]+/-/g' \
75
+ | sed -E 's/^-+|-+$//g' \
76
+ | cut -c1-60
77
+ }
78
+
79
+ # Reject control characters that would break YAML frontmatter when interpolated.
80
+ reject_newlines() {
81
+ case "$2" in
82
+ *$'\n'*|*$'\r'*) die "invalid $1: must not contain newlines" ;;
83
+ esac
84
+ }
85
+
86
+ # Emit a value as a JSON-encoded string (valid YAML), so colons, quotes,
87
+ # and other YAML metacharacters in user-supplied values cannot inject keys.
88
+ yaml_str() {
89
+ jq -n --arg v "$1" '$v'
90
+ }
91
+
92
+ ensure_memory() {
93
+ [ -d "$MEMORY_DIR" ] || die "memory directory not found: $MEMORY_DIR"
94
+ }
95
+
96
+ is_memory_git() {
97
+ git -C "$MEMORY_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1
98
+ }
99
+
100
+ memory_branch() {
101
+ git -C "$MEMORY_DIR" branch --show-current 2>/dev/null || echo "main"
102
+ }
103
+
104
+ sync_memory() {
105
+ ensure_memory
106
+ if is_memory_git && git -C "$MEMORY_DIR" remote get-url origin >/dev/null 2>&1; then
107
+ local branch
108
+ branch="$(memory_branch)"
109
+ [ -z "$branch" ] && branch="main"
110
+ git -C "$MEMORY_DIR" pull --rebase origin "$branch" --quiet 2>/dev/null || true
111
+ fi
112
+ }
113
+
114
+ save_memory() {
115
+ local message="$1"
116
+ shift
117
+ local no_push="${NO_PUSH:-0}"
118
+
119
+ ensure_memory
120
+ if ! is_memory_git; then
121
+ return 0
122
+ fi
123
+
124
+ git -C "$MEMORY_DIR" add "$@" >/dev/null 2>&1 || true
125
+ if ! git -C "$MEMORY_DIR" diff --cached --quiet 2>/dev/null; then
126
+ git -C "$MEMORY_DIR" commit -m "$message" --quiet 2>/dev/null || true
127
+ fi
128
+
129
+ [ "$no_push" = "1" ] && return 0
130
+ git -C "$MEMORY_DIR" remote get-url origin >/dev/null 2>&1 || return 0
131
+
132
+ local branch
133
+ branch="$(memory_branch)"
134
+ [ -z "$branch" ] && branch="main"
135
+ for _try in 1 2 3; do
136
+ if git -C "$MEMORY_DIR" pull --rebase origin "$branch" --quiet 2>/dev/null \
137
+ && git -C "$MEMORY_DIR" push origin "$branch" --quiet 2>/dev/null; then
138
+ return 0
139
+ fi
140
+ sleep 1
141
+ done
142
+ die "could not push memory after 3 attempts"
143
+ }
144
+
145
+ person_from_state() {
146
+ if [ -f "$SCRIPT_DIR/.egregore-state.json" ]; then
147
+ jq -r '.github_username // .name // empty' "$SCRIPT_DIR/.egregore-state.json" 2>/dev/null || true
148
+ fi
149
+ }
150
+
151
+ current_author() {
152
+ local author
153
+ author="$(person_from_state)"
154
+ [ -n "$author" ] || author="$(git -C "$SCRIPT_DIR" config user.name 2>/dev/null | awk '{print tolower($1)}')"
155
+ [ -n "$author" ] || author="agent"
156
+ echo "$author"
157
+ }
158
+
159
+ worktree_for_branch() {
160
+ local branch="$1"
161
+ git -C "$MAIN_PROJECT_DIR" worktree list --porcelain 2>/dev/null | awk -v ref="refs/heads/$branch" '
162
+ $1 == "worktree" { path = $2 }
163
+ $1 == "branch" && $2 == ref { print path; exit }
164
+ '
165
+ }
166
+
167
+ base_ref() {
168
+ local base
169
+ base="$(resolve_base_branch)" || return 1
170
+ git -C "$MAIN_PROJECT_DIR" fetch origin "$base" --quiet 2>/dev/null || true
171
+ if git -C "$MAIN_PROJECT_DIR" show-ref --verify --quiet "refs/remotes/origin/$base" 2>/dev/null; then
172
+ echo "origin/$base"
173
+ elif git -C "$MAIN_PROJECT_DIR" show-ref --verify --quiet "refs/heads/$base" 2>/dev/null; then
174
+ echo "$base"
175
+ else
176
+ echo "agent.sh: configured base branch '$base' does not exist locally or on origin" >&2
177
+ return 1
178
+ fi
179
+ }
180
+
181
+ cmd_protocol() {
182
+ cat <<'EOF'
183
+ Egregore portable agent protocol
184
+
185
+ 1. Work inside an Egregore checkout with a linked memory/ repository.
186
+ 2. Run `bin/agent.sh sync` before reading shared state.
187
+ 3. Read `memory/people/` for collaborators.
188
+ 4. Use `bin/agent.sh handoff` to leave structured context for another agent.
189
+ 5. Use `bin/agent.sh ask` and `bin/agent.sh answer` for asynchronous questions.
190
+ 6. Run `bin/agent.sh activity --for <person>` to see pending work.
191
+ 7. Run `bin/agent.sh search "<query>"` to recall shared memory (decisions, handoffs, knowledge) before answering questions about past work.
192
+ 8. Run `bin/agent.sh branch --topic "<what you are working on>"` before code changes when you need a task branch/worktree.
193
+ 9. Run `bin/agent.sh save` and `bin/agent.sh wrap` for portable save/wrap flows.
194
+
195
+ This protocol is runtime-neutral. Claude Code may still use .claude hooks and
196
+ skills, while Codex or another agent can call this script directly.
197
+ EOF
198
+ }
199
+
200
+ cmd_people() {
201
+ ensure_memory
202
+ local people_dir="$MEMORY_DIR/people"
203
+ [ -d "$people_dir" ] || return 0
204
+ local f github display
205
+ for f in "$people_dir"/*.md; do
206
+ [ -f "$f" ] || continue
207
+ github="$(basename "$f" .md)"
208
+ display="$(sed -nE 's/^name:[[:space:]]*(.*)$/\1/p; s/^# (.*)$/\1/p' "$f" | head -1)"
209
+ [ -z "$display" ] && display="$github"
210
+ printf '%s\t%s\n' "$github" "$display"
211
+ done
212
+ }
213
+
214
+ cmd_sync() {
215
+ sync_memory
216
+ echo "synced: $MEMORY_DIR"
217
+ }
218
+
219
+ cmd_activity() {
220
+ ensure_memory
221
+ local for_person=""
222
+ local limit=12
223
+ while [ $# -gt 0 ]; do
224
+ case "$1" in
225
+ --for) for_person="${2:-}"; shift 2 ;;
226
+ --limit) limit="${2:-12}"; shift 2 ;;
227
+ *) die "unknown activity option: $1" ;;
228
+ esac
229
+ done
230
+
231
+ echo "Handoffs"
232
+ local index="$MEMORY_DIR/handoffs/index.md"
233
+ if [ -f "$index" ]; then
234
+ if [ -n "$for_person" ]; then
235
+ grep -i "handoff to ${for_person}" "$index" 2>/dev/null | head -n "$limit" || true
236
+ else
237
+ grep '^- ' "$index" 2>/dev/null | head -n "$limit" || true
238
+ fi
239
+ fi
240
+
241
+ echo
242
+ echo "Questions"
243
+ local qdir="$MEMORY_DIR/knowledge/questions"
244
+ [ -d "$qdir" ] || return 0
245
+ local q from to topic status
246
+ while IFS= read -r q; do
247
+ from="$(sed -nE 's/^from:[[:space:]]*(.*)$/\1/p' "$q" | head -1)"
248
+ to="$(sed -nE 's/^to:[[:space:]]*(.*)$/\1/p' "$q" | head -1)"
249
+ topic="$(sed -nE 's/^topic:[[:space:]]*(.*)$/\1/p' "$q" | head -1)"
250
+ status="$(sed -nE 's/^status:[[:space:]]*(.*)$/\1/p' "$q" | head -1)"
251
+ [ -z "$status" ] && status="pending"
252
+ if [ -n "$for_person" ] && [ "$(echo "$to" | tr '[:upper:]' '[:lower:]')" != "$(echo "$for_person" | tr '[:upper:]' '[:lower:]')" ]; then
253
+ continue
254
+ fi
255
+ printf '%s\n' "- ${from} -> ${to} [${status}]: ${topic} (${q#$MEMORY_DIR/})"
256
+ done < <(find "$qdir" -type f -name '*.md' | sort -r | head -n "$limit")
257
+ }
258
+
259
+ cmd_branch() {
260
+ local topic="" author="" branch_type="" slug="" branch="" wt_path="" existing_wt="" base=""
261
+
262
+ while [ $# -gt 0 ]; do
263
+ case "$1" in
264
+ --topic|--description) topic="${2:-}"; shift 2 ;;
265
+ --from|--author) author="${2:-}"; shift 2 ;;
266
+ --type) branch_type="${2:-}"; shift 2 ;;
267
+ *) die "unknown branch option: $1" ;;
268
+ esac
269
+ done
270
+
271
+ [ -n "$topic" ] || topic="$(date +%Y-%m-%d)"
272
+ [ -n "$author" ] || author="$(current_author)"
273
+
274
+ if [ -z "$branch_type" ]; then
275
+ case "$(echo "$topic" | tr '[:upper:]' '[:lower:]')" in
276
+ *fix*|*bug*|*broken*|*crash*) branch_type="bugfix" ;;
277
+ *feature*|*add*|*implement*|*new*) branch_type="feature" ;;
278
+ *) branch_type="dev" ;;
279
+ esac
280
+ fi
281
+ case "$branch_type" in
282
+ dev|feature|bugfix) ;;
283
+ *) die "invalid --type: $branch_type (valid: dev|feature|bugfix)" ;;
284
+ esac
285
+
286
+ slug="$(slugify "$topic")"
287
+ [ -n "$slug" ] || slug="$(date +%Y-%m-%d)"
288
+ case "$branch_type" in
289
+ dev) branch="dev/${author}/${slug}" ;;
290
+ feature) branch="feature/${slug}" ;;
291
+ bugfix) branch="bugfix/${slug}" ;;
292
+ esac
293
+
294
+ base="$(base_ref)" || die "could not find a safe branch point"
295
+
296
+ if [ -f "$SCRIPT_DIR/.git" ]; then
297
+ if git -C "$SCRIPT_DIR" show-ref --verify --quiet "refs/heads/$branch" 2>/dev/null; then
298
+ git -C "$SCRIPT_DIR" checkout "$branch" --quiet
299
+ else
300
+ git -C "$SCRIPT_DIR" checkout -b "$branch" "$base" --quiet
301
+ fi
302
+ egregore_link_shared_state "$SCRIPT_DIR" "$MAIN_PROJECT_DIR" >/dev/null 2>/dev/null || true
303
+ echo "branch: $branch"
304
+ echo "worktree: $SCRIPT_DIR"
305
+ return 0
306
+ fi
307
+
308
+ existing_wt="$(worktree_for_branch "$branch")"
309
+ if [ -n "$existing_wt" ] && [ -d "$existing_wt" ]; then
310
+ bash "$MAIN_PROJECT_DIR/bin/worktree.sh" setup "$existing_wt" "$MAIN_PROJECT_DIR" >/dev/null 2>/dev/null || true
311
+ echo "branch: $branch"
312
+ echo "worktree: $existing_wt"
313
+ return 0
314
+ fi
315
+
316
+ if ! git -C "$MAIN_PROJECT_DIR" show-ref --verify --quiet "refs/heads/$branch" 2>/dev/null; then
317
+ git -C "$MAIN_PROJECT_DIR" branch "$branch" "$base" >/dev/null 2>&1 || die "failed to create branch $branch"
318
+ fi
319
+
320
+ wt_path="$MAIN_PROJECT_DIR/.claude/worktrees/$slug"
321
+ if [ -e "$wt_path" ]; then
322
+ die "worktree path already exists: $wt_path"
323
+ fi
324
+
325
+ mkdir -p "$MAIN_PROJECT_DIR/.claude/worktrees"
326
+ git -C "$MAIN_PROJECT_DIR" worktree add "$wt_path" "$branch" --quiet 2>/dev/null || die "failed to create worktree at $wt_path"
327
+ bash "$MAIN_PROJECT_DIR/bin/worktree.sh" setup "$wt_path" "$MAIN_PROJECT_DIR" >/dev/null 2>/dev/null || true
328
+
329
+ echo "branch: $branch"
330
+ echo "worktree: $wt_path"
331
+ }
332
+
333
+ ensure_working_branch() {
334
+ local topic="${1:-$(date +%Y-%m-%d)}"
335
+ local author branch slug base
336
+ branch="$(git -C "$SCRIPT_DIR" branch --show-current 2>/dev/null || echo "")"
337
+ case "$branch" in
338
+ dev/*|feature/*|bugfix/*) echo "$branch"; return 0 ;;
339
+ esac
340
+
341
+ author="$(current_author)"
342
+ slug="$(slugify "$topic")"
343
+ [ -n "$slug" ] || slug="$(date +%Y-%m-%d)"
344
+ branch="dev/${author}/${slug}"
345
+ base="$(base_ref)" || die "could not find a safe branch point"
346
+
347
+ if git -C "$SCRIPT_DIR" show-ref --verify --quiet "refs/heads/$branch" 2>/dev/null; then
348
+ git -C "$SCRIPT_DIR" checkout "$branch" --quiet || die "failed to checkout $branch"
349
+ else
350
+ git -C "$SCRIPT_DIR" checkout -b "$branch" "$base" --quiet || die "failed to create $branch"
351
+ fi
352
+ echo "$branch"
353
+ }
354
+
355
+ build_pr_body() {
356
+ # Compliant fallback PR body (spec: .claude/context/pr-format.md) built
357
+ # from the commit log + diff when the calling agent supplies none.
358
+ local topic="${1:-portable save}" base commits stat
359
+ base="$(resolve_base_branch)" || return 1
360
+ commits="$(git -C "$SCRIPT_DIR" log --pretty='- %s' "origin/$base..HEAD" 2>/dev/null | head -8)"
361
+ [ -n "$commits" ] || commits="- (commit list unavailable — see the Files tab)"
362
+ stat="$(git -C "$SCRIPT_DIR" diff --shortstat "origin/$base...HEAD" 2>/dev/null | sed 's/^ *//')"
363
+ printf '## What\n%s\n\n## Why\nShell-agent session save — topic: %s. Body auto-generated by bin/agent.sh; pass --pr-body for a hand-written description.\n\n## Verification\nNot verified — auto-generated skeleton; review the diff before merging.%s\n\n🤖 Saved via bin/agent.sh\n' \
364
+ "$commits" "$topic" "${stat:+ ($stat)}"
365
+ }
366
+
367
+ cmd_save() {
368
+ local message="" topic="" no_pr=0 branch base pushed="false" committed="false" pr_url="" existing_pr="" pr_number="" non_md="" merged="false" pr_body="" pr_body_file=""
369
+
370
+ while [ $# -gt 0 ]; do
371
+ case "$1" in
372
+ --message|-m) message="${2:-}"; shift 2 ;;
373
+ --topic) topic="${2:-}"; shift 2 ;;
374
+ --pr-body) pr_body="${2:-}"; shift 2 ;;
375
+ --pr-body-file) pr_body_file="${2:-}"; shift 2 ;;
376
+ --no-pr) no_pr=1; shift ;;
377
+ *) die "unknown save option: $1" ;;
378
+ esac
379
+ done
380
+
381
+ [ -n "$topic" ] || topic="portable save"
382
+ [ -n "$message" ] || message="Save: $topic"
383
+ base="$(resolve_base_branch)" || die "save stopped before Git changes"
384
+
385
+ if [ -d "$MEMORY_DIR" ]; then
386
+ save_memory "$message" "."
387
+ echo "memory: synced"
388
+ fi
389
+
390
+ branch="$(git -C "$SCRIPT_DIR" branch --show-current 2>/dev/null || echo "")"
391
+ if [ -n "$(git -C "$SCRIPT_DIR" status --porcelain 2>/dev/null | head -1)" ]; then
392
+ branch="$(ensure_working_branch "$topic")"
393
+ git -C "$SCRIPT_DIR" add -A >/dev/null 2>&1 || die "git add failed"
394
+ if ! git -C "$SCRIPT_DIR" diff --cached --quiet 2>/dev/null; then
395
+ git -C "$SCRIPT_DIR" commit -m "$message" --quiet || die "git commit failed"
396
+ committed="true"
397
+ fi
398
+ fi
399
+ [ -n "$branch" ] || branch="$(git -C "$SCRIPT_DIR" branch --show-current 2>/dev/null || echo "")"
400
+
401
+ if [ -n "$branch" ] && git -C "$SCRIPT_DIR" remote get-url origin >/dev/null 2>&1; then
402
+ if git -C "$SCRIPT_DIR" push -u origin "$branch" --quiet 2>/dev/null; then
403
+ pushed="true"
404
+ else
405
+ die "push failed for $branch; commits are safe locally"
406
+ fi
407
+ fi
408
+
409
+ if [ "$no_pr" = "0" ] && [ "$pushed" = "true" ] && command -v gh >/dev/null 2>&1; then
410
+ existing_pr="$(gh pr list --head "$branch" --state open --json url --jq '.[0].url // empty' 2>/dev/null || true)"
411
+ if [ -n "$existing_pr" ]; then
412
+ pr_url="$existing_pr"
413
+ else
414
+ if [ -z "$pr_body" ] && [ -n "$pr_body_file" ] && [ -f "$pr_body_file" ]; then
415
+ pr_body="$(cat "$pr_body_file")"
416
+ fi
417
+ [ -n "$pr_body" ] || pr_body="$(build_pr_body "$topic")"
418
+ pr_url="$(gh pr create --base "$base" --head "$branch" --title "$message" --body "$pr_body" 2>/dev/null || true)"
419
+ fi
420
+ fi
421
+
422
+ # Non-coding saves auto-merge to the configured base (same rule as /save and the
423
+ # handoff helper; policy in bin/lib/noncode.sh). `--auto` needs repo
424
+ # auto-merge + pending required checks; without branch protection it always
425
+ # errors, so fall back to a direct merge.
426
+ if [ -n "$pr_url" ]; then
427
+ # shellcheck source=bin/lib/noncode.sh
428
+ . "$SCRIPT_DIR/bin/lib/noncode.sh" 2>/dev/null || true
429
+ type noncode_blockers >/dev/null 2>&1 || noncode_blockers() {
430
+ git -C "${2:-.}" diff "${1:-origin/$base}...HEAD" --name-only 2>/dev/null | grep -v '\.md$' | head -1 || true
431
+ }
432
+ pr_number="${pr_url##*/}"
433
+ non_code="$(noncode_blockers "origin/$base" "$SCRIPT_DIR")"
434
+ if [ -z "$non_code" ] && [ -n "$pr_number" ]; then
435
+ if gh pr merge "$pr_number" --auto --merge >/dev/null 2>&1 \
436
+ || gh pr merge "$pr_number" --merge >/dev/null 2>&1; then
437
+ merged="true"
438
+ fi
439
+ fi
440
+ fi
441
+
442
+ echo "branch: ${branch:-none}"
443
+ echo "commit: $committed"
444
+ echo "push: $pushed"
445
+ [ -n "$pr_url" ] && echo "pr: $pr_url"
446
+ [ "$merged" = "true" ] && echo "merge: non-coding, merged to $base"
447
+ return 0
448
+ }
449
+
450
+ cmd_wrap() {
451
+ ensure_memory
452
+ local from="" topic="" summary="" body="" body_file="" no_push=0
453
+
454
+ while [ $# -gt 0 ]; do
455
+ case "$1" in
456
+ --from) from="${2:-}"; shift 2 ;;
457
+ --topic) topic="${2:-}"; shift 2 ;;
458
+ --summary) summary="${2:-}"; shift 2 ;;
459
+ --body) body="${2:-}"; shift 2 ;;
460
+ --body-file) body_file="${2:-}"; shift 2 ;;
461
+ --no-push) no_push=1; shift ;;
462
+ *) die "unknown wrap option: $1" ;;
463
+ esac
464
+ done
465
+
466
+ [ -n "$from" ] || from="$(current_author)"
467
+ [ -n "$topic" ] || die "wrap requires --topic"
468
+ [ -n "$summary" ] || die "wrap requires --summary"
469
+ if [ -n "$body_file" ]; then
470
+ [ -f "$body_file" ] || die "body file not found: $body_file"
471
+ body="$(cat "$body_file")"
472
+ elif [ -z "$body" ] && [ ! -t 0 ]; then
473
+ body="$(cat)"
474
+ fi
475
+ [ -n "$body" ] || body="$summary"
476
+
477
+ local tmpd result rel session_id branch
478
+ local capture_args=(--mode personal)
479
+ tmpd="$(mktemp -d -t egregore-agent-wrap-XXXXXX)"
480
+ trap 'rm -rf "$tmpd"' RETURN
481
+ session_id="$(cat "$SCRIPT_DIR/.egregore-session-id" 2>/dev/null || true)"
482
+ branch="$(git -C "$SCRIPT_DIR" branch --show-current 2>/dev/null || true)"
483
+ [ "$no_push" = "1" ] && capture_args+=(--no-push)
484
+
485
+ printf '%s\n' "$body" |
486
+ TMPDIR="$tmpd" bash "$SCRIPT_DIR/bin/capture-run.sh" \
487
+ "${capture_args[@]}" \
488
+ --author "$from" \
489
+ --topic "$topic" \
490
+ --summary "$summary" \
491
+ --session-id "$session_id" \
492
+ --branch "$branch" >/dev/null
493
+
494
+ result="$tmpd/capture-run-result.json"
495
+ rel="$(jq -r '.file // empty' "$result" 2>/dev/null || true)"
496
+ [ -n "$rel" ] || die "wrap was not created"
497
+ echo "wrap: memory/$rel"
498
+
499
+ # Session close: non-coding core-repo changes save + merge themselves
500
+ # (gated inside session-autosave.sh — code is never auto-committed).
501
+ if [ "$no_push" = "0" ]; then
502
+ ( bash "$SCRIPT_DIR/bin/session-autosave.sh" --dir "$SCRIPT_DIR" >/dev/null 2>&1 & ) >/dev/null 2>&1
503
+ fi
504
+ }
505
+
506
+ cmd_handoff() {
507
+ ensure_memory
508
+ local from="" to="" topic="" body="" body_file="" project="" intent="action"
509
+ local no_publish=0 no_notify=0 json=0 body_supplied=0 content_mode="generated"
510
+ NO_PUSH=0
511
+
512
+ while [ $# -gt 0 ]; do
513
+ case "$1" in
514
+ --from) from="${2:-}"; shift 2 ;;
515
+ --to) to="${2:-}"; shift 2 ;;
516
+ --topic) topic="${2:-}"; shift 2 ;;
517
+ --body) body="${2:-}"; shift 2 ;;
518
+ --body-file) body_file="${2:-}"; shift 2 ;;
519
+ --project) project="${2:-}"; shift 2 ;;
520
+ --intent) intent="${2:-}"; shift 2 ;;
521
+ --no-push) NO_PUSH=1; shift ;;
522
+ --no-publish) no_publish=1; shift ;;
523
+ --no-notify) no_notify=1; shift ;;
524
+ --json) json=1; shift ;;
525
+ *) die "unknown handoff option: $1" ;;
526
+ esac
527
+ done
528
+
529
+ [ -z "$from" ] && from="$(person_from_state)"
530
+ [ -n "$from" ] || die "handoff requires --from"
531
+ [ -n "$topic" ] || die "handoff requires --topic"
532
+ case "$intent" in
533
+ action|feedback|fyi) ;;
534
+ *) die "invalid --intent: $intent (action|feedback|fyi)" ;;
535
+ esac
536
+ if [ -n "$body_file" ]; then
537
+ [ -f "$body_file" ] || die "body file not found: $body_file"
538
+ body="$(cat "$body_file")"
539
+ body_supplied=1
540
+ elif [ -z "$body" ] && [ ! -t 0 ]; then
541
+ body="$(cat)"
542
+ [ -n "$body" ] && body_supplied=1
543
+ elif [ -n "$body" ]; then
544
+ body_supplied=1
545
+ fi
546
+ [ "$body_supplied" = "1" ] && content_mode="supplied"
547
+
548
+ local today tmpd result result_out rel stdout_file
549
+ local handoff_args=()
550
+ today="$(date +%Y-%m-%d)"
551
+ tmpd="$(mktemp -d -t egregore-agent-handoff-XXXXXX)"
552
+ trap 'rm -rf "$tmpd"' RETURN
553
+ result_out="$(mktemp -t egregore-agent-handoff-result-XXXXXX.json)"
554
+ stdout_file="$tmpd/stdout"
555
+
556
+ [ -n "$to" ] && handoff_args+=(--recipient "$to")
557
+ [ -n "$project" ] && handoff_args+=(--project "$project")
558
+ handoff_args+=(--intent "$intent" --content-mode "$content_mode")
559
+ [ "$content_mode" = "generated" ] && handoff_args+=(--include-session-artifacts)
560
+ [ "$NO_PUSH" = "1" ] && handoff_args+=(--no-push)
561
+ [ "$no_publish" = "1" ] && handoff_args+=(--no-publish)
562
+ [ "$no_notify" = "1" ] && handoff_args+=(--no-notify)
563
+
564
+ {
565
+ printf '%s\n' '---'
566
+ printf 'capture_schema: egregore-capture/v1\n'
567
+ printf 'capture_mode: addressed\n'
568
+ printf 'kind: addressed\n'
569
+ printf 'content_mode: %s\n' "$content_mode"
570
+ printf 'from: %s\n' "$(yaml_str "$from")"
571
+ [ -n "$to" ] && printf 'addressed_to: %s\n' "$(yaml_str "$to")"
572
+ printf 'date: %s\n' "$today"
573
+ printf 'topic: %s\n' "$(yaml_str "$topic")"
574
+ printf 'intent: %s\n' "$intent"
575
+ [ -n "$project" ] && printf 'project: %s\n' "$(yaml_str "$project")"
576
+ printf '%s\n\n' '---'
577
+ if [ "$content_mode" = "supplied" ]; then
578
+ printf '%s\n' "$body"
579
+ else
580
+ printf '# Handoff: %s\n\n' "$topic"
581
+ printf '## Briefing\n\nNo briefing was supplied.\n\n'
582
+ printf '## Next Steps\n\n1. Continue from this handoff using the shared Egregore memory protocol.\n'
583
+ fi
584
+ } | TMPDIR="$tmpd" bash "$SCRIPT_DIR/bin/capture-run.sh" \
585
+ --mode addressed \
586
+ --author "$from" \
587
+ --topic "$topic" \
588
+ "${handoff_args[@]}" \
589
+ > "$stdout_file"
590
+
591
+ result="$tmpd/handoff-run-result.json"
592
+ rel="$(jq -r '.file // empty' "$result" 2>/dev/null || true)"
593
+ [ -n "$rel" ] || die "handoff was not created"
594
+ cp "$result" "$result_out"
595
+ if [ "$json" = "1" ]; then
596
+ cat "$result_out"
597
+ return 0
598
+ fi
599
+ [ -s "$stdout_file" ] && sed 's/^/status: /' "$stdout_file"
600
+ echo "handoff: memory/$rel"
601
+ echo "result: $result_out"
602
+
603
+ # Parity with the Claude /handoff skill: save core-repo session work in the
604
+ # background (non-coding diffs auto-merge to the configured base; code stays
605
+ # in a PR).
606
+ if [ "$NO_PUSH" = "0" ]; then
607
+ ( bash "$SCRIPT_DIR/bin/handoff-save-egregore.sh" "$from" "$topic" --repo-dir "$SCRIPT_DIR" >/dev/null 2>&1 & ) >/dev/null 2>&1
608
+ fi
609
+ }
610
+
611
+ cmd_ask() {
612
+ ensure_memory
613
+ local from="" to="" topic="" question=""
614
+ local harvest_id="" harvest_session_id="" turn="" question_intent="" context_mode=""
615
+ NO_PUSH=0
616
+
617
+ while [ $# -gt 0 ]; do
618
+ case "$1" in
619
+ --from) from="${2:-}"; shift 2 ;;
620
+ --to) to="${2:-}"; shift 2 ;;
621
+ --topic) topic="${2:-}"; shift 2 ;;
622
+ --question) question="${2:-}"; shift 2 ;;
623
+ --harvest-id) harvest_id="${2:-}"; shift 2 ;;
624
+ --harvest-session-id) harvest_session_id="${2:-}"; shift 2 ;;
625
+ --turn) turn="${2:-}"; shift 2 ;;
626
+ --question-intent) question_intent="${2:-}"; shift 2 ;;
627
+ --context-mode) context_mode="${2:-}"; shift 2 ;;
628
+ --no-push) NO_PUSH=1; shift ;;
629
+ *) die "unknown ask option: $1" ;;
630
+ esac
631
+ done
632
+
633
+ [ -z "$from" ] && from="$(person_from_state)"
634
+ [ -n "$from" ] || die "ask requires --from"
635
+ [ -n "$to" ] || die "ask requires --to"
636
+ [ -n "$topic" ] || die "ask requires --topic"
637
+ [ -n "$question" ] || die "ask requires --question"
638
+
639
+ if [ -n "$context_mode" ]; then
640
+ case "$context_mode" in
641
+ blind|disclosed|comparative) ;;
642
+ *) die "invalid --context-mode: $context_mode (valid: blind|disclosed|comparative)" ;;
643
+ esac
644
+ fi
645
+
646
+ if [ -n "$turn" ]; then
647
+ case "$turn" in
648
+ ''|*[!0-9]*) die "invalid --turn: must be a non-negative integer" ;;
649
+ esac
650
+ fi
651
+
652
+ reject_newlines "--from" "$from"
653
+ reject_newlines "--to" "$to"
654
+ reject_newlines "--topic" "$topic"
655
+ reject_newlines "--question" "$question"
656
+ reject_newlines "--harvest-id" "$harvest_id"
657
+ reject_newlines "--harvest-session-id" "$harvest_session_id"
658
+ reject_newlines "--question-intent" "$question_intent"
659
+
660
+ local now slug rel abs n
661
+ now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
662
+ slug="$(slugify "$topic")"
663
+ mkdir -p "$MEMORY_DIR/knowledge/questions"
664
+ rel="knowledge/questions/$(date +%Y-%m-%d)-${from}-to-${to}-${slug}.md"
665
+ abs="$MEMORY_DIR/$rel"
666
+ n=2
667
+ while [ -e "$abs" ]; do
668
+ rel="knowledge/questions/$(date +%Y-%m-%d)-${from}-to-${to}-${slug}-${n}.md"
669
+ abs="$MEMORY_DIR/$rel"
670
+ n=$((n + 1))
671
+ done
672
+
673
+ {
674
+ echo "---"
675
+ echo "from: $from"
676
+ echo "to: $to"
677
+ echo "topic: $topic"
678
+ echo "status: pending"
679
+ echo "created: $now"
680
+ [ -n "$harvest_id" ] && echo "harvest_id: $(yaml_str "$harvest_id")"
681
+ [ -n "$harvest_session_id" ] && echo "harvest_session_id: $(yaml_str "$harvest_session_id")"
682
+ [ -n "$turn" ] && echo "turn: $turn"
683
+ [ -n "$question_intent" ] && echo "question_intent: $(yaml_str "$question_intent")"
684
+ [ -n "$context_mode" ] && echo "context_mode: $context_mode"
685
+ echo "---"
686
+ echo ""
687
+ echo "## Questions"
688
+ echo ""
689
+ echo "- $question"
690
+ } > "$abs"
691
+
692
+ save_memory "Question: ${topic} (to ${to})" "$rel"
693
+ echo "question: memory/$rel"
694
+ }
695
+
696
+ cmd_answer() {
697
+ ensure_memory
698
+ local from="" question_path="" body=""
699
+ NO_PUSH=0
700
+
701
+ while [ $# -gt 0 ]; do
702
+ case "$1" in
703
+ --from) from="${2:-}"; shift 2 ;;
704
+ --question) question_path="${2:-}"; shift 2 ;;
705
+ --body) body="${2:-}"; shift 2 ;;
706
+ --no-push) NO_PUSH=1; shift ;;
707
+ *) die "unknown answer option: $1" ;;
708
+ esac
709
+ done
710
+
711
+ [ -z "$from" ] && from="$(person_from_state)"
712
+ [ -n "$from" ] || die "answer requires --from"
713
+ [ -n "$question_path" ] || die "answer requires --question"
714
+ [ -n "$body" ] || die "answer requires --body"
715
+
716
+ local abs rel now tmp
717
+ case "$question_path" in
718
+ "$MEMORY_DIR"/*) abs="$question_path" ;;
719
+ memory/*) abs="$MEMORY_DIR/${question_path#memory/}" ;;
720
+ *) abs="$MEMORY_DIR/$question_path" ;;
721
+ esac
722
+ [ -f "$abs" ] || die "question file not found: $question_path"
723
+ rel="${abs#$MEMORY_DIR/}"
724
+ now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
725
+ tmp="$abs.tmp.$$"
726
+
727
+ awk -v from="$from" -v now="$now" '
728
+ BEGIN { in_fm = 0; added = 0; saw_status = 0 }
729
+ NR == 1 && $0 == "---" { in_fm = 1; print; next }
730
+ in_fm && /^status:/ { print "status: answered"; saw_status = 1; next }
731
+ in_fm && $0 == "---" {
732
+ if (!saw_status) print "status: answered"
733
+ if (!added) {
734
+ print "answered_by: " from
735
+ print "answered_at: " now
736
+ added = 1
737
+ }
738
+ in_fm = 0
739
+ print
740
+ next
741
+ }
742
+ { print }
743
+ ' "$abs" > "$tmp" && mv "$tmp" "$abs"
744
+
745
+ {
746
+ printf '\n## Answer\n\n'
747
+ printf '**From**: %s\n' "$from"
748
+ printf '**At**: %s\n\n' "$now"
749
+ printf '%s\n' "$body"
750
+ } >> "$abs"
751
+
752
+ save_memory "Answer question: ${rel}" "$rel"
753
+ echo "answered: memory/$rel"
754
+ }
755
+
756
+ cmd_search() {
757
+ bash "$SCRIPT_DIR/bin/search.sh" query "$@"
758
+ }
759
+
760
+ case "${1:-}" in
761
+ --help|-h|help|"") usage ;;
762
+ protocol) shift; cmd_protocol "$@" ;;
763
+ sync) shift; cmd_sync "$@" ;;
764
+ activity) shift; cmd_activity "$@" ;;
765
+ search) shift; cmd_search "$@" ;;
766
+ people) shift; cmd_people "$@" ;;
767
+ branch) shift; cmd_branch "$@" ;;
768
+ save) shift; cmd_save "$@" ;;
769
+ autosave) shift; bash "$SCRIPT_DIR/bin/autosave.sh" "$@" ;;
770
+ wrap) shift; cmd_wrap "$@" ;;
771
+ handoff) shift; cmd_handoff "$@" ;;
772
+ ask) shift; cmd_ask "$@" ;;
773
+ answer) shift; cmd_answer "$@" ;;
774
+ *) die "unknown command: $1" ;;
775
+ esac