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
@@ -32,13 +32,14 @@ WAL_FILE="$WAL_DIR/graph-wal-${PROJ_HASH}.jsonl"
32
32
  MAX_BUFFER_BYTES=2097152 # 2MB
33
33
  MAX_ENTRIES_AFTER_TRUNCATE=200
34
34
  LOCK_DIR="$WAL_DIR/graph-wal-${PROJ_HASH}.lock"
35
+ LOCK_ATTEMPTS="${GRAPH_WAL_LOCK_ATTEMPTS:-50}"
35
36
 
36
37
  # --- Cross-platform file locking via mkdir (atomic on all POSIX systems) ---
37
38
  _lock() {
38
39
  local attempts=0
39
40
  while ! mkdir "$LOCK_DIR" 2>/dev/null; do
40
41
  attempts=$((attempts + 1))
41
- if [ "$attempts" -gt 50 ]; then
42
+ if [ "$attempts" -gt "$LOCK_ATTEMPTS" ]; then
42
43
  # Stale lock — check if holder PID is alive
43
44
  local holder_pid
44
45
  holder_pid=$(cat "$LOCK_DIR/pid" 2>/dev/null || echo "")
@@ -46,7 +47,7 @@ _lock() {
46
47
  rm -rf "$LOCK_DIR" 2>/dev/null
47
48
  continue
48
49
  fi
49
- echo "graph-wal: lock timeout after 5s" >&2
50
+ echo "graph-wal: lock timeout after ${LOCK_ATTEMPTS} attempt(s)" >&2
50
51
  return 1
51
52
  fi
52
53
  sleep 0.1
@@ -11,6 +11,7 @@ set -euo pipefail
11
11
  # --topic "<short topic>" \
12
12
  # [--recipient <name>] \
13
13
  # [--project <name>] \
14
+ # [--intent <action|feedback|fyi>] \
14
15
  # <<'HANDOFFEOF'
15
16
  # ...full markdown body of the handoff...
16
17
  # HANDOFFEOF
@@ -45,6 +46,7 @@ AUTHOR=""
45
46
  TOPIC=""
46
47
  RECIPIENT=""
47
48
  PROJECT=""
49
+ INTENT="action"
48
50
  NO_PUSH=0
49
51
  NO_NOTIFY=0
50
52
  NO_PUBLISH=0
@@ -56,6 +58,7 @@ while [ $# -gt 0 ]; do
56
58
  --topic) TOPIC="${2:?}"; shift 2 ;;
57
59
  --recipient) RECIPIENT="${2:-}"; shift 2 ;;
58
60
  --project) PROJECT="${2:-}"; shift 2 ;;
61
+ --intent) INTENT="${2:-}"; shift 2 ;;
59
62
  --composed) COMPOSED="${2:-}"; shift 2 ;;
60
63
  --no-push) NO_PUSH=1; shift ;;
61
64
  --no-notify) NO_NOTIFY=1; shift ;;
@@ -76,6 +79,10 @@ fi
76
79
 
77
80
  [ -z "$AUTHOR" ] && { echo "Missing --author" >&2; exit 1; }
78
81
  [ -z "$TOPIC" ] && { echo "Missing --topic" >&2; exit 1; }
82
+ case "$INTENT" in
83
+ action|feedback|fyi) ;;
84
+ *) echo "Invalid --intent: $INTENT (action|feedback|fyi)" >&2; exit 1 ;;
85
+ esac
79
86
 
80
87
  MODE=$(jq -r '.mode // "connected"' "$CONFIG" 2>/dev/null || echo "connected")
81
88
  TODAY=$(date +%Y-%m-%d)
@@ -115,6 +122,48 @@ if [ ! -s "$ABS_FILE" ]; then
115
122
  exit 1
116
123
  fi
117
124
 
