@dyyz1993/pi-coding-agent 0.74.23 → 0.74.25
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/dist/extensions/agent-permissions/index.ts +235 -0
- package/dist/extensions/ask-tools/index.ts +115 -0
- package/dist/extensions/auto-memory/contract.d.ts +51 -0
- package/dist/extensions/auto-memory/contract.d.ts.map +1 -0
- package/dist/extensions/auto-memory/contract.js +2 -0
- package/dist/extensions/auto-memory/contract.js.map +1 -0
- package/dist/extensions/auto-memory/contract.ts +56 -0
- package/dist/extensions/auto-memory/index.ts +969 -0
- package/dist/extensions/auto-memory/prompts.ts +202 -0
- package/dist/extensions/auto-memory/skip-rules.ts +297 -0
- package/dist/extensions/auto-memory/utils.ts +208 -0
- package/dist/extensions/auto-session-title/index.ts +83 -0
- package/dist/extensions/bash-ext/contract.d.ts +79 -0
- package/dist/extensions/bash-ext/contract.d.ts.map +1 -0
- package/dist/extensions/bash-ext/contract.js +2 -0
- package/dist/extensions/bash-ext/contract.js.map +1 -0
- package/dist/extensions/bash-ext/contract.ts +69 -0
- package/dist/extensions/bash-ext/index.ts +858 -0
- package/dist/extensions/claude-hooks-compat/config-loader.ts +49 -0
- package/dist/extensions/claude-hooks-compat/handler-runner.ts +377 -0
- package/dist/extensions/claude-hooks-compat/if-parser.ts +53 -0
- package/dist/extensions/claude-hooks-compat/index.ts +178 -0
- package/dist/extensions/claude-hooks-compat/matcher.ts +17 -0
- package/dist/extensions/claude-hooks-compat/stdin-builder.ts +27 -0
- package/dist/extensions/claude-hooks-compat/types.ts +77 -0
- package/dist/extensions/compaction-manager/config.ts +47 -0
- package/dist/extensions/compaction-manager/context-fold.ts +63 -0
- package/dist/extensions/compaction-manager/index.ts +151 -0
- package/dist/extensions/compaction-manager/microcompact.ts +49 -0
- package/dist/extensions/compaction-manager/reactive.ts +9 -0
- package/dist/extensions/compaction-manager/session-memory.ts +48 -0
- package/dist/extensions/coordinator/INTEGRATION.md +376 -0
- package/dist/extensions/coordinator/handler.test.ts +277 -0
- package/dist/extensions/coordinator/handler.ts +189 -0
- package/dist/extensions/coordinator/index.ts +261 -0
- package/dist/extensions/coordinator/types.d.ts +100 -0
- package/dist/extensions/coordinator/types.d.ts.map +1 -0
- package/dist/extensions/coordinator/types.js +2 -0
- package/dist/extensions/coordinator/types.js.map +1 -0
- package/dist/extensions/coordinator/types.ts +72 -0
- package/dist/extensions/file-snapshot/index.ts +131 -0
- package/dist/extensions/file-time-guard/README.md +133 -0
- package/dist/extensions/file-time-guard/config.ts +13 -0
- package/dist/extensions/file-time-guard/index.ts +171 -0
- package/dist/extensions/hooks-engine/index.ts +117 -0
- package/dist/extensions/lsp/lsp/client/file-tracker.ts +70 -0
- package/dist/extensions/lsp/lsp/client/registry.ts +305 -0
- package/dist/extensions/lsp/lsp/client/runtime.ts +832 -0
- package/dist/extensions/lsp/lsp/config/resolver.ts +573 -0
- package/dist/extensions/lsp/lsp/contract.d.ts +101 -0
- package/dist/extensions/lsp/lsp/contract.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/contract.js +2 -0
- package/dist/extensions/lsp/lsp/contract.js.map +1 -0
- package/dist/extensions/lsp/lsp/contract.ts +103 -0
- package/dist/extensions/lsp/lsp/hooks/agent-end.ts +169 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts +10 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js +30 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js.map +1 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.ts +41 -0
- package/dist/extensions/lsp/lsp/hooks/writethrough.ts +342 -0
- package/dist/extensions/lsp/lsp/index.ts +307 -0
- package/dist/extensions/lsp/lsp/lsp.test.ts +684 -0
- package/dist/extensions/lsp/lsp/monitoring/server-metrics.ts +176 -0
- package/dist/extensions/lsp/lsp/tools/lsp-tool.ts +402 -0
- package/dist/extensions/lsp/lsp/utils/dependency-resolver.ts +147 -0
- package/dist/extensions/lsp/lsp/utils/diagnostics-wait.ts +41 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts +20 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.js +64 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.js.map +1 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.ts +76 -0
- package/dist/extensions/message-bridge/GUIDE.md +210 -0
- package/dist/extensions/message-bridge/index.ts +222 -0
- package/dist/extensions/output-guard/index.ts +384 -0
- package/dist/extensions/preview/index.ts +278 -0
- package/dist/extensions/rules-engine/MATCH_HISTORY_RECONCILIATION.md +111 -0
- package/dist/extensions/rules-engine/RULES-ENGINE-GUIDE.md +470 -0
- package/dist/extensions/rules-engine/cache.js +232 -0
- package/dist/extensions/rules-engine/cache.ts +38 -0
- package/dist/extensions/rules-engine/config.js +63 -0
- package/dist/extensions/rules-engine/config.ts +70 -0
- package/dist/extensions/rules-engine/index.js +1530 -0
- package/dist/extensions/rules-engine/index.ts +552 -0
- package/dist/extensions/rules-engine/injector.js +68 -0
- package/dist/extensions/rules-engine/injector.ts +74 -0
- package/dist/extensions/rules-engine/loader.js +179 -0
- package/dist/extensions/rules-engine/loader.ts +205 -0
- package/dist/extensions/rules-engine/matcher.js +534 -0
- package/dist/extensions/rules-engine/matcher.ts +52 -0
- package/dist/extensions/rules-engine/types.d.ts +156 -0
- package/dist/extensions/rules-engine/types.d.ts.map +1 -0
- package/dist/extensions/rules-engine/types.js +2 -0
- package/dist/extensions/rules-engine/types.js.map +1 -0
- package/dist/extensions/rules-engine/types.ts +169 -0
- package/dist/extensions/session-supervisor/checker.ts +116 -0
- package/dist/extensions/session-supervisor/config.ts +45 -0
- package/dist/extensions/session-supervisor/index.ts +726 -0
- package/dist/extensions/session-supervisor/prompts.ts +132 -0
- package/dist/extensions/session-supervisor/scheduler.ts +69 -0
- package/dist/extensions/session-supervisor/types.ts +215 -0
- package/dist/extensions/subagent/README.md +172 -0
- package/dist/extensions/subagent/agents/explorer.md +25 -0
- package/dist/extensions/subagent/agents/guide.md +27 -0
- package/dist/extensions/subagent/agents/planner.md +37 -0
- package/dist/extensions/subagent/agents/reviewer.md +35 -0
- package/dist/extensions/subagent/agents/scout.md +50 -0
- package/dist/extensions/subagent/agents/verification.md +35 -0
- package/dist/extensions/subagent/agents/worker.md +24 -0
- package/dist/extensions/subagent/agents.ts +25 -0
- package/dist/extensions/subagent/index.ts +987 -0
- package/dist/extensions/subagent/prompts/implement-and-review.md +10 -0
- package/dist/extensions/subagent/prompts/implement.md +10 -0
- package/dist/extensions/subagent/prompts/scout-and-plan.md +9 -0
- package/dist/extensions/subagent-ext/contract.d.ts +2 -0
- package/dist/extensions/subagent-ext/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-ext/contract.js +2 -0
- package/dist/extensions/subagent-ext/contract.js.map +1 -0
- package/dist/extensions/subagent-ext/contract.ts +1 -0
- package/dist/extensions/subagent-ext/index.ts +347 -0
- package/dist/extensions/subagent-shared/contract.d.ts +25 -0
- package/dist/extensions/subagent-shared/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-shared/contract.js +2 -0
- package/dist/extensions/subagent-shared/contract.js.map +1 -0
- package/dist/extensions/subagent-shared/contract.ts +28 -0
- package/dist/extensions/subagent-shared/index.ts +4 -0
- package/dist/extensions/subagent-shared/render.ts +166 -0
- package/dist/extensions/subagent-shared/types.ts +35 -0
- package/dist/extensions/subagent-shared/utils.ts +112 -0
- package/dist/extensions/subagent-v2/contract.d.ts +2 -0
- package/dist/extensions/subagent-v2/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-v2/contract.js +2 -0
- package/dist/extensions/subagent-v2/contract.js.map +1 -0
- package/dist/extensions/subagent-v2/contract.ts +1 -0
- package/dist/extensions/subagent-v2/index.ts +599 -0
- package/dist/extensions/todo-ext/contract.d.ts +27 -0
- package/dist/extensions/todo-ext/contract.d.ts.map +1 -0
- package/dist/extensions/todo-ext/contract.js +2 -0
- package/dist/extensions/todo-ext/contract.js.map +1 -0
- package/dist/extensions/todo-ext/contract.ts +30 -0
- package/dist/extensions/todo-ext/index.ts +419 -0
- package/package.json +3 -2
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import { Type } from "@dyyz1993/pi-ai";
|
|
2
|
+
import { createTypedChannel, defineTool, type ExtensionAPI } from "@dyyz1993/pi-coding-agent";
|
|
3
|
+
import { getRules, invalidateCache } from "./cache.js";
|
|
4
|
+
import { loadConfig } from "./config.js";
|
|
5
|
+
import { buildSystemReminderSection, buildToolReminderSection } from "./injector.js";
|
|
6
|
+
import { matchesAnyGlob } from "./matcher.js";
|
|
7
|
+
import type {
|
|
8
|
+
InjectedPayload,
|
|
9
|
+
LifecycleEntry,
|
|
10
|
+
MatchedRuleDetail,
|
|
11
|
+
MatchRecord,
|
|
12
|
+
ParsedRule,
|
|
13
|
+
RuleDetail,
|
|
14
|
+
RuleSeverity,
|
|
15
|
+
RulesChannelContract,
|
|
16
|
+
RulesChannelEvent,
|
|
17
|
+
RulesConfig,
|
|
18
|
+
ScannedDir,
|
|
19
|
+
SnapshotPayload,
|
|
20
|
+
} from "./types.js";
|
|
21
|
+
import { RULES_CHANNEL_NAME } from "./types.js";
|
|
22
|
+
|
|
23
|
+
export { createTypedChannel } from "@dyyz1993/pi-coding-agent";
|
|
24
|
+
export { getRules, invalidateCache } from "./cache.js";
|
|
25
|
+
export { loadConfig, resolveDirs } from "./config.js";
|
|
26
|
+
export { buildCompactContext, buildSystemReminderSection, buildToolReminderSection, buildSystemPromptSection, buildToolContextSection } from "./injector.js";
|
|
27
|
+
export { loadRules, parseFrontmatter, parseRuleFile } from "./loader.js";
|
|
28
|
+
export { matchesAnyGlob, matchGlob } from "./matcher.js";
|
|
29
|
+
export type {
|
|
30
|
+
InjectedPayload,
|
|
31
|
+
LifecycleEntry,
|
|
32
|
+
MatchedPayload,
|
|
33
|
+
MatchRecord,
|
|
34
|
+
ParsedRule,
|
|
35
|
+
ReloadedPayload,
|
|
36
|
+
RuleDetail,
|
|
37
|
+
RuleFrontmatter,
|
|
38
|
+
RuleScope,
|
|
39
|
+
RuleSeverity,
|
|
40
|
+
RulesChannelContract,
|
|
41
|
+
RulesChannelEvent,
|
|
42
|
+
RulesConfig,
|
|
43
|
+
ScannedDir,
|
|
44
|
+
SnapshotPayload,
|
|
45
|
+
UnloadedPayload,
|
|
46
|
+
} from "./types.js";
|
|
47
|
+
|
|
48
|
+
const READ_TOOLS = new Set(["read", "grep", "glob"]);
|
|
49
|
+
|
|
50
|
+
export default function rulesEnginePlugin(pi: ExtensionAPI) {
|
|
51
|
+
let config: RulesConfig | null = null;
|
|
52
|
+
let rules: ParsedRule[] = [];
|
|
53
|
+
let cachedMatchHash = "";
|
|
54
|
+
let hasSentSnapshot = false;
|
|
55
|
+
let _lastCwd = "";
|
|
56
|
+
let lastMessages: unknown[] = [];
|
|
57
|
+
/** Track which rule+file combos have been injected (ruleName -> Set<filePath>) */
|
|
58
|
+
let injectedRuleFiles: Map<string, Set<string>> = new Map();
|
|
59
|
+
|
|
60
|
+
function rebuildMatchHistory(messages: unknown[]): MatchRecord[] {
|
|
61
|
+
const history: MatchRecord[] = [];
|
|
62
|
+
for (const msg of messages) {
|
|
63
|
+
if ((msg as Record<string, unknown>).role !== "toolResult") continue;
|
|
64
|
+
const details = (msg as { details?: Record<string, unknown> }).details;
|
|
65
|
+
if (!details?.rulesMatched) continue;
|
|
66
|
+
const rulesMatched = details.rulesMatched as MatchedRuleDetail[];
|
|
67
|
+
history.push({
|
|
68
|
+
filePath: (details.matchedFilePath as string) || "",
|
|
69
|
+
ruleNames: rulesMatched.map((r) => r.name),
|
|
70
|
+
toolName: (msg as { toolName?: string }).toolName || "",
|
|
71
|
+
toolCallId: (msg as { toolCallId?: string }).toolCallId || "",
|
|
72
|
+
severity: rulesMatched.some((r) => r.severity === "critical" || r.severity === "high") ? "warning" : "info",
|
|
73
|
+
timestamp: (msg as { timestamp?: number }).timestamp || 0,
|
|
74
|
+
matchedRuleDetails: rulesMatched,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return history;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const rawChannel = pi.registerChannel(RULES_CHANNEL_NAME);
|
|
81
|
+
const channel = createTypedChannel<RulesChannelContract>(rawChannel).server;
|
|
82
|
+
|
|
83
|
+
channel.handle("getSnapshot", (params) => {
|
|
84
|
+
const unconditional = getUnconditionalRules();
|
|
85
|
+
const conditional = getConditionalRules();
|
|
86
|
+
const matchHistory = rebuildMatchHistory(lastMessages);
|
|
87
|
+
return {
|
|
88
|
+
type: "snapshot" as const,
|
|
89
|
+
rules: rules.map(toRuleDetail),
|
|
90
|
+
injectedRuleNames: unconditional.map((r) => r.name),
|
|
91
|
+
totalRules: rules.length,
|
|
92
|
+
unconditionalCount: unconditional.length,
|
|
93
|
+
conditionalCount: conditional.length,
|
|
94
|
+
matchHistory,
|
|
95
|
+
lifecycleLog: [] as LifecycleEntry[],
|
|
96
|
+
loadedAt: Date.now(),
|
|
97
|
+
cacheTTL: config?.cacheTTL || 30000,
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
function getUnconditionalRules(): ParsedRule[] {
|
|
102
|
+
return rules.filter((r) => r.isUnconditional);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getConditionalRules(): ParsedRule[] {
|
|
106
|
+
return rules.filter((r) => !r.isUnconditional);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getMatchingRules(targetPath: string): ParsedRule[] {
|
|
110
|
+
return getConditionalRules().filter((rule) => {
|
|
111
|
+
const globs = rule.frontmatter.globs ?? rule.frontmatter.paths;
|
|
112
|
+
if (!globs || globs.length === 0) return false;
|
|
113
|
+
return matchesAnyGlob(globs, targetPath);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function refreshRules(cwd: string): Promise<void> {
|
|
118
|
+
config = await loadConfig(cwd);
|
|
119
|
+
rules = await getRules(cwd, config);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function toRuleDetail(r: ParsedRule): RuleDetail {
|
|
123
|
+
return {
|
|
124
|
+
name: r.name,
|
|
125
|
+
title: r.title,
|
|
126
|
+
filePath: r.filePath,
|
|
127
|
+
scope: r.scope,
|
|
128
|
+
source: r.source,
|
|
129
|
+
severity: r.frontmatter.severity || ("medium" as RuleSeverity),
|
|
130
|
+
isUnconditional: r.isUnconditional,
|
|
131
|
+
globs: r.frontmatter.globs ?? r.frontmatter.paths ?? [],
|
|
132
|
+
description: r.frontmatter.description,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function buildSnapshot(matchHistory: MatchRecord[], lifecycleLog: LifecycleEntry[]): SnapshotPayload {
|
|
137
|
+
const unconditional = getUnconditionalRules();
|
|
138
|
+
const conditional = getConditionalRules();
|
|
139
|
+
return {
|
|
140
|
+
type: "snapshot",
|
|
141
|
+
rules: rules.map(toRuleDetail),
|
|
142
|
+
injectedRuleNames: unconditional.map((r) => r.name),
|
|
143
|
+
totalRules: rules.length,
|
|
144
|
+
unconditionalCount: unconditional.length,
|
|
145
|
+
conditionalCount: conditional.length,
|
|
146
|
+
matchHistory,
|
|
147
|
+
lifecycleLog,
|
|
148
|
+
loadedAt: Date.now(),
|
|
149
|
+
cacheTTL: config?.cacheTTL || 30000,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function extractTargetPath(args: Record<string, unknown>): string | undefined {
|
|
154
|
+
if ("filePath" in args && typeof args.filePath === "string") return args.filePath;
|
|
155
|
+
if ("path" in args && typeof args.path === "string") return args.path;
|
|
156
|
+
if ("pattern" in args && typeof args.pattern === "string") return args.pattern;
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
pi.registerTool(
|
|
161
|
+
defineTool({
|
|
162
|
+
name: "rules_list",
|
|
163
|
+
label: "List Rules",
|
|
164
|
+
description: "List all discovered rules from all configured directories across all scopes",
|
|
165
|
+
parameters: Type.Object({}),
|
|
166
|
+
async execute() {
|
|
167
|
+
const unconditional = getUnconditionalRules();
|
|
168
|
+
const conditional = getConditionalRules();
|
|
169
|
+
|
|
170
|
+
const byScope: Record<string, number> = {};
|
|
171
|
+
for (const r of rules) {
|
|
172
|
+
byScope[r.scope] = (byScope[r.scope] || 0) + 1;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let output = `# Loaded Rules (${rules.length})\n\n`;
|
|
176
|
+
output += `Scopes: ${Object.entries(byScope)
|
|
177
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
178
|
+
.join(", ")}\n\n`;
|
|
179
|
+
|
|
180
|
+
output += `**Unconditional** (${unconditional.length}):\n`;
|
|
181
|
+
for (const rule of unconditional) {
|
|
182
|
+
output += `- ${rule.title} (${rule.source})\n`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
output += `\n**Conditional** (${conditional.length}):\n`;
|
|
186
|
+
for (const rule of conditional) {
|
|
187
|
+
output += `- ${rule.title} [${(rule.frontmatter.globs ?? rule.frontmatter.paths)?.join(", ")}] (${rule.source})\n`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { content: [{ type: "text", text: output }], details: undefined };
|
|
191
|
+
},
|
|
192
|
+
}),
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
pi.registerTool(
|
|
196
|
+
defineTool({
|
|
197
|
+
name: "rules_match",
|
|
198
|
+
label: "Match Rules",
|
|
199
|
+
description: "Find conditional rules that match a given file path by glob pattern",
|
|
200
|
+
parameters: Type.Object({
|
|
201
|
+
filePath: Type.String({ description: "File path to match" }),
|
|
202
|
+
}),
|
|
203
|
+
async execute(_id, params) {
|
|
204
|
+
const matching = getMatchingRules(params.filePath);
|
|
205
|
+
const unconditional = getUnconditionalRules();
|
|
206
|
+
|
|
207
|
+
let output = `# Rule Match: ${params.filePath}\n\n`;
|
|
208
|
+
output += `**Unconditional** (always active, ${unconditional.length}):\n`;
|
|
209
|
+
for (const r of unconditional) {
|
|
210
|
+
output += `- ${r.title}\n`;
|
|
211
|
+
}
|
|
212
|
+
output += `\n**Conditional matches** (${matching.length}):\n`;
|
|
213
|
+
for (const r of matching) {
|
|
214
|
+
const sev = r.frontmatter.severity || "medium";
|
|
215
|
+
output += `- [${sev}] ${r.title} (${(r.frontmatter.globs ?? r.frontmatter.paths)?.join(", ")})\n`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { content: [{ type: "text", text: output }], details: undefined };
|
|
219
|
+
},
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
pi.registerTool(
|
|
224
|
+
defineTool({
|
|
225
|
+
name: "rules_reload",
|
|
226
|
+
label: "Reload Rules",
|
|
227
|
+
description: "Force reload all rules from disk (clears cache and re-reads config)",
|
|
228
|
+
parameters: Type.Object({}),
|
|
229
|
+
async execute(_id, _params, _signal, _onUpdate, ctx) {
|
|
230
|
+
invalidateCache();
|
|
231
|
+
await refreshRules(ctx.cwd);
|
|
232
|
+
return {
|
|
233
|
+
content: [
|
|
234
|
+
{
|
|
235
|
+
type: "text",
|
|
236
|
+
text: `Rules reloaded: ${rules.length} total (${getUnconditionalRules().length} unconditional, ${getConditionalRules().length} conditional)`,
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
details: undefined,
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
pi.registerTool(
|
|
246
|
+
defineTool({
|
|
247
|
+
name: "rules_show",
|
|
248
|
+
label: "Show Rule",
|
|
249
|
+
description: "Show the full content of a specific rule by name",
|
|
250
|
+
parameters: Type.Object({
|
|
251
|
+
name: Type.String({ description: "Rule name (filename without .md)" }),
|
|
252
|
+
}),
|
|
253
|
+
async execute(_id, params) {
|
|
254
|
+
const rule = rules.find((r) => r.name === params.name);
|
|
255
|
+
if (!rule) {
|
|
256
|
+
return {
|
|
257
|
+
content: [{ type: "text", text: `Rule '${params.name}' not found` }],
|
|
258
|
+
isError: true,
|
|
259
|
+
details: undefined,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let output = `# ${rule.title}\n\n`;
|
|
264
|
+
output += `- **Name**: ${rule.name}\n`;
|
|
265
|
+
output += `- **Scope**: ${rule.scope}\n`;
|
|
266
|
+
output += `- **Source**: ${rule.source}\n`;
|
|
267
|
+
output += `- **File**: ${rule.filePath}\n`;
|
|
268
|
+
if ((rule.frontmatter.globs ?? rule.frontmatter.paths)?.length) {
|
|
269
|
+
output += `- **Globs**: ${(rule.frontmatter.globs ?? rule.frontmatter.paths)?.join(", ")}\n`;
|
|
270
|
+
}
|
|
271
|
+
if (rule.frontmatter.description) {
|
|
272
|
+
output += `- **Description**: ${rule.frontmatter.description}\n`;
|
|
273
|
+
}
|
|
274
|
+
output += `\n${rule.content}`;
|
|
275
|
+
|
|
276
|
+
return { content: [{ type: "text", text: output }], details: undefined };
|
|
277
|
+
},
|
|
278
|
+
}),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
pi.registerCommand("rules", {
|
|
282
|
+
description: "Rules management (list, reload, check <path>, active)",
|
|
283
|
+
handler: async (args, ctx) => {
|
|
284
|
+
const parts = args.trim().split(/\s+/);
|
|
285
|
+
const sub = parts[0] || "list";
|
|
286
|
+
|
|
287
|
+
if (sub === "list" || sub === "ls") {
|
|
288
|
+
ctx.ui.notify(
|
|
289
|
+
`${rules.length} rules loaded (${getUnconditionalRules().length} unconditional, ${getConditionalRules().length} conditional)`,
|
|
290
|
+
"info",
|
|
291
|
+
);
|
|
292
|
+
} else if (sub === "reload") {
|
|
293
|
+
invalidateCache();
|
|
294
|
+
await refreshRules(ctx.cwd);
|
|
295
|
+
ctx.ui.notify(`Rules reloaded: ${rules.length} total`, "info");
|
|
296
|
+
} else if (sub === "check" && parts[1]) {
|
|
297
|
+
const target = parts.slice(1).join(" ");
|
|
298
|
+
const matching = getMatchingRules(target);
|
|
299
|
+
ctx.ui.notify(
|
|
300
|
+
matching.length > 0
|
|
301
|
+
? `${matching.length} rules match ${target}: ${matching.map((r) => r.title).join(", ")}`
|
|
302
|
+
: `No conditional rules match ${target}`,
|
|
303
|
+
"info",
|
|
304
|
+
);
|
|
305
|
+
} else if (sub === "active") {
|
|
306
|
+
const active = getUnconditionalRules();
|
|
307
|
+
ctx.ui.notify(
|
|
308
|
+
`Active: ${active.length} unconditional (in system prompt), ${getConditionalRules().length} conditional (on file match)`,
|
|
309
|
+
"info",
|
|
310
|
+
);
|
|
311
|
+
} else {
|
|
312
|
+
ctx.ui.notify("Usage: /rules [list|reload|check <path>|active]", "info");
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
318
|
+
_lastCwd = ctx.cwd;
|
|
319
|
+
await refreshRules(ctx.cwd);
|
|
320
|
+
ctx.ui.setStatus("rules-engine", `Rules: ${rules.length}`);
|
|
321
|
+
|
|
322
|
+
const unconditional = getUnconditionalRules();
|
|
323
|
+
const conditional = getConditionalRules();
|
|
324
|
+
|
|
325
|
+
if (!hasSentSnapshot) {
|
|
326
|
+
hasSentSnapshot = true;
|
|
327
|
+
|
|
328
|
+
const scopeGroups = new Map<string, ParsedRule[]>();
|
|
329
|
+
for (const r of rules) {
|
|
330
|
+
const list = scopeGroups.get(r.scope) || [];
|
|
331
|
+
list.push(r);
|
|
332
|
+
scopeGroups.set(r.scope, list);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const scannedDirs: ScannedDir[] = [...scopeGroups.entries()].map(([scope, scopeRules]) => ({
|
|
336
|
+
dir: scopeRules[0]?.source || scope,
|
|
337
|
+
fileCount: scopeRules.length,
|
|
338
|
+
ruleNames: scopeRules.map((r) => r.name),
|
|
339
|
+
}));
|
|
340
|
+
|
|
341
|
+
channel.emit(
|
|
342
|
+
"snapshot",
|
|
343
|
+
buildSnapshot(
|
|
344
|
+
[],
|
|
345
|
+
[
|
|
346
|
+
{
|
|
347
|
+
event: "loaded",
|
|
348
|
+
message: `Loaded ${rules.length} rules (${unconditional.length} unconditional, ${conditional.length} conditional)`,
|
|
349
|
+
ruleCount: rules.length,
|
|
350
|
+
timestamp: Date.now(),
|
|
351
|
+
details: {
|
|
352
|
+
scannedDirs,
|
|
353
|
+
configSource: config ? ".rules-config.json" : "default",
|
|
354
|
+
cacheHit: false,
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
),
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
pi.on("before_agent_start", async (event) => {
|
|
364
|
+
const unconditional = getUnconditionalRules();
|
|
365
|
+
|
|
366
|
+
if (unconditional.length === 0) {
|
|
367
|
+
channel.emit("injected", {
|
|
368
|
+
type: "injected",
|
|
369
|
+
ruleNames: [],
|
|
370
|
+
systemPromptLength: event.systemPrompt.length,
|
|
371
|
+
});
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const alreadyInjected = lastMessages.some(
|
|
376
|
+
(msg) =>
|
|
377
|
+
(msg as Record<string, unknown>).role === "custom" &&
|
|
378
|
+
(msg as Record<string, unknown>).customType === "rules-engine",
|
|
379
|
+
);
|
|
380
|
+
if (alreadyInjected) {
|
|
381
|
+
channel.emit("injected", {
|
|
382
|
+
type: "injected",
|
|
383
|
+
ruleNames: unconditional.map((r) => r.name),
|
|
384
|
+
systemPromptLength: event.systemPrompt.length,
|
|
385
|
+
deduplicated: true,
|
|
386
|
+
});
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const sources = [...new Set(unconditional.map((r) => r.source))];
|
|
391
|
+
const reminderContent = buildSystemReminderSection(unconditional, sources);
|
|
392
|
+
|
|
393
|
+
channel.emit("injected", {
|
|
394
|
+
type: "injected",
|
|
395
|
+
ruleNames: unconditional.map((r) => r.name),
|
|
396
|
+
systemPromptLength: event.systemPrompt.length,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
message: {
|
|
401
|
+
customType: "rules-engine",
|
|
402
|
+
content: reminderContent,
|
|
403
|
+
display: false,
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
pi.on("tool_result", async (event) => {
|
|
409
|
+
if (!READ_TOOLS.has(event.toolName)) return undefined;
|
|
410
|
+
|
|
411
|
+
const targetPath = extractTargetPath(event.input);
|
|
412
|
+
if (!targetPath) return undefined;
|
|
413
|
+
|
|
414
|
+
const matching = getMatchingRules(targetPath);
|
|
415
|
+
if (matching.length === 0) return undefined;
|
|
416
|
+
|
|
417
|
+
const matchedRuleDetails: MatchedRuleDetail[] = matching.map((r) => {
|
|
418
|
+
const ruleName = r.name;
|
|
419
|
+
const injectedFiles = injectedRuleFiles.get(ruleName);
|
|
420
|
+
const wasAlreadyLoaded = injectedFiles !== undefined && injectedFiles.has(targetPath);
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
name: ruleName,
|
|
424
|
+
title: r.title,
|
|
425
|
+
severity: r.frontmatter.severity || ("medium" as RuleSeverity),
|
|
426
|
+
matchedGlob:
|
|
427
|
+
(r.frontmatter.globs ?? r.frontmatter.paths)?.find((p) => matchesAnyGlob([p], targetPath)) ||
|
|
428
|
+
(r.frontmatter.globs ?? r.frontmatter.paths)?.[0] ||
|
|
429
|
+
"",
|
|
430
|
+
alreadyLoaded: wasAlreadyLoaded || undefined,
|
|
431
|
+
};
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// Determine which rules are newly injected vs already loaded
|
|
435
|
+
const newRules = matching.filter((r) => {
|
|
436
|
+
const injectedFiles = injectedRuleFiles.get(r.name);
|
|
437
|
+
return !injectedFiles || !injectedFiles.has(targetPath);
|
|
438
|
+
});
|
|
439
|
+
const allAlreadyLoaded = newRules.length === 0;
|
|
440
|
+
|
|
441
|
+
// Record that these rules have now been injected for this file
|
|
442
|
+
for (const r of matching) {
|
|
443
|
+
let injectedFiles = injectedRuleFiles.get(r.name);
|
|
444
|
+
if (!injectedFiles) {
|
|
445
|
+
injectedFiles = new Set();
|
|
446
|
+
injectedRuleFiles.set(r.name, injectedFiles);
|
|
447
|
+
}
|
|
448
|
+
injectedFiles.add(targetPath);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const hasCritical = matching.some((r) => r.frontmatter.severity === "critical");
|
|
452
|
+
const hasHigh = matching.some((r) => r.frontmatter.severity === "high");
|
|
453
|
+
|
|
454
|
+
channel.emit("matched", {
|
|
455
|
+
type: "matched",
|
|
456
|
+
filePath: targetPath,
|
|
457
|
+
matchedRules: matchedRuleDetails,
|
|
458
|
+
toolName: event.toolName,
|
|
459
|
+
toolCallId: event.toolCallId,
|
|
460
|
+
severity: hasCritical ? "warning" : hasHigh ? "warning" : "info",
|
|
461
|
+
timestamp: Date.now(),
|
|
462
|
+
alreadyLoaded: allAlreadyLoaded || undefined,
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// Only inject content for NEW rules (skip if all already loaded)
|
|
466
|
+
if (allAlreadyLoaded) {
|
|
467
|
+
return {
|
|
468
|
+
details: {
|
|
469
|
+
...((event.details as Record<string, unknown>) || {}),
|
|
470
|
+
rulesMatched: matchedRuleDetails,
|
|
471
|
+
matchedFilePath: targetPath,
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const contextSection = buildToolReminderSection(newRules, targetPath);
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
content: [...event.content, { type: "text" as const, text: `\n\n${contextSection}` }],
|
|
480
|
+
details: {
|
|
481
|
+
...((event.details as Record<string, unknown>) || {}),
|
|
482
|
+
rulesMatched: matchedRuleDetails,
|
|
483
|
+
matchedFilePath: targetPath,
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
pi.on("context", async (event) => {
|
|
489
|
+
lastMessages = event.messages;
|
|
490
|
+
const matchHistory = rebuildMatchHistory(event.messages);
|
|
491
|
+
|
|
492
|
+
const hash = JSON.stringify(matchHistory.map((r) => `${r.toolCallId}:${r.filePath}`));
|
|
493
|
+
if (hash !== cachedMatchHash) {
|
|
494
|
+
cachedMatchHash = hash;
|
|
495
|
+
const unconditional = getUnconditionalRules();
|
|
496
|
+
const conditional = getConditionalRules();
|
|
497
|
+
channel.emit("snapshot", {
|
|
498
|
+
type: "snapshot",
|
|
499
|
+
rules: rules.map(toRuleDetail),
|
|
500
|
+
injectedRuleNames: unconditional.map((r) => r.name),
|
|
501
|
+
totalRules: rules.length,
|
|
502
|
+
unconditionalCount: unconditional.length,
|
|
503
|
+
conditionalCount: conditional.length,
|
|
504
|
+
matchHistory,
|
|
505
|
+
lifecycleLog: [],
|
|
506
|
+
loadedAt: Date.now(),
|
|
507
|
+
cacheTTL: config?.cacheTTL || 30000,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return undefined;
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
pi.on("session_compact", async (_event, ctx) => {
|
|
515
|
+
cachedMatchHash = "";
|
|
516
|
+
lastMessages = [];
|
|
517
|
+
injectedRuleFiles = new Map();
|
|
518
|
+
ctx.ui.setStatus("rules-engine", `Rules: ${rules.length} (re-injected after compact)`);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
pi.on("session_tree", async () => {
|
|
522
|
+
cachedMatchHash = "";
|
|
523
|
+
lastMessages = [];
|
|
524
|
+
injectedRuleFiles = new Map();
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
pi.on("turn_end", async () => {
|
|
528
|
+
if (lastMessages.length === 0 && rules.length > 0) return;
|
|
529
|
+
const matchHistory = rebuildMatchHistory(lastMessages);
|
|
530
|
+
const hash = JSON.stringify(matchHistory.map((r) => `${r.toolCallId}:${r.filePath}`));
|
|
531
|
+
if (hash !== cachedMatchHash) {
|
|
532
|
+
cachedMatchHash = hash;
|
|
533
|
+
channel.emit("snapshot", {
|
|
534
|
+
type: "snapshot",
|
|
535
|
+
rules: rules.map(toRuleDetail),
|
|
536
|
+
injectedRuleNames: getUnconditionalRules().map((r) => r.name),
|
|
537
|
+
totalRules: rules.length,
|
|
538
|
+
unconditionalCount: getUnconditionalRules().length,
|
|
539
|
+
conditionalCount: getConditionalRules().length,
|
|
540
|
+
matchHistory,
|
|
541
|
+
lifecycleLog: [],
|
|
542
|
+
loadedAt: Date.now(),
|
|
543
|
+
cacheTTL: config?.cacheTTL || 30000,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
549
|
+
channel.emit("unloaded", { type: "unloaded", reason: "session_shutdown" });
|
|
550
|
+
ctx.ui.setStatus("rules-engine", undefined);
|
|
551
|
+
});
|
|
552
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// packages/coding-agent/extensions/rules-engine/injector.ts
|
|
2
|
+
var SEVERITY_ICONS = {
|
|
3
|
+
critical: "\u{1F534}",
|
|
4
|
+
high: "\u{1F7E0}",
|
|
5
|
+
medium: "\u{1F7E1}",
|
|
6
|
+
low: "\u{1F535}",
|
|
7
|
+
hint: "\u{1F4A1}"
|
|
8
|
+
};
|
|
9
|
+
function buildSystemReminderSection(rules, sources) {
|
|
10
|
+
if (rules.length === 0) return "";
|
|
11
|
+
const lines = [
|
|
12
|
+
"<system-reminder>",
|
|
13
|
+
`Instructions from: ${sources.join(", ")}`,
|
|
14
|
+
"The following rules are always active. Follow them strictly.",
|
|
15
|
+
""
|
|
16
|
+
];
|
|
17
|
+
for (const rule of rules) {
|
|
18
|
+
lines.push(`Rule: ${rule.name} \u2014 ${rule.title}`);
|
|
19
|
+
if (rule.frontmatter.description) {
|
|
20
|
+
lines.push(`> ${rule.frontmatter.description}`);
|
|
21
|
+
}
|
|
22
|
+
lines.push("");
|
|
23
|
+
lines.push(rule.content);
|
|
24
|
+
lines.push("");
|
|
25
|
+
}
|
|
26
|
+
lines.push("</system-reminder>");
|
|
27
|
+
return lines.join("\n");
|
|
28
|
+
}
|
|
29
|
+
function buildToolReminderSection(rules, targetPath) {
|
|
30
|
+
if (rules.length === 0) return "";
|
|
31
|
+
const lines = [
|
|
32
|
+
"<system-reminder>",
|
|
33
|
+
`Conditional rules matched for ${targetPath}:`,
|
|
34
|
+
""
|
|
35
|
+
];
|
|
36
|
+
for (const rule of rules) {
|
|
37
|
+
const severity = rule.frontmatter.severity || "medium";
|
|
38
|
+
const icon = SEVERITY_ICONS[severity] || "\u{1F7E1}";
|
|
39
|
+
lines.push(`Rule: ${icon} ${rule.title} [${severity}]`);
|
|
40
|
+
if (rule.frontmatter.description) {
|
|
41
|
+
lines.push(`> ${rule.frontmatter.description}`);
|
|
42
|
+
}
|
|
43
|
+
lines.push("");
|
|
44
|
+
lines.push(rule.content);
|
|
45
|
+
lines.push("");
|
|
46
|
+
}
|
|
47
|
+
lines.push("</system-reminder>");
|
|
48
|
+
return lines.join("\n");
|
|
49
|
+
}
|
|
50
|
+
var buildSystemPromptSection = buildSystemReminderSection;
|
|
51
|
+
var buildToolContextSection = buildToolReminderSection;
|
|
52
|
+
function buildCompactContext(rules) {
|
|
53
|
+
if (rules.length === 0) return "";
|
|
54
|
+
const lines = ["## Active Rules (persist across compaction)", ""];
|
|
55
|
+
for (const rule of rules) {
|
|
56
|
+
const desc = rule.frontmatter.description || rule.content.split("\n")[0];
|
|
57
|
+
lines.push(`- **${rule.title}** (${rule.name}): ${desc}`);
|
|
58
|
+
}
|
|
59
|
+
return lines.join("\n");
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
buildCompactContext,
|
|
63
|
+
buildSystemPromptSection,
|
|
64
|
+
buildSystemReminderSection,
|
|
65
|
+
buildToolContextSection,
|
|
66
|
+
buildToolReminderSection
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiaW5qZWN0b3IudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImltcG9ydCB0eXBlIHsgUGFyc2VkUnVsZSB9IGZyb20gXCIuL3R5cGVzLmpzXCI7XG5cbmNvbnN0IFNFVkVSSVRZX0lDT05TOiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmc+ID0ge1xuXHRjcml0aWNhbDogXCJcdUQ4M0RcdUREMzRcIixcblx0aGlnaDogXCJcdUQ4M0RcdURGRTBcIixcblx0bWVkaXVtOiBcIlx1RDgzRFx1REZFMVwiLFxuXHRsb3c6IFwiXHVEODNEXHVERDM1XCIsXG5cdGhpbnQ6IFwiXHVEODNEXHVEQ0ExXCIsXG59O1xuXG5leHBvcnQgZnVuY3Rpb24gYnVpbGRTeXN0ZW1SZW1pbmRlclNlY3Rpb24ocnVsZXM6IFBhcnNlZFJ1bGVbXSwgc291cmNlczogc3RyaW5nW10pOiBzdHJpbmcge1xuXHRpZiAocnVsZXMubGVuZ3RoID09PSAwKSByZXR1cm4gXCJcIjtcblxuXHRjb25zdCBsaW5lczogc3RyaW5nW10gPSBbXG5cdFx0XCI8c3lzdGVtLXJlbWluZGVyPlwiLFxuXHRcdGBJbnN0cnVjdGlvbnMgZnJvbTogJHtzb3VyY2VzLmpvaW4oXCIsIFwiKX1gLFxuXHRcdFwiVGhlIGZvbGxvd2luZyBydWxlcyBhcmUgYWx3YXlzIGFjdGl2ZS4gRm9sbG93IHRoZW0gc3RyaWN0bHkuXCIsXG5cdFx0XCJcIixcblx0XTtcblxuXHRmb3IgKGNvbnN0IHJ1bGUgb2YgcnVsZXMpIHtcblx0XHRsaW5lcy5wdXNoKGBSdWxlOiAke3J1bGUubmFtZX0gXHUyMDE0ICR7cnVsZS50aXRsZX1gKTtcblx0XHRpZiAocnVsZS5mcm9udG1hdHRlci5kZXNjcmlwdGlvbikge1xuXHRcdFx0bGluZXMucHVzaChgPiAke3J1bGUuZnJvbnRtYXR0ZXIuZGVzY3JpcHRpb259YCk7XG5cdFx0fVxuXHRcdGxpbmVzLnB1c2goXCJcIik7XG5cdFx0bGluZXMucHVzaChydWxlLmNvbnRlbnQpO1xuXHRcdGxpbmVzLnB1c2goXCJcIik7XG5cdH1cblxuXHRsaW5lcy5wdXNoKFwiPC9zeXN0ZW0tcmVtaW5kZXI+XCIpO1xuXHRyZXR1cm4gbGluZXMuam9pbihcIlxcblwiKTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGJ1aWxkVG9vbFJlbWluZGVyU2VjdGlvbihydWxlczogUGFyc2VkUnVsZVtdLCB0YXJnZXRQYXRoOiBzdHJpbmcpOiBzdHJpbmcge1xuXHRpZiAocnVsZXMubGVuZ3RoID09PSAwKSByZXR1cm4gXCJcIjtcblxuXHRjb25zdCBsaW5lczogc3RyaW5nW10gPSBbXG5cdFx0XCI8c3lzdGVtLXJlbWluZGVyPlwiLFxuXHRcdGBDb25kaXRpb25hbCBydWxlcyBtYXRjaGVkIGZvciAke3RhcmdldFBhdGh9OmAsXG5cdFx0XCJcIixcblx0XTtcblxuXHRmb3IgKGNvbnN0IHJ1bGUgb2YgcnVsZXMpIHtcblx0XHRjb25zdCBzZXZlcml0eSA9IHJ1bGUuZnJvbnRtYXR0ZXIuc2V2ZXJpdHkgfHwgXCJtZWRpdW1cIjtcblx0XHRjb25zdCBpY29uID0gU0VWRVJJVFlfSUNPTlNbc2V2ZXJpdHldIHx8IFwiXHVEODNEXHVERkUxXCI7XG5cdFx0bGluZXMucHVzaChgUnVsZTogJHtpY29ufSAke3J1bGUudGl0bGV9IFske3NldmVyaXR5fV1gKTtcblx0XHRpZiAocnVsZS5mcm9udG1hdHRlci5kZXNjcmlwdGlvbikge1xuXHRcdFx0bGluZXMucHVzaChgPiAke3J1bGUuZnJvbnRtYXR0ZXIuZGVzY3JpcHRpb259YCk7XG5cdFx0fVxuXHRcdGxpbmVzLnB1c2goXCJcIik7XG5cdFx0bGluZXMucHVzaChydWxlLmNvbnRlbnQpO1xuXHRcdGxpbmVzLnB1c2goXCJcIik7XG5cdH1cblxuXHRsaW5lcy5wdXNoKFwiPC9zeXN0ZW0tcmVtaW5kZXI+XCIpO1xuXHRyZXR1cm4gbGluZXMuam9pbihcIlxcblwiKTtcbn1cblxuZXhwb3J0IGNvbnN0IGJ1aWxkU3lzdGVtUHJvbXB0U2VjdGlvbiA9IGJ1aWxkU3lzdGVtUmVtaW5kZXJTZWN0aW9uO1xuZXhwb3J0IGNvbnN0IGJ1aWxkVG9vbENvbnRleHRTZWN0aW9uID0gYnVpbGRUb29sUmVtaW5kZXJTZWN0aW9uO1xuXG5leHBvcnQgZnVuY3Rpb24gYnVpbGRDb21wYWN0Q29udGV4dChydWxlczogUGFyc2VkUnVsZVtdKTogc3RyaW5nIHtcblx0aWYgKHJ1bGVzLmxlbmd0aCA9PT0gMCkgcmV0dXJuIFwiXCI7XG5cblx0Y29uc3QgbGluZXM6IHN0cmluZ1tdID0gW1wiIyMgQWN0aXZlIFJ1bGVzIChwZXJzaXN0IGFjcm9zcyBjb21wYWN0aW9uKVwiLCBcIlwiXTtcblxuXHRmb3IgKGNvbnN0IHJ1bGUgb2YgcnVsZXMpIHtcblx0XHRjb25zdCBkZXNjID0gcnVsZS5mcm9udG1hdHRlci5kZXNjcmlwdGlvbiB8fCBydWxlLmNvbnRlbnQuc3BsaXQoXCJcXG5cIilbMF07XG5cdFx0bGluZXMucHVzaChgLSAqKiR7cnVsZS50aXRsZX0qKiAoJHtydWxlLm5hbWV9KTogJHtkZXNjfWApO1xuXHR9XG5cblx0cmV0dXJuIGxpbmVzLmpvaW4oXCJcXG5cIik7XG59XG4iXSwKICAibWFwcGluZ3MiOiAiO0FBRUEsSUFBTSxpQkFBeUM7QUFBQSxFQUM5QyxVQUFVO0FBQUEsRUFDVixNQUFNO0FBQUEsRUFDTixRQUFRO0FBQUEsRUFDUixLQUFLO0FBQUEsRUFDTCxNQUFNO0FBQ1A7QUFFTyxTQUFTLDJCQUEyQixPQUFxQixTQUEyQjtBQUMxRixNQUFJLE1BQU0sV0FBVyxFQUFHLFFBQU87QUFFL0IsUUFBTSxRQUFrQjtBQUFBLElBQ3ZCO0FBQUEsSUFDQSxzQkFBc0IsUUFBUSxLQUFLLElBQUksQ0FBQztBQUFBLElBQ3hDO0FBQUEsSUFDQTtBQUFBLEVBQ0Q7QUFFQSxhQUFXLFFBQVEsT0FBTztBQUN6QixVQUFNLEtBQUssU0FBUyxLQUFLLElBQUksV0FBTSxLQUFLLEtBQUssRUFBRTtBQUMvQyxRQUFJLEtBQUssWUFBWSxhQUFhO0FBQ2pDLFlBQU0sS0FBSyxLQUFLLEtBQUssWUFBWSxXQUFXLEVBQUU7QUFBQSxJQUMvQztBQUNBLFVBQU0sS0FBSyxFQUFFO0FBQ2IsVUFBTSxLQUFLLEtBQUssT0FBTztBQUN2QixVQUFNLEtBQUssRUFBRTtBQUFBLEVBQ2Q7QUFFQSxRQUFNLEtBQUssb0JBQW9CO0FBQy9CLFNBQU8sTUFBTSxLQUFLLElBQUk7QUFDdkI7QUFFTyxTQUFTLHlCQUF5QixPQUFxQixZQUE0QjtBQUN6RixNQUFJLE1BQU0sV0FBVyxFQUFHLFFBQU87QUFFL0IsUUFBTSxRQUFrQjtBQUFBLElBQ3ZCO0FBQUEsSUFDQSxpQ0FBaUMsVUFBVTtBQUFBLElBQzNDO0FBQUEsRUFDRDtBQUVBLGFBQVcsUUFBUSxPQUFPO0FBQ3pCLFVBQU0sV0FBVyxLQUFLLFlBQVksWUFBWTtBQUM5QyxVQUFNLE9BQU8sZUFBZSxRQUFRLEtBQUs7QUFDekMsVUFBTSxLQUFLLFNBQVMsSUFBSSxJQUFJLEtBQUssS0FBSyxLQUFLLFFBQVEsR0FBRztBQUN0RCxRQUFJLEtBQUssWUFBWSxhQUFhO0FBQ2pDLFlBQU0sS0FBSyxLQUFLLEtBQUssWUFBWSxXQUFXLEVBQUU7QUFBQSxJQUMvQztBQUNBLFVBQU0sS0FBSyxFQUFFO0FBQ2IsVUFBTSxLQUFLLEtBQUssT0FBTztBQUN2QixVQUFNLEtBQUssRUFBRTtBQUFBLEVBQ2Q7QUFFQSxRQUFNLEtBQUssb0JBQW9CO0FBQy9CLFNBQU8sTUFBTSxLQUFLLElBQUk7QUFDdkI7QUFFTyxJQUFNLDJCQUEyQjtBQUNqQyxJQUFNLDBCQUEwQjtBQUVoQyxTQUFTLG9CQUFvQixPQUE2QjtBQUNoRSxNQUFJLE1BQU0sV0FBVyxFQUFHLFFBQU87QUFFL0IsUUFBTSxRQUFrQixDQUFDLCtDQUErQyxFQUFFO0FBRTFFLGFBQVcsUUFBUSxPQUFPO0FBQ3pCLFVBQU0sT0FBTyxLQUFLLFlBQVksZUFBZSxLQUFLLFFBQVEsTUFBTSxJQUFJLEVBQUUsQ0FBQztBQUN2RSxVQUFNLEtBQUssT0FBTyxLQUFLLEtBQUssT0FBTyxLQUFLLElBQUksTUFBTSxJQUFJLEVBQUU7QUFBQSxFQUN6RDtBQUVBLFNBQU8sTUFBTSxLQUFLLElBQUk7QUFDdkI7IiwKICAibmFtZXMiOiBbXQp9Cg==
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ParsedRule } from "./types.js";
|
|
2
|
+
|
|
3
|
+
const SEVERITY_ICONS: Record<string, string> = {
|
|
4
|
+
critical: "🔴",
|
|
5
|
+
high: "🟠",
|
|
6
|
+
medium: "🟡",
|
|
7
|
+
low: "🔵",
|
|
8
|
+
hint: "💡",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function buildSystemReminderSection(rules: ParsedRule[], sources: string[]): string {
|
|
12
|
+
if (rules.length === 0) return "";
|
|
13
|
+
|
|
14
|
+
const lines: string[] = [
|
|
15
|
+
"<system-reminder>",
|
|
16
|
+
`Instructions from: ${sources.join(", ")}`,
|
|
17
|
+
"The following rules are always active. Follow them strictly.",
|
|
18
|
+
"",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
for (const rule of rules) {
|
|
22
|
+
lines.push(`Rule: ${rule.name} — ${rule.title}`);
|
|
23
|
+
if (rule.frontmatter.description) {
|
|
24
|
+
lines.push(`> ${rule.frontmatter.description}`);
|
|
25
|
+
}
|
|
26
|
+
lines.push("");
|
|
27
|
+
lines.push(rule.content);
|
|
28
|
+
lines.push("");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
lines.push("</system-reminder>");
|
|
32
|
+
return lines.join("\n");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildToolReminderSection(rules: ParsedRule[], targetPath: string): string {
|
|
36
|
+
if (rules.length === 0) return "";
|
|
37
|
+
|
|
38
|
+
const lines: string[] = [
|
|
39
|
+
"<system-reminder>",
|
|
40
|
+
`Conditional rules matched for ${targetPath}:`,
|
|
41
|
+
"",
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
for (const rule of rules) {
|
|
45
|
+
const severity = rule.frontmatter.severity || "medium";
|
|
46
|
+
const icon = SEVERITY_ICONS[severity] || "🟡";
|
|
47
|
+
lines.push(`Rule: ${icon} ${rule.title} [${severity}]`);
|
|
48
|
+
if (rule.frontmatter.description) {
|
|
49
|
+
lines.push(`> ${rule.frontmatter.description}`);
|
|
50
|
+
}
|
|
51
|
+
lines.push("");
|
|
52
|
+
lines.push(rule.content);
|
|
53
|
+
lines.push("");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
lines.push("</system-reminder>");
|
|
57
|
+
return lines.join("\n");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const buildSystemPromptSection = buildSystemReminderSection;
|
|
61
|
+
export const buildToolContextSection = buildToolReminderSection;
|
|
62
|
+
|
|
63
|
+
export function buildCompactContext(rules: ParsedRule[]): string {
|
|
64
|
+
if (rules.length === 0) return "";
|
|
65
|
+
|
|
66
|
+
const lines: string[] = ["## Active Rules (persist across compaction)", ""];
|
|
67
|
+
|
|
68
|
+
for (const rule of rules) {
|
|
69
|
+
const desc = rule.frontmatter.description || rule.content.split("\n")[0];
|
|
70
|
+
lines.push(`- **${rule.title}** (${rule.name}): ${desc}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return lines.join("\n");
|
|
74
|
+
}
|