create-egregore 0.19.8 → 0.19.11

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 (91) hide show
  1. package/lib/local.js +11 -1
  2. package/lib/setup.js +33 -0
  3. package/package.json +1 -1
  4. package/runtime/codex/.codex/hooks/branch-guard.js +242 -34
  5. package/runtime/codex/.codex/skills/activity/SKILL.md +18 -6
  6. package/runtime/codex/.codex/skills/deep-reflect/SKILL.md +90 -55
  7. package/runtime/codex/.codex/skills/deploy-bridge/SKILL.md +25 -0
  8. package/runtime/codex/.codex/skills/handoff/SKILL.md +6 -2
  9. package/runtime/codex/.codex/skills/save/SKILL.md +21 -3
  10. package/runtime/codex/.codex/skills/scroll/SKILL.md +54 -3
  11. package/runtime/codex/.codex/skills/task/SKILL.md +25 -0
  12. package/runtime/codex/.codex/skills/tui-design/SKILL.md +1 -1
  13. package/runtime/codex/.codex/skills/wrap/SKILL.md +4 -1
  14. package/runtime/codex/.codex/spec-manifest.json +15 -3
  15. package/runtime/codex/AGENTS.md +45 -9
  16. package/runtime/codex/bin/activity-data.sh +1 -1
  17. package/runtime/codex/bin/agent.sh +66 -23
  18. package/runtime/codex/bin/attendant.sh +525 -0
  19. package/runtime/codex/bin/capture-reconcile.sh +67 -0
  20. package/runtime/codex/bin/capture-run.sh +341 -0
  21. package/runtime/codex/bin/codex-render-spec.mjs +15 -4
  22. package/runtime/codex/bin/codex-session-start.sh +45 -3
  23. package/runtime/codex/bin/codex-skill-render.mjs +23 -4
  24. package/runtime/codex/bin/graph-op.sh +719 -49
  25. package/runtime/codex/bin/graph-wal.sh +3 -2
  26. package/runtime/codex/bin/handoff-run.sh +72 -0
  27. package/runtime/codex/bin/handoff-save-egregore.sh +51 -11
  28. package/runtime/codex/bin/index-handoff.sh +26 -17
  29. package/runtime/codex/bin/ingest-graph.sh +80 -0
  30. package/runtime/codex/bin/ingest-surface.html +429 -0
  31. package/runtime/codex/bin/ingest.py +614 -0
  32. package/runtime/codex/bin/ingest.sh +231 -0
  33. package/runtime/codex/bin/ingest_graph.py +776 -0
  34. package/runtime/codex/bin/ingest_surface.py +464 -0
  35. package/runtime/codex/bin/lib/git-sync.sh +2 -6
  36. package/runtime/codex/bin/lib/greeting.sh +157 -22
  37. package/runtime/codex/bin/lib/identity.sh +50 -29
  38. package/runtime/codex/bin/lib/noncode.sh +31 -0
  39. package/runtime/codex/bin/person.py +610 -0
  40. package/runtime/codex/bin/person.sh +448 -0
  41. package/runtime/codex/bin/session-autosave.sh +184 -0
  42. package/runtime/codex/bin/session-log.sh +199 -0
  43. package/runtime/codex/bin/settings.sh +1 -1
  44. package/runtime/codex/manifest.json +19 -1
  45. package/runtime/pi/.codex/hooks/branch-guard.js +242 -34
  46. package/runtime/pi/.codex/skills/activity/SKILL.md +18 -6
  47. package/runtime/pi/.codex/skills/deep-reflect/SKILL.md +90 -55
  48. package/runtime/pi/.codex/skills/deploy-bridge/SKILL.md +25 -0
  49. package/runtime/pi/.codex/skills/handoff/SKILL.md +6 -2
  50. package/runtime/pi/.codex/skills/save/SKILL.md +21 -3
  51. package/runtime/pi/.codex/skills/scroll/SKILL.md +54 -3
  52. package/runtime/pi/.codex/skills/task/SKILL.md +25 -0
  53. package/runtime/pi/.codex/skills/tui-design/SKILL.md +1 -1
  54. package/runtime/pi/.codex/skills/wrap/SKILL.md +4 -1
  55. package/runtime/pi/.codex/spec-manifest.json +15 -3
  56. package/runtime/pi/.pi/APPEND_SYSTEM.md +41 -5
  57. package/runtime/pi/.pi/extensions/egregore.ts +98 -8
  58. package/runtime/pi/.pi/spec-manifest.json +2 -2
  59. package/runtime/pi/AGENTS.md +45 -9
  60. package/runtime/pi/bin/activity-data.sh +1 -1
  61. package/runtime/pi/bin/agent.sh +66 -23
  62. package/runtime/pi/bin/attendant.sh +525 -0
  63. package/runtime/pi/bin/capture-reconcile.sh +67 -0
  64. package/runtime/pi/bin/capture-run.sh +341 -0
  65. package/runtime/pi/bin/codex-render-spec.mjs +15 -4
  66. package/runtime/pi/bin/codex-session-start.sh +45 -3
  67. package/runtime/pi/bin/codex-skill-render.mjs +23 -4
  68. package/runtime/pi/bin/graph-op.sh +719 -49
  69. package/runtime/pi/bin/graph-wal.sh +3 -2
  70. package/runtime/pi/bin/handoff-run.sh +72 -0
  71. package/runtime/pi/bin/handoff-save-egregore.sh +51 -11
  72. package/runtime/pi/bin/index-handoff.sh +26 -17
  73. package/runtime/pi/bin/ingest-graph.sh +80 -0
  74. package/runtime/pi/bin/ingest-surface.html +429 -0
  75. package/runtime/pi/bin/ingest.py +614 -0
  76. package/runtime/pi/bin/ingest.sh +231 -0
  77. package/runtime/pi/bin/ingest_graph.py +776 -0
  78. package/runtime/pi/bin/ingest_surface.py +464 -0
  79. package/runtime/pi/bin/lib/git-sync.sh +2 -6
  80. package/runtime/pi/bin/lib/greeting.sh +157 -22
  81. package/runtime/pi/bin/lib/identity.sh +50 -29
  82. package/runtime/pi/bin/lib/noncode.sh +31 -0
  83. package/runtime/pi/bin/person.py +610 -0
  84. package/runtime/pi/bin/person.sh +448 -0
  85. package/runtime/pi/bin/pi-branch-policy.mjs +164 -4
  86. package/runtime/pi/bin/pi-product-workflows.mjs +31 -2
  87. package/runtime/pi/bin/publish-artifact.sh +14 -0
  88. package/runtime/pi/bin/session-autosave.sh +184 -0
  89. package/runtime/pi/bin/session-log.sh +199 -0
  90. package/runtime/pi/bin/settings.sh +274 -0
  91. package/runtime/pi/manifest.json +20 -1