125
+ # --- Canonical capture contract -----------------------------------------
126
+ # /handoff is the addressed door into the shared capture engine. Persist the
127
+ # same schema/mode fields used by /wrap and SessionEnd so filesystem and graph
128
+ # retrieval can distinguish captures without inferring from directory names.
129
+ if ! grep -qiE '^(capture_schema:|\*\*[Cc]apture [Ss]chema\*\*:)' "$ABS_FILE"; then
130
+ CAPTURE_KIND=""
131
+ if ! grep -qiE '^(kind:|\*\*[Kk]ind\*\*:)' "$ABS_FILE"; then
132
+ CAPTURE_KIND="addressed"
133
+ fi
134
+ if [ "$(head -1 "$ABS_FILE")" = "---" ]; then
135
+ awk -v kind="$CAPTURE_KIND" '
136
+ NR == 1 {
137
+ print
138
+ print "capture_schema: egregore-capture/v1"
139
+ print "capture_mode: addressed"
140
+ if (kind != "") print "kind: " kind
141
+ next
142
+ }
143
+ { print }
144
+ ' "$ABS_FILE" > "$ABS_FILE.new" && mv "$ABS_FILE.new" "$ABS_FILE"
145
+ else
146
+ awk -v kind="$CAPTURE_KIND" '
147
+ !injected && /^\*\*[Aa]uthor\*\*:/ {
148
+ print
149
+ print "**Capture Schema**: egregore-capture/v1"
150
+ print "**Capture Mode**: addressed"
151
+ if (kind != "") print "**Kind**: " kind
152
+ injected=1
153
+ next
154
+ }
155
+ { print }
156
+ END {
157
+ if (!injected) {
158
+ print "**Capture Schema**: egregore-capture/v1"
159
+ print "**Capture Mode**: addressed"
160
+ if (kind != "") print "**Kind**: " kind
161
+ }
162
+ }
163
+ ' "$ABS_FILE" > "$ABS_FILE.new" && mv "$ABS_FILE.new" "$ABS_FILE"
164
+ fi
165
+ fi
166
+
118
167
  # --- Inject **To**: line if --recipient given and body omits it -----
119
168
  # index-handoff.sh parses recipients from the file body. Skills
120
169
  # normally write the To line themselves, but callers that invoke
@@ -132,6 +181,29 @@ if [ -n "$RECIPIENT" ] && ! grep -qi "^\*\*[Tt]o\*\*:" "$ABS_FILE"; then
132
181
  ' "$ABS_FILE" > "$ABS_FILE.new" && mv "$ABS_FILE.new" "$ABS_FILE"
133
182
  fi
134
183
 
184
+ # --- Persist lifecycle intent in the canonical handoff file -------------
185
+ if ! grep -qiE '^(intent:|\*\*[Ii]ntent\*\*:)' "$ABS_FILE"; then
186
+ if [ "$(head -1 "$ABS_FILE")" = "---" ]; then
187
+ awk -v intent="$INTENT" '
188
+ NR == 1 { print; print "intent: " intent; next }
189
+ { print }
190
+ ' "$ABS_FILE" > "$ABS_FILE.new" && mv "$ABS_FILE.new" "$ABS_FILE"
191
+ else
192
+ awk -v intent="$INTENT" '
193
+ !injected && /^\*\*[Aa]uthor\*\*:/ {
194
+ print
195
+ print "**Intent**: " intent
196
+ injected=1
197
+ next
198
+ }
199
+ { print }
200
+ END {
201
+ if (!injected) print "**Intent**: " intent
202
+ }
203
+ ' "$ABS_FILE" > "$ABS_FILE.new" && mv "$ABS_FILE.new" "$ABS_FILE"
204
+ fi
205
+ fi
206
+
135
207
  # --- Append repo state ---------------------------------------------------
136
208
  # --no-pr skips `gh pr list` (~400–600ms per repo). A detached PR backfill
137
209
  # after the parallel phase rewrites `—` → `#N` and re-pushes memory.
@@ -31,13 +31,22 @@ TOPIC="${2:-session work}"
31
31
  shift 2 2>/dev/null || true
