pi-orchestrator-config 4.1.5
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/README.md +808 -0
- package/agents/api-documenter.md +32 -0
- package/agents/bash-expert.md +53 -0
- package/agents/code-reviewer-docs.md +145 -0
- package/agents/code-reviewer-guidelines.md +97 -0
- package/agents/code-reviewer-quality.md +142 -0
- package/agents/code-reviewer-security.md +107 -0
- package/agents/code-reviewer-spec.md +164 -0
- package/agents/debugger.md +63 -0
- package/agents/docker-expert.md +40 -0
- package/agents/docs-fetcher.md +48 -0
- package/agents/git-expert.md +63 -0
- package/agents/github-expert.md +83 -0
- package/agents/go-expert.md +62 -0
- package/agents/issue-reviewer-feasibility.md +60 -0
- package/agents/issue-reviewer-scope.md +63 -0
- package/agents/issue-reviewer-spec.md +77 -0
- package/agents/java-expert.md +31 -0
- package/agents/jenkins-expert.md +38 -0
- package/agents/kubernetes-expert.md +40 -0
- package/agents/planner.md +42 -0
- package/agents/python-expert.md +61 -0
- package/agents/reviewer.md +26 -0
- package/agents/scout.md +40 -0
- package/agents/security-auditor.md +254 -0
- package/agents/technical-documentation-writer.md +37 -0
- package/agents/test-automator.md +55 -0
- package/agents/test-runner.md +47 -0
- package/agents/ts-expert.md +23 -0
- package/agents/worker.md +30 -0
- package/extensions/acpx-provider/index.ts +104 -0
- package/extensions/acpx-provider/load-runtime.ts +99 -0
- package/extensions/acpx-provider/runtime-models.ts +40 -0
- package/extensions/cli-provider/agents/claude.ts +128 -0
- package/extensions/cli-provider/agents/cursor.ts +84 -0
- package/extensions/cli-provider/agents/gemini.ts +100 -0
- package/extensions/cli-provider/discover.ts +77 -0
- package/extensions/cli-provider/index.ts +57 -0
- package/extensions/cli-provider/parsers.ts +304 -0
- package/extensions/cli-provider/providers.ts +58 -0
- package/extensions/cli-provider/runner.ts +225 -0
- package/extensions/cli-provider/runtime-models.ts +32 -0
- package/extensions/cli-provider/session-reaper.ts +122 -0
- package/extensions/cli-provider/sessions.ts +533 -0
- package/extensions/cli-provider/shared/discover-cache.ts +235 -0
- package/extensions/cli-provider/types.ts +48 -0
- package/extensions/coms/coms-net-server.ts +1642 -0
- package/extensions/coms/coms-net-wrapper.ts +521 -0
- package/extensions/coms/coms-net.ts +1589 -0
- package/extensions/coms/coms-p2p.ts +1879 -0
- package/extensions/coms/coms-shared.ts +510 -0
- package/extensions/coms/coms-wrapper.ts +138 -0
- package/extensions/coms/index.ts +20 -0
- package/extensions/coms/themeMap.ts +147 -0
- package/extensions/image-gen/image-gen.ts +332 -0
- package/extensions/image-gen/index.ts +14 -0
- package/extensions/orchestrator/agents.ts +134 -0
- package/extensions/orchestrator/ask-user.ts +293 -0
- package/extensions/orchestrator/async-agents.ts +1037 -0
- package/extensions/orchestrator/async-capability.ts +129 -0
- package/extensions/orchestrator/async-runner.ts +324 -0
- package/extensions/orchestrator/async-status-parse.ts +31 -0
- package/extensions/orchestrator/async-status-ui.ts +201 -0
- package/extensions/orchestrator/async-wait.ts +36 -0
- package/extensions/orchestrator/btw.ts +210 -0
- package/extensions/orchestrator/cron-status-format.ts +126 -0
- package/extensions/orchestrator/cron-status-ui.ts +132 -0
- package/extensions/orchestrator/cron.ts +730 -0
- package/extensions/orchestrator/dreaming.ts +355 -0
- package/extensions/orchestrator/enforcement-helpers.ts +530 -0
- package/extensions/orchestrator/enforcement-rules.ts +329 -0
- package/extensions/orchestrator/enforcement.ts +828 -0
- package/extensions/orchestrator/extended-autocomplete.ts +593 -0
- package/extensions/orchestrator/git-helpers.ts +420 -0
- package/extensions/orchestrator/github-autocomplete.ts +250 -0
- package/extensions/orchestrator/icons.ts +17 -0
- package/extensions/orchestrator/index.ts +100 -0
- package/extensions/orchestrator/memory-embeddings.ts +281 -0
- package/extensions/orchestrator/memory-promotion.ts +449 -0
- package/extensions/orchestrator/memory-provenance.ts +80 -0
- package/extensions/orchestrator/memory-query-class.ts +90 -0
- package/extensions/orchestrator/memory-scoring.ts +434 -0
- package/extensions/orchestrator/memory-tools.ts +883 -0
- package/extensions/orchestrator/memory-tree.ts +212 -0
- package/extensions/orchestrator/nvim.ts +169 -0
- package/extensions/orchestrator/overlay-dashboard-utils.ts +25 -0
- package/extensions/orchestrator/overlay-dashboard.ts +512 -0
- package/extensions/orchestrator/pi-config-review-state.ts +303 -0
- package/extensions/orchestrator/preference-extractor.ts +106 -0
- package/extensions/orchestrator/project-settings.ts +468 -0
- package/extensions/orchestrator/promotion-queue.ts +311 -0
- package/extensions/orchestrator/resolve-agent-model.ts +32 -0
- package/extensions/orchestrator/review-ui.ts +204 -0
- package/extensions/orchestrator/rule-placeholders.ts +12 -0
- package/extensions/orchestrator/rules.ts +481 -0
- package/extensions/orchestrator/session-search.ts +243 -0
- package/extensions/orchestrator/session-validation.ts +374 -0
- package/extensions/orchestrator/situation-report.ts +370 -0
- package/extensions/orchestrator/status-bar.ts +31 -0
- package/extensions/orchestrator/status-line.ts +192 -0
- package/extensions/orchestrator/status.ts +110 -0
- package/extensions/orchestrator/subagent-tool.ts +1748 -0
- package/extensions/orchestrator/task-lifecycle.ts +88 -0
- package/extensions/orchestrator/utils.ts +227 -0
- package/extensions/pidash/index.ts +15 -0
- package/extensions/pidash/pidash.ts +941 -0
- package/extensions/pidiff/index.ts +13 -0
- package/extensions/pidiff/pidiff.ts +369 -0
- package/extensions/providers/acpx-driver.ts +479 -0
- package/extensions/providers/built-in-drivers.ts +52 -0
- package/extensions/providers/claude-driver.ts +319 -0
- package/extensions/providers/cursor-acpx-driver.ts +398 -0
- package/extensions/providers/cursor-cli-driver.ts +484 -0
- package/extensions/providers/gemini-driver.ts +302 -0
- package/extensions/providers/index.ts +513 -0
- package/extensions/shared/build-system-prompt.ts +57 -0
- package/extensions/shared/create-runtime-provider.ts +190 -0
- package/extensions/shared/daemon-manager.ts +248 -0
- package/extensions/shared/file-logger.ts +115 -0
- package/extensions/shared/managed-refresh.ts +211 -0
- package/extensions/shared/provider-driver.ts +295 -0
- package/extensions/shared/provider-errors.ts +305 -0
- package/extensions/shared/provider-registry.ts +388 -0
- package/extensions/shared/stream-builder.ts +275 -0
- package/extensions/shared/types.ts +20 -0
- package/extensions/shared/ui/README.md +10 -0
- package/extensions/shared/ui/badge.tsx +52 -0
- package/extensions/shared/ui/button.tsx +58 -0
- package/extensions/shared/ui/collapsible.tsx +21 -0
- package/extensions/shared/ui/dropdown-menu.tsx +266 -0
- package/extensions/shared/ui/input.tsx +20 -0
- package/extensions/shared/ui/popover.tsx +114 -0
- package/extensions/shared/ui/resizable.tsx +48 -0
- package/extensions/shared/ui/scroll-area.tsx +53 -0
- package/extensions/shared/ui/select.tsx +201 -0
- package/extensions/shared/ui/separator.tsx +25 -0
- package/extensions/shared/ui/tooltip.tsx +64 -0
- package/extensions/shared/ws-client.ts +74 -0
- package/package.json +59 -0
- package/prompts/coderabbit-rate-limit.md +95 -0
- package/prompts/create-coms-feature-manager.md +186 -0
- package/prompts/create-skill.md +128 -0
- package/prompts/domain-model.md +109 -0
- package/prompts/external-ai.md +665 -0
- package/prompts/implement-and-review.md +25 -0
- package/prompts/implement.md +26 -0
- package/prompts/issue-review.md +256 -0
- package/prompts/pr-review.md +753 -0
- package/prompts/query-db.md +154 -0
- package/prompts/refine-review.md +208 -0
- package/prompts/release.md +235 -0
- package/prompts/remember.md +33 -0
- package/prompts/review-handler-status.md +27 -0
- package/prompts/review-handler.md +677 -0
- package/prompts/review-local.md +159 -0
- package/prompts/scout-and-plan.md +21 -0
- package/rules/00-orchestrator-core.md +42 -0
- package/rules/05-issue-first-workflow.md +119 -0
- package/rules/10-agent-routing.md +58 -0
- package/rules/15-mcp-launchpad.md +46 -0
- package/rules/20-code-review-loop.md +234 -0
- package/rules/25-documentation-updates.md +15 -0
- package/rules/30-prompt-templates.md +19 -0
- package/rules/35-memory.md +240 -0
- package/rules/40-critical-rules.md +124 -0
- package/rules/45-file-preview.md +29 -0
- package/rules/50-agent-bug-reporting.md +90 -0
- package/rules/55-coms-protocol.md +63 -0
- package/rules/60-task-tracking.md +66 -0
- package/skills/conflict-resolver/SKILL.md +213 -0
- package/skills/skill-creator/SKILL.md +85 -0
- package/skills/systematic-debugging/SKILL.md +353 -0
- package/skills/test-driven-development/SKILL.md +335 -0
- package/skills/update-agents-md/SKILL.md +282 -0
- package/templates/coms-feature-manager-prompt.md +313 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule & memory injection — loads rules/*.md for the orchestrator
|
|
3
|
+
* and project memories from topic files for all agents.
|
|
4
|
+
*
|
|
5
|
+
* Includes: social closer gate, session history auto-injection,
|
|
6
|
+
* retrieval telemetry, and vector-based contextual memory injection.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as os from "node:os";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { formatDuration } from "./async-agents.js";
|
|
14
|
+
import { buildSituationReport, estimateMemoryBudget, rebuildAndOrganize } from "./situation-report.js";
|
|
15
|
+
import { classifyQueryClass, getQueryClassBias } from "./memory-query-class.js";
|
|
16
|
+
import { getSetting } from "./project-settings.js";
|
|
17
|
+
import { substituteRulePlaceholders } from "./rule-placeholders.js";
|
|
18
|
+
import { getProjectDataDir } from "./utils.js";
|
|
19
|
+
|
|
20
|
+
/** Social closer gate — skip expensive vector search for trivial messages */
|
|
21
|
+
const SOCIAL_CLOSERS = new Set([
|
|
22
|
+
"ok", "yes", "no", "thanks", "thank you", "got it", "sure",
|
|
23
|
+
"right", "correct", "agreed", "nice", "cool", "great", "perfect",
|
|
24
|
+
"👍", "👌", "✅", "🙏", "😊", "🎉", "💯",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
function isSocialCloser(text: string): boolean {
|
|
28
|
+
const stripped = text.trim().toLowerCase();
|
|
29
|
+
if (SOCIAL_CLOSERS.has(stripped)) return true;
|
|
30
|
+
// Emoji-only messages (no alphanumeric, no non-Latin scripts like CJK/Cyrillic)
|
|
31
|
+
if (stripped.length > 0 && stripped.length <= 8 && !/[a-z0-9\u00C0-\u024F\u0400-\u04FF\u4E00-\u9FFF\u3040-\u30FF\uAC00-\uD7AF]/.test(stripped)) return true;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Track last injected memories for usage detection in turn_end */
|
|
36
|
+
let lastInjectedMemories: { text: string; category: string; similarity: number }[] = [];
|
|
37
|
+
|
|
38
|
+
// Track the user message ID that last triggered task-focus enforcement.
|
|
39
|
+
// Don't re-fire for the same user message (prevents loops from followUp turns).
|
|
40
|
+
let lastTaskFocusUserMsgId: string | null = null;
|
|
41
|
+
let taskFocusPending = false;
|
|
42
|
+
let taskFocusPendingMsgId: string | null = null;
|
|
43
|
+
|
|
44
|
+
/** Log what memories were auto-injected for retrieval telemetry */
|
|
45
|
+
function logMemoryInjection(
|
|
46
|
+
cwd: string,
|
|
47
|
+
prompt: string,
|
|
48
|
+
injected: { text: string; category: string; similarity: number }[],
|
|
49
|
+
queryClass?: string,
|
|
50
|
+
): void {
|
|
51
|
+
try {
|
|
52
|
+
const telemetryPath = path.join(getProjectDataDir(cwd), "memory-telemetry.jsonl");
|
|
53
|
+
const entry = JSON.stringify({
|
|
54
|
+
ts: new Date().toISOString(),
|
|
55
|
+
prompt: prompt.slice(0, 200),
|
|
56
|
+
queryClass: queryClass ?? "general",
|
|
57
|
+
injected: injected.map(m => ({ text: m.text.slice(0, 100), category: m.category, similarity: m.similarity })),
|
|
58
|
+
});
|
|
59
|
+
fs.appendFileSync(telemetryPath, entry + "\n", "utf-8");
|
|
60
|
+
// Cap file at 500KB
|
|
61
|
+
const stat = fs.statSync(telemetryPath);
|
|
62
|
+
if (stat.size > 512000) {
|
|
63
|
+
const lines = fs.readFileSync(telemetryPath, "utf-8").split("\n").filter(Boolean);
|
|
64
|
+
fs.writeFileSync(telemetryPath, lines.slice(-200).join("\n") + "\n", "utf-8");
|
|
65
|
+
}
|
|
66
|
+
} catch (e: any) { console.debug("[rules] telemetry write failed:", e?.message?.slice(0, 100)); }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Log whether injected memories were referenced in the LLM response */
|
|
70
|
+
function logMemoryUsage(cwd: string, injected: { text: string; category: string; similarity: number }[], used: { text: string; category: string; similarity: number }[]): void {
|
|
71
|
+
try {
|
|
72
|
+
const telemetryPath = path.join(getProjectDataDir(cwd), "memory-telemetry.jsonl");
|
|
73
|
+
const entry = JSON.stringify({
|
|
74
|
+
ts: new Date().toISOString(),
|
|
75
|
+
event: "usage",
|
|
76
|
+
injectedCount: injected.length,
|
|
77
|
+
usedCount: used.length,
|
|
78
|
+
usageRate: injected.length > 0 ? +(used.length / injected.length).toFixed(2) : 0,
|
|
79
|
+
used: used.map(m => ({ text: m.text.slice(0, 100), category: m.category })),
|
|
80
|
+
});
|
|
81
|
+
fs.appendFileSync(telemetryPath, entry + "\n", "utf-8");
|
|
82
|
+
} catch (e: any) { console.debug("[rules] telemetry usage write failed:", e?.message?.slice(0, 100)); }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function registerRules(
|
|
86
|
+
pi: ExtensionAPI,
|
|
87
|
+
getAsyncJobs?: () => Array<{ id: string; agent: string; name?: string; task: string; status: string; startedAt: number }>,
|
|
88
|
+
): void {
|
|
89
|
+
const isSubagent = process.env.PI_SUBAGENT_CHILD === "1";
|
|
90
|
+
let rebuildDone = false;
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
// Run full rebuild on session start (once per session, not every turn)
|
|
94
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
95
|
+
rebuildDone = false;
|
|
96
|
+
lastTaskFocusUserMsgId = null;
|
|
97
|
+
taskFocusPending = false;
|
|
98
|
+
taskFocusPendingMsgId = null;
|
|
99
|
+
try {
|
|
100
|
+
rebuildAndOrganize(ctx.cwd);
|
|
101
|
+
rebuildDone = true;
|
|
102
|
+
} catch (e: any) { console.debug("[rules] rebuildAndOrganize on session_start failed:", e?.message || e); }
|
|
103
|
+
|
|
104
|
+
// Bootstrap vector embeddings — embed missing entries only (orchestrator only)
|
|
105
|
+
// Skips model init entirely if all entries are already embedded
|
|
106
|
+
if (!isSubagent) {
|
|
107
|
+
try {
|
|
108
|
+
const { readAllTopicEntries } = await import("./memory-tree.js");
|
|
109
|
+
const entries = readAllTopicEntries(ctx.cwd);
|
|
110
|
+
if (entries.length > 0) {
|
|
111
|
+
const { embedMissing } = await import("./memory-embeddings.js");
|
|
112
|
+
// embedMissing checks store first — only inits model if entries are actually missing
|
|
113
|
+
await embedMissing(ctx.cwd, entries);
|
|
114
|
+
}
|
|
115
|
+
} catch (e: any) { console.debug("[rules] embedding bootstrap failed:", e?.message?.slice(0, 100)); }
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
120
|
+
// Run rebuild on first agent start if session_start didn't fire yet
|
|
121
|
+
if (!rebuildDone) {
|
|
122
|
+
try {
|
|
123
|
+
rebuildAndOrganize(ctx.cwd);
|
|
124
|
+
rebuildDone = true;
|
|
125
|
+
} catch (e: any) { console.debug("[rules] rebuildAndOrganize on agent_start failed:", e?.message || e); }
|
|
126
|
+
}
|
|
127
|
+
let extra = "";
|
|
128
|
+
|
|
129
|
+
// Orchestrator rules — skip for specialist agents
|
|
130
|
+
// Load from: package rules/ → user ~/.pi/agent/rules/ → project .pi/rules/
|
|
131
|
+
// Same-filename override: project > user > package
|
|
132
|
+
if (!isSubagent) {
|
|
133
|
+
const packageRulesDir = path.resolve(__dirname, "..", "..", "rules");
|
|
134
|
+
const userRulesDir = path.join(os.homedir(), ".pi", "agent", "rules");
|
|
135
|
+
const projectRulesDir = path.join(ctx.cwd, ".pi", "rules");
|
|
136
|
+
|
|
137
|
+
// Collect rules from all layers — later layers override same-filename entries
|
|
138
|
+
const ruleCandidates = new Map<string, string[]>(); // filename → paths in precedence order
|
|
139
|
+
for (const dir of [packageRulesDir, userRulesDir, projectRulesDir]) {
|
|
140
|
+
try {
|
|
141
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
142
|
+
if ((!entry.isFile() && !entry.isSymbolicLink()) || !entry.name.endsWith(".md")) continue;
|
|
143
|
+
const f = entry.name;
|
|
144
|
+
const existing = ruleCandidates.get(f) || [];
|
|
145
|
+
existing.push(path.join(dir, f));
|
|
146
|
+
ruleCandidates.set(f, existing);
|
|
147
|
+
}
|
|
148
|
+
} catch (e: any) {
|
|
149
|
+
if (e?.code !== "ENOENT") console.debug("[rules] failed to read", dir, e?.message?.slice(0, 100));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (ruleCandidates.size > 0) {
|
|
154
|
+
const sorted = [...ruleCandidates.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
155
|
+
const ruleContents: string[] = [];
|
|
156
|
+
for (const [fileName, candidates] of sorted) {
|
|
157
|
+
// Try from highest precedence (last) to lowest (first)
|
|
158
|
+
let loaded = false;
|
|
159
|
+
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
160
|
+
try {
|
|
161
|
+
const stat = fs.statSync(candidates[i]);
|
|
162
|
+
if (stat.size > 128 * 1024) {
|
|
163
|
+
console.debug(`[rules] ${fileName} skipped (${Math.round(stat.size / 1024)}KB > 128KB limit)`);
|
|
164
|
+
loaded = true; // Mark as loaded to prevent lower-precedence fallback
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
ruleContents.push(fs.readFileSync(candidates[i], "utf-8"));
|
|
168
|
+
loaded = true;
|
|
169
|
+
break;
|
|
170
|
+
} catch (e: any) {
|
|
171
|
+
console.debug(`[rules] ${fileName} failed from ${candidates[i]}:`, e?.message?.slice(0, 100));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (!loaded) {
|
|
175
|
+
console.debug(`[rules] ${fileName}: all candidates failed, skipping`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (ruleContents.length > 0) {
|
|
179
|
+
const reviewLoopMaxCycles = getSetting(ctx.cwd, "review_loop_max_cycles");
|
|
180
|
+
const joined = substituteRulePlaceholders(ruleContents.join("\n\n"), { reviewLoopMaxCycles });
|
|
181
|
+
extra += "\n\n" + joined;
|
|
182
|
+
} else {
|
|
183
|
+
extra +=
|
|
184
|
+
"\n\n[ORCHESTRATOR RULES] You are a MANAGER. Delegate work to subagents.\n";
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
extra +=
|
|
188
|
+
"\n\n[ORCHESTRATOR RULES] You are a MANAGER. Delegate work to subagents.\n";
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Async agent status — always show so the AI knows what's running
|
|
193
|
+
if (!isSubagent && getAsyncJobs) {
|
|
194
|
+
const jobs = getAsyncJobs();
|
|
195
|
+
if (jobs.length === 0) {
|
|
196
|
+
extra += "\n\n# Async Agents Status\n\nNo async agents running.\n";
|
|
197
|
+
} else {
|
|
198
|
+
let status = `\n\n# Async Agents Status\n\n${jobs.length} async agent(s) running — kill any that are no longer needed:\n`;
|
|
199
|
+
for (const j of jobs) {
|
|
200
|
+
const elapsed = formatDuration(Date.now() - j.startedAt);
|
|
201
|
+
status += `- ${j.name || j.agent} (${j.agent}) — ${elapsed}\n`;
|
|
202
|
+
}
|
|
203
|
+
extra += status;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Project memories — situation report (scored, token-budgeted) injected BEFORE rules
|
|
208
|
+
// systemPrompt.length is chars; estimateMemoryBudget converts internally
|
|
209
|
+
const budget = estimateMemoryBudget(event.systemPrompt?.length ?? 0);
|
|
210
|
+
const queryClass =
|
|
211
|
+
!isSubagent && event.prompt && !isSocialCloser(event.prompt)
|
|
212
|
+
? classifyQueryClass(event.prompt)
|
|
213
|
+
: "general";
|
|
214
|
+
const memories = loadMemoriesWithScoring(ctx.cwd, isSubagent, budget, queryClass);
|
|
215
|
+
|
|
216
|
+
// Auto-inject contextually relevant memories via vector search (~2.5ms, in-process)
|
|
217
|
+
const shouldSearch = !isSubagent && !!event.prompt && !isSocialCloser(event.prompt);
|
|
218
|
+
let contextMemories = "";
|
|
219
|
+
if (shouldSearch) {
|
|
220
|
+
try {
|
|
221
|
+
const { vectorSearch } = await import("./memory-embeddings.js");
|
|
222
|
+
const { readAllTopicEntries } = await import("./memory-tree.js");
|
|
223
|
+
const entries = readAllTopicEntries(ctx.cwd);
|
|
224
|
+
if (entries.length > 0) {
|
|
225
|
+
const topK = getQueryClassBias(queryClass).vectorTopK;
|
|
226
|
+
const boostCats = new Set(getQueryClassBias(queryClass).boostCategories);
|
|
227
|
+
// Prefer boosted categories when present; fall back to full set
|
|
228
|
+
const searchPool =
|
|
229
|
+
boostCats.size > 0 && entries.some((e) => boostCats.has(e.category))
|
|
230
|
+
? [
|
|
231
|
+
...entries.filter((e) => boostCats.has(e.category)),
|
|
232
|
+
...entries.filter((e) => !boostCats.has(e.category)),
|
|
233
|
+
]
|
|
234
|
+
: entries;
|
|
235
|
+
const results = await vectorSearch(ctx.cwd, event.prompt, searchPool, topK);
|
|
236
|
+
const relevant = results.filter(r => r.similarity > 0.65);
|
|
237
|
+
if (relevant.length > 0) {
|
|
238
|
+
contextMemories = "\n# Contextually Relevant Memories\n\n" +
|
|
239
|
+
"These memories were automatically retrieved based on your current message:\n\n" +
|
|
240
|
+
relevant.map(r => `- [${r.category}] ${r.text} (similarity: ${r.similarity.toFixed(3)})`).join("\n") +
|
|
241
|
+
"\n\n";
|
|
242
|
+
// Retrieval telemetry — track what was injected
|
|
243
|
+
logMemoryInjection(ctx.cwd, event.prompt, relevant, queryClass);
|
|
244
|
+
lastInjectedMemories = relevant;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} catch (e: any) { console.debug("[rules] vector search auto-inject failed:", e?.message?.slice(0, 100)); }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Auto-inject relevant session history (keyword search, zero LLM cost)
|
|
251
|
+
let sessionContext = "";
|
|
252
|
+
if (shouldSearch) {
|
|
253
|
+
try {
|
|
254
|
+
const { searchSessions } = await import("./session-search.js");
|
|
255
|
+
const sessionResults = searchSessions(ctx.cwd, event.prompt, 3);
|
|
256
|
+
if (sessionResults.length > 0) {
|
|
257
|
+
sessionContext = "\n# Relevant Past Sessions\n\n" +
|
|
258
|
+
"Automatically retrieved from past conversation summaries:\n\n" +
|
|
259
|
+
sessionResults.map(r =>
|
|
260
|
+
`- **${r.timestamp.split("T")[0]}** (${r.sessionId.slice(0, 8)}): ${r.snippet.replace(/[`$]/g, "")}`
|
|
261
|
+
).join("\n") +
|
|
262
|
+
"\n\n";
|
|
263
|
+
// Telemetry — log session injections (same safeguards as logMemoryInjection)
|
|
264
|
+
try {
|
|
265
|
+
const telemetryPath = path.join(getProjectDataDir(ctx.cwd), "memory-telemetry.jsonl");
|
|
266
|
+
const entry = JSON.stringify({
|
|
267
|
+
ts: new Date().toISOString(),
|
|
268
|
+
event: "session-inject",
|
|
269
|
+
prompt: event.prompt.slice(0, 200),
|
|
270
|
+
sessionCount: sessionResults.length,
|
|
271
|
+
});
|
|
272
|
+
fs.appendFileSync(telemetryPath, entry + "\n", "utf-8");
|
|
273
|
+
const stat = fs.statSync(telemetryPath);
|
|
274
|
+
if (stat.size > 512000) {
|
|
275
|
+
const lines = fs.readFileSync(telemetryPath, "utf-8").split("\n").filter(Boolean);
|
|
276
|
+
fs.writeFileSync(telemetryPath, lines.slice(-200).join("\n") + "\n", "utf-8");
|
|
277
|
+
}
|
|
278
|
+
} catch (e: any) { console.debug("[rules] session telemetry write failed:", e?.message?.slice(0, 100)); }
|
|
279
|
+
}
|
|
280
|
+
} catch (e: any) { console.debug("[rules] session history auto-inject failed:", e?.message?.slice(0, 100)); }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Task-focus reminder — compose with FRESH task data (flag set at turn_end)
|
|
284
|
+
if (taskFocusPending) {
|
|
285
|
+
taskFocusPending = false;
|
|
286
|
+
try {
|
|
287
|
+
const tasksDir = path.join(ctx.cwd, ".pi", "tasks");
|
|
288
|
+
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
289
|
+
const taskCandidates: string[] = [];
|
|
290
|
+
if (sessionId) taskCandidates.push(path.join(tasksDir, `tasks-${sessionId}.json`));
|
|
291
|
+
taskCandidates.push(path.join(tasksDir, "tasks.json"));
|
|
292
|
+
// Session-first fallback: prefer session-scoped store, only fall back
|
|
293
|
+
// to tasks.json when the session store doesn't exist or has no active tasks.
|
|
294
|
+
// This matches the semantics used by readTaskSummary() and task lifecycle functions.
|
|
295
|
+
let allActiveTasks: Array<{ id: string; status: string; subject: string }> = [];
|
|
296
|
+
for (const taskFile of taskCandidates) {
|
|
297
|
+
try {
|
|
298
|
+
if (!fs.existsSync(taskFile)) continue;
|
|
299
|
+
const data = JSON.parse(fs.readFileSync(taskFile, "utf-8"));
|
|
300
|
+
const tasks = data.tasks || [];
|
|
301
|
+
const active = tasks.filter((t: any) => t.status === "in_progress" || t.status === "pending");
|
|
302
|
+
if (active.length > 0) {
|
|
303
|
+
allActiveTasks = active;
|
|
304
|
+
break; // Use the first store that has active tasks
|
|
305
|
+
}
|
|
306
|
+
} catch { continue; }
|
|
307
|
+
}
|
|
308
|
+
if (allActiveTasks.length > 0) {
|
|
309
|
+
const summary = allActiveTasks
|
|
310
|
+
.slice(0, 3)
|
|
311
|
+
.map((t) => `#${t.id} [${t.status}] ${t.subject}`)
|
|
312
|
+
.join(", ");
|
|
313
|
+
pi.sendMessage({
|
|
314
|
+
customType: "task-focus-enforcement",
|
|
315
|
+
content: `⚠️ You have active tasks — resume your workflow now:\n${summary}${allActiveTasks.length > 3 ? ` (+${allActiveTasks.length - 3} more)` : ""}`,
|
|
316
|
+
display: true,
|
|
317
|
+
}, { triggerTurn: false, deliverAs: "nextTurn" });
|
|
318
|
+
lastTaskFocusUserMsgId = taskFocusPendingMsgId;
|
|
319
|
+
}
|
|
320
|
+
} catch (e: any) { console.debug("[rules] task-focus reminder failed:", e?.message?.slice(0, 100)); }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (!extra && !memories && !contextMemories && !sessionContext) return;
|
|
324
|
+
// Memories at TAIL position (after system prompt) — research proves tail
|
|
325
|
+
// gets highest LLM attention (U-shaped attention curve). Rules/extra stay
|
|
326
|
+
// before the prompt for backwards compatibility.
|
|
327
|
+
return { systemPrompt: event.systemPrompt + extra + memories + contextMemories + sessionContext };
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Post-turn memory reminder: after a turn completes, check if any tool
|
|
331
|
+
// results suggest relevant memories the AI should know about.
|
|
332
|
+
// This catches cases where the user's prompt doesn't mention deployment
|
|
333
|
+
// but the AI just modified files that have deployment-related memories.
|
|
334
|
+
pi.on("turn_end", async (_event, ctx) => {
|
|
335
|
+
if (process.env.PI_SUBAGENT_CHILD === "1") return;
|
|
336
|
+
|
|
337
|
+
// Retrieval telemetry — detect if injected memories were used in the response
|
|
338
|
+
try {
|
|
339
|
+
const response = (_event as any).response || (_event as any).assistantMessage || "";
|
|
340
|
+
if (response && lastInjectedMemories.length > 0) {
|
|
341
|
+
const responseLower = typeof response === "string" ? response.toLowerCase() : "";
|
|
342
|
+
if (responseLower.length > 50) {
|
|
343
|
+
const used = lastInjectedMemories.filter(m =>
|
|
344
|
+
responseLower.includes(m.text.toLowerCase().slice(0, 40))
|
|
345
|
+
);
|
|
346
|
+
if (used.length > 0 || lastInjectedMemories.length > 0) {
|
|
347
|
+
logMemoryUsage(ctx.cwd, lastInjectedMemories, used);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
lastInjectedMemories = [];
|
|
351
|
+
}
|
|
352
|
+
} catch (e: any) { console.debug("[rules] telemetry usage detection failed:", e?.message?.slice(0, 100)); }
|
|
353
|
+
|
|
354
|
+
// Task-focus enforcement: if this turn had no tool calls but tasks are active,
|
|
355
|
+
// the LLM likely answered a side question and forgot to resume work.
|
|
356
|
+
// Set a flag here; the actual reminder is composed at before_agent_start
|
|
357
|
+
// with fresh task data (avoids stale/deleted tasks in the message).
|
|
358
|
+
try {
|
|
359
|
+
const turnToolResults = (_event as any).toolResults;
|
|
360
|
+
const hadToolCalls = turnToolResults && Array.isArray(turnToolResults) && turnToolResults.length > 0;
|
|
361
|
+
let userTriggered = false;
|
|
362
|
+
let triggeringMsgId: string | null = null;
|
|
363
|
+
try {
|
|
364
|
+
const branch = ctx.sessionManager.getBranch();
|
|
365
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
366
|
+
const entry = branch[i];
|
|
367
|
+
if (entry.type === "message" && (entry as any).message?.role === "assistant") continue;
|
|
368
|
+
if (entry.type === "custom_message") {
|
|
369
|
+
userTriggered = false;
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
if (entry.type === "message" && (entry as any).message?.role === "user") {
|
|
373
|
+
const msgId = (entry as any).id;
|
|
374
|
+
if (msgId && msgId === lastTaskFocusUserMsgId) {
|
|
375
|
+
userTriggered = false;
|
|
376
|
+
} else {
|
|
377
|
+
userTriggered = true;
|
|
378
|
+
triggeringMsgId = msgId ?? null;
|
|
379
|
+
}
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
} catch { /* best-effort */ }
|
|
385
|
+
|
|
386
|
+
if (!hadToolCalls && userTriggered) {
|
|
387
|
+
taskFocusPending = true;
|
|
388
|
+
taskFocusPendingMsgId = triggeringMsgId;
|
|
389
|
+
}
|
|
390
|
+
} catch (e: any) { console.debug("[rules] task-focus enforcement failed:", e?.message?.slice(0, 100)); }
|
|
391
|
+
|
|
392
|
+
// Semantic enforcement: check verifier rules against this turn's tool calls
|
|
393
|
+
try {
|
|
394
|
+
const { loadVerifierEntries, checkVerifiers } = await import("./enforcement-rules.js");
|
|
395
|
+
const verifierEntries = loadVerifierEntries(ctx.cwd);
|
|
396
|
+
if (verifierEntries.length > 0) {
|
|
397
|
+
const rawResults = (_event as any).toolResults;
|
|
398
|
+
if (rawResults && Array.isArray(rawResults) && rawResults.length > 0) {
|
|
399
|
+
const turnToolResults = rawResults.map((tr: any) => ({
|
|
400
|
+
toolName: tr?.toolName || "",
|
|
401
|
+
input: tr?.input || {},
|
|
402
|
+
}));
|
|
403
|
+
const violations = checkVerifiers(verifierEntries, turnToolResults);
|
|
404
|
+
|
|
405
|
+
if (violations.length > 0) {
|
|
406
|
+
pi.sendMessage({
|
|
407
|
+
customType: "enforcement-violation",
|
|
408
|
+
content: `\u26d4 ENFORCEMENT VIOLATION \u2014 fix before continuing:\n\n${violations.map(v => `- ${v}`).join("\n")}`,
|
|
409
|
+
display: true,
|
|
410
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
} catch (e: any) { console.debug("[rules] semantic enforcement failed:", e?.message?.slice(0, 100)); }
|
|
415
|
+
|
|
416
|
+
// Check what files were modified in this turn by looking at tool results
|
|
417
|
+
const toolResults = (_event as any).toolResults;
|
|
418
|
+
if (!toolResults || !Array.isArray(toolResults) || toolResults.length === 0) return;
|
|
419
|
+
|
|
420
|
+
// Extract file paths from edit/write tool results
|
|
421
|
+
const modifiedPaths: string[] = [];
|
|
422
|
+
for (const tr of toolResults) {
|
|
423
|
+
const name = (tr as any)?.toolName;
|
|
424
|
+
if (name === "edit" || name === "write") {
|
|
425
|
+
const path = (tr as any)?.input?.path;
|
|
426
|
+
if (typeof path === "string") modifiedPaths.push(path);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (modifiedPaths.length === 0) return;
|
|
430
|
+
|
|
431
|
+
// Deduplicate and cap paths
|
|
432
|
+
const uniquePaths = [...new Set(modifiedPaths)].slice(0, 10);
|
|
433
|
+
|
|
434
|
+
// Search for memories related to the modified files
|
|
435
|
+
try {
|
|
436
|
+
const { vectorSearch } = await import("./memory-embeddings.js");
|
|
437
|
+
const { readAllTopicEntries } = await import("./memory-tree.js");
|
|
438
|
+
const entries = readAllTopicEntries(ctx.cwd);
|
|
439
|
+
if (entries.length === 0) return;
|
|
440
|
+
|
|
441
|
+
// Build a search query from modified file paths
|
|
442
|
+
const searchQuery = `files modified: ${uniquePaths.join(", ")}`;
|
|
443
|
+
const results = await vectorSearch(ctx.cwd, searchQuery, entries, 3);
|
|
444
|
+
const relevant = results.filter(r => r.similarity > 0.70);
|
|
445
|
+
|
|
446
|
+
if (relevant.length > 0) {
|
|
447
|
+
const reminder = relevant
|
|
448
|
+
.map(r => `- [${r.category}] ${r.text}`)
|
|
449
|
+
.join("\n");
|
|
450
|
+
pi.sendMessage({
|
|
451
|
+
customType: "memory-reminder",
|
|
452
|
+
content: `📝 **Memory reminder** (based on files you just modified):\n\n${reminder}`,
|
|
453
|
+
display: true,
|
|
454
|
+
}, { triggerTurn: false, deliverAs: "followUp" });
|
|
455
|
+
}
|
|
456
|
+
} catch (e: any) {
|
|
457
|
+
console.debug("[rules] turn_end memory reminder failed:", e?.message?.slice(0, 100));
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Loads memories using situation report (scored, token-budgeted) from topic files
|
|
463
|
+
function loadMemoriesWithScoring(
|
|
464
|
+
cwd: string,
|
|
465
|
+
isSubagent: boolean,
|
|
466
|
+
tokenBudget?: number,
|
|
467
|
+
queryClass: import("./memory-query-class.js").QueryClass = "general",
|
|
468
|
+
): string {
|
|
469
|
+
if (!tokenBudget || tokenBudget <= 0) return "";
|
|
470
|
+
try {
|
|
471
|
+
const report = buildSituationReport(cwd, tokenBudget, { queryClass });
|
|
472
|
+
if (report) {
|
|
473
|
+
let result = "\n" + report + "\n";
|
|
474
|
+
if (isSubagent) {
|
|
475
|
+
result += "\n**Do NOT write to memory** \u2014 only the orchestrator writes memories.\n";
|
|
476
|
+
}
|
|
477
|
+
return result + "\n";
|
|
478
|
+
}
|
|
479
|
+
} catch (e: any) { console.debug("[rules] situation report failed:", e?.message || e); }
|
|
480
|
+
return "";
|
|
481
|
+
}
|