@@ -6,17 +6,19 @@ set -euo pipefail
6
6
  # Usage: bash bin/graph-op.sh <operation> [args...]
7
7
  #
8
8
  # Operations:
9
- # mark-read <session-id> Mark a handoff as read
10
- # mark-done <session-id> Mark a handoff as done/resolved
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
11
13
  # answer-question <set-id> Mark a question set as answered
12
- # resolve-handoffs <user> Auto-resolve read handoffs with later sessions
14
+ # resolve-handoffs <user> Resolve completed explicit implementation lineage
13
15
  # set-topic <session-id> <topic> [branch]
14
16
  # Set topic (and optionally branch) on a Session node
15
17
  # record-focus <session-id> <shown-json> <selected> [dismissed-json]
16
18
  # Track Focus option selection for adaptive options
17
- # merge-person <keep-name> <remove-name>
18
- # Merge two Person nodes — transfers relationships from
19
- # remove-name to keep-name, stores remove-name as alias
19
+ # merge-person <keep-name> <alias-name>
20
+ # Reconcile two Person nodes — projects relationships
21
+ # to keep-name and retains alias-name append-only
20
22
  # create-harvest <id> <topic> <intent> <initiator>
21
23
  # Create a Harvest node and link to initiator (WAL-backed)
22
24
  # create-harvest-session <harvest-id> <session-id> <person-name> [status]
@@ -28,6 +30,14 @@ set -euo pipefail
28
30
  # Record a question-answer turn in a HarvestSession