32
32
 
33
33
  REPO_DIR=""
34
+ KIND="handoff"
35
+ BRANCH_SUFFIX=""
34
36
  while [ $# -gt 0 ]; do
35
37
  case "$1" in
36
38
  --repo-dir) REPO_DIR="${2:-}"; shift 2 ;;
39
+ --kind) KIND="${2:-handoff}"; shift 2 ;;
40
+ --branch-suffix) BRANCH_SUFFIX="${2:-}"; shift 2 ;;
37
41
  *) shift ;;
38
42
  esac
39
43
  done
40
44
 
45
+ case "$KIND" in
46
+ autosave) MSG_PREFIX="Auto-save" ; PR_BODY="Auto-saved session content (non-coding). No explicit /save needed." ;;
47
+ *) MSG_PREFIX="Handoff" ; PR_BODY="Auto-created by /handoff. Session work for:" ;;
48
+ esac
49
+
41
50
  [ -n "$AUTHOR" ] || { echo "handoff-save-egregore: missing author" >&2; exit 1; }
42
51
 
43
52
  SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
@@ -50,6 +59,13 @@ cd "$REPO_DIR" || exit 1
50
59
  . "$SCRIPT_DIR/bin/lib/git-safe.sh" 2>/dev/null || true
51
60
  type git_add_guarded >/dev/null 2>&1 || git_add_guarded() { git add -A 2>/dev/null || true; }
52
61
 
62
+ # Shared non-coding classifier (auto-merge policy lives in one place).
63
+ # shellcheck source=bin/lib/noncode.sh
64
+ . "$SCRIPT_DIR/bin/lib/noncode.sh" 2>/dev/null || true
65
+ type noncode_blockers >/dev/null 2>&1 || noncode_blockers() {
66
+ git -C "${2:-.}" diff "${1:-origin/develop}...HEAD" --name-only 2>/dev/null | grep -v '\.md$' | head -1 || true
67
+ }
68
+
53
69
  # --- Early exit if nothing to do -----------------------------------------
54
70
  DIRTY=0
55
71
  [ -n "$(git status --porcelain 2>/dev/null)" ] && DIRTY=1
@@ -70,7 +86,9 @@ fi
70
86
  case "$CURRENT_BRANCH" in
71
87
  develop|main|master|"")
72
88
  SLUG_DATE=$(date +%Y-%m-%d)
73
- NEW_BRANCH="dev/${AUTHOR}/handoff-${SLUG_DATE}"
89
+ # --branch-suffix keeps concurrent rescues distinct (e.g. per-checkout
90
+ # hash from the autosave sweep) — same-name pushes wedge on non-FF.
91
+ NEW_BRANCH="dev/${AUTHOR}/${KIND}-${SLUG_DATE}${BRANCH_SUFFIX:+-$BRANCH_SUFFIX}"
74
92
  git fetch origin develop --quiet 2>/dev/null || true
75
93
  git checkout -b "$NEW_BRANCH" origin/develop --quiet 2>/dev/null || \
76
94
  git checkout "$NEW_BRANCH" --quiet 2>/dev/null || exit 0
@@ -81,7 +99,7 @@ esac
81
99
  # --- Commit whatever is uncommitted --------------------------------------
82
100
  if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
83
101
  git_add_guarded
84
- git commit -m "Handoff: ${TOPIC}" --quiet 2>/dev/null || true
102
+ git commit -m "${MSG_PREFIX}: ${TOPIC}" --quiet 2>/dev/null || true
85
103
  fi
86
104
 
87
105
  # --- Rebase onto develop, fall back to merge if rebase conflicts ---------
@@ -102,8 +120,21 @@ if ! git push -u origin "$CURRENT_BRANCH" --quiet 2>/dev/null; then
102
120
  exit 0
103
121
  fi
104
122
 
