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.
Files changed (171) hide show
  1. package/assets/egregore-launcher.js +118 -13
  2. package/bin/cli.js +19 -3
  3. package/lib/connect-runtime.js +4 -2
  4. package/lib/local.js +15 -1
  5. package/lib/prime-runtime.js +208 -0
  6. package/lib/setup.js +45 -3
  7. package/package.json +4 -3
  8. package/runtime/codex/.codex/skills/ingest-notion/SKILL.md +29 -0
  9. package/runtime/codex/.codex/skills/notion-connect/SKILL.md +29 -0
  10. package/runtime/codex/.codex/skills/teams-connect/SKILL.md +29 -0
  11. package/runtime/codex/.codex/spec-manifest.json +3 -3
  12. package/runtime/codex/AGENTS.md +11 -4
  13. package/runtime/codex/bin/activity-data.sh +4 -8
  14. package/runtime/codex/bin/attendant.sh +13 -0
  15. package/runtime/codex/bin/autosave.sh +27 -1
  16. package/runtime/codex/bin/codex-render-spec.mjs +6 -0
  17. package/runtime/codex/bin/codex-session-start.sh +3 -1
  18. package/runtime/codex/bin/graph-batch.sh +25 -6
  19. package/runtime/codex/bin/graph-op.sh +5 -4
  20. package/runtime/codex/bin/graph.sh +31 -5
  21. package/runtime/codex/bin/ingest.py +81 -5
  22. package/runtime/codex/bin/lib/context.sh +26 -1
  23. package/runtime/codex/bin/lib/greeting.sh +4 -1
  24. package/runtime/codex/bin/lib/worktree-links.sh +25 -0
  25. package/runtime/codex/manifest.json +7 -1
  26. package/runtime/pi/.codex/skills/ingest-notion/SKILL.md +29 -0
  27. package/runtime/pi/.codex/skills/notion-connect/SKILL.md +29 -0
  28. package/runtime/pi/.codex/skills/teams-connect/SKILL.md +29 -0
  29. package/runtime/pi/.codex/spec-manifest.json +3 -3
  30. package/runtime/pi/.pi/APPEND_SYSTEM.md +3 -2
  31. package/runtime/pi/.pi/extensions/egregore.ts +17 -6
  32. package/runtime/pi/.pi/spec-manifest.json +2 -2
  33. package/runtime/pi/AGENTS.md +11 -4
  34. package/runtime/pi/bin/activity-data.sh +4 -8
  35. package/runtime/pi/bin/attendant.sh +13 -0
  36. package/runtime/pi/bin/autosave.sh +27 -1
  37. package/runtime/pi/bin/codex-render-spec.mjs +6 -0
  38. package/runtime/pi/bin/codex-session-start.sh +3 -1
  39. package/runtime/pi/bin/graph-batch.sh +25 -6
  40. package/runtime/pi/bin/graph-op.sh +5 -4
  41. package/runtime/pi/bin/graph.sh +31 -5
  42. package/runtime/pi/bin/ingest.py +81 -5
  43. package/runtime/pi/bin/lib/context.sh +26 -1
  44. package/runtime/pi/bin/lib/greeting.sh +4 -1
  45. package/runtime/pi/bin/lib/worktree-links.sh +25 -0
  46. package/runtime/pi/bin/pi-branch-policy.mjs +138 -1
  47. package/runtime/pi/bin/pi-product-workflows.mjs +35 -7
  48. package/runtime/pi/manifest.json +7 -1
  49. package/runtime/prime/.claude/context/notification-consent.md +66 -0
  50. package/runtime/prime/.codex/config.toml +17 -0
  51. package/runtime/prime/.codex/hooks/branch-guard.js +461 -0
  52. package/runtime/prime/.codex/hooks/search-hint.js +67 -0
  53. package/runtime/prime/.codex/hooks.json +30 -0
  54. package/runtime/prime/.codex/skills/activity/SKILL.md +82 -0
  55. package/runtime/prime/.codex/skills/announce/SKILL.md +67 -0
  56. package/runtime/prime/.codex/skills/archive/SKILL.md +45 -0
  57. package/runtime/prime/.codex/skills/branch/SKILL.md +29 -0
  58. package/runtime/prime/.codex/skills/checkup/SKILL.md +45 -0
  59. package/runtime/prime/.codex/skills/commit/SKILL.md +29 -0
  60. package/runtime/prime/.codex/skills/contribute/SKILL.md +29 -0
  61. package/runtime/prime/.codex/skills/create-skill/SKILL.md +29 -0
  62. package/runtime/prime/.codex/skills/dark-mode/SKILL.md +29 -0
  63. package/runtime/prime/.codex/skills/dashboard/SKILL.md +59 -0
  64. package/runtime/prime/.codex/skills/deep-reflect/SKILL.md +109 -0
  65. package/runtime/prime/.codex/skills/delete-user/SKILL.md +29 -0
  66. package/runtime/prime/.codex/skills/emissary/SKILL.md +45 -0
  67. package/runtime/prime/.codex/skills/handoff/SKILL.md +166 -0
  68. package/runtime/prime/.codex/skills/harvest/QUESTION_PALETTE.md +76 -0
  69. package/runtime/prime/.codex/skills/harvest/SKILL.md +131 -0
  70. package/runtime/prime/.codex/skills/ingest/SKILL.md +29 -0
  71. package/runtime/prime/.codex/skills/ingest-notion/SKILL.md +29 -0
  72. package/runtime/prime/.codex/skills/ingest-user-interview/SKILL.md +45 -0
  73. package/runtime/prime/.codex/skills/invite/SKILL.md +44 -0
  74. package/runtime/prime/.codex/skills/issue/SKILL.md +45 -0
  75. package/runtime/prime/.codex/skills/me/SKILL.md +29 -0
  76. package/runtime/prime/.codex/skills/meeting/SKILL.md +45 -0
  77. package/runtime/prime/.codex/skills/note/SKILL.md +29 -0
  78. package/runtime/prime/.codex/skills/notion-connect/SKILL.md +29 -0
  79. package/runtime/prime/.codex/skills/onboarding/SKILL.md +45 -0
  80. package/runtime/prime/.codex/skills/pr/SKILL.md +29 -0
  81. package/runtime/prime/.codex/skills/pull/SKILL.md +29 -0
  82. package/runtime/prime/.codex/skills/push/SKILL.md +29 -0
  83. package/runtime/prime/.codex/skills/quest/SKILL.md +80 -0
  84. package/runtime/prime/.codex/skills/reflect/SKILL.md +45 -0
  85. package/runtime/prime/.codex/skills/review-pr/SKILL.md +45 -0
  86. package/runtime/prime/.codex/skills/save/SKILL.md +125 -0
  87. package/runtime/prime/.codex/skills/slack-connect/SKILL.md +29 -0
  88. package/runtime/prime/.codex/skills/summon/SKILL.md +45 -0
  89. package/runtime/prime/.codex/skills/sync-repos/SKILL.md +29 -0
  90. package/runtime/prime/.codex/skills/teams-connect/SKILL.md +29 -0
  91. package/runtime/prime/.codex/skills/telegram-connect/SKILL.md +29 -0
  92. package/runtime/prime/.codex/skills/telemetry/SKILL.md +29 -0
  93. package/runtime/prime/.codex/skills/test/SKILL.md +45 -0
  94. package/runtime/prime/.codex/skills/the-spiral/SKILL.md +84 -0
  95. package/runtime/prime/.codex/skills/tui-design/SKILL.md +45 -0
  96. package/runtime/prime/.codex/skills/tutorial/SKILL.md +45 -0
  97. package/runtime/prime/.codex/skills/update/SKILL.md +29 -0
  98. package/runtime/prime/.codex/skills/view/SKILL.md +156 -0
  99. package/runtime/prime/.codex/skills/view/agents/openai.yaml +4 -0
  100. package/runtime/prime/.codex/skills/view/assets/compose-scaffold.html +221 -0
  101. package/runtime/prime/.codex/skills/visual-explain/SKILL.md +45 -0
  102. package/runtime/prime/.codex/skills/wrap/SKILL.md +58 -0
  103. package/runtime/prime/.codex/spec-manifest.json +132 -0
  104. package/runtime/prime/.prime/agent/APPEND_SYSTEM.md +393 -0
  105. package/runtime/prime/.prime/agent/extensions/egregore.ts +460 -0
  106. package/runtime/prime/.prime/agent/settings.json +4 -0
  107. package/runtime/prime/.prime/agent/skills/egregore/SKILL.md +34 -0
  108. package/runtime/prime/.prime/agent/skills/egregore/pyproject.toml +13 -0
  109. package/runtime/prime/.prime/agent/skills/egregore/src/egregore/__init__.py +156 -0
  110. package/runtime/prime/.prime/agent/spec-manifest.json +12 -0
  111. package/runtime/prime/AGENTS.md +489 -0
  112. package/runtime/prime/bin/activity-data.sh +238 -0
  113. package/runtime/prime/bin/agent.sh +775 -0
  114. package/runtime/prime/bin/attendant.sh +707 -0
  115. package/runtime/prime/bin/autosave.sh +112 -0
  116. package/runtime/prime/bin/capture-reconcile.sh +69 -0
  117. package/runtime/prime/bin/capture-run.sh +341 -0
  118. package/runtime/prime/bin/codex-install-skills.sh +78 -0
  119. package/runtime/prime/bin/codex-render-spec.mjs +440 -0
  120. package/runtime/prime/bin/codex-session-start.sh +265 -0
  121. package/runtime/prime/bin/codex-skill-render.mjs +347 -0
  122. package/runtime/prime/bin/codex-sync-skills.sh +232 -0
  123. package/runtime/prime/bin/connect-refresh.sh +33 -0
  124. package/runtime/prime/bin/dashboard-data.sh +178 -0
  125. package/runtime/prime/bin/graph-batch.sh +79 -0
  126. package/runtime/prime/bin/graph-op.sh +1102 -0
  127. package/runtime/prime/bin/graph-wal.sh +229 -0
  128. package/runtime/prime/bin/graph.sh +223 -0
  129. package/runtime/prime/bin/handoff-pr-backfill.sh +86 -0
  130. package/runtime/prime/bin/handoff-run.sh +671 -0
  131. package/runtime/prime/bin/handoff-save-egregore.sh +212 -0
  132. package/runtime/prime/bin/index-handoff.sh +322 -0
  133. package/runtime/prime/bin/ingest-graph.sh +80 -0
  134. package/runtime/prime/bin/ingest-surface.html +495 -0
  135. package/runtime/prime/bin/ingest.py +756 -0
  136. package/runtime/prime/bin/ingest.sh +279 -0
  137. package/runtime/prime/bin/ingest_graph.py +856 -0
  138. package/runtime/prime/bin/ingest_surface.py +544 -0
  139. package/runtime/prime/bin/lib/artifact-id.sh +46 -0
  140. package/runtime/prime/bin/lib/config.sh +116 -0
  141. package/runtime/prime/bin/lib/context.sh +621 -0
  142. package/runtime/prime/bin/lib/dashboard-artifact.sh +242 -0
  143. package/runtime/prime/bin/lib/ensure-develop.sh +183 -0
  144. package/runtime/prime/bin/lib/git-sync.sh +406 -0
  145. package/runtime/prime/bin/lib/greeting.sh +519 -0
  146. package/runtime/prime/bin/lib/hash.sh +13 -0
  147. package/runtime/prime/bin/lib/identity.sh +213 -0
  148. package/runtime/prime/bin/lib/metrics.sh +374 -0
  149. package/runtime/prime/bin/lib/noncode.sh +31 -0
  150. package/runtime/prime/bin/lib/permissions.sh +106 -0
  151. package/runtime/prime/bin/lib/time.sh +56 -0
  152. package/runtime/prime/bin/lib/worktree-links.sh +79 -0
  153. package/runtime/prime/bin/notify.sh +636 -0
  154. package/runtime/prime/bin/person.py +630 -0
  155. package/runtime/prime/bin/person.sh +448 -0
  156. package/runtime/prime/bin/pi-branch-policy.mjs +312 -0
  157. package/runtime/prime/bin/pi-product-workflows.mjs +394 -0
  158. package/runtime/prime/bin/prime-render-spec.mjs +123 -0
  159. package/runtime/prime/bin/prime-session-start.sh +24 -0
  160. package/runtime/prime/bin/publish-artifact.sh +271 -0
  161. package/runtime/prime/bin/publish-references.sh +144 -0
  162. package/runtime/prime/bin/repo-state.sh +96 -0
  163. package/runtime/prime/bin/search.sh +240 -0
  164. package/runtime/prime/bin/session-autosave.sh +209 -0
  165. package/runtime/prime/bin/session-log.sh +199 -0
  166. package/runtime/prime/bin/settings.sh +304 -0
  167. package/runtime/prime/bin/telemetry.sh +391 -0
  168. package/runtime/prime/bin/worktree-create.sh +128 -0
  169. package/runtime/prime/bin/worktree-remove.sh +50 -0
  170. package/runtime/prime/bin/worktree.sh +198 -0
  171. package/runtime/prime/manifest.json +178 -0