29
31
  # complete-harvest <harvest-id> <artifact-path>
30
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
31
41
 
32
42
  SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
33
43
 
@@ -37,50 +47,287 @@ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
37
47
  echo "Named graph operations — clean interface over raw Cypher."
38
48
  echo ""
39
49
  echo "Operations:"
40
- echo " mark-read <sid> Mark handoff as read"
41
- echo " mark-done <sid> Mark handoff as done"
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"
42
54
  echo " answer-question <qid> Mark question set as answered"
43
- echo " resolve-handoffs <user> Auto-resolve read handoffs"
55
+ echo " resolve-handoffs <user> Resolve completed implementation lineage"
44
56
  echo " set-topic <sid> <topic> Set topic on a Session node"
45
- echo " merge-person <keep> <rm> Merge two Person nodes"
57
+ echo " merge-person <keep> <alias> Reconcile a Person as a canonical alias"
46
58
  echo " claim-handoff <sid> <ho> Link implementing session to handoff"
47
59
  echo " create-pr <sid> <num> <repo> <author> [title]"
48
60
  echo " register-artifact <id> <url> <title> <type> <author> <topics> <excerpt>"
49
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"
50
67
  echo " wal-status Show WAL pending count"
51
68
  exit 0
52
69
  fi
53
70
 
71
+ GS="$SCRIPT_DIR/bin/graph.sh"
72
+
73
+ OP="${1:-}"
74
+ shift || true
75
+
54
76
  # --- Local mode gate: bail immediately ---
55
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
+
56
125
  if [ "$_MODE" = "local" ]; then
57
126
  echo '{"results":[]}'
58
127
  exit 0
59
128
  fi
60
129
 
61
- GS="$SCRIPT_DIR/bin/graph.sh"
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=""
62
140
 
63
- OP="${1:-}"
64
- shift || true
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
+ }
65
240
 
66
241
  case "$OP" in
67
242
 
68
243
  mark-read)
69
244
  SID="${1:?missing session-id}"
245
+ USER="${2:-}"
70
246
  bash "$GS" query "
71
247
  MATCH (s:Session {id: \$sid})
72
- SET s.handoffStatus = 'read', s.handoffReadDate = date()
73
- RETURN s.id AS id, s.topic AS topic
74
- " "$(jq -n --arg sid "$SID" '{sid: $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}')"
75
267
  ;;
76
268
 
77
269
  mark-done)
78
270
  SID="${1:?missing session-id}"
271
+ USER="${2:-}"
79
272
  bash "$GS" query "
80
273
  MATCH (s:Session {id: \$sid})
81
- SET s.handoffStatus = 'done'
82
- RETURN s.id AS id, s.topic AS topic
83
- " "$(jq -n --arg sid "$SID" '{sid: $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}')"
84
331
  ;;
85
332
 
86
333
  answer-question)
@@ -96,13 +343,20 @@ case "$OP" in
96
343
  USER="${1:?missing username}"
97
344
  bash "$GS" query "
98
345
  MATCH (s:Session)-[:HANDED_TO]->(p:Person {name: \$user})
99
- WHERE s.handoffStatus = 'read'
100
- WITH s, p, coalesce(s.handoffReadDate, s.date) AS sinceDate
101
- MATCH (later:Session)-[:BY]->(p)
102
- WHERE later.date > sinceDate
103
- WITH s, count(later) AS laterSessions WHERE laterSessions > 0
104
- SET s.handoffStatus = 'done'
105
- RETURN s.id AS resolved
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
106
360
  " "$(jq -n --arg user "$USER" '{user: $user}')"
107
361
  ;;
108
362
 
@@ -141,34 +395,110 @@ case "$OP" in
141
395
 
142
396
  merge-person)
143
397
  KEEP="${1:?missing keep-name (the Person to keep)}"
144
- REMOVE="${2:?missing remove-name (the Person to absorb)}"
398
+ ALIAS="${2:?missing alias-name (the Person to reconcile)}"
145
399
  bash "$GS" query "
