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
|
@@ -58,6 +58,12 @@ const AGENTS_PREAMBLE = `# Egregore Agent Protocol
|
|
|
58
58
|
> \`.pi/APPEND_SYSTEM.md\`, and your workflows are exposed through the
|
|
59
59
|
> project-local \`.pi/\` runtime. Do not follow the Codex-specific block below.
|
|
60
60
|
>
|
|
61
|
+
> **If you are Prime Agent** (the \`prime-agent\` harness): **stop here.** Your
|
|
62
|
+
> authoritative Egregore instructions are loaded from
|
|
63
|
+
> \`.prime/agent/APPEND_SYSTEM.md\`, and your workflows are exposed through the
|
|
64
|
+
> project-local \`.prime/agent/\` runtime. Do not follow the Codex-specific
|
|
65
|
+
> block below.
|
|
66
|
+
>
|
|
61
67
|
> **If you are Codex or another shell-only agent:** this file is yours — continue.
|
|
62
68
|
|
|
63
69
|
Egregore is no longer only a Claude Code workspace. Claude Code remains the
|
|
@@ -210,7 +210,9 @@ render_card() {
|
|
|
210
210
|
|
|
211
211
|
# Staged auto-saves awaiting the user's merge (the autosave consent gate).
|
|
212
212
|
# Live gh query so the line persists until the PRs actually merge.
|
|
213
|
-
|
|
213
|
+
# Shown only while the autosave experiment is enabled.
|
|
214
|
+
autosave_on=$(jq -r '.autosave_enabled // false' "$STATE_FILE" 2>/dev/null)
|
|
215
|
+
if [ "$autosave_on" = "true" ] && command -v gh >/dev/null 2>&1; then
|
|
214
216
|
gh_self=$(jq -r '.github_username // empty' "$STATE_FILE" 2>/dev/null)
|
|
215
217
|
if [ -n "$gh_self" ]; then
|
|
216
218
|
waiting_as=$(gh pr list --state open --author "$gh_self" --limit 20 \
|
|
@@ -24,21 +24,40 @@ if [ "$_MODE" = "local" ]; then
|
|
|
24
24
|
exit 0
|
|
25
25
|
fi
|
|
26
26
|
|
|
27
|
-
# Load specific variables from .env if it exists (safe extraction, no arbitrary code execution)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
# Load specific variables from .env if it exists (safe extraction, no arbitrary code execution).
|
|
28
|
+
# A worktree missing its .env symlink resolves through worktree-links.sh, which
|
|
29
|
+
# self-heals the link and falls back to the main checkout's .env.
|
|
30
|
+
_ENV_FILE="$SCRIPT_DIR/.env"
|
|
31
|
+
if [ ! -f "$_ENV_FILE" ] && [ -f "$SCRIPT_DIR/bin/lib/worktree-links.sh" ]; then
|
|
32
|
+
# shellcheck source=bin/lib/worktree-links.sh
|
|
33
|
+
source "$SCRIPT_DIR/bin/lib/worktree-links.sh" 2>/dev/null || true
|
|
34
|
+
_ENV_FILE="$(egregore_env_file "$SCRIPT_DIR" 2>/dev/null || true)"
|
|
35
|
+
fi
|
|
36
|
+
if [ -n "$_ENV_FILE" ] && [ -f "$_ENV_FILE" ]; then
|
|
37
|
+
EGREGORE_API_URL="${EGREGORE_API_URL:-$(grep '^EGREGORE_API_URL=' "$_ENV_FILE" 2>/dev/null | cut -d'=' -f2- || true)}"
|
|
38
|
+
EGREGORE_API_KEY="${EGREGORE_API_KEY:-$(grep '^EGREGORE_API_KEY=' "$_ENV_FILE" 2>/dev/null | cut -d'=' -f2- || true)}"
|
|
31
39
|
fi
|
|
32
40
|
|
|
33
41
|
API_URL="${EGREGORE_API_URL:-$(jq -r '.api_url // empty' "$CONFIG")}"
|
|
34
42
|
API_KEY="${EGREGORE_API_KEY:-}"
|
|
35
43
|
|
|
36
|
-
if [ -z "$API_URL" ]
|
|
37
|
-
# === OFFLINE MODE:
|
|
44
|
+
if [ -z "$API_URL" ]; then
|
|
45
|
+
# === OFFLINE MODE: no api_url configured (OSS/unconfigured) ===
|
|
38
46
|
echo '{"results":[]}'
|
|
39
47
|
exit 0
|
|
40
48
|
fi
|
|
41
49
|
|
|
50
|
+
if [ -z "$API_KEY" ]; then
|
|
51
|
+
# api_url is configured but no key was found — the local-mode gate exited
|
|
52
|
+
# above, so the org expects a live graph. Faking empty results here hides a
|
|
53
|
+
# missing .env (e.g. a worktree without its symlink) from every caller.
|
|
54
|
+
{
|
|
55
|
+
echo "Error: api_url is configured but EGREGORE_API_KEY is empty (looked in: ${_ENV_FILE:-$SCRIPT_DIR/.env})."
|
|
56
|
+
echo " If this is a worktree, its .env symlink may be missing. Run /env to configure."
|
|
57
|
+
} >&2
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
|
|
42
61
|
QUERIES_JSON="${1:?Usage: graph-batch.sh '<json array of queries>'}"
|
|
43
62
|
|
|
44
63
|
# Wrap the array into the batch request format
|
|
@@ -708,7 +708,8 @@ case "$OP" in
|
|
|
708
708
|
fi
|
|
709
709
|
bash "$GS" query "
|
|
710
710
|
MATCH (s:Session)-[:HANDED_TO]->(p:Person)
|
|
711
|
-
WHERE (
|
|
711
|
+
WHERE NOT coalesce(p.kind, '') IN ['identity_alias']
|
|
712
|
+
AND (
|
|
712
713
|
toLower(coalesce(p.name, '')) = toLower(\$user)
|
|
713
714
|
OR toLower(coalesce(p.github, '')) = toLower(\$user)
|
|
714
715
|
OR toLower(coalesce(p.githubUsername, '')) = toLower(\$user)
|
|
@@ -718,13 +719,13 @@ case "$OP" in
|
|
|
718
719
|
AND coalesce(s.handoffStatus, 'pending')
|
|
719
720
|
IN ['pending','unread','read','claimed']
|
|
720
721
|
WITH DISTINCT s
|
|
721
|
-
|
|
722
|
-
|
|
722
|
+
WITH s, head([(s)-[:BY]->(author:Person)
|
|
723
|
+
WHERE NOT coalesce(author.kind, '') IN ['identity_alias'] | author.name]) AS authorName,
|
|
723
724
|
CASE WHEN s.date IS NULL THEN null
|
|
724
725
|
ELSE duration.inDays(date(s.date), date()).days END AS ageDays
|
|
725
726
|
RETURN s.id AS id,
|
|
726
727
|
coalesce(s.topic, s.summary, s.branch) AS topic,
|
|
727
|
-
coalesce(
|
|
728
|
+
coalesce(authorName, s.author, 'unknown') AS author,
|
|
728
729
|
toString(s.date) AS date,
|
|
729
730
|
coalesce(s.handoffStatus, 'pending') AS status,
|
|
730
731
|
coalesce(s.handoffIntent, 'unclassified') AS intent,
|
package/runtime/pi/bin/graph.sh
CHANGED
|
@@ -21,10 +21,18 @@ if [ "$_MODE" = "local" ]; then
|
|
|
21
21
|
exit 0
|
|
22
22
|
fi
|
|
23
23
|
|
|
24
|
-
# Load specific variables from .env if it exists (safe extraction, no arbitrary code execution)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
# Load specific variables from .env if it exists (safe extraction, no arbitrary code execution).
|
|
25
|
+
# A worktree missing its .env symlink resolves through worktree-links.sh, which
|
|
26
|
+
# self-heals the link and falls back to the main checkout's .env.
|
|
27
|
+
_ENV_FILE="$SCRIPT_DIR/.env"
|
|
28
|
+
if [ ! -f "$_ENV_FILE" ] && [ -f "$SCRIPT_DIR/bin/lib/worktree-links.sh" ]; then
|
|
29
|
+
# shellcheck source=bin/lib/worktree-links.sh
|
|
30
|
+
source "$SCRIPT_DIR/bin/lib/worktree-links.sh" 2>/dev/null || true
|
|
31
|
+
_ENV_FILE="$(egregore_env_file "$SCRIPT_DIR" 2>/dev/null || true)"
|
|
32
|
+
fi
|
|
33
|
+
if [ -n "$_ENV_FILE" ] && [ -f "$_ENV_FILE" ]; then
|
|
34
|
+
EGREGORE_API_URL="${EGREGORE_API_URL:-$(grep '^EGREGORE_API_URL=' "$_ENV_FILE" 2>/dev/null | cut -d'=' -f2- || true)}"
|
|
35
|
+
EGREGORE_API_KEY="${EGREGORE_API_KEY:-$(grep '^EGREGORE_API_KEY=' "$_ENV_FILE" 2>/dev/null | cut -d'=' -f2- || true)}"
|
|
28
36
|
fi
|
|
29
37
|
|
|
30
38
|
# api_url comes from egregore.json (committed, non-secret)
|
|
@@ -168,8 +176,26 @@ if [ -n "$API_URL" ] && [ -n "$API_KEY" ]; then
|
|
|
168
176
|
fi
|
|
169
177
|
}
|
|
170
178
|
|
|
179
|
+
elif [ -n "$API_URL" ]; then
|
|
180
|
+
# === CONNECTED CONFIG, MISSING KEY ===
|
|
181
|
+
# api_url is configured, so the org expects a live graph, but no API key was
|
|
182
|
+
# found. Faking '{"results":[]}' here made a missing credential (e.g. a
|
|
183
|
+
# worktree without its .env symlink) indistinguishable from an empty graph —
|
|
184
|
+
# fail loudly instead. `test` keeps its offline JSON contract: dashboards,
|
|
185
|
+
# render-card, and startup-check consume it.
|
|
186
|
+
_missing_credentials() {
|
|
187
|
+
{
|
|
188
|
+
echo "Error: api_url is configured but EGREGORE_API_KEY is empty (looked in: ${_ENV_FILE:-$SCRIPT_DIR/.env})."
|
|
189
|
+
echo " If this is a worktree, its .env symlink may be missing. Run /env to configure."
|
|
190
|
+
} >&2
|
|
191
|
+
exit 1
|
|
192
|
+
}
|
|
193
|
+
run_query() { _missing_credentials; }
|
|
194
|
+
get_schema() { _missing_credentials; }
|
|
195
|
+
test_connection() { echo '{"status":"offline","reason":"no_api_key"}'; }
|
|
196
|
+
|
|
171
197
|
else
|
|
172
|
-
# === OFFLINE MODE:
|
|
198
|
+
# === OFFLINE MODE: no api_url configured (OSS/unconfigured) ===
|
|
173
199
|
run_query() { echo '{"results":[]}'; }
|
|
174
200
|
get_schema() { echo '{}'; }
|
|
175
201
|
test_connection() { echo '{"status":"offline","reason":"no_api_key"}'; }
|
package/runtime/pi/bin/ingest.py
CHANGED
|
@@ -23,10 +23,12 @@ from datetime import datetime, timezone
|
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
from xml.etree import ElementTree
|
|
25
25
|
|
|
26
|
-
sys.path.
|
|
26
|
+
sys.path.append(str(Path(__file__).resolve().parent))
|
|
27
|
+
import catalogue as catalogue_module # noqa: E402
|
|
27
28
|
import doc_segment # noqa: E402 (same-directory module, path set above)
|
|
28
29
|
import pdf_extract # noqa: E402
|
|
29
30
|
import source_profile # noqa: E402
|
|
31
|
+
import text_repair # noqa: E402
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
ROOT = Path(__file__).resolve().parent.parent
|
|
@@ -152,6 +154,35 @@ def chunks(text: str, limit: int = 7000) -> list[str]:
|
|
|
152
154
|
return result
|
|
153
155
|
|
|
154
156
|
|
|
157
|
+
def find_publisher_profile() -> Path | None:
|
|
158
|
+
"""Where this instance's publisher profile is, if it has one.
|
|
159
|
+
|
|
160
|
+
The profile is what assigns a zone and a trust tier to each document, and
|
|
161
|
+
zone is a hard retrieval boundary. Ingesting without it produces a corpus
|
|
162
|
+
where every document is unplaced, so every later question is refused — a
|
|
163
|
+
failure that looks like an empty archive rather than a missing setting.
|
|
164
|
+
|
|
165
|
+
So this looks in the places a profile actually lives rather than in one
|
|
166
|
+
fixed path. An instance keeps its ontology under memory/knowledge/tools/,
|
|
167
|
+
not beside its intake metadata, and requiring an environment variable to
|
|
168
|
+
bridge that gap means the common case is the broken one.
|
|
169
|
+
"""
|
|
170
|
+
override = os.environ.get("EGREGORE_PUBLISHER_PROFILE")
|
|
171
|
+
if override:
|
|
172
|
+
path = Path(override)
|
|
173
|
+
return path if path.is_file() else None
|
|
174
|
+
|
|
175
|
+
candidates = [metadata_root() / "publisher-profile.json"]
|
|
176
|
+
memory = Path("memory")
|
|
177
|
+
if memory.is_dir():
|
|
178
|
+
candidates.extend(sorted(memory.glob("knowledge/tools/*/publisher-profile.json")))
|
|
179
|
+
candidates.append(memory / "publisher-profile.json")
|
|
180
|
+
for path in candidates:
|
|
181
|
+
if path.is_file():
|
|
182
|
+
return path
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
|
|
155
186
|
def publisher_profile() -> dict | None:
|
|
156
187
|
"""Load the instance's publisher profile, if one is configured.
|
|
157
188
|
|
|
@@ -159,9 +190,8 @@ def publisher_profile() -> dict | None:
|
|
|
159
190
|
operator set: deriving a region is an inference, and an inference must not
|
|
160
191
|
silently replace a stated fact.
|
|
161
192
|
"""
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if not path.is_file():
|
|
193
|
+
path = find_publisher_profile()
|
|
194
|
+
if path is None:
|
|
165
195
|
return None
|
|
166
196
|
try:
|
|
167
197
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
@@ -223,7 +253,19 @@ def add_path(args: argparse.Namespace) -> dict:
|
|
|
223
253
|
existing_manifest = load_manifest(directory)
|
|
224
254
|
previous = {d["id"]: d for d in existing_manifest.get("documents", [])}
|
|
225
255
|
now = datetime.now(timezone.utc).isoformat()
|
|
256
|
+
profile_path = find_publisher_profile()
|
|
226
257
|
profile = publisher_profile()
|
|
258
|
+
if profile is None:
|
|
259
|
+
# Said once, plainly, because the alternative is a corpus that ingests
|
|
260
|
+
# cleanly and then refuses every question with no indication why.
|
|
261
|
+
print("ingest: no publisher-profile.json found — documents will carry no zone "
|
|
262
|
+
"or trust tier, and a zoneless document can never be served. Put one "
|
|
263
|
+
"under memory/knowledge/tools/<ontology>/ or set "
|
|
264
|
+
"EGREGORE_PUBLISHER_PROFILE.", file=sys.stderr)
|
|
265
|
+
catalogue = catalogue_module.load(
|
|
266
|
+
profile or {}, base=profile_path.parent if profile_path else None
|
|
267
|
+
)
|
|
268
|
+
repair_map = text_repair.load_mapping(profile or {})
|
|
227
269
|
ingested = unchanged = metadata_updated = skipped = pruned = 0
|
|
228
270
|
errors: list[dict] = []
|
|
229
271
|
seen_paths: set[str] = set()
|
|
@@ -252,6 +294,15 @@ def add_path(args: argparse.Namespace) -> dict:
|
|
|
252
294
|
extraction = {"kind": "local", "extractor": extractor} if extractor else None
|
|
253
295
|
if extraction and detail:
|
|
254
296
|
extraction["pages"] = detail
|
|
297
|
+
# Fix characters the document's own font encoding got wrong, before
|
|
298
|
+
# anything downstream indexes or quotes them.
|
|
299
|
+
repaired = None
|
|
300
|
+
if text and repair_map:
|
|
301
|
+
outcome = text_repair.repair(text, repair_map)
|
|
302
|
+
if outcome.replaced:
|
|
303
|
+
text = outcome.text
|
|
304
|
+
repaired = outcome.as_report()
|
|
305
|
+
|
|
255
306
|
if error or text is None or not text.strip():
|
|
256
307
|
skipped += 1
|
|
257
308
|
errors.append({"path": relative, "error": error or "empty extracted text"})
|
|
@@ -283,6 +334,9 @@ def add_path(args: argparse.Namespace) -> dict:
|
|
|
283
334
|
continue
|
|
284
335
|
# Segment before chunking so no passage straddles two works. A document
|
|
285
336
|
# holding a single work yields one segment, and chunking is unchanged.
|
|
337
|
+
if repaired and extraction:
|
|
338
|
+
extraction["repair"] = repaired
|
|
339
|
+
|
|
286
340
|
segments = doc_segment.segment(text)
|
|
287
341
|
document_chunks: list[tuple[str, doc_segment.Segment]] = []
|
|
288
342
|
for seg in segments:
|
|
@@ -314,12 +368,34 @@ def add_path(args: argparse.Namespace) -> dict:
|
|
|
314
368
|
# is absent filters nothing. A derived value is marked as derived: anything
|
|
315
369
|
# downstream can tell it apart from one a person stated, and weigh it by the
|
|
316
370
|
# confidence recorded alongside. An operator's value is never overwritten.
|
|
371
|
+
# Evidence strongest first: what the operator stated, then what the
|
|
372
|
+
# organisation's own catalogue records, then what can be inferred from
|
|
373
|
+
# the document's origin. A weaker source never overwrites a stronger one.
|
|
317
374
|
provenance = None
|
|
318
375
|
doc_boundaries = dict(boundaries)
|
|
376
|
+
catalogued = catalogue_module.lookup(catalogue, relative)
|
|
377
|
+
|
|
378
|
+
if catalogued:
|
|
379
|
+
provenance = {"from_catalogue": {k: v for k, v in catalogued.items()}}
|
|
380
|
+
for field_name, value in catalogued.items():
|
|
381
|
+
if field_name in ("zone", "crop", "topic", "customer", "jurisdiction", "project"):
|
|
382
|
+
if field_name not in doc_boundaries:
|
|
383
|
+
doc_boundaries[field_name] = value
|
|
384
|
+
provenance["applied_boundary"] = field_name
|
|
385
|
+
if catalogued.get("year"):
|
|
386
|
+
structure.setdefault("publication_year", catalogued["year"])
|
|
387
|
+
structure["year_source"] = structure.get("year_source") or "catalogue"
|
|
388
|
+
if catalogued.get("tier"):
|
|
389
|
+
provenance["tier"] = catalogued["tier"]
|
|
390
|
+
provenance["confidence"] = "high"
|
|
391
|
+
|
|
319
392
|
if profile:
|
|
320
393
|
resolved = source_profile.resolve(profile, filename=relative)
|
|
321
394
|
if resolved.publisher_class or resolved.notes:
|
|
322
|
-
|
|
395
|
+
inferred = resolved.as_dict()
|
|
396
|
+
# Keep the inference visible even when the catalogue answered,
|
|
397
|
+
# so a disagreement between the two is inspectable.
|
|
398
|
+
provenance = {**inferred, **(provenance or {})} if provenance else inferred
|
|
323
399
|
key = source_profile.boundary_key(profile)
|
|
324
400
|
if key and resolved.zone and key not in doc_boundaries:
|
|
325
401
|
doc_boundaries[key] = resolved.zone
|
|
@@ -370,6 +370,29 @@ fi
|
|
|
370
370
|
JSON="[]"
|
|
371
371
|
RICH="[]"
|
|
372
372
|
if [ -d "$SCRIPT_DIR/memory/handoffs" ]; then
|
|
373
|
+
# One definition everywhere: in connected mode the list comes from the
|
|
374
|
+
# graph's open-handoffs named read (open = pending/unread/read/claimed,
|
|
375
|
+
# matched by name/github/aliases) — the same definition the dashboard
|
|
376
|
+
# uses. The file grep below stays as the local-mode/offline fallback.
|
|
377
|
+
ADDRESSED=""
|
|
378
|
+
if [ "${LOCAL_MODE:-false}" != "true" ]; then
|
|
379
|
+
_GRAPH_LIST=$(bash "$SCRIPT_DIR/bin/graph-op.sh" open-handoffs "$AUTHOR" 5 2>/dev/null || true)
|
|
380
|
+
if echo "$_GRAPH_LIST" | jq -e '.values | length > 0' >/dev/null 2>&1; then
|
|
381
|
+
ADDRESSED=$(echo "$_GRAPH_LIST" | jq -r '.values[][0]' 2>/dev/null | awk '!seen[$0]++' | while IFS= read -r _SID; do
|
|
382
|
+
# Two on-disk layouts: dated subdirs (YYYY-MM/DD-slug.md, the id's
|
|
383
|
+
# own shape) and flat files at the handoffs root. Probe both, else
|
|
384
|
+
# a graph-listed handoff silently vanishes from the greeting.
|
|
385
|
+
_MONTH="${_SID:0:7}"
|
|
386
|
+
_REST="${_SID:8}"
|
|
387
|
+
_F="$SCRIPT_DIR/memory/handoffs/$_MONTH/$_REST.md"
|
|
388
|
+
if [ -f "$_F" ]; then
|
|
389
|
+
echo "$_F"
|
|
390
|
+
elif [ -f "$SCRIPT_DIR/memory/handoffs/$_SID.md" ]; then
|
|
391
|
+
echo "$SCRIPT_DIR/memory/handoffs/$_SID.md"
|
|
392
|
+
fi
|
|
393
|
+
done | head -5)
|
|
394
|
+
fi
|
|
395
|
+
fi
|
|
373
396
|
# Match github username, display name, github_name first word, and people-file name
|
|
374
397
|
# Handoff files may use "to: name", "**To**: name", or "To: name"
|
|
375
398
|
_DISPLAY=$(jq -r '.display_name // empty' "$STATE_FILE" 2>/dev/null)
|
|
@@ -387,7 +410,9 @@ fi
|
|
|
387
410
|
echo "$_GREP_PAT" | grep -qF "$_VARIANT" && continue
|
|
388
411
|
_GREP_PAT="$_GREP_PAT\|[Tt]o[*]*: *$_VARIANT\|[Tt]o[*]*:$_VARIANT"
|
|
389
412
|
done
|
|
390
|
-
|
|
413
|
+
if [ -z "$ADDRESSED" ]; then
|
|
414
|
+
ADDRESSED=$(grep -rli "$_GREP_PAT" "$SCRIPT_DIR/memory/handoffs/" 2>/dev/null | sort -r | head -5 || true)
|
|
415
|
+
fi
|
|
391
416
|
JSON="["
|
|
392
417
|
RICH="["
|
|
393
418
|
FIRST=true
|
|
@@ -188,7 +188,10 @@ fi
|
|
|
188
188
|
# Staged auto-saves awaiting the user's merge (the /autosave consent gate).
|
|
189
189
|
# Live gh query so the line persists until the PRs actually merge — the
|
|
190
190
|
# ledger cursor above only reports once. Fail-soft when gh/network is out.
|
|
191
|
-
|
|
191
|
+
# Shown only while the autosave experiment is enabled: stale PRs from a
|
|
192
|
+
# disabled experiment should not nag every greeting.
|
|
193
|
+
AUTOSAVE_ON=$(jq -r '.autosave_enabled // false' "$STATE_FILE" 2>/dev/null)
|
|
194
|
+
if [ "$AUTOSAVE_ON" = "true" ] && command -v gh >/dev/null 2>&1; then
|
|
192
195
|
GH_SELF=$(jq -r '.github_username // empty' "$STATE_FILE" 2>/dev/null)
|
|
193
196
|
if [ -n "$GH_SELF" ]; then
|
|
194
197
|
WAITING_AS=$(gh pr list --state open --author "$GH_SELF" --limit 20 \
|
|
@@ -52,3 +52,28 @@ egregore_link_shared_state() {
|
|
|
52
52
|
fi
|
|
53
53
|
done
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
# Resolve the .env that governs a checkout. A worktree can be missing its .env
|
|
57
|
+
# symlink (created before .env existed, or by a runtime that skipped worktree
|
|
58
|
+
# setup) — self-heal the links, then fall back to the main checkout's .env so
|
|
59
|
+
# credential loading never silently comes up empty. Prints the usable path, or
|
|
60
|
+
# nothing when neither checkout has one. Always returns 0.
|
|
61
|
+
egregore_env_file() {
|
|
62
|
+
local project_dir="${1:?project dir required}"
|
|
63
|
+
if [ ! -f "$project_dir/.env" ] && [ -f "$project_dir/.git" ]; then
|
|
64
|
+
egregore_link_shared_state "$project_dir" 2>/dev/null || true
|
|
65
|
+
fi
|
|
66
|
+
if [ -f "$project_dir/.env" ]; then
|
|
67
|
+
echo "$project_dir/.env"
|
|
68
|
+
return 0
|
|
69
|
+
fi
|
|
70
|
+
if [ -f "$project_dir/.git" ]; then
|
|
71
|
+
local main_dir
|
|
72
|
+
main_dir="$(egregore_main_project_dir "$project_dir" 2>/dev/null || true)"
|
|
73
|
+
if [ -n "$main_dir" ] && [ -f "$main_dir/.env" ]; then
|
|
74
|
+
echo "$main_dir/.env"
|
|
75
|
+
return 0
|
|
76
|
+
fi
|
|
77
|
+
fi
|
|
78
|
+
return 0
|
|
79
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
2
2
|
|
|
3
3
|
const MUTATING_COMMANDS = /(^|[;&|]\s*)(?:sudo\s+)?(rm|mv|cp|mkdir|rmdir|touch|chmod|chown|install|tee)\b/;
|
|
4
|
-
|
|
4
|
+
// Global options (-C <path>, -c k=v, --git-dir=…) may sit between `git` and
|
|
5
|
+
// the subcommand. The repo convention actively teaches `git -C <abs-path>`,
|
|
6
|
+
// so a classifier that only matches bare `git commit` misses the shape the
|
|
7
|
+
// model is most likely to emit.
|
|
8
|
+
const MUTATING_GIT = /(^|[;&|]\s*)(?:sudo\s+)?git\s+(?:(?:-C|-c|--git-dir|--work-tree)(?:=\S+|\s+\S+)\s+)*(add|commit|push|merge|rebase|cherry-pick|tag|rm|mv|reset|restore|clean)\b/;
|
|
5
9
|
const MUTATING_PACKAGES = /(^|[;&|]\s*)(?:sudo\s+)?(npm|pnpm|yarn|bun)\s+(install|add|remove|uninstall|update)\b/;
|
|
6
10
|
|
|
7
11
|
// A `>` only acts as a shell redirect when it sits OUTSIDE quotes. Quoted
|
|
@@ -112,6 +116,7 @@ export function isRuntimeStatePath(target, projectDir) {
|
|
|
112
116
|
rel === ".claude" || rel.startsWith(".claude/") ||
|
|
113
117
|
rel === ".codex" || rel.startsWith(".codex/") ||
|
|
114
118
|
rel === ".pi" || rel.startsWith(".pi/") ||
|
|
119
|
+
rel === ".prime" || rel.startsWith(".prime/") ||
|
|
115
120
|
rel === ".egregore-state.json" ||
|
|
116
121
|
rel === ".egregore-branch-consent" ||
|
|
117
122
|
rel === ".env";
|
|
@@ -162,6 +167,20 @@ export function mutatesOnlyRuntimeState(command, projectDir) {
|
|
|
162
167
|
return foundTarget;
|
|
163
168
|
}
|
|
164
169
|
|
|
170
|
+
// The documented escape hatch must itself be runnable, but ONLY as the whole
|
|
171
|
+
// command: an unanchored match lets `git push origin develop && echo 'develop'
|
|
172
|
+
// > .egregore-branch-consent` pass the gate in one call, and the block message
|
|
173
|
+
// teaches that exact echo string. Canonical form mirrors
|
|
174
|
+
// .codex/hooks/branch-guard.js isConsentCommand — anchored, branch escaped,
|
|
175
|
+
// optional ./ prefix — and is shared so the three runtimes cannot drift.
|
|
176
|
+
export function isConsentCommand(command, branch) {
|
|
177
|
+
if (!branch) return false;
|
|
178
|
+
const escaped = String(branch).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
179
|
+
return new RegExp(
|
|
180
|
+
`^\\s*echo\\s+['"]?${escaped}['"]?\\s*>\\s*(?:\\./)?\\.egregore-branch-consent\\s*$`
|
|
181
|
+
).test(String(command || ""));
|
|
182
|
+
}
|
|
183
|
+
|
|
165
184
|
export function isMutatingBash(command) {
|
|
166
185
|
if (!command) return false;
|
|
167
186
|
const meaningfulRedirects = stripQuotedSegments(String(command))
|
|
@@ -173,3 +192,121 @@ export function isMutatingBash(command) {
|
|
|
173
192
|
/(^|[^<])>>?\s*[^&]/.test(meaningfulRedirects) ||
|
|
174
193
|
/\bsed\s+[^\n]*\s-i(?:\s|$)/.test(command);
|
|
175
194
|
}
|
|
195
|
+
|
|
196
|
+
// ---- IPython code classification (Prime Agent) -----------------------------
|
|
197
|
+
// Prime Agent's default tool surface is a persistent IPython kernel: file
|
|
198
|
+
// writes and shell escapes arrive as Python code, not bash tool calls. This
|
|
199
|
+
// classifier is deliberately static and therefore porous (exec/eval and
|
|
200
|
+
// string-built commands pass); the generated runtime spec carries the
|
|
201
|
+
// standing-instruction discipline as the backstop, and the adversarial test
|
|
202
|
+
// fixtures pin what this actually catches.
|
|
203
|
+
|
|
204
|
+
// Strip Python string literals so call-name detection ignores docstrings and
|
|
205
|
+
// string content. Triple quotes first, then single-line quotes; f-string
|
|
206
|
+
// prefixes fall out naturally because only the quoted part is replaced.
|
|
207
|
+
function stripPythonStrings(code) {
|
|
208
|
+
return String(code)
|
|
209
|
+
.replace(/[rbuf]{0,2}("""|''')[\s\S]*?\1/gi, '""')
|
|
210
|
+
.replace(/[rbuf]{0,2}"(?:\\.|[^"\\\n])*"/gi, '""')
|
|
211
|
+
.replace(/[rbuf]{0,2}'(?:\\.|[^'\\\n])*'/gi, "''");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function pythonShellCommands(code) {
|
|
215
|
+
const shell = [];
|
|
216
|
+
const lines = String(code).split("\n");
|
|
217
|
+
const first = (lines[0] || "").trim();
|
|
218
|
+
if (/^%%(bash|sh|script\s+(ba)?sh)\b/.test(first)) {
|
|
219
|
+
shell.push(lines.slice(1).join("\n"));
|
|
220
|
+
return { shell, rest: "" };
|
|
221
|
+
}
|
|
222
|
+
if (/^%%writefile\b/.test(first)) {
|
|
223
|
+
const target = first.replace(/^%%writefile\s+(-a\s+)?/, "").trim();
|
|
224
|
+
return { shell: [`tee ${target || "unknown-target"}`], rest: "" };
|
|
225
|
+
}
|
|
226
|
+
const rest = [];
|
|
227
|
+
for (const line of lines) {
|
|
228
|
+
const bang = line.match(/^\s*!(.+)$/);
|
|
229
|
+
const sysMagic = line.match(/^\s*%(?:system|sx)\s+(.+)$/);
|
|
230
|
+
if (bang) shell.push(bang[1]);
|
|
231
|
+
else if (sysMagic) shell.push(sysMagic[1]);
|
|
232
|
+
else rest.push(line);
|
|
233
|
+
}
|
|
234
|
+
return { shell, rest: rest.join("\n") };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const PY_MUTATING_CALLS =
|
|
238
|
+
/\b(?:os\.(?:remove|unlink|rename|replace|rmdir|removedirs|makedirs|mkdir|symlink|link|chmod|chown|truncate)|shutil\.(?:copy|copy2|copyfile|copytree|move|rmtree|make_archive|unpack_archive))\s*\(/;
|
|
239
|
+
const PY_WRITE_METHODS = /\.\s*(?:write_text|write_bytes|unlink|touch|mkdir|rmdir|hardlink_to|symlink_to)\s*\(/;
|
|
240
|
+
const PY_OPEN_WRITE =
|
|
241
|
+
/\bopen\s*\(\s*[^,]+,\s*(?:mode\s*=\s*)?['"](?:[wax]|r\+)[rwxab+]*['"]/;
|
|
242
|
+
const PY_OPEN_MODE_KWARG = /\bopen\s*\([^)]*mode\s*=\s*['"](?:[wax]|r\+)[rwxab+]*['"]/;
|
|
243
|
+
|
|
244
|
+
function pythonSubprocessArgv(code) {
|
|
245
|
+
const argvs = [];
|
|
246
|
+
const calls =
|
|
247
|
+
/\b(?:subprocess\.(?:run|call|check_call|check_output|Popen)|os\.system|os\.popen)\s*\(\s*(\[[^\]]*\]|[rbuf]{0,2}["'](?:\\.|[^"'\\\n])*["'])/g;
|
|
248
|
+
let match;
|
|
249
|
+
while ((match = calls.exec(String(code)))) {
|
|
250
|
+
const arg = match[1];
|
|
251
|
+
if (arg.startsWith("[")) {
|
|
252
|
+
const words = [...arg.matchAll(/["']((?:\\.|[^"'\\])*)["']/g)].map((m) => m[1]);
|
|
253
|
+
if (words.length) argvs.push(words.join(" "));
|
|
254
|
+
} else {
|
|
255
|
+
argvs.push(arg.replace(/^[rbuf]{0,2}["']/i, "").replace(/["']$/, ""));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return argvs;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Classify IPython cell code. Returns true when the code statically performs a
|
|
262
|
+
// repo mutation that should meet the protected-branch gate. Mutations that
|
|
263
|
+
// target only Egregore runtime state (memory/, dot-dirs, consent files) are
|
|
264
|
+
// exempt when the target is statically extractable, mirroring
|
|
265
|
+
// mutatesOnlyRuntimeState for bash.
|
|
266
|
+
export function isMutatingIpython(code, projectDir) {
|
|
267
|
+
if (!code) return false;
|
|
268
|
+
const { shell, rest } = pythonShellCommands(code);
|
|
269
|
+
for (const cmd of shell) {
|
|
270
|
+
if (isMutatingBash(cmd) && !(projectDir && mutatesOnlyRuntimeState(cmd, projectDir))) return true;
|
|
271
|
+
}
|
|
272
|
+
if (!rest) return false;
|
|
273
|
+
|
|
274
|
+
for (const argv of pythonSubprocessArgv(rest)) {
|
|
275
|
+
if (isMutatingBash(argv) && !(projectDir && mutatesOnlyRuntimeState(argv, projectDir))) return true;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const stripped = stripPythonStrings(rest);
|
|
279
|
+
if (PY_MUTATING_CALLS.test(stripped) || PY_WRITE_METHODS.test(stripped)) {
|
|
280
|
+
// Exempt when every extractable string target is runtime state. The
|
|
281
|
+
// mutated location differs by call: copy/move/rename mutate their LAST
|
|
282
|
+
// argument (the destination); remove/rmtree/mkdir mutate their first.
|
|
283
|
+
if (projectDir) {
|
|
284
|
+
const DEST_LAST = /^(?:copy|copy2|copyfile|copytree|move|rename|replace|symlink|link)$/;
|
|
285
|
+
const targets = [...String(rest).matchAll(
|
|
286
|
+
/\b(?:os|shutil)\.(\w+)\s*\(([^)]*)\)/g
|
|
287
|
+
)].flatMap((m) => {
|
|
288
|
+
const args = [...m[2].matchAll(/["']((?:\\.|[^"'\\])*)["']/g)].map((s) => s[1]);
|
|
289
|
+
if (!args.length) return [];
|
|
290
|
+
return [DEST_LAST.test(m[1]) ? args[args.length - 1] : args[0]];
|
|
291
|
+
});
|
|
292
|
+
const pathTargets = [...String(rest).matchAll(
|
|
293
|
+
/Path\s*\(\s*[rbuf]{0,2}["']((?:\\.|[^"'\\\n])*)["']\s*\)\s*\.\s*(?:write_text|write_bytes|unlink|touch|mkdir|rmdir)/g
|
|
294
|
+
)].map((m) => m[1]);
|
|
295
|
+
const all = [...targets, ...pathTargets];
|
|
296
|
+
if (all.length && all.every((t) => isRuntimeStatePath(t, projectDir))) return false;
|
|
297
|
+
}
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (PY_OPEN_WRITE.test(rest) || PY_OPEN_MODE_KWARG.test(rest)) {
|
|
302
|
+
if (projectDir) {
|
|
303
|
+
const openTargets = [...String(rest).matchAll(
|
|
304
|
+
/\bopen\s*\(\s*[rbuf]{0,2}["']((?:\\.|[^"'\\\n])*)["']\s*,\s*(?:mode\s*=\s*)?["'](?:[wax]|r\+)/g
|
|
305
|
+
)].map((m) => m[1]);
|
|
306
|
+
if (openTargets.length && openTargets.every((t) => isRuntimeStatePath(t, projectDir))) return false;
|
|
307
|
+
}
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
existsSync,
|
|
5
|
+
mkdirSync,
|
|
5
6
|
mkdtempSync,
|
|
6
7
|
readFileSync,
|
|
7
8
|
readdirSync,
|
|
@@ -141,12 +142,37 @@ export async function renderView(pi, root, request, signal) {
|
|
|
141
142
|
return { text, file, type: request.type };
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
export async function renderGreeting(pi, root, signal) {
|
|
145
|
-
const script = join(root, "bin",
|
|
146
|
-
if (!existsSync(script)) throw new Error("
|
|
145
|
+
export async function renderGreeting(pi, root, signal, startScript = "pi-session-start.sh") {
|
|
146
|
+
const script = join(root, "bin", startScript);
|
|
147
|
+
if (!existsSync(script)) throw new Error("Egregore startup renderer is missing");
|
|
147
148
|
return checkedExec(pi, "bash", [script, "--card"], { signal, timeout: 45000 }, "Egregore startup");
|
|
148
149
|
}
|
|
149
150
|
|
|
151
|
+
// Raw JSON from the most recent activity-data.sh run, so follow-up focus
|
|
152
|
+
// turns can receive the data inline instead of re-running the script inside
|
|
153
|
+
// a harness surface that may cap command time (Prime's kernel kills execs at
|
|
154
|
+
// ~5s, truncating the 10-20s data script to a partial fragment).
|
|
155
|
+
let lastActivityData = "";
|
|
156
|
+
export function lastActivityJson() {
|
|
157
|
+
return lastActivityData;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Stable on-disk copy of the same data. Focus turns point the model at this
|
|
161
|
+
// path instead of an inline excerpt: a file read is one fast kernel call,
|
|
162
|
+
// never truncated, and removes any reason to re-run the data script.
|
|
163
|
+
export function activityDataPath(root) {
|
|
164
|
+
return join(root, ".egregore", "activity-data.json");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function persistActivityData(root, json) {
|
|
168
|
+
try {
|
|
169
|
+
mkdirSync(join(root, ".egregore"), { recursive: true });
|
|
170
|
+
writeFileSync(activityDataPath(root), json);
|
|
171
|
+
} catch {
|
|
172
|
+
// Cache only — rendering proceeds from memory when the disk write fails.
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
150
176
|
export async function renderActivity(pi, root, signal, options = {}) {
|
|
151
177
|
if (options.sync !== false) {
|
|
152
178
|
await checkedExec(
|
|
@@ -177,6 +203,8 @@ export async function renderActivity(pi, root, signal, options = {}) {
|
|
|
177
203
|
{ signal, timeout: 120000 },
|
|
178
204
|
"Activity refresh",
|
|
179
205
|
);
|
|
206
|
+
lastActivityData = String(json || "");
|
|
207
|
+
persistActivityData(root, lastActivityData);
|
|
180
208
|
const temp = mkdtempSync(join(tmpdir(), "egregore-pi-activity-"));
|
|
181
209
|
const input = join(temp, "activity.json");
|
|
182
210
|
try {
|
|
@@ -276,7 +304,7 @@ export async function createHandoff(pi, root, params, signal) {
|
|
|
276
304
|
}
|
|
277
305
|
}
|
|
278
306
|
|
|
279
|
-
export async function captureSessionEnd(pi, root, sessionFile, signal) {
|
|
307
|
+
export async function captureSessionEnd(pi, root, sessionFile, signal, runtime = "pi") {
|
|
280
308
|
const sessionIdFile = join(root, ".egregore-session-id");
|
|
281
309
|
if (!existsSync(sessionIdFile)) return false;
|
|
282
310
|
const sessionId = cleanArgs(readFileSync(sessionIdFile, "utf8"));
|
|
@@ -287,7 +315,7 @@ export async function captureSessionEnd(pi, root, sessionFile, signal) {
|
|
|
287
315
|
writeFileSync(input, JSON.stringify({
|
|
288
316
|
session_id: sessionId,
|
|
289
317
|
transcript_path: cleanArgs(sessionFile),
|
|
290
|
-
runtime
|
|
318
|
+
runtime,
|
|
291
319
|
}));
|
|
292
320
|
|
|
293
321
|
try {
|
|
@@ -353,11 +381,11 @@ export async function publishScroll(pi, root, params, signal) {
|
|
|
353
381
|
};
|
|
354
382
|
}
|
|
355
383
|
|
|
356
|
-
export function workflowPrompt(name, args, extra = "") {
|
|
384
|
+
export function workflowPrompt(name, args, extra = "", spec = { runtimeName: "Pi", specPath: ".pi/APPEND_SYSTEM.md" }) {
|
|
357
385
|
const userArgs = cleanArgs(args);
|
|
358
386
|
return [
|
|
359
387
|
`Run the Egregore /${name} workflow now.`,
|
|
360
|
-
`Read .codex/skills/${name}/SKILL.md completely and adapt Codex-specific wording to
|
|
388
|
+
`Read .codex/skills/${name}/SKILL.md completely and adapt Codex-specific wording to ${spec.runtimeName} using ${spec.specPath}.`,
|
|
361
389
|
"Work quietly: do not narrate progress, expose raw command output, or repeat tool results.",
|
|
362
390
|
"Use the fewest targeted reads and commands that preserve the workflow contract.",
|
|
363
391
|
userArgs ? `User arguments: ${userArgs}` : "",
|