create-egregore 0.19.5 → 0.19.6

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 (130) hide show
  1. package/assets/egregore-launcher.js +407 -25
  2. package/bin/cli.js +34 -20
  3. package/lib/local.js +21 -5
  4. package/lib/pi-runtime.js +186 -0
  5. package/lib/setup.js +56 -11
  6. package/lib/ui.js +1 -1
  7. package/package.json +7 -5
  8. package/runtime/codex/.codex/spec-manifest.json +1 -1
  9. package/runtime/codex/AGENTS.md +7 -2
  10. package/runtime/codex/bin/codex-render-spec.mjs +6 -1
  11. package/runtime/codex/bin/codex-session-start.sh +5 -59
  12. package/runtime/codex/bin/codex-skill-render.mjs +7 -5
  13. package/runtime/codex/bin/lib/context.sh +8 -1
  14. package/runtime/codex/bin/lib/greeting.sh +12 -108
  15. package/runtime/codex/bin/lib/metrics.sh +374 -0
  16. package/runtime/codex/bin/publish-artifact.sh +14 -0
  17. package/runtime/codex/bin/settings.sh +274 -0
  18. package/runtime/codex/manifest.json +2 -0
  19. package/runtime/pi/.codex/config.toml +15 -0
  20. package/runtime/pi/.codex/hooks/branch-guard.js +242 -0
  21. package/runtime/pi/.codex/hooks/search-hint.js +65 -0
  22. package/runtime/pi/.codex/hooks.json +30 -0
  23. package/runtime/pi/.codex/skills/activity/SKILL.md +70 -0
  24. package/runtime/pi/.codex/skills/add/SKILL.md +25 -0
  25. package/runtime/pi/.codex/skills/announce/SKILL.md +54 -0
  26. package/runtime/pi/.codex/skills/answer/SKILL.md +25 -0
  27. package/runtime/pi/.codex/skills/archive/SKILL.md +41 -0
  28. package/runtime/pi/.codex/skills/ask/SKILL.md +59 -0
  29. package/runtime/pi/.codex/skills/branch/SKILL.md +25 -0
  30. package/runtime/pi/.codex/skills/checkup/SKILL.md +41 -0
  31. package/runtime/pi/.codex/skills/commit/SKILL.md +25 -0
  32. package/runtime/pi/.codex/skills/contribute/SKILL.md +25 -0
  33. package/runtime/pi/.codex/skills/dark-mode/SKILL.md +25 -0
  34. package/runtime/pi/.codex/skills/dashboard/SKILL.md +59 -0
  35. package/runtime/pi/.codex/skills/deep-reflect/SKILL.md +74 -0
  36. package/runtime/pi/.codex/skills/delete-user/SKILL.md +25 -0
  37. package/runtime/pi/.codex/skills/egregore-voice/SKILL.md +25 -0
  38. package/runtime/pi/.codex/skills/emissary/SKILL.md +41 -0
  39. package/runtime/pi/.codex/skills/env/SKILL.md +25 -0
  40. package/runtime/pi/.codex/skills/handoff/SKILL.md +67 -0
  41. package/runtime/pi/.codex/skills/harvest/SKILL.md +107 -0
  42. package/runtime/pi/.codex/skills/ingest/SKILL.md +25 -0
  43. package/runtime/pi/.codex/skills/ingest-google/SKILL.md +25 -0
  44. package/runtime/pi/.codex/skills/ingest-user-interview/SKILL.md +41 -0
  45. package/runtime/pi/.codex/skills/invite/SKILL.md +40 -0
  46. package/runtime/pi/.codex/skills/issue/SKILL.md +41 -0
  47. package/runtime/pi/.codex/skills/me/SKILL.md +25 -0
  48. package/runtime/pi/.codex/skills/meeting/SKILL.md +41 -0
  49. package/runtime/pi/.codex/skills/minion/SKILL.md +25 -0
  50. package/runtime/pi/.codex/skills/note/SKILL.md +25 -0
  51. package/runtime/pi/.codex/skills/onboarding/SKILL.md +41 -0
  52. package/runtime/pi/.codex/skills/pr/SKILL.md +25 -0
  53. package/runtime/pi/.codex/skills/project/SKILL.md +41 -0
  54. package/runtime/pi/.codex/skills/pull/SKILL.md +25 -0
  55. package/runtime/pi/.codex/skills/push/SKILL.md +25 -0
  56. package/runtime/pi/.codex/skills/quest/SKILL.md +77 -0
  57. package/runtime/pi/.codex/skills/quest-suggest/SKILL.md +25 -0
  58. package/runtime/pi/.codex/skills/reflect/SKILL.md +41 -0
  59. package/runtime/pi/.codex/skills/review-pr/SKILL.md +41 -0
  60. package/runtime/pi/.codex/skills/save/SKILL.md +62 -0
  61. package/runtime/pi/.codex/skills/scroll/SKILL.md +168 -0
  62. package/runtime/pi/.codex/skills/scroll/agents/openai.yaml +4 -0
  63. package/runtime/pi/.codex/skills/scroll/assets/shell.html +542 -0
  64. package/runtime/pi/.codex/skills/search/SKILL.md +56 -0
  65. package/runtime/pi/.codex/skills/session-view/SKILL.md +25 -0
  66. package/runtime/pi/.codex/skills/setup/SKILL.md +25 -0
  67. package/runtime/pi/.codex/skills/summon/SKILL.md +41 -0
  68. package/runtime/pi/.codex/skills/sync-repos/SKILL.md +25 -0
  69. package/runtime/pi/.codex/skills/telegram-connect/SKILL.md +25 -0
  70. package/runtime/pi/.codex/skills/telemetry/SKILL.md +25 -0
  71. package/runtime/pi/.codex/skills/test/SKILL.md +41 -0
  72. package/runtime/pi/.codex/skills/the-spiral/SKILL.md +84 -0
  73. package/runtime/pi/.codex/skills/thread/SKILL.md +25 -0
  74. package/runtime/pi/.codex/skills/todo/SKILL.md +41 -0
  75. package/runtime/pi/.codex/skills/tui-design/SKILL.md +41 -0
  76. package/runtime/pi/.codex/skills/tutorial/SKILL.md +41 -0
  77. package/runtime/pi/.codex/skills/update/SKILL.md +25 -0
  78. package/runtime/pi/.codex/skills/view/SKILL.md +156 -0
  79. package/runtime/pi/.codex/skills/view/agents/openai.yaml +4 -0
  80. package/runtime/pi/.codex/skills/view/assets/compose-scaffold.html +221 -0
  81. package/runtime/pi/.codex/skills/visual-explain/SKILL.md +41 -0
  82. package/runtime/pi/.codex/skills/wrap/SKILL.md +55 -0
  83. package/runtime/pi/.codex/spec-manifest.json +114 -0
  84. package/runtime/pi/.pi/APPEND_SYSTEM.md +255 -0
  85. package/runtime/pi/.pi/extensions/egregore.ts +324 -0
  86. package/runtime/pi/.pi/settings.json +6 -0
  87. package/runtime/pi/.pi/spec-manifest.json +12 -0
  88. package/runtime/pi/AGENTS.md +350 -0
  89. package/runtime/pi/bin/activity-data.sh +242 -0
  90. package/runtime/pi/bin/agent.sh +677 -0
  91. package/runtime/pi/bin/codex-install-skills.sh +78 -0
  92. package/runtime/pi/bin/codex-render-spec.mjs +394 -0
  93. package/runtime/pi/bin/codex-session-start.sh +188 -0
  94. package/runtime/pi/bin/codex-skill-render.mjs +323 -0
  95. package/runtime/pi/bin/codex-sync-skills.sh +227 -0
  96. package/runtime/pi/bin/dashboard-data.sh +178 -0
  97. package/runtime/pi/bin/graph-batch.sh +60 -0
  98. package/runtime/pi/bin/graph-op.sh +431 -0
  99. package/runtime/pi/bin/graph-wal.sh +228 -0
  100. package/runtime/pi/bin/graph.sh +177 -0
  101. package/runtime/pi/bin/handoff-pr-backfill.sh +86 -0
  102. package/runtime/pi/bin/handoff-run.sh +581 -0
  103. package/runtime/pi/bin/handoff-save-egregore.sh +136 -0
  104. package/runtime/pi/bin/index-handoff.sh +312 -0
  105. package/runtime/pi/bin/lib/artifact-id.sh +46 -0
  106. package/runtime/pi/bin/lib/config.sh +71 -0
  107. package/runtime/pi/bin/lib/context.sh +542 -0
  108. package/runtime/pi/bin/lib/dashboard-artifact.sh +142 -0
  109. package/runtime/pi/bin/lib/ensure-develop.sh +183 -0
  110. package/runtime/pi/bin/lib/git-sync.sh +288 -0
  111. package/runtime/pi/bin/lib/greeting.sh +340 -0
  112. package/runtime/pi/bin/lib/hash.sh +13 -0
  113. package/runtime/pi/bin/lib/identity.sh +174 -0
  114. package/runtime/pi/bin/lib/metrics.sh +374 -0
  115. package/runtime/pi/bin/lib/permissions.sh +106 -0
  116. package/runtime/pi/bin/lib/time.sh +56 -0
  117. package/runtime/pi/bin/lib/worktree-links.sh +54 -0
  118. package/runtime/pi/bin/notify.sh +207 -0
  119. package/runtime/pi/bin/pi-branch-policy.mjs +15 -0
  120. package/runtime/pi/bin/pi-product-workflows.mjs +324 -0
  121. package/runtime/pi/bin/pi-render-spec.mjs +114 -0
  122. package/runtime/pi/bin/pi-session-start.sh +24 -0
  123. package/runtime/pi/bin/publish-artifact.sh +201 -0
  124. package/runtime/pi/bin/publish-references.sh +144 -0
  125. package/runtime/pi/bin/repo-state.sh +99 -0
  126. package/runtime/pi/bin/telemetry.sh +391 -0
  127. package/runtime/pi/bin/worktree-create.sh +118 -0
  128. package/runtime/pi/bin/worktree-remove.sh +50 -0
  129. package/runtime/pi/bin/worktree.sh +198 -0
  130. package/runtime/pi/manifest.json +176 -0