146
- MATCH (keep:Person) WHERE toLower(keep.name) = toLower(\$keep) OR keep.github = \$keep
147
- MATCH (remove:Person) WHERE toLower(remove.name) = toLower(\$remove) OR remove.github = \$remove
148
- WITH keep, remove WHERE keep <> remove
149
- SET keep.previousNames = coalesce(keep.previousNames, []) + remove.name
150
- WITH keep, remove
151
- OPTIONAL MATCH (s1)-[:BY]->(remove)
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)
152
446
  FOREACH (_ IN CASE WHEN s1 IS NOT NULL THEN [1] ELSE [] END | MERGE (s1)-[:BY]->(keep))
153
- WITH keep, remove
154
- OPTIONAL MATCH (s2)-[:HANDED_TO]->(remove)
447
+ WITH keep, alias
448
+ OPTIONAL MATCH (s2:Session)-[:HANDED_TO]->(alias)
155
449
  FOREACH (_ IN CASE WHEN s2 IS NOT NULL THEN [1] ELSE [] END | MERGE (s2)-[:HANDED_TO]->(keep))
156
- WITH keep, remove
157
- OPTIONAL MATCH (m)-[:INVOLVES]->(remove)
450
+ WITH keep, alias
451
+ OPTIONAL MATCH (m:Meeting)-[:INVOLVES]->(alias)
158
452
  FOREACH (_ IN CASE WHEN m IS NOT NULL THEN [1] ELSE [] END | MERGE (m)-[:INVOLVES]->(keep))
159
- WITH keep, remove
160
- OPTIONAL MATCH (a)-[:CONTRIBUTED_BY]->(remove)
453
+ WITH keep, alias
454
+ OPTIONAL MATCH (a:Artifact)-[:CONTRIBUTED_BY]->(alias)
161
455
  FOREACH (_ IN CASE WHEN a IS NOT NULL THEN [1] ELSE [] END | MERGE (a)-[:CONTRIBUTED_BY]->(keep))
162
- WITH keep, remove
163
- OPTIONAL MATCH (i)-[:CONDUCTED_BY]->(remove)
456
+ WITH keep, alias
457
+ OPTIONAL MATCH (i:Interview)-[:CONDUCTED_BY]->(alias)
164
458
  FOREACH (_ IN CASE WHEN i IS NOT NULL THEN [1] ELSE [] END | MERGE (i)-[:CONDUCTED_BY]->(keep))
165
- WITH keep, remove
166
- OPTIONAL MATCH (remove)-[:MEMBER_OF]->(o:Org)
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)
167
494
  FOREACH (_ IN CASE WHEN o IS NOT NULL THEN [1] ELSE [] END | MERGE (keep)-[:MEMBER_OF]->(o))
168
- WITH keep, remove
169
- DETACH DELETE remove
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'
170
500
  RETURN keep.name AS name, keep.github AS github, keep.previousNames AS aliases
171
- " "$(jq -n --arg keep "$KEEP" --arg remove "$REMOVE" '{keep: $keep, remove: $remove}')"
501
+ " "$(jq -n --arg keep "$KEEP" --arg alias "$ALIAS" '{keep: $keep, alias: $alias}')"
172
502
  ;;
173
503
 
174
504
  claim-handoff)
@@ -176,15 +506,69 @@ case "$OP" in
176
506
  HO_SID="${2:?missing handoff-session-id}"
177
507
  CYPHER="
178
508
  MATCH (impl:Session {id: \$implSid}), (ho:Session {id: \$hoSid})
179
- WHERE ho.handoffStatus IS NOT NULL
509
+ MATCH (impl)-[:BY]->(claimant:Person)
510
+ MATCH (ho)-[:HANDED_TO]->(claimant)
511
+ WHERE ho.handoffStatus IN ['pending','read','claimed']
180
512
  MERGE (impl)-[:IMPLEMENTS]->(ho)
181
- RETURN impl.id AS implementor, ho.id AS handoff, ho.topic AS topic
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
182
522
  "
183
523
  PARAMS="$(jq -n --arg implSid "$IMPL_SID" --arg hoSid "$HO_SID" '{implSid: $implSid, hoSid: $hoSid}')"
184
524
  bash "$SCRIPT_DIR/bin/graph-wal.sh" append "$CYPHER" "$PARAMS" 2>/dev/null || true
185
525
  bash "$GS" query "$CYPHER" "$PARAMS"
