create-egregore 0.19.32 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/egregore-launcher.js +118 -13
- package/bin/cli.js +48 -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 +5 -4
- 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 +8 -1
- package/runtime/codex/bin/connect-refresh.sh +33 -0
- 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 +8 -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 +8 -1
- package/runtime/pi/bin/connect-refresh.sh +33 -0
- 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 +8 -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,240 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Hybrid search over shared memory — powered by qmd (BM25 + vector + rerank, fully local).
|
|
3
|
+
# Usage:
|
|
4
|
+
# bash bin/search.sh query "text" [-n N] [--fast|--semantic] [--all]
|
|
5
|
+
# bash bin/search.sh reindex [--embed]
|
|
6
|
+
# bash bin/search.sh status
|
|
7
|
+
#
|
|
8
|
+
# The memory repo is registered as a qmd collection named "{slug}-memory".
|
|
9
|
+
# Every call is scoped to that collection with -c, so other qmd collections
|
|
10
|
+
# on the machine are never read or modified.
|
|
11
|
+
#
|
|
12
|
+
# Modes:
|
|
13
|
+
# (default) qmd query — hybrid BM25 + vector + rerank (needs local models, ~2GB one-time download)
|
|
14
|
+
# --fast qmd search — BM25 keyword only, instant, no models
|
|
15
|
+
# --semantic qmd vsearch — vector similarity only (needs embeddings: reindex --embed)
|
|
16
|
+
# Hybrid/semantic fall back to BM25 automatically if models or embeddings are missing.
|
|
17
|
+
|
|
18
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
19
|
+
export NODE_NO_WARNINGS=1
|
|
20
|
+
# Do NOT set CI=1 here — it disables qmd's LLM ops (query expansion, rerank).
|
|
21
|
+
|
|
22
|
+
# qmd's progress spinner can leak into stdout when it misdetects a TTY;
|
|
23
|
+
# strip ANSI escapes and spinner frames from anything we return.
|
|
24
|
+
_clean() {
|
|
25
|
+
sed -E $'s/\x1b\\[[0-9;?]*[A-Za-z]//g' | grep -v 'Gathering information' || true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
SLUG=$(jq -r '.slug // "egregore"' "$SCRIPT_DIR/egregore.json" 2>/dev/null || echo "egregore")
|
|
29
|
+
COLLECTION="${SLUG}-memory"
|
|
30
|
+
MEM_PATH="$(readlink -f "$SCRIPT_DIR/memory" 2>/dev/null || true)"
|
|
31
|
+
|
|
32
|
+
QMD_VERSION="2.5.3" # pinned — bump deliberately, never float @latest
|
|
33
|
+
|
|
34
|
+
_qmd() {
|
|
35
|
+
if command -v qmd >/dev/null 2>&1; then
|
|
36
|
+
qmd "$@"
|
|
37
|
+
else
|
|
38
|
+
npx -y "@tobilu/qmd@${QMD_VERSION}" "$@"
|
|
39
|
+
fi
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# --- Weight ladder ----------------------------------------------------------
|
|
43
|
+
# Three tiers, chosen by what is already on disk — no mode ever triggers a
|
|
44
|
+
# model download in a session's critical path:
|
|
45
|
+
# keyword 0 MB BM25, works at install, forever
|
|
46
|
+
# semantic ~334 MB embed model, fetched silently by the background warm
|
|
47
|
+
# hybrid ~2.2 GB rerank + expansion — org opt-in (features.search_hybrid),
|
|
48
|
+
# prefetched by the background warm, never inline
|
|
49
|
+
MODELS_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/qmd/models"
|
|
50
|
+
_have_embed() { ls "$MODELS_DIR"/*embedding* >/dev/null 2>&1; }
|
|
51
|
+
_have_hybrid() { ls "$MODELS_DIR"/*reranker* >/dev/null 2>&1 && ls "$MODELS_DIR"/*expansion* >/dev/null 2>&1; }
|
|
52
|
+
_hybrid_opted() { [ "$(jq -r '.features.search_hybrid // false' "$SCRIPT_DIR/egregore.json" 2>/dev/null)" = "true" ]; }
|
|
53
|
+
|
|
54
|
+
_ensure() {
|
|
55
|
+
if [ -z "$MEM_PATH" ] || [ ! -d "$MEM_PATH" ]; then
|
|
56
|
+
echo "memory/ not found — run bin/search.sh from an Egregore instance with a memory symlink" >&2
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
59
|
+
if ! _qmd collection list 2>/dev/null | grep -q "^${COLLECTION} "; then
|
|
60
|
+
_qmd collection add "$MEM_PATH" --name "$COLLECTION" >/dev/null 2>&1 || true
|
|
61
|
+
fi
|
|
62
|
+
# Incremental re-index so new handoffs/decisions are searchable immediately.
|
|
63
|
+
# NEVER swallow a failure silently: a dead path in ANY registered collection
|
|
64
|
+
# (e.g. a deleted side-project dir) crashes `qmd update` for every collection,
|
|
65
|
+
# and the index quietly stops absorbing new documents. Caught in dogfooding
|
|
66
|
+
# 2026-07-07 — results looked fine while missing three days of handoffs.
|
|
67
|
+
if ! _qmd update >/dev/null 2>&1; then
|
|
68
|
+
echo "(search: index update FAILED — results may be missing recent documents. A registered qmd collection likely points at a deleted path: run 'qmd collection list', remove the dead one, retry.)" >&2
|
|
69
|
+
fi
|
|
70
|
+
_embed_bg
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Keep vector embeddings warm without anyone running a manual step: fire a
|
|
74
|
+
# detached, lock-guarded `qmd embed` on every query. Nothing pending → exits
|
|
75
|
+
# in ~1s. First run on a machine → downloads models + embeds in the
|
|
76
|
+
# background while queries fall back to keyword mode.
|
|
77
|
+
_embed_bg() {
|
|
78
|
+
[ "${EGREGORE_SEARCH_NO_WARM:-0}" = "1" ] && return 0
|
|
79
|
+
local lock="${TMPDIR:-/tmp}/egregore-qmd-embed-${COLLECTION}.lock"
|
|
80
|
+
if [ -f "$lock" ]; then
|
|
81
|
+
# A crashed embed shouldn't block warming forever — expire the lock after 60 min.
|
|
82
|
+
if [ -n "$(find "$lock" -mmin +60 2>/dev/null)" ]; then rm -f "$lock"; else return 0; fi
|
|
83
|
+
fi
|
|
84
|
+
touch "$lock"
|
|
85
|
+
if ! _have_embed; then
|
|
86
|
+
echo "(first run: building the semantic index in the background — searches use keyword mode until it finishes)" >&2
|
|
87
|
+
fi
|
|
88
|
+
# Background fetch policy: `qmd embed` pulls only the ~334MB embed model
|
|
89
|
+
# (the semantic tier). The ~1.9GB hybrid pack is prefetched here ONLY when
|
|
90
|
+
# the org opted in — a session never waits on either.
|
|
91
|
+
( ( _qmd embed >/dev/null 2>&1
|
|
92
|
+
if _hybrid_opted && ! _have_hybrid; then
|
|
93
|
+
_qmd query "warmup" -c "$COLLECTION" -n 1 >/dev/null 2>&1
|
|
94
|
+
fi
|
|
95
|
+
rm -f "$lock" ) & ) >/dev/null 2>&1
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Annotate results with live graph state (connected mode): each hit that the
|
|
99
|
+
# graph knows (Session by filePath = handoffs; Artifact by filePath = knowledge)
|
|
100
|
+
# gets one "↳ graph:" line — search finds the artifact, the graph tells you its
|
|
101
|
+
# state, in a single call. Fails soft everywhere: local mode / graph offline /
|
|
102
|
+
# unknown paths simply produce no annotations.
|
|
103
|
+
_enrich() {
|
|
104
|
+
local results paths params out
|
|
105
|
+
results=$(cat)
|
|
106
|
+
echo "$results"
|
|
107
|
+
paths=$(echo "$results" | grep -o "qmd://${COLLECTION}/[^: ]*\.md" | sed "s|qmd://${COLLECTION}/||" | sort -u)
|
|
108
|
+
[ -z "$paths" ] && return 0
|
|
109
|
+
params=$(printf '%s\n' "$paths" | jq -R . | jq -sc '{paths: .}')
|
|
110
|
+
out=$(bash "$SCRIPT_DIR/bin/graph.sh" query "UNWIND \$paths AS fp
|
|
111
|
+
OPTIONAL MATCH (s:Session {filePath: fp})
|
|
112
|
+
OPTIONAL MATCH (a:Artifact {filePath: fp})
|
|
113
|
+
WITH fp, s, a WHERE s IS NOT NULL OR a IS NOT NULL
|
|
114
|
+
RETURN fp,
|
|
115
|
+
CASE WHEN s IS NOT NULL THEN coalesce(s.handoffStatus, s.status) ELSE null END AS sessionStatus,
|
|
116
|
+
s.handedTo AS handedTo, a.type AS artifactType,
|
|
117
|
+
CASE WHEN a IS NOT NULL THEN [q IN [(a)-[:PART_OF]->(qq:Quest) | qq.id] | q][0] ELSE null END AS quest" \
|
|
118
|
+
"$params" 2>/dev/null)
|
|
119
|
+
echo "$out" | jq -r '.values[]? | @tsv' 2>/dev/null | while IFS=$'\t' read -r fp sstat sto atype quest; do
|
|
120
|
+
local ann=""
|
|
121
|
+
[ -n "$sstat" ] && [ "$sstat" != "null" ] && ann="status: $sstat"
|
|
122
|
+
[ -n "$sto" ] && [ "$sto" != "null" ] && ann="$ann${ann:+ · }to: $sto"
|
|
123
|
+
[ -n "$atype" ] && [ "$atype" != "null" ] && ann="$ann${ann:+ · }artifact: $atype"
|
|
124
|
+
[ -n "$quest" ] && [ "$quest" != "null" ] && ann="$ann${ann:+ · }quest: $quest"
|
|
125
|
+
[ -n "$ann" ] && echo "↳ graph · $fp — $ann"
|
|
126
|
+
done
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
cmd_query() {
|
|
130
|
+
local mode="query" n=6 enrich=auto all=()
|
|
131
|
+
local -a words=()
|
|
132
|
+
while [ $# -gt 0 ]; do
|
|
133
|
+
case "$1" in
|
|
134
|
+
--fast) mode="search" ;;
|
|
135
|
+
--semantic) mode="vsearch" ;;
|
|
136
|
+
--enrich) enrich=1 ;;
|
|
137
|
+
--no-enrich) enrich=0 ;;
|
|
138
|
+
--all) all=(--all) ;;
|
|
139
|
+
-n) n="$2"; shift ;;
|
|
140
|
+
*) words+=("$1") ;;
|
|
141
|
+
esac
|
|
142
|
+
shift
|
|
143
|
+
done
|
|
144
|
+
# Default mode rides the weight ladder: use the heaviest tier whose models
|
|
145
|
+
# are already local. Explicit flags always win; nothing downloads inline.
|
|
146
|
+
if [ "$mode" = "query" ] && ! _have_hybrid; then
|
|
147
|
+
if _have_embed; then mode="vsearch"; else mode="search"; fi
|
|
148
|
+
fi
|
|
149
|
+
# Enrichment is managed, not a user choice: on wherever a graph exists
|
|
150
|
+
# (connected mode), silently off everywhere else. Flags only override.
|
|
151
|
+
if [ "$enrich" = "auto" ]; then
|
|
152
|
+
if [ "$(jq -r '.mode // "connected"' "$SCRIPT_DIR/egregore.json" 2>/dev/null)" != "local" ] && \
|
|
153
|
+
[ -n "$(jq -r '.api_url // empty' "$SCRIPT_DIR/egregore.json" 2>/dev/null)" ]; then
|
|
154
|
+
enrich=1
|
|
155
|
+
else
|
|
156
|
+
enrich=0
|
|
157
|
+
fi
|
|
158
|
+
fi
|
|
159
|
+
local q="${words[*]}"
|
|
160
|
+
if [ -z "$q" ]; then
|
|
161
|
+
echo "usage: bash bin/search.sh query \"text\" [-n N] [--fast|--semantic] [--all]" >&2
|
|
162
|
+
exit 1
|
|
163
|
+
fi
|
|
164
|
+
_ensure
|
|
165
|
+
local emit="cat"
|
|
166
|
+
[ "$enrich" = 1 ] && emit="_enrich"
|
|
167
|
+
local t0 out label
|
|
168
|
+
t0=$(date +%s)
|
|
169
|
+
if [ "$mode" != "search" ]; then
|
|
170
|
+
out=$(_qmd "$mode" "$q" -c "$COLLECTION" -n "$n" --format md --full-path "${all[@]}" 2>/dev/null | _clean)
|
|
171
|
+
if [ -n "$out" ] && [ "$out" != "No results found." ]; then
|
|
172
|
+
[ "$mode" = "query" ] && label="hybrid" || label="semantic"
|
|
173
|
+
_banner_and_emit "$label" "$t0" "$emit" "$q" "$out"
|
|
174
|
+
return 0
|
|
175
|
+
fi
|
|
176
|
+
echo "_(hybrid/semantic returned nothing — showing keyword results. If this persists, run: bash bin/search.sh reindex --embed)_"
|
|
177
|
+
echo ""
|
|
178
|
+
fi
|
|
179
|
+
out=$(_qmd search "$q" -c "$COLLECTION" -n "$n" --format md --full-path "${all[@]}" | _clean)
|
|
180
|
+
_banner_and_emit "keyword" "$t0" "$emit" "$q" "$out"
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# One visible product-attribution line per search — the tell that Egregore's
|
|
184
|
+
# organizational retrieval layer (not an improvised agent grep loop) answered.
|
|
185
|
+
# It names the graph only when enrichment actually ran, then reports mode,
|
|
186
|
+
# hit count, and latency. Colored when stdout is a terminal; plain glyph in
|
|
187
|
+
# harness transcripts. Also appends to ~/.egregore/search.log (local only,
|
|
188
|
+
# never uploaded) so dogfooding can measure search-first rate and latency.
|
|
189
|
+
_banner_and_emit() {
|
|
190
|
+
local label="$1" t0="$2" emit="$3" q="$4" out="$5"
|
|
191
|
+
local hits dur modef product surface plural=""
|
|
192
|
+
hits=$(printf '%s\n' "$out" | grep -c '^qmd://' 2>/dev/null | tr -d ' ')
|
|
193
|
+
dur=$(( $(date +%s) - t0 ))
|
|
194
|
+
modef="$label"
|
|
195
|
+
product="Egregore"
|
|
196
|
+
surface="searching your organization’s memory"
|
|
197
|
+
if [ "$emit" = "_enrich" ] && [ "$hits" -gt 0 ]; then
|
|
198
|
+
modef="${label}+graph"
|
|
199
|
+
product="Egregore Connect"
|
|
200
|
+
surface="${surface} and relationships"
|
|
201
|
+
fi
|
|
202
|
+
[ "$hits" != "1" ] && plural="s"
|
|
203
|
+
local line="⌕ ${product} · ${surface} · ${label} · ${hits} hit${plural} · ${dur}s"
|
|
204
|
+
if [ -t 1 ]; then
|
|
205
|
+
printf '\033[1;38;5;30m%s\033[0m\n\n' "$line"
|
|
206
|
+
else
|
|
207
|
+
printf '%s\n\n' "$line"
|
|
208
|
+
fi
|
|
209
|
+
printf '%s\n' "$out" | $emit
|
|
210
|
+
mkdir -p "$HOME/.egregore" 2>/dev/null
|
|
211
|
+
echo "$(date '+%Y-%m-%d %H:%M:%S') mode=${modef} hits=${hits} dur=${dur}s q=\"$(printf '%.80s' "$q")\"" >> "$HOME/.egregore/search.log" 2>/dev/null || true
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
cmd_reindex() {
|
|
215
|
+
_ensure
|
|
216
|
+
_qmd update
|
|
217
|
+
if [ "${1:-}" = "--embed" ]; then
|
|
218
|
+
_qmd embed
|
|
219
|
+
# Hybrid pack prefetch only when the org opted in (features.search_hybrid).
|
|
220
|
+
if _hybrid_opted && ! _have_hybrid; then
|
|
221
|
+
_qmd query "warmup" -c "$COLLECTION" -n 1 >/dev/null 2>&1 || true
|
|
222
|
+
fi
|
|
223
|
+
fi
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
cmd_status() {
|
|
227
|
+
_qmd status 2>/dev/null
|
|
228
|
+
echo ""
|
|
229
|
+
echo "collection: $COLLECTION → $MEM_PATH"
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
case "${1:-}" in
|
|
233
|
+
query) shift; cmd_query "$@" ;;
|
|
234
|
+
reindex) shift; cmd_reindex "$@" ;;
|
|
235
|
+
status) shift; cmd_status ;;
|
|
236
|
+
*)
|
|
237
|
+
echo "usage: bash bin/search.sh {query|reindex|status}" >&2
|
|
238
|
+
exit 1
|
|
239
|
+
;;
|
|
240
|
+
esac
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# session-autosave.sh — save non-coding session work without an explicit /save.
|
|
3
|
+
#
|
|
4
|
+
# Terminal-close is the design constraint: no single "session end" event is
|
|
5
|
+
# reliable (people just close the window), so durability comes from three
|
|
6
|
+
# overlapping triggers:
|
|
7
|
+
# - Stop hook, debounced (Claude Code): saves DURING the session, after a
|
|
8
|
+
# turn completes — a killed terminal loses at most the debounce window.
|
|
9
|
+
# - Session-start sweep (--sweep, all runtimes): the next launch on this
|
|
10
|
+
# machine rescues leftovers in the main checkout and lingering worktrees.
|
|
11
|
+
# - SessionEnd hook / agent.sh wrap (Codex, Pi): immediate save on clean exit.
|
|
12
|
+
#
|
|
13
|
+
# Gate: acts ONLY when every pending change — uncommitted files AND commits
|
|
14
|
+
# ahead of origin/develop — is non-coding per bin/lib/noncode.sh. Any code in
|
|
15
|
+
# the pending set → exit silently; code always goes through explicit /save and
|
|
16
|
+
# review. Never auto-commits half-finished code.
|
|
17
|
+
#
|
|
18
|
+
# When the gate passes, delegates commit→push→PR→auto-merge to
|
|
19
|
+
# handoff-save-egregore.sh (--kind autosave), and pushes the memory repo.
|
|
20
|
+
#
|
|
21
|
+
# Usage:
|
|
22
|
+
# session-autosave.sh [--dir <repo-dir>] [--author <name>]
|
|
23
|
+
# [--debounce <sec>] [--min-idle <sec>] [--sweep]
|
|
24
|
+
# --debounce skip if this dir was autosaved less than <sec> ago (Stop hook)
|
|
25
|
+
# --min-idle skip if any dirty file was modified less than <sec> ago
|
|
26
|
+
# (don't commit prose someone is actively typing)
|
|
27
|
+
# --sweep iterate main checkout + .claude/worktrees/* (session start)
|
|
28
|
+
# (Hook invocations pass nothing; stdin JSON supplies cwd.)
|
|
29
|
+
|
|
30
|
+
set -uo pipefail
|
|
31
|
+
|
|
32
|
+
DIR=""
|
|
33
|
+
AUTHOR=""
|
|
34
|
+
DEBOUNCE=0
|
|
35
|
+
MIN_IDLE=0
|
|
36
|
+
SWEEP=0
|
|
37
|
+
while [ $# -gt 0 ]; do
|
|
38
|
+
case "$1" in
|
|
39
|
+
--dir) DIR="${2:-}"; shift 2 ;;
|
|
40
|
+
--author) AUTHOR="${2:-}"; shift 2 ;;
|
|
41
|
+
--debounce) DEBOUNCE="${2:-0}"; shift 2 ;;
|
|
42
|
+
--min-idle) MIN_IDLE="${2:-0}"; shift 2 ;;
|
|
43
|
+
--sweep) SWEEP=1; shift ;;
|
|
44
|
+
*) shift ;;
|
|
45
|
+
esac
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
SCRIPT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
49
|
+
|
|
50
|
+
# --- Sweep mode: rescue abandoned checkouts, then exit -------------------
|
|
51
|
+
if [ "$SWEEP" = "1" ]; then
|
|
52
|
+
# Let session-start's own git syncs settle before touching the checkout.
|
|
53
|
+
sleep 15
|
|
54
|
+
for d in "$SCRIPT_ROOT" "$SCRIPT_ROOT"/.claude/worktrees/*/; do
|
|
55
|
+
d="${d%/}"
|
|
56
|
+
[ -d "$d" ] || continue
|
|
57
|
+
# min-idle protects live sessions in other terminals: never commit files
|
|
58
|
+
# someone touched in the last 10 minutes. debounce dedupes concurrent
|
|
59
|
+
# session launches sweeping the same dirs.
|
|
60
|
+
bash "$0" --dir "$d" --min-idle 600 --debounce 120 >/dev/null 2>&1 || true
|
|
61
|
+
done
|
|
62
|
+
exit 0
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Hook invocations deliver JSON on stdin (cwd = where the session worked).
|
|
66
|
+
if [ -z "$DIR" ] && [ ! -t 0 ]; then
|
|
67
|
+
HOOK_INPUT=$(cat 2>/dev/null || true)
|
|
68
|
+
DIR=$(echo "$HOOK_INPUT" | jq -r '.cwd // empty' 2>/dev/null || true)
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
[ -n "$DIR" ] && [ -d "$DIR" ] || DIR="$SCRIPT_ROOT"
|
|
72
|
+
cd "$DIR" 2>/dev/null || exit 0
|
|
73
|
+
git rev-parse --git-dir >/dev/null 2>&1 || exit 0
|
|
74
|
+
|
|
75
|
+
# --- Debounce (per-dir stamp) --------------------------------------------
|
|
76
|
+
# NB: `cmd | cut || fallback` doesn't work — cut succeeds on empty input, so
|
|
77
|
+
# the fallback never fires and every dir collapses to one stamp (macOS has
|
|
78
|
+
# md5, not md5sum). Group the hash commands, then cut.
|
|
79
|
+
DIR_HASH=$( { md5sum <<<"$DIR" 2>/dev/null || md5 -q -s "$DIR" 2>/dev/null || echo default; } | cut -c1-8 )
|
|
80
|
+
STAMP="/tmp/.egregore-autosave-${DIR_HASH}"
|
|
81
|
+
if [ "$DEBOUNCE" -gt 0 ] && [ -f "$STAMP" ]; then
|
|
82
|
+
LAST=$(stat -f %m "$STAMP" 2>/dev/null || stat -c %Y "$STAMP" 2>/dev/null || echo 0)
|
|
83
|
+
[ $(( $(date +%s) - LAST )) -lt "$DEBOUNCE" ] && exit 0
|
|
84
|
+
fi
|
|
85
|
+
touch "$STAMP" 2>/dev/null || true
|
|
86
|
+
|
|
87
|
+
# shellcheck source=bin/lib/noncode.sh
|
|
88
|
+
. "$SCRIPT_ROOT/bin/lib/noncode.sh" 2>/dev/null || exit 0
|
|
89
|
+
|
|
90
|
+
# --- Per-user consent settings (bin/autosave.sh / the /autosave skill) ----
|
|
91
|
+
# enabled=false → fully off, nothing captured ambiently
|
|
92
|
+
# scope=handoffs → memory repo only; the core repo is never touched ambiently
|
|
93
|
+
# publish=gate → PR is opened but NEVER merged without the user's word
|
|
94
|
+
# NB: jq's `//` swallows false (falsy) — `.k // empty` on k:false yields the
|
|
95
|
+
# default, silently re-enabling autosave. Test key presence instead (this is
|
|
96
|
+
# the same trap that broke auto_update:false).
|
|
97
|
+
_as_conf() { # key default
|
|
98
|
+
local v
|
|
99
|
+
v=$(jq -r "if (.$1 != null) then (.$1|tostring) else empty end" "$DIR/.egregore-state.json" 2>/dev/null)
|
|
100
|
+
[ -n "$v" ] || v=$(jq -r "if (.$1 != null) then (.$1|tostring) else empty end" "$SCRIPT_ROOT/.egregore-state.json" 2>/dev/null)
|
|
101
|
+
[ -n "$v" ] && echo "$v" || echo "$2"
|
|
102
|
+
}
|
|
103
|
+
# Autosave is an opt-in experiment: enabled defaults to FALSE. Nothing is
|
|
104
|
+
# captured ambiently until the user runs `/autosave on` (or autosave.sh on).
|
|
105
|
+
AS_ENABLED=$(_as_conf autosave_enabled false)
|
|
106
|
+
AS_SCOPE=$(_as_conf autosave_scope all)
|
|
107
|
+
AS_PUBLISH=$(_as_conf autosave_publish gate)
|
|
108
|
+
[ "$AS_ENABLED" = "true" ] || exit 0
|
|
109
|
+
|
|
110
|
+
if [ -z "$AUTHOR" ]; then
|
|
111
|
+
AUTHOR=$(jq -r '.github_username // .name // empty' "$DIR/.egregore-state.json" 2>/dev/null || true)
|
|
112
|
+
[ -n "$AUTHOR" ] || AUTHOR=$(jq -r '.github_username // .name // empty' "$SCRIPT_ROOT/.egregore-state.json" 2>/dev/null || true)
|
|
113
|
+
[ -n "$AUTHOR" ] || AUTHOR=$(git config user.name 2>/dev/null | tr '[:upper:] ' '[:lower:]-' || true)
|
|
114
|
+
[ -n "$AUTHOR" ] || AUTHOR="egregore"
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# --- Memory repo: markdown by definition, push straight to main ----------
|
|
118
|
+
if git -C "$DIR/memory" rev-parse --git-dir >/dev/null 2>&1; then
|
|
119
|
+
if [ -n "$(git -C "$DIR/memory" status --porcelain 2>/dev/null)" ]; then
|
|
120
|
+
git -C "$DIR/memory" add -A >/dev/null 2>&1 || true
|
|
121
|
+
git -C "$DIR/memory" commit -m "Auto-save: session content" --quiet 2>/dev/null || true
|
|
122
|
+
fi
|
|
123
|
+
if [ -n "$(git -C "$DIR/memory" log origin/main..HEAD --oneline 2>/dev/null)" ]; then
|
|
124
|
+
for _try in 1 2 3; do
|
|
125
|
+
if git -C "$DIR/memory" pull --rebase origin main --quiet 2>/dev/null \
|
|
126
|
+
&& git -C "$DIR/memory" push origin main --quiet 2>/dev/null; then
|
|
127
|
+
break
|
|
128
|
+
fi
|
|
129
|
+
sleep 1
|
|
130
|
+
done
|
|
131
|
+
fi
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
# scope=handoffs: memory (above) is the handoff channel; stop before the
|
|
135
|
+
# core repo — it is never captured ambiently under this scope.
|
|
136
|
+
[ "$AS_SCOPE" = "handoffs" ] && exit 0
|
|
137
|
+
|
|
138
|
+
# --- Core repo gate: everything pending must be non-coding ---------------
|
|
139
|
+
# -uall expands untracked dirs to individual files: a collapsed "docs/" entry
|
|
140
|
+
# would defeat both the classifier (code nested under a content dir) and the
|
|
141
|
+
# idle guard (directory mtime, not file mtime).
|
|
142
|
+
DIRTY_PATHS=$(git status --porcelain -uall 2>/dev/null | cut -c4- | sed 's/.* -> //' | sed '/^$/d')
|
|
143
|
+
PENDING=$(
|
|
144
|
+
{
|
|
145
|
+
printf '%s\n' "$DIRTY_PATHS"
|
|
146
|
+
git diff origin/develop...HEAD --name-only 2>/dev/null
|
|
147
|
+
} | sed '/^$/d'
|
|
148
|
+
)
|
|
149
|
+
[ -n "$PENDING" ] || exit 0
|
|
150
|
+
|
|
151
|
+
BLOCKER=$(printf '%s\n' "$PENDING" | noncode_filter)
|
|
152
|
+
if [ -n "$BLOCKER" ]; then
|
|
153
|
+
# Code in the pending set — leave it for an explicit /save + review.
|
|
154
|
+
exit 0
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# --- Idle guard: don't commit files someone is actively editing ----------
|
|
158
|
+
if [ "$MIN_IDLE" -gt 0 ] && [ -n "$DIRTY_PATHS" ]; then
|
|
159
|
+
NOW=$(date +%s)
|
|
160
|
+
while IFS= read -r f; do
|
|
161
|
+
[ -e "$f" ] || continue
|
|
162
|
+
M=$(stat -f %m "$f" 2>/dev/null || stat -c %Y "$f" 2>/dev/null || echo 0)
|
|
163
|
+
[ $(( NOW - M )) -lt "$MIN_IDLE" ] && exit 0
|
|
164
|
+
done <<< "$DIRTY_PATHS"
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
ORIG_BRANCH=$(git branch --show-current 2>/dev/null || echo "")
|
|
168
|
+
|
|
169
|
+
bash "$SCRIPT_ROOT/bin/handoff-save-egregore.sh" "$AUTHOR" \
|
|
170
|
+
"session content $(date +%Y-%m-%d)" --kind autosave --repo-dir "$DIR" \
|
|
171
|
+
--branch-suffix "$DIR_HASH" --publish "$AS_PUBLISH" \
|
|
172
|
+
>/dev/null 2>&1 || true
|
|
173
|
+
|
|
174
|
+
# Tell the user what just happened. Under the Stop hook this line surfaces in
|
|
175
|
+
# the Claude Code UI; sweep/SessionEnd invocations discard stdout (their trail
|
|
176
|
+
# is the ledger, reported by the next greeting).
|
|
177
|
+
LEDGER="${EGREGORE_AUTOSAVE_LOG:-$HOME/.egregore/autosave.log}"
|
|
178
|
+
LAST=$(grep -F "|autosave|$DIR|" "$LEDGER" 2>/dev/null | tail -1 || true)
|
|
179
|
+
if [ -n "$LAST" ]; then
|
|
180
|
+
TS=$(echo "$LAST" | cut -d'|' -f1)
|
|
181
|
+
PR=$(echo "$LAST" | cut -d'|' -f5)
|
|
182
|
+
RESULT=$(echo "$LAST" | cut -d'|' -f6)
|
|
183
|
+
NF=$(echo "$LAST" | cut -d'|' -f7)
|
|
184
|
+
NOW_EPOCH=$(date +%s)
|
|
185
|
+
TS_EPOCH=$(date -u -j -f %Y-%m-%dT%H:%M:%SZ "$TS" +%s 2>/dev/null || date -u -d "$TS" +%s 2>/dev/null || echo 0)
|
|
186
|
+
if [ $(( NOW_EPOCH - TS_EPOCH )) -lt 120 ]; then
|
|
187
|
+
case "$RESULT" in
|
|
188
|
+
merged) echo "⟲ auto-saved: $NF non-coding file(s) → develop (PR #$PR merged)" ;;
|
|
189
|
+
staged) echo "⟲ auto-saved: $NF non-coding file(s) → PR #$PR — yours to merge (say \"merge my auto-saves\" or /autosave)" ;;
|
|
190
|
+
open) echo "⟲ auto-saved: $NF non-coding file(s) → PR #$PR (merge pending)" ;;
|
|
191
|
+
*) echo "⟲ auto-saved: $NF non-coding file(s) → branch pushed (PR pending)" ;;
|
|
192
|
+
esac
|
|
193
|
+
fi
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
# The helper branches off protected branches before committing. If this
|
|
197
|
+
# checkout was on develop/main (e.g. the main checkout during a sweep),
|
|
198
|
+
# restore it so the next session doesn't land on a stray autosave branch.
|
|
199
|
+
case "$ORIG_BRANCH" in
|
|
200
|
+
develop|main|master)
|
|
201
|
+
CUR=$(git branch --show-current 2>/dev/null || echo "")
|
|
202
|
+
if [ "$CUR" != "$ORIG_BRANCH" ]; then
|
|
203
|
+
git checkout "$ORIG_BRANCH" --quiet 2>/dev/null || true
|
|
204
|
+
git pull --ff-only origin "$ORIG_BRANCH" --quiet 2>/dev/null || true
|
|
205
|
+
fi
|
|
206
|
+
;;
|
|
207
|
+
esac
|
|
208
|
+
|
|
209
|
+
exit 0
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Graceful session-end adapter: write a structured session log to memory/sessions/.
|
|
3
|
+
# Receives runtime-neutral JSON on stdin from Claude Code or Pi.
|
|
4
|
+
# Runs BEFORE transcript-archive.sh (needs obs buffer + worktree intact).
|
|
5
|
+
# Makes sessions visible in local-mode without requiring /handoff or /wrap.
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
# Suppress all output — hook scripts must not leak to terminal
|
|
9
|
+
exec >/dev/null 2>&1
|
|
10
|
+
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")/.." 2>/dev/null && pwd)" || SCRIPT_DIR=""
|
|
12
|
+
if [ -z "$SCRIPT_DIR" ] || [ ! -d "$SCRIPT_DIR" ]; then
|
|
13
|
+
SCRIPT_DIR="${CLAUDE_PROJECT_DIR:-}"
|
|
14
|
+
fi
|
|
15
|
+
if [ -z "$SCRIPT_DIR" ] || [ ! -d "$SCRIPT_DIR" ]; then
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# --- Read hook input from stdin ---
|
|
20
|
+
INPUT=$(cat /dev/stdin 2>/dev/null || echo "{}")
|
|
21
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null || true)
|
|
22
|
+
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null || true)
|
|
23
|
+
|
|
24
|
+
if [ -z "$SESSION_ID" ]; then
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Sanitize SESSION_ID
|
|
29
|
+
SESSION_ID=$(echo "$SESSION_ID" | tr -cd 'a-zA-Z0-9_-')
|
|
30
|
+
if [ -z "$SESSION_ID" ] || [ ${#SESSION_ID} -gt 128 ]; then
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# --- Gate: memory must exist ---
|
|
35
|
+
if [ ! -L "$SCRIPT_DIR/memory" ] && [ ! -d "$SCRIPT_DIR/memory" ]; then
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# --- Read identity ---
|
|
40
|
+
STATE_FILE="$SCRIPT_DIR/.egregore-state.json"
|
|
41
|
+
AUTHOR=""
|
|
42
|
+
DISPLAY_NAME=""
|
|
43
|
+
if [ -f "$STATE_FILE" ]; then
|
|
44
|
+
AUTHOR=$(jq -r '.github_username // empty' "$STATE_FILE" 2>/dev/null || true)
|
|
45
|
+
DISPLAY_NAME=$(jq -r '.display_name // empty' "$STATE_FILE" 2>/dev/null || true)
|
|
46
|
+
fi
|
|
47
|
+
AUTHOR=$(echo "$AUTHOR" | tr -cd 'a-zA-Z0-9_-')
|
|
48
|
+
if [ -z "$AUTHOR" ]; then
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# --- Git metadata ---
|
|
53
|
+
BRANCH=$(git -C "$SCRIPT_DIR" branch --show-current 2>/dev/null || echo "")
|
|
54
|
+
|
|
55
|
+
# --- Derive topic from branch name ---
|
|
56
|
+
TOPIC=""
|
|
57
|
+
if [ -n "$BRANCH" ] && [ "$BRANCH" != "develop" ] && [ "$BRANCH" != "main" ]; then
|
|
58
|
+
TOPIC=$(echo "$BRANCH" | sed 's|^dev/[^/]*/||; s|^feature/||; s|^bugfix/||' | tr '-' ' ')
|
|
59
|
+
fi
|
|
60
|
+
[ -z "$TOPIC" ] && TOPIC="session"
|
|
61
|
+
|
|
62
|
+
# --- Read baseline (written by session-start.sh) ---
|
|
63
|
+
BASELINE_FILE="/tmp/egregore-baseline-${SESSION_ID}.json"
|
|
64
|
+
BASELINE_COMMIT=""
|
|
65
|
+
BASELINE_STARTED=""
|
|
66
|
+
if [ -f "$BASELINE_FILE" ]; then
|
|
67
|
+
BASELINE_COMMIT=$(jq -r '.commit // empty' "$BASELINE_FILE" 2>/dev/null || true)
|
|
68
|
+
BASELINE_STARTED=$(jq -r '.started_at // empty' "$BASELINE_FILE" 2>/dev/null || true)
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# --- Extract timestamps from transcript ---
|
|
72
|
+
STARTED_AT=""
|
|
73
|
+
ENDED_AT=""
|
|
74
|
+
if [ -n "$TRANSCRIPT_PATH" ]; then
|
|
75
|
+
TRANSCRIPT_PATH="${TRANSCRIPT_PATH/#\~/$HOME}"
|
|
76
|
+
TRANSCRIPT_PATH=$(realpath "$TRANSCRIPT_PATH" 2>/dev/null || echo "")
|
|
77
|
+
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
78
|
+
STARTED_AT=$(head -1 "$TRANSCRIPT_PATH" 2>/dev/null | jq -r '.timestamp // empty' 2>/dev/null || echo "")
|
|
79
|
+
ENDED_AT=$(tail -1 "$TRANSCRIPT_PATH" 2>/dev/null | jq -r '.timestamp // empty' 2>/dev/null || echo "")
|
|
80
|
+
fi
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# Use baseline start time if transcript didn't provide one
|
|
84
|
+
[ -z "$STARTED_AT" ] && STARTED_AT="$BASELINE_STARTED"
|
|
85
|
+
# Fallback to now
|
|
86
|
+
[ -z "$ENDED_AT" ] && ENDED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
87
|
+
[ -z "$STARTED_AT" ] && STARTED_AT="$ENDED_AT"
|
|
88
|
+
|
|
89
|
+
# --- Compute duration ---
|
|
90
|
+
DURATION=""
|
|
91
|
+
DURATION_MIN=0
|
|
92
|
+
if [ -n "$STARTED_AT" ] && [ -n "$ENDED_AT" ]; then
|
|
93
|
+
S_EPOCH=$(python3 -c "from datetime import datetime; print(int(datetime.fromisoformat('$STARTED_AT'.replace('Z','+00:00')).timestamp()))" 2>/dev/null || echo "0")
|
|
94
|
+
E_EPOCH=$(python3 -c "from datetime import datetime; print(int(datetime.fromisoformat('$ENDED_AT'.replace('Z','+00:00')).timestamp()))" 2>/dev/null || echo "0")
|
|
95
|
+
if [ "$S_EPOCH" -gt 0 ] 2>/dev/null && [ "$E_EPOCH" -gt 0 ] 2>/dev/null; then
|
|
96
|
+
DURATION_MIN=$(( (E_EPOCH - S_EPOCH) / 60 ))
|
|
97
|
+
if [ "$DURATION_MIN" -lt 1 ] 2>/dev/null; then
|
|
98
|
+
DURATION="<1min"
|
|
99
|
+
elif [ "$DURATION_MIN" -lt 60 ] 2>/dev/null; then
|
|
100
|
+
DURATION="${DURATION_MIN}min"
|
|
101
|
+
else
|
|
102
|
+
HOURS=$(( DURATION_MIN / 60 ))
|
|
103
|
+
MINS=$(( DURATION_MIN % 60 ))
|
|
104
|
+
if [ "$MINS" -gt 0 ] 2>/dev/null; then
|
|
105
|
+
DURATION="${HOURS}h ${MINS}min"
|
|
106
|
+
else
|
|
107
|
+
DURATION="${HOURS}h"
|
|
108
|
+
fi
|
|
109
|
+
fi
|
|
110
|
+
fi
|
|
111
|
+
fi
|
|
112
|
+
[ -z "$DURATION" ] && DURATION="unknown"
|
|
113
|
+
|
|
114
|
+
# --- Read obs buffer for files touched ---
|
|
115
|
+
OBS_BUFFER="/tmp/egregore-obs-${SESSION_ID}.jsonl"
|
|
116
|
+
OBS_FILES=""
|
|
117
|
+
OBS_COUNT=0
|
|
118
|
+
if [ -f "$OBS_BUFFER" ] && [ -s "$OBS_BUFFER" ]; then
|
|
119
|
+
OBS_COUNT=$(wc -l < "$OBS_BUFFER" 2>/dev/null | tr -d ' ')
|
|
120
|
+
OBS_FILES=$(jq -r '.path // empty' "$OBS_BUFFER" 2>/dev/null | grep -v '^$' | grep -v '^unknown$' | sort -u | head -10 || true)
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# --- Count commits since baseline ---
|
|
124
|
+
COMMIT_COUNT=0
|
|
125
|
+
if [ -n "$BASELINE_COMMIT" ] && git -C "$SCRIPT_DIR" rev-parse "$BASELINE_COMMIT" >/dev/null 2>&1; then
|
|
126
|
+
COMMIT_COUNT=$(git -C "$SCRIPT_DIR" rev-list "${BASELINE_COMMIT}..HEAD" --count 2>/dev/null || echo "0")
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# --- Empty session guard ---
|
|
130
|
+
# Skip if: duration < 2 min AND no commits AND no obs buffer activity
|
|
131
|
+
if [ "$DURATION_MIN" -lt 2 ] 2>/dev/null && [ "$COMMIT_COUNT" -eq 0 ] 2>/dev/null && [ "$OBS_COUNT" -eq 0 ] 2>/dev/null; then
|
|
132
|
+
rm -f "$BASELINE_FILE" 2>/dev/null
|
|
133
|
+
exit 0
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# --- Deduplication: explicit /handoff or /wrap wins over baseline capture ---
|
|
137
|
+
TODAY=$(date +%d)
|
|
138
|
+
MONTH=$(date +%Y-%m)
|
|
139
|
+
if [ -n "$STARTED_AT" ]; then
|
|
140
|
+
MONTH=$(echo "$STARTED_AT" | grep -oE '^[0-9]{4}-[0-9]{2}' || date +%Y-%m)
|
|
141
|
+
TODAY=$(echo "$STARTED_AT" | grep -oE '^[0-9]{4}-[0-9]{2}-([0-9]{2})' | tail -c3 | sed 's/^-//' || date +%d)
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
AUTHOR_LC=$(echo "$AUTHOR" | tr '[:upper:]' '[:lower:]')
|
|
145
|
+
DISPLAY_LC=$(echo "$DISPLAY_NAME" | tr '[:upper:]' '[:lower:]')
|
|
146
|
+
AUTHOR_PATTERN="-${AUTHOR_LC}-"
|
|
147
|
+
[ -n "$DISPLAY_LC" ] && AUTHOR_PATTERN="${AUTHOR_PATTERN}|-${DISPLAY_LC}-"
|
|
148
|
+
for CAPTURE_DIR in \
|
|
149
|
+
"$SCRIPT_DIR/memory/handoffs/$MONTH" \
|
|
150
|
+
"$SCRIPT_DIR/memory/wraps/$MONTH"; do
|
|
151
|
+
[ -d "$CAPTURE_DIR" ] || continue
|
|
152
|
+
# Match the canonical handle or display name in today's filename.
|
|
153
|
+
EXISTING=""
|
|
154
|
+
for CANDIDATE in "$CAPTURE_DIR/${TODAY}-"*.md; do
|
|
155
|
+
[ -e "$CANDIDATE" ] || continue
|
|
156
|
+
CANDIDATE_NAME="$(basename "$CANDIDATE" | tr '[:upper:]' '[:lower:]')"
|
|
157
|
+
if printf '%s\n' "$CANDIDATE_NAME" | grep -Eq -- "$AUTHOR_PATTERN"; then
|
|
158
|
+
EXISTING="$CANDIDATE_NAME"
|
|
159
|
+
break
|
|
160
|
+
fi
|
|
161
|
+
done
|
|
162
|
+
if [ -n "$EXISTING" ]; then
|
|
163
|
+
rm -f "$BASELINE_FILE" 2>/dev/null
|
|
164
|
+
exit 0
|
|
165
|
+
fi
|
|
166
|
+
done
|
|
167
|
+
|
|
168
|
+
# --- Write through the shared capture engine -----------------------------
|
|
169
|
+
SUMMARY="${DURATION} session on ${TOPIC}; ${COMMIT_COUNT} commit(s), ${OBS_COUNT} observed action(s)."
|
|
170
|
+
{
|
|
171
|
+
if [ -n "$OBS_FILES" ]; then
|
|
172
|
+
echo "## Files"
|
|
173
|
+
echo "$OBS_FILES" | while read -r F; do
|
|
174
|
+
[ -n "$F" ] && echo "- $F"
|
|
175
|
+
done
|
|
176
|
+
echo ""
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
if [ "$COMMIT_COUNT" -gt 0 ] 2>/dev/null; then
|
|
180
|
+
echo "## Commits"
|
|
181
|
+
echo "$COMMIT_COUNT commits on ${BRANCH:-develop}"
|
|
182
|
+
echo ""
|
|
183
|
+
fi
|
|
184
|
+
} | bash "$SCRIPT_DIR/bin/capture-run.sh" \
|
|
185
|
+
--mode baseline \
|
|
186
|
+
--author "$AUTHOR" \
|
|
187
|
+
--display-name "$DISPLAY_NAME" \
|
|
188
|
+
--topic "$TOPIC" \
|
|
189
|
+
--summary "$SUMMARY" \
|
|
190
|
+
--session-id "$SESSION_ID" \
|
|
191
|
+
--branch "${BRANCH:-develop}" \
|
|
192
|
+
--date "$STARTED_AT" \
|
|
193
|
+
--duration "$DURATION" \
|
|
194
|
+
--async-push >/dev/null 2>&1 || true
|
|
195
|
+
|
|
196
|
+
# --- Clean up baseline ---
|
|
197
|
+
rm -f "$BASELINE_FILE" 2>/dev/null
|
|
198
|
+
|
|
199
|
+
exit 0
|