create-egregore 0.19.5 → 0.19.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/egregore-launcher.js +407 -25
- package/bin/cli.js +34 -20
- package/lib/local.js +21 -5
- package/lib/pi-runtime.js +186 -0
- package/lib/setup.js +56 -11
- package/lib/ui.js +1 -1
- package/package.json +7 -5
- package/runtime/codex/.codex/spec-manifest.json +1 -1
- package/runtime/codex/AGENTS.md +7 -2
- package/runtime/codex/bin/codex-render-spec.mjs +6 -1
- package/runtime/codex/bin/codex-session-start.sh +5 -59
- package/runtime/codex/bin/codex-skill-render.mjs +7 -5
- package/runtime/codex/bin/lib/context.sh +8 -1
- package/runtime/codex/bin/lib/greeting.sh +12 -108
- package/runtime/codex/bin/lib/metrics.sh +374 -0
- package/runtime/codex/bin/publish-artifact.sh +14 -0
- package/runtime/codex/bin/settings.sh +274 -0
- package/runtime/codex/manifest.json +2 -0
- package/runtime/pi/.codex/config.toml +15 -0
- package/runtime/pi/.codex/hooks/branch-guard.js +242 -0
- package/runtime/pi/.codex/hooks/search-hint.js +65 -0
- package/runtime/pi/.codex/hooks.json +30 -0
- package/runtime/pi/.codex/skills/activity/SKILL.md +70 -0
- package/runtime/pi/.codex/skills/add/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/announce/SKILL.md +54 -0
- package/runtime/pi/.codex/skills/answer/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/archive/SKILL.md +41 -0
- package/runtime/pi/.codex/skills/ask/SKILL.md +59 -0
- package/runtime/pi/.codex/skills/branch/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/checkup/SKILL.md +41 -0
- package/runtime/pi/.codex/skills/commit/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/contribute/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/dark-mode/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/dashboard/SKILL.md +59 -0
- package/runtime/pi/.codex/skills/deep-reflect/SKILL.md +74 -0
- package/runtime/pi/.codex/skills/delete-user/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/egregore-voice/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/emissary/SKILL.md +41 -0
- package/runtime/pi/.codex/skills/env/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/handoff/SKILL.md +67 -0
- package/runtime/pi/.codex/skills/harvest/SKILL.md +107 -0
- package/runtime/pi/.codex/skills/ingest/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/ingest-google/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/ingest-user-interview/SKILL.md +41 -0
- package/runtime/pi/.codex/skills/invite/SKILL.md +40 -0
- package/runtime/pi/.codex/skills/issue/SKILL.md +41 -0
- package/runtime/pi/.codex/skills/me/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/meeting/SKILL.md +41 -0
- package/runtime/pi/.codex/skills/minion/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/note/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/onboarding/SKILL.md +41 -0
- package/runtime/pi/.codex/skills/pr/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/project/SKILL.md +41 -0
- package/runtime/pi/.codex/skills/pull/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/push/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/quest/SKILL.md +77 -0
- package/runtime/pi/.codex/skills/quest-suggest/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/reflect/SKILL.md +41 -0
- package/runtime/pi/.codex/skills/review-pr/SKILL.md +41 -0
- package/runtime/pi/.codex/skills/save/SKILL.md +62 -0
- package/runtime/pi/.codex/skills/scroll/SKILL.md +168 -0
- package/runtime/pi/.codex/skills/scroll/agents/openai.yaml +4 -0
- package/runtime/pi/.codex/skills/scroll/assets/shell.html +542 -0
- package/runtime/pi/.codex/skills/search/SKILL.md +56 -0
- package/runtime/pi/.codex/skills/session-view/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/setup/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/summon/SKILL.md +41 -0
- package/runtime/pi/.codex/skills/sync-repos/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/telegram-connect/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/telemetry/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/test/SKILL.md +41 -0
- package/runtime/pi/.codex/skills/the-spiral/SKILL.md +84 -0
- package/runtime/pi/.codex/skills/thread/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/todo/SKILL.md +41 -0
- package/runtime/pi/.codex/skills/tui-design/SKILL.md +41 -0
- package/runtime/pi/.codex/skills/tutorial/SKILL.md +41 -0
- package/runtime/pi/.codex/skills/update/SKILL.md +25 -0
- package/runtime/pi/.codex/skills/view/SKILL.md +156 -0
- package/runtime/pi/.codex/skills/view/agents/openai.yaml +4 -0
- package/runtime/pi/.codex/skills/view/assets/compose-scaffold.html +221 -0
- package/runtime/pi/.codex/skills/visual-explain/SKILL.md +41 -0
- package/runtime/pi/.codex/skills/wrap/SKILL.md +55 -0
- package/runtime/pi/.codex/spec-manifest.json +114 -0
- package/runtime/pi/.pi/APPEND_SYSTEM.md +255 -0
- package/runtime/pi/.pi/extensions/egregore.ts +324 -0
- package/runtime/pi/.pi/settings.json +6 -0
- package/runtime/pi/.pi/spec-manifest.json +12 -0
- package/runtime/pi/AGENTS.md +350 -0
- package/runtime/pi/bin/activity-data.sh +242 -0
- package/runtime/pi/bin/agent.sh +677 -0
- package/runtime/pi/bin/codex-install-skills.sh +78 -0
- package/runtime/pi/bin/codex-render-spec.mjs +394 -0
- package/runtime/pi/bin/codex-session-start.sh +188 -0
- package/runtime/pi/bin/codex-skill-render.mjs +323 -0
- package/runtime/pi/bin/codex-sync-skills.sh +227 -0
- package/runtime/pi/bin/dashboard-data.sh +178 -0
- package/runtime/pi/bin/graph-batch.sh +60 -0
- package/runtime/pi/bin/graph-op.sh +431 -0
- package/runtime/pi/bin/graph-wal.sh +228 -0
- package/runtime/pi/bin/graph.sh +177 -0
- package/runtime/pi/bin/handoff-pr-backfill.sh +86 -0
- package/runtime/pi/bin/handoff-run.sh +581 -0
- package/runtime/pi/bin/handoff-save-egregore.sh +136 -0
- package/runtime/pi/bin/index-handoff.sh +312 -0
- package/runtime/pi/bin/lib/artifact-id.sh +46 -0
- package/runtime/pi/bin/lib/config.sh +71 -0
- package/runtime/pi/bin/lib/context.sh +542 -0
- package/runtime/pi/bin/lib/dashboard-artifact.sh +142 -0
- package/runtime/pi/bin/lib/ensure-develop.sh +183 -0
- package/runtime/pi/bin/lib/git-sync.sh +288 -0
- package/runtime/pi/bin/lib/greeting.sh +340 -0
- package/runtime/pi/bin/lib/hash.sh +13 -0
- package/runtime/pi/bin/lib/identity.sh +174 -0
- package/runtime/pi/bin/lib/metrics.sh +374 -0
- package/runtime/pi/bin/lib/permissions.sh +106 -0
- package/runtime/pi/bin/lib/time.sh +56 -0
- package/runtime/pi/bin/lib/worktree-links.sh +54 -0
- package/runtime/pi/bin/notify.sh +207 -0
- package/runtime/pi/bin/pi-branch-policy.mjs +15 -0
- package/runtime/pi/bin/pi-product-workflows.mjs +324 -0
- package/runtime/pi/bin/pi-render-spec.mjs +114 -0
- package/runtime/pi/bin/pi-session-start.sh +24 -0
- package/runtime/pi/bin/publish-artifact.sh +201 -0
- package/runtime/pi/bin/publish-references.sh +144 -0
- package/runtime/pi/bin/repo-state.sh +99 -0
- package/runtime/pi/bin/telemetry.sh +391 -0
- package/runtime/pi/bin/worktree-create.sh +118 -0
- package/runtime/pi/bin/worktree-remove.sh +50 -0
- package/runtime/pi/bin/worktree.sh +198 -0
- package/runtime/pi/manifest.json +176 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
5
|
+
CONFIG="$SCRIPT_DIR/egregore.json"
|
|
6
|
+
|
|
7
|
+
if [ ! -f "$CONFIG" ]; then
|
|
8
|
+
echo "Error: egregore.json not found." >&2
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
# --- Mode detection ---
|
|
13
|
+
_MODE=$(jq -r '.mode // "connected"' "$CONFIG" 2>/dev/null)
|
|
14
|
+
|
|
15
|
+
# Public relay for local mode group messages.
|
|
16
|
+
# No API key needed — rate-limited, group messages only.
|
|
17
|
+
RELAY_URL="https://egregore-production-55f2.up.railway.app"
|
|
18
|
+
|
|
19
|
+
# Load specific variables from .env if it exists (safe extraction, no arbitrary code execution)
|
|
20
|
+
if [ -f "$SCRIPT_DIR/.env" ]; then
|
|
21
|
+
EGREGORE_API_URL="${EGREGORE_API_URL:-$(grep '^EGREGORE_API_URL=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)}"
|
|
22
|
+
EGREGORE_API_KEY="${EGREGORE_API_KEY:-$(grep '^EGREGORE_API_KEY=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)}"
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# Check if API mode or direct mode
|
|
26
|
+
# api_url comes from egregore.json (committed, non-secret)
|
|
27
|
+
# api_key comes from .env only (EGREGORE_API_KEY) — never from egregore.json
|
|
28
|
+
API_URL="${EGREGORE_API_URL:-$(jq -r '.api_url // empty' "$CONFIG")}"
|
|
29
|
+
API_KEY="${EGREGORE_API_KEY:-}"
|
|
30
|
+
|
|
31
|
+
if [ -n "$API_URL" ] && [ -n "$API_KEY" ]; then
|
|
32
|
+
# === API MODE: Call Egregore API gateway ===
|
|
33
|
+
|
|
34
|
+
send_to_person() {
|
|
35
|
+
local name="$1"
|
|
36
|
+
local message="$2"
|
|
37
|
+
|
|
38
|
+
local response
|
|
39
|
+
response=$(curl -s -X POST "${API_URL}/api/notify/send" \
|
|
40
|
+
-H "Authorization: Bearer $API_KEY" \
|
|
41
|
+
-H "Content-Type: application/json" \
|
|
42
|
+
-d "$(jq -n --arg to "$name" --arg message "$message" \
|
|
43
|
+
'{to: $to, message: $message}')" \
|
|
44
|
+
--max-time 10)
|
|
45
|
+
|
|
46
|
+
if echo "$response" | jq -e '.status == "sent"' >/dev/null 2>&1; then
|
|
47
|
+
echo "Sent"
|
|
48
|
+
bash "$SCRIPT_DIR/bin/telemetry.sh" emit "notification" \
|
|
49
|
+
'{"type":"send","status":"sent"}' 2>/dev/null &
|
|
50
|
+
else
|
|
51
|
+
local detail
|
|
52
|
+
detail=$(echo "$response" | jq -r '.detail // .status // "unknown error"')
|
|
53
|
+
if echo "$detail" | grep -qi "forbidden\|can't initiate\|bot was blocked"; then
|
|
54
|
+
echo "Failed: DM not available. Tell $name to message the Egregore bot first (/start)."
|
|
55
|
+
else
|
|
56
|
+
echo "Failed: $detail"
|
|
57
|
+
fi
|
|
58
|
+
bash "$SCRIPT_DIR/bin/telemetry.sh" emit "notification" \
|
|
59
|
+
'{"type":"send","status":"failed"}' 2>/dev/null &
|
|
60
|
+
fi
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
send_to_group() {
|
|
64
|
+
local message="$1"
|
|
65
|
+
|
|
66
|
+
local response
|
|
67
|
+
response=$(curl -s -X POST "${API_URL}/api/notify/group" \
|
|
68
|
+
-H "Authorization: Bearer $API_KEY" \
|
|
69
|
+
-H "Content-Type: application/json" \
|
|
70
|
+
-d "$(jq -n --arg message "$message" '{message: $message}')" \
|
|
71
|
+
--max-time 10)
|
|
72
|
+
|
|
73
|
+
if echo "$response" | jq -e '.status == "sent"' >/dev/null 2>&1; then
|
|
74
|
+
echo "Sent"
|
|
75
|
+
bash "$SCRIPT_DIR/bin/telemetry.sh" emit "notification" \
|
|
76
|
+
'{"type":"group","status":"sent"}' 2>/dev/null &
|
|
77
|
+
else
|
|
78
|
+
local detail
|
|
79
|
+
detail=$(echo "$response" | jq -r '.detail // .status // "unknown error"')
|
|
80
|
+
echo "Failed: $detail"
|
|
81
|
+
bash "$SCRIPT_DIR/bin/telemetry.sh" emit "notification" \
|
|
82
|
+
'{"type":"group","status":"failed"}' 2>/dev/null &
|
|
83
|
+
fi
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
test_connection() {
|
|
87
|
+
local response
|
|
88
|
+
response=$(curl -s -X GET "${API_URL}/api/notify/test" \
|
|
89
|
+
-H "Authorization: Bearer $API_KEY" \
|
|
90
|
+
--max-time 10)
|
|
91
|
+
|
|
92
|
+
if echo "$response" | jq -e '.status == "ok"' >/dev/null 2>&1; then
|
|
93
|
+
echo "Telegram connected via Egregore API"
|
|
94
|
+
else
|
|
95
|
+
echo "Failed to connect" >&2
|
|
96
|
+
echo "$response" >&2
|
|
97
|
+
exit 1
|
|
98
|
+
fi
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
elif [ "$_MODE" = "local" ]; then
|
|
102
|
+
# === LOCAL MODE: Group messages via public relay, DMs not available ===
|
|
103
|
+
# Telegram config: prefer .egregore-state.json (symlinked across worktrees),
|
|
104
|
+
# fall back to egregore.json for backward compatibility.
|
|
105
|
+
STATE_FILE="$SCRIPT_DIR/.egregore-state.json"
|
|
106
|
+
|
|
107
|
+
_read_telegram_config() {
|
|
108
|
+
local field="$1"
|
|
109
|
+
local val
|
|
110
|
+
val=$(jq -r ".$field // empty" "$STATE_FILE" 2>/dev/null)
|
|
111
|
+
[ -z "$val" ] && val=$(jq -r ".$field // empty" "$CONFIG" 2>/dev/null)
|
|
112
|
+
echo "$val"
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
send_to_person() {
|
|
116
|
+
echo "DMs require managed hosting. Group message sent instead."
|
|
117
|
+
send_to_group "@${1}: ${2}"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
send_to_group() {
|
|
121
|
+
local message="$1"
|
|
122
|
+
local chat_id
|
|
123
|
+
chat_id=$(_read_telegram_config telegram_chat_id)
|
|
124
|
+
local group_link
|
|
125
|
+
group_link=$(_read_telegram_config telegram_group_link)
|
|
126
|
+
|
|
127
|
+
if [ -z "$chat_id" ] && [ -z "$group_link" ]; then
|
|
128
|
+
echo '{"status":"offline","reason":"no_telegram_group"}'
|
|
129
|
+
return
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
local slug
|
|
133
|
+
slug=$(jq -r '.slug // empty' "$CONFIG" 2>/dev/null)
|
|
134
|
+
local org_name
|
|
135
|
+
org_name=$(jq -r '.org_name // empty' "$CONFIG" 2>/dev/null)
|
|
136
|
+
|
|
137
|
+
local response
|
|
138
|
+
response=$(curl -s -X POST "${RELAY_URL}/api/notify/relay" \
|
|
139
|
+
-H "Content-Type: application/json" \
|
|
140
|
+
-d "$(jq -n \
|
|
141
|
+
--arg chat_id "$chat_id" \
|
|
142
|
+
--arg group_link "$group_link" \
|
|
143
|
+
--arg message "$message" \
|
|
144
|
+
--arg slug "$slug" \
|
|
145
|
+
--arg org_name "$org_name" \
|
|
146
|
+
'{chat_id: $chat_id, group_link: $group_link, message: $message, slug: $slug, org_name: $org_name}')" \
|
|
147
|
+
--max-time 10 2>/dev/null)
|
|
148
|
+
|
|
149
|
+
if echo "$response" | jq -e '.status == "sent"' >/dev/null 2>&1; then
|
|
150
|
+
echo "Sent"
|
|
151
|
+
else
|
|
152
|
+
local detail
|
|
153
|
+
detail=$(echo "$response" | jq -r '.detail // .status // "relay unavailable"' 2>/dev/null)
|
|
154
|
+
echo "Failed: $detail"
|
|
155
|
+
fi
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
test_connection() {
|
|
159
|
+
local group_link
|
|
160
|
+
group_link=$(_read_telegram_config telegram_group_link)
|
|
161
|
+
if [ -n "$group_link" ]; then
|
|
162
|
+
echo "Telegram group configured (local mode — group messages via relay)"
|
|
163
|
+
else
|
|
164
|
+
echo "No Telegram group configured. Run /telegram-connect."
|
|
165
|
+
fi
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
else
|
|
169
|
+
# === OFFLINE MODE: No API key, not local — skip notifications ===
|
|
170
|
+
send_to_person() { echo '{"status":"offline","reason":"no_api_key"}'; }
|
|
171
|
+
send_to_group() { echo '{"status":"offline","reason":"no_api_key"}'; }
|
|
172
|
+
test_connection() { echo '{"status":"offline","reason":"no_api_key"}'; }
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
case "${1:-help}" in
|
|
176
|
+
send)
|
|
177
|
+
recipient="${2:?Usage: notify.sh send <name> <message>}"
|
|
178
|
+
message="${3:?Usage: notify.sh send <name> <message>}"
|
|
179
|
+
result=$(send_to_person "$recipient" "$message")
|
|
180
|
+
if [[ "$result" == Failed* ]]; then
|
|
181
|
+
send_to_group "@${recipient}: ${message}"
|
|
182
|
+
echo "DM failed, sent to group instead. ${result}"
|
|
183
|
+
else
|
|
184
|
+
echo "$result"
|
|
185
|
+
fi
|
|
186
|
+
;;
|
|
187
|
+
group)
|
|
188
|
+
message="${2:?Usage: notify.sh group <message>}"
|
|
189
|
+
send_to_group "$message"
|
|
190
|
+
;;
|
|
191
|
+
file)
|
|
192
|
+
echo "File upload not yet supported via API. Use direct mode." >&2
|
|
193
|
+
exit 1
|
|
194
|
+
;;
|
|
195
|
+
test)
|
|
196
|
+
test_connection
|
|
197
|
+
;;
|
|
198
|
+
help|*)
|
|
199
|
+
echo "Usage: notify.sh <command>"
|
|
200
|
+
echo ""
|
|
201
|
+
echo "Commands:"
|
|
202
|
+
echo " send <name> <message> Send DM to a person (auto-fallback to group on failure)"
|
|
203
|
+
echo " group <message> Send to the group chat"
|
|
204
|
+
echo " file <path> [caption] Send a file to the group chat"
|
|
205
|
+
echo " test Test connection"
|
|
206
|
+
;;
|
|
207
|
+
esac
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const MUTATING_COMMANDS = /(^|[;&|]\s*)(rm|mv|cp|mkdir|rmdir|touch|chmod|chown|install)\b/;
|
|
2
|
+
const MUTATING_GIT = /(^|[;&|]\s*)git\s+(add|commit|push|merge|rebase|cherry-pick|tag|rm|mv)\b/;
|
|
3
|
+
const MUTATING_PACKAGES = /(^|[;&|]\s*)(npm|pnpm|yarn|bun)\s+(install|add|remove|uninstall|update)\b/;
|
|
4
|
+
|
|
5
|
+
export function isMutatingBash(command) {
|
|
6
|
+
if (!command) return false;
|
|
7
|
+
const meaningfulRedirects = String(command)
|
|
8
|
+
.replace(/(?:^|\s)\d*>>?\s*\/dev\/null\b/g, " ")
|
|
9
|
+
.replace(/(?:^|\s)\d*>\s*&\d+\b/g, " ");
|
|
10
|
+
return MUTATING_COMMANDS.test(command) ||
|
|
11
|
+
MUTATING_GIT.test(command) ||
|
|
12
|
+
MUTATING_PACKAGES.test(command) ||
|
|
13
|
+
/(^|[^<])>>?\s*[^&]/.test(meaningfulRedirects) ||
|
|
14
|
+
/\bsed\s+[^\n]*\s-i(?:\s|$)/.test(command);
|
|
15
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { basename, join, relative, resolve } from "node:path";
|
|
13
|
+
|
|
14
|
+
const ANSI = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
15
|
+
|
|
16
|
+
function resultCode(result) {
|
|
17
|
+
return Number(result?.code ?? result?.status ?? 0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function commandFailure(label, result) {
|
|
21
|
+
const detail = String(result?.stderr || result?.stdout || "").replace(ANSI, "").trim();
|
|
22
|
+
return new Error(detail ? `${label}: ${detail}` : `${label} failed`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function checkedExec(pi, command, args, options = {}, label = command) {
|
|
26
|
+
const result = await pi.exec(command, args, options);
|
|
27
|
+
if (resultCode(result) !== 0) throw commandFailure(label, result);
|
|
28
|
+
return String(result.stdout || "");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readJson(file, fallback = {}) {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
34
|
+
} catch {
|
|
35
|
+
return fallback;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function cleanArgs(value) {
|
|
40
|
+
return String(value || "").trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function walkMarkdown(dir, files = []) {
|
|
44
|
+
if (!existsSync(dir)) return files;
|
|
45
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
46
|
+
const full = join(dir, entry.name);
|
|
47
|
+
if (entry.isDirectory()) walkMarkdown(full, files);
|
|
48
|
+
else if (entry.isFile() && entry.name.endsWith(".md")) files.push(full);
|
|
49
|
+
}
|
|
50
|
+
return files;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function inferArtifactType(file) {
|
|
54
|
+
const normalized = file.replaceAll("\\", "/");
|
|
55
|
+
if (normalized.includes("/memory/quests/")) return "quest";
|
|
56
|
+
if (normalized.includes("/memory/handoffs/")) return "handoff";
|
|
57
|
+
return "document";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function matchingFiles(root, type, query) {
|
|
61
|
+
const base = type === "quest"
|
|
62
|
+
? join(root, "memory", "quests")
|
|
63
|
+
: type === "handoff"
|
|
64
|
+
? join(root, "memory", "handoffs")
|
|
65
|
+
: join(root, "memory");
|
|
66
|
+
const needle = query.toLowerCase().replace(/\.md$/, "");
|
|
67
|
+
return walkMarkdown(base)
|
|
68
|
+
.filter((file) => basename(file, ".md").toLowerCase().includes(needle))
|
|
69
|
+
.sort((a, b) => b.localeCompare(a));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function parseViewRequest(root, rawArgs) {
|
|
73
|
+
const raw = cleanArgs(rawArgs);
|
|
74
|
+
if (!raw) return { kind: "choose" };
|
|
75
|
+
|
|
76
|
+
const fast = /(?:^|\s)--(?:fast|floor)(?:\s|$)/.test(raw);
|
|
77
|
+
const compose = /(?:^|\s)--compose(?:\s|$)/.test(raw);
|
|
78
|
+
const cleaned = raw.replace(/(?:^|\s)--(?:fast|floor|compose)(?=\s|$)/g, " ").trim();
|
|
79
|
+
const surface = cleaned.toLowerCase();
|
|
80
|
+
if (["activity", "board", "network"].includes(surface)) {
|
|
81
|
+
return { kind: "native", type: surface, source: "", fast: true };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const explicit = cleaned.match(/^(quest|handoff|document)\s+(.+)$/i);
|
|
85
|
+
const type = explicit?.[1]?.toLowerCase();
|
|
86
|
+
const target = (explicit?.[2] || cleaned).replace(/^@/, "").trim();
|
|
87
|
+
const direct = resolve(root, target);
|
|
88
|
+
if (existsSync(direct)) {
|
|
89
|
+
const inferred = type || inferArtifactType(direct);
|
|
90
|
+
if (inferred === "document" && (!fast || compose)) {
|
|
91
|
+
return { kind: "model", reason: "document-composition", raw };
|
|
92
|
+
}
|
|
93
|
+
return { kind: "native", type: inferred, source: direct, fast };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (type) {
|
|
97
|
+
const matches = matchingFiles(root, type, target);
|
|
98
|
+
if (matches.length === 1 && (type !== "document" || fast)) {
|
|
99
|
+
return { kind: "native", type, source: matches[0], fast };
|
|
100
|
+
}
|
|
101
|
+
if (matches.length > 1) return { kind: "choose-file", type, matches, fast };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { kind: "model", reason: "resolve-or-synthesize", raw };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function rendererCommand(root) {
|
|
108
|
+
const local = join(root, "packages", "egregore-artifacts", "bin", "cli.js");
|
|
109
|
+
const react = join(root, "packages", "egregore-artifacts", "node_modules", "react");
|
|
110
|
+
if (existsSync(local) && existsSync(react)) return { command: "node", prefix: [local] };
|
|
111
|
+
return { command: "npx", prefix: ["-y", "egregore-artifacts@latest"] };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function artifactPath(output) {
|
|
115
|
+
const clean = String(output || "").replace(ANSI, "");
|
|
116
|
+
const matches = clean.match(/(?:\/[^\s'\"]+|[A-Za-z]:\\[^\r\n'\"]+)\.html/g);
|
|
117
|
+
return matches?.at(-1) || "";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function renderView(pi, root, request, signal) {
|
|
121
|
+
const renderer = rendererCommand(root);
|
|
122
|
+
const args = [...renderer.prefix, request.type];
|
|
123
|
+
if (request.source) args.push(request.source);
|
|
124
|
+
const output = await checkedExec(
|
|
125
|
+
pi,
|
|
126
|
+
renderer.command,
|
|
127
|
+
args,
|
|
128
|
+
{ signal, timeout: 120000 },
|
|
129
|
+
"Artifact render",
|
|
130
|
+
);
|
|
131
|
+
const file = artifactPath(output);
|
|
132
|
+
let text = "✓ Artifact opened in browser";
|
|
133
|
+
if (file) text += `\n File: ${file}`;
|
|
134
|
+
|
|
135
|
+
if (request.type === "board") {
|
|
136
|
+
const config = readJson(join(root, "egregore.json"));
|
|
137
|
+
if (config.slug && config.mode !== "local" && config.api_url) {
|
|
138
|
+
text += `\n◆ https://egregore.xyz/view/${config.slug}/board (stable — refresh for latest)`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return { text, file, type: request.type };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function renderGreeting(pi, root, signal) {
|
|
145
|
+
const script = join(root, "bin", "pi-session-start.sh");
|
|
146
|
+
if (!existsSync(script)) throw new Error("Pi startup renderer is missing");
|
|
147
|
+
return checkedExec(pi, "bash", [script, "--card"], { signal, timeout: 45000 }, "Egregore startup");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function renderActivity(pi, root, signal, options = {}) {
|
|
151
|
+
if (options.sync !== false) {
|
|
152
|
+
await checkedExec(
|
|
153
|
+
pi,
|
|
154
|
+
"bash",
|
|
155
|
+
[join(root, "bin", "agent.sh"), "sync"],
|
|
156
|
+
{ signal, timeout: 120000 },
|
|
157
|
+
"Memory sync",
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const state = readJson(join(root, ".egregore-state.json"));
|
|
162
|
+
let handle = state.github_username || state.name || "";
|
|
163
|
+
if (!handle) {
|
|
164
|
+
handle = (await checkedExec(
|
|
165
|
+
pi,
|
|
166
|
+
"git",
|
|
167
|
+
["-C", root, "config", "user.name"],
|
|
168
|
+
{ signal, timeout: 5000 },
|
|
169
|
+
"Identity lookup",
|
|
170
|
+
)).trim();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const json = await checkedExec(
|
|
174
|
+
pi,
|
|
175
|
+
"bash",
|
|
176
|
+
[join(root, "bin", "activity-data.sh"), handle],
|
|
177
|
+
{ signal, timeout: 120000 },
|
|
178
|
+
"Activity refresh",
|
|
179
|
+
);
|
|
180
|
+
const temp = mkdtempSync(join(tmpdir(), "egregore-pi-activity-"));
|
|
181
|
+
const input = join(temp, "activity.json");
|
|
182
|
+
try {
|
|
183
|
+
writeFileSync(input, json);
|
|
184
|
+
return await checkedExec(
|
|
185
|
+
pi,
|
|
186
|
+
"node",
|
|
187
|
+
[join(root, "bin", "codex-skill-render.mjs"), "activity-card", input],
|
|
188
|
+
{ signal, timeout: 10000 },
|
|
189
|
+
"Activity render",
|
|
190
|
+
);
|
|
191
|
+
} finally {
|
|
192
|
+
rmSync(temp, { recursive: true, force: true });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function section(title, body) {
|
|
197
|
+
const value = cleanArgs(body) || "- None recorded.";
|
|
198
|
+
return `## ${title}\n\n${value}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function buildHandoffDocument(params, author, date = new Date().toISOString().slice(0, 10)) {
|
|
202
|
+
const lines = [
|
|
203
|
+
`# Handoff: ${cleanArgs(params.topic)}`,
|
|
204
|
+
"",
|
|
205
|
+
`**Date**: ${date}`,
|
|
206
|
+
`**Author**: ${author}`,
|
|
207
|
+
];
|
|
208
|
+
if (cleanArgs(params.recipient)) lines.push(`**To**: ${cleanArgs(params.recipient)}`);
|
|
209
|
+
lines.push(
|
|
210
|
+
"",
|
|
211
|
+
section("Briefing", params.briefing),
|
|
212
|
+
"",
|
|
213
|
+
section("Key Decisions", params.key_decisions),
|
|
214
|
+
"",
|
|
215
|
+
section("Current State", params.current_state),
|
|
216
|
+
"",
|
|
217
|
+
section("Open Threads", params.open_threads),
|
|
218
|
+
"",
|
|
219
|
+
section("Next Steps", params.next_steps),
|
|
220
|
+
"",
|
|
221
|
+
section("Entry Points", params.entry_points),
|
|
222
|
+
"",
|
|
223
|
+
);
|
|
224
|
+
return lines.join("\n");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function createHandoff(pi, root, params, signal) {
|
|
228
|
+
const state = readJson(join(root, ".egregore-state.json"));
|
|
229
|
+
const author = state.github_username || state.name || "egregore";
|
|
230
|
+
const temp = mkdtempSync(join(tmpdir(), "egregore-pi-handoff-"));
|
|
231
|
+
const body = join(temp, "handoff.md");
|
|
232
|
+
const recipient = cleanArgs(params.recipient);
|
|
233
|
+
const runnerArgs = ["--author", author, "--topic", cleanArgs(params.topic)];
|
|
234
|
+
if (recipient) runnerArgs.push("--recipient", recipient);
|
|
235
|
+
writeFileSync(body, buildHandoffDocument(params, author));
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const shell = 'tmp="$1"; body="$2"; shift 2; TMPDIR="$tmp" bash bin/handoff-run.sh "$@" < "$body"';
|
|
239
|
+
await checkedExec(
|
|
240
|
+
pi,
|
|
241
|
+
"bash",
|
|
242
|
+
["-c", shell, "egregore-pi-handoff", temp, body, ...runnerArgs],
|
|
243
|
+
{ signal, timeout: 180000 },
|
|
244
|
+
"Handoff",
|
|
245
|
+
);
|
|
246
|
+
const resultFile = join(temp, "handoff-run-result.json");
|
|
247
|
+
if (!existsSync(resultFile)) throw new Error("Handoff completed without a result receipt");
|
|
248
|
+
const result = readJson(resultFile);
|
|
249
|
+
const card = await checkedExec(
|
|
250
|
+
pi,
|
|
251
|
+
"node",
|
|
252
|
+
[join(root, "bin", "codex-skill-render.mjs"), "handoff-card", resultFile],
|
|
253
|
+
{ signal, timeout: 10000 },
|
|
254
|
+
"Handoff receipt",
|
|
255
|
+
);
|
|
256
|
+
return {
|
|
257
|
+
text: card.trim(),
|
|
258
|
+
path: result.file ? `memory/${result.file}` : "",
|
|
259
|
+
url: result.artifactUrl || "",
|
|
260
|
+
};
|
|
261
|
+
} finally {
|
|
262
|
+
rmSync(temp, { recursive: true, force: true });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export async function publishScroll(pi, root, params, signal) {
|
|
267
|
+
const source = resolve(root, String(params.source || "").replace(/^@/, ""));
|
|
268
|
+
if (!existsSync(source)) throw new Error(`Scroll source not found: ${relative(root, source)}`);
|
|
269
|
+
const html = readFileSync(source, "utf8");
|
|
270
|
+
if (/__[A-Z][A-Z0-9_]*__/.test(html)) throw new Error("Scroll still contains unresolved placeholders");
|
|
271
|
+
|
|
272
|
+
const config = readJson(join(root, "egregore.json"));
|
|
273
|
+
let url = "";
|
|
274
|
+
if (config.mode !== "local" && config.api_url) {
|
|
275
|
+
const output = await checkedExec(
|
|
276
|
+
pi,
|
|
277
|
+
"bash",
|
|
278
|
+
[
|
|
279
|
+
join(root, "bin", "publish-artifact.sh"),
|
|
280
|
+
"document",
|
|
281
|
+
source,
|
|
282
|
+
"--raw-html",
|
|
283
|
+
"--id",
|
|
284
|
+
cleanArgs(params.slug),
|
|
285
|
+
"--title",
|
|
286
|
+
cleanArgs(params.title),
|
|
287
|
+
"--author",
|
|
288
|
+
cleanArgs(params.author),
|
|
289
|
+
"--description",
|
|
290
|
+
cleanArgs(params.description),
|
|
291
|
+
],
|
|
292
|
+
{ signal, timeout: 180000 },
|
|
293
|
+
"Scroll publish",
|
|
294
|
+
);
|
|
295
|
+
url = String(output).match(/https:\/\/[^\s]+/)?.[0] || "";
|
|
296
|
+
if (!url) throw new Error("Scroll publish did not return a stable URL");
|
|
297
|
+
const opener = process.platform === "darwin" ? "open" : "xdg-open";
|
|
298
|
+
await checkedExec(pi, opener, [url], { signal, timeout: 10000 }, "Open scroll");
|
|
299
|
+
} else {
|
|
300
|
+
const opener = process.platform === "darwin" ? "open" : "xdg-open";
|
|
301
|
+
await checkedExec(pi, opener, [source], { signal, timeout: 10000 }, "Open scroll");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const version = cleanArgs(params.version) || "v1";
|
|
305
|
+
const destination = url || source;
|
|
306
|
+
return {
|
|
307
|
+
text: `✓ Scroll ${version} opened\n ${url ? "URL" : "File"}: ${destination}`,
|
|
308
|
+
path: source,
|
|
309
|
+
url,
|
|
310
|
+
version,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function workflowPrompt(name, args, extra = "") {
|
|
315
|
+
const userArgs = cleanArgs(args);
|
|
316
|
+
return [
|
|
317
|
+
`Run the Egregore /${name} workflow now.`,
|
|
318
|
+
`Read .codex/skills/${name}/SKILL.md completely and adapt Codex-specific wording to Pi using .pi/APPEND_SYSTEM.md.`,
|
|
319
|
+
"Work quietly: do not narrate progress, expose raw command output, or repeat tool results.",
|
|
320
|
+
"Use the fewest targeted reads and commands that preserve the workflow contract.",
|
|
321
|
+
userArgs ? `User arguments: ${userArgs}` : "",
|
|
322
|
+
extra,
|
|
323
|
+
].filter(Boolean).join("\n");
|
|
324
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Derive Pi's project-local system appendix from the generated Codex protocol.
|
|
3
|
+
// CLAUDE.md remains canonical: CLAUDE.md -> AGENTS.md -> .pi/APPEND_SYSTEM.md.
|
|
4
|
+
// The second hop deliberately reuses the reviewed shell-runtime translation,
|
|
5
|
+
// then adapts only the harness surfaces that differ between Codex and Pi.
|
|
6
|
+
|
|
7
|
+
import crypto from "node:crypto";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
|
+
const SOURCE = path.join(ROOT, "AGENTS.md");
|
|
14
|
+
const OUTPUT = path.join(ROOT, ".pi", "APPEND_SYSTEM.md");
|
|
15
|
+
const MANIFEST = path.join(ROOT, ".pi", "spec-manifest.json");
|
|
16
|
+
const CODEX_BEGIN = "<!-- BEGIN GENERATED EGREGORE CODEX SPEC";
|
|
17
|
+
const CODEX_END = "<!-- END GENERATED EGREGORE CODEX SPEC -->";
|
|
18
|
+
|
|
19
|
+
function hash(value) {
|
|
20
|
+
return crypto.createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function extractSpec(source) {
|
|
24
|
+
const begin = source.indexOf(CODEX_BEGIN);
|
|
25
|
+
const contentStart = source.indexOf("\n", begin);
|
|
26
|
+
const end = source.indexOf(CODEX_END, contentStart);
|
|
27
|
+
if (begin < 0 || contentStart < 0 || end < 0) {
|
|
28
|
+
throw new Error("AGENTS.md does not contain the generated Codex spec");
|
|
29
|
+
}
|
|
30
|
+
return source.slice(contentStart + 1, end).trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function replaceSection(markdown, heading, body) {
|
|
34
|
+
const marker = `## ${heading}`;
|
|
35
|
+
const start = markdown.indexOf(marker);
|
|
36
|
+
if (start < 0) throw new Error(`missing section in AGENTS.md: ${heading}`);
|
|
37
|
+
const rest = markdown.slice(start + marker.length);
|
|
38
|
+
const boundary = rest.search(/\n\n---\n\n## /);
|
|
39
|
+
const end = boundary < 0 ? markdown.length : start + marker.length + boundary;
|
|
40
|
+
return `${markdown.slice(0, start)}${marker}\n\n${body}${markdown.slice(end)}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function adapt(sourceSpec) {
|
|
44
|
+
let text = sourceSpec
|
|
45
|
+
.replaceAll("Egregore on Codex", "Egregore on Pi")
|
|
46
|
+
.replaceAll("Codex-native", "Pi-native")
|
|
47
|
+
.replaceAll("Codex sessions", "Pi sessions")
|
|
48
|
+
.replaceAll("On Codex", "On Pi")
|
|
49
|
+
.replaceAll("from Codex", "from Pi")
|
|
50
|
+
.replaceAll("inside Codex", "inside Pi")
|
|
51
|
+
.replaceAll("Codex has no", "Pi has no built-in")
|
|
52
|
+
.replaceAll("normal sandboxed Codex shell", "the Pi shell")
|
|
53
|
+
.replaceAll("Codex graph network access", "Pi graph network access")
|
|
54
|
+
.replaceAll("structured Codex question tooling", "Pi UI question tooling")
|
|
55
|
+
.replaceAll("structured Codex", "structured Pi")
|
|
56
|
+
.replaceAll("bin/codex-session-start.sh", "bin/pi-session-start.sh")
|
|
57
|
+
.replaceAll(".codex/hooks/branch-guard.js", ".pi/extensions/egregore.ts")
|
|
58
|
+
.replaceAll("PreToolUse hook (enabled by the launcher via `--enable hooks`)", "`tool_call` gate (loaded after Pi project trust)")
|
|
59
|
+
.replaceAll("If this Codex build does not support hooks", "If Pi project resources are disabled")
|
|
60
|
+
.replaceAll("change permissions in `.claude/settings.json` anytime", "review or disable project resources in `.pi/` anytime")
|
|
61
|
+
.replace(/`\$([a-z][a-z0-9-]*)/g, "`/$1")
|
|
62
|
+
.replace(/\$([a-z][a-z0-9-]*)/g, "/$1");
|
|
63
|
+
|
|
64
|
+
text = replaceSection(text, "Egregore on Pi", `You are Pi running inside Egregore — a shared intelligence layer for organizations using AI coding agents. You operate through Git-based shared memory, project-local Pi resources, Egregore skills, and conventions that accumulate knowledge across sessions and people. You are not a tool. You are a participant.
|
|
65
|
+
|
|
66
|
+
This appendix is generated from the reviewed shell-runtime translation of \`CLAUDE.md\`, the behavioral source of truth. Pi loads it through \`.pi/APPEND_SYSTEM.md\` after the launcher grants project trust for this run. Egregore workflows are shared from \`.codex/skills/\` using Pi's documented cross-harness skill support and exposed as familiar slash commands by \`.pi/extensions/egregore.ts\`.`);
|
|
67
|
+
|
|
68
|
+
text = replaceSection(text, "On Launch — MANDATORY FIRST ACTION", `The project-local Pi extension renders the Egregore startup card (identity, handoffs, team activity) inside Pi via \`bin/pi-session-start.sh\`. This works for direct \`pi\` launches and launcher-managed sessions. Do not rerun startup checks and do not narrate startup. The card ends with **"What are you working on?"** — that question is already on screen; treat the user's first message as the answer to it. To re-show the card outside Pi, run \`bash bin/pi-session-start.sh --card\`.`);
|
|
69
|
+
|
|
70
|
+
text = replaceSection(text, "Command Awareness", `Egregore workflows are available as Pi slash commands such as \`/activity\`, \`/handoff\`, \`/save\`, and \`/view\`. They are registered from the shared skill inventory by \`.pi/extensions/egregore.ts\`. Pi also exposes the underlying Agent Skills as \`/skill:name\`, and natural-language requests work normally. \`/save\` is the user-facing abstraction for committing, pushing, opening or reusing pull requests, and syncing memory — never make users manage the git workflow by hand.
|
|
71
|
+
|
|
72
|
+
Invoke commands from user intent — don't wait for the slash. Read the selected skill's \`SKILL.md\` completely before acting. Codex wording inside the shared portable skills means the shell-capable runtime adapter; in Pi, run referenced \`bin/\` scripts directly, use ordinary Pi shell/network access, ignore Codex approval-channel wording, and render compact numbered questions when no Pi UI question surface is available.
|
|
73
|
+
|
|
74
|
+
**Core loop** — \`/activity\` \`/dashboard\` \`/handoff\` \`/wrap\` \`/save\` \`/reflect\` \`/todo\`
|
|
75
|
+
**Knowledge** — \`/search\` \`/deep-reflect\` \`/archive\` \`/note\` \`/add\` \`/meeting\` \`/ingest\` \`/scroll\`
|
|
76
|
+
**Identity** — \`/me\`
|
|
77
|
+
**Coordination** — \`/ask\` \`/quest\` \`/issue\` \`/invite\` \`/delete-user\` \`/announce\`
|
|
78
|
+
**Git** — \`/branch\` \`/commit\` \`/push\` \`/pr\` \`/save\` \`/review-pr\` \`/contribute\`
|
|
79
|
+
**Infra** — \`/setup\` \`/update\` \`/pull\` \`/env\` \`/infra\` \`/sync-repos\` \`/release\` \`/checkup\``);
|
|
80
|
+
|
|
81
|
+
return `# Egregore on Pi\n\nThis file is generated by \`bin/pi-render-spec.mjs\`; do not edit it by hand.\n\n${text}\n`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const check = process.argv.includes("--check");
|
|
85
|
+
const source = fs.readFileSync(SOURCE, "utf8");
|
|
86
|
+
const sourceSpec = extractSpec(source);
|
|
87
|
+
const rendered = adapt(sourceSpec);
|
|
88
|
+
const manifest = `${JSON.stringify({
|
|
89
|
+
generatedBy: "bin/pi-render-spec.mjs",
|
|
90
|
+
source: "AGENTS.md generated Codex spec",
|
|
91
|
+
sourceHash: hash(sourceSpec),
|
|
92
|
+
outputHash: hash(rendered),
|
|
93
|
+
adaptations: [
|
|
94
|
+
"Pi project trust and APPEND_SYSTEM delivery",
|
|
95
|
+
"Pi tool_call branch gate",
|
|
96
|
+
"Pi slash-command extension over shared portable skills",
|
|
97
|
+
"plain-text question fallback and direct shell/network execution",
|
|
98
|
+
],
|
|
99
|
+
}, null, 2)}\n`;
|
|
100
|
+
|
|
101
|
+
if (check) {
|
|
102
|
+
const currentOutput = fs.existsSync(OUTPUT) ? fs.readFileSync(OUTPUT, "utf8") : "";
|
|
103
|
+
const currentManifest = fs.existsSync(MANIFEST) ? fs.readFileSync(MANIFEST, "utf8") : "";
|
|
104
|
+
if (currentOutput !== rendered || currentManifest !== manifest) {
|
|
105
|
+
console.error("pi spec out of date — run: node bin/pi-render-spec.mjs");
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
console.log("pi spec up to date");
|
|
109
|
+
} else {
|
|
110
|
+
fs.mkdirSync(path.dirname(OUTPUT), { recursive: true });
|
|
111
|
+
fs.writeFileSync(OUTPUT, rendered);
|
|
112
|
+
fs.writeFileSync(MANIFEST, manifest);
|
|
113
|
+
console.log(`pi spec rendered: .pi/APPEND_SYSTEM.md (${Buffer.byteLength(rendered)} bytes)`);
|
|
114
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -o pipefail
|
|
3
|
+
|
|
4
|
+
# Pi's project extension invokes this during session_start so direct `pi`
|
|
5
|
+
# launches and launcher-managed sessions share the same greeting experience.
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
7
|
+
|
|
8
|
+
case "${1:---card}" in
|
|
9
|
+
--card|card)
|
|
10
|
+
exec bash "$SCRIPT_DIR/bin/codex-session-start.sh" --card
|
|
11
|
+
;;
|
|
12
|
+
--prompt|prompt)
|
|
13
|
+
cat <<'EOF'
|
|
14
|
+
You are Pi running inside Egregore. The project extension renders the startup
|
|
15
|
+
card during session_start. Project instructions are in
|
|
16
|
+
.pi/APPEND_SYSTEM.md, Egregore workflows are
|
|
17
|
+
registered as slash commands, and portable skill bodies live in .codex/skills.
|
|
18
|
+
EOF
|
|
19
|
+
;;
|
|
20
|
+
*)
|
|
21
|
+
echo "usage: bin/pi-session-start.sh [--card|--prompt]" >&2
|
|
22
|
+
exit 2
|
|
23
|
+
;;
|
|
24
|
+
esac
|