186
526
  ;;
187
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
+
188
572
  create-pr)
189
573
  SID="${1:?missing session-id}"
190
574
  PR_NUM="${2:?missing pr-number}"
@@ -315,6 +699,292 @@ case "$OP" in
315
699
  fi
316
700
  ;;
317
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 (
712
+ toLower(coalesce(p.name, '')) = toLower(\$user)
713
+ OR toLower(coalesce(p.github, '')) = toLower(\$user)
714
+ OR toLower(coalesce(p.githubUsername, '')) = toLower(\$user)
715
+ OR any(alias IN coalesce(p.previousNames, [])
716
+ WHERE toLower(alias) = toLower(\$user))
717
+ )
718
+ AND coalesce(s.handoffStatus, 'pending')
719
+ IN ['pending','unread','read','claimed']
720
+ WITH DISTINCT s
721
+ OPTIONAL MATCH (s)-[:BY]->(author:Person)
722
+ WITH s, author,
723
+ CASE WHEN s.date IS NULL THEN null
724
+ ELSE duration.inDays(date(s.date), date()).days END AS ageDays
725
+ RETURN s.id AS id,
726
+ coalesce(s.topic, s.summary, s.branch) AS topic,
727
+ coalesce(author.name, s.author, 'unknown') AS author,
728
+ toString(s.date) AS date,
729
+ coalesce(s.handoffStatus, 'pending') AS status,
730
+ coalesce(s.handoffIntent, 'unclassified') AS intent,
731
+ ageDays,
732
+ CASE WHEN ageDays IS NULL THEN 'unknown'
733
+ WHEN ageDays <= 14 THEN 'current'
734
+ WHEN ageDays <= 30 THEN 'aging'
735
+ ELSE 'stale' END AS freshness,
736
+ 1 AS definitionVersion
737
+ ORDER BY s.date DESC, s.id
738
+ LIMIT \$limit
739
+ " "$(jq -n --arg user "$USER" --argjson limit "$LIMIT" '{user: $user, limit: $limit}')"
740
+ ;;
741
+
742
+ pending-questions)
743
+ USER="${1:?missing username}"
744
+ LIMIT="${2:-10}"
745
+ if ! [[ "$LIMIT" =~ ^[0-9]+$ ]] || [ "$LIMIT" -lt 1 ] || [ "$LIMIT" -gt 25 ]; then
746
+ echo '{"error":"limit must be between 1 and 25"}'
747
+ exit 2
748
+ fi
749
+ bash "$GS" query "
750
+ MATCH (qs:QuestionSet)-[:ASKED_TO]->(p:Person)
751
+ WHERE (
752
+ toLower(coalesce(p.name, '')) = toLower(\$user)
753
+ OR toLower(coalesce(p.github, '')) = toLower(\$user)
754
+ OR toLower(coalesce(p.githubUsername, '')) = toLower(\$user)
755
+ OR any(alias IN coalesce(p.previousNames, [])
756
+ WHERE toLower(alias) = toLower(\$user))
757
+ )
758
+ AND coalesce(qs.status, 'pending') IN ['pending','active']
759
+ WITH DISTINCT qs
760
+ OPTIONAL MATCH (q:Question)-[:PART_OF]->(qs)
761
+ OPTIONAL MATCH (qs)-[:ASKED_BY]->(asker:Person)
762
+ WITH qs, asker, count(DISTINCT q) AS questions,
763
+ CASE WHEN qs.created IS NULL THEN null
764
+ ELSE duration.inDays(date(datetime(qs.created)), date()).days END AS ageDays
765
+ RETURN qs.id AS id,
766
+ qs.topic AS topic,
767
+ coalesce(asker.name, qs.askedBy, qs.from, 'unknown') AS askedBy,
768
+ questions,
769
+ toString(qs.created) AS created,
770
+ ageDays,
771
+ CASE WHEN ageDays IS NULL THEN 'unknown'
772
+ WHEN ageDays <= 14 THEN 'current'
773
+ WHEN ageDays <= 30 THEN 'aging'
774
+ ELSE 'stale-open' END AS freshness,
775
+ 1 AS definitionVersion
776
+ ORDER BY qs.created DESC, qs.id
777
+ LIMIT \$limit
778
+ " "$(jq -n --arg user "$USER" --argjson limit "$LIMIT" '{user: $user, limit: $limit}')"
779
+ ;;
780
+
781
+ lineage)
782
+ QUERY="${1:?missing topic}"
783
+ LIMIT="${2:-10}"
784
+ if ! [[ "$LIMIT" =~ ^[0-9]+$ ]] || [ "$LIMIT" -lt 1 ] || [ "$LIMIT" -gt 15 ]; then
785
+ echo '{"error":"limit must be between 1 and 15"}'
786
+ exit 2
787
+ fi
788
+ GRAPH_RAW="$(bash "$GS" query "
789
+ MATCH (s:Session)
790
+ WHERE toLower(coalesce(s.topic, '')) CONTAINS toLower(\$query)
791
+ OR toLower(coalesce(s.summary, '')) CONTAINS toLower(\$query)
792
+ OR toLower(coalesce(s.branch, '')) CONTAINS toLower(\$query)
793
+ OPTIONAL MATCH (s)-[:BY]->(author:Person)
794
+ OPTIONAL MATCH (s)-[:HANDED_TO]->(recipient:Person)
795
+ OPTIONAL MATCH (implementation:Session)-[:IMPLEMENTS]->(s)
796
+ OPTIONAL MATCH (implementation)-[:BY]->(implementer:Person)
797
+ OPTIONAL MATCH (s)-[:IMPLEMENTS]->(prior:Session)
798
+ OPTIONAL MATCH (prior)-[:BY]->(priorAuthor:Person)
799
+ OPTIONAL MATCH (s)-[:PRODUCED]->(pr:PR)
800
+ OPTIONAL MATCH (implementation)-[:PRODUCED]->(implementationPr:PR)
801
+ WITH s, author,
802
+ collect(DISTINCT coalesce(recipient.name, recipient.github)) AS recipients,
803
+ collect(DISTINCT CASE WHEN implementation IS NULL THEN null ELSE {
804
+ id: implementation.id,
805
+ topic: implementation.topic,
806
+ summary: implementation.summary,
807
+ branch: implementation.branch,
808
+ date: toString(implementation.date),
809
+ evidencePath: CASE
810
+ WHEN implementation.filePath IS NULL THEN null
811
+ WHEN implementation.filePath STARTS WITH 'memory/' THEN implementation.filePath
812
+ ELSE 'memory/' + implementation.filePath END,
813
+ implementedBy: coalesce(implementer.name, implementer.github)
814
+ } END) AS implementations,
815
+ collect(DISTINCT CASE WHEN prior IS NULL THEN null ELSE {
816
+ id: prior.id,
817
+ topic: prior.topic,
818
+ summary: prior.summary,
819
+ branch: prior.branch,
820
+ date: toString(prior.date),
821
+ evidencePath: CASE
822
+ WHEN prior.filePath IS NULL THEN null
823
+ WHEN prior.filePath STARTS WITH 'memory/' THEN prior.filePath
824
+ ELSE 'memory/' + prior.filePath END,
825
+ author: coalesce(priorAuthor.name, priorAuthor.github)
826
+ } END) AS implements,
827
+ collect(DISTINCT CASE WHEN pr IS NULL THEN null ELSE {
828
+ number: pr.number, repo: pr.repo, title: pr.title,
829
+ status: pr.status, mergedAt: toString(pr.mergedAt)
830
+ } END)
831
+ + collect(DISTINCT CASE WHEN implementationPr IS NULL THEN null ELSE {
832
+ number: implementationPr.number, repo: implementationPr.repo,
833
+ title: implementationPr.title, status: implementationPr.status,
834
+ mergedAt: toString(implementationPr.mergedAt)
835
+ } END) AS prs
836
+ RETURN s.id AS sessionId,
837
+ s.topic AS topic,
838
+ s.summary AS summary,
839
+ s.branch AS branch,
840
+ toString(s.date) AS date,
841
+ CASE WHEN s.filePath IS NULL THEN null
842
+ WHEN s.filePath STARTS WITH 'memory/' THEN s.filePath
843
+ ELSE 'memory/' + s.filePath END AS evidencePath,
844
+ coalesce(author.name, author.github, s.author, 'unknown') AS author,
845
+ recipients[0..20] AS recipients,
846
+ implementations[0..10] AS implementations,
847
+ implements[0..10] AS implements,
848
+ prs[0..10] AS prs
849
+ ORDER BY s.date DESC, s.id
850
+ LIMIT \$limit
851
+ " "$(jq -n --arg query "$QUERY" --argjson limit "$LIMIT" '{query:$query,limit:$limit}')")"
852
+ FILES_RAW="$(memory_file_candidates "$QUERY" "$LIMIT" \
853
+ "$SCRIPT_DIR/memory/handoffs" \
854
+ "$SCRIPT_DIR/memory/wraps" \
855
+ "$SCRIPT_DIR/memory/sessions")"
856
+ jq -n \
857
+ --arg query "$QUERY" \
858
+ --argjson limit "$LIMIT" \
859
+ --argjson graph "$GRAPH_RAW" \
860
+ --argjson files "$FILES_RAW" '
861
+ ($graph.values // [] | map({
862
+ sessionId: .[0], topic: .[1], summary: .[2], branch: .[3],
863
+ date: .[4], evidencePath: .[5], author: .[6],
864
+ recipients: (.[7] // []), implementations: (.[8] // []),
865
+ implements: (.[9] // []), prs: (.[10] // [])
866
+ })) as $matches
867
+ | ($matches | map(.evidencePath) | map(select(. != null)) | unique) as $graphPaths
868
+ | ($files | map(.evidencePath) | unique) as $filePaths
869
+ | ($filePaths - $graphPaths) as $unprojected
870
+ | {
871
+ schema: "egregore.graph-read/v1",
872
+ operation: "lineage",
873
+ definitionVersion: 1,
874
+ query: $query,
875
+ limit: $limit,
876
+ coverage:
877
+ (if ($graphPaths | length) == 0 then
878
+ (if ($filePaths | length) == 0 then "none" else "filesystem-only" end)
879
+ elif ($unprojected | length) > 0 then "partial"
880
+ else "complete" end),
881
+ graphMatches: $matches,
882
+ fileCandidates:
883
+ ($files | map(
884
+ . as $candidate
885
+ | . + {
886
+ projection:
887
+ (if ($graphPaths | index($candidate.evidencePath)) != null
888
+ then "projected" else "unprojected" end)
889
+ }
890
+ )),
891
+ unprojectedPaths: $unprojected
892
+ }
893
+ '
894
+ ;;
895
+
896
+ meeting-history)
897
+ QUERY="${1:?missing company, person, or topic}"
898
+ LIMIT="${2:-10}"
899
+ if ! [[ "$LIMIT" =~ ^[0-9]+$ ]] || [ "$LIMIT" -lt 1 ] || [ "$LIMIT" -gt 15 ]; then
900
+ echo '{"error":"limit must be between 1 and 15"}'
901
+ exit 2
902
+ fi
903
+ QUERY_TERMS="$(meaningful_query_terms "$QUERY")"
904
+ GRAPH_RAW="$(bash "$GS" query "
905
+ MATCH (m:Meeting)
906
+ OPTIONAL MATCH (m)-[:INVOLVES|CONDUCTED_BY]->(participant:Person)
907
+ WITH m, collect(DISTINCT participant) AS participants
908
+ WITH m, participants,
909
+ [term IN \$terms WHERE
910
+ toLower(coalesce(m.title, '')) CONTAINS term
911
+ OR toLower(coalesce(m.name, '')) CONTAINS term
912
+ OR toLower(coalesce(m.id, '')) CONTAINS term
913
+ OR any(p IN participants WHERE
914
+ toLower(coalesce(p.name, '')) CONTAINS term
915
+ OR toLower(coalesce(p.github, '')) CONTAINS term)
916
+ ] AS matchedTerms
917
+ WHERE size(matchedTerms) > 0
918
+ OPTIONAL MATCH (artifact:Artifact)-[:FROM_MEETING]->(m)
919
+ RETURN m.id AS meetingId,
920
+ coalesce(m.title, m.name, m.id) AS title,
921
+ toString(m.date) AS date,
922
+ CASE WHEN m.filePath IS NULL THEN null
923
+ WHEN m.filePath STARTS WITH 'memory/' THEN m.filePath
924
+ ELSE 'memory/' + m.filePath END AS evidencePath,
925
+ ([p IN participants | coalesce(p.name, p.github)])[0..20] AS participants,
926
+ (collect(DISTINCT CASE WHEN artifact IS NULL THEN null ELSE {
927
+ id: artifact.id,
928
+ type: artifact.type,
929
+ title: artifact.title,
930
+ summary: artifact.summary,
931
+ evidencePath: CASE
932
+ WHEN artifact.filePath IS NULL THEN null
933
+ WHEN artifact.filePath STARTS WITH 'memory/' THEN artifact.filePath
934
+ ELSE 'memory/' + artifact.filePath END
935
+ } END))[0..10] AS derivedKnowledge,
936
+ coalesce(m.verificationStatus, 'legacy') AS verificationStatus,
937
+ m.ingestManifestId AS ingestManifestId,
938
+ matchedTerms,
939
+ size(matchedTerms) AS matchScore
940
+ ORDER BY matchScore DESC, date ASC, meetingId
941
+ LIMIT \$limit
942
+ " "$(jq -n --arg query "$QUERY" --argjson terms "$QUERY_TERMS" --argjson limit "$LIMIT" \
943
+ '{query:$query,terms:$terms,limit:$limit}')")"
944
+ FILES_RAW="$(memory_file_candidates_for_terms \
945
+ "$QUERY_TERMS" "$LIMIT" "$SCRIPT_DIR/memory/meetings")"
946
+ jq -n \
947
+ --arg query "$QUERY" \
948
+ --argjson queryTerms "$QUERY_TERMS" \
949
+ --argjson limit "$LIMIT" \
950
+ --argjson graph "$GRAPH_RAW" \
951
+ --argjson files "$FILES_RAW" '
952
+ ($graph.values // [] | map({
953
+ meetingId: .[0], title: .[1], date: .[2], evidencePath: .[3],
954
+ participants: (.[4] // []), derivedKnowledge: (.[5] // []),
955
+ verificationStatus: .[6], ingestManifestId: .[7],
956
+ matchedTerms: (.[8] // []), matchScore: (.[9] // 0)
957
+ })) as $meetings
958
+ | ($meetings | map(.evidencePath) | map(select(. != null)) | unique) as $graphPaths
959
+ | ($files | map(.evidencePath) | unique) as $filePaths
960
+ | ($filePaths - $graphPaths) as $unprojected
961
+ | {
962
+ schema: "egregore.graph-read/v1",
963
+ operation: "meeting-history",
964
+ definitionVersion: 1,
965
+ query: $query,
966
+ queryTerms: $queryTerms,
967
+ limit: $limit,
968
+ coverage:
969
+ (if ($graphPaths | length) == 0 then
970
+ (if ($filePaths | length) == 0 then "none" else "filesystem-only" end)
971
+ elif ($unprojected | length) > 0 then "partial"
972
+ else "complete" end),
973
+ graphMeetings: $meetings,
974
+ fileCandidates:
975
+ ($files | map(
976
+ . as $candidate
977
+ | . + {
978
+ projection:
979
+ (if ($graphPaths | index($candidate.evidencePath)) != null
980
+ then "projected" else "unprojected" end)
981
+ }
982
+ )),
983
+ unprojectedPaths: $unprojected
984
+ }
985
+ '
986
+ ;;
987
+
318
988
  wal-status)
319
989
  bash "$SCRIPT_DIR/bin/graph-wal.sh" status
320
990
  ;;
@@ -424,7 +1094,7 @@ case "$OP" in
424
1094
  ;;
425
1095
 
426
1096
  *)
427
- echo '{"error":"unknown operation: '"$OP"'","operations":["mark-read","mark-done","answer-question","resolve-handoffs","set-topic","record-focus","merge-person","claim-handoff","check-implements","create-pr","update-pr","my-merged-prs","my-implemented-handoffs","wal-status","create-harvest","create-harvest-session","record-harvest-turn","complete-harvest"]}'
1097
+ 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"]}'
428
1098
  exit 1
429
1099
  ;;
430
1100