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,312 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
const MUTATING_COMMANDS = /(^|[;&|]\s*)(?:sudo\s+)?(rm|mv|cp|mkdir|rmdir|touch|chmod|chown|install|tee)\b/;
|
|
4
|
+
// Global options (-C <path>, -c k=v, --git-dir=…) may sit between `git` and
|
|
5
|
+
// the subcommand. The repo convention actively teaches `git -C <abs-path>`,
|
|
6
|
+
// so a classifier that only matches bare `git commit` misses the shape the
|
|
7
|
+
// model is most likely to emit.
|
|
8
|
+
const MUTATING_GIT = /(^|[;&|]\s*)(?:sudo\s+)?git\s+(?:(?:-C|-c|--git-dir|--work-tree)(?:=\S+|\s+\S+)\s+)*(add|commit|push|merge|rebase|cherry-pick|tag|rm|mv|reset|restore|clean)\b/;
|
|
9
|
+
const MUTATING_PACKAGES = /(^|[;&|]\s*)(?:sudo\s+)?(npm|pnpm|yarn|bun)\s+(install|add|remove|uninstall|update)\b/;
|
|
10
|
+
|
|
11
|
+
// A `>` only acts as a shell redirect when it sits OUTSIDE quotes. Quoted
|
|
12
|
+
// `>` shows up constantly in read-only commands — Cypher comparisons
|
|
13
|
+
// (`WHERE x >= y`), JS/Python formatters (`f"{a} -> {b}"`), jq filters —
|
|
14
|
+
// so the redirect test runs on a quote-stripped copy of the command.
|
|
15
|
+
// Quoted segments collapse to a placeholder char so a redirect with a
|
|
16
|
+
// quoted TARGET (`echo hi > "/tmp/x"`) is still detected.
|
|
17
|
+
function stripQuotedSegments(command) {
|
|
18
|
+
let out = "";
|
|
19
|
+
let quote = null;
|
|
20
|
+
for (let i = 0; i < command.length; i++) {
|
|
21
|
+
const ch = command[i];
|
|
22
|
+
if (quote) {
|
|
23
|
+
if (quote === '"' && ch === "\\") i++; // skip escaped char inside double quotes
|
|
24
|
+
else if (ch === quote) quote = null;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (ch === "'" || ch === '"') { quote = ch; out += "x"; continue; }
|
|
28
|
+
out += ch;
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function maskQuotedOperators(command) {
|
|
34
|
+
let out = "";
|
|
35
|
+
let quote = null;
|
|
36
|
+
for (let i = 0; i < command.length; i++) {
|
|
37
|
+
const ch = command[i];
|
|
38
|
+
if (quote) {
|
|
39
|
+
if (quote === '"' && ch === "\\" && i + 1 < command.length) {
|
|
40
|
+
out += ch + command[++i];
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (ch === quote) quote = null;
|
|
44
|
+
out += /[><|;&]/.test(ch) ? " " : ch;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (ch === "'" || ch === '"') quote = ch;
|
|
48
|
+
out += ch;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function shellSegments(command) {
|
|
54
|
+
const segments = [];
|
|
55
|
+
let current = "";
|
|
56
|
+
let quote = null;
|
|
57
|
+
for (let i = 0; i < command.length; i++) {
|
|
58
|
+
const ch = command[i];
|
|
59
|
+
if (quote) {
|
|
60
|
+
current += ch;
|
|
61
|
+
if (quote === '"' && ch === "\\" && i + 1 < command.length) current += command[++i];
|
|
62
|
+
else if (ch === quote) quote = null;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (ch === "'" || ch === '"') {
|
|
66
|
+
quote = ch;
|
|
67
|
+
current += ch;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (ch === ";" || ch === "|" || ch === "&" || ch === "\n") {
|
|
71
|
+
if (current.trim()) segments.push(current.trim());
|
|
72
|
+
current = "";
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
current += ch;
|
|
76
|
+
}
|
|
77
|
+
if (current.trim()) segments.push(current.trim());
|
|
78
|
+
return segments;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function shellWords(segment) {
|
|
82
|
+
const words = [];
|
|
83
|
+
let current = "";
|
|
84
|
+
let quote = null;
|
|
85
|
+
for (let i = 0; i < segment.length; i++) {
|
|
86
|
+
const ch = segment[i];
|
|
87
|
+
if (quote) {
|
|
88
|
+
if (quote === '"' && ch === "\\" && i + 1 < segment.length) current += segment[++i];
|
|
89
|
+
else if (ch === quote) quote = null;
|
|
90
|
+
else current += ch;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (ch === "'" || ch === '"') {
|
|
94
|
+
quote = ch;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (/\s/.test(ch)) {
|
|
98
|
+
if (current) words.push(current);
|
|
99
|
+
current = "";
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (ch === "\\" && i + 1 < segment.length) current += segment[++i];
|
|
103
|
+
else current += ch;
|
|
104
|
+
}
|
|
105
|
+
if (current) words.push(current);
|
|
106
|
+
return words;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function isRuntimeStatePath(target, projectDir) {
|
|
110
|
+
if (!target || /[$`*?{}]/.test(target)) return false;
|
|
111
|
+
const absolute = isAbsolute(target) ? resolve(target) : resolve(projectDir, target);
|
|
112
|
+
const rel = relative(resolve(projectDir), absolute);
|
|
113
|
+
if (rel.startsWith("..") || isAbsolute(rel)) return true;
|
|
114
|
+
if (rel === ".claude/worktrees" || rel.startsWith(".claude/worktrees/")) return false;
|
|
115
|
+
return rel === "memory" || rel.startsWith("memory/") ||
|
|
116
|
+
rel === ".claude" || rel.startsWith(".claude/") ||
|
|
117
|
+
rel === ".codex" || rel.startsWith(".codex/") ||
|
|
118
|
+
rel === ".pi" || rel.startsWith(".pi/") ||
|
|
119
|
+
rel === ".prime" || rel.startsWith(".prime/") ||
|
|
120
|
+
rel === ".egregore-state.json" ||
|
|
121
|
+
rel === ".egregore-branch-consent" ||
|
|
122
|
+
rel === ".env";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function mutatesOnlyRuntimeState(command, projectDir) {
|
|
126
|
+
if (!isMutatingBash(command)) return false;
|
|
127
|
+
const visible = stripQuotedSegments(String(command));
|
|
128
|
+
if (MUTATING_GIT.test(visible) || MUTATING_PACKAGES.test(visible) ||
|
|
129
|
+
/\bsed\s+[^\n]*\s-i(?:\s|$)/.test(visible)) return false;
|
|
130
|
+
|
|
131
|
+
let foundTarget = false;
|
|
132
|
+
const pathCommands = new Set(["rm", "mv", "cp", "mkdir", "rmdir", "touch", "chmod", "chown", "install", "tee"]);
|
|
133
|
+
for (const segment of shellSegments(String(command))) {
|
|
134
|
+
const words = shellWords(segment);
|
|
135
|
+
while (words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[0])) words.shift();
|
|
136
|
+
while (words[0] === "sudo" || words[0] === "command" || words[0] === "env") words.shift();
|
|
137
|
+
if (!words.length) continue;
|
|
138
|
+
const name = words.shift().split("/").pop();
|
|
139
|
+
if (!pathCommands.has(name)) continue;
|
|
140
|
+
|
|
141
|
+
const args = words.filter((word) => !word.startsWith("-"));
|
|
142
|
+
let targets = args;
|
|
143
|
+
if (name === "cp" || name === "install") targets = args.length ? [args[args.length - 1]] : [];
|
|
144
|
+
if (name === "chmod" || name === "chown") targets = args.slice(1);
|
|
145
|
+
if (!targets.length || targets.some((target) => !isRuntimeStatePath(target, projectDir))) return false;
|
|
146
|
+
foundTarget = true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const redirectSource = maskQuotedOperators(String(command));
|
|
150
|
+
const redirects = /(?:^|[\s;|&])(?:\d*)>>?\s*([^&\s;|]+)/g;
|
|
151
|
+
let redirect;
|
|
152
|
+
while ((redirect = redirects.exec(redirectSource))) {
|
|
153
|
+
const target = String(redirect[1] || "").replace(/^["']|["']$/g, "");
|
|
154
|
+
if (!target || target === "/dev/null") continue;
|
|
155
|
+
if (!isRuntimeStatePath(target, projectDir)) return false;
|
|
156
|
+
foundTarget = true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const tees = /\|\s*tee\s+(?:-a\s+)?([^&\s;|]+)/g;
|
|
160
|
+
let tee;
|
|
161
|
+
while ((tee = tees.exec(redirectSource))) {
|
|
162
|
+
const target = String(tee[1] || "").replace(/^["']|["']$/g, "");
|
|
163
|
+
if (!isRuntimeStatePath(target, projectDir)) return false;
|
|
164
|
+
foundTarget = true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return foundTarget;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// The documented escape hatch must itself be runnable, but ONLY as the whole
|
|
171
|
+
// command: an unanchored match lets `git push origin develop && echo 'develop'
|
|
172
|
+
// > .egregore-branch-consent` pass the gate in one call, and the block message
|
|
173
|
+
// teaches that exact echo string. Canonical form mirrors
|
|
174
|
+
// .codex/hooks/branch-guard.js isConsentCommand — anchored, branch escaped,
|
|
175
|
+
// optional ./ prefix — and is shared so the three runtimes cannot drift.
|
|
176
|
+
export function isConsentCommand(command, branch) {
|
|
177
|
+
if (!branch) return false;
|
|
178
|
+
const escaped = String(branch).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
179
|
+
return new RegExp(
|
|
180
|
+
`^\\s*echo\\s+['"]?${escaped}['"]?\\s*>\\s*(?:\\./)?\\.egregore-branch-consent\\s*$`
|
|
181
|
+
).test(String(command || ""));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function isMutatingBash(command) {
|
|
185
|
+
if (!command) return false;
|
|
186
|
+
const meaningfulRedirects = stripQuotedSegments(String(command))
|
|
187
|
+
.replace(/(?:^|\s)\d*>>?\s*\/dev\/null\b/g, " ")
|
|
188
|
+
.replace(/(?:^|\s)\d*>\s*&\d+\b/g, " ");
|
|
189
|
+
return MUTATING_COMMANDS.test(command) ||
|
|
190
|
+
MUTATING_GIT.test(command) ||
|
|
191
|
+
MUTATING_PACKAGES.test(command) ||
|
|
192
|
+
/(^|[^<])>>?\s*[^&]/.test(meaningfulRedirects) ||
|
|
193
|
+
/\bsed\s+[^\n]*\s-i(?:\s|$)/.test(command);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---- IPython code classification (Prime Agent) -----------------------------
|
|
197
|
+
// Prime Agent's default tool surface is a persistent IPython kernel: file
|
|
198
|
+
// writes and shell escapes arrive as Python code, not bash tool calls. This
|
|
199
|
+
// classifier is deliberately static and therefore porous (exec/eval and
|
|
200
|
+
// string-built commands pass); the generated runtime spec carries the
|
|
201
|
+
// standing-instruction discipline as the backstop, and the adversarial test
|
|
202
|
+
// fixtures pin what this actually catches.
|
|
203
|
+
|
|
204
|
+
// Strip Python string literals so call-name detection ignores docstrings and
|
|
205
|
+
// string content. Triple quotes first, then single-line quotes; f-string
|
|
206
|
+
// prefixes fall out naturally because only the quoted part is replaced.
|
|
207
|
+
function stripPythonStrings(code) {
|
|
208
|
+
return String(code)
|
|
209
|
+
.replace(/[rbuf]{0,2}("""|''')[\s\S]*?\1/gi, '""')
|
|
210
|
+
.replace(/[rbuf]{0,2}"(?:\\.|[^"\\\n])*"/gi, '""')
|
|
211
|
+
.replace(/[rbuf]{0,2}'(?:\\.|[^'\\\n])*'/gi, "''");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function pythonShellCommands(code) {
|
|
215
|
+
const shell = [];
|
|
216
|
+
const lines = String(code).split("\n");
|
|
217
|
+
const first = (lines[0] || "").trim();
|
|
218
|
+
if (/^%%(bash|sh|script\s+(ba)?sh)\b/.test(first)) {
|
|
219
|
+
shell.push(lines.slice(1).join("\n"));
|
|
220
|
+
return { shell, rest: "" };
|
|
221
|
+
}
|
|
222
|
+
if (/^%%writefile\b/.test(first)) {
|
|
223
|
+
const target = first.replace(/^%%writefile\s+(-a\s+)?/, "").trim();
|
|
224
|
+
return { shell: [`tee ${target || "unknown-target"}`], rest: "" };
|
|
225
|
+
}
|
|
226
|
+
const rest = [];
|
|
227
|
+
for (const line of lines) {
|
|
228
|
+
const bang = line.match(/^\s*!(.+)$/);
|
|
229
|
+
const sysMagic = line.match(/^\s*%(?:system|sx)\s+(.+)$/);
|
|
230
|
+
if (bang) shell.push(bang[1]);
|
|
231
|
+
else if (sysMagic) shell.push(sysMagic[1]);
|
|
232
|
+
else rest.push(line);
|
|
233
|
+
}
|
|
234
|
+
return { shell, rest: rest.join("\n") };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const PY_MUTATING_CALLS =
|
|
238
|
+
/\b(?:os\.(?:remove|unlink|rename|replace|rmdir|removedirs|makedirs|mkdir|symlink|link|chmod|chown|truncate)|shutil\.(?:copy|copy2|copyfile|copytree|move|rmtree|make_archive|unpack_archive))\s*\(/;
|
|
239
|
+
const PY_WRITE_METHODS = /\.\s*(?:write_text|write_bytes|unlink|touch|mkdir|rmdir|hardlink_to|symlink_to)\s*\(/;
|
|
240
|
+
const PY_OPEN_WRITE =
|
|
241
|
+
/\bopen\s*\(\s*[^,]+,\s*(?:mode\s*=\s*)?['"](?:[wax]|r\+)[rwxab+]*['"]/;
|
|
242
|
+
const PY_OPEN_MODE_KWARG = /\bopen\s*\([^)]*mode\s*=\s*['"](?:[wax]|r\+)[rwxab+]*['"]/;
|
|
243
|
+
|
|
244
|
+
function pythonSubprocessArgv(code) {
|
|
245
|
+
const argvs = [];
|
|
246
|
+
const calls =
|
|
247
|
+
/\b(?:subprocess\.(?:run|call|check_call|check_output|Popen)|os\.system|os\.popen)\s*\(\s*(\[[^\]]*\]|[rbuf]{0,2}["'](?:\\.|[^"'\\\n])*["'])/g;
|
|
248
|
+
let match;
|
|
249
|
+
while ((match = calls.exec(String(code)))) {
|
|
250
|
+
const arg = match[1];
|
|
251
|
+
if (arg.startsWith("[")) {
|
|
252
|
+
const words = [...arg.matchAll(/["']((?:\\.|[^"'\\])*)["']/g)].map((m) => m[1]);
|
|
253
|
+
if (words.length) argvs.push(words.join(" "));
|
|
254
|
+
} else {
|
|
255
|
+
argvs.push(arg.replace(/^[rbuf]{0,2}["']/i, "").replace(/["']$/, ""));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return argvs;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Classify IPython cell code. Returns true when the code statically performs a
|
|
262
|
+
// repo mutation that should meet the protected-branch gate. Mutations that
|
|
263
|
+
// target only Egregore runtime state (memory/, dot-dirs, consent files) are
|
|
264
|
+
// exempt when the target is statically extractable, mirroring
|
|
265
|
+
// mutatesOnlyRuntimeState for bash.
|
|
266
|
+
export function isMutatingIpython(code, projectDir) {
|
|
267
|
+
if (!code) return false;
|
|
268
|
+
const { shell, rest } = pythonShellCommands(code);
|
|
269
|
+
for (const cmd of shell) {
|
|
270
|
+
if (isMutatingBash(cmd) && !(projectDir && mutatesOnlyRuntimeState(cmd, projectDir))) return true;
|
|
271
|
+
}
|
|
272
|
+
if (!rest) return false;
|
|
273
|
+
|
|
274
|
+
for (const argv of pythonSubprocessArgv(rest)) {
|
|
275
|
+
if (isMutatingBash(argv) && !(projectDir && mutatesOnlyRuntimeState(argv, projectDir))) return true;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const stripped = stripPythonStrings(rest);
|
|
279
|
+
if (PY_MUTATING_CALLS.test(stripped) || PY_WRITE_METHODS.test(stripped)) {
|
|
280
|
+
// Exempt when every extractable string target is runtime state. The
|
|
281
|
+
// mutated location differs by call: copy/move/rename mutate their LAST
|
|
282
|
+
// argument (the destination); remove/rmtree/mkdir mutate their first.
|
|
283
|
+
if (projectDir) {
|
|
284
|
+
const DEST_LAST = /^(?:copy|copy2|copyfile|copytree|move|rename|replace|symlink|link)$/;
|
|
285
|
+
const targets = [...String(rest).matchAll(
|
|
286
|
+
/\b(?:os|shutil)\.(\w+)\s*\(([^)]*)\)/g
|
|
287
|
+
)].flatMap((m) => {
|
|
288
|
+
const args = [...m[2].matchAll(/["']((?:\\.|[^"'\\])*)["']/g)].map((s) => s[1]);
|
|
289
|
+
if (!args.length) return [];
|
|
290
|
+
return [DEST_LAST.test(m[1]) ? args[args.length - 1] : args[0]];
|
|
291
|
+
});
|
|
292
|
+
const pathTargets = [...String(rest).matchAll(
|
|
293
|
+
/Path\s*\(\s*[rbuf]{0,2}["']((?:\\.|[^"'\\\n])*)["']\s*\)\s*\.\s*(?:write_text|write_bytes|unlink|touch|mkdir|rmdir)/g
|
|
294
|
+
)].map((m) => m[1]);
|
|
295
|
+
const all = [...targets, ...pathTargets];
|
|
296
|
+
if (all.length && all.every((t) => isRuntimeStatePath(t, projectDir))) return false;
|
|
297
|
+
}
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (PY_OPEN_WRITE.test(rest) || PY_OPEN_MODE_KWARG.test(rest)) {
|
|
302
|
+
if (projectDir) {
|
|
303
|
+
const openTargets = [...String(rest).matchAll(
|
|
304
|
+
/\bopen\s*\(\s*[rbuf]{0,2}["']((?:\\.|[^"'\\\n])*)["']\s*,\s*(?:mode\s*=\s*)?["'](?:[wax]|r\+)/g
|
|
305
|
+
)].map((m) => m[1]);
|
|
306
|
+
if (openTargets.length && openTargets.every((t) => isRuntimeStatePath(t, projectDir))) return false;
|
|
307
|
+
}
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return false;
|
|
312
|
+
}
|