123
+ # --- Ledger: the user-visible trail of background saves ------------------
124
+ # Read by session-autosave.sh (immediate Stop-hook notice) and the greeting
125
+ # (next-launch summary). Machine-scoped so worktree saves surface everywhere.
126
+ LEDGER="${EGREGORE_AUTOSAVE_LOG:-$HOME/.egregore/autosave.log}"
127
+ ledger_write() { # pr outcome
128
+ local n_files
129
+ n_files=$(git diff origin/develop...HEAD --name-only 2>/dev/null | grep -c . || echo 0)
130
+ mkdir -p "$(dirname "$LEDGER")" 2>/dev/null || true
131
+ printf '%s|%s|%s|%s|%s|%s|%s\n' \
132
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$KIND" "$REPO_DIR" "$CURRENT_BRANCH" \
133
+ "$1" "$2" "$n_files" >> "$LEDGER" 2>/dev/null || true
134
+ }
135
+
105
136
  # --- PR: reuse open PR on this branch, or create one --------------------
106
- command -v gh >/dev/null 2>&1 || exit 0
137
+ command -v gh >/dev/null 2>&1 || { ledger_write "-" "pushed"; exit 0; }
107
138
 
108
139
  PR_NUMBER=""
109
140
  EXISTING=$(gh pr list --head "$CURRENT_BRANCH" --state open --json number,baseRefName --jq '.[0]' 2>/dev/null)
@@ -119,18 +150,27 @@ else
119
150
  PR_URL=$(gh pr create \
120
151
  --base develop \
121
152
  --head "$CURRENT_BRANCH" \
122
- --title "Handoff: ${TOPIC}" \
123
- --body "Auto-created by /handoff. Session work for: ${TOPIC}" \
153
+ --title "${MSG_PREFIX}: ${TOPIC}" \
154
+ --body "${PR_BODY} ${TOPIC}" \
124
155
  2>/dev/null || echo "")
125
156
  PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$' || echo "")
126
157
  fi
127
158
 
128
- [ -n "$PR_NUMBER" ] || exit 0
129
-
130
- # --- Auto-merge iff markdown-only ---------------------------------------
131
- NON_MD=$(git diff origin/develop --name-only 2>/dev/null | grep -v '\.md$' | head -1 || echo "")
132
- if [ -z "$NON_MD" ]; then
133
- gh pr merge "$PR_NUMBER" --auto --merge >/dev/null 2>&1 || true
159
+ [ -n "$PR_NUMBER" ] || { ledger_write "-" "pushed"; exit 0; }
160
+
161
+ # --- Auto-merge iff non-coding -------------------------------------------
162
+ # `--auto` only works when the repo allows auto-merge AND required checks are
163
+ # pending; on a repo with no branch protection it always errors. Fall back to
164
+ # an immediate merge so non-coding PRs never sit open.
165
+ OUTCOME="open"
166
+ NON_CODE_BLOCKER=$(noncode_blockers origin/develop .)
167
+ if [ -z "$NON_CODE_BLOCKER" ]; then
168
+ if gh pr merge "$PR_NUMBER" --auto --merge >/dev/null 2>&1 \
169
+ || gh pr merge "$PR_NUMBER" --merge >/dev/null 2>&1; then
170
+ OUTCOME="merged"
171
+ fi
134
172
  fi
135
173
 
174
+ ledger_write "$PR_NUMBER" "$OUTCOME"
175
+
136
176
  exit 0
@@ -3,7 +3,9 @@ set -euo pipefail
3
3
 
4
4
  # Index a single handoff file into the Neo4j graph.
5
5
  # Usage: bash bin/index-handoff.sh <file-path>
6
- # Returns: {"sessionId":"...","resolved":N} or {"error":"..."}
6
+ # Returns: {"sessionId":"...","resolved":0} or {"error":"..."}.
7
+ # `resolved` remains for compatibility; lifecycle reconciliation is queued by
8
+ # capture-run.sh after indexing.
7
9
  #
