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,1102 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ # Named graph operations — clean interface over raw Cypher.
4
+ # Keeps implementation details out of the TUI.
5
+ #
6
+ # Usage: bash bin/graph-op.sh <operation> [args...]
7
+ #
8
+ # Operations:
9
+ # mark-read <session-id> [user] Mark a handoff as read
10
+ # mark-done <session-id> [user] Mark a handoff as done/resolved
11
+ # mark-expired <session-id> [user] Explicitly expire an open handoff
12
+ # reopen-handoff <session-id> [user] Reopen a terminal handoff
13
+ # answer-question <set-id> Mark a question set as answered
14
+ # resolve-handoffs <user> Resolve completed explicit implementation lineage
15
+ # set-topic <session-id> <topic> [branch]
16
+ # Set topic (and optionally branch) on a Session node
17
+ # record-focus <session-id> <shown-json> <selected> [dismissed-json]
18
+ # Track Focus option selection for adaptive options
19
+ # merge-person <keep-name> <alias-name>
20
+ # Reconcile two Person nodes — projects relationships
21
+ # to keep-name and retains alias-name append-only
22
+ # create-harvest <id> <topic> <intent> <initiator>
23
+ # Create a Harvest node and link to initiator (WAL-backed)
24
+ # create-harvest-session <harvest-id> <session-id> <person-name> [status]
25
+ # Create a HarvestSession linked to a Harvest and Person.
26
+ # status defaults to 'active' (back-compat). Valid values:
27
+ # pending | active | answered | complete | incorporated.
28
+ # Async respondents should be created with 'pending'.
29
+ # record-harvest-turn <session-id> <turn> <question> <intent> [answer] [eval]
30
+ # Record a question-answer turn in a HarvestSession
31
+ # complete-harvest <harvest-id> <artifact-path>
32
+ # Mark harvest complete and link synthesis artifact
33
+ # catalog List bounded read operations and their contracts
34
+ # open-handoffs <user> [limit] Read non-terminal handoffs addressed to a person
35
+ # pending-questions <user> [limit] Read active question sets awaiting a person
36
+ # lineage <topic> [limit] Trace matching sessions, handoffs, implementations,
37
+ # PRs, and canonical evidence files
38
+ # meeting-history <query> [limit]
39
+ # Find a meeting sequence, participants, derived
40
+ # knowledge, and unprojected canonical meeting files
41
+
42
+ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
43
+
44
+ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
45
+ echo "Usage: graph-op.sh <operation> [args...]"
46
+ echo ""
47
+ echo "Named graph operations — clean interface over raw Cypher."
48
+ echo ""
49
+ echo "Operations:"
50
+ echo " mark-read <sid> [user] Mark handoff as read"
51
+ echo " mark-done <sid> [user] Mark handoff as done"
52
+ echo " mark-expired <sid> [user] Explicitly expire an open handoff"
53
+ echo " reopen-handoff <sid> [user] Reopen a done/expired handoff"
54
+ echo " answer-question <qid> Mark question set as answered"
55
+ echo " resolve-handoffs <user> Resolve completed implementation lineage"
56
+ echo " set-topic <sid> <topic> Set topic on a Session node"
57
+ echo " merge-person <keep> <alias> Reconcile a Person as a canonical alias"
58
+ echo " claim-handoff <sid> <ho> Link implementing session to handoff"
59
+ echo " create-pr <sid> <num> <repo> <author> [title]"
60
+ echo " register-artifact <id> <url> <title> <type> <author> <topics> <excerpt>"
61
+ echo " create-harvest / complete-harvest / record-harvest-turn"
62
+ echo " catalog List bounded read operation contracts"
63
+ echo " open-handoffs <user> [n] Read recent non-terminal handoffs"
64
+ echo " pending-questions <user> [n] Read recent active question sets"
65
+ echo " lineage <topic> [n] Trace handoff/implementation/PR evidence"
66
+ echo " meeting-history <query> [n] Find meeting continuity and coverage gaps"
67
+ echo " wal-status Show WAL pending count"
68
+ exit 0
69
+ fi
70
+
71
+ GS="$SCRIPT_DIR/bin/graph.sh"
72
+
73
+ OP="${1:-}"
74
+ shift || true
75
+
76
+ # --- Local mode gate: bail immediately ---
77
+ _MODE=$(jq -r '.mode // "connected"' "$SCRIPT_DIR/egregore.json" 2>/dev/null)
78
+
79
+ # The catalog is static runtime metadata, not a graph read. Keep it available
80
+ # in local mode so every harness can route correctly without opening this file
81
+ # or guessing whether an operation exists.
82
+ if [ "$OP" = "catalog" ]; then
83
+ jq -n --arg mode "$_MODE" '{
84
+ schema: "egregore.graph-operation-catalog/v1",
85
+ definitionVersion: 1,
86
+ mode: $mode,
87
+ operations: [
88
+ {
89
+ name: "open-handoffs",
90
+ intent: "current non-terminal handoffs addressed to a person",
91
+ usage: "bash bin/graph-op.sh open-handoffs <user> [limit]",
92
+ availability: "connected",
93
+ maxResults: 25,
94
+ evidence: "live graph state"
95
+ },
96
+ {
97
+ name: "pending-questions",
98
+ intent: "current active question sets awaiting a person",
99
+ usage: "bash bin/graph-op.sh pending-questions <user> [limit]",
100
+ availability: "connected",
101
+ maxResults: 25,
102
+ evidence: "live graph state"
103
+ },
104
+ {
105
+ name: "lineage",
106
+ intent: "handoff to implementation, branch, PR, and superseding direction",
107
+ usage: "bash bin/graph-op.sh lineage <topic> [limit]",
108
+ availability: "connected",
109
+ maxResults: 15,
110
+ evidence: "graph relationships plus canonical file pointers"
111
+ },
112
+ {
113
+ name: "meeting-history",
114
+ intent: "cross-meeting continuity for a company, person, or topic",
115
+ usage: "bash bin/graph-op.sh meeting-history <query> [limit]",
116
+ availability: "connected",
117
+ maxResults: 15,
118
+ evidence: "projected meeting relationships plus canonical file coverage"
119
+ }
120
+ ]
121
+ }'
122
+ exit 0
123
+ fi
124
+
125
+ if [ "$_MODE" = "local" ]; then
126
+ echo '{"results":[]}'
127
+ exit 0
128
+ fi
129
+
130
+ # Return a bounded set of exact canonical paths containing a literal query.
131
+ # This is intentionally a coverage check around graph results, not a second
132
+ # full-text answer corpus: agents still open only the relevant canonical files.
133
+ memory_file_candidates() {
134
+ local query="$1"
135
+ local limit="$2"
136
+ shift 2
137
+ local filename_hits=""
138
+ local content_hits=""
139
+ local hits=""
140
+
141
+ if command -v rg >/dev/null 2>&1; then
142
+ filename_hits="$(
143
+ rg --files --glob '*.md' --glob '!index.md' "$@" 2>/dev/null \
144
+ | sort \
145
+ | while IFS= read -r path; do
146
+ printf '%s' "$path" | grep -Fqi -- "$query" \
147
+ && printf 'filename\t%s\n' "$path"
148
+ done || true
149
+ )"
150
+ content_hits="$(
151
+ rg -i -l -F --glob '*.md' --glob '!index.md' -- "$query" "$@" 2>/dev/null \
152
+ | sort \
153
+ | while IFS= read -r path; do printf 'content\t%s\n' "$path"; done || true
154
+ )"
155
+ else
156
+ content_hits="$(
157
+ find -L "$@" -type f -name '*.md' -print 2>/dev/null \
158
+ | while IFS= read -r path; do
159
+ [ "$(basename "$path")" = "index.md" ] && continue
160
+ printf '%s' "$path" | grep -Fqi -- "$query" \
161
+ && printf 'filename\t%s\n' "$path"
162
+ grep -Fqi -- "$query" "$path" 2>/dev/null \
163
+ && printf 'content\t%s\n' "$path"
164
+ done || true
165
+ )"
166
+ fi
167
+
168
+ hits="$(
169
+ printf '%s\n%s\n' "$filename_hits" "$content_hits" \
170
+ | awk -F ' ' 'NF >= 2 && !seen[$2]++' \
171
+ | head -n "$limit"
172
+ )"
173
+
174
+ while IFS=$'\t' read -r match_kind path; do
175
+ [ -n "$path" ] || continue
176
+ case "$path" in
177
+ "$SCRIPT_DIR"/memory/*)
178
+ printf 'memory/%s\t%s\n' "${path#"$SCRIPT_DIR"/memory/}" "$match_kind"
179
+ ;;
180
+ esac
181
+ done <<< "$hits" | jq -Rsc '
182
+ split("\n")
183
+ | map(select(length > 0))
184
+ | map(split("\t") | {evidencePath: .[0], matchKind: .[1]})
185
+ '
186
+ }
187
+
188
+ # Reduce natural-language read queries to a small set of discriminative terms.
189
+ # Named reads should tolerate "42CAP Moritz partner call" without requiring that
190
+ # exact phrase to exist in a graph property or canonical Markdown file.
191
+ meaningful_query_terms() {
192
+ jq -Rn --arg query "$1" '
193
+ [
194
+ $query
195
+ | ascii_downcase
196
+ | scan("[a-z0-9][a-z0-9._+-]*")
197
+ | select(length >= 3)
198
+ | select(. as $term |
199
+ [
200
+ "about","across","after","before","between","call","calls",
201
+ "company","conversation","evolved","first","from","history",
202
+ "later","meeting","meetings","month","partner","partners",
203
+ "person","the","topic","transcript","transcripts","what","with"
204
+ ]
205
+ | index($term) == null
206
+ )
207
+ ]
208
+ | unique
209
+ | if length == 0 then [$query | ascii_downcase] else .[0:8] end
210
+ '
211
+ }
212
+
213
+ # Build a bounded union of literal coverage checks for each meaningful term.
214
+ # The result remains a list of exact canonical paths, not a second answer corpus.
215
+ memory_file_candidates_for_terms() {
216
+ local terms_json="$1"
217
+ local limit="$2"
218
+ shift 2
219
+ local combined='[]'
220
+ local term=""
221
+ local term_hits=""
222
+
223
+ while IFS= read -r term; do
224
+ [ -n "$term" ] || continue
225
+ term_hits="$(memory_file_candidates "$term" "$limit" "$@")"
226
+ combined="$(
227
+ jq -n \
228
+ --argjson prior "$combined" \
229
+ --argjson next "$term_hits" \
230
+ --argjson limit "$limit" '
231
+ ($prior + $next)
232
+ | unique_by(.evidencePath)
233
+ | .[0:$limit]
234
+ '
235
+ )"
236
+ done < <(printf '%s' "$terms_json" | jq -r '.[]')
237
+
238
+ printf '%s\n' "$combined"
239
+ }
240
+
241
+ case "$OP" in
242
+
243
+ mark-read)
244
+ SID="${1:?missing session-id}"
245
+ USER="${2:-}"
246
+ bash "$GS" query "
247
+ MATCH (s:Session {id: \$sid})
248
+ OPTIONAL MATCH (s)-[:HANDED_TO]->(recipient:Person)
249
+ WITH s, collect(DISTINCT recipient) AS recipients
250
+ WHERE (\$user = '' OR any(p IN recipients WHERE
251
+ toLower(p.name) = toLower(\$user)
252
+ OR toLower(coalesce(p.github, '')) = toLower(\$user)))
253
+ AND coalesce(s.handoffStatus, 'pending') IN ['pending','read']
254
+ SET s.handoffReadDate = coalesce(s.handoffReadDate, date()),
255
+ s.handoffUpdatedAt = datetime(),
256
+ s.handoffLifecycleVersion = 1
257
+ FOREACH (_ IN CASE WHEN s.handoffIntent = 'fyi' THEN [1] ELSE [] END |
258
+ SET s.handoffStatus = 'done',
259
+ s.handoffDoneAt = coalesce(s.handoffDoneAt, datetime()),
260
+ s.handoffLifecycleReason = 'fyi_read')
261
+ FOREACH (_ IN CASE WHEN coalesce(s.handoffIntent, 'unclassified') <> 'fyi' THEN [1] ELSE [] END |
262
+ SET s.handoffStatus = 'read',
263
+ s.handoffLifecycleReason = 'recipient_read')
264
+ RETURN s.id AS id, s.topic AS topic, s.handoffStatus AS status,
265
+ coalesce(s.handoffIntent, 'unclassified') AS intent
266
+ " "$(jq -n --arg sid "$SID" --arg user "$USER" '{sid: $sid,user: $user}')"
267
+ ;;
268
+
269
+ mark-done)
270
+ SID="${1:?missing session-id}"
271
+ USER="${2:-}"
272
+ bash "$GS" query "
273
+ MATCH (s:Session {id: \$sid})
274
+ OPTIONAL MATCH (s)-[:HANDED_TO]->(recipient:Person)
275
+ WITH s, collect(DISTINCT recipient) AS recipients
276
+ WHERE (\$user = '' OR any(p IN recipients WHERE
277
+ toLower(p.name) = toLower(\$user)
278
+ OR toLower(coalesce(p.github, '')) = toLower(\$user)))
279
+ AND coalesce(s.handoffStatus, 'pending') IN ['pending','read','claimed']
280
+ SET s.handoffStatus = 'done',
281
+ s.handoffDoneAt = datetime(),
282
+ s.handoffUpdatedAt = datetime(),
283
+ s.handoffLifecycleReason = 'explicit_done',
284
+ s.handoffLifecycleVersion = 1
285
+ RETURN s.id AS id, s.topic AS topic, s.handoffStatus AS status
286
+ " "$(jq -n --arg sid "$SID" --arg user "$USER" '{sid: $sid,user: $user}')"
287
+ ;;
288
+
289
+ mark-expired)
290
+ SID="${1:?missing session-id}"
291
+ USER="${2:-}"
292
+ bash "$GS" query "
293
+ MATCH (s:Session {id: \$sid})
294
+ OPTIONAL MATCH (s)-[:HANDED_TO]->(recipient:Person)
295
+ WITH s, collect(DISTINCT recipient) AS recipients
296
+ WHERE (\$user = '' OR any(p IN recipients WHERE
297
+ toLower(p.name) = toLower(\$user)
298
+ OR toLower(coalesce(p.github, '')) = toLower(\$user)))
299
+ AND coalesce(s.handoffStatus, 'pending') IN ['pending','read','claimed']
300
+ SET s.handoffStatus = 'expired',
301
+ s.handoffExpiredAt = datetime(),
302
+ s.handoffUpdatedAt = datetime(),
303
+ s.handoffLifecycleReason = 'explicit_expired',
304
+ s.handoffLifecycleVersion = 1
305
+ RETURN s.id AS id, s.topic AS topic, s.handoffStatus AS status
306
+ " "$(jq -n --arg sid "$SID" --arg user "$USER" '{sid: $sid,user: $user}')"
307
+ ;;
308
+
309
+ reopen-handoff)
310
+ SID="${1:?missing session-id}"
311
+ USER="${2:-}"
312
+ bash "$GS" query "
313
+ MATCH (s:Session {id: \$sid})
314
+ OPTIONAL MATCH (s)-[:HANDED_TO]->(recipient:Person)
315
+ WITH s, collect(DISTINCT recipient) AS recipients
316
+ WHERE (\$user = '' OR any(p IN recipients WHERE
317
+ toLower(p.name) = toLower(\$user)
318
+ OR toLower(coalesce(p.github, '')) = toLower(\$user)))
319
+ AND s.handoffStatus IN ['done','expired','completed']
320
+ SET s.handoffLastDoneAt = s.handoffDoneAt,
321
+ s.handoffLastExpiredAt = s.handoffExpiredAt,
322
+ s.handoffDoneAt = null,
323
+ s.handoffExpiredAt = null,
324
+ s.handoffStatus = 'pending',
325
+ s.handoffReopenedAt = datetime(),
326
+ s.handoffUpdatedAt = datetime(),
327
+ s.handoffLifecycleReason = 'explicit_reopened',
328
+ s.handoffLifecycleVersion = 1
329
+ RETURN s.id AS id, s.topic AS topic, s.handoffStatus AS status
330
+ " "$(jq -n --arg sid "$SID" --arg user "$USER" '{sid: $sid,user: $user}')"
331
+ ;;
332
+
333
+ answer-question)
334
+ QID="${1:?missing question-set-id}"
335
+ bash "$GS" query "
336
+ MATCH (qs:QuestionSet {id: \$qid})
337
+ SET qs.status = 'answered'
338
+ RETURN qs.id AS id, qs.topic AS topic
339
+ " "$(jq -n --arg qid "$QID" '{qid: $qid}')"
340
+ ;;
341
+
342
+ resolve-handoffs)
343
+ USER="${1:?missing username}"
344
+ bash "$GS" query "
345
+ MATCH (s:Session)-[:HANDED_TO]->(p:Person {name: \$user})
346
+ WHERE s.handoffStatus IN ['read','claimed']
347
+ WITH DISTINCT s, p, size([(s)-[:HANDED_TO]->(:Person) | 1]) AS recipientCount
348
+ WHERE recipientCount = 1
349
+ MATCH (implementation:Session)-[:IMPLEMENTS]->(s)
350
+ MATCH (implementation)-[:BY]->(p)
351
+ WHERE implementation.wrappedAt IS NOT NULL
352
+ OR implementation.status IN ['completed','handed_off','wrapped']
353
+ WITH DISTINCT s, implementation
354
+ SET s.handoffStatus = 'done',
355
+ s.handoffDoneAt = coalesce(s.handoffDoneAt, datetime()),
356
+ s.handoffUpdatedAt = datetime(),
357
+ s.handoffLifecycleReason = 'single_recipient_implemented',
358
+ s.handoffLifecycleVersion = 1
359
+ RETURN s.id AS resolved, implementation.id AS implementation
360
+ " "$(jq -n --arg user "$USER" '{user: $user}')"
361
+ ;;
362
+
363
+ set-topic)
364
+ SID="${1:?missing session-id}"
365
+ TOPIC="${2:?missing topic}"
366
+ BRANCH="${3:-}"
367
+ if [ -n "$BRANCH" ]; then
368
+ bash "$GS" query "
369
+ MATCH (s:Session {id: \$sid})
370
+ SET s.topic = \$topic, s.branch = \$branch
371
+ RETURN s.id AS id, s.topic AS topic, s.branch AS branch
372
+ " "$(jq -n --arg sid "$SID" --arg topic "$TOPIC" --arg branch "$BRANCH" '{sid: $sid, topic: $topic, branch: $branch}')"
373
+ else
374
+ bash "$GS" query "
375
+ MATCH (s:Session {id: \$sid})
376
+ SET s.topic = \$topic
377
+ RETURN s.id AS id, s.topic AS topic
378
+ " "$(jq -n --arg sid "$SID" --arg topic "$TOPIC" '{sid: $sid, topic: $topic}')"
379
+ fi
380
+ ;;
381
+
382
+ record-focus)
383
+ SID="${1:?missing session-id}"
384
+ SHOWN="${2:?missing shown options}"
385
+ SELECTED="${3:?missing selected option}"
386
+ DISMISSED="${4:-[]}"
387
+ bash "$GS" query "
388
+ MATCH (s:Session {id: \$sid})
389
+ SET s.focusShown = \$shown,
390
+ s.focusSelected = \$selected,
391
+ s.focusDismissed = \$dismissed
392
+ RETURN s.id AS id
393
+ " "$(jq -n --arg sid "$SID" --argjson shown "$SHOWN" --arg selected "$SELECTED" --argjson dismissed "$DISMISSED" '{sid: $sid, shown: $shown, selected: $selected, dismissed: $dismissed}')"
394
+ ;;
395
+
396
+ merge-person)
397
+ KEEP="${1:?missing keep-name (the Person to keep)}"
398
+ ALIAS="${2:?missing alias-name (the Person to reconcile)}"
399
+ bash "$GS" query "
400
+ MATCH (keep:Person) WHERE (toLower(keep.name) = toLower(\$keep) OR toLower(keep.github) = toLower(\$keep) OR keep.personId = \$keep OR toLower(\$keep) IN [x IN coalesce(keep.githubAliases, []) | toLower(x)] OR toLower(\$keep) IN [x IN coalesce(keep.emails, []) | toLower(x)])
401
+ AND NOT coalesce(keep.kind, '') IN ['external', 'identity_alias']
402
+ AND coalesce(keep.status, 'active') = 'active' AND keep.ingestRef IS NULL
403
+ MATCH (alias:Person) WHERE (toLower(alias.name) = toLower(\$alias) OR toLower(alias.github) = toLower(\$alias) OR alias.personId = \$alias OR toLower(\$alias) IN [x IN coalesce(alias.githubAliases, []) | toLower(x)] OR toLower(\$alias) IN [x IN coalesce(alias.emails, []) | toLower(x)])
404
+ AND NOT coalesce(alias.kind, '') IN ['external', 'identity_alias']
405
+ AND coalesce(alias.status, 'active') = 'active' AND alias.ingestRef IS NULL
406
+ WITH keep, alias WHERE keep <> alias
407
+ AND (
408
+ keep.githubId IS NULL OR alias.githubId IS NULL
409
+ OR toString(keep.githubId) = toString(alias.githubId)
410
+ )
411
+ AND NOT (
412
+ coalesce(keep.personId, '') STARTS WITH 'github:'
413
+ AND coalesce(alias.personId, '') STARTS WITH 'github:'
414
+ AND keep.personId <> alias.personId
415
+ )
416
+ SET keep.previousNames = reduce(
417
+ acc = coalesce(keep.previousNames, []),
418
+ value IN coalesce(alias.previousNames, []) + [alias.name, alias.displayName] |
419
+ CASE WHEN value IS NULL OR toLower(value) = toLower(keep.name) OR value IN acc
420
+ THEN acc ELSE acc + value END
421
+ ),
422
+ keep.githubAliases = reduce(
423
+ acc = coalesce(keep.githubAliases, []),
424
+ value IN coalesce(alias.githubAliases, []) + [alias.github] |
425
+ CASE WHEN value IS NULL OR toLower(value) = toLower(keep.github) OR value IN acc
426
+ THEN acc ELSE acc + value END
427
+ ),
428
+ keep.emails = reduce(
429
+ acc = coalesce(keep.emails, []),
430
+ value IN coalesce(alias.emails, []) + [alias.email] |
431
+ CASE WHEN value IS NULL OR value IN acc THEN acc ELSE acc + value END
432
+ ),
433
+ keep.joined = coalesce(alias.joined, keep.joined),
434
+ keep.invited = coalesce(keep.invited, alias.invited),
435
+ keep.invitedBy = coalesce(keep.invitedBy, alias.invitedBy),
436
+ keep.role = coalesce(keep.role, alias.role),
437
+ keep.domain = coalesce(keep.domain, alias.domain),
438
+ keep.focus = coalesce(keep.focus, alias.focus),
439
+ keep.workStyle = coalesce(keep.workStyle, alias.workStyle),
440
+ keep.telegramId = coalesce(keep.telegramId, alias.telegramId),
441
+ keep.lastBrief = coalesce(keep.lastBrief, alias.lastBrief),
442
+ keep.lastBriefDate = coalesce(keep.lastBriefDate, alias.lastBriefDate),
443
+ keep.lastRecommendations = coalesce(keep.lastRecommendations, alias.lastRecommendations)
444
+ WITH keep, alias
445
+ OPTIONAL MATCH (s1:Session)-[:BY]->(alias)
446
+ FOREACH (_ IN CASE WHEN s1 IS NOT NULL THEN [1] ELSE [] END | MERGE (s1)-[:BY]->(keep))
447
+ WITH keep, alias
448
+ OPTIONAL MATCH (s2:Session)-[:HANDED_TO]->(alias)
449
+ FOREACH (_ IN CASE WHEN s2 IS NOT NULL THEN [1] ELSE [] END | MERGE (s2)-[:HANDED_TO]->(keep))
450
+ WITH keep, alias
451
+ OPTIONAL MATCH (m:Meeting)-[:INVOLVES]->(alias)
452
+ FOREACH (_ IN CASE WHEN m IS NOT NULL THEN [1] ELSE [] END | MERGE (m)-[:INVOLVES]->(keep))
453
+ WITH keep, alias
454
+ OPTIONAL MATCH (a:Artifact)-[:CONTRIBUTED_BY]->(alias)
455
+ FOREACH (_ IN CASE WHEN a IS NOT NULL THEN [1] ELSE [] END | MERGE (a)-[:CONTRIBUTED_BY]->(keep))
456
+ WITH keep, alias
457
+ OPTIONAL MATCH (i:Interview)-[:CONDUCTED_BY]->(alias)
458
+ FOREACH (_ IN CASE WHEN i IS NOT NULL THEN [1] ELSE [] END | MERGE (i)-[:CONDUCTED_BY]->(keep))
459
+ WITH keep, alias
460
+ OPTIONAL MATCH (t:Todo)-[:BY]->(alias)
461
+ FOREACH (_ IN CASE WHEN t IS NOT NULL THEN [1] ELSE [] END | MERGE (t)-[:BY]->(keep))
462
+ WITH keep, alias
463
+ OPTIONAL MATCH (mentionedArtifact:Artifact)-[:MENTIONS]->(alias)
464
+ FOREACH (_ IN CASE WHEN mentionedArtifact IS NOT NULL THEN [1] ELSE [] END | MERGE (mentionedArtifact)-[:MENTIONS]->(keep))
465
+ WITH keep, alias
466
+ OPTIONAL MATCH (mentionedTodo:Todo)-[:MENTIONS]->(alias)
467
+ FOREACH (_ IN CASE WHEN mentionedTodo IS NOT NULL THEN [1] ELSE [] END | MERGE (mentionedTodo)-[:MENTIONS]->(keep))
468
+ WITH keep, alias
469
+ OPTIONAL MATCH (q:Quest)-[:STARTED_BY]->(alias)
470
+ FOREACH (_ IN CASE WHEN q IS NOT NULL THEN [1] ELSE [] END | MERGE (q)-[:STARTED_BY]->(keep))
471
+ WITH keep, alias
472
+ OPTIONAL MATCH (askedBy:QuestionSet)-[:ASKED_BY]->(alias)
473
+ FOREACH (_ IN CASE WHEN askedBy IS NOT NULL THEN [1] ELSE [] END | MERGE (askedBy)-[:ASKED_BY]->(keep))
474
+ WITH keep, alias
475
+ OPTIONAL MATCH (askedTo:QuestionSet)-[:ASKED_TO]->(alias)
476
+ FOREACH (_ IN CASE WHEN askedTo IS NOT NULL THEN [1] ELSE [] END | MERGE (askedTo)-[:ASKED_TO]->(keep))
477
+ WITH keep, alias
478
+ OPTIONAL MATCH (tu:TelegramUser)-[:IDENTIFIES]->(alias)
479
+ FOREACH (_ IN CASE WHEN tu IS NOT NULL THEN [1] ELSE [] END | MERGE (tu)-[:IDENTIFIES]->(keep))
480
+ WITH keep, alias
481
+ OPTIONAL MATCH (reported:Issue)-[:REPORTED_BY]->(alias)
482
+ FOREACH (_ IN CASE WHEN reported IS NOT NULL THEN [1] ELSE [] END | MERGE (reported)-[:REPORTED_BY]->(keep))
483
+ WITH keep, alias
484
+ OPTIONAL MATCH (initiated:Harvest)-[:INITIATED_BY]->(alias)
485
+ FOREACH (_ IN CASE WHEN initiated IS NOT NULL THEN [1] ELSE [] END | MERGE (initiated)-[:INITIATED_BY]->(keep))
486
+ WITH keep, alias
487
+ OPTIONAL MATCH (withPerson:HarvestSession)-[:WITH]->(alias)
488
+ FOREACH (_ IN CASE WHEN withPerson IS NOT NULL THEN [1] ELSE [] END | MERGE (withPerson)-[:WITH]->(keep))
489
+ WITH keep, alias
490
+ OPTIONAL MATCH (alias)-[:SENT_EMISSARY]->(emissary:Emissary)
491
+ FOREACH (_ IN CASE WHEN emissary IS NOT NULL THEN [1] ELSE [] END | MERGE (keep)-[:SENT_EMISSARY]->(emissary))
492
+ WITH keep, alias
493
+ OPTIONAL MATCH (alias)-[:MEMBER_OF]->(o:Org)
494
+ FOREACH (_ IN CASE WHEN o IS NOT NULL THEN [1] ELSE [] END | MERGE (keep)-[:MEMBER_OF]->(o))
495
+ WITH keep, alias
496
+ MERGE (alias)-[:ALIAS_OF]->(keep)
497
+ SET alias.kind = 'identity_alias',
498
+ alias.canonicalPersonId = keep.personId,
499
+ alias.identityStatus = 'alias'
500
+ RETURN keep.name AS name, keep.github AS github, keep.previousNames AS aliases
501
+ " "$(jq -n --arg keep "$KEEP" --arg alias "$ALIAS" '{keep: $keep, alias: $alias}')"
502
+ ;;
503
+
504
+ claim-handoff)
505
+ IMPL_SID="${1:?missing implementing-session-id}"
506
+ HO_SID="${2:?missing handoff-session-id}"
507
+ CYPHER="
508
+ MATCH (impl:Session {id: \$implSid}), (ho:Session {id: \$hoSid})
509
+ MATCH (impl)-[:BY]->(claimant:Person)
510
+ MATCH (ho)-[:HANDED_TO]->(claimant)
511
+ WHERE ho.handoffStatus IN ['pending','read','claimed']
512
+ MERGE (impl)-[:IMPLEMENTS]->(ho)
513
+ WITH impl, ho, size([(ho)-[:HANDED_TO]->(:Person) | 1]) AS recipientCount
514
+ FOREACH (_ IN CASE WHEN recipientCount = 1 THEN [1] ELSE [] END |
515
+ SET ho.handoffStatus = 'claimed',
516
+ ho.handoffClaimedAt = coalesce(ho.handoffClaimedAt, datetime()),
517
+ ho.handoffUpdatedAt = datetime(),
518
+ ho.handoffLifecycleReason = 'implementation_claimed',
519
+ ho.handoffLifecycleVersion = 1)
520
+ RETURN impl.id AS implementor, ho.id AS handoff, ho.topic AS topic,
521
+ ho.handoffStatus AS status, recipientCount
522
+ "
523
+ PARAMS="$(jq -n --arg implSid "$IMPL_SID" --arg hoSid "$HO_SID" '{implSid: $implSid, hoSid: $hoSid}')"
524
+ bash "$SCRIPT_DIR/bin/graph-wal.sh" append "$CYPHER" "$PARAMS" 2>/dev/null || true
525
+ bash "$GS" query "$CYPHER" "$PARAMS"
526
+ ;;
527
+
528
+ claim-handoff-nudge)
529
+ SID="${1:?missing handoff-session-id}"
530
+ bash "$GS" query "
531
+ MATCH (s:Session {id: \$sid})-[:HANDED_TO]->(recipient:Person)
532
+ WHERE coalesce(s.handoffStatus, 'pending') IN ['pending','read','claimed']
533
+ AND coalesce(s.handoffLifecycleVersion, 0) >= 1
534
+ AND coalesce(s.handoffIntent, 'unclassified') IN ['action','feedback','fyi']
535
+ AND s.handoffNudgedAt IS NULL
536
+ AND (s.handoffNudgeClaimedAt IS NULL
537
+ OR s.handoffNudgeClaimedAt < datetime() - duration('PT1H'))
538
+ WITH s, collect(DISTINCT recipient.name) AS recipients
539
+ WHERE size(recipients) = 1
540
+ SET s.handoffNudgeClaimedAt = datetime(),
541
+ s.handoffNudgeStatus = 'claimed'
542
+ RETURN s.id AS id, s.topic AS topic, recipients
543
+ " "$(jq -n --arg sid "$SID" '{sid:$sid}')"
544
+ ;;
545
+
546
+ mark-handoff-nudged)
547
+ SID="${1:?missing handoff-session-id}"
548
+ bash "$GS" query "
549
+ MATCH (s:Session {id: \$sid})
550
+ WHERE s.handoffNudgeStatus = 'claimed' AND s.handoffNudgedAt IS NULL
551
+ SET s.handoffNudgedAt = datetime(),
552
+ s.handoffNudgeStatus = 'sent',
553
+ s.handoffUpdatedAt = datetime(),
554
+ s.handoffLifecycleReason = 'recipient_nudged',
555
+ s.handoffLifecycleVersion = 1
556
+ RETURN s.id AS id, s.handoffNudgeStatus AS status
557
+ " "$(jq -n --arg sid "$SID" '{sid:$sid}')"
558
+ ;;
559
+
560
+ release-handoff-nudge)
561
+ SID="${1:?missing handoff-session-id}"
562
+ bash "$GS" query "
563
+ MATCH (s:Session {id: \$sid})
564
+ WHERE s.handoffNudgeStatus = 'claimed' AND s.handoffNudgedAt IS NULL
565
+ SET s.handoffNudgeClaimedAt = null,
566
+ s.handoffNudgeStatus = 'failed',
567
+ s.handoffNudgeFailedAt = datetime()
568
+ RETURN s.id AS id, s.handoffNudgeStatus AS status
569
+ " "$(jq -n --arg sid "$SID" '{sid:$sid}')"
570
+ ;;
571
+
572
+ create-pr)
573
+ SID="${1:?missing session-id}"
574
+ PR_NUM="${2:?missing pr-number}"
575
+ REPO="${3:?missing repo}"
576
+ AUTHOR_GH="${4:?missing author-github}"
577
+ TITLE="${5:-}"
578
+ CYPHER="
579
+ MERGE (pr:PR {number: toInteger(\$num), repo: \$repo})
580
+ ON CREATE SET pr.author = \$author, pr.status = 'open',
581
+ pr.createdAt = datetime(), pr.title = \$title
582
+ WITH pr
583
+ OPTIONAL MATCH (s:Session {id: \$sid})
584
+ FOREACH (_ IN CASE WHEN s IS NOT NULL THEN [1] ELSE [] END |
585
+ MERGE (s)-[:PRODUCED]->(pr))
586
+ RETURN pr.number AS number, pr.repo AS repo
587
+ "
588
+ PARAMS="$(jq -n --arg sid "$SID" --argjson num "$PR_NUM" --arg repo "$REPO" --arg author "$AUTHOR_GH" --arg title "$TITLE" '{sid: $sid, num: $num, repo: $repo, author: $author, title: $title}')"
589
+ bash "$SCRIPT_DIR/bin/graph-wal.sh" append "$CYPHER" "$PARAMS" 2>/dev/null || true
590
+ bash "$GS" query "$CYPHER" "$PARAMS"
591
+ ;;
592
+
593
+ register-artifact)
594
+ # Index a published (hosted) artifact as an Artifact node so the finder can
595
+ # retrieve it from the graph in connected mode — by content (title/topics/
596
+ # excerpt CONTAINS) and by relationship (who made it, which quest). The node
597
+ # id is the registry FILENAME STEM (date-author-slug), matching sync-graph.sh
598
+ # so a later /save sync upserts the same node instead of duplicating it.
599
+ # args: <id> <url> <title> <type> <author-handle> <topics-csv> <excerpt>
600
+ A_ID="${1:?missing id}"
601
+ A_URL="${2:?missing url}"
602
+ A_TITLE="${3:-}"
603
+ A_TYPE="${4:-artifact}"
604
+ A_AUTHOR="$(printf '%s' "${5:-}" | tr '[:upper:]' '[:lower:]' | awk '{print $1}')"
605
+ A_TOPICS_JSON="$(printf '%s' "${6:-}" | jq -Rc 'split(",") | map(gsub("^\\s+|\\s+$";"")) | map(select(length > 0))' 2>/dev/null || echo '[]')"
606
+ A_EXCERPT="$(printf '%s' "${7:-}" | tr '\n' ' ' | cut -c1-2000)"
607
+ CYPHER="
608
+ MERGE (a:Artifact {id: \$id})
609
+ ON CREATE SET a.created = datetime()
610
+ SET a.title = \$title, a.type = \$type, a.url = \$url,
611
+ a.topics = \$topics, a.excerpt = \$excerpt, a.published = true
612
+ WITH a
613
+ OPTIONAL MATCH (p:Person)
614
+ WHERE \$author <> '' AND (toLower(p.name) = \$author OR p.github = \$author)
615
+ FOREACH (_ IN CASE WHEN p IS NOT NULL THEN [1] ELSE [] END |
616
+ MERGE (a)-[:CONTRIBUTED_BY]->(p))
617
+ RETURN a.id AS id
618
+ "
619
+ PARAMS="$(jq -n --arg id "$A_ID" --arg url "$A_URL" --arg title "$A_TITLE" --arg type "$A_TYPE" \
620
+ --arg author "$A_AUTHOR" --argjson topics "$A_TOPICS_JSON" --arg excerpt "$A_EXCERPT" \
621
+ '{id: $id, url: $url, title: $title, type: $type, author: $author, topics: $topics, excerpt: $excerpt}')"
622
+ bash "$SCRIPT_DIR/bin/graph-wal.sh" append "$CYPHER" "$PARAMS" 2>/dev/null || true
623
+ bash "$GS" query "$CYPHER" "$PARAMS"
624
+ ;;
625
+
626
+ check-implements)
627
+ SID="${1:?missing session-id}"
628
+ bash "$GS" query "
629
+ MATCH (impl:Session {id: \$sid})-[:IMPLEMENTS]->(ho:Session)-[:BY]->(author:Person)
630
+ RETURN ho.id AS handoffId, ho.topic AS topic, author.name AS author,
631
+ author.github AS authorGithub
632
+ " "$(jq -n --arg sid "$SID" '{sid: $sid}')"
633
+ ;;
634
+
635
+ update-pr)
636
+ PR_NUM="${1:?missing pr-number}"
637
+ REPO="${2:?missing repo}"
638
+ STATUS="${3:?missing status}"
639
+ MERGED_AT="${4:-}"
640
+ if [ -n "$MERGED_AT" ]; then
641
+ bash "$GS" query "
642
+ MATCH (pr:PR {number: toInteger(\$num), repo: \$repo})
643
+ SET pr.status = \$status, pr.mergedAt = datetime(\$mergedAt)
644
+ RETURN pr.number AS number, pr.status AS status
645
+ " "$(jq -n --argjson num "$PR_NUM" --arg repo "$REPO" --arg status "$STATUS" --arg mergedAt "$MERGED_AT" '{num: $num, repo: $repo, status: $status, mergedAt: $mergedAt}')"
646
+ else
647
+ bash "$GS" query "
648
+ MATCH (pr:PR {number: toInteger(\$num), repo: \$repo})
649
+ SET pr.status = \$status
650
+ RETURN pr.number AS number, pr.status AS status
651
+ " "$(jq -n --argjson num "$PR_NUM" --arg repo "$REPO" --arg status "$STATUS" '{num: $num, repo: $repo, status: $status}')"
652
+ fi
653
+ ;;
654
+
655
+ my-merged-prs)
656
+ AUTHOR_GH="${1:?missing author-github}"
657
+ SINCE="${2:-}"
658
+ if [ -n "$SINCE" ]; then
659
+ bash "$GS" query "
660
+ MATCH (pr:PR {author: \$author})
661
+ WHERE pr.status = 'merged' AND pr.mergedAt >= datetime(\$since)
662
+ RETURN pr.number AS number, pr.repo AS repo, pr.title AS title,
663
+ toString(pr.mergedAt) AS mergedAt
664
+ ORDER BY pr.mergedAt DESC LIMIT 10
665
+ " "$(jq -n --arg author "$AUTHOR_GH" --arg since "${SINCE}T00:00:00Z" '{author: $author, since: $since}')"
666
+ else
667
+ bash "$GS" query "
668
+ MATCH (pr:PR {author: \$author})
669
+ WHERE pr.status = 'merged'
670
+ RETURN pr.number AS number, pr.repo AS repo, pr.title AS title,
671
+ toString(pr.mergedAt) AS mergedAt
672
+ ORDER BY pr.mergedAt DESC LIMIT 5
673
+ " "$(jq -n --arg author "$AUTHOR_GH" '{author: $author}')"
674
+ fi
675
+ ;;
676
+
677
+ my-implemented-handoffs)
678
+ AUTHOR_NAME="${1:?missing author-name}"
679
+ SINCE="${2:-}"
680
+ if [ -n "$SINCE" ]; then
681
+ bash "$GS" query "
682
+ MATCH (impl:Session)-[:IMPLEMENTS]->(ho:Session)-[:BY]->(author:Person)
683
+ WHERE toLower(author.name) = toLower(\$author)
684
+ AND impl.wrappedAt >= datetime(\$since)
685
+ MATCH (impl)-[:BY]->(implementor:Person)
686
+ RETURN ho.topic AS handoffTopic, implementor.name AS implementedBy,
687
+ toString(impl.wrappedAt) AS completedAt, impl.summary AS summary
688
+ ORDER BY impl.wrappedAt DESC LIMIT 10
689
+ " "$(jq -n --arg author "$AUTHOR_NAME" --arg since "${SINCE}T00:00:00Z" '{author: $author, since: $since}')"
690
+ else
691
+ bash "$GS" query "
692
+ MATCH (impl:Session)-[:IMPLEMENTS]->(ho:Session)-[:BY]->(author:Person)
693
+ WHERE toLower(author.name) = toLower(\$author)
694
+ MATCH (impl)-[:BY]->(implementor:Person)
695
+ RETURN ho.topic AS handoffTopic, implementor.name AS implementedBy,
696
+ toString(impl.wrappedAt) AS completedAt, impl.summary AS summary
697
+ ORDER BY impl.wrappedAt DESC LIMIT 5
698
+ " "$(jq -n --arg author "$AUTHOR_NAME" '{author: $author}')"
699
+ fi
700
+ ;;
701
+
702
+ open-handoffs)
703
+ USER="${1:?missing username}"
704
+ LIMIT="${2:-10}"
705
+ if ! [[ "$LIMIT" =~ ^[0-9]+$ ]] || [ "$LIMIT" -lt 1 ] || [ "$LIMIT" -gt 25 ]; then
706
+ echo '{"error":"limit must be between 1 and 25"}'
707
+ exit 2
708
+ fi
709
+ bash "$GS" query "
710
+ MATCH (s:Session)-[:HANDED_TO]->(p:Person)
711
+ WHERE NOT coalesce(p.kind, '') IN ['identity_alias']
712
+ AND (
713
+ toLower(coalesce(p.name, '')) = toLower(\$user)
714
+ OR toLower(coalesce(p.github, '')) = toLower(\$user)
715
+ OR toLower(coalesce(p.githubUsername, '')) = toLower(\$user)
716
+ OR any(alias IN coalesce(p.previousNames, [])
717
+ WHERE toLower(alias) = toLower(\$user))
718
+ )
719
+ AND coalesce(s.handoffStatus, 'pending')
720
+ IN ['pending','unread','read','claimed']
721
+ WITH DISTINCT s
722
+ WITH s, head([(s)-[:BY]->(author:Person)
723
+ WHERE NOT coalesce(author.kind, '') IN ['identity_alias'] | author.name]) AS authorName,
724
+ CASE WHEN s.date IS NULL THEN null
725
+ ELSE duration.inDays(date(s.date), date()).days END AS ageDays
726
+ RETURN s.id AS id,
727
+ coalesce(s.topic, s.summary, s.branch) AS topic,
728
+ coalesce(authorName, s.author, 'unknown') AS author,
729
+ toString(s.date) AS date,
730
+ coalesce(s.handoffStatus, 'pending') AS status,
731
+ coalesce(s.handoffIntent, 'unclassified') AS intent,
732
+ ageDays,
733
+ CASE WHEN ageDays IS NULL THEN 'unknown'
734
+ WHEN ageDays <= 14 THEN 'current'
735
+ WHEN ageDays <= 30 THEN 'aging'
736
+ ELSE 'stale' END AS freshness,
737
+ 1 AS definitionVersion
738
+ ORDER BY s.date DESC, s.id
739
+ LIMIT \$limit
740
+ " "$(jq -n --arg user "$USER" --argjson limit "$LIMIT" '{user: $user, limit: $limit}')"
741
+ ;;
742
+
743
+ pending-questions)
744
+ USER="${1:?missing username}"
745
+ LIMIT="${2:-10}"
746
+ if ! [[ "$LIMIT" =~ ^[0-9]+$ ]] || [ "$LIMIT" -lt 1 ] || [ "$LIMIT" -gt 25 ]; then
747
+ echo '{"error":"limit must be between 1 and 25"}'
748
+ exit 2
749
+ fi
750
+ bash "$GS" query "
751
+ MATCH (qs:QuestionSet)-[:ASKED_TO]->(p:Person)
752
+ WHERE (
753
+ toLower(coalesce(p.name, '')) = toLower(\$user)
754
+ OR toLower(coalesce(p.github, '')) = toLower(\$user)
755
+ OR toLower(coalesce(p.githubUsername, '')) = toLower(\$user)
756
+ OR any(alias IN coalesce(p.previousNames, [])
757
+ WHERE toLower(alias) = toLower(\$user))
758
+ )
759
+ AND coalesce(qs.status, 'pending') IN ['pending','active']
760
+ WITH DISTINCT qs
761
+ OPTIONAL MATCH (q:Question)-[:PART_OF]->(qs)
762
+ OPTIONAL MATCH (qs)-[:ASKED_BY]->(asker:Person)
763
+ WITH qs, asker, count(DISTINCT q) AS questions,
764
+ CASE WHEN qs.created IS NULL THEN null
765
+ ELSE duration.inDays(date(datetime(qs.created)), date()).days END AS ageDays
766
+ RETURN qs.id AS id,
767
+ qs.topic AS topic,
768
+ coalesce(asker.name, qs.askedBy, qs.from, 'unknown') AS askedBy,
769
+ questions,
770
+ toString(qs.created) AS created,
771
+ ageDays,
772
+ CASE WHEN ageDays IS NULL THEN 'unknown'
773
+ WHEN ageDays <= 14 THEN 'current'
774
+ WHEN ageDays <= 30 THEN 'aging'
775
+ ELSE 'stale-open' END AS freshness,
776
+ 1 AS definitionVersion
777
+ ORDER BY qs.created DESC, qs.id
778
+ LIMIT \$limit
779
+ " "$(jq -n --arg user "$USER" --argjson limit "$LIMIT" '{user: $user, limit: $limit}')"
780
+ ;;
781
+
782
+ lineage)
783
+ QUERY="${1:?missing topic}"
784
+ LIMIT="${2:-10}"
785
+ if ! [[ "$LIMIT" =~ ^[0-9]+$ ]] || [ "$LIMIT" -lt 1 ] || [ "$LIMIT" -gt 15 ]; then
786
+ echo '{"error":"limit must be between 1 and 15"}'
787
+ exit 2
788
+ fi
789
+ GRAPH_RAW="$(bash "$GS" query "
790
+ MATCH (s:Session)
791
+ WHERE toLower(coalesce(s.topic, '')) CONTAINS toLower(\$query)
792
+ OR toLower(coalesce(s.summary, '')) CONTAINS toLower(\$query)
793
+ OR toLower(coalesce(s.branch, '')) CONTAINS toLower(\$query)
794
+ OPTIONAL MATCH (s)-[:BY]->(author:Person)
795
+ OPTIONAL MATCH (s)-[:HANDED_TO]->(recipient:Person)
796
+ OPTIONAL MATCH (implementation:Session)-[:IMPLEMENTS]->(s)
797
+ OPTIONAL MATCH (implementation)-[:BY]->(implementer:Person)
798
+ OPTIONAL MATCH (s)-[:IMPLEMENTS]->(prior:Session)
799
+ OPTIONAL MATCH (prior)-[:BY]->(priorAuthor:Person)
800
+ OPTIONAL MATCH (s)-[:PRODUCED]->(pr:PR)
801
+ OPTIONAL MATCH (implementation)-[:PRODUCED]->(implementationPr:PR)
802
+ WITH s, author,
803
+ collect(DISTINCT coalesce(recipient.name, recipient.github)) AS recipients,
804
+ collect(DISTINCT CASE WHEN implementation IS NULL THEN null ELSE {
805
+ id: implementation.id,
806
+ topic: implementation.topic,
807
+ summary: implementation.summary,
808
+ branch: implementation.branch,
809
+ date: toString(implementation.date),
810
+ evidencePath: CASE
811
+ WHEN implementation.filePath IS NULL THEN null
812
+ WHEN implementation.filePath STARTS WITH 'memory/' THEN implementation.filePath
813
+ ELSE 'memory/' + implementation.filePath END,
814
+ implementedBy: coalesce(implementer.name, implementer.github)
815
+ } END) AS implementations,
816
+ collect(DISTINCT CASE WHEN prior IS NULL THEN null ELSE {
817
+ id: prior.id,
818
+ topic: prior.topic,
819
+ summary: prior.summary,
820
+ branch: prior.branch,
821
+ date: toString(prior.date),
822
+ evidencePath: CASE
823
+ WHEN prior.filePath IS NULL THEN null
824
+ WHEN prior.filePath STARTS WITH 'memory/' THEN prior.filePath
825
+ ELSE 'memory/' + prior.filePath END,
826
+ author: coalesce(priorAuthor.name, priorAuthor.github)
827
+ } END) AS implements,
828
+ collect(DISTINCT CASE WHEN pr IS NULL THEN null ELSE {
829
+ number: pr.number, repo: pr.repo, title: pr.title,
830
+ status: pr.status, mergedAt: toString(pr.mergedAt)
831
+ } END)
832
+ + collect(DISTINCT CASE WHEN implementationPr IS NULL THEN null ELSE {
833
+ number: implementationPr.number, repo: implementationPr.repo,
834
+ title: implementationPr.title, status: implementationPr.status,
835
+ mergedAt: toString(implementationPr.mergedAt)
836
+ } END) AS prs
837
+ RETURN s.id AS sessionId,
838
+ s.topic AS topic,
839
+ s.summary AS summary,
840
+ s.branch AS branch,
841
+ toString(s.date) AS date,
842
+ CASE WHEN s.filePath IS NULL THEN null
843
+ WHEN s.filePath STARTS WITH 'memory/' THEN s.filePath
844
+ ELSE 'memory/' + s.filePath END AS evidencePath,
845
+ coalesce(author.name, author.github, s.author, 'unknown') AS author,
846
+ recipients[0..20] AS recipients,
847
+ implementations[0..10] AS implementations,
848
+ implements[0..10] AS implements,
849
+ prs[0..10] AS prs
850
+ ORDER BY s.date DESC, s.id
851
+ LIMIT \$limit
852
+ " "$(jq -n --arg query "$QUERY" --argjson limit "$LIMIT" '{query:$query,limit:$limit}')")"
853
+ FILES_RAW="$(memory_file_candidates "$QUERY" "$LIMIT" \
854
+ "$SCRIPT_DIR/memory/handoffs" \
855
+ "$SCRIPT_DIR/memory/wraps" \
856
+ "$SCRIPT_DIR/memory/sessions")"
857
+ jq -n \
858
+ --arg query "$QUERY" \
859
+ --argjson limit "$LIMIT" \
860
+ --argjson graph "$GRAPH_RAW" \
861
+ --argjson files "$FILES_RAW" '
862
+ ($graph.values // [] | map({
863
+ sessionId: .[0], topic: .[1], summary: .[2], branch: .[3],
864
+ date: .[4], evidencePath: .[5], author: .[6],
865
+ recipients: (.[7] // []), implementations: (.[8] // []),
866
+ implements: (.[9] // []), prs: (.[10] // [])
867
+ })) as $matches
868
+ | ($matches | map(.evidencePath) | map(select(. != null)) | unique) as $graphPaths
869
+ | ($files | map(.evidencePath) | unique) as $filePaths
870
+ | ($filePaths - $graphPaths) as $unprojected
871
+ | {
872
+ schema: "egregore.graph-read/v1",
873
+ operation: "lineage",
874
+ definitionVersion: 1,
875
+ query: $query,
876
+ limit: $limit,
877
+ coverage:
878
+ (if ($graphPaths | length) == 0 then
879
+ (if ($filePaths | length) == 0 then "none" else "filesystem-only" end)
880
+ elif ($unprojected | length) > 0 then "partial"
881
+ else "complete" end),
882
+ graphMatches: $matches,
883
+ fileCandidates:
884
+ ($files | map(
885
+ . as $candidate
886
+ | . + {
887
+ projection:
888
+ (if ($graphPaths | index($candidate.evidencePath)) != null
889
+ then "projected" else "unprojected" end)
890
+ }
891
+ )),
892
+ unprojectedPaths: $unprojected
893
+ }
894
+ '
895
+ ;;
896
+
897
+ meeting-history)
898
+ QUERY="${1:?missing company, person, or topic}"
899
+ LIMIT="${2:-10}"
900
+ if ! [[ "$LIMIT" =~ ^[0-9]+$ ]] || [ "$LIMIT" -lt 1 ] || [ "$LIMIT" -gt 15 ]; then
901
+ echo '{"error":"limit must be between 1 and 15"}'
902
+ exit 2
903
+ fi
904
+ QUERY_TERMS="$(meaningful_query_terms "$QUERY")"
905
+ GRAPH_RAW="$(bash "$GS" query "
906
+ MATCH (m:Meeting)
907
+ OPTIONAL MATCH (m)-[:INVOLVES|CONDUCTED_BY]->(participant:Person)
908
+ WITH m, collect(DISTINCT participant) AS participants
909
+ WITH m, participants,
910
+ [term IN \$terms WHERE
911
+ toLower(coalesce(m.title, '')) CONTAINS term
912
+ OR toLower(coalesce(m.name, '')) CONTAINS term
913
+ OR toLower(coalesce(m.id, '')) CONTAINS term
914
+ OR any(p IN participants WHERE
915
+ toLower(coalesce(p.name, '')) CONTAINS term
916
+ OR toLower(coalesce(p.github, '')) CONTAINS term)
917
+ ] AS matchedTerms
918
+ WHERE size(matchedTerms) > 0
919
+ OPTIONAL MATCH (artifact:Artifact)-[:FROM_MEETING]->(m)
920
+ RETURN m.id AS meetingId,
921
+ coalesce(m.title, m.name, m.id) AS title,
922
+ toString(m.date) AS date,
923
+ CASE WHEN m.filePath IS NULL THEN null
924
+ WHEN m.filePath STARTS WITH 'memory/' THEN m.filePath
925
+ ELSE 'memory/' + m.filePath END AS evidencePath,
926
+ ([p IN participants | coalesce(p.name, p.github)])[0..20] AS participants,
927
+ (collect(DISTINCT CASE WHEN artifact IS NULL THEN null ELSE {
928
+ id: artifact.id,
929
+ type: artifact.type,
930
+ title: artifact.title,
931
+ summary: artifact.summary,
932
+ evidencePath: CASE
933
+ WHEN artifact.filePath IS NULL THEN null
934
+ WHEN artifact.filePath STARTS WITH 'memory/' THEN artifact.filePath
935
+ ELSE 'memory/' + artifact.filePath END
936
+ } END))[0..10] AS derivedKnowledge,
937
+ coalesce(m.verificationStatus, 'legacy') AS verificationStatus,
938
+ m.ingestManifestId AS ingestManifestId,
939
+ matchedTerms,
940
+ size(matchedTerms) AS matchScore
941
+ ORDER BY matchScore DESC, date ASC, meetingId
942
+ LIMIT \$limit
943
+ " "$(jq -n --arg query "$QUERY" --argjson terms "$QUERY_TERMS" --argjson limit "$LIMIT" \
944
+ '{query:$query,terms:$terms,limit:$limit}')")"
945
+ FILES_RAW="$(memory_file_candidates_for_terms \
946
+ "$QUERY_TERMS" "$LIMIT" "$SCRIPT_DIR/memory/meetings")"
947
+ jq -n \
948
+ --arg query "$QUERY" \
949
+ --argjson queryTerms "$QUERY_TERMS" \
950
+ --argjson limit "$LIMIT" \
951
+ --argjson graph "$GRAPH_RAW" \
952
+ --argjson files "$FILES_RAW" '
953
+ ($graph.values // [] | map({
954
+ meetingId: .[0], title: .[1], date: .[2], evidencePath: .[3],
955
+ participants: (.[4] // []), derivedKnowledge: (.[5] // []),
956
+ verificationStatus: .[6], ingestManifestId: .[7],
957
+ matchedTerms: (.[8] // []), matchScore: (.[9] // 0)
958
+ })) as $meetings
959
+ | ($meetings | map(.evidencePath) | map(select(. != null)) | unique) as $graphPaths
960
+ | ($files | map(.evidencePath) | unique) as $filePaths
961
+ | ($filePaths - $graphPaths) as $unprojected
962
+ | {
963
+ schema: "egregore.graph-read/v1",
964
+ operation: "meeting-history",
965
+ definitionVersion: 1,
966
+ query: $query,
967
+ queryTerms: $queryTerms,
968
+ limit: $limit,
969
+ coverage:
970
+ (if ($graphPaths | length) == 0 then
971
+ (if ($filePaths | length) == 0 then "none" else "filesystem-only" end)
972
+ elif ($unprojected | length) > 0 then "partial"
973
+ else "complete" end),
974
+ graphMeetings: $meetings,
975
+ fileCandidates:
976
+ ($files | map(
977
+ . as $candidate
978
+ | . + {
979
+ projection:
980
+ (if ($graphPaths | index($candidate.evidencePath)) != null
981
+ then "projected" else "unprojected" end)
982
+ }
983
+ )),
984
+ unprojectedPaths: $unprojected
985
+ }
986
+ '
987
+ ;;
988
+
989
+ wal-status)
990
+ bash "$SCRIPT_DIR/bin/graph-wal.sh" status
991
+ ;;
992
+
993
+ create-harvest)
994
+ HID="${1:?missing harvest-id}"
995
+ TOPIC="${2:?missing topic}"
996
+ INTENT="${3:?missing intent}"
997
+ INITIATOR="${4:?missing initiator}"
998
+ CYPHER="
999
+ MERGE (h:Harvest {id: \$hid})
1000
+ ON CREATE SET h.topic = \$topic, h.intent = \$intent,
1001
+ h.status = 'active', h.created = datetime()
1002
+ WITH h
1003
+ MATCH (p:Person) WHERE toLower(p.name) = toLower(\$initiator) OR p.github = \$initiator
1004
+ MERGE (h)-[:INITIATED_BY]->(p)
1005
+ RETURN h.id AS id, h.topic AS topic, h.status AS status
1006
+ "
1007
+ PARAMS="$(jq -n --arg hid "$HID" --arg topic "$TOPIC" --arg intent "$INTENT" --arg initiator "$INITIATOR" '{hid: $hid, topic: $topic, intent: $intent, initiator: $initiator}')"
1008
+ bash "$SCRIPT_DIR/bin/graph-wal.sh" append "$CYPHER" "$PARAMS" 2>/dev/null || true
1009
+ bash "$GS" query "$CYPHER" "$PARAMS"
1010
+ ;;
1011
+
1012
+ create-harvest-session)
1013
+ HID="${1:?missing harvest-id}"
1014
+ HSID="${2:?missing session-id}"
1015
+ PERSON="${3:?missing person-name}"
1016
+ STATUS="${4:-active}"
1017
+ case "$STATUS" in
1018
+ pending|active|answered|complete|incorporated) ;;
1019
+ *) jq -n --arg s "$STATUS" '{error:("invalid status: "+$s),valid:["pending","active","answered","complete","incorporated"]}' >&2; exit 2 ;;
1020
+ esac
1021
+ CYPHER="
1022
+ MATCH (h:Harvest {id: \$hid})
1023
+ MERGE (hs:HarvestSession {id: \$hsid})
1024
+ ON CREATE SET hs.status = \$status, hs.created = datetime()
1025
+ MERGE (h)-[:HAS_SESSION]->(hs)
1026
+ WITH hs
1027
+ MATCH (p:Person) WHERE toLower(p.name) = toLower(\$person) OR p.github = \$person
1028
+ MERGE (hs)-[:WITH]->(p)
1029
+ RETURN hs.id AS id, hs.status AS status
1030
+ "
1031
+ PARAMS="$(jq -n --arg hid "$HID" --arg hsid "$HSID" --arg person "$PERSON" --arg status "$STATUS" '{hid: $hid, hsid: $hsid, person: $person, status: $status}')"
1032
+ bash "$SCRIPT_DIR/bin/graph-wal.sh" append "$CYPHER" "$PARAMS" 2>/dev/null || true
1033
+ OUT="$(bash "$GS" query "$CYPHER" "$PARAMS")"
1034
+ if [ "$(echo "$OUT" | jq -r '.results | length' 2>/dev/null)" = "0" ]; then
1035
+ jq -n --arg hid "$HID" --arg hsid "$HSID" --arg person "$PERSON" \
1036
+ '{error:"create-harvest-session: harvest or person not found",hid:$hid,hsid:$hsid,person:$person}' >&2
1037
+ exit 3
1038
+ fi
1039
+ echo "$OUT"
1040
+ ;;
1041
+
1042
+ record-harvest-turn)
1043
+ HSID="${1:?missing session-id}"
1044
+ TURN="${2:?missing turn-number}"
1045
+ QUESTION="${3:?missing question}"
1046
+ QINTENT="${4:?missing question-intent}"
1047
+ ANSWER="${5:-}"
1048
+ EVAL="${6:-}"
1049
+ TURN_ID="${HSID}-turn-${TURN}"
1050
+ PARAMS=$(jq -n \
1051
+ --arg hsid "$HSID" --arg tid "$TURN_ID" --arg turn "$TURN" \
1052
+ --arg question "$QUESTION" --arg qintent "$QINTENT" \
1053
+ --arg answer "$ANSWER" --arg eval "$EVAL" \
1054
+ '{hsid:$hsid, tid:$tid, turn:$turn, question:$question, qintent:$qintent, answer:$answer, eval:$eval}')
1055
+ if [ -n "$ANSWER" ]; then
1056
+ bash "$GS" query "
1057
+ MATCH (hs:HarvestSession {id: \$hsid})
1058
+ MERGE (t:HarvestTurn {id: \$tid})
1059
+ ON CREATE SET t.turnNumber = toInteger(\$turn), t.question = \$question,
1060
+ t.questionIntent = \$qintent, t.created = datetime()
1061
+ SET t.answer = \$answer, t.answeredAt = datetime()
1062
+ SET t.evaluation = CASE WHEN \$eval <> '' THEN \$eval ELSE t.evaluation END
1063
+ MERGE (hs)-[:HAS_TURN]->(t)
1064
+ RETURN t.id AS id, t.turnNumber AS turnNumber
1065
+ " "$PARAMS"
1066
+ else
1067
+ bash "$GS" query "
1068
+ MATCH (hs:HarvestSession {id: \$hsid})
1069
+ MERGE (t:HarvestTurn {id: \$tid})
1070
+ ON CREATE SET t.turnNumber = toInteger(\$turn), t.question = \$question,
1071
+ t.questionIntent = \$qintent, t.created = datetime()
1072
+ MERGE (hs)-[:HAS_TURN]->(t)
1073
+ RETURN t.id AS id, t.turnNumber AS turnNumber
1074
+ " "$PARAMS"
1075
+ fi
1076
+ ;;
1077
+
1078
+ complete-harvest)
1079
+ HID="${1:?missing harvest-id}"
1080
+ ARTIFACT_PATH="${2:?missing artifact-path}"
1081
+ bash "$GS" query "
1082
+ MATCH (h:Harvest {id: \$hid})
1083
+ SET h.status = 'complete', h.completedAt = datetime(), h.synthesisPath = \$path
1084
+ WITH h
1085
+ OPTIONAL MATCH (h)-[:HAS_SESSION]->(hs:HarvestSession)
1086
+ WHERE hs.status <> 'complete'
1087
+ SET hs.status = 'complete', hs.completedAt = datetime()
1088
+ WITH h
1089
+ MERGE (a:Artifact {id: \$hid + '-synthesis'})
1090
+ ON CREATE SET a.type = 'harvest', a.title = h.topic, a.filePath = \$path,
1091
+ a.created = datetime(), a.topics = coalesce([h.topic], [])
1092
+ MERGE (h)-[:PRODUCED]->(a)
1093
+ RETURN h.id AS id, h.status AS status, a.id AS artifact
1094
+ " "$(jq -n --arg hid "$HID" --arg path "$ARTIFACT_PATH" '{hid: $hid, path: $path}')"
1095
+ ;;
1096
+
1097
+ *)
1098
+ echo '{"error":"unknown operation: '"$OP"'","operations":["catalog","mark-read","mark-done","mark-expired","reopen-handoff","answer-question","resolve-handoffs","set-topic","record-focus","merge-person","claim-handoff","claim-handoff-nudge","mark-handoff-nudged","release-handoff-nudge","check-implements","create-pr","update-pr","my-merged-prs","my-implemented-handoffs","open-handoffs","pending-questions","lineage","meeting-history","wal-status","create-harvest","create-harvest-session","record-harvest-turn","complete-harvest"]}'
1099
+ exit 1
1100
+ ;;
1101
+
1102
+ esac