create-egregore 0.19.33 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/egregore-launcher.js +118 -13
- package/bin/cli.js +19 -3
- package/lib/connect-runtime.js +4 -2
- package/lib/local.js +15 -1
- package/lib/prime-runtime.js +208 -0
- package/lib/setup.js +45 -3
- package/package.json +4 -3
- package/runtime/codex/.codex/skills/ingest-notion/SKILL.md +29 -0
- package/runtime/codex/.codex/skills/notion-connect/SKILL.md +29 -0
- package/runtime/codex/.codex/skills/teams-connect/SKILL.md +29 -0
- package/runtime/codex/.codex/spec-manifest.json +3 -3
- package/runtime/codex/AGENTS.md +11 -4
- package/runtime/codex/bin/activity-data.sh +4 -8
- package/runtime/codex/bin/attendant.sh +13 -0
- package/runtime/codex/bin/autosave.sh +27 -1
- package/runtime/codex/bin/codex-render-spec.mjs +6 -0
- package/runtime/codex/bin/codex-session-start.sh +3 -1
- package/runtime/codex/bin/graph-batch.sh +25 -6
- package/runtime/codex/bin/graph-op.sh +5 -4
- package/runtime/codex/bin/graph.sh +31 -5
- package/runtime/codex/bin/ingest.py +81 -5
- package/runtime/codex/bin/lib/context.sh +26 -1
- package/runtime/codex/bin/lib/greeting.sh +4 -1
- package/runtime/codex/bin/lib/worktree-links.sh +25 -0
- package/runtime/codex/manifest.json +7 -1
- package/runtime/pi/.codex/skills/ingest-notion/SKILL.md +29 -0
- package/runtime/pi/.codex/skills/notion-connect/SKILL.md +29 -0
- package/runtime/pi/.codex/skills/teams-connect/SKILL.md +29 -0
- package/runtime/pi/.codex/spec-manifest.json +3 -3
- package/runtime/pi/.pi/APPEND_SYSTEM.md +3 -2
- package/runtime/pi/.pi/extensions/egregore.ts +17 -6
- package/runtime/pi/.pi/spec-manifest.json +2 -2
- package/runtime/pi/AGENTS.md +11 -4
- package/runtime/pi/bin/activity-data.sh +4 -8
- package/runtime/pi/bin/attendant.sh +13 -0
- package/runtime/pi/bin/autosave.sh +27 -1
- package/runtime/pi/bin/codex-render-spec.mjs +6 -0
- package/runtime/pi/bin/codex-session-start.sh +3 -1
- package/runtime/pi/bin/graph-batch.sh +25 -6
- package/runtime/pi/bin/graph-op.sh +5 -4
- package/runtime/pi/bin/graph.sh +31 -5
- package/runtime/pi/bin/ingest.py +81 -5
- package/runtime/pi/bin/lib/context.sh +26 -1
- package/runtime/pi/bin/lib/greeting.sh +4 -1
- package/runtime/pi/bin/lib/worktree-links.sh +25 -0
- package/runtime/pi/bin/pi-branch-policy.mjs +138 -1
- package/runtime/pi/bin/pi-product-workflows.mjs +35 -7
- package/runtime/pi/manifest.json +7 -1
- package/runtime/prime/.claude/context/notification-consent.md +66 -0
- package/runtime/prime/.codex/config.toml +17 -0
- package/runtime/prime/.codex/hooks/branch-guard.js +461 -0
- package/runtime/prime/.codex/hooks/search-hint.js +67 -0
- package/runtime/prime/.codex/hooks.json +30 -0
- package/runtime/prime/.codex/skills/activity/SKILL.md +82 -0
- package/runtime/prime/.codex/skills/announce/SKILL.md +67 -0
- package/runtime/prime/.codex/skills/archive/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/branch/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/checkup/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/commit/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/contribute/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/create-skill/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/dark-mode/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/dashboard/SKILL.md +59 -0
- package/runtime/prime/.codex/skills/deep-reflect/SKILL.md +109 -0
- package/runtime/prime/.codex/skills/delete-user/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/emissary/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/handoff/SKILL.md +166 -0
- package/runtime/prime/.codex/skills/harvest/QUESTION_PALETTE.md +76 -0
- package/runtime/prime/.codex/skills/harvest/SKILL.md +131 -0
- package/runtime/prime/.codex/skills/ingest/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/ingest-notion/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/ingest-user-interview/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/invite/SKILL.md +44 -0
- package/runtime/prime/.codex/skills/issue/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/me/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/meeting/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/note/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/notion-connect/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/onboarding/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/pr/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/pull/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/push/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/quest/SKILL.md +80 -0
- package/runtime/prime/.codex/skills/reflect/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/review-pr/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/save/SKILL.md +125 -0
- package/runtime/prime/.codex/skills/slack-connect/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/summon/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/sync-repos/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/teams-connect/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/telegram-connect/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/telemetry/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/test/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/the-spiral/SKILL.md +84 -0
- package/runtime/prime/.codex/skills/tui-design/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/tutorial/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/update/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/view/SKILL.md +156 -0
- package/runtime/prime/.codex/skills/view/agents/openai.yaml +4 -0
- package/runtime/prime/.codex/skills/view/assets/compose-scaffold.html +221 -0
- package/runtime/prime/.codex/skills/visual-explain/SKILL.md +45 -0
- package/runtime/prime/.codex/skills/wrap/SKILL.md +58 -0
- package/runtime/prime/.codex/spec-manifest.json +132 -0
- package/runtime/prime/.prime/agent/APPEND_SYSTEM.md +393 -0
- package/runtime/prime/.prime/agent/extensions/egregore.ts +460 -0
- package/runtime/prime/.prime/agent/settings.json +4 -0
- package/runtime/prime/.prime/agent/skills/egregore/SKILL.md +34 -0
- package/runtime/prime/.prime/agent/skills/egregore/pyproject.toml +13 -0
- package/runtime/prime/.prime/agent/skills/egregore/src/egregore/__init__.py +156 -0
- package/runtime/prime/.prime/agent/spec-manifest.json +12 -0
- package/runtime/prime/AGENTS.md +489 -0
- package/runtime/prime/bin/activity-data.sh +238 -0
- package/runtime/prime/bin/agent.sh +775 -0
- package/runtime/prime/bin/attendant.sh +707 -0
- package/runtime/prime/bin/autosave.sh +112 -0
- package/runtime/prime/bin/capture-reconcile.sh +69 -0
- package/runtime/prime/bin/capture-run.sh +341 -0
- package/runtime/prime/bin/codex-install-skills.sh +78 -0
- package/runtime/prime/bin/codex-render-spec.mjs +440 -0
- package/runtime/prime/bin/codex-session-start.sh +265 -0
- package/runtime/prime/bin/codex-skill-render.mjs +347 -0
- package/runtime/prime/bin/codex-sync-skills.sh +232 -0
- package/runtime/prime/bin/connect-refresh.sh +33 -0
- package/runtime/prime/bin/dashboard-data.sh +178 -0
- package/runtime/prime/bin/graph-batch.sh +79 -0
- package/runtime/prime/bin/graph-op.sh +1102 -0
- package/runtime/prime/bin/graph-wal.sh +229 -0
- package/runtime/prime/bin/graph.sh +223 -0
- package/runtime/prime/bin/handoff-pr-backfill.sh +86 -0
- package/runtime/prime/bin/handoff-run.sh +671 -0
- package/runtime/prime/bin/handoff-save-egregore.sh +212 -0
- package/runtime/prime/bin/index-handoff.sh +322 -0
- package/runtime/prime/bin/ingest-graph.sh +80 -0
- package/runtime/prime/bin/ingest-surface.html +495 -0
- package/runtime/prime/bin/ingest.py +756 -0
- package/runtime/prime/bin/ingest.sh +279 -0
- package/runtime/prime/bin/ingest_graph.py +856 -0
- package/runtime/prime/bin/ingest_surface.py +544 -0
- package/runtime/prime/bin/lib/artifact-id.sh +46 -0
- package/runtime/prime/bin/lib/config.sh +116 -0
- package/runtime/prime/bin/lib/context.sh +621 -0
- package/runtime/prime/bin/lib/dashboard-artifact.sh +242 -0
- package/runtime/prime/bin/lib/ensure-develop.sh +183 -0
- package/runtime/prime/bin/lib/git-sync.sh +406 -0
- package/runtime/prime/bin/lib/greeting.sh +519 -0
- package/runtime/prime/bin/lib/hash.sh +13 -0
- package/runtime/prime/bin/lib/identity.sh +213 -0
- package/runtime/prime/bin/lib/metrics.sh +374 -0
- package/runtime/prime/bin/lib/noncode.sh +31 -0
- package/runtime/prime/bin/lib/permissions.sh +106 -0
- package/runtime/prime/bin/lib/time.sh +56 -0
- package/runtime/prime/bin/lib/worktree-links.sh +79 -0
- package/runtime/prime/bin/notify.sh +636 -0
- package/runtime/prime/bin/person.py +630 -0
- package/runtime/prime/bin/person.sh +448 -0
- package/runtime/prime/bin/pi-branch-policy.mjs +312 -0
- package/runtime/prime/bin/pi-product-workflows.mjs +394 -0
- package/runtime/prime/bin/prime-render-spec.mjs +123 -0
- package/runtime/prime/bin/prime-session-start.sh +24 -0
- package/runtime/prime/bin/publish-artifact.sh +271 -0
- package/runtime/prime/bin/publish-references.sh +144 -0
- package/runtime/prime/bin/repo-state.sh +96 -0
- package/runtime/prime/bin/search.sh +240 -0
- package/runtime/prime/bin/session-autosave.sh +209 -0
- package/runtime/prime/bin/session-log.sh +199 -0
- package/runtime/prime/bin/settings.sh +304 -0
- package/runtime/prime/bin/telemetry.sh +391 -0
- package/runtime/prime/bin/worktree-create.sh +128 -0
- package/runtime/prime/bin/worktree-remove.sh +50 -0
- package/runtime/prime/bin/worktree.sh +198 -0
- package/runtime/prime/manifest.json +178 -0
|
@@ -0,0 +1,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,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
|
+
}
|