8
10
  # The file-path should be relative to memory/ (e.g. handoffs/2026-02/14-oz-topic.md)
9
11
  # or an absolute/relative path that contains "handoffs/" somewhere in it.
@@ -16,7 +18,7 @@ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
16
18
  echo "Index a single handoff file into the Neo4j graph."
17
19
  echo "Parses metadata (author, date, topic, recipients) from"
18
20
  echo "markdown headers or YAML front matter, creates Session"
19
- echo "and relationship nodes, and auto-resolves old handoffs."
21
+ echo "and relationship nodes. Lifecycle completion is queued separately."
20
22
  echo ""
21
23
  echo "Returns: {\"sessionId\":\"...\",\"resolved\":N}"
22
24
  exit 0
@@ -135,6 +137,15 @@ PROJECT="$(md_field 'Project')"
135
137
  KIND="$(md_field 'Kind')"
136
138
  [ -z "$KIND" ] && KIND="$(yaml_extract 'kind')"
137
139
 
140
+ # Lifecycle intent determines which automatic transitions are safe.
141
+ INTENT="$(md_field 'Intent')"
142
+ [ -z "$INTENT" ] && INTENT="$(yaml_extract 'intent')"
143
+ INTENT="$(printf '%s' "${INTENT:-unclassified}" | tr '[:upper:]' '[:lower:]')"
144
+ case "$INTENT" in
145
+ action|feedback|fyi|unclassified) ;;
146
+ *) INTENT="unclassified" ;;
147
+ esac
148
+
138
149
  # Summary: 5-level precedence chain
139
150
  # 1. Frontmatter summary field
140
151
  SUMMARY="$(yaml_extract 'summary')"
@@ -180,7 +191,7 @@ if [ -n "$RECIPIENTS_RAW" ]; then
180
191
  [ -z "$R" ] && continue
181
192
  HANDED_TO_CYPHER="${HANDED_TO_CYPHER}
182
193
  WITH s
183
- OPTIONAL MATCH (r${i}:Person) WHERE toLower(r${i}.name) = \$recipient${i} OR r${i}.github = \$recipient${i} OR toLower(r${i}.fullName) = \$recipient${i}
194
+ OPTIONAL MATCH (r${i}:Person) WHERE toLower(r${i}.name) = \$recipient${i} OR toLower(r${i}.github) = \$recipient${i} OR toLower(r${i}.fullName) = \$recipient${i} OR \$recipient${i} IN [x IN coalesce(r${i}.previousNames, []) | toLower(x)] OR \$recipient${i} IN [x IN coalesce(r${i}.githubAliases, []) | toLower(x)] OR \$recipient${i} IN [x IN coalesce(r${i}.emails, []) | toLower(x)]
184
195
  FOREACH (_ IN CASE WHEN r${i} IS NOT NULL THEN [1] ELSE [] END |
185
196
  MERGE (s)-[:HANDED_TO]->(r${i})
186
197
  )"
