create-egregore 0.19.33 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/egregore-launcher.js +118 -13
- package/bin/cli.js +19 -3
- package/lib/connect-runtime.js +4 -2
- package/lib/local.js +15 -1
- package/lib/prime-runtime.js +208 -0
- package/lib/setup.js +45 -3
- package/package.json +4 -3
- package/runtime/codex/.codex/skills/ingest-notion/SKILL.md +29 -0
- package/runtime/codex/.codex/skills/notion-connect/SKILL.md +29 -0
- package/runtime/codex/.codex/skills/teams-connect/SKILL.md +29 -0
- package/runtime/codex/.codex/spec-manifest.json +3 -3
- package/runtime/codex/AGENTS.md +11 -4
- package/runtime/codex/bin/activity-data.sh +4 -8
- package/runtime/codex/bin/attendant.sh +13 -0
- package/runtime/codex/bin/autosave.sh +27 -1
- package/runtime/codex/bin/codex-render-spec.mjs +6 -0
- package/runtime/codex/bin/codex-session-start.sh +3 -1
- package/runtime/codex/bin/graph-batch.sh +25 -6
- package/runtime/codex/bin/graph-op.sh +5 -4
- package/runtime/codex/bin/graph.sh +31 -5
- package/runtime/codex/bin/ingest.py +81 -5
- package/runtime/codex/bin/lib/context.sh +26 -1
- package/runtime/codex/bin/lib/greeting.sh +4 -1
- package/runtime/codex/bin/lib/worktree-links.sh +25 -0
- package/runtime/codex/manifest.json +7 -1
- package/runtime/pi/.codex/skills/ingest-notion/SKILL.md +29 -0
- package/runtime/pi/.codex/skills/notion-connect/SKILL.md +29 -0
- package/runtime/pi/.codex/skills/teams-connect/SKILL.md +29 -0
- package/runtime/pi/.codex/spec-manifest.json +3 -3
- package/runtime/pi/.pi/APPEND_SYSTEM.md +3 -2
- package/runtime/pi/.pi/extensions/egregore.ts +17 -6
- package/runtime/pi/.pi/spec-manifest.json +2 -2
- package/runtime/pi/AGENTS.md +11 -4
- package/runtime/pi/bin/activity-data.sh +4 -8
- package/runtime/pi/bin/attendant.sh +13 -0
- package/runtime/pi/bin/autosave.sh +27 -1
- package/runtime/pi/bin/codex-render-spec.mjs +6 -0
- package/runtime/pi/bin/codex-session-start.sh +3 -1
- package/runtime/pi/bin/graph-batch.sh +25 -6
- package/runtime/pi/bin/graph-op.sh +5 -4
- package/runtime/pi/bin/graph.sh +31 -5
- package/runtime/pi/bin/ingest.py +81 -5
- package/runtime/pi/bin/lib/context.sh +26 -1
- package/runtime/pi/bin/lib/greeting.sh +4 -1
- package/runtime/pi/bin/lib/worktree-links.sh +25 -0
- package/runtime/pi/bin/pi-branch-policy.mjs +138 -1
- package/runtime/pi/bin/pi-product-workflows.mjs +35 -7
- package/runtime/pi/manifest.json +7 -1
- package/runtime/prime/.claude/context/notification-consent.md +66 -0
- package/runtime/prime/.codex/config.toml +17 -0
- package/runtime/prime/.codex/hooks/branch-guard.js +461 -0
- package/runtime/prime/.codex/hooks/search-hint.js +67 -0
- package/runtime/prime/.codex/hooks.json +30 -0
- package/runtime/prime/.codex/skills/activity/SKILL.md +82 -0
- package/runtime/prime/.codex/skills/announce/SKILL.md +67 -0
- package/runtime/prime/.codex/skills/archive/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/branch/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/checkup/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/commit/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/contribute/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/create-skill/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/dark-mode/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/dashboard/SKILL.md +59 -0
- package/runtime/prime/.codex/skills/deep-reflect/SKILL.md +109 -0
- package/runtime/prime/.codex/skills/delete-user/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/emissary/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/handoff/SKILL.md +166 -0
- package/runtime/prime/.codex/skills/harvest/QUESTION_PALETTE.md +76 -0
- package/runtime/prime/.codex/skills/harvest/SKILL.md +131 -0
- package/runtime/prime/.codex/skills/ingest/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/ingest-notion/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/ingest-user-interview/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/invite/SKILL.md +44 -0
- package/runtime/prime/.codex/skills/issue/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/me/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/meeting/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/note/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/notion-connect/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/onboarding/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/pr/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/pull/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/push/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/quest/SKILL.md +80 -0
- package/runtime/prime/.codex/skills/reflect/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/review-pr/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/save/SKILL.md +125 -0
- package/runtime/prime/.codex/skills/slack-connect/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/summon/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/sync-repos/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/teams-connect/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/telegram-connect/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/telemetry/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/test/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/the-spiral/SKILL.md +84 -0
- package/runtime/prime/.codex/skills/tui-design/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/tutorial/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/update/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/view/SKILL.md +156 -0
- package/runtime/prime/.codex/skills/view/agents/openai.yaml +4 -0
- package/runtime/prime/.codex/skills/view/assets/compose-scaffold.html +221 -0
- package/runtime/prime/.codex/skills/visual-explain/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/wrap/SKILL.md +58 -0
- package/runtime/prime/.codex/spec-manifest.json +132 -0
- package/runtime/prime/.prime/agent/APPEND_SYSTEM.md +393 -0
- package/runtime/prime/.prime/agent/extensions/egregore.ts +460 -0
- package/runtime/prime/.prime/agent/settings.json +4 -0
- package/runtime/prime/.prime/agent/skills/egregore/SKILL.md +34 -0
- package/runtime/prime/.prime/agent/skills/egregore/pyproject.toml +13 -0
- package/runtime/prime/.prime/agent/skills/egregore/src/egregore/__init__.py +156 -0
- package/runtime/prime/.prime/agent/spec-manifest.json +12 -0
- package/runtime/prime/AGENTS.md +489 -0
- package/runtime/prime/bin/activity-data.sh +238 -0
- package/runtime/prime/bin/agent.sh +775 -0
- package/runtime/prime/bin/attendant.sh +707 -0
- package/runtime/prime/bin/autosave.sh +112 -0
- package/runtime/prime/bin/capture-reconcile.sh +69 -0
- package/runtime/prime/bin/capture-run.sh +341 -0
- package/runtime/prime/bin/codex-install-skills.sh +78 -0
- package/runtime/prime/bin/codex-render-spec.mjs +440 -0
- package/runtime/prime/bin/codex-session-start.sh +265 -0
- package/runtime/prime/bin/codex-skill-render.mjs +347 -0
- package/runtime/prime/bin/codex-sync-skills.sh +232 -0
- package/runtime/prime/bin/connect-refresh.sh +33 -0
- package/runtime/prime/bin/dashboard-data.sh +178 -0
- package/runtime/prime/bin/graph-batch.sh +79 -0
- package/runtime/prime/bin/graph-op.sh +1102 -0
- package/runtime/prime/bin/graph-wal.sh +229 -0
- package/runtime/prime/bin/graph.sh +223 -0
- package/runtime/prime/bin/handoff-pr-backfill.sh +86 -0
- package/runtime/prime/bin/handoff-run.sh +671 -0
- package/runtime/prime/bin/handoff-save-egregore.sh +212 -0
- package/runtime/prime/bin/index-handoff.sh +322 -0
- package/runtime/prime/bin/ingest-graph.sh +80 -0
- package/runtime/prime/bin/ingest-surface.html +495 -0
- package/runtime/prime/bin/ingest.py +756 -0
- package/runtime/prime/bin/ingest.sh +279 -0
- package/runtime/prime/bin/ingest_graph.py +856 -0
- package/runtime/prime/bin/ingest_surface.py +544 -0
- package/runtime/prime/bin/lib/artifact-id.sh +46 -0
- package/runtime/prime/bin/lib/config.sh +116 -0
- package/runtime/prime/bin/lib/context.sh +621 -0
- package/runtime/prime/bin/lib/dashboard-artifact.sh +242 -0
- package/runtime/prime/bin/lib/ensure-develop.sh +183 -0
- package/runtime/prime/bin/lib/git-sync.sh +406 -0
- package/runtime/prime/bin/lib/greeting.sh +519 -0
- package/runtime/prime/bin/lib/hash.sh +13 -0
- package/runtime/prime/bin/lib/identity.sh +213 -0
- package/runtime/prime/bin/lib/metrics.sh +374 -0
- package/runtime/prime/bin/lib/noncode.sh +31 -0
- package/runtime/prime/bin/lib/permissions.sh +106 -0
- package/runtime/prime/bin/lib/time.sh +56 -0
- package/runtime/prime/bin/lib/worktree-links.sh +79 -0
- package/runtime/prime/bin/notify.sh +636 -0
- package/runtime/prime/bin/person.py +630 -0
- package/runtime/prime/bin/person.sh +448 -0
- package/runtime/prime/bin/pi-branch-policy.mjs +312 -0
- package/runtime/prime/bin/pi-product-workflows.mjs +394 -0
- package/runtime/prime/bin/prime-render-spec.mjs +123 -0
- package/runtime/prime/bin/prime-session-start.sh +24 -0
- package/runtime/prime/bin/publish-artifact.sh +271 -0
- package/runtime/prime/bin/publish-references.sh +144 -0
- package/runtime/prime/bin/repo-state.sh +96 -0
- package/runtime/prime/bin/search.sh +240 -0
- package/runtime/prime/bin/session-autosave.sh +209 -0
- package/runtime/prime/bin/session-log.sh +199 -0
- package/runtime/prime/bin/settings.sh +304 -0
- package/runtime/prime/bin/telemetry.sh +391 -0
- package/runtime/prime/bin/worktree-create.sh +128 -0
- package/runtime/prime/bin/worktree-remove.sh +50 -0
- package/runtime/prime/bin/worktree.sh +198 -0
- package/runtime/prime/manifest.json +178 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# handoff-save-egregore.sh — background save+auto-merge for the core repo after a handoff.
|
|
3
|
+
#
|
|
4
|
+
# /handoff renders its card first, then fires this detached. The user never waits on git.
|
|
5
|
+
#
|
|
6
|
+
# Behavior:
|
|
7
|
+
# - If nothing to save (clean working tree + up-to-date with origin) → exit silently.
|
|
8
|
+
# - If on the configured base (or another protected branch) → create a
|
|
9
|
+
# dev/{author}/handoff-{YYYY-MM-DD} branch from origin/{base}.
|
|
10
|
+
# - Commit all changes, rebase onto origin/{base}, push.
|
|
11
|
+
# - If push succeeds: create a PR to the configured base (or reuse existing), then:
|
|
12
|
+
# - Markdown-only diff → `gh pr merge --auto --merge` (auto-merges when checks pass).
|
|
13
|
+
# - Code/config present → leave PR open for review (no auto-merge).
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# handoff-save-egregore.sh <author> <topic> [--repo-dir <path>]
|
|
17
|
+
#
|
|
18
|
+
# --repo-dir overrides the default working directory (normally the script's
|
|
19
|
+
# own repo root, resolved from $0). Tests and sandbox runs MUST pass it;
|
|
20
|
+
# without it, the script operates on the real repo regardless of caller cwd.
|
|
21
|
+
#
|
|
22
|
+
# Detached usage (the skill calls it this way):
|
|
23
|
+
# ( bash bin/handoff-save-egregore.sh "$AUTHOR" "$TOPIC" >/dev/null 2>&1 & ) >/dev/null 2>&1
|
|
24
|
+
#
|
|
25
|
+
# Exit 0 on any non-fatal outcome. The caller never reads stdout/stderr — all status
|
|
26
|
+
# lives in the PR itself and in `git log`.
|
|
27
|
+
|
|
28
|
+
set -uo pipefail
|
|
29
|
+
|
|
30
|
+
AUTHOR="${1:-}"
|
|
31
|
+
TOPIC="${2:-session work}"
|
|
32
|
+
shift 2 2>/dev/null || true
|
|
33
|
+
|
|
34
|
+
REPO_DIR=""
|
|
35
|
+
KIND="handoff"
|
|
36
|
+
BRANCH_SUFFIX=""
|
|
37
|
+
PUBLISH="auto"
|
|
38
|
+
while [ $# -gt 0 ]; do
|
|
39
|
+
case "$1" in
|
|
40
|
+
--repo-dir) REPO_DIR="${2:-}"; shift 2 ;;
|
|
41
|
+
--kind) KIND="${2:-handoff}"; shift 2 ;;
|
|
42
|
+
--branch-suffix) BRANCH_SUFFIX="${2:-}"; shift 2 ;;
|
|
43
|
+
--publish) PUBLISH="${2:-auto}"; shift 2 ;;
|
|
44
|
+
*) shift ;;
|
|
45
|
+
esac
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
case "$KIND" in
|
|
49
|
+
autosave)
|
|
50
|
+
MSG_PREFIX="Auto-save"
|
|
51
|
+
PR_WHAT="Save session content captured by the opted-in autosave workflow."
|
|
52
|
+
PR_WHY="This preserves unattended non-coding work without requiring an explicit /save."
|
|
53
|
+
;;
|
|
54
|
+
*)
|
|
55
|
+
MSG_PREFIX="Handoff"
|
|
56
|
+
PR_WHAT="Save core-repository session work captured when the handoff was created."
|
|
57
|
+
PR_WHY="The handoff records shared context immediately; this companion PR keeps the originating checkout's work from remaining only local."
|
|
58
|
+
;;
|
|
59
|
+
esac
|
|
60
|
+
PR_BODY=$(printf '## What\n\n- %s\n\n## Why\n\n%s Topic: `%s`.\n\n## Verification\n\nNot verified — background %s save; review code or configuration changes before merging.\n\n🤖 Saved via `bin/handoff-save-egregore.sh`\n' \
|
|
61
|
+
"$PR_WHAT" "$PR_WHY" "$TOPIC" "$KIND")
|
|
62
|
+
|
|
63
|
+
[ -n "$AUTHOR" ] || { echo "handoff-save-egregore: missing author" >&2; exit 1; }
|
|
64
|
+
|
|
65
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
66
|
+
# If --repo-dir wasn't given, default to the script's own repo root.
|
|
67
|
+
[ -z "$REPO_DIR" ] && REPO_DIR="$SCRIPT_DIR"
|
|
68
|
+
cd "$REPO_DIR" || exit 1
|
|
69
|
+
|
|
70
|
+
# Resolve the target checkout's configured integration branch. SCRIPT_DIR points
|
|
71
|
+
# at the framework that owns this helper, while CONFIG must follow --repo-dir so
|
|
72
|
+
# sandboxed and installed instances read their own egregore.json.
|
|
73
|
+
CONFIG="$REPO_DIR/egregore.json"
|
|
74
|
+
[ -f "$SCRIPT_DIR/bin/lib/config.sh" ] || {
|
|
75
|
+
echo "handoff-save-egregore: base-branch resolver is unavailable" >&2
|
|
76
|
+
exit 1
|
|
77
|
+
}
|
|
78
|
+
# shellcheck source=bin/lib/config.sh
|
|
79
|
+
. "$SCRIPT_DIR/bin/lib/config.sh" 2>/dev/null || exit 1
|
|
80
|
+
if ! BASE_BRANCH=$(_get_base_branch); then
|
|
81
|
+
echo "handoff-save-egregore: could not resolve the configured base branch" >&2
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
BASE_REF="origin/$BASE_BRANCH"
|
|
85
|
+
|
|
86
|
+
# Shared safe-staging guard — never auto-commit stray sibling-repo gitlinks.
|
|
87
|
+
# shellcheck source=bin/lib/git-safe.sh
|
|
88
|
+
. "$SCRIPT_DIR/bin/lib/git-safe.sh" 2>/dev/null || true
|
|
89
|
+
type git_add_guarded >/dev/null 2>&1 || git_add_guarded() { git add -A 2>/dev/null || true; }
|
|
90
|
+
|
|
91
|
+
# Shared non-coding classifier (auto-merge policy lives in one place).
|
|
92
|
+
# shellcheck source=bin/lib/noncode.sh
|
|
93
|
+
. "$SCRIPT_DIR/bin/lib/noncode.sh" 2>/dev/null || true
|
|
94
|
+
type noncode_blockers >/dev/null 2>&1 || noncode_blockers() {
|
|
95
|
+
git -C "${2:-.}" diff "${1:-$BASE_REF}...HEAD" --name-only 2>/dev/null | grep -v '\.md$' | head -1 || true
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# --- Early exit if nothing to do -----------------------------------------
|
|
99
|
+
DIRTY=0
|
|
100
|
+
[ -n "$(git status --porcelain 2>/dev/null)" ] && DIRTY=1
|
|
101
|
+
|
|
102
|
+
CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || echo "")
|
|
103
|
+
AHEAD=0
|
|
104
|
+
if [ -n "$CURRENT_BRANCH" ]; then
|
|
105
|
+
# A missing remote base is not evidence that there is nothing to save. The
|
|
106
|
+
# former `|| echo 0` made a main-only instance silently discard clean,
|
|
107
|
+
# committed topic work because origin/develop did not exist.
|
|
108
|
+
if ! AHEAD=$(git rev-list --count "$BASE_REF..HEAD" 2>/dev/null); then
|
|
109
|
+
AHEAD=1
|
|
110
|
+
fi
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
if [ "$DIRTY" = "0" ] && [ "$AHEAD" = "0" ]; then
|
|
114
|
+
exit 0
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# --- Protect the protected branches: create a working branch if needed --
|
|
118
|
+
if [ -z "$CURRENT_BRANCH" ] || [ "$CURRENT_BRANCH" = "$BASE_BRANCH" ] \
|
|
119
|
+
|| [ "$CURRENT_BRANCH" = "develop" ] || [ "$CURRENT_BRANCH" = "main" ] \
|
|
120
|
+
|| [ "$CURRENT_BRANCH" = "master" ]; then
|
|
121
|
+
SLUG_DATE=$(date +%Y-%m-%d)
|
|
122
|
+
# --branch-suffix keeps concurrent rescues distinct (e.g. per-checkout
|
|
123
|
+
# hash from the autosave sweep) — same-name pushes wedge on non-FF.
|
|
124
|
+
NEW_BRANCH="dev/${AUTHOR}/${KIND}-${SLUG_DATE}${BRANCH_SUFFIX:+-$BRANCH_SUFFIX}"
|
|
125
|
+
git fetch origin "$BASE_BRANCH" --quiet 2>/dev/null || true
|
|
126
|
+
git checkout -b "$NEW_BRANCH" "$BASE_REF" --quiet 2>/dev/null || \
|
|
127
|
+
git checkout "$NEW_BRANCH" --quiet 2>/dev/null || exit 0
|
|
128
|
+
CURRENT_BRANCH="$NEW_BRANCH"
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# --- Commit whatever is uncommitted --------------------------------------
|
|
132
|
+
if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
|
|
133
|
+
git_add_guarded
|
|
134
|
+
git commit -m "${MSG_PREFIX}: ${TOPIC}" --quiet 2>/dev/null || true
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
# --- Rebase onto the configured base, fall back to merge on conflicts ----
|
|
138
|
+
git fetch origin "$BASE_BRANCH" --quiet 2>/dev/null || true
|
|
139
|
+
if ! git rebase "$BASE_REF" --quiet 2>/dev/null; then
|
|
140
|
+
git rebase --abort 2>/dev/null || true
|
|
141
|
+
if ! git merge "$BASE_REF" --quiet -m "Sync with $BASE_BRANCH" 2>/dev/null; then
|
|
142
|
+
# Conflicts we can't auto-resolve. Leave the branch as-is; the user will see it
|
|
143
|
+
# next session or when they run /save explicitly.
|
|
144
|
+
git merge --abort 2>/dev/null || true
|
|
145
|
+
exit 0
|
|
146
|
+
fi
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
# --- Push ---------------------------------------------------------------
|
|
150
|
+
if ! git push -u origin "$CURRENT_BRANCH" --quiet 2>/dev/null; then
|
|
151
|
+
# Push failed (network, auth). Commits are safe locally.
|
|
152
|
+
exit 0
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
# --- Ledger: the user-visible trail of background saves ------------------
|
|
156
|
+
# Read by session-autosave.sh (immediate Stop-hook notice) and the greeting
|
|
157
|
+
# (next-launch summary). Machine-scoped so worktree saves surface everywhere.
|
|
158
|
+
LEDGER="${EGREGORE_AUTOSAVE_LOG:-$HOME/.egregore/autosave.log}"
|
|
159
|
+
ledger_write() { # pr outcome
|
|
160
|
+
local n_files
|
|
161
|
+
n_files=$(git diff "$BASE_REF...HEAD" --name-only 2>/dev/null | grep -c . || echo 0)
|
|
162
|
+
mkdir -p "$(dirname "$LEDGER")" 2>/dev/null || true
|
|
163
|
+
printf '%s|%s|%s|%s|%s|%s|%s\n' \
|
|
164
|
+
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$KIND" "$REPO_DIR" "$CURRENT_BRANCH" \
|
|
165
|
+
"$1" "$2" "$n_files" >> "$LEDGER" 2>/dev/null || true
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# --- PR: reuse open PR on this branch, or create one --------------------
|
|
169
|
+
command -v gh >/dev/null 2>&1 || { ledger_write "-" "pushed"; exit 0; }
|
|
170
|
+
|
|
171
|
+
PR_NUMBER=""
|
|
172
|
+
EXISTING=$(gh pr list --head "$CURRENT_BRANCH" --state open --json number,baseRefName --jq '.[0]' 2>/dev/null)
|
|
173
|
+
EXISTING_NUMBER=$(echo "$EXISTING" | jq -r '.number // empty' 2>/dev/null)
|
|
174
|
+
EXISTING_BASE=$(echo "$EXISTING" | jq -r '.baseRefName // empty' 2>/dev/null)
|
|
175
|
+
|
|
176
|
+
if [ -n "$EXISTING_NUMBER" ]; then
|
|
177
|
+
PR_NUMBER="$EXISTING_NUMBER"
|
|
178
|
+
if [ "$EXISTING_BASE" != "$BASE_BRANCH" ]; then
|
|
179
|
+
gh pr edit "$PR_NUMBER" --base "$BASE_BRANCH" >/dev/null 2>&1 || true
|
|
180
|
+
fi
|
|
181
|
+
else
|
|
182
|
+
PR_URL=$(gh pr create \
|
|
183
|
+
--base "$BASE_BRANCH" \
|
|
184
|
+
--head "$CURRENT_BRANCH" \
|
|
185
|
+
--title "${MSG_PREFIX}: ${TOPIC}" \
|
|
186
|
+
--body "$PR_BODY" \
|
|
187
|
+
2>/dev/null || echo "")
|
|
188
|
+
PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$' || echo "")
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
[ -n "$PR_NUMBER" ] || { ledger_write "-" "pushed"; exit 0; }
|
|
192
|
+
|
|
193
|
+
# --- Merge decision -------------------------------------------------------
|
|
194
|
+
# Non-coding + publish=auto → merge now. publish=gate → the PR is staged and
|
|
195
|
+
# the merge waits for the user's explicit word (the /autosave consent gate).
|
|
196
|
+
# `--auto` only works when the repo allows auto-merge AND required checks are
|
|
197
|
+
# pending; on a repo with no branch protection it always errors. Fall back to
|
|
198
|
+
# an immediate merge so non-coding PRs never sit open.
|
|
199
|
+
OUTCOME="open"
|
|
200
|
+
NON_CODE_BLOCKER=$(noncode_blockers "$BASE_REF" .)
|
|
201
|
+
if [ -z "$NON_CODE_BLOCKER" ]; then
|
|
202
|
+
if [ "$PUBLISH" = "gate" ]; then
|
|
203
|
+
OUTCOME="staged"
|
|
204
|
+
elif gh pr merge "$PR_NUMBER" --auto --merge >/dev/null 2>&1 \
|
|
205
|
+
|| gh pr merge "$PR_NUMBER" --merge >/dev/null 2>&1; then
|
|
206
|
+
OUTCOME="merged"
|
|
207
|
+
fi
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
ledger_write "$PR_NUMBER" "$OUTCOME"
|
|
211
|
+
|
|
212
|
+
exit 0
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Index a single handoff file into the Neo4j graph.
|
|
5
|
+
# Usage: bash bin/index-handoff.sh <file-path>
|
|
6
|
+
# Returns: {"sessionId":"...","resolved":0} or {"error":"..."}.
|
|
7
|
+
# `resolved` remains for compatibility; lifecycle reconciliation is queued by
|
|
8
|
+
# capture-run.sh after indexing.
|
|
9
|
+
#
|
|
10
|
+
# The file-path should be relative to memory/ (e.g. handoffs/2026-02/14-oz-topic.md)
|
|
11
|
+
# or an absolute/relative path that contains "handoffs/" somewhere in it.
|
|
12
|
+
|
|
13
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
14
|
+
|
|
15
|
+
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
|
16
|
+
echo "Usage: index-handoff.sh <handoff-file-path>"
|
|
17
|
+
echo ""
|
|
18
|
+
echo "Index a single handoff file into the Neo4j graph."
|
|
19
|
+
echo "Parses metadata (author, date, topic, recipients) from"
|
|
20
|
+
echo "markdown headers or YAML front matter, creates Session"
|
|
21
|
+
echo "and relationship nodes. Lifecycle completion is queued separately."
|
|
22
|
+
echo ""
|
|
23
|
+
echo "Returns: {\"sessionId\":\"...\",\"resolved\":N}"
|
|
24
|
+
exit 0
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# --- Local mode gate: bail immediately ---
|
|
28
|
+
_MODE=$(jq -r '.mode // "connected"' "$SCRIPT_DIR/egregore.json" 2>/dev/null)
|
|
29
|
+
if [ "$_MODE" = "local" ]; then
|
|
30
|
+
echo '{"sessionId":"","resolved":0,"mode":"local"}'
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# --- Validate input ---
|
|
35
|
+
FILE_ARG="${1:?Usage: index-handoff.sh <handoff-file-path>}"
|
|
36
|
+
|
|
37
|
+
# Resolve to absolute path if needed
|
|
38
|
+
if [[ "$FILE_ARG" = /* ]]; then
|
|
39
|
+
FILE_PATH="$FILE_ARG"
|
|
40
|
+
elif [[ "$FILE_ARG" = memory/* ]]; then
|
|
41
|
+
FILE_PATH="$SCRIPT_DIR/$FILE_ARG"
|
|
42
|
+
else
|
|
43
|
+
FILE_PATH="$SCRIPT_DIR/memory/$FILE_ARG"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
if [ ! -f "$FILE_PATH" ]; then
|
|
47
|
+
echo '{"error":"File not found: '"$FILE_ARG"'"}'
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# --- Derive filePath (relative to memory/) and sessionId ---
|
|
52
|
+
REL_PATH="${FILE_PATH##*/memory/}"
|
|
53
|
+
if [[ "$REL_PATH" = "$FILE_PATH" ]]; then
|
|
54
|
+
REL_PATH="handoffs/${FILE_PATH##*handoffs/}"
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
FILENAME="$(basename "$FILE_PATH" .md)"
|
|
58
|
+
DIR_MONTH="$(basename "$(dirname "$FILE_PATH")")"
|
|
59
|
+
|
|
60
|
+
# sessionId = YYYY-MM-DD-author-topic
|
|
61
|
+
# If file is in a YYYY-MM subdirectory, derive from dir + filename
|
|
62
|
+
# If file is in root handoffs/ (no month subdir), extract date from filename
|
|
63
|
+
if [[ "$DIR_MONTH" =~ ^[0-9]{4}-[0-9]{2}$ ]]; then
|
|
64
|
+
DAY="${FILENAME%%-*}"
|
|
65
|
+
REST="${FILENAME#*-}"
|
|
66
|
+
SESSION_ID="${DIR_MONTH}-${DAY}-${REST}"
|
|
67
|
+
else
|
|
68
|
+
# Filename starts with YYYY-MM-DD — use it directly
|
|
69
|
+
SESSION_ID="$FILENAME"
|
|
70
|
+
DAY="$(echo "$FILENAME" | grep -oE '^[0-9]{4}-[0-9]{2}-[0-9]{2}' || true)"
|
|
71
|
+
REST="${FILENAME#*-*-*-}"
|
|
72
|
+
DIR_MONTH="$(echo "$DAY" | grep -oE '^[0-9]{4}-[0-9]{2}' || true)"
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# --- Parse handoff metadata from markdown ---
|
|
76
|
+
CONTENT="$(cat "$FILE_PATH")"
|
|
77
|
+
|
|
78
|
+
# Helper: extract from markdown headers, handles both **Key**: Value and **Key:** Value
|
|
79
|
+
md_extract() {
|
|
80
|
+
echo "$CONTENT" | grep -m1 "$1" 2>/dev/null | sed "$2" || true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Helper: extract markdown field by name (handles **Key**: and **Key:** patterns)
|
|
84
|
+
md_field() {
|
|
85
|
+
local key="$1"
|
|
86
|
+
local val
|
|
87
|
+
# Try **Key**: Value (colon outside bold)
|
|
88
|
+
val="$(echo "$CONTENT" | grep -m1 "^\*\*${key}\*\*:" 2>/dev/null | sed "s/\*\*${key}\*\*:[[:space:]]*//" | tr -d '\n' || true)"
|
|
89
|
+
# Try **Key:** Value (colon inside bold)
|
|
90
|
+
if [ -z "$val" ]; then
|
|
91
|
+
val="$(echo "$CONTENT" | grep -m1 "^\*\*${key}:\*\*" 2>/dev/null | sed "s/\*\*${key}:\*\*[[:space:]]*//" | tr -d '\n' || true)"
|
|
92
|
+
fi
|
|
93
|
+
echo "$val"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Helper: extract from YAML front matter (key: value between --- delimiters)
|
|
97
|
+
yaml_extract() {
|
|
98
|
+
local key="$1"
|
|
99
|
+
sed -n '/^---$/,/^---$/p' "$FILE_PATH" 2>/dev/null | grep -m1 "^${key}:" 2>/dev/null | sed "s/^${key}:[[:space:]]*//" | sed 's/^"//' | sed 's/"$//' | xargs 2>/dev/null || true
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# H1 topic (always from markdown body, not front matter)
|
|
103
|
+
TOPIC="$(md_extract '^# ' 's/^# //; s/^Handoff: //')"
|
|
104
|
+
# Also check YAML front matter topic
|
|
105
|
+
[ -z "$TOPIC" ] && TOPIC="$(yaml_extract 'topic')"
|
|
106
|
+
[ -z "$TOPIC" ] && TOPIC="$REST"
|
|
107
|
+
|
|
108
|
+
# Date: try markdown header, then YAML
|
|
109
|
+
DATE="$(md_field 'Date')"
|
|
110
|
+
[ -z "$DATE" ] && DATE="$(yaml_extract 'date')"
|
|
111
|
+
[ -z "$DATE" ] && DATE="${DIR_MONTH}-${DAY}"
|
|
112
|
+
|
|
113
|
+
# Author: try markdown header (Author or From), then YAML (author or from)
|
|
114
|
+
AUTHOR="$(md_field 'Author')"
|
|
115
|
+
[ -z "$AUTHOR" ] && AUTHOR="$(md_field 'From')"
|
|
116
|
+
[ -z "$AUTHOR" ] && AUTHOR="$(yaml_extract 'author')"
|
|
117
|
+
[ -z "$AUTHOR" ] && AUTHOR="$(yaml_extract 'from')"
|
|
118
|
+
# Strip " → recipient" if present (e.g. "cem → oz" → "cem")
|
|
119
|
+
AUTHOR_HANDLE="$(echo "$AUTHOR" | sed 's/[[:space:]]*→.*//' | awk '{print tolower($1)}')"
|
|
120
|
+
[ -z "$AUTHOR_HANDLE" ] && AUTHOR_HANDLE="unknown"
|
|
121
|
+
|
|
122
|
+
# Recipients: try markdown header, then canonical/legacy YAML names.
|
|
123
|
+
RECIPIENTS_RAW="$(md_field 'To')"
|
|
124
|
+
[ -z "$RECIPIENTS_RAW" ] && RECIPIENTS_RAW="$(md_field 'For')"
|
|
125
|
+
[ -z "$RECIPIENTS_RAW" ] && RECIPIENTS_RAW="$(yaml_extract 'to')"
|
|
126
|
+
[ -z "$RECIPIENTS_RAW" ] && RECIPIENTS_RAW="$(yaml_extract 'for')"
|
|
127
|
+
[ -z "$RECIPIENTS_RAW" ] && RECIPIENTS_RAW="$(yaml_extract 'addressed_to')"
|
|
128
|
+
# Also extract from "From: author → recipient" pattern
|
|
129
|
+
if [ -z "$RECIPIENTS_RAW" ] && echo "$AUTHOR" | grep -q '→' 2>/dev/null; then
|
|
130
|
+
RECIPIENTS_RAW="$(echo "$AUTHOR" | sed 's/.*→[[:space:]]*//')"
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
# Project: try markdown header, then YAML
|
|
134
|
+
PROJECT="$(md_field 'Project')"
|
|
135
|
+
[ -z "$PROJECT" ] && PROJECT="$(yaml_extract 'project')"
|
|
136
|
+
|
|
137
|
+
# Handoff kind: used by readers to distinguish automatic captures from manual handoffs.
|
|
138
|
+
KIND="$(md_field 'Kind')"
|
|
139
|
+
[ -z "$KIND" ] && KIND="$(yaml_extract 'kind')"
|
|
140
|
+
|
|
141
|
+
# Lifecycle intent determines which automatic transitions are safe.
|
|
142
|
+
INTENT="$(md_field 'Intent')"
|
|
143
|
+
[ -z "$INTENT" ] && INTENT="$(yaml_extract 'intent')"
|
|
144
|
+
INTENT="$(printf '%s' "${INTENT:-unclassified}" | tr '[:upper:]' '[:lower:]')"
|
|
145
|
+
case "$INTENT" in
|
|
146
|
+
action|feedback|fyi|unclassified) ;;
|
|
147
|
+
*) INTENT="unclassified" ;;
|
|
148
|
+
esac
|
|
149
|
+
|
|
150
|
+
# Summary: 5-level precedence chain
|
|
151
|
+
# 1. Frontmatter summary field
|
|
152
|
+
SUMMARY="$(yaml_extract 'summary')"
|
|
153
|
+
# 2. First non-empty paragraph after ## Briefing
|
|
154
|
+
[ -z "$SUMMARY" ] && SUMMARY="$(echo "$CONTENT" | awk '/^## Briefing/{found=1; next} found && /^#/{exit} found && /^[^#]/ && !/^[[:space:]]*$/{print; exit}' || true)"
|
|
155
|
+
# 3. First non-empty paragraph after ## Session Summary (legacy)
|
|
156
|
+
[ -z "$SUMMARY" ] && SUMMARY="$(echo "$CONTENT" | awk '/^## Session Summary/{found=1; next} found && /^#/{exit} found && /^[^#]/ && !/^[[:space:]]*$/{print; exit}' || true)"
|
|
157
|
+
# 4. First non-heading, non-metadata, non-empty line in body
|
|
158
|
+
[ -z "$SUMMARY" ] && SUMMARY="$(echo "$CONTENT" | awk '!/^[[:space:]]*$/ && !/^#/ && !/^\*\*/ && !/^---/ && !/^\|/ && !/^- /{print; exit}' || true)"
|
|
159
|
+
# 5. Topic fallback
|
|
160
|
+
[ -z "$SUMMARY" ] && SUMMARY="$TOPIC"
|
|
161
|
+
|
|
162
|
+
# Repo State: parse markdown table after "## Repo State"
|
|
163
|
+
# Extracts repo, branch, PR number, and base branch from each table row
|
|
164
|
+
REPO_STATE_JSON="[]"
|
|
165
|
+
REPO_TABLE=$(echo "$CONTENT" | awk '/^## Repo State/{found=1; next} found && /^#/{exit} found && /^\|[^-]/ && !/^\| Repo/{print}' || true)
|
|
166
|
+
if [ -n "$REPO_TABLE" ]; then
|
|
167
|
+
REPO_STATE_JSON=$(echo "$REPO_TABLE" | while IFS='|' read -r _ R_REPO R_BRANCH R_PR R_BASE _; do
|
|
168
|
+
R_REPO=$(echo "$R_REPO" | xargs 2>/dev/null || true)
|
|
169
|
+
R_BRANCH=$(echo "$R_BRANCH" | xargs 2>/dev/null || true)
|
|
170
|
+
R_PR=$(echo "$R_PR" | xargs 2>/dev/null | sed 's/^#//' | sed 's/—//' || true)
|
|
171
|
+
R_BASE=$(echo "$R_BASE" | xargs 2>/dev/null || true)
|
|
172
|
+
[ -z "$R_REPO" ] && continue
|
|
173
|
+
if [ -n "$R_PR" ] && [ "$R_PR" -eq "$R_PR" ] 2>/dev/null; then
|
|
174
|
+
jq -n --arg repo "$R_REPO" --arg branch "$R_BRANCH" --argjson pr "$R_PR" --arg base "$R_BASE" \
|
|
175
|
+
'{repo:$repo, branch:$branch, pr:$pr, base:$base}'
|
|
176
|
+
else
|
|
177
|
+
jq -n --arg repo "$R_REPO" --arg branch "$R_BRANCH" --arg base "$R_BASE" \
|
|
178
|
+
'{repo:$repo, branch:$branch, pr:null, base:$base}'
|
|
179
|
+
fi
|
|
180
|
+
done | jq -s '.' 2>/dev/null || echo "[]")
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
# --- Build Cypher queries ---
|
|
184
|
+
|
|
185
|
+
# Q1: MERGE Session + relationships
|
|
186
|
+
HANDED_TO_CYPHER=""
|
|
187
|
+
_RECIP_ARGS=()
|
|
188
|
+
if [ -n "$RECIPIENTS_RAW" ]; then
|
|
189
|
+
IFS=',' read -ra RECIPS <<< "$RECIPIENTS_RAW"
|
|
190
|
+
for i in "${!RECIPS[@]}"; do
|
|
191
|
+
R="$(echo "${RECIPS[$i]}" | xargs | tr '[:upper:]' '[:lower:]')"
|
|
192
|
+
[ -z "$R" ] && continue
|
|
193
|
+
HANDED_TO_CYPHER="${HANDED_TO_CYPHER}
|
|
194
|
+
WITH s
|
|
195
|
+
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)]
|
|
196
|
+
FOREACH (_ IN CASE WHEN r${i} IS NOT NULL THEN [1] ELSE [] END |
|
|
197
|
+
MERGE (s)-[:HANDED_TO]->(r${i})
|
|
198
|
+
)"
|
|
199
|
+
_RECIP_ARGS+=("--arg" "recipient${i}" "$R")
|
|
200
|
+
done
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
PROJECT_CYPHER=""
|
|
204
|
+
_PROJECT_ARGS=()
|
|
205
|
+
if [ -n "$PROJECT" ]; then
|
|
206
|
+
PROJECT_CYPHER="
|
|
207
|
+
WITH s
|
|
208
|
+
OPTIONAL MATCH (proj:Project) WHERE toLower(proj.name) = toLower(\$project)
|
|
209
|
+
FOREACH (_ IN CASE WHEN proj IS NOT NULL THEN [1] ELSE [] END |
|
|
210
|
+
MERGE (s)-[:ABOUT]->(proj)
|
|
211
|
+
)"
|
|
212
|
+
_PROJECT_ARGS=("--arg" "project" "$PROJECT")
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
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)]
|
|
216
|
+
MERGE (s:Session {id: \$sessionId})
|
|
217
|
+
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')
|
|
218
|
+
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
|
|
219
|
+
WITH s, p
|
|
220
|
+
MERGE (s)-[:BY]->(p)${PROJECT_CYPHER}${HANDED_TO_CYPHER}
|
|
221
|
+
RETURN s.id AS sessionId"
|
|
222
|
+
|
|
223
|
+
Q1_PARAMS=$(jq -n \
|
|
224
|
+
--arg sessionId "$SESSION_ID" \
|
|
225
|
+
--arg author "$AUTHOR_HANDLE" \
|
|
226
|
+
--arg date "$DATE" \
|
|
227
|
+
--arg topic "$TOPIC" \
|
|
228
|
+
--arg summary "$SUMMARY" \
|
|
229
|
+
--arg filePath "$REL_PATH" \
|
|
230
|
+
--arg kind "$KIND" \
|
|
231
|
+
--arg intent "$INTENT" \
|
|
232
|
+
--arg repoState "$REPO_STATE_JSON" \
|
|
233
|
+
"${_PROJECT_ARGS[@]:+${_PROJECT_ARGS[@]}}" \
|
|
234
|
+
"${_RECIP_ARGS[@]:+${_RECIP_ARGS[@]}}" \
|
|
235
|
+
'$ARGS.named')
|
|
236
|
+
|
|
237
|
+
# Q2: Preserve the stable three-result batch shape without mutating lifecycle.
|
|
238
|
+
# Completion used to run inside this foreground indexing request. It now enters
|
|
239
|
+
# through capture-run.sh's local WAL queue and is reconciled off the caller's
|
|
240
|
+
# critical path.
|
|
241
|
+
Q2_CYPHER="MATCH (s:Session {id: \$sessionId})
|
|
242
|
+
RETURN 0 AS resolved"
|
|
243
|
+
|
|
244
|
+
Q2_PARAMS=$(jq -n --arg sessionId "$SESSION_ID" '{sessionId: $sessionId}')
|
|
245
|
+
|
|
246
|
+
# Q3: Capture the handoff's knowledge-graph neighborhood for artifact rendering.
|
|
247
|
+
# Only the "graph-only" relations — things not already visible in the markdown
|
|
248
|
+
# header. Author/recipients/project live in the metadata row above the graph,
|
|
249
|
+
# so including them would be noise.
|
|
250
|
+
Q3_CYPHER="MATCH (s:Session {id: \$sessionId})
|
|
251
|
+
OPTIONAL MATCH (s)-[:IMPLEMENTS]->(prior:Session)
|
|
252
|
+
OPTIONAL MATCH (prior)-[:BY]->(priorAuthor:Person)
|
|
253
|
+
OPTIONAL MATCH (later:Session)-[:IMPLEMENTS]->(s)
|
|
254
|
+
OPTIONAL MATCH (later)-[:BY]->(laterAuthor:Person)
|
|
255
|
+
OPTIONAL MATCH (s)-[:CONTINUES]->(cont:Session)
|
|
256
|
+
OPTIONAL MATCH (cont)-[:BY]->(contAuthor:Person)
|
|
257
|
+
OPTIONAL MATCH (s)-[:ADVANCED|INVOLVES]->(quest:Quest)
|
|
258
|
+
OPTIONAL MATCH (s)-[:HAS_ACTIVITY]->(art:Artifact)
|
|
259
|
+
OPTIONAL MATCH (s)-[:PRODUCED]->(pr:PR)
|
|
260
|
+
RETURN
|
|
261
|
+
collect(DISTINCT {id: prior.id, topic: prior.topic, author: priorAuthor.name}) AS implementsHandoff,
|
|
262
|
+
collect(DISTINCT {id: later.id, topic: later.topic, author: laterAuthor.name}) AS implementedBy,
|
|
263
|
+
collect(DISTINCT {id: cont.id, topic: cont.topic, author: contAuthor.name}) AS continues,
|
|
264
|
+
collect(DISTINCT {id: quest.id, title: quest.title}) AS quests,
|
|
265
|
+
collect(DISTINCT {id: art.id, title: art.title, type: art.type, path: art.path}) AS artifacts,
|
|
266
|
+
collect(DISTINCT {id: pr.id, number: pr.number, title: pr.title, repo: pr.repo}) AS prs"
|
|
267
|
+
|
|
268
|
+
Q3_PARAMS=$(jq -n --arg sessionId "$SESSION_ID" '{sessionId: $sessionId}')
|
|
269
|
+
|
|
270
|
+
# --- Build batch request ---
|
|
271
|
+
BATCH_JSON=$(jq -n \
|
|
272
|
+
--arg q1 "$Q1_CYPHER" \
|
|
273
|
+
--argjson p1 "$Q1_PARAMS" \
|
|
274
|
+
--arg q2 "$Q2_CYPHER" \
|
|
275
|
+
--argjson p2 "$Q2_PARAMS" \
|
|
276
|
+
--arg q3 "$Q3_CYPHER" \
|
|
277
|
+
--argjson p3 "$Q3_PARAMS" \
|
|
278
|
+
'[{statement: $q1, parameters: $p1}, {statement: $q2, parameters: $p2}, {statement: $q3, parameters: $p3}]')
|
|
279
|
+
|
|
280
|
+
# --- Execute ---
|
|
281
|
+
RESPONSE=$(bash "$SCRIPT_DIR/bin/graph-batch.sh" "$BATCH_JSON" 2>/dev/null) || {
|
|
282
|
+
echo '{"error":"graph-batch.sh failed"}'
|
|
283
|
+
exit 1
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
# --- Mark auto-captured personal Session as handed_off ---
|
|
287
|
+
PROJ_HASH=$(echo -n "$SCRIPT_DIR" | md5 2>/dev/null || echo -n "$SCRIPT_DIR" | md5sum 2>/dev/null | cut -d' ' -f1)
|
|
288
|
+
CURRENT_SID=$(cat "$HOME/.egregore/session-${PROJ_HASH}.id" 2>/dev/null || echo "")
|
|
289
|
+
if [ -n "$CURRENT_SID" ]; then
|
|
290
|
+
bash "$SCRIPT_DIR/bin/graph.sh" query \
|
|
291
|
+
"MATCH (s:Session {id: \$sid}) SET s.status = 'handed_off' RETURN s.id" \
|
|
292
|
+
"$(jq -n --arg sid "$CURRENT_SID" '{sid: $sid}')" >/dev/null 2>&1 || true
|
|
293
|
+
fi
|
|
294
|
+
|
|
295
|
+
# --- Parse response ---
|
|
296
|
+
RESOLVED=$(echo "$RESPONSE" | jq -r '.results[1].values[0][0] // 0' 2>/dev/null || echo "0")
|
|
297
|
+
|
|
298
|
+
# Parse Q3 neighborhood into a subgraph object. Collect() entries that didn't
|
|
299
|
+
# match anything come back as {name: null, ...} — filter those out.
|
|
300
|
+
EMPTY_SG='{"implementsHandoff":[],"implementedBy":[],"continues":[],"quests":[],"artifacts":[],"prs":[]}'
|
|
301
|
+
SUBGRAPH_JSON=$(echo "$RESPONSE" | jq -c '
|
|
302
|
+
(.results[2] // null) as $r
|
|
303
|
+
| if $r == null or (($r.values // []) | length) == 0 then
|
|
304
|
+
{implementsHandoff:[], implementedBy:[], continues:[], quests:[], artifacts:[], prs:[]}
|
|
305
|
+
else
|
|
306
|
+
($r.fields // []) as $f
|
|
307
|
+
| ($r.values[0] // []) as $v
|
|
308
|
+
| reduce range(0; ($f | length)) as $i ({}; .[$f[$i]] = $v[$i])
|
|
309
|
+
| {
|
|
310
|
+
implementsHandoff: ((.implementsHandoff // []) | map(select(.id != null))),
|
|
311
|
+
implementedBy: ((.implementedBy // []) | map(select(.id != null))),
|
|
312
|
+
continues: ((.continues // []) | map(select(.id != null))),
|
|
313
|
+
quests: ((.quests // []) | map(select(.id != null))),
|
|
314
|
+
artifacts: ((.artifacts // []) | map(select(.id != null or .path != null))),
|
|
315
|
+
prs: ((.prs // []) | map(select(.number != null or .id != null)))
|
|
316
|
+
}
|
|
317
|
+
end
|
|
318
|
+
' 2>/dev/null || echo "$EMPTY_SG")
|
|
319
|
+
[ -z "$SUBGRAPH_JSON" ] && SUBGRAPH_JSON="$EMPTY_SG"
|
|
320
|
+
|
|
321
|
+
jq -n --arg sessionId "$SESSION_ID" --argjson resolved "$RESOLVED" --argjson subgraph "$SUBGRAPH_JSON" \
|
|
322
|
+
'{sessionId: $sessionId, resolved: $resolved, subgraph: $subgraph}'
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env 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}'
|