@@ -0,0 +1,460 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
3
+ import { Text } from "@earendil-works/pi-tui";
4
+ import { Type } from "typebox";
5
+ import {
6
+ isConsentCommand,
7
+ isMutatingBash,
8
+ isMutatingIpython,
9
+ isRuntimeStatePath,
10
+ mutatesOnlyRuntimeState,
11
+ } from "../../../bin/pi-branch-policy.mjs";
12
+ import {
13
+ activityDataPath,
14
+ captureSessionEnd,
15
+ createHandoff,
16
+ lastActivityJson,
17
+ parseViewRequest,
18
+ publishScroll,
19
+ renderActivity,
20
+ renderGreeting,
21
+ renderView,
22
+ workflowPrompt,
23
+ } from "../../../bin/pi-product-workflows.mjs";
24
+
25
+ const PROTECTED_BRANCHES = new Set(["main", "master", "develop"]);
26
+ // Prime Agent built-in slash commands (README Commands table) plus
27
+ // conservative extras for surfaces added around launch. An Egregore skill
28
+ // with a colliding name stays reachable via /skill:<name>.
29
+ const BUILTIN_COMMANDS = new Set([
30
+ "login", "logout", "model", "effort", "scoped-models", "settings", "resume",
31
+ "new", "clear", "name", "session", "traces", "usage", "tree", "fork",
32
+ "clone", "compact", "copy", "btw", "side", "export", "share", "reload",
33
+ "hotkeys", "changelog", "quit", "skill", "mcp",
34
+ "refine", "autonomous", "trust", "import", "agents", "harness", "schedule",
35
+ "goal", "memory",
36
+ // Observed at runtime (not in the README command table); Prime warns and
37
+ // skips conflicting extension commands in autocomplete, so any future
38
+ // collision is visible and non-fatal — add it here to keep startup quiet.
39
+ "update",
40
+ ]);
41
+ const PRODUCT_COMMANDS = new Set(["activity", "view", "handoff", "scroll"]);
42
+ const PRIME_SPEC = { runtimeName: "Prime Agent", specPath: ".prime/agent/APPEND_SYSTEM.md" };
43
+
44
+ function skillDescription(skillFile) {
45
+ try {
46
+ const body = readFileSync(skillFile, "utf8");
47
+ return body.match(/^description:\s*['"]?(.+?)['"]?\s*$/m)?.[1] || "Run an Egregore workflow";
48
+ } catch {
49
+ return "Run an Egregore workflow";
50
+ }
51
+ }
52
+
53
+ function messageText(result) {
54
+ return (result?.content || [])
55
+ .filter((item) => item?.type === "text")
56
+ .map((item) => item.text)
57
+ .join("\n");
58
+ }
59
+
60
+ function compactToolResult(result, options, theme, working) {
61
+ if (options.isPartial) return new Text(theme.fg("muted", working), 0, 0);
62
+ const text = result?.details?.text || messageText(result) || "Done";
63
+ return new Text(result?.details?.text ? text : theme.fg("error", text), 0, 0);
64
+ }
65
+
66
+ function errorMessage(error) {
67
+ return error instanceof Error ? error.message : String(error);
68
+ }
69
+
70
+ function nearestExistingDirectory(candidate) {
71
+ let cursor = resolve(candidate);
72
+ try {
73
+ if (existsSync(cursor) && !statSync(cursor).isDirectory()) cursor = dirname(cursor);
74
+ } catch {}
75
+ while (!existsSync(cursor)) {
76
+ const parent = dirname(cursor);
77
+ if (parent === cursor) break;
78
+ cursor = parent;
79
+ }
80
+ return cursor;
81
+ }
82
+
83
+ async function gitContext(pi, candidate) {
84
+ const probe = nearestExistingDirectory(candidate);
85
+ const topResult = await pi.exec("git", ["-C", probe, "rev-parse", "--show-toplevel"], { timeout: 5000 });
86
+ const top = String(topResult.stdout || "").trim();
87
+ if (!top) return null;
88
+ const commonResult = await pi.exec("git", ["-C", top, "rev-parse", "--git-common-dir"], { timeout: 5000 });
89
+ const commonRaw = String(commonResult.stdout || "").trim();
90
+ return {
91
+ top: resolve(top),
92
+ common: resolve(top, commonRaw || ".git"),
93
+ };
94
+ }
95
+
96
+ export default function egregore(pi) {
97
+ const root = process.cwd();
98
+ const skillsDir = join(root, ".codex", "skills");
99
+ let workflowCount = 0;
100
+ let lastSyncAt = 0;
101
+ let activeWorkflow = "";
102
+ const hasEntryRenderer = typeof pi.registerEntryRenderer === "function";
103
+ const prompt = (name, args, extra = "") => workflowPrompt(name, args, extra, PRIME_SPEC);
104
+
105
+ const appendCard = (kind, text) => {
106
+ const content = String(text || "").trimEnd();
107
+ if (hasEntryRenderer) {
108
+ pi.appendEntry("egregore-card", { kind, text: content });
109
+ return;
110
+ }
111
+ pi.sendMessage({
112
+ customType: "egregore-card",
113
+ content,
114
+ display: true,
115
+ details: { kind },
116
+ }, { triggerTurn: false });
117
+ };
118
+
119
+ const setBusy = (ctx, label) => {
120
+ if (!ctx.hasUI) return;
121
+ ctx.ui.setStatus("egregore-workflow", label ? `● ${label}` : undefined);
122
+ };
123
+
124
+ const sendQuietWorkflow = (name, args, ctx, options = {}) => {
125
+ activeWorkflow = name;
126
+ if (ctx.hasUI) {
127
+ ctx.ui.setToolsExpanded?.(false);
128
+ ctx.ui.setWorkingMessage?.(options.working || `Running /${name}…`);
129
+ }
130
+ const content = options.prompt || prompt(name, args, options.extra || "");
131
+ if (typeof pi.sendMessage === "function") {
132
+ pi.sendMessage({
133
+ customType: "egregore-workflow",
134
+ content,
135
+ display: false,
136
+ details: { command: name },
137
+ }, { triggerTurn: true, deliverAs: "steer" });
138
+ } else {
139
+ pi.sendUserMessage(content);
140
+ }
141
+ };
142
+
143
+ if (hasEntryRenderer) {
144
+ pi.registerEntryRenderer("egregore-card", (entry) => {
145
+ return new Text(String(entry.data?.text || ""), 0, 0);
146
+ });
147
+ } else if (typeof pi.registerMessageRenderer === "function") {
148
+ pi.registerMessageRenderer("egregore-card", (message) => {
149
+ return new Text(String(message.content || ""), 0, 0);
150
+ });
151
+ }
152
+
153
+ pi.registerTool({
154
+ name: "egregore_handoff",
155
+ label: "Egregore Handoff",
156
+ description: "Create one internal Egregore team handoff from structured session context. Use only for the /handoff workflow.",
157
+ parameters: Type.Object({
158
+ topic: Type.String({ description: "Short operational topic" }),
159
+ recipient: Type.Optional(Type.String({ description: "Known teammate handle; omit when uncertain" })),
160
+ briefing: Type.String({ description: "Concise operational briefing" }),
161
+ key_decisions: Type.String({ description: "Markdown bullets of settled decisions" }),
162
+ current_state: Type.String({ description: "Exact implementation and verification state" }),
163
+ open_threads: Type.String({ description: "Markdown bullets of unresolved work" }),
164
+ next_steps: Type.String({ description: "Ordered, actionable next steps" }),
165
+ entry_points: Type.String({ description: "Exact files, branches, commands, and URLs" }),
166
+ }),
167
+ async execute(_toolCallId, params, signal) {
168
+ const receipt = await createHandoff(pi, root, params, signal);
169
+ return {
170
+ content: [{ type: "text", text: `Handoff created${receipt.path ? ` at ${receipt.path}` : ""}.` }],
171
+ details: { ...receipt },
172
+ };
173
+ },
174
+ renderCall(_args, theme) {
175
+ return new Text(theme.fg("muted", "⇌ Preparing handoff…"), 0, 0);
176
+ },
177
+ renderResult(result, options, theme) {
178
+ return compactToolResult(result, options, theme, "⇌ Preparing handoff…");
179
+ },
180
+ });
181
+
182
+ pi.registerTool({
183
+ name: "egregore_publish_scroll",
184
+ label: "Publish Egregore Scroll",
185
+ description: "Validate, publish, open, and receipt a completed Egregore scroll. Use only as the final action of /scroll.",
186
+ parameters: Type.Object({
187
+ source: Type.String({ description: "Project-relative tmp/<slug>/index.html source" }),
188
+ slug: Type.String({ description: "Stable scroll id reused across versions" }),
189
+ title: Type.String(),
190
+ author: Type.String(),
191
+ description: Type.String(),
192
+ version: Type.String({ description: "Version marker such as v1" }),
193
+ }),
194
+ async execute(_toolCallId, params, signal) {
195
+ const receipt = await publishScroll(pi, root, params, signal);
196
+ return {
197
+ content: [{ type: "text", text: `Scroll ${receipt.version} opened at ${receipt.url || receipt.path}.` }],
198
+ details: { ...receipt },
199
+ };
200
+ },
201
+ renderCall(_args, theme) {
202
+ return new Text(theme.fg("muted", "◉ Publishing scroll…"), 0, 0);
203
+ },
204
+ renderResult(result, options, theme) {
205
+ return compactToolResult(result, options, theme, "◉ Publishing scroll…");
206
+ },
207
+ });
208
+
209
+ if (existsSync(skillsDir)) {
210
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
211
+ if (!entry.isDirectory() || BUILTIN_COMMANDS.has(entry.name)) continue;
212
+ const skillFile = join(skillsDir, entry.name, "SKILL.md");
213
+ if (!existsSync(skillFile)) continue;
214
+ workflowCount += 1;
215
+ if (PRODUCT_COMMANDS.has(entry.name)) continue;
216
+ pi.registerCommand(entry.name, {
217
+ description: skillDescription(skillFile),
218
+ handler: async (args, ctx) => sendQuietWorkflow(entry.name, args, ctx),
219
+ });
220
+ }
221
+ }
222
+
223
+ pi.registerCommand("activity", {
224
+ description: skillDescription(join(skillsDir, "activity", "SKILL.md")),
225
+ handler: async (_args, ctx) => {
226
+ setBusy(ctx, "Refreshing activity");
227
+ try {
228
+ const shouldSync = Date.now() - lastSyncAt > 60_000;
229
+ const card = await renderActivity(pi, root, ctx.signal, { sync: shouldSync });
230
+ lastSyncAt = Date.now();
231
+ appendCard("activity", card);
232
+ } catch (error) {
233
+ ctx.ui.notify(`Activity: ${errorMessage(error)}`, "error");
234
+ return;
235
+ } finally {
236
+ setBusy(ctx, "");
237
+ }
238
+
239
+ if (!ctx.hasUI) return;
240
+ const focus = await ctx.ui.select("Focus", ["Handoffs", "Questions", "Recent work", "Dashboard", "Done"]);
241
+ if (!focus || focus === "Done") return;
242
+ const command = focus === "Dashboard" ? "dashboard" : "activity";
243
+ // Hand the model the complete data the extension already fetched, as a
244
+ // file: one fast kernel read, never truncated, no reason to re-run the
245
+ // time-capped data script or spelunk renderer sources.
246
+ const cached = lastActivityJson();
247
+ const focusPrompt = focus === "Dashboard"
248
+ ? prompt("dashboard", "")
249
+ : [
250
+ `Continue the Egregore /activity workflow with focus: ${focus.toLowerCase()}.`,
251
+ "Output nothing except the final rendered result — no prose, planning, or narration between commands.",
252
+ cached
253
+ ? `The complete activity data is already fresh at ${relative(root, activityDataPath(root))} — read that file in one call; do NOT re-run bin/activity-data.sh, read renderer sources, or run other data commands.`
254
+ : "",
255
+ "If required data is missing or a command fails, render the single line '◦ activity data unavailable' and stop — never debug data plumbing inside this workflow.",
256
+ "Rows without a topic: show the branch name instead; with neither, omit the row — never print (untitled).",
257
+ ].filter(Boolean).join("\n");
258
+ sendQuietWorkflow(command, "", ctx, { prompt: focusPrompt, working: `Opening ${focus.toLowerCase()}…` });
259
+ },
260
+ });
261
+
262
+ pi.registerCommand("view", {
263
+ description: skillDescription(join(skillsDir, "view", "SKILL.md")),
264
+ handler: async (args, ctx) => {
265
+ let request = parseViewRequest(root, args);
266
+ if (request.kind === "choose" && ctx.hasUI) {
267
+ const choice = await ctx.ui.select("Open a view", ["Activity", "Board", "Network", "Document or topic…"]);
268
+ if (!choice) return;
269
+ if (choice === "Document or topic…") {
270
+ const target = await ctx.ui.input("Document or topic", "path, handoff, quest, or subject");
271
+ if (!target) return;
272
+ request = parseViewRequest(root, target);
273
+ } else {
274
+ request = parseViewRequest(root, choice.toLowerCase());
275
+ }
276
+ }
277
+
278
+ if (request.kind === "choose-file" && ctx.hasUI) {
279
+ const labels = request.matches.map((file) => relative(root, file));
280
+ const selected = await ctx.ui.select("Choose a source", labels);
281
+ if (!selected) return;
282
+ request = { kind: "native", type: request.type, source: join(root, selected), fast: request.fast };
283
+ }
284
+
285
+ if (request.kind !== "native") {
286
+ sendQuietWorkflow("view", args, ctx, {
287
+ working: "Composing view…",
288
+ extra: "Open the browser artifact and return only the compact receipt required by the skill.",
289
+ });
290
+ return;
291
+ }
292
+
293
+ setBusy(ctx, "Opening view");
294
+ try {
295
+ const receipt = await renderView(pi, root, request, ctx.signal);
296
+ appendCard("view", receipt.text);
297
+ } catch (error) {
298
+ ctx.ui.notify(`View: ${errorMessage(error)}`, "error");
299
+ } finally {
300
+ setBusy(ctx, "");
301
+ }
302
+ },
303
+ });
304
+
305
+ pi.registerCommand("handoff", {
306
+ description: skillDescription(join(skillsDir, "handoff", "SKILL.md")),
307
+ handler: async (args, ctx) => {
308
+ const people = existsSync(join(root, "memory", "people"))
309
+ ? readdirSync(join(root, "memory", "people"))
310
+ .filter((name) => name.endsWith(".md") && name !== "README.md")
311
+ .map((name) => name.replace(/\.md$/, ""))
312
+ .slice(0, 40)
313
+ : [];
314
+ const handoffPrompt = [
315
+ "Create the requested internal Egregore handoff from the current conversation and repository context.",
316
+ "Call egregore_handoff exactly once. Do not call bash, write, edit, or read the handoff skill; the tool implements the full contract.",
317
+ "Keep every field concise and operational. Include exact files, branch, commands, open questions, and verification state.",
318
+ "Omit recipient when uncertain. Do not use the deprecated egregore-handoff CLI.",
319
+ args?.trim() ? `User arguments: ${args.trim()}` : "Infer the topic and intended recipient from the conversation.",
320
+ people.length ? `Known teammate handles: ${people.join(", ")}` : "",
321
+ "After the tool succeeds, do not repeat its card or add a success preamble.",
322
+ ].filter(Boolean).join("\n");
323
+ sendQuietWorkflow("handoff", args, ctx, { prompt: handoffPrompt, working: "Preparing handoff…" });
324
+ },
325
+ });
326
+
327
+ pi.registerCommand("scroll", {
328
+ description: skillDescription(join(skillsDir, "scroll", "SKILL.md")),
329
+ handler: async (args, ctx) => {
330
+ sendQuietWorkflow("scroll", args, ctx, {
331
+ working: "Composing scroll…",
332
+ extra: [
333
+ "Use egregore_publish_scroll exactly once as the final validation, publish, open, and receipt step.",
334
+ "Do not run publish-artifact.sh or a browser opener directly.",
335
+ "Return only what changed, the stable URL or local path, the source path, and the versioned decision fence.",
336
+ ].join("\n"),
337
+ });
338
+ },
339
+ });
340
+
341
+ pi.on("session_start", async (event, ctx) => {
342
+ if (!ctx.hasUI) return;
343
+ ctx.ui.setToolsExpanded?.(false);
344
+ if (workflowCount > 0) ctx.ui.setStatus("egregore", `Egregore · ${workflowCount} workflows`);
345
+ if (event.reason !== "startup") return;
346
+
347
+ setBusy(ctx, "Loading team context");
348
+ try {
349
+ const card = await renderGreeting(pi, root, ctx.signal, "prime-session-start.sh");
350
+ lastSyncAt = Date.now();
351
+ appendCard("greeting", card);
352
+ } catch (error) {
353
+ ctx.ui.notify(`Startup: ${errorMessage(error)}`, "warning");
354
+ } finally {
355
+ setBusy(ctx, "");
356
+ }
357
+ });
358
+
359
+ pi.on("session_shutdown", async (event, ctx) => {
360
+ // Reload keeps the same logical session alive. Every other graceful
361
+ // shutdown is Prime Agent's equivalent of Claude Code's SessionEnd hook.
362
+ if (event?.reason === "reload") return;
363
+ try {
364
+ await captureSessionEnd(pi, root, ctx.sessionManager.getSessionFile(), undefined, "prime");
365
+ } catch {
366
+ // Loss-prevention capture must never prevent the harness from exiting.
367
+ // The attendant remains the fallback for interrupted or failed shutdowns.
368
+ }
369
+ });
370
+
371
+ pi.on("agent_settled", async (_event, ctx) => {
372
+ if (!activeWorkflow || !ctx.hasUI) return;
373
+ activeWorkflow = "";
374
+ ctx.ui.setWorkingMessage?.();
375
+ ctx.ui.setToolsExpanded?.(false);
376
+ });
377
+
378
+ pi.on("tool_call", async (event, ctx) => {
379
+ const isFileWrite = event.toolName === "write" || event.toolName === "edit";
380
+ const command = String(event.input?.command || "");
381
+ // Prime Agent's default surface is the persistent IPython kernel: file
382
+ // writes and shell escapes arrive as Python code, not bash tool calls.
383
+ const ipythonCode = event.toolName === "ipython" ? String(event.input?.code || "") : "";
384
+ const mutates = isFileWrite ||
385
+ (event.toolName === "bash" && isMutatingBash(command)) ||
386
+ (event.toolName === "ipython" && isMutatingIpython(ipythonCode, ctx.cwd));
387
+ if (!mutates) return;
388
+
389
+ // Resolve the branch in the git context the write actually lands in.
390
+ // Task worktrees from `bin/agent.sh branch` live outside ctx.cwd but sit
391
+ // on a task branch, so checking ctx.cwd alone blocks legitimate worktree
392
+ // edits whenever the session checkout itself is on develop. Memory and
393
+ // managed repos have a different Git common dir and are outside this guard.
394
+ const hub = await gitContext(pi, ctx.cwd);
395
+ if (event.toolName === "bash" && mutatesOnlyRuntimeState(command, hub?.top || ctx.cwd)) return;
396
+ if (event.toolName === "ipython" && hub && hub.top !== ctx.cwd &&
397
+ !isMutatingIpython(ipythonCode, hub.top)) return;
398
+ let operation = hub;
399
+ if (isFileWrite) {
400
+ const targetPath = String(event.input?.path || "");
401
+ const abs = isAbsolute(targetPath) ? targetPath : join(ctx.cwd, targetPath);
402
+ if (hub && isRuntimeStatePath(abs, hub.top)) return;
403
+ operation = await gitContext(pi, abs);
404
+ if (!operation && hub) {
405
+ const rel = relative(hub.top, abs);
406
+ if (rel.startsWith("..") || isAbsolute(rel)) return;
407
+ }
408
+ } else if (event.toolName === "bash") {
409
+ // bash: `git -C PATH` or a leading `cd DIR &&` retargets the command
410
+ // away from ctx.cwd — resolve the branch where it actually runs so
411
+ // worktree commits (git -C $WT commit) aren't misjudged by the session
412
+ // checkout's branch.
413
+ const gitC = command.match(/(?:^|[;&|]\s*)git\s+-C\s+("[^"]*"|'[^']*'|\S+)/);
414
+ const cdAnd = command.match(/^\s*cd\s+("[^"]*"|'[^']*'|\S+)\s*&&/);
415
+ const raw = (gitC && gitC[1]) || (cdAnd && cdAnd[1]);
416
+ if (raw) {
417
+ const target = raw.replace(/^['"]|['"]$/g, "");
418
+ const abs = isAbsolute(target) ? target : join(ctx.cwd, target);
419
+ operation = await gitContext(pi, abs);
420
+ }
421
+ }
422
+
423
+ if (hub && operation && operation.common !== hub.common) return;
424
+ const gitDir = operation?.top || hub?.top || ctx.cwd;
425
+ const result = await pi.exec("git", ["-C", gitDir, "branch", "--show-current"], { timeout: 5000 });
426
+ const branch = String(result.stdout || "").trim();
427
+ let configuredBase = "develop";
428
+ try {
429
+ const config = JSON.parse(readFileSync(join(gitDir, "egregore.json"), "utf8"));
430
+ if (typeof config.base_branch === "string" && config.base_branch.trim()) {
431
+ configuredBase = config.base_branch.trim();
432
+ }
433
+ } catch {}
434
+ if (!PROTECTED_BRANCHES.has(branch) && branch !== configuredBase) return;
435
+
436
+ // The documented escape hatch must itself be runnable: recording consent
437
+ // (echo 'develop' into .egregore-branch-consent) contains a redirect and
438
+ // would otherwise be blocked by this very gate.
439
+ if (event.toolName === "bash" || event.toolName === "ipython") {
440
+ const probe = event.toolName === "bash"
441
+ ? String(event.input?.command || "").trim()
442
+ : ipythonCode.replace(/^\s*!/, "").trim();
443
+ if (isConsentCommand(probe, branch)) return;
444
+ }
445
+
446
+ const consentFile = join(gitDir, ".egregore-branch-consent");
447
+ if (existsSync(consentFile) && readFileSync(consentFile, "utf8").trim() === branch) return;
448
+
449
+ return {
450
+ block: true,
451
+ reason: [
452
+ `Protected branch (${branch}). Move project work to a task branch before writing.`,
453
+ `If the work topic is clear, run bin/agent.sh branch --topic "<work topic>" automatically and continue in the printed worktree; do not ask for routine Git permission.`,
454
+ `Ask only when the topic is genuinely ambiguous.`,
455
+ `Only when the user explicitly requested ${branch}, record consent with: echo '${branch}' > .egregore-branch-consent`,
456
+ "Memory, managed-repo, and runtime-state writes should bypass this project guard.",
457
+ ].join("\n"),
458
+ };
459
+ });
460
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "skills": ["../../.codex/skills"],
3
+ "enableSkillCommands": false
4
+ }
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: egregore
3
+ description: Egregore mechanics as kernel functions — search shared memory, read team activity, create handoffs, save work, branch, and prepare notification plans without re-deriving bin/ CLI usage. Import and call directly; every function wraps the same runtime-neutral bin/ scripts the other harnesses use.
4
+ ---
5
+
6
+ # egregore — kernel bridge to Egregore mechanics
7
+
8
+ Python-backed skill. The module is installed into the kernel venv; call the
9
+ functions directly instead of grepping `bin/` scripts to reconstruct CLI usage:
10
+
11
+ ```python
12
+ import egregore
13
+
14
+ egregore.search("pricing decision") # ranked shared-memory recall
15
+ egregore.activity() # team activity JSON
16
+ egregore.handoff(to="renc", topic="...", body="...")
17
+ egregore.save(message="Save: topic", topic="topic")
18
+ egregore.branch("new topic") # task branch + worktree
19
+ egregore.notify_plan(recipient="renc", message="...") # proposal ONLY
20
+ ```
21
+
22
+ Ground rules:
23
+
24
+ - Every function shells out to the corresponding `bin/` script with a generous
25
+ internal timeout and returns its text output. `bin/` stays the single
26
+ mechanics layer — this module contains no logic of its own.
27
+ - `notify_plan` only CREATES a notification plan. Approval and dispatch remain
28
+ subject to the explicit human Send / Edit / Cancel checkpoint: show the plan,
29
+ get the user's approval, then run `bin/notify.sh approve` / `dispatch` with
30
+ the plan's one-use approval token. Never call approve or dispatch without a
31
+ fresh human yes in this conversation.
32
+ - Functions raise `RuntimeError` with the script's stderr on failure — report
33
+ the single relevant line, do not debug the plumbing mid-workflow.
34
+ - `help()` on the module or any function shows signatures and semantics.
@@ -0,0 +1,13 @@
1
+ [project]
2
+ name = "egregore"
3
+ version = "0.1.0"
4
+ description = "Egregore mechanics as Prime Agent kernel functions (thin wrappers over bin/ scripts)"
5
+ requires-python = ">=3.10"
6
+ dependencies = []
7
+
8
+ [build-system]
9
+ requires = ["setuptools>=61"]
10
+ build-backend = "setuptools.build_meta"
11
+
12
+ [tool.setuptools.packages.find]
13
+ where = ["src"]
@@ -0,0 +1,156 @@
1
+ """Egregore mechanics as kernel functions.
2
+
3
+ Thin wrappers over the runtime-neutral ``bin/`` scripts of the enclosing
4
+ Egregore checkout. No logic lives here: every function resolves the instance
5
+ root from the current working directory, shells out to the same script the
6
+ other harnesses use, and returns its text output.
7
+
8
+ Consent boundary: :func:`notify_plan` only creates a proposal. Approving and
9
+ dispatching a notification require an explicit human Send / Edit / Cancel
10
+ checkpoint in the conversation — this module deliberately provides no
11
+ ``notify_approve``/``notify_dispatch`` so that step stays visible.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import os
18
+ import subprocess
19
+ from pathlib import Path
20
+
21
+ __all__ = [
22
+ "activity",
23
+ "branch",
24
+ "handoff",
25
+ "instance_root",
26
+ "notify_plan",
27
+ "run",
28
+ "save",
29
+ "search",
30
+ ]
31
+
32
+ _DEFAULT_TIMEOUT = 120
33
+
34
+
35
+ def instance_root(start: str | os.PathLike | None = None) -> Path:
36
+ """Walk up from ``start`` (default: cwd) to the directory holding egregore.json."""
37
+ cursor = Path(start or os.getcwd()).resolve()
38
+ for candidate in (cursor, *cursor.parents):
39
+ if (candidate / "egregore.json").is_file():
40
+ return candidate
41
+ raise RuntimeError(
42
+ "not inside an Egregore instance (no egregore.json above %s)" % cursor
43
+ )
44
+
45
+
46
+ def _run_script(args: list[str], timeout: int = _DEFAULT_TIMEOUT) -> str:
47
+ root = instance_root()
48
+ script = root / args[0]
49
+ if not script.is_file():
50
+ raise RuntimeError(f"missing script: {script}")
51
+ try:
52
+ result = subprocess.run(
53
+ ["bash", *args],
54
+ cwd=root,
55
+ capture_output=True,
56
+ text=True,
57
+ timeout=timeout,
58
+ start_new_session=True,
59
+ )
60
+ except subprocess.TimeoutExpired as exc:
61
+ # Own process group (start_new_session) so the timeout kills the real
62
+ # workers, not just the bash wrapper; surface it as the documented
63
+ # RuntimeError instead of an uncaught traceback in the kernel.
64
+ raise RuntimeError(f"{args[0]} timed out after {timeout}s") from exc
65
+ if result.returncode != 0:
66
+ detail = (result.stderr or result.stdout or "").strip().splitlines()
67
+ raise RuntimeError(
68
+ f"{args[0]} failed (exit {result.returncode}): {detail[-1] if detail else 'no output'}"
69
+ )
70
+ return result.stdout.strip()
71
+
72
+
73
+ def search(query: str, limit: int = 6) -> str:
74
+ """Ranked shared-memory recall (decisions, handoffs, knowledge, meetings)."""
75
+ return _run_script(["bin/search.sh", "query", query, "-n", str(limit)])
76
+
77
+
78
+ def activity(for_user: str | None = None) -> str:
79
+ """Team activity: handoffs, questions, and recent work.
80
+
81
+ Returns the runtime-neutral ``bin/agent.sh activity`` text view; pass
82
+ ``for_user`` to scope to a handle.
83
+ """
84
+ args = ["bin/agent.sh", "activity"]
85
+ if for_user:
86
+ args += ["--for", for_user]
87
+ return _run_script(args)
88
+
89
+
90
+ def branch(topic: str) -> str:
91
+ """Create a task branch + worktree for ``topic``; returns branch and path."""
92
+ return _run_script(["bin/agent.sh", "branch", "--topic", topic])
93
+
94
+
95
+ def save(message: str, topic: str, pr_body: str | None = None) -> str:
96
+ """Commit, push, and open/update the PR for the current working branch."""
97
+ args = ["bin/agent.sh", "save", "--message", message, "--topic", topic]
98
+ if pr_body:
99
+ args += ["--pr-body", pr_body]
100
+ return _run_script(args, timeout=300)
101
+
102
+
103
+ def handoff(
104
+ to: str | None,
105
+ topic: str,
106
+ body: str,
107
+ sender: str | None = None,
108
+ notify: bool = False,
109
+ ) -> str:
110
+ """Create an internal handoff document addressed to ``to``.
111
+
112
+ ``notify=False`` (default) skips the notification leg entirely; enabling it
113
+ still routes through the plan/approve/dispatch consent protocol.
114
+ """
115
+ root = instance_root()
116
+ if sender is None:
117
+ try:
118
+ state = json.loads((root / ".egregore-state.json").read_text())
119
+ sender = state.get("github_username") or "unknown"
120
+ except (OSError, ValueError):
121
+ sender = "unknown"
122
+ args = [
123
+ "bin/agent.sh", "handoff",
124
+ "--from", sender,
125
+ "--topic", topic,
126
+ "--body", body,
127
+ ]
128
+ if to:
129
+ args += ["--to", to]
130
+ if not notify:
131
+ args += ["--no-notify"]
132
+ return _run_script(args, timeout=300)
133
+
134
+
135
+ def notify_plan(recipient: str | None, message: str) -> str:
136
+ """Create a notification PLAN (proposal only — nothing is sent).
137
+
138
+ ``recipient`` is a person's handle for a DM, or ``None`` for the group
139
+ channel. Show the returned plan (recipient, channel, exact message, plan
140
+ id and one-use approval token) to the user in a Send / Edit / Cancel
141
+ checkpoint. Only after an explicit yes run ``bin/notify.sh approve`` and
142
+ ``dispatch`` yourself — this module intentionally stops at the plan.
143
+ """
144
+ if recipient:
145
+ return _run_script(["bin/notify.sh", "plan", "send", recipient, message])
146
+ return _run_script(["bin/notify.sh", "plan", "group", message])
147
+
148
+
149
+ def run() -> str:
150
+ """Discovery entry point: list the bridge functions and the consent rule."""
151
+ return (
152
+ "egregore kernel bridge — search(query), activity(for_user=None), "
153
+ "branch(topic), save(message, topic), handoff(to, topic, body), "
154
+ "notify_plan(recipient, message). notify_plan is proposal-only: "
155
+ "approval and dispatch require an explicit human checkpoint."
156
+ )
@@ -0,0 +1,12 @@
1
+ {
2
+ "generatedBy": "bin/prime-render-spec.mjs",
3
+ "source": "AGENTS.md generated Codex spec",
4
+ "sourceHash": "72d7c3986402",
5
+ "outputHash": "6d17e4953725",
6
+ "adaptations": [
7
+ "Prime Agent APPEND_SYSTEM delivery from .prime/agent/",
8
+ "Prime Agent tool_call branch gate incl. ipython classification",
9
+ "Prime Agent slash-command extension over shared portable skills",
10
+ "rlm() subagents are not Loom lanes; Loom preambles run inline"
11
+ ]
12
+ }