@@ -200,10 +211,10 @@ FOREACH (_ IN CASE WHEN proj IS NOT NULL THEN [1] ELSE [] END |
200
211
  _PROJECT_ARGS=("--arg" "project" "$PROJECT")
201
212
  fi
202
213
 
203
- Q1_CYPHER="MATCH (p:Person) WHERE toLower(p.name) = \$author OR p.github = \$author OR toLower(p.fullName) = \$author OR \$author IN [x IN coalesce(p.previousNames, []) | toLower(x)]
214
+ Q1_CYPHER="MATCH (p:Person) WHERE toLower(p.name) = \$author OR toLower(p.github) = \$author OR toLower(p.fullName) = \$author OR \$author IN [x IN coalesce(p.previousNames, []) | toLower(x)] OR \$author IN [x IN coalesce(p.githubAliases, []) | toLower(x)] OR \$author IN [x IN coalesce(p.emails, []) | toLower(x)]
204
215
  MERGE (s:Session {id: \$sessionId})
205
- ON CREATE SET s.date = date(\$date), s.topic = \$topic, s.summary = \$summary, s.filePath = \$filePath, s.author = \$author, s.handoffKind = CASE WHEN \$kind = '' THEN null ELSE \$kind END, s.handoffStatus = 'pending', s.repoState = \$repoState, s.startedAt = datetime(\$date + 'T00:00:00Z')
206
- ON MATCH SET s.topic = \$topic, s.summary = \$summary, s.filePath = \$filePath, s.author = \$author, s.handoffKind = CASE WHEN \$kind = '' THEN null ELSE \$kind END, s.repoState = \$repoState
216
+ ON CREATE SET s.date = date(\$date), s.topic = \$topic, s.summary = \$summary, s.filePath = \$filePath, s.author = \$author, s.handoffKind = CASE WHEN \$kind = '' THEN null ELSE \$kind END, s.handoffStatus = 'pending', s.handoffIntent = \$intent, s.handoffLifecycleVersion = 1, s.repoState = \$repoState, s.startedAt = datetime(\$date + 'T00:00:00Z')
217
+ ON MATCH SET s.topic = \$topic, s.summary = \$summary, s.filePath = \$filePath, s.author = \$author, s.handoffKind = CASE WHEN \$kind = '' THEN null ELSE \$kind END, s.handoffIntent = CASE WHEN \$intent = 'unclassified' THEN coalesce(s.handoffIntent, 'unclassified') ELSE \$intent END, s.handoffLifecycleVersion = 1, s.repoState = \$repoState
207
218
  WITH s, p
208
219
  MERGE (s)-[:BY]->(p)${PROJECT_CYPHER}${HANDED_TO_CYPHER}
209
220
  RETURN s.id AS sessionId"
@@ -216,22 +227,20 @@ Q1_PARAMS=$(jq -n \
216
227
  --arg summary "$SUMMARY" \
217
228
  --arg filePath "$REL_PATH" \
218
229
  --arg kind "$KIND" \
230
+ --arg intent "$INTENT" \
219
231
  --arg repoState "$REPO_STATE_JSON" \
220
232
  "${_PROJECT_ARGS[@]:+${_PROJECT_ARGS[@]}}" \
221
233
  "${_RECIP_ARGS[@]:+${_RECIP_ARGS[@]}}" \
222
234
  '$ARGS.named')
223
235
 
224
- # Q2: Auto-resolve old read handoffs from this author
225
- Q2_CYPHER="MATCH (s:Session)-[:HANDED_TO]->(p:Person) WHERE (toLower(p.name) = \$author OR p.github = \$author OR toLower(p.fullName) = \$author) AND s.handoffStatus = 'read' AND s.id <> \$sessionId
226
- WITH s, p, coalesce(s.handoffReadDate, s.date) AS sinceDate
227
- MATCH (later:Session)-[:BY]->(p)
228
- WHERE later.date > sinceDate
229
- WITH s, count(later) AS laterSessions WHERE laterSessions > 0
230
- SET s.handoffStatus = 'done'
231
- RETURN count(s) AS resolved"
232
-
233
- Q2_PARAMS=$(jq -n --arg author "$AUTHOR_HANDLE" --arg sessionId "$SESSION_ID" \
234
- '{author: $author, sessionId: $sessionId}')
236
+ # Q2: Preserve the stable three-result batch shape without mutating lifecycle.
237
+ # Completion used to run inside this foreground indexing request. It now enters
238
+ # through capture-run.sh's local WAL queue and is reconciled off the caller's
239
+ # critical path.
240
+ Q2_CYPHER="MATCH (s:Session {id: \$sessionId})
241
+ RETURN 0 AS resolved"
242
+
243
+ Q2_PARAMS=$(jq -n --arg sessionId "$SESSION_ID" '{sessionId: $sessionId}')
235
244
 
236
245
  # Q3: Capture the handoff's knowledge-graph neighborhood for artifact rendering.
237
246
  # Only the "graph-only" relations — things not already visible in the markdown
@@ -0,0 +1,80 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # Validate, plan, or apply a durable ingestion manifest to the organization graph.
5
+ # The planner owns the source-to-graph contract; this wrapper owns mode detection,
6
+ # batching, and truthful delivery receipts.
7
+
8
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
9
+ SCRIPT_DIR="$ROOT"
10
+ CONFIG="${CONFIG:-$ROOT/egregore.json}"
11
+ PLANNER="${EGREGORE_INGEST_GRAPH_PLANNER:-$ROOT/bin/ingest_graph.py}"
12
+ GRAPH_BATCH="${EGREGORE_INGEST_GRAPH_BATCH:-$ROOT/bin/graph-batch.sh}"
13
+
14
+ # shellcheck source=bin/lib/config.sh
15
+ source "$ROOT/bin/lib/config.sh"
16
+
17
+ usage() {
18
+ echo "usage: bash bin/ingest-graph.sh {validate|plan|apply} MANIFEST" >&2
19
+ exit 2
20
+ }
21
+
22
+ mode="${1:-}"
23
+ manifest="${2:-}"
24
+ [ -n "$mode" ] && [ -n "$manifest" ] || usage
25
+
26
+ case "$mode" in
27
+ validate|plan)
28
+ exec python3 "$PLANNER" "$mode" "$manifest"
29
+ ;;
30
+ apply) ;;
31
+ *) usage ;;
32
+ esac
33
+
34
+ plan="$(python3 "$PLANNER" plan "$manifest")"
35
+ manifest_id="$(printf '%s' "$plan" | jq -r '.manifest_id')"
36
+ query_count="$(printf '%s' "$plan" | jq -r '.query_count')"
37
+
38
+ if [ "$(_detect_mode)" = "local" ]; then
39
+ jq -cn \
40
+ --arg manifest_id "$manifest_id" \
41
+ --argjson queries "$query_count" \
42
+ '{status:"stored",graph:"unavailable-in-local-mode",manifest_id:$manifest_id,queries:$queries,batches:0,failed:0,replayable:true}'
43
+ exit 0
44
+ fi
45
+
46
+ total=0
47
+ succeeded=0
48
+ failed=0
49
+ while IFS= read -r batch; do
50
+ [ -n "$batch" ] || continue
51
+ total=$((total + 1))
52
+ expected="$(printf '%s' "$batch" | jq 'length')"
53
+ if response="$(bash "$GRAPH_BATCH" "$batch" 2>/dev/null)" \
54
+ && actual="$(printf '%s' "$response" | jq -er '.results | length' 2>/dev/null)" \
55
+ && [ "$actual" -eq "$expected" ]; then
56
+ succeeded=$((succeeded + 1))
57
+ else
58
+ failed=$((failed + 1))
59
+ fi
60
+ done < <(
61
+ printf '%s' "$plan" |
62
+ jq -c '.queries | map({statement, parameters}) as $all | range(0; length; 20) as $i | $all[$i:$i+20]'
63
+ )
64
+
65
+ if [ "$failed" -gt 0 ]; then
66
+ jq -cn \
67
+ --arg manifest_id "$manifest_id" \
68
+ --argjson queries "$query_count" \
69
+ --argjson batches "$total" \
70
+ --argjson succeeded "$succeeded" \
71
+ --argjson failed "$failed" \
72
+ '{status:"partial",manifest_id:$manifest_id,queries:$queries,batches:$batches,succeeded:$succeeded,failed:$failed,replayable:true}'
73
+ exit 1
74
+ fi
75
+
76
+ jq -cn \
77
+ --arg manifest_id "$manifest_id" \
78
+ --argjson queries "$query_count" \
79
+ --argjson batches "$total" \
80
+ '{status:"projected",manifest_id:$manifest_id,queries:$queries,batches:$batches,failed:0,replayable:true}'