@@ -0,0 +1,178 @@
1
+ #!/bin/bash
2
+ # Fetches personal dashboard data: single API call + parallel git ops.
3
+ # Usage: bash bin/dashboard-data.sh [username] [time_range]
4
+ # Returns a single JSON object with all query results.
5
+ #
6
+ # time_range: P1D (today), P7D (week, default), P30D (month), P365D (all)
7
+ set -euo pipefail
8
+
9
+ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
10
+ TMPDIR=$(mktemp -d)
11
+ trap "rm -rf $TMPDIR" EXIT
12
+
13
+ # --- Read config ---
14
+ API_URL=$(jq -r '.api_url // empty' "$SCRIPT_DIR/egregore.json" 2>/dev/null)
15
+ API_KEY=$(grep '^EGREGORE_API_KEY=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)
16
+ ORG=$(jq -r '.org_name // "Egregore"' "$SCRIPT_DIR/egregore.json" 2>/dev/null || echo "Egregore")
17
+ DATE=$(date '+%b %d')
18
+
19
+ # --- Detect user ---
20
+ GH_USER="${1:-}"
21
+ if [ -z "$GH_USER" ]; then
22
+ STATE_FILE="$SCRIPT_DIR/.egregore-state.json"
23
+ if [ -f "$STATE_FILE" ]; then
24
+ GH_USER=$(jq -r '.github_username // empty' "$STATE_FILE" 2>/dev/null)
25
+ fi
26
+ if [ -z "$GH_USER" ]; then
27
+ GH_USER=$(git -C "$SCRIPT_DIR" config user.name 2>/dev/null || echo "unknown")
28
+ fi
29
+ fi
30
+
31
+ # --- Time range (default P7D) ---
32
+ TIME_RANGE="${2:-P7D}"
33
+
34
+ # --- Read current session ID (client-side fallback for WAL-not-yet-drained) ---
35
+ PROJ_HASH=$(echo -n "$SCRIPT_DIR" | md5 2>/dev/null || echo -n "$SCRIPT_DIR" | md5sum 2>/dev/null | cut -d' ' -f1)
36
+ SESSION_ID=$(cat "$HOME/.egregore/session-${PROJ_HASH}.id" 2>/dev/null || echo "")
37
+
38
+ # --- Fire API call + git ops in parallel ---
39
+
40
+ # Graph data: single API call (all queries run server-side)
41
+ (
42
+ GRAPH_STATUS="offline"
43
+ GRAPH_REASON=""
44
+ if [ -z "$API_URL" ] || [ -z "$API_KEY" ]; then
45
+ GRAPH_REASON="missing_config"
46
+ else
47
+ # Keep curl's transport status separate from %{http_code}; otherwise a
48
+ # failed request can concatenate 000 + fallback text and look like an
49
+ # unexpected API response.
50
+ CURL_STATUS=0
51
+ HTTP_CODE=$(curl -s -o "$TMPDIR/dashboard_raw.json" -w "%{http_code}" \
52
+ "${API_URL}/api/personal/dashboard?github_username=$(printf '%s' "$GH_USER" | jq -sRr @uri)&time_range=$(printf '%s' "$TIME_RANGE" | jq -sRr @uri)&session_id=$(printf '%s' "$SESSION_ID" | jq -sRr @uri)" \
53
+ -H "Authorization: Bearer $API_KEY" \
54
+ --connect-timeout 5 --max-time 15 2>/dev/null) || CURL_STATUS=$?
55
+
56
+ if [ "$CURL_STATUS" -ne 0 ] || [ "$HTTP_CODE" = "000" ] || ! [[ "$HTTP_CODE" =~ ^[0-9][0-9][0-9]$ ]]; then
57
+ GRAPH_REASON="unreachable"
58
+ elif [ "$HTTP_CODE" = "401" ] || [ "$HTTP_CODE" = "403" ]; then
59
+ GRAPH_REASON="auth_error"
60
+ elif [ "$HTTP_CODE" -ge 500 ] 2>/dev/null; then
61
+ GRAPH_REASON="server_error"
62
+ elif [ -s "$TMPDIR/dashboard_raw.json" ] && jq -e '.me' "$TMPDIR/dashboard_raw.json" >/dev/null 2>&1; then
63
+ cp "$TMPDIR/dashboard_raw.json" "$TMPDIR/dashboard.json"
64
+ GRAPH_STATUS="connected"
65
+ else
66
+ GRAPH_REASON="invalid_response"
67
+ fi
68
+ fi
69
+
70
+ # If not connected, write status-only response
71
+ if [ "$GRAPH_STATUS" != "connected" ]; then
72
+ jq -n --arg status "$GRAPH_STATUS" --arg reason "$GRAPH_REASON" \
73
+ '{graph_status: $status, graph_reason: $reason}' > "$TMPDIR/dashboard.json"
74
+ else
75
+ jq --arg status "$GRAPH_STATUS" '. + {graph_status: $status}' \
76
+ "$TMPDIR/dashboard.json" > "$TMPDIR/dashboard_merged.json" \
77
+ && mv "$TMPDIR/dashboard_merged.json" "$TMPDIR/dashboard.json"
78
+ fi
79
+ ) &
80
+ API_PID=$!
81
+
82
+ # Git: branch + dirty status (parallel, stays client-side)
83
+ (
84
+ CURRENT=$(git -C "$SCRIPT_DIR" branch --show-current 2>/dev/null || echo "unknown")
85
+ DIRTY="false"
86
+ if [ -n "$(git -C "$SCRIPT_DIR" status --porcelain 2>/dev/null | head -1)" ]; then
87
+ DIRTY="true"
88
+ fi
89
+ jq -n --arg branch "$CURRENT" --argjson dirty "$DIRTY" \
90
+ '{branch: $branch, dirty: $dirty}' > "$TMPDIR/git.json"
91
+ ) &
92
+ GIT_PID=$!
93
+
94
+ # Local sessions: parse memory files when graph is offline (parallel)
95
+ (
96
+ GH_LC=$(echo "$GH_USER" | tr '[:upper:]' '[:lower:]')
97
+ DISPLAY_LC=""
98
+ if [ -f "$SCRIPT_DIR/.egregore-state.json" ]; then
99
+ DISPLAY_LC=$(jq -r '.display_name // empty' "$SCRIPT_DIR/.egregore-state.json" 2>/dev/null | tr '[:upper:]' '[:lower:]')
100
+ fi
101
+
102
+ # Compute cutoff date from time range
103
+ case "$TIME_RANGE" in
104
+ P1D) CUTOFF=$(date -v-1d +%Y-%m-%d 2>/dev/null || date -d '1 day ago' +%Y-%m-%d 2>/dev/null || echo "2000-01-01") ;;
105
+ P7D) CUTOFF=$(date -v-7d +%Y-%m-%d 2>/dev/null || date -d '7 days ago' +%Y-%m-%d 2>/dev/null || echo "2000-01-01") ;;
106
+ P30D) CUTOFF=$(date -v-30d +%Y-%m-%d 2>/dev/null || date -d '30 days ago' +%Y-%m-%d 2>/dev/null || echo "2000-01-01") ;;
107
+ P365D) CUTOFF="2000-01-01" ;;
108
+ *) CUTOFF=$(date -v-7d +%Y-%m-%d 2>/dev/null || date -d '7 days ago' +%Y-%m-%d 2>/dev/null || echo "2000-01-01") ;;
109
+ esac
110
+
111
+ # Scan sessions, wraps, and handoffs for this user
112
+ for DIR in "$SCRIPT_DIR/memory/sessions" "$SCRIPT_DIR/memory/wraps" "$SCRIPT_DIR/memory/handoffs"; do
113
+ [ -d "$DIR" ] || continue
114
+ TYPE=$(basename "$DIR")
115
+ find -L "$DIR" -name '*.md' -not -name 'index*' -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -20 | while read -r MFILE; do
116
+ [ -z "$MFILE" ] && continue
117
+ M_AUTH=$(head -10 "$MFILE" 2>/dev/null | grep -m1 '^\*\*Author\*\*:' | sed 's/^\*\*Author\*\*:[[:space:]]*//' || true)
118
+ [ -z "$M_AUTH" ] && continue
119
+ M_AUTH_LC=$(echo "$M_AUTH" | tr '[:upper:]' '[:lower:]')
120
+ # Filter to this user only
121
+ [ "$M_AUTH_LC" != "$GH_LC" ] && { [ -z "$DISPLAY_LC" ] || [ "$M_AUTH_LC" != "$DISPLAY_LC" ]; } && continue
122
+ M_DATE=$(head -10 "$MFILE" 2>/dev/null | grep -m1 '^\*\*Date\*\*:' | sed 's/^\*\*Date\*\*:[[:space:]]*//' || true)
123
+ M_DAY="${M_DATE%%T*}"
124
+ [ -n "$M_DAY" ] && [ "$M_DAY" \< "$CUTOFF" ] && continue
125
+ M_BRANCH=$(head -10 "$MFILE" 2>/dev/null | grep -m1 '^\*\*Branch\*\*:' | sed 's/^\*\*Branch\*\*:[[:space:]]*//' || true)
126
+ M_DURATION=$(head -10 "$MFILE" 2>/dev/null | grep -m1 '^\*\*Duration\*\*:' | sed 's/^\*\*Duration\*\*:[[:space:]]*//' || true)
127
+ M_TOPIC=$(head -3 "$MFILE" 2>/dev/null | grep -m1 '^# ' | sed 's/^# [^:]*:[[:space:]]*//' || true)
128
+ echo "${M_DATE}|${M_TOPIC}|${M_BRANCH}|${M_DURATION}|${TYPE}"
129
+ done
130
+ done | jq -Rn '
131
+ [inputs | split("|") |
132
+ {date: .[0], topic: .[1], branch: .[2], duration: .[3], type: .[4]}
133
+ ] | sort_by(.date) | reverse | {
134
+ sessions: .,
135
+ session_count: length
136
+ }
137
+ ' 2>/dev/null > "$TMPDIR/local_sessions.json" || echo '{"sessions":[],"session_count":0}' > "$TMPDIR/local_sessions.json"
138
+ ) &
139
+ LOCAL_PID=$!
140
+
141
+ # --- Wait for all ---
142
+ wait $API_PID $GIT_PID $LOCAL_PID 2>/dev/null || true
143
+
144
+ # --- Read results ---
145
+ GIT_INFO=$(cat "$TMPDIR/git.json" 2>/dev/null || echo '{"branch":"unknown","dirty":false}')
146
+ BRANCH=$(echo "$GIT_INFO" | jq -r '.branch' 2>/dev/null || echo "unknown")
147
+ LOCAL_SESSIONS=$(cat "$TMPDIR/local_sessions.json" 2>/dev/null || echo '{"sessions":[],"session_count":0}')
148
+ echo "$LOCAL_SESSIONS" | jq . >/dev/null 2>&1 || LOCAL_SESSIONS='{"sessions":[],"session_count":0}'
149
+
150
+ # --- Client-side current session fallback ---
151
+ # If the graph returned no current_session (WAL hasn't drained yet),
152
+ # inject one from the local session ID file so the TUI always renders it.
153
+ DASHBOARD=$(cat "$TMPDIR/dashboard.json" 2>/dev/null || echo '{"graph_status":"offline"}')
154
+ CS_ID=$(echo "$DASHBOARD" | jq -r '.current_session.id // empty' 2>/dev/null)
155
+ if [ -z "$CS_ID" ] && [ -n "$SESSION_ID" ]; then
156
+ DASHBOARD=$(echo "$DASHBOARD" | jq \
157
+ --arg sid "$SESSION_ID" \
158
+ --arg branch "$BRANCH" \
159
+ '.current_session = {id: $sid, status: "active", topic: null, branch: $branch}' 2>/dev/null || echo "$DASHBOARD")
160
+ fi
161
+
162
+ # --- Derive time range label ---
163
+ case "$TIME_RANGE" in
164
+ P1D) RANGE_LABEL="today" ;;
165
+ P7D) RANGE_LABEL="last 7 days" ;;
166
+ P30D) RANGE_LABEL="last 30 days" ;;
167
+ P365D) RANGE_LABEL="all time" ;;
168
+ *) RANGE_LABEL="last 7 days" ;;
169
+ esac
170
+
171
+ # --- Merge server response + local data ---
172
+ echo "$DASHBOARD" | jq \
173
+ --arg org "$ORG" \
174
+ --arg date "$DATE" \
175
+ --arg range_label "$RANGE_LABEL" \
176
+ --argjson git "$GIT_INFO" \
177
+ --argjson local_sessions "$LOCAL_SESSIONS" \
178
+ '. + {org: $org, date: $date, range_label: $range_label, git: $git, local_sessions: $local_sessions}'
@@ -0,0 +1,60 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # Execute multiple Cypher queries in a single HTTP call.
5
+ # Usage: graph-batch.sh '<json array of {statement, parameters} objects>'
6
+ #
7
+ # Example:
8
+ # graph-batch.sh '[{"statement":"MATCH (p:Person) RETURN p.name"},{"statement":"MATCH (q:Quest) RETURN q.id"}]'
9
+ #
10
+ # Returns: {"results": [{...}, {...}]}
11
+
12
+ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
13
+ CONFIG="$SCRIPT_DIR/egregore.json"
14
+
15
+ if [ ! -f "$CONFIG" ]; then
16
+ echo "Error: egregore.json not found. Run onboarding first." >&2
17
+ exit 1
18
+ fi
19
+
20
+ # --- Local mode gate: bail immediately ---
21
+ _MODE=$(jq -r '.mode // "connected"' "$CONFIG" 2>/dev/null)
22
+ if [ "$_MODE" = "local" ]; then
23
+ echo '{"results":[]}'
24
+ exit 0
25
+ fi
26
+
27
+ # Load specific variables from .env if it exists (safe extraction, no arbitrary code execution)
28
+ if [ -f "$SCRIPT_DIR/.env" ]; then
29
+ EGREGORE_API_URL="${EGREGORE_API_URL:-$(grep '^EGREGORE_API_URL=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)}"
30
+ EGREGORE_API_KEY="${EGREGORE_API_KEY:-$(grep '^EGREGORE_API_KEY=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)}"
31
+ fi
32
+
33
+ API_URL="${EGREGORE_API_URL:-$(jq -r '.api_url // empty' "$CONFIG")}"
34
+ API_KEY="${EGREGORE_API_KEY:-}"
35
+
36
+ if [ -z "$API_URL" ] || [ -z "$API_KEY" ]; then
37
+ # === OFFLINE MODE: No API key — return empty results (OSS/local) ===
38
+ echo '{"results":[]}'
39
+ exit 0
40
+ fi
41
+
42
+ QUERIES_JSON="${1:?Usage: graph-batch.sh '<json array of queries>'}"
43
+
44
+ # Wrap the array into the batch request format
45
+ BODY=$(jq -n --argjson queries "$QUERIES_JSON" '{queries: $queries}')
46
+
47
+ RESPONSE=$(curl -s -X POST "${API_URL}/api/graph/batch" \
48
+ -H "Authorization: Bearer $API_KEY" \
49
+ -H "Content-Type: application/json" \
50
+ -d "$BODY" \
51
+ --max-time 60)
52
+
53
+ # Check for HTTP-level errors
54
+ if echo "$RESPONSE" | jq -e '.detail' >/dev/null 2>&1; then
55
+ echo "API error:" >&2
56
+ echo "$RESPONSE" | jq '.detail' >&2
57
+ exit 1
58
+ fi
59
+
60
+ echo "$RESPONSE"
@@ -0,0 +1,431 @@
1
+ #!/bin/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> Mark a handoff as read
10
+ # mark-done <session-id> Mark a handoff as done/resolved
11
+ # answer-question <set-id> Mark a question set as answered
12
+ # resolve-handoffs <user> Auto-resolve read handoffs with later sessions
13
+ # set-topic <session-id> <topic> [branch]
14
+ # Set topic (and optionally branch) on a Session node
15
+ # record-focus <session-id> <shown-json> <selected> [dismissed-json]
16
+ # 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
20
+ # create-harvest <id> <topic> <intent> <initiator>
21
+ # Create a Harvest node and link to initiator (WAL-backed)
22
+ # create-harvest-session <harvest-id> <session-id> <person-name> [status]
23
+ # Create a HarvestSession linked to a Harvest and Person.
24
+ # status defaults to 'active' (back-compat). Valid values:
25
+ # pending | active | answered | complete | incorporated.
26
+ # Async respondents should be created with 'pending'.
27
+ # record-harvest-turn <session-id> <turn> <question> <intent> [answer] [eval]
28
+ # Record a question-answer turn in a HarvestSession
29
+ # complete-harvest <harvest-id> <artifact-path>
30
+ # Mark harvest complete and link synthesis artifact
31
+
32
+ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
33
+
34
+ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
35
+ echo "Usage: graph-op.sh <operation> [args...]"
36
+ echo ""
37
+ echo "Named graph operations — clean interface over raw Cypher."
38
+ echo ""
39
+ echo "Operations:"
40
+ echo " mark-read <sid> Mark handoff as read"
41
+ echo " mark-done <sid> Mark handoff as done"
42
+ echo " answer-question <qid> Mark question set as answered"
43
+ echo " resolve-handoffs <user> Auto-resolve read handoffs"
44
+ echo " set-topic <sid> <topic> Set topic on a Session node"
45
+ echo " merge-person <keep> <rm> Merge two Person nodes"
46
+ echo " claim-handoff <sid> <ho> Link implementing session to handoff"
47
+ echo " create-pr <sid> <num> <repo> <author> [title]"
48
+ echo " register-artifact <id> <url> <title> <type> <author> <topics> <excerpt>"
49
+ echo " create-harvest / complete-harvest / record-harvest-turn"
50
+ echo " wal-status Show WAL pending count"
51
+ exit 0
52
+ fi
53
+
54
+ # --- Local mode gate: bail immediately ---
55
+ _MODE=$(jq -r '.mode // "connected"' "$SCRIPT_DIR/egregore.json" 2>/dev/null)
56
+ if [ "$_MODE" = "local" ]; then
57
+ echo '{"results":[]}'
58
+ exit 0
59
+ fi
60
+
61
+ GS="$SCRIPT_DIR/bin/graph.sh"
62
+
63
+ OP="${1:-}"
64
+ shift || true
65
+
66
+ case "$OP" in
67
+
68
+ mark-read)
69
+ SID="${1:?missing session-id}"
70
+ bash "$GS" query "
71
+ 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}')"
75
+ ;;
76
+
77
+ mark-done)
78
+ SID="${1:?missing session-id}"
79
+ bash "$GS" query "
80
+ 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}')"
84
+ ;;
85
+
86
+ answer-question)
87
+ QID="${1:?missing question-set-id}"
88
+ bash "$GS" query "
89
+ MATCH (qs:QuestionSet {id: \$qid})
90
+ SET qs.status = 'answered'
91
+ RETURN qs.id AS id, qs.topic AS topic
92
+ " "$(jq -n --arg qid "$QID" '{qid: $qid}')"
93
+ ;;
94
+
95
+ resolve-handoffs)
96
+ USER="${1:?missing username}"
97
+ bash "$GS" query "
98
+ 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
106
+ " "$(jq -n --arg user "$USER" '{user: $user}')"
107
+ ;;
108
+
109
+ set-topic)
110
+ SID="${1:?missing session-id}"
111
+ TOPIC="${2:?missing topic}"
112
+ BRANCH="${3:-}"
113
+ if [ -n "$BRANCH" ]; then
114
+ bash "$GS" query "
115
+ MATCH (s:Session {id: \$sid})
116
+ SET s.topic = \$topic, s.branch = \$branch
117
+ RETURN s.id AS id, s.topic AS topic, s.branch AS branch
118
+ " "$(jq -n --arg sid "$SID" --arg topic "$TOPIC" --arg branch "$BRANCH" '{sid: $sid, topic: $topic, branch: $branch}')"
119
+ else
120
+ bash "$GS" query "
121
+ MATCH (s:Session {id: \$sid})
122
+ SET s.topic = \$topic
123
+ RETURN s.id AS id, s.topic AS topic
124
+ " "$(jq -n --arg sid "$SID" --arg topic "$TOPIC" '{sid: $sid, topic: $topic}')"
125
+ fi
126
+ ;;
127
+
128
+ record-focus)
129
+ SID="${1:?missing session-id}"
130
+ SHOWN="${2:?missing shown options}"
131
+ SELECTED="${3:?missing selected option}"
132
+ DISMISSED="${4:-[]}"
133
+ bash "$GS" query "
134
+ MATCH (s:Session {id: \$sid})
135
+ SET s.focusShown = \$shown,
136
+ s.focusSelected = \$selected,
137
+ s.focusDismissed = \$dismissed
138
+ RETURN s.id AS id
139
+ " "$(jq -n --arg sid "$SID" --argjson shown "$SHOWN" --arg selected "$SELECTED" --argjson dismissed "$DISMISSED" '{sid: $sid, shown: $shown, selected: $selected, dismissed: $dismissed}')"
140
+ ;;
141
+
142
+ merge-person)
143
+ KEEP="${1:?missing keep-name (the Person to keep)}"
144
+ REMOVE="${2:?missing remove-name (the Person to absorb)}"
145
+ 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)
152
+ 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)
155
+ 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)
158
+ 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)
161
+ 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)
164
+ 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)
167
+ 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
170
+ 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}')"
172
+ ;;
173
+
174
+ claim-handoff)
175
+ IMPL_SID="${1:?missing implementing-session-id}"
176
+ HO_SID="${2:?missing handoff-session-id}"
177
+ CYPHER="
178
+ MATCH (impl:Session {id: \$implSid}), (ho:Session {id: \$hoSid})
179
+ WHERE ho.handoffStatus IS NOT NULL
180
+ MERGE (impl)-[:IMPLEMENTS]->(ho)
181
+ RETURN impl.id AS implementor, ho.id AS handoff, ho.topic AS topic
182
+ "
183
+ PARAMS="$(jq -n --arg implSid "$IMPL_SID" --arg hoSid "$HO_SID" '{implSid: $implSid, hoSid: $hoSid}')"
184
+ bash "$SCRIPT_DIR/bin/graph-wal.sh" append "$CYPHER" "$PARAMS" 2>/dev/null || true
185
+ bash "$GS" query "$CYPHER" "$PARAMS"
186
+ ;;
187
+
188
+ create-pr)
189
+ SID="${1:?missing session-id}"
190
+ PR_NUM="${2:?missing pr-number}"
191
+ REPO="${3:?missing repo}"
192
+ AUTHOR_GH="${4:?missing author-github}"
193
+ TITLE="${5:-}"
194
+ CYPHER="
195
+ MERGE (pr:PR {number: toInteger(\$num), repo: \$repo})
196
+ ON CREATE SET pr.author = \$author, pr.status = 'open',
197
+ pr.createdAt = datetime(), pr.title = \$title
198
+ WITH pr
199
+ OPTIONAL MATCH (s:Session {id: \$sid})
200
+ FOREACH (_ IN CASE WHEN s IS NOT NULL THEN [1] ELSE [] END |
201
+ MERGE (s)-[:PRODUCED]->(pr))
202
+ RETURN pr.number AS number, pr.repo AS repo
203
+ "
204
+ 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}')"
205
+ bash "$SCRIPT_DIR/bin/graph-wal.sh" append "$CYPHER" "$PARAMS" 2>/dev/null || true
206
+ bash "$GS" query "$CYPHER" "$PARAMS"
207
+ ;;
208
+
209
+ register-artifact)
210
+ # Index a published (hosted) artifact as an Artifact node so the finder can
211
+ # retrieve it from the graph in connected mode — by content (title/topics/
212
+ # excerpt CONTAINS) and by relationship (who made it, which quest). The node
213
+ # id is the registry FILENAME STEM (date-author-slug), matching sync-graph.sh
214
+ # so a later /save sync upserts the same node instead of duplicating it.
215
+ # args: <id> <url> <title> <type> <author-handle> <topics-csv> <excerpt>
216
+ A_ID="${1:?missing id}"
217
+ A_URL="${2:?missing url}"
218
+ A_TITLE="${3:-}"
219
+ A_TYPE="${4:-artifact}"
220
+ A_AUTHOR="$(printf '%s' "${5:-}" | tr '[:upper:]' '[:lower:]' | awk '{print $1}')"
221
+ A_TOPICS_JSON="$(printf '%s' "${6:-}" | jq -Rc 'split(",") | map(gsub("^\\s+|\\s+$";"")) | map(select(length > 0))' 2>/dev/null || echo '[]')"
222
+ A_EXCERPT="$(printf '%s' "${7:-}" | tr '\n' ' ' | cut -c1-2000)"
223
+ CYPHER="
224
+ MERGE (a:Artifact {id: \$id})
225
+ ON CREATE SET a.created = datetime()
226
+ SET a.title = \$title, a.type = \$type, a.url = \$url,
227
+ a.topics = \$topics, a.excerpt = \$excerpt, a.published = true
228
+ WITH a
229
+ OPTIONAL MATCH (p:Person)
230
+ WHERE \$author <> '' AND (toLower(p.name) = \$author OR p.github = \$author)
231
+ FOREACH (_ IN CASE WHEN p IS NOT NULL THEN [1] ELSE [] END |
232
+ MERGE (a)-[:CONTRIBUTED_BY]->(p))
233
+ RETURN a.id AS id
234
+ "
235
+ PARAMS="$(jq -n --arg id "$A_ID" --arg url "$A_URL" --arg title "$A_TITLE" --arg type "$A_TYPE" \
236
+ --arg author "$A_AUTHOR" --argjson topics "$A_TOPICS_JSON" --arg excerpt "$A_EXCERPT" \
237
+ '{id: $id, url: $url, title: $title, type: $type, author: $author, topics: $topics, excerpt: $excerpt}')"
238
+ bash "$SCRIPT_DIR/bin/graph-wal.sh" append "$CYPHER" "$PARAMS" 2>/dev/null || true
239
+ bash "$GS" query "$CYPHER" "$PARAMS"
240
+ ;;
241
+
242
+ check-implements)
243
+ SID="${1:?missing session-id}"
244
+ bash "$GS" query "
245
+ MATCH (impl:Session {id: \$sid})-[:IMPLEMENTS]->(ho:Session)-[:BY]->(author:Person)
246
+ RETURN ho.id AS handoffId, ho.topic AS topic, author.name AS author,
247
+ author.github AS authorGithub
248
+ " "$(jq -n --arg sid "$SID" '{sid: $sid}')"
249
+ ;;
250
+
251
+ update-pr)
252
+ PR_NUM="${1:?missing pr-number}"
253
+ REPO="${2:?missing repo}"
254
+ STATUS="${3:?missing status}"
255
+ MERGED_AT="${4:-}"
256
+ if [ -n "$MERGED_AT" ]; then
257
+ bash "$GS" query "
258
+ MATCH (pr:PR {number: toInteger(\$num), repo: \$repo})
259
+ SET pr.status = \$status, pr.mergedAt = datetime(\$mergedAt)
260
+ RETURN pr.number AS number, pr.status AS status
261
+ " "$(jq -n --argjson num "$PR_NUM" --arg repo "$REPO" --arg status "$STATUS" --arg mergedAt "$MERGED_AT" '{num: $num, repo: $repo, status: $status, mergedAt: $mergedAt}')"
262
+ else
263
+ bash "$GS" query "
264
+ MATCH (pr:PR {number: toInteger(\$num), repo: \$repo})
265
+ SET pr.status = \$status
266
+ RETURN pr.number AS number, pr.status AS status
267
+ " "$(jq -n --argjson num "$PR_NUM" --arg repo "$REPO" --arg status "$STATUS" '{num: $num, repo: $repo, status: $status}')"
268
+ fi
269
+ ;;
270
+
271
+ my-merged-prs)
272
+ AUTHOR_GH="${1:?missing author-github}"
273
+ SINCE="${2:-}"
274
+ if [ -n "$SINCE" ]; then
275
+ bash "$GS" query "
276
+ MATCH (pr:PR {author: \$author})
277
+ WHERE pr.status = 'merged' AND pr.mergedAt >= datetime(\$since)
278
+ RETURN pr.number AS number, pr.repo AS repo, pr.title AS title,
279
+ toString(pr.mergedAt) AS mergedAt
280
+ ORDER BY pr.mergedAt DESC LIMIT 10
281
+ " "$(jq -n --arg author "$AUTHOR_GH" --arg since "${SINCE}T00:00:00Z" '{author: $author, since: $since}')"
282
+ else
283
+ bash "$GS" query "
284
+ MATCH (pr:PR {author: \$author})
285
+ WHERE pr.status = 'merged'
286
+ RETURN pr.number AS number, pr.repo AS repo, pr.title AS title,
287
+ toString(pr.mergedAt) AS mergedAt
288
+ ORDER BY pr.mergedAt DESC LIMIT 5
289
+ " "$(jq -n --arg author "$AUTHOR_GH" '{author: $author}')"
290
+ fi
291
+ ;;
292
+
293
+ my-implemented-handoffs)
294
+ AUTHOR_NAME="${1:?missing author-name}"
295
+ SINCE="${2:-}"
296
+ if [ -n "$SINCE" ]; then
297
+ bash "$GS" query "
298
+ MATCH (impl:Session)-[:IMPLEMENTS]->(ho:Session)-[:BY]->(author:Person)
299
+ WHERE toLower(author.name) = toLower(\$author)
300
+ AND impl.wrappedAt >= datetime(\$since)
301
+ MATCH (impl)-[:BY]->(implementor:Person)
302
+ RETURN ho.topic AS handoffTopic, implementor.name AS implementedBy,
303
+ toString(impl.wrappedAt) AS completedAt, impl.summary AS summary
304
+ ORDER BY impl.wrappedAt DESC LIMIT 10
305
+ " "$(jq -n --arg author "$AUTHOR_NAME" --arg since "${SINCE}T00:00:00Z" '{author: $author, since: $since}')"
306
+ else
307
+ bash "$GS" query "
308
+ MATCH (impl:Session)-[:IMPLEMENTS]->(ho:Session)-[:BY]->(author:Person)
309
+ WHERE toLower(author.name) = toLower(\$author)
310
+ MATCH (impl)-[:BY]->(implementor:Person)
311
+ RETURN ho.topic AS handoffTopic, implementor.name AS implementedBy,
312
+ toString(impl.wrappedAt) AS completedAt, impl.summary AS summary
313
+ ORDER BY impl.wrappedAt DESC LIMIT 5
314
+ " "$(jq -n --arg author "$AUTHOR_NAME" '{author: $author}')"
315
+ fi
316
+ ;;
317
+
318
+ wal-status)
319
+ bash "$SCRIPT_DIR/bin/graph-wal.sh" status
320
+ ;;
321
+
322
+ create-harvest)
323
+ HID="${1:?missing harvest-id}"
324
+ TOPIC="${2:?missing topic}"
325
+ INTENT="${3:?missing intent}"
326
+ INITIATOR="${4:?missing initiator}"
327
+ CYPHER="
328
+ MERGE (h:Harvest {id: \$hid})
329
+ ON CREATE SET h.topic = \$topic, h.intent = \$intent,
330
+ h.status = 'active', h.created = datetime()
331
+ WITH h
332
+ MATCH (p:Person) WHERE toLower(p.name) = toLower(\$initiator) OR p.github = \$initiator
333
+ MERGE (h)-[:INITIATED_BY]->(p)
334
+ RETURN h.id AS id, h.topic AS topic, h.status AS status
335
+ "
336
+ PARAMS="$(jq -n --arg hid "$HID" --arg topic "$TOPIC" --arg intent "$INTENT" --arg initiator "$INITIATOR" '{hid: $hid, topic: $topic, intent: $intent, initiator: $initiator}')"
337
+ bash "$SCRIPT_DIR/bin/graph-wal.sh" append "$CYPHER" "$PARAMS" 2>/dev/null || true
338
+ bash "$GS" query "$CYPHER" "$PARAMS"
339
+ ;;
340
+
341
+ create-harvest-session)
342
+ HID="${1:?missing harvest-id}"
343
+ HSID="${2:?missing session-id}"
344
+ PERSON="${3:?missing person-name}"
345
+ STATUS="${4:-active}"
346
+ case "$STATUS" in
347
+ pending|active|answered|complete|incorporated) ;;
348
+ *) jq -n --arg s "$STATUS" '{error:("invalid status: "+$s),valid:["pending","active","answered","complete","incorporated"]}' >&2; exit 2 ;;
349
+ esac
350
+ CYPHER="
351
+ MATCH (h:Harvest {id: \$hid})
352
+ MERGE (hs:HarvestSession {id: \$hsid})
353
+ ON CREATE SET hs.status = \$status, hs.created = datetime()
354
+ MERGE (h)-[:HAS_SESSION]->(hs)
355
+ WITH hs
356
+ MATCH (p:Person) WHERE toLower(p.name) = toLower(\$person) OR p.github = \$person
357
+ MERGE (hs)-[:WITH]->(p)
358
+ RETURN hs.id AS id, hs.status AS status
359
+ "
360
+ PARAMS="$(jq -n --arg hid "$HID" --arg hsid "$HSID" --arg person "$PERSON" --arg status "$STATUS" '{hid: $hid, hsid: $hsid, person: $person, status: $status}')"
361
+ bash "$SCRIPT_DIR/bin/graph-wal.sh" append "$CYPHER" "$PARAMS" 2>/dev/null || true
362
+ OUT="$(bash "$GS" query "$CYPHER" "$PARAMS")"
363
+ if [ "$(echo "$OUT" | jq -r '.results | length' 2>/dev/null)" = "0" ]; then
364
+ jq -n --arg hid "$HID" --arg hsid "$HSID" --arg person "$PERSON" \
365
+ '{error:"create-harvest-session: harvest or person not found",hid:$hid,hsid:$hsid,person:$person}' >&2
366
+ exit 3
367
+ fi
368
+ echo "$OUT"
369
+ ;;
370
+
371
+ record-harvest-turn)
372
+ HSID="${1:?missing session-id}"
373
+ TURN="${2:?missing turn-number}"
374
+ QUESTION="${3:?missing question}"
375
+ QINTENT="${4:?missing question-intent}"
376
+ ANSWER="${5:-}"
377
+ EVAL="${6:-}"
378
+ TURN_ID="${HSID}-turn-${TURN}"
379
+ PARAMS=$(jq -n \
380
+ --arg hsid "$HSID" --arg tid "$TURN_ID" --arg turn "$TURN" \
381
+ --arg question "$QUESTION" --arg qintent "$QINTENT" \
382
+ --arg answer "$ANSWER" --arg eval "$EVAL" \
383
+ '{hsid:$hsid, tid:$tid, turn:$turn, question:$question, qintent:$qintent, answer:$answer, eval:$eval}')
384
+ if [ -n "$ANSWER" ]; then
385
+ bash "$GS" query "
386
+ MATCH (hs:HarvestSession {id: \$hsid})
387
+ MERGE (t:HarvestTurn {id: \$tid})
388
+ ON CREATE SET t.turnNumber = toInteger(\$turn), t.question = \$question,
389
+ t.questionIntent = \$qintent, t.created = datetime()
390
+ SET t.answer = \$answer, t.answeredAt = datetime()
391
+ SET t.evaluation = CASE WHEN \$eval <> '' THEN \$eval ELSE t.evaluation END
392
+ MERGE (hs)-[:HAS_TURN]->(t)
393
+ RETURN t.id AS id, t.turnNumber AS turnNumber
394
+ " "$PARAMS"
395
+ else
396
+ bash "$GS" query "
397
+ MATCH (hs:HarvestSession {id: \$hsid})
398
+ MERGE (t:HarvestTurn {id: \$tid})
399
+ ON CREATE SET t.turnNumber = toInteger(\$turn), t.question = \$question,
400
+ t.questionIntent = \$qintent, t.created = datetime()
401
+ MERGE (hs)-[:HAS_TURN]->(t)
402
+ RETURN t.id AS id, t.turnNumber AS turnNumber
403
+ " "$PARAMS"
404
+ fi
405
+ ;;
406
+
407
+ complete-harvest)
408
+ HID="${1:?missing harvest-id}"
409
+ ARTIFACT_PATH="${2:?missing artifact-path}"
410
+ bash "$GS" query "
411
+ MATCH (h:Harvest {id: \$hid})
412
+ SET h.status = 'complete', h.completedAt = datetime(), h.synthesisPath = \$path
413
+ WITH h
414
+ OPTIONAL MATCH (h)-[:HAS_SESSION]->(hs:HarvestSession)
415
+ WHERE hs.status <> 'complete'
416
+ SET hs.status = 'complete', hs.completedAt = datetime()
417
+ WITH h
418
+ MERGE (a:Artifact {id: \$hid + '-synthesis'})
419
+ ON CREATE SET a.type = 'harvest', a.title = h.topic, a.filePath = \$path,
420
+ a.created = datetime(), a.topics = coalesce([h.topic], [])
421
+ MERGE (h)-[:PRODUCED]->(a)
422
+ RETURN h.id AS id, h.status AS status, a.id AS artifact
423
+ " "$(jq -n --arg hid "$HID" --arg path "$ARTIFACT_PATH" '{hid: $hid, path: $path}')"
424
+ ;;
425
+
426
+ *)
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"]}'
428
+ exit 1
429
+ ;;
430
+
431
+ esac