supipowers 1.2.5 → 1.3.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/package.json +1 -1
- package/skills/context-mode/SKILL.md +3 -7
- package/src/bootstrap.ts +3 -0
- package/src/commands/commit.ts +8 -0
- package/src/commands/fix-pr.ts +20 -49
- package/src/commands/mcp.ts +14 -0
- package/src/commands/model.ts +3 -2
- package/src/commands/optimize-context.ts +202 -0
- package/src/commands/plan.ts +8 -9
- package/src/commands/qa.ts +15 -0
- package/src/commands/release.ts +16 -1
- package/src/commands/review.ts +7 -8
- package/src/commands/supi.ts +1 -0
- package/src/config/model-resolver.ts +87 -0
- package/src/context/analyzer.ts +57 -0
- package/src/context/optimizer.ts +199 -0
- package/src/context-mode/compressor.ts +14 -11
- package/src/context-mode/event-extractor.ts +45 -16
- package/src/context-mode/event-store.ts +225 -16
- package/src/context-mode/hooks.ts +62 -7
- package/src/context-mode/routing.ts +9 -14
- package/src/context-mode/snapshot-builder.ts +243 -7
- package/src/fix-pr/config.ts +0 -5
- package/src/fix-pr/prompt-builder.ts +7 -6
- package/src/fix-pr/types.ts +0 -11
- package/src/git/commit.ts +74 -26
- package/src/planning/approval-flow.ts +14 -1
- package/src/platform/omp.ts +5 -2
- package/src/platform/types.ts +2 -1
|
@@ -111,3 +111,90 @@ export function createModelBridge(platform: Platform): ModelPlatformBridge {
|
|
|
111
111
|
},
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Apply a resolved model override to the current session.
|
|
117
|
+
*
|
|
118
|
+
* Resolves the string model ID to an OMP Model object via the context's
|
|
119
|
+
* modelRegistry, then calls platform.setModel() with it. Also applies
|
|
120
|
+
* thinking level if specified.
|
|
121
|
+
*
|
|
122
|
+
* @param platform - The platform adapter
|
|
123
|
+
* @param ctx - Command handler context (must have modelRegistry.getAvailable())
|
|
124
|
+
* @param actionId - The action being configured (e.g. "plan", "review") — used in notification
|
|
125
|
+
* @param resolved - The resolved model from resolveModelForAction()
|
|
126
|
+
* @returns true if model was applied, false if skipped or failed
|
|
127
|
+
*/
|
|
128
|
+
export async function applyModelOverride(
|
|
129
|
+
platform: Platform,
|
|
130
|
+
ctx: any,
|
|
131
|
+
actionId: string,
|
|
132
|
+
resolved: ResolvedModel,
|
|
133
|
+
): Promise<boolean> {
|
|
134
|
+
// Skip if resolution fell through to the main session model (nothing to change)
|
|
135
|
+
if (resolved.source === "main") return false;
|
|
136
|
+
|
|
137
|
+
const modelId = resolved.model;
|
|
138
|
+
if (!modelId) return false;
|
|
139
|
+
|
|
140
|
+
// Apply thinking level (independent of model switch success)
|
|
141
|
+
if (resolved.thinkingLevel && platform.setThinkingLevel) {
|
|
142
|
+
platform.setThinkingLevel(resolved.thinkingLevel);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!platform.setModel) return false;
|
|
146
|
+
|
|
147
|
+
// Resolve string model ID to full OMP Model object via the context's model registry.
|
|
148
|
+
// OMP's setModel expects a Model object (with provider, id, api, etc.), not a string.
|
|
149
|
+
const available = ctx.modelRegistry?.getAvailable?.() as any[] | undefined;
|
|
150
|
+
if (!available) return false;
|
|
151
|
+
|
|
152
|
+
const modelObj = available.find((m: any) => {
|
|
153
|
+
if (!m?.id) return false;
|
|
154
|
+
if (modelId === m.id) return true;
|
|
155
|
+
if (modelId === `${m.provider}/${m.id}`) return true;
|
|
156
|
+
return modelId.includes("/") ? false : m.id === modelId;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (!modelObj) return false;
|
|
160
|
+
|
|
161
|
+
// Save current model so we can restore after the agent turn completes.
|
|
162
|
+
// OMP's extension API setModel() persists to settings (calls session.setModel,
|
|
163
|
+
// not session.setModelTemporary). We must restore to avoid permanently
|
|
164
|
+
// overriding the user's default model.
|
|
165
|
+
const originalModel = ctx.model;
|
|
166
|
+
|
|
167
|
+
const applied = await platform.setModel(modelObj);
|
|
168
|
+
if (!applied) return false;
|
|
169
|
+
|
|
170
|
+
// Show persistent model override info in the footer status bar.
|
|
171
|
+
// ctx.ui.notify() is transient and gets immediately replaced by progress widgets;
|
|
172
|
+
// setStatus persists alongside them.
|
|
173
|
+
const STATUS_KEY = "supi-model";
|
|
174
|
+
const displayName = modelObj.name ?? modelObj.id ?? modelId;
|
|
175
|
+
const sourceLabel =
|
|
176
|
+
resolved.source === "action" ? `configured for ${actionId}` :
|
|
177
|
+
resolved.source === "default" ? "supipowers default" :
|
|
178
|
+
"harness role";
|
|
179
|
+
let detail = sourceLabel;
|
|
180
|
+
if (resolved.thinkingLevel) {
|
|
181
|
+
detail += ` \u00b7 ${resolved.thinkingLevel} thinking`;
|
|
182
|
+
}
|
|
183
|
+
ctx.ui?.setStatus?.(STATUS_KEY, `Model: ${displayName} (${detail})`);
|
|
184
|
+
|
|
185
|
+
// Register a one-shot agent_end hook to restore the original model
|
|
186
|
+
// and clear the status bar entry.
|
|
187
|
+
{
|
|
188
|
+
let restored = false;
|
|
189
|
+
platform.on("agent_end", async () => {
|
|
190
|
+
if (restored) return;
|
|
191
|
+
restored = true;
|
|
192
|
+
ctx.ui?.setStatus?.(STATUS_KEY, undefined);
|
|
193
|
+
if (originalModel) {
|
|
194
|
+
await platform.setModel!(originalModel);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return true;
|
|
200
|
+
}
|
package/src/context/analyzer.ts
CHANGED
|
@@ -43,6 +43,63 @@ export function parseSystemPrompt(text: string): PromptSection[] {
|
|
|
43
43
|
return sections;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// ── Per-Skill Parser ──────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/** A single skill extracted from the system prompt */
|
|
49
|
+
export interface ParsedSkill {
|
|
50
|
+
name: string;
|
|
51
|
+
bytes: number;
|
|
52
|
+
tokens: number;
|
|
53
|
+
content: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Extract individual skills from system prompt text */
|
|
57
|
+
export function parseIndividualSkills(systemPrompt: string): ParsedSkill[] {
|
|
58
|
+
if (!systemPrompt) return [];
|
|
59
|
+
|
|
60
|
+
// OMP renders skills as markdown headings under "# Skills":
|
|
61
|
+
// ## skill-name
|
|
62
|
+
// Description text...
|
|
63
|
+
// Find the Skills section bounded by the next h1 heading or end of text.
|
|
64
|
+
const skillsSectionMatch = systemPrompt.match(
|
|
65
|
+
/^# Skills\n[\s\S]*?\n(?=##\s)/m,
|
|
66
|
+
);
|
|
67
|
+
if (!skillsSectionMatch) return [];
|
|
68
|
+
|
|
69
|
+
// Extract the region from first ## to the next # (h1) or end of text
|
|
70
|
+
const sectionStart = skillsSectionMatch.index! + skillsSectionMatch[0].length;
|
|
71
|
+
const afterSection = systemPrompt.slice(sectionStart);
|
|
72
|
+
const nextH1 = afterSection.search(/^# [^#]/m);
|
|
73
|
+
const skillsBody =
|
|
74
|
+
skillsSectionMatch[0].slice(skillsSectionMatch[0].indexOf("\n## ") + 1) +
|
|
75
|
+
(nextH1 === -1 ? afterSection : afterSection.slice(0, nextH1));
|
|
76
|
+
|
|
77
|
+
// Split on ## headings
|
|
78
|
+
const skills: ParsedSkill[] = [];
|
|
79
|
+
const headingRegex = /^## (.+)$/gm;
|
|
80
|
+
const headings: { name: string; index: number }[] = [];
|
|
81
|
+
let match;
|
|
82
|
+
|
|
83
|
+
while ((match = headingRegex.exec(skillsBody)) !== null) {
|
|
84
|
+
headings.push({ name: match[1].trim(), index: match.index });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < headings.length; i++) {
|
|
88
|
+
const start = headings[i].index;
|
|
89
|
+
const end = i + 1 < headings.length ? headings[i + 1].index : skillsBody.length;
|
|
90
|
+
const content = skillsBody.slice(start, end).trimEnd();
|
|
91
|
+
const bytes = byteLength(content);
|
|
92
|
+
skills.push({
|
|
93
|
+
name: headings[i].name,
|
|
94
|
+
bytes,
|
|
95
|
+
tokens: estimateTokens(content),
|
|
96
|
+
content,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return skills;
|
|
101
|
+
}
|
|
102
|
+
|
|
46
103
|
// ── Breakdown Builder ─────────────────────────────────────
|
|
47
104
|
|
|
48
105
|
/** Context usage data from OMP runtime */
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { Platform } from "../platform/types.js";
|
|
2
|
+
import type { ParsedSkill, PromptSection } from "./analyzer.js";
|
|
3
|
+
import { estimateTokens } from "./analyzer.js";
|
|
4
|
+
|
|
5
|
+
// ── Types ───────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export interface TechStack {
|
|
8
|
+
languages: string[];
|
|
9
|
+
frameworks: string[];
|
|
10
|
+
tools: string[];
|
|
11
|
+
runtime: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A skill with its token cost — no classification judgment. */
|
|
15
|
+
export interface SkillEntry {
|
|
16
|
+
name: string;
|
|
17
|
+
tokens: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A non-skill section with token cost and optional note. */
|
|
21
|
+
export interface SectionEntry {
|
|
22
|
+
label: string;
|
|
23
|
+
tokens: number;
|
|
24
|
+
note: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Raw context report — data only, no classification. */
|
|
28
|
+
export interface ContextReport {
|
|
29
|
+
totalTokens: number;
|
|
30
|
+
techStack: TechStack;
|
|
31
|
+
skills: SkillEntry[];
|
|
32
|
+
sections: SectionEntry[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Framework / Tool Detection Maps ─────────────────────────
|
|
36
|
+
|
|
37
|
+
/** Maps package.json dependency names → detected framework or tool */
|
|
38
|
+
const DEP_TO_FRAMEWORK: Record<string, string> = {
|
|
39
|
+
react: "react",
|
|
40
|
+
"react-dom": "react",
|
|
41
|
+
next: "next",
|
|
42
|
+
vue: "vue",
|
|
43
|
+
svelte: "svelte",
|
|
44
|
+
"@sveltejs/kit": "svelte",
|
|
45
|
+
express: "express",
|
|
46
|
+
fastify: "fastify",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const DEP_TO_TOOL: Record<string, string> = {
|
|
50
|
+
tailwindcss: "tailwind",
|
|
51
|
+
"@playwright/test": "playwright",
|
|
52
|
+
prisma: "prisma",
|
|
53
|
+
"@prisma/client": "prisma",
|
|
54
|
+
"@shadcn/ui": "shadcn",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// ── Tech Stack Detection ────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/** Read a file via platform.exec; returns content or null on failure. */
|
|
60
|
+
async function tryRead(
|
|
61
|
+
platform: Platform,
|
|
62
|
+
cwd: string,
|
|
63
|
+
filename: string,
|
|
64
|
+
): Promise<string | null> {
|
|
65
|
+
const r = await platform.exec("cat", [filename], { cwd });
|
|
66
|
+
return r.code === 0 ? r.stdout : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Check if a file exists at `cwd/filename` via exec. */
|
|
70
|
+
async function fileExists(
|
|
71
|
+
platform: Platform,
|
|
72
|
+
cwd: string,
|
|
73
|
+
filename: string,
|
|
74
|
+
): Promise<boolean> {
|
|
75
|
+
const r = await platform.exec("test", ["-f", filename], { cwd });
|
|
76
|
+
return r.code === 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Deterministic tech-stack detection — no LLM.
|
|
81
|
+
* Inspects package.json, lockfiles, and config files.
|
|
82
|
+
*/
|
|
83
|
+
export async function detectTechStack(
|
|
84
|
+
platform: Platform,
|
|
85
|
+
cwd: string,
|
|
86
|
+
): Promise<TechStack> {
|
|
87
|
+
const languages = new Set<string>();
|
|
88
|
+
const frameworks = new Set<string>();
|
|
89
|
+
const tools = new Set<string>();
|
|
90
|
+
let runtime: string | null = null;
|
|
91
|
+
|
|
92
|
+
// 1. package.json — deps & devDeps
|
|
93
|
+
const pkgRaw = await tryRead(platform, cwd, "package.json");
|
|
94
|
+
if (pkgRaw) {
|
|
95
|
+
try {
|
|
96
|
+
const pkg = JSON.parse(pkgRaw);
|
|
97
|
+
const allDeps: Record<string, string> = {
|
|
98
|
+
...pkg.dependencies,
|
|
99
|
+
...pkg.devDependencies,
|
|
100
|
+
};
|
|
101
|
+
for (const dep of Object.keys(allDeps)) {
|
|
102
|
+
const fw = DEP_TO_FRAMEWORK[dep];
|
|
103
|
+
if (fw) frameworks.add(fw);
|
|
104
|
+
const tl = DEP_TO_TOOL[dep];
|
|
105
|
+
if (tl) tools.add(tl);
|
|
106
|
+
}
|
|
107
|
+
if (allDeps.typescript) languages.add("typescript");
|
|
108
|
+
} catch {
|
|
109
|
+
// malformed package.json — continue with file-based detection
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 2. Lockfiles → runtime (first match wins, most-specific first)
|
|
114
|
+
if (await fileExists(platform, cwd, "bun.lock")) {
|
|
115
|
+
runtime = "bun";
|
|
116
|
+
} else if (await fileExists(platform, cwd, "package-lock.json")) {
|
|
117
|
+
runtime = "node";
|
|
118
|
+
} else if (await fileExists(platform, cwd, "pnpm-lock.yaml")) {
|
|
119
|
+
runtime = "node";
|
|
120
|
+
} else if (await fileExists(platform, cwd, "yarn.lock")) {
|
|
121
|
+
runtime = "node";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 3. Config files → languages
|
|
125
|
+
const configChecks: [string, string][] = [
|
|
126
|
+
["tsconfig.json", "typescript"],
|
|
127
|
+
["Cargo.toml", "rust"],
|
|
128
|
+
["pyproject.toml", "python"],
|
|
129
|
+
["requirements.txt", "python"],
|
|
130
|
+
["go.mod", "go"],
|
|
131
|
+
["Gemfile", "ruby"],
|
|
132
|
+
];
|
|
133
|
+
const results = await Promise.all(
|
|
134
|
+
configChecks.map(([file, lang]) =>
|
|
135
|
+
fileExists(platform, cwd, file).then((exists) =>
|
|
136
|
+
exists ? lang : null,
|
|
137
|
+
),
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
for (const lang of results) {
|
|
141
|
+
if (lang) languages.add(lang);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
languages: [...languages],
|
|
146
|
+
frameworks: [...frameworks],
|
|
147
|
+
tools: [...tools],
|
|
148
|
+
runtime,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Context Report ──────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Build a raw context report from parsed prompt data.
|
|
156
|
+
* Gathers token costs per skill and per section, flags anomalies.
|
|
157
|
+
* Does NOT classify or recommend — that's the LLM's job.
|
|
158
|
+
*/
|
|
159
|
+
export function buildContextReport(
|
|
160
|
+
sections: PromptSection[],
|
|
161
|
+
skills: ParsedSkill[],
|
|
162
|
+
techStack: TechStack,
|
|
163
|
+
): ContextReport {
|
|
164
|
+
const totalTokens = sections.reduce(
|
|
165
|
+
(sum, s) => sum + estimateTokens(s.content),
|
|
166
|
+
0,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const skillEntries: SkillEntry[] = skills.map((s) => ({
|
|
170
|
+
name: s.name,
|
|
171
|
+
tokens: s.tokens,
|
|
172
|
+
}));
|
|
173
|
+
|
|
174
|
+
// Annotate non-skill sections with notes for anomalies
|
|
175
|
+
const sectionEntries: SectionEntry[] = [];
|
|
176
|
+
|
|
177
|
+
const routingSections = sections.filter((s) =>
|
|
178
|
+
s.label.toLowerCase().includes("routing"),
|
|
179
|
+
);
|
|
180
|
+
const hasRoutingDupes = routingSections.length > 1;
|
|
181
|
+
|
|
182
|
+
for (const s of sections) {
|
|
183
|
+
// Skip the aggregate "Skills (N)" section — per-skill data is separate
|
|
184
|
+
if (s.label.toLowerCase().startsWith("skills")) continue;
|
|
185
|
+
|
|
186
|
+
const tokens = estimateTokens(s.content);
|
|
187
|
+
let note = "";
|
|
188
|
+
|
|
189
|
+
if (s.label.toLowerCase().includes("routing") && hasRoutingDupes) {
|
|
190
|
+
note = `Duplicate (${routingSections.length} found) — consolidate`;
|
|
191
|
+
} else if (s.label.toLowerCase().includes("memory") && tokens > 500) {
|
|
192
|
+
note = "Large — consider compressing or summarizing";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
sectionEntries.push({ label: s.label, tokens, note });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { totalTokens, techStack, skills: skillEntries, sections: sectionEntries };
|
|
199
|
+
}
|
|
@@ -14,7 +14,8 @@ interface ToolResultEventResult {
|
|
|
14
14
|
|
|
15
15
|
const BASH_HEAD_LINES = 5;
|
|
16
16
|
const BASH_TAIL_LINES = 10;
|
|
17
|
-
const
|
|
17
|
+
const READ_HEAD_LINES = 80;
|
|
18
|
+
const READ_TAIL_LINES = 30;
|
|
18
19
|
const GREP_MAX_MATCHES = 10;
|
|
19
20
|
const FIND_MAX_PATHS = 20;
|
|
20
21
|
|
|
@@ -68,23 +69,25 @@ function compressBash(text: string, details: unknown): string | undefined {
|
|
|
68
69
|
].join("\n");
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
/** Compress read tool output */
|
|
72
|
+
/** Compress read tool output — head+tail with hashline preservation */
|
|
72
73
|
function compressRead(text: string, input: Record<string, unknown>): string | undefined {
|
|
73
|
-
// Scoped reads (offset/limit) are already targeted — pass through
|
|
74
|
-
if (input.offset
|
|
74
|
+
// Scoped reads (offset/limit/sel) are already targeted — pass through
|
|
75
|
+
if (input.offset != null || input.limit != null || input.sel != null) return undefined;
|
|
75
76
|
|
|
76
77
|
const lines = text.split("\n");
|
|
77
78
|
const totalLines = lines.length;
|
|
78
|
-
const path = typeof input.path === "string" ? input.path : "unknown";
|
|
79
79
|
|
|
80
|
-
if (totalLines <=
|
|
80
|
+
if (totalLines <= READ_HEAD_LINES + READ_TAIL_LINES) return undefined;
|
|
81
|
+
|
|
82
|
+
const head = lines.slice(0, READ_HEAD_LINES);
|
|
83
|
+
const tail = lines.slice(-READ_TAIL_LINES);
|
|
84
|
+
const omittedStart = READ_HEAD_LINES + 1;
|
|
85
|
+
const omittedEnd = totalLines - READ_TAIL_LINES;
|
|
81
86
|
|
|
82
|
-
const preview = lines.slice(0, READ_PREVIEW_LINES);
|
|
83
87
|
return [
|
|
84
|
-
|
|
85
|
-
""
|
|
86
|
-
...
|
|
87
|
-
`[...compressed: remaining ${totalLines - READ_PREVIEW_LINES} lines omitted...]`,
|
|
88
|
+
...head,
|
|
89
|
+
`[...${omittedEnd - omittedStart + 1} lines omitted. Use read(path, sel="L${omittedStart}-L${omittedEnd}") to view...]`,
|
|
90
|
+
...tail,
|
|
88
91
|
].join("\n");
|
|
89
92
|
}
|
|
90
93
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// src/context-mode/event-extractor.ts
|
|
2
|
+
import { PRIORITY } from "./event-store.js";
|
|
2
3
|
import type { EventCategory, EventPriority, TrackedEvent } from "./event-store.js";
|
|
3
4
|
|
|
4
|
-
type Event = Omit<TrackedEvent, "id">;
|
|
5
|
+
type Event = Omit<TrackedEvent, "id" | "dataHash">;
|
|
5
6
|
|
|
6
7
|
const GIT_COMMAND_PATTERNS = [
|
|
7
8
|
/^git\s+(commit|merge|rebase|checkout|switch|branch|push|pull|stash|reset|cherry-pick|tag)\b/,
|
|
@@ -39,8 +40,7 @@ function getTextContent(content: Array<{ type: string; text?: string }>): string
|
|
|
39
40
|
return content
|
|
40
41
|
.filter((c) => c.type === "text" && c.text)
|
|
41
42
|
.map((c) => c.text!)
|
|
42
|
-
.join("\n")
|
|
43
|
-
.slice(0, 500); // Cap for storage
|
|
43
|
+
.join("\n");
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/** Extract events from a tool result */
|
|
@@ -62,21 +62,29 @@ export function extractEvents(
|
|
|
62
62
|
events.push(makeEvent(sessionId, "error", {
|
|
63
63
|
toolName: event.toolName,
|
|
64
64
|
content: text,
|
|
65
|
-
},
|
|
65
|
+
}, PRIORITY.critical, "tool_result"));
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
switch (event.toolName) {
|
|
69
69
|
case "bash":
|
|
70
70
|
extractBash(events, event, sessionId, text);
|
|
71
71
|
break;
|
|
72
|
-
case "read":
|
|
72
|
+
case "read": {
|
|
73
|
+
const readPath = typeof event.input.path === "string" ? event.input.path : "";
|
|
74
|
+
if (/AGENTS\.md$/.test(readPath) || /CLAUDE\.md$/.test(readPath) || /\.omp\//.test(readPath)) {
|
|
75
|
+
events.push(makeEvent(sessionId, "rule", { path: readPath, type: "project-rule" }, PRIORITY.critical, "tool_result"));
|
|
76
|
+
}
|
|
77
|
+
if (readPath.includes("/skills/")) {
|
|
78
|
+
events.push(makeEvent(sessionId, "skill", { path: readPath }, PRIORITY.medium, "tool_result"));
|
|
79
|
+
}
|
|
73
80
|
extractFile(events, event, sessionId, "read");
|
|
74
81
|
break;
|
|
82
|
+
}
|
|
75
83
|
case "edit":
|
|
76
|
-
extractFile(events, event, sessionId, "edit",
|
|
84
|
+
extractFile(events, event, sessionId, "edit", PRIORITY.high);
|
|
77
85
|
break;
|
|
78
86
|
case "write":
|
|
79
|
-
extractFile(events, event, sessionId, "write",
|
|
87
|
+
extractFile(events, event, sessionId, "write", PRIORITY.high);
|
|
80
88
|
break;
|
|
81
89
|
case "grep":
|
|
82
90
|
extractFile(events, event, sessionId, "search");
|
|
@@ -87,18 +95,18 @@ export function extractEvents(
|
|
|
87
95
|
case "todo_write":
|
|
88
96
|
events.push(makeEvent(sessionId, "task", {
|
|
89
97
|
input: event.input,
|
|
90
|
-
},
|
|
98
|
+
}, PRIORITY.high, "tool_result"));
|
|
91
99
|
break;
|
|
92
100
|
default:
|
|
93
101
|
if (event.toolName.startsWith("ctx_")) {
|
|
94
102
|
events.push(makeEvent(sessionId, "mcp", {
|
|
95
103
|
tool: event.toolName,
|
|
96
|
-
},
|
|
104
|
+
}, PRIORITY.low, "tool_result"));
|
|
97
105
|
} else if (event.toolName === "task" || event.toolName === "sub_agent") {
|
|
98
106
|
events.push(makeEvent(sessionId, "subagent", {
|
|
99
107
|
toolName: event.toolName,
|
|
100
108
|
input: event.input,
|
|
101
|
-
},
|
|
109
|
+
}, PRIORITY.medium, "tool_result"));
|
|
102
110
|
}
|
|
103
111
|
// Unknown tools: no events
|
|
104
112
|
break;
|
|
@@ -123,7 +131,7 @@ function extractBash(
|
|
|
123
131
|
events.push(makeEvent(sessionId, "git", {
|
|
124
132
|
command,
|
|
125
133
|
output: text,
|
|
126
|
-
},
|
|
134
|
+
}, PRIORITY.high, "tool_result"));
|
|
127
135
|
}
|
|
128
136
|
|
|
129
137
|
// Non-zero exit (in addition to general isError rule)
|
|
@@ -132,14 +140,28 @@ function extractBash(
|
|
|
132
140
|
command,
|
|
133
141
|
exitCode,
|
|
134
142
|
output: text,
|
|
135
|
-
},
|
|
143
|
+
}, PRIORITY.critical, "tool_result"));
|
|
136
144
|
}
|
|
137
145
|
|
|
138
146
|
// Working directory change
|
|
139
147
|
if (/\bcd\s+/.test(command)) {
|
|
140
148
|
events.push(makeEvent(sessionId, "cwd", {
|
|
141
149
|
command,
|
|
142
|
-
},
|
|
150
|
+
}, PRIORITY.low, "tool_result"));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Environment/version commands
|
|
154
|
+
const ENV_PATTERNS = [
|
|
155
|
+
/^(node|bun|python|ruby|go)\s+--?version/,
|
|
156
|
+
/^(npm|yarn|pnpm|pip|cargo)\s+--?version/,
|
|
157
|
+
/\bprintenv\b/,
|
|
158
|
+
/\becho\s+\$\w+/,
|
|
159
|
+
];
|
|
160
|
+
if (ENV_PATTERNS.some((p) => p.test(command))) {
|
|
161
|
+
events.push(makeEvent(sessionId, "env", {
|
|
162
|
+
command,
|
|
163
|
+
output: text,
|
|
164
|
+
}, PRIORITY.medium, "tool_result"));
|
|
143
165
|
}
|
|
144
166
|
}
|
|
145
167
|
|
|
@@ -148,7 +170,7 @@ function extractFile(
|
|
|
148
170
|
event: { input: Record<string, unknown> },
|
|
149
171
|
sessionId: string,
|
|
150
172
|
op: string,
|
|
151
|
-
priority: EventPriority =
|
|
173
|
+
priority: EventPriority = PRIORITY.medium,
|
|
152
174
|
): void {
|
|
153
175
|
const path = typeof event.input.path === "string" ? event.input.path : "unknown";
|
|
154
176
|
events.push(makeEvent(sessionId, "file", { op, path }, priority, "tool_result"));
|
|
@@ -159,11 +181,18 @@ export function extractPromptEvents(prompt: string, sessionId: string): Event[]
|
|
|
159
181
|
const events: Event[] = [];
|
|
160
182
|
|
|
161
183
|
// Always capture the prompt
|
|
162
|
-
events.push(makeEvent(sessionId, "prompt", { prompt },
|
|
184
|
+
events.push(makeEvent(sessionId, "prompt", { prompt }, PRIORITY.high, "before_agent_start"));
|
|
163
185
|
|
|
164
186
|
// Check for decision patterns
|
|
165
187
|
if (DECISION_PATTERNS.some((p) => p.test(prompt))) {
|
|
166
|
-
events.push(makeEvent(sessionId, "decision", { prompt },
|
|
188
|
+
events.push(makeEvent(sessionId, "decision", { prompt }, PRIORITY.high, "before_agent_start"));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Detect high-level intent markers
|
|
192
|
+
const INTENT_PATTERN = /\b(build|fix|refactor|test|review|deploy|debug|plan|implement|create|delete|remove|update|add|migrate)\b/i;
|
|
193
|
+
const intentMatch = prompt.match(INTENT_PATTERN);
|
|
194
|
+
if (intentMatch) {
|
|
195
|
+
events.push(makeEvent(sessionId, "intent", { intent: intentMatch[1].toLowerCase(), prompt }, PRIORITY.low, "before_agent_start"));
|
|
167
196
|
}
|
|
168
197
|
|
|
169
198
|
return events;
|