@czottmann/pi-automode 1.7.0 → 1.8.1
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 +10 -6
- package/docs/automode-classifier-flow.md +31 -46
- package/docs/defaults.md +13 -2
- package/docs/observability-logging.md +28 -12
- package/extensions/auto-mode/classifier.ts +270 -59
- package/extensions/auto-mode/config.ts +33 -11
- package/extensions/auto-mode/constants.ts +9 -4
- package/extensions/auto-mode/extension.ts +20 -33
- package/extensions/auto-mode/hard-deny.ts +5 -2
- package/extensions/auto-mode/log.ts +17 -2
- package/extensions/auto-mode/paths.ts +67 -36
- package/extensions/auto-mode/transcript.ts +181 -33
- package/extensions/auto-mode/types.ts +20 -4
- package/extensions/auto-mode.ts +1 -1
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, extname, join } from "node:path";
|
|
4
4
|
import type {
|
|
5
|
+
ClassifierIo,
|
|
5
6
|
ClassifierIoAttempt,
|
|
6
7
|
ClassificationDecision,
|
|
7
8
|
DecisionKind,
|
|
@@ -28,13 +29,27 @@ export type ClassifierLogEntry = {
|
|
|
28
29
|
ts: string;
|
|
29
30
|
decisionId: string;
|
|
30
31
|
model: string;
|
|
31
|
-
prompt:
|
|
32
|
+
prompt: ClassifierIo["prompt"];
|
|
32
33
|
attempts: ClassifierIoAttempt[];
|
|
33
34
|
durationMs: number;
|
|
34
35
|
parsed: ClassificationDecision;
|
|
35
36
|
};
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
/** A ccusage-compatible record for one classifier model response. */
|
|
39
|
+
export type ClassifierUsageLogEntry = {
|
|
40
|
+
type: "message";
|
|
41
|
+
timestamp: string;
|
|
42
|
+
message: {
|
|
43
|
+
role: "assistant";
|
|
44
|
+
model: string;
|
|
45
|
+
usage: NonNullable<ClassifierIoAttempt["response"]>["usage"];
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type LogEntry =
|
|
50
|
+
| DecisionLogEntry
|
|
51
|
+
| ClassifierLogEntry
|
|
52
|
+
| ClassifierUsageLogEntry;
|
|
38
53
|
|
|
39
54
|
export type Logger = {
|
|
40
55
|
enabled: boolean;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { realpathSync } from "node:fs";
|
|
1
|
+
import { lstatSync, readlinkSync, realpathSync } from "node:fs";
|
|
2
2
|
import {
|
|
3
3
|
basename,
|
|
4
4
|
dirname,
|
|
5
5
|
isAbsolute,
|
|
6
|
-
join,
|
|
7
6
|
normalize,
|
|
8
7
|
relative,
|
|
9
8
|
resolve,
|
|
@@ -34,53 +33,85 @@ export function isInside(child: string, parent: string): boolean {
|
|
|
34
33
|
return rel === "" || (!!rel && !rel.startsWith("..") && !isAbsolute(rel));
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
|
|
36
|
+
/** Resolve symlinks through the nearest existing ancestor of a path. */
|
|
37
|
+
export function resolvePathForPolicy(path: string): string | undefined {
|
|
38
|
+
return resolvePathForPolicyInner(resolve(path), new Set<string>());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolvePathForPolicyInner(
|
|
38
42
|
path: string,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
resolved = realpathSync(path);
|
|
46
|
-
} catch {
|
|
47
|
-
// File doesn't exist yet — try resolving the parent directory.
|
|
43
|
+
visitedSymlinks: Set<string>,
|
|
44
|
+
): string | undefined {
|
|
45
|
+
let current = path;
|
|
46
|
+
const missingSegments: string[] = [];
|
|
47
|
+
|
|
48
|
+
while (true) {
|
|
48
49
|
try {
|
|
49
|
-
|
|
50
|
-
const base = basename(path);
|
|
51
|
-
resolved = join(realpathSync(dir), base);
|
|
50
|
+
return resolve(realpathSync(current), ...missingSegments);
|
|
52
51
|
} catch {
|
|
53
|
-
|
|
52
|
+
try {
|
|
53
|
+
const stat = lstatSync(current);
|
|
54
|
+
if (!stat.isSymbolicLink() || visitedSymlinks.has(current)) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
visitedSymlinks.add(current);
|
|
58
|
+
const target = resolve(dirname(current), readlinkSync(current));
|
|
59
|
+
return resolvePathForPolicyInner(
|
|
60
|
+
resolve(target, ...missingSegments),
|
|
61
|
+
visitedSymlinks,
|
|
62
|
+
);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const code = error && typeof error === "object" && "code" in error
|
|
65
|
+
? String(error.code)
|
|
66
|
+
: undefined;
|
|
67
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") return undefined;
|
|
68
|
+
const parent = dirname(current);
|
|
69
|
+
if (parent === current) return undefined;
|
|
70
|
+
missingSegments.unshift(basename(current));
|
|
71
|
+
current = parent;
|
|
72
|
+
}
|
|
54
73
|
}
|
|
55
74
|
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function matchesProtectedPath(
|
|
78
|
+
relativePath: string,
|
|
79
|
+
protectedPaths: string[],
|
|
80
|
+
): boolean {
|
|
81
|
+
const normalizedPath = relativePath.replace(/\\/g, "/");
|
|
82
|
+
return protectedPaths.some((pattern) => {
|
|
83
|
+
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
84
|
+
return normalizedPath === normalizedPattern ||
|
|
85
|
+
normalizedPath.startsWith(`${normalizedPattern}/`);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function isProtectedPath(
|
|
90
|
+
path: string,
|
|
91
|
+
cwd: string,
|
|
92
|
+
protectedPaths: string[],
|
|
93
|
+
): boolean {
|
|
94
|
+
// Resolve through the nearest existing ancestor so symlinked directories are
|
|
95
|
+
// respected even when the final write target does not exist yet.
|
|
96
|
+
const resolved = resolvePathForPolicy(path) ?? path;
|
|
97
|
+
const resolvedCwd = resolvePathForPolicy(cwd) ?? cwd;
|
|
56
98
|
|
|
57
99
|
// For paths inside the project: use relative path for matching.
|
|
58
|
-
if (resolved
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
relativePath.startsWith(`${pattern}/`)
|
|
64
|
-
) {
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return false;
|
|
100
|
+
if (isInside(resolved, resolvedCwd)) {
|
|
101
|
+
return matchesProtectedPath(
|
|
102
|
+
relative(resolvedCwd, resolved),
|
|
103
|
+
protectedPaths,
|
|
104
|
+
);
|
|
69
105
|
}
|
|
70
106
|
|
|
71
107
|
// For paths outside the project: check every path component suffix.
|
|
72
108
|
// This catches writes like ../other-project/.git/config even when cwd
|
|
73
109
|
// doesn't contain the target.
|
|
74
|
-
const
|
|
110
|
+
const normalizedResolved = resolved.replace(/\\/g, "/");
|
|
111
|
+
const segments = normalizedResolved.split("/").filter(Boolean);
|
|
75
112
|
for (let i = 0; i < segments.length; i++) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
suffix === pattern ||
|
|
80
|
-
suffix.startsWith(`${pattern}/`)
|
|
81
|
-
) {
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
113
|
+
if (matchesProtectedPath(segments.slice(i).join("/"), protectedPaths)) {
|
|
114
|
+
return true;
|
|
84
115
|
}
|
|
85
116
|
}
|
|
86
117
|
return false;
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { safeJson, truncateMiddle } from "./utils.ts";
|
|
3
3
|
|
|
4
|
+
const MAX_USER_ENTRY_TOKENS = 1000;
|
|
5
|
+
const MAX_TOOL_ENTRY_TOKENS = 1000;
|
|
6
|
+
const MAX_RECENT_TOOL_ENTRIES = 40;
|
|
7
|
+
const CHARS_PER_APPROX_TOKEN = 4;
|
|
8
|
+
|
|
9
|
+
type TranscriptEntry = {
|
|
10
|
+
index: number;
|
|
11
|
+
order: number;
|
|
12
|
+
kind: "user" | "tool";
|
|
13
|
+
text: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type ClassifierTranscriptBudgets = {
|
|
17
|
+
maxUserTokens: number;
|
|
18
|
+
maxToolTokens: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
4
21
|
function flattenUserContent(content: unknown): string {
|
|
5
22
|
if (typeof content === "string") return content;
|
|
6
23
|
if (!Array.isArray(content)) return "";
|
|
@@ -14,19 +31,10 @@ function flattenUserContent(content: unknown): string {
|
|
|
14
31
|
.join("\n");
|
|
15
32
|
}
|
|
16
33
|
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
(block): block is { type: string; text?: string } =>
|
|
22
|
-
!!block && typeof block === "object" && "type" in block,
|
|
23
|
-
)
|
|
24
|
-
.filter((block) => block.type === "text" && typeof block.text === "string")
|
|
25
|
-
.map((block) => block.text ?? "")
|
|
26
|
-
.join("\n");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function collectAssistantToolCalls(content: unknown): string[] {
|
|
34
|
+
function collectAssistantToolCalls(content: unknown): Array<{
|
|
35
|
+
name: string;
|
|
36
|
+
input: unknown;
|
|
37
|
+
}> {
|
|
30
38
|
if (!Array.isArray(content)) return [];
|
|
31
39
|
return content
|
|
32
40
|
.filter(
|
|
@@ -40,34 +48,174 @@ function collectAssistantToolCalls(content: unknown): string[] {
|
|
|
40
48
|
} => !!block && typeof block === "object" && "type" in block,
|
|
41
49
|
)
|
|
42
50
|
.filter((block) => block.type === "toolCall" || block.type === "tool_use")
|
|
43
|
-
.map(
|
|
44
|
-
(block)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}`,
|
|
48
|
-
);
|
|
51
|
+
.map((block) => ({
|
|
52
|
+
name: String(block.name ?? "tool"),
|
|
53
|
+
input: "arguments" in block ? block.arguments : block.input,
|
|
54
|
+
}));
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
export function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
export function approximateTokenCount(text: string): number {
|
|
58
|
+
return Math.ceil(text.length / CHARS_PER_APPROX_TOKEN);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function truncateToTokenCap(
|
|
62
|
+
text: string,
|
|
63
|
+
maxTokens: number,
|
|
64
|
+
): { text: string; truncated: boolean } {
|
|
65
|
+
if (approximateTokenCount(text) <= maxTokens) {
|
|
66
|
+
return { text, truncated: false };
|
|
67
|
+
}
|
|
68
|
+
const maxCharacters = Math.max(1, maxTokens * CHARS_PER_APPROX_TOKEN);
|
|
69
|
+
const omittedTokens = Math.max(
|
|
70
|
+
1,
|
|
71
|
+
approximateTokenCount(text) - maxTokens,
|
|
72
|
+
);
|
|
73
|
+
const marker = `<truncated approx_tokens="${omittedTokens}" />`;
|
|
74
|
+
if (marker.length >= maxCharacters) {
|
|
75
|
+
return { text: marker.slice(0, maxCharacters), truncated: true };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const retainedCharacters = maxCharacters - marker.length;
|
|
79
|
+
const prefixCharacters = Math.ceil(retainedCharacters * 0.65);
|
|
80
|
+
const suffixCharacters = retainedCharacters - prefixCharacters;
|
|
81
|
+
return {
|
|
82
|
+
text: `${text.slice(0, prefixCharacters)}${marker}${
|
|
83
|
+
suffixCharacters > 0 ? text.slice(-suffixCharacters) : ""
|
|
84
|
+
}`,
|
|
85
|
+
truncated: true,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function collectTranscriptEntries(ctx: ExtensionContext): TranscriptEntry[] {
|
|
90
|
+
const entries: TranscriptEntry[] = [];
|
|
91
|
+
const sessionManager = ctx.sessionManager as typeof ctx.sessionManager & {
|
|
92
|
+
buildContextEntries?: () => ReturnType<typeof ctx.sessionManager.getBranch>;
|
|
93
|
+
};
|
|
94
|
+
const contextEntries = sessionManager.buildContextEntries?.() ??
|
|
95
|
+
sessionManager.getBranch();
|
|
96
|
+
|
|
97
|
+
for (const [index, entry] of contextEntries.entries()) {
|
|
57
98
|
if (entry.type !== "message") continue;
|
|
58
99
|
const message = entry.message as { role?: string; content?: unknown };
|
|
59
100
|
if (message.role === "user") {
|
|
60
101
|
const text = flattenUserContent(message.content).trim();
|
|
61
|
-
if (text)
|
|
62
|
-
|
|
63
|
-
const text = flattenAssistantText(message.content).trim();
|
|
64
|
-
if (text) lines.push(`Assistant: ${truncateMiddle(text, 2000)}`);
|
|
65
|
-
for (const toolCall of collectAssistantToolCalls(message.content)) {
|
|
66
|
-
lines.push(`AssistantAction: ${toolCall}`);
|
|
67
|
-
}
|
|
102
|
+
if (text) entries.push({ index, order: 0, kind: "user", text });
|
|
103
|
+
continue;
|
|
68
104
|
}
|
|
105
|
+
if (message.role !== "assistant") continue;
|
|
106
|
+
|
|
107
|
+
for (const [order, toolCall] of collectAssistantToolCalls(
|
|
108
|
+
message.content,
|
|
109
|
+
).entries()) {
|
|
110
|
+
entries.push({
|
|
111
|
+
index,
|
|
112
|
+
order,
|
|
113
|
+
kind: "tool",
|
|
114
|
+
text: `${toolCall.name}: ${safeJson(toolCall.input, 8000)}`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return entries;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function selectUserEntries(
|
|
123
|
+
entries: TranscriptEntry[],
|
|
124
|
+
maxTokens: number,
|
|
125
|
+
): { selected: TranscriptEntry[]; omitted: boolean } {
|
|
126
|
+
const users = entries.filter((entry) => entry.kind === "user");
|
|
127
|
+
if (users.length === 0) return { selected: [], omitted: false };
|
|
128
|
+
|
|
129
|
+
const distinctAnchors = users.length > 1;
|
|
130
|
+
const anchorBudget = distinctAnchors
|
|
131
|
+
? Math.max(1, Math.floor(maxTokens / 2))
|
|
132
|
+
: maxTokens;
|
|
133
|
+
const entryCap = Math.min(MAX_USER_ENTRY_TOKENS, anchorBudget);
|
|
134
|
+
const rendered = users.map((entry) => {
|
|
135
|
+
const truncated = truncateToTokenCap(`User: ${entry.text}`, entryCap);
|
|
136
|
+
return { ...entry, text: truncated.text, truncated: truncated.truncated };
|
|
137
|
+
});
|
|
138
|
+
const selectedIndices = new Set<number>();
|
|
139
|
+
let usedTokens = 0;
|
|
140
|
+
|
|
141
|
+
const include = (index: number): void => {
|
|
142
|
+
if (selectedIndices.has(index)) return;
|
|
143
|
+
const entry = rendered[index];
|
|
144
|
+
if (!entry) return;
|
|
145
|
+
const tokens = approximateTokenCount(entry.text);
|
|
146
|
+
if (usedTokens + tokens > maxTokens) return;
|
|
147
|
+
selectedIndices.add(index);
|
|
148
|
+
usedTokens += tokens;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Prefer the latest user instruction when an extremely small configured
|
|
152
|
+
// budget cannot retain both intent anchors.
|
|
153
|
+
include(rendered.length - 1);
|
|
154
|
+
include(0);
|
|
155
|
+
for (let index = rendered.length - 2; index > 0; index -= 1) {
|
|
156
|
+
include(index);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
selected: rendered.filter((_entry, index) => selectedIndices.has(index)),
|
|
161
|
+
omitted: selectedIndices.size < users.length || rendered.some((entry) =>
|
|
162
|
+
entry.truncated
|
|
163
|
+
),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function selectToolEntries(
|
|
168
|
+
entries: TranscriptEntry[],
|
|
169
|
+
maxTokens: number,
|
|
170
|
+
): { selected: TranscriptEntry[]; omitted: boolean } {
|
|
171
|
+
const tools = entries.filter((entry) => entry.kind === "tool");
|
|
172
|
+
const selected: TranscriptEntry[] = [];
|
|
173
|
+
let usedTokens = 0;
|
|
174
|
+
|
|
175
|
+
for (let index = tools.length - 1; index >= 0; index -= 1) {
|
|
176
|
+
if (selected.length >= MAX_RECENT_TOOL_ENTRIES) break;
|
|
177
|
+
const entry = tools[index];
|
|
178
|
+
if (!entry) continue;
|
|
179
|
+
const truncated = truncateToTokenCap(
|
|
180
|
+
`ToolCall ${entry.text}`,
|
|
181
|
+
Math.min(MAX_TOOL_ENTRY_TOKENS, maxTokens),
|
|
182
|
+
);
|
|
183
|
+
const tokens = approximateTokenCount(truncated.text);
|
|
184
|
+
if (usedTokens + tokens > maxTokens) continue;
|
|
185
|
+
selected.push({ ...entry, text: truncated.text });
|
|
186
|
+
usedTokens += tokens;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
selected.reverse();
|
|
190
|
+
return {
|
|
191
|
+
selected,
|
|
192
|
+
omitted: selected.length < tools.length || tools.some((entry) =>
|
|
193
|
+
approximateTokenCount(`ToolCall ${entry.text}`) >
|
|
194
|
+
Math.min(MAX_TOOL_ENTRY_TOKENS, maxTokens)
|
|
195
|
+
),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Build classifier evidence from user text and assistant tool-call payloads only. */
|
|
200
|
+
export function buildClassifierTranscript(
|
|
201
|
+
ctx: ExtensionContext,
|
|
202
|
+
budgets: ClassifierTranscriptBudgets,
|
|
203
|
+
): string {
|
|
204
|
+
const entries = collectTranscriptEntries(ctx);
|
|
205
|
+
const users = selectUserEntries(entries, budgets.maxUserTokens);
|
|
206
|
+
const tools = selectToolEntries(entries, budgets.maxToolTokens);
|
|
207
|
+
const selected = [...users.selected, ...tools.selected].sort(
|
|
208
|
+
(left, right) => left.index - right.index || left.order - right.order,
|
|
209
|
+
);
|
|
210
|
+
if (users.omitted || tools.omitted) {
|
|
211
|
+
selected.push({
|
|
212
|
+
index: Number.MAX_SAFE_INTEGER,
|
|
213
|
+
order: 0,
|
|
214
|
+
kind: "tool",
|
|
215
|
+
text: "<transcript_entries_omitted />",
|
|
216
|
+
});
|
|
69
217
|
}
|
|
70
|
-
return
|
|
218
|
+
return selected.map((entry) => entry.text).join("\n");
|
|
71
219
|
}
|
|
72
220
|
|
|
73
221
|
export function loadedContextFromSystemPromptOptions(options: unknown): string {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
1
2
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
|
|
3
4
|
/** Observability log configuration. Off by default. */
|
|
@@ -10,7 +11,8 @@ export type LogConfig = {
|
|
|
10
11
|
export type AutoModeSettings = {
|
|
11
12
|
enabled?: boolean;
|
|
12
13
|
classifierModel?: string;
|
|
13
|
-
|
|
14
|
+
maxUserTranscriptTokens?: number;
|
|
15
|
+
maxToolTranscriptTokens?: number;
|
|
14
16
|
environment?: unknown;
|
|
15
17
|
allow?: unknown;
|
|
16
18
|
protectedPaths?: unknown;
|
|
@@ -44,7 +46,8 @@ export type ToolPattern = {
|
|
|
44
46
|
export type EffectiveConfig = {
|
|
45
47
|
enabled: boolean;
|
|
46
48
|
classifierModel?: string;
|
|
47
|
-
|
|
49
|
+
maxUserTranscriptTokens: number;
|
|
50
|
+
maxToolTranscriptTokens: number;
|
|
48
51
|
environment: string[];
|
|
49
52
|
allow: string[];
|
|
50
53
|
protectedPaths: string[];
|
|
@@ -90,8 +93,16 @@ export type ClassificationDecision = {
|
|
|
90
93
|
|
|
91
94
|
/** One classifier attempt: the raw model response (or error) and parsed decision. */
|
|
92
95
|
export type ClassifierIoAttempt = {
|
|
96
|
+
stage: "fast" | "detailed";
|
|
93
97
|
attempt: number;
|
|
94
|
-
response?: {
|
|
98
|
+
response?: {
|
|
99
|
+
stopReason?: string;
|
|
100
|
+
text: string;
|
|
101
|
+
model: string;
|
|
102
|
+
timestamp: number;
|
|
103
|
+
usage: AssistantMessage["usage"];
|
|
104
|
+
errorMessage?: string;
|
|
105
|
+
};
|
|
95
106
|
parsed?: ClassificationDecision;
|
|
96
107
|
error?: string;
|
|
97
108
|
durationMs: number;
|
|
@@ -100,7 +111,12 @@ export type ClassifierIoAttempt = {
|
|
|
100
111
|
/** Full classifier I/O for an action, surfaced for optional observability logging. */
|
|
101
112
|
export type ClassifierIo = {
|
|
102
113
|
model: string;
|
|
103
|
-
prompt: {
|
|
114
|
+
prompt: {
|
|
115
|
+
system: string;
|
|
116
|
+
context: string;
|
|
117
|
+
fastInstruction: string;
|
|
118
|
+
detailedInstruction: string;
|
|
119
|
+
};
|
|
104
120
|
attempts: ClassifierIoAttempt[];
|
|
105
121
|
durationMs: number;
|
|
106
122
|
};
|
package/extensions/auto-mode.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The enforcement order is deliberately different from simple "auto reviewer" plugins:
|
|
5
5
|
* permission deny/ask rules and deterministic hard-deny checks run before any fast-path allow.
|
|
6
|
-
* Only
|
|
6
|
+
* Only read-only built-in tools bypass classification; every side-effecting action goes through the classifier.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export * from "./auto-mode/classifier.ts";
|