@kibitzsh/kibitz 0.0.3 → 0.0.4
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 +2 -0
- package/dist/cli/index.js +341 -2609
- package/dist/core/commentary.js +602 -924
- package/dist/core/platform-support.js +155 -127
- package/dist/core/providers/anthropic.d.ts.map +1 -1
- package/dist/core/providers/anthropic.js +9 -2
- package/dist/core/providers/anthropic.js.map +1 -1
- package/dist/core/session-dispatch.js +379 -391
- package/dist/core/watcher.d.ts +1 -1
- package/dist/core/watcher.d.ts.map +1 -1
- package/dist/core/watcher.js +834 -795
- package/dist/core/watcher.js.map +1 -1
- package/package.json +1 -1
package/dist/core/commentary.js
CHANGED
|
@@ -1,376 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
|
|
20
|
-
// src/core/commentary.ts
|
|
21
|
-
var commentary_exports = {};
|
|
22
|
-
__export(commentary_exports, {
|
|
23
|
-
CommentaryEngine: () => CommentaryEngine,
|
|
24
|
-
applyAssessmentSignals: () => applyAssessmentSignals,
|
|
25
|
-
buildCommentaryAssessment: () => buildCommentaryAssessment,
|
|
26
|
-
sanitizeGeneratedCommentary: () => sanitizeGeneratedCommentary
|
|
27
|
-
});
|
|
28
|
-
module.exports = __toCommonJS(commentary_exports);
|
|
29
|
-
var import_events = require("events");
|
|
30
|
-
|
|
31
|
-
// src/core/types.ts
|
|
32
|
-
var COMMENTARY_STYLE_OPTIONS = [
|
|
33
|
-
{ id: "bullets", label: "Bullet list" },
|
|
34
|
-
{ id: "one-liner", label: "One-liner" },
|
|
35
|
-
{ id: "headline-context", label: "Headline + context" },
|
|
36
|
-
{ id: "numbered-progress", label: "Numbered progress" },
|
|
37
|
-
{ id: "short-question", label: "Short + question" },
|
|
38
|
-
{ id: "table", label: "Table" },
|
|
39
|
-
{ id: "emoji-bullets", label: "Emoji bullets" },
|
|
40
|
-
{ id: "scoreboard", label: "Scoreboard" }
|
|
41
|
-
];
|
|
42
|
-
var MODELS = [
|
|
43
|
-
{ id: "claude-opus-4-6", label: "Claude Opus", provider: "anthropic" },
|
|
44
|
-
{ id: "claude-sonnet-4-6", label: "Claude Sonnet", provider: "anthropic" },
|
|
45
|
-
{ id: "claude-haiku-4-5-20251001", label: "Claude Haiku", provider: "anthropic" },
|
|
46
|
-
{ id: "gpt-4o", label: "GPT-4o", provider: "openai" },
|
|
47
|
-
{ id: "gpt-4o-mini", label: "GPT-4o mini", provider: "openai" }
|
|
48
|
-
];
|
|
49
|
-
var DEFAULT_MODEL = "claude-opus-4-6";
|
|
50
|
-
|
|
51
|
-
// src/core/providers/anthropic.ts
|
|
52
|
-
var import_child_process = require("child_process");
|
|
53
|
-
var import_fs = require("fs");
|
|
54
|
-
var import_path = require("path");
|
|
55
|
-
var import_os = require("os");
|
|
56
|
-
var cachedClaudePath = null;
|
|
57
|
-
var PROVIDER_TIMEOUT_MS = 9e4;
|
|
58
|
-
function resolveNodeScript(cmdPath) {
|
|
59
|
-
if (process.platform !== "win32" || !cmdPath.endsWith(".cmd")) return null;
|
|
60
|
-
try {
|
|
61
|
-
const content = (0, import_fs.readFileSync)(cmdPath, "utf-8");
|
|
62
|
-
const match = content.match(/%dp0%\\(.+?\.js)/);
|
|
63
|
-
if (match) {
|
|
64
|
-
const script = (0, import_path.join)((0, import_path.dirname)(cmdPath), match[1]);
|
|
65
|
-
if ((0, import_fs.existsSync)(script)) return script;
|
|
66
|
-
}
|
|
67
|
-
} catch {
|
|
68
|
-
}
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
function resolveClaudePath() {
|
|
72
|
-
if (cachedClaudePath) return cachedClaudePath;
|
|
73
|
-
const home = (0, import_os.homedir)();
|
|
74
|
-
const isWindows = process.platform === "win32";
|
|
75
|
-
const candidates = isWindows ? [
|
|
76
|
-
(0, import_path.join)(home, ".claude", "bin", "claude.exe"),
|
|
77
|
-
(0, import_path.join)(home, "AppData", "Local", "Programs", "claude-code", "claude.exe"),
|
|
78
|
-
(0, import_path.join)(home, "AppData", "Local", "Claude", "claude.exe"),
|
|
79
|
-
(0, import_path.join)(home, "AppData", "Local", "Microsoft", "WinGet", "Links", "claude.exe"),
|
|
80
|
-
"C:\\Program Files\\Claude\\claude.exe",
|
|
81
|
-
(0, import_path.join)(home, "AppData", "Roaming", "npm", "claude.cmd")
|
|
82
|
-
] : [
|
|
83
|
-
(0, import_path.join)(home, ".local", "bin", "claude"),
|
|
84
|
-
(0, import_path.join)(home, ".claude", "bin", "claude"),
|
|
85
|
-
"/usr/local/bin/claude",
|
|
86
|
-
"/usr/bin/claude",
|
|
87
|
-
"/snap/bin/claude",
|
|
88
|
-
"/opt/homebrew/bin/claude"
|
|
89
|
-
];
|
|
90
|
-
for (const p of candidates) {
|
|
91
|
-
if ((0, import_fs.existsSync)(p)) {
|
|
92
|
-
cachedClaudePath = p;
|
|
93
|
-
return p;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
const env = { ...process.env };
|
|
97
|
-
delete env.ELECTRON_RUN_AS_NODE;
|
|
98
|
-
delete env.CLAUDECODE;
|
|
99
|
-
if (isWindows) {
|
|
100
|
-
try {
|
|
101
|
-
const resolved = (0, import_child_process.execSync)("where claude", {
|
|
102
|
-
encoding: "utf-8",
|
|
103
|
-
env,
|
|
104
|
-
timeout: 5e3
|
|
105
|
-
}).trim().split("\n")[0].trim();
|
|
106
|
-
if (resolved && (0, import_fs.existsSync)(resolved)) {
|
|
107
|
-
cachedClaudePath = resolved;
|
|
108
|
-
return resolved;
|
|
109
|
-
}
|
|
110
|
-
} catch {
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
const shells = ["/bin/zsh", "/bin/bash"];
|
|
114
|
-
for (const sh of shells) {
|
|
115
|
-
if (!(0, import_fs.existsSync)(sh)) continue;
|
|
116
|
-
try {
|
|
117
|
-
const resolved = (0, import_child_process.execSync)(`${sh} -lic 'which claude'`, {
|
|
118
|
-
encoding: "utf-8",
|
|
119
|
-
env,
|
|
120
|
-
timeout: 5e3
|
|
121
|
-
}).trim();
|
|
122
|
-
if (resolved && (0, import_fs.existsSync)(resolved)) {
|
|
123
|
-
cachedClaudePath = resolved;
|
|
124
|
-
return resolved;
|
|
125
|
-
}
|
|
126
|
-
} catch {
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
var AnthropicProvider = class {
|
|
133
|
-
async generate(systemPrompt, userPrompt, _apiKey, model, onChunk) {
|
|
134
|
-
return new Promise((resolve, reject) => {
|
|
135
|
-
const claudePath = resolveClaudePath();
|
|
136
|
-
if (!claudePath) {
|
|
137
|
-
reject(new Error("Claude CLI not found. Install from https://docs.anthropic.com/en/docs/claude-code"));
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
const fullPrompt = `${systemPrompt}
|
|
141
|
-
|
|
142
|
-
${userPrompt}`;
|
|
143
|
-
const args = [
|
|
144
|
-
"-p",
|
|
145
|
-
fullPrompt,
|
|
146
|
-
"--output-format",
|
|
147
|
-
"stream-json",
|
|
148
|
-
"--verbose",
|
|
149
|
-
"--max-turns",
|
|
150
|
-
"1",
|
|
151
|
-
"--model",
|
|
152
|
-
model
|
|
153
|
-
];
|
|
154
|
-
const env = { ...process.env };
|
|
155
|
-
delete env.ELECTRON_RUN_AS_NODE;
|
|
156
|
-
delete env.CLAUDECODE;
|
|
157
|
-
let proc;
|
|
158
|
-
try {
|
|
159
|
-
const nodeScript = resolveNodeScript(claudePath);
|
|
160
|
-
if (nodeScript) {
|
|
161
|
-
proc = (0, import_child_process.spawn)(process.execPath, [nodeScript, ...args], {
|
|
162
|
-
cwd: (0, import_os.homedir)(),
|
|
163
|
-
env,
|
|
164
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
165
|
-
windowsHide: true
|
|
166
|
-
});
|
|
167
|
-
} else {
|
|
168
|
-
proc = (0, import_child_process.spawn)(claudePath, args, {
|
|
169
|
-
cwd: (0, import_os.homedir)(),
|
|
170
|
-
env,
|
|
171
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
172
|
-
windowsHide: true,
|
|
173
|
-
shell: process.platform === "win32"
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
} catch (err) {
|
|
177
|
-
reject(new Error(`Failed to spawn claude CLI: ${err instanceof Error ? err.message : String(err)}`));
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
if (!proc.stdout || !proc.stderr) {
|
|
181
|
-
reject(new Error("Failed to create stdio pipes for Claude CLI"));
|
|
182
|
-
try {
|
|
183
|
-
proc.kill();
|
|
184
|
-
} catch {
|
|
185
|
-
}
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
let full = "";
|
|
189
|
-
let buffer = "";
|
|
190
|
-
let stderr = "";
|
|
191
|
-
let timedOut = false;
|
|
192
|
-
proc.stdout.on("data", (data) => {
|
|
193
|
-
buffer += data.toString();
|
|
194
|
-
const lines = buffer.split("\n");
|
|
195
|
-
buffer = lines.pop() ?? "";
|
|
196
|
-
for (const line of lines) {
|
|
197
|
-
if (!line.trim()) continue;
|
|
198
|
-
try {
|
|
199
|
-
const event = JSON.parse(line);
|
|
200
|
-
if (event.type === "assistant") {
|
|
201
|
-
const content = event.message?.content;
|
|
202
|
-
if (Array.isArray(content)) {
|
|
203
|
-
for (const block of content) {
|
|
204
|
-
if (block.type === "text" && typeof block.text === "string") {
|
|
205
|
-
full += block.text;
|
|
206
|
-
onChunk(block.text);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
if (event.type === "result" && event.result) {
|
|
212
|
-
if (!full) {
|
|
213
|
-
full = String(event.result);
|
|
214
|
-
onChunk(full);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
} catch {
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
proc.stderr.on("data", (data) => {
|
|
222
|
-
stderr += data.toString();
|
|
223
|
-
});
|
|
224
|
-
const timeout = setTimeout(() => {
|
|
225
|
-
timedOut = true;
|
|
226
|
-
try {
|
|
227
|
-
proc.kill();
|
|
228
|
-
} catch {
|
|
229
|
-
}
|
|
230
|
-
}, PROVIDER_TIMEOUT_MS);
|
|
231
|
-
proc.on("close", (code) => {
|
|
232
|
-
clearTimeout(timeout);
|
|
233
|
-
if (timedOut && !full) {
|
|
234
|
-
reject(new Error(`Claude CLI timed out after ${PROVIDER_TIMEOUT_MS}ms`));
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
if (code !== 0 && !full) {
|
|
238
|
-
reject(new Error(`Claude CLI exited with code ${code}: ${stderr.slice(0, 500)}`));
|
|
239
|
-
} else {
|
|
240
|
-
resolve(full);
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
proc.on("error", (err) => {
|
|
244
|
-
clearTimeout(timeout);
|
|
245
|
-
reject(new Error(`Claude CLI error: ${err.message}`));
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
// src/core/providers/openai.ts
|
|
252
|
-
var import_child_process2 = require("child_process");
|
|
253
|
-
var import_fs2 = require("fs");
|
|
254
|
-
var import_path2 = require("path");
|
|
255
|
-
var import_os2 = require("os");
|
|
256
|
-
var cachedCodexScript = null;
|
|
257
|
-
var codexScriptResolved = false;
|
|
258
|
-
var PROVIDER_TIMEOUT_MS2 = 9e4;
|
|
259
|
-
function resolveCodexNodeScript() {
|
|
260
|
-
if (codexScriptResolved) return cachedCodexScript;
|
|
261
|
-
codexScriptResolved = true;
|
|
262
|
-
if (process.platform !== "win32") return null;
|
|
263
|
-
try {
|
|
264
|
-
const cmdPath = (0, import_child_process2.execSync)("where codex.cmd", {
|
|
265
|
-
encoding: "utf-8",
|
|
266
|
-
timeout: 5e3,
|
|
267
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
268
|
-
}).trim().split("\n")[0].trim();
|
|
269
|
-
const content = (0, import_fs2.readFileSync)(cmdPath, "utf-8");
|
|
270
|
-
const match = content.match(/%dp0%\\(.+?\.js)/);
|
|
271
|
-
if (!match) return null;
|
|
272
|
-
const script = (0, import_path2.join)((0, import_path2.dirname)(cmdPath), match[1]);
|
|
273
|
-
if ((0, import_fs2.existsSync)(script)) {
|
|
274
|
-
cachedCodexScript = script;
|
|
275
|
-
return script;
|
|
276
|
-
}
|
|
277
|
-
} catch {
|
|
278
|
-
}
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
var OpenAIProvider = class {
|
|
282
|
-
async generate(systemPrompt, userPrompt, _apiKey, _model, onChunk) {
|
|
283
|
-
return new Promise((resolve, reject) => {
|
|
284
|
-
const fullPrompt = `${systemPrompt}
|
|
285
|
-
|
|
286
|
-
${userPrompt}`;
|
|
287
|
-
const args = ["exec", "--json", "--skip-git-repo-check", fullPrompt];
|
|
288
|
-
let proc;
|
|
289
|
-
try {
|
|
290
|
-
const nodeScript = resolveCodexNodeScript();
|
|
291
|
-
if (nodeScript) {
|
|
292
|
-
proc = (0, import_child_process2.spawn)(process.execPath, [nodeScript, ...args], {
|
|
293
|
-
cwd: (0, import_os2.homedir)(),
|
|
294
|
-
env: process.env,
|
|
295
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
296
|
-
windowsHide: true
|
|
297
|
-
});
|
|
298
|
-
} else {
|
|
299
|
-
const codexCmd = process.platform === "win32" ? "codex.cmd" : "codex";
|
|
300
|
-
proc = (0, import_child_process2.spawn)(codexCmd, args, {
|
|
301
|
-
cwd: (0, import_os2.homedir)(),
|
|
302
|
-
env: process.env,
|
|
303
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
304
|
-
windowsHide: true,
|
|
305
|
-
shell: process.platform === "win32"
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
} catch (err) {
|
|
309
|
-
reject(new Error(`Failed to spawn codex CLI: ${err instanceof Error ? err.message : String(err)}`));
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
if (!proc.stdout || !proc.stderr) {
|
|
313
|
-
reject(new Error("Failed to create stdio pipes for codex CLI"));
|
|
314
|
-
try {
|
|
315
|
-
proc.kill();
|
|
316
|
-
} catch {
|
|
317
|
-
}
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
let full = "";
|
|
321
|
-
let buffer = "";
|
|
322
|
-
let stderr = "";
|
|
323
|
-
let timedOut = false;
|
|
324
|
-
proc.stdout.on("data", (data) => {
|
|
325
|
-
buffer += data.toString();
|
|
326
|
-
const lines = buffer.split("\n");
|
|
327
|
-
buffer = lines.pop() ?? "";
|
|
328
|
-
for (const line of lines) {
|
|
329
|
-
if (!line.trim()) continue;
|
|
330
|
-
try {
|
|
331
|
-
const event = JSON.parse(line);
|
|
332
|
-
if (event.type === "item.completed" && event.item) {
|
|
333
|
-
if (event.item.type === "agent_message" && typeof event.item.text === "string") {
|
|
334
|
-
full += event.item.text;
|
|
335
|
-
onChunk(event.item.text);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
} catch {
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
});
|
|
342
|
-
proc.stderr.on("data", (data) => {
|
|
343
|
-
stderr += data.toString();
|
|
344
|
-
});
|
|
345
|
-
const timeout = setTimeout(() => {
|
|
346
|
-
timedOut = true;
|
|
347
|
-
try {
|
|
348
|
-
proc.kill();
|
|
349
|
-
} catch {
|
|
350
|
-
}
|
|
351
|
-
}, PROVIDER_TIMEOUT_MS2);
|
|
352
|
-
proc.on("close", (code) => {
|
|
353
|
-
clearTimeout(timeout);
|
|
354
|
-
if (timedOut && !full) {
|
|
355
|
-
reject(new Error(`Codex CLI timed out after ${PROVIDER_TIMEOUT_MS2}ms`));
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
if (code !== 0 && !full) {
|
|
359
|
-
reject(new Error(`Codex CLI exited with code ${code}: ${stderr.slice(0, 500)}`));
|
|
360
|
-
} else {
|
|
361
|
-
resolve(full);
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
proc.on("error", (err) => {
|
|
365
|
-
clearTimeout(timeout);
|
|
366
|
-
reject(new Error(`Codex CLI error: ${err.message}`));
|
|
367
|
-
});
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
// src/core/commentary.ts
|
|
373
|
-
var SYSTEM_PROMPT = `You oversee AI coding agents. Summarize what they did in plain language anyone can understand.
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CommentaryEngine = void 0;
|
|
4
|
+
exports.buildCommentaryAssessment = buildCommentaryAssessment;
|
|
5
|
+
exports.applyAssessmentSignals = applyAssessmentSignals;
|
|
6
|
+
exports.sanitizeGeneratedCommentary = sanitizeGeneratedCommentary;
|
|
7
|
+
const events_1 = require("events");
|
|
8
|
+
const types_1 = require("./types");
|
|
9
|
+
const anthropic_1 = require("./providers/anthropic");
|
|
10
|
+
const openai_1 = require("./providers/openai");
|
|
11
|
+
const SYSTEM_PROMPT = `You oversee AI coding agents. Summarize what they did in plain language anyone can understand.
|
|
374
12
|
|
|
375
13
|
Rules:
|
|
376
14
|
- Plain language. "Fixed the login page" not "Edited auth middleware".
|
|
@@ -385,38 +23,39 @@ Rules:
|
|
|
385
23
|
- Never mention session IDs, logs, traces, prompts, JSONL files, or internal tooling.
|
|
386
24
|
- Never write in first person ("I", "I'll", "we") or future tense plans.
|
|
387
25
|
- Never use the word "verdict". Write narrative, not a ruling.`;
|
|
388
|
-
|
|
389
|
-
|
|
26
|
+
// Format templates — one is selected per commentary from the enabled style set.
|
|
27
|
+
const FORMAT_TEMPLATES = {
|
|
28
|
+
bullets: `Use bullet points. Each bullet = one thing done or one observation.
|
|
390
29
|
Close with a sentence on direction and momentum.
|
|
391
30
|
Example:
|
|
392
31
|
- Researched how the feature works
|
|
393
32
|
- Rewrote the page with new layout
|
|
394
|
-
- Shipped without **tests**
|
|
33
|
+
- Shipped without **tests** — RISKY
|
|
395
34
|
- Checked for errors before pushing`,
|
|
396
|
-
|
|
35
|
+
'one-liner': `Write a single sentence. Punchy, complete, under 20 words.
|
|
397
36
|
One closing sentence on where this is headed.
|
|
398
|
-
Example: Agent investigated the bug, found the root cause, and fixed it
|
|
399
|
-
Example: Still reading code after 30 actions
|
|
400
|
-
|
|
37
|
+
Example: Agent investigated the bug, found the root cause, and fixed it — NICE WORK.
|
|
38
|
+
Example: Still reading code after 30 actions — hasn't changed anything yet.`,
|
|
39
|
+
'headline-context': `Start with a short UPPER CASE reaction (2-4 words), then one sentence of context.
|
|
401
40
|
Close with a read on momentum.
|
|
402
41
|
Example:
|
|
403
42
|
SOLID APPROACH. Agent read the dependencies first, then made targeted changes across three files.
|
|
404
43
|
Example:
|
|
405
|
-
NOT GREAT. Pushed to **production** without running any tests
|
|
406
|
-
|
|
44
|
+
NOT GREAT. Pushed to **production** without running any tests — hope nothing breaks.`,
|
|
45
|
+
'numbered-progress': `Use numbered steps showing what the agent did in order. Finish with a momentum line.
|
|
407
46
|
Example:
|
|
408
47
|
1. Read the existing code
|
|
409
48
|
2. Made changes to the login flow
|
|
410
49
|
3. Tested locally
|
|
411
50
|
4. Pushed to **production**
|
|
412
51
|
Proper process, nothing to flag.`,
|
|
413
|
-
|
|
52
|
+
'short-question': `Summarize in one sentence, then ask a pointed rhetorical question.
|
|
414
53
|
End with a one-sentence read on direction.
|
|
415
54
|
Example:
|
|
416
55
|
Agent rewrote the entire settings page and shipped it immediately. Did they test this at all?
|
|
417
56
|
Example:
|
|
418
57
|
Third time reading the same code. Lost, or just being thorough?`,
|
|
419
|
-
|
|
58
|
+
table: `Use a compact markdown table with columns: Action | Why it mattered.
|
|
420
59
|
Include 3-5 rows max, then one sentence on momentum.
|
|
421
60
|
Example:
|
|
422
61
|
| Action | Why it mattered |
|
|
@@ -424,618 +63,657 @@ Example:
|
|
|
424
63
|
| Ran tests | Verified changes before shipping |
|
|
425
64
|
| Skipped lint | Could hide style regressions |
|
|
426
65
|
Mostly careful, one loose end.`,
|
|
427
|
-
|
|
428
|
-
Use only these tags:
|
|
66
|
+
'emoji-bullets': `Use bullet points with emoji tags to signal risk/quality.
|
|
67
|
+
Use only these tags: ✅, ⚠️, 🔥, ❄️.
|
|
429
68
|
Close with a direction sentence.
|
|
430
69
|
Example:
|
|
431
|
-
-
|
|
432
|
-
-
|
|
433
|
-
-
|
|
434
|
-
|
|
70
|
+
- ✅ Fixed the failing migration script
|
|
71
|
+
- ⚠️ Pushed without rerunning integration tests
|
|
72
|
+
- 🔥 Found the root cause in auth token parsing`,
|
|
73
|
+
scoreboard: `Use a scoreboard format with these labels:
|
|
435
74
|
Wins:
|
|
436
75
|
Risks:
|
|
437
76
|
Next:
|
|
438
|
-
Each label should have 1-3 short bullets, then one line on where things stand
|
|
77
|
+
Each label should have 1-3 short bullets, then one line on where things stand.`,
|
|
439
78
|
};
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
79
|
+
const DEFAULT_FORMAT_STYLE_IDS = types_1.COMMENTARY_STYLE_OPTIONS.map((option) => option.id);
|
|
80
|
+
const PRESET_INSTRUCTIONS = {
|
|
81
|
+
auto: '',
|
|
82
|
+
'critical-coder': 'Be a VERY CRITICAL coder using code terminology. Call out architectural or process flaws directly and sharply.',
|
|
83
|
+
'precise-short': 'Be precise and short. Keep the output compact and information-dense with minimal words.',
|
|
84
|
+
emotional: 'Be emotional and expressive. React strongly to good or risky behavior while staying factual.',
|
|
85
|
+
newbie: 'Explain for non-developers. Avoid jargon or explain it in plain words with simple cause/effect.',
|
|
447
86
|
};
|
|
448
87
|
function normalizeFormatStyles(styleIds) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
88
|
+
const requested = new Set(styleIds.map((styleId) => String(styleId || '').trim()));
|
|
89
|
+
const normalized = types_1.COMMENTARY_STYLE_OPTIONS
|
|
90
|
+
.map((option) => option.id)
|
|
91
|
+
.filter((styleId) => requested.has(styleId));
|
|
92
|
+
return normalized.length > 0 ? normalized : DEFAULT_FORMAT_STYLE_IDS.slice();
|
|
452
93
|
}
|
|
453
94
|
function sameFormatStyles(a, b) {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
95
|
+
if (a.length !== b.length)
|
|
96
|
+
return false;
|
|
97
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
98
|
+
if (a[i] !== b[i])
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
459
102
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
103
|
+
const SESSION_ID_PATTERNS = [
|
|
104
|
+
/\b[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\b/gi,
|
|
105
|
+
/\brollout-\d{4}-\d{2}-\d{2}t\d{2}[-:]\d{2}[-:]\d{2}[-a-z0-9]+(?:\.jsonl)?\b/gi,
|
|
106
|
+
/\bsession[\s:_-]*[0-9a-f]{8,}\b/gi,
|
|
107
|
+
/\bturn[\s:_-]*[0-9a-f]{8,}\b/gi,
|
|
465
108
|
];
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
109
|
+
// Adaptive batching
|
|
110
|
+
const MIN_BATCH_SIZE = 1;
|
|
111
|
+
const MAX_BATCH_SIZE = 40;
|
|
112
|
+
const IDLE_TIMEOUT_MS = 5000;
|
|
113
|
+
const MAX_WAIT_MS = 15000;
|
|
114
|
+
const TEST_COMMAND_PATTERNS = [
|
|
115
|
+
/\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:test|test:[\w:-]+)\b/i,
|
|
116
|
+
/\b(?:jest|vitest|mocha|ava|pytest|go\s+test|cargo\s+test|dotnet\s+test|mvn\s+test|gradle(?:w)?\s+test|rspec|phpunit)\b/i,
|
|
473
117
|
];
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
118
|
+
const DEPLOY_COMMAND_PATTERNS = [
|
|
119
|
+
/\b(?:deploy|release|publish)\b/i,
|
|
120
|
+
/\b(?:kubectl\s+apply|helm\s+upgrade|terraform\s+apply|serverless\s+deploy|vercel\s+--prod|netlify\s+deploy|fly\s+deploy)\b/i,
|
|
477
121
|
];
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
122
|
+
const ERROR_SIGNAL_PATTERN = /\b(?:error|failed|failure|exception|traceback|panic|denied|timeout|cannot|can't|unable)\b/i;
|
|
123
|
+
const SECURITY_COMMAND_RULES = [
|
|
124
|
+
{
|
|
125
|
+
severity: 'high',
|
|
126
|
+
signal: 'Remote script piped directly into a shell',
|
|
127
|
+
pattern: /\b(?:curl|wget)\b[^\n|]{0,300}\|\s*(?:bash|sh|zsh|pwsh|powershell)\b/i,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
severity: 'high',
|
|
131
|
+
signal: 'TLS verification explicitly disabled',
|
|
132
|
+
pattern: /\bNODE_TLS_REJECT_UNAUTHORIZED\s*=\s*0\b/i,
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
severity: 'high',
|
|
136
|
+
signal: 'Destructive root-level delete command',
|
|
137
|
+
pattern: /\brm\s+-rf\s+\/(?:\s|$)/i,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
severity: 'medium',
|
|
141
|
+
signal: 'Insecure transport flag used in command',
|
|
142
|
+
pattern: /\bcurl\b[^\n]*\s(?:--insecure|-k)\b/i,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
severity: 'medium',
|
|
146
|
+
signal: 'Git hooks bypassed with --no-verify',
|
|
147
|
+
pattern: /\bgit\b[^\n]*\s--no-verify\b/i,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
severity: 'medium',
|
|
151
|
+
signal: 'SSH host key verification disabled',
|
|
152
|
+
pattern: /\bStrictHostKeyChecking\s*=\s*no\b/i,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
severity: 'medium',
|
|
156
|
+
signal: 'Overly broad file permissions (chmod 777)',
|
|
157
|
+
pattern: /\bchmod\s+777\b/i,
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
severity: 'medium',
|
|
161
|
+
signal: 'Potential secret exposed in command arguments',
|
|
162
|
+
pattern: /\b(?:api[_-]?key|token|password)\s*=\s*\S+/i,
|
|
163
|
+
},
|
|
520
164
|
];
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
165
|
+
const SECURITY_PATH_RULES = [
|
|
166
|
+
{
|
|
167
|
+
severity: 'medium',
|
|
168
|
+
signal: 'Secret-related file touched',
|
|
169
|
+
pattern: /(?:^|[\\/])(?:\.env(?:\.[^\\/]+)?|id_rsa|id_dsa|authorized_keys|known_hosts|credentials?|secrets?)(?:$|[\\/])/i,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
severity: 'medium',
|
|
173
|
+
signal: 'Sensitive key/cert file touched',
|
|
174
|
+
pattern: /\.(?:pem|key|p12|pfx)$/i,
|
|
175
|
+
},
|
|
532
176
|
];
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
177
|
+
const JUDGMENT_PHRASES = [
|
|
178
|
+
'NOT IDEAL',
|
|
179
|
+
'NOT GREAT',
|
|
180
|
+
'WATCH OUT',
|
|
181
|
+
'RISK ALERT',
|
|
182
|
+
'GREAT FIND',
|
|
183
|
+
'NICE MOVE',
|
|
184
|
+
'SOLID CHECK',
|
|
541
185
|
];
|
|
542
186
|
function getDetailValue(details, ...keys) {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
187
|
+
for (const key of keys) {
|
|
188
|
+
const value = details[key];
|
|
189
|
+
if (typeof value === 'string' && value.trim())
|
|
190
|
+
return value.trim();
|
|
191
|
+
}
|
|
192
|
+
return '';
|
|
548
193
|
}
|
|
549
194
|
function extractCommand(event) {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
195
|
+
if (event.type !== 'tool_call')
|
|
196
|
+
return '';
|
|
197
|
+
const details = event.details || {};
|
|
198
|
+
const input = typeof details.input === 'object' && details.input
|
|
199
|
+
? details.input
|
|
200
|
+
: {};
|
|
201
|
+
const direct = getDetailValue(details, 'command', 'cmd');
|
|
202
|
+
if (direct)
|
|
203
|
+
return direct;
|
|
204
|
+
return getDetailValue(input, 'command', 'cmd');
|
|
556
205
|
}
|
|
557
206
|
function extractTouchedPath(event) {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
207
|
+
if (event.type !== 'tool_call')
|
|
208
|
+
return '';
|
|
209
|
+
const details = event.details || {};
|
|
210
|
+
const input = typeof details.input === 'object' && details.input
|
|
211
|
+
? details.input
|
|
212
|
+
: {};
|
|
213
|
+
const direct = getDetailValue(details, 'path', 'file_path');
|
|
214
|
+
if (direct)
|
|
215
|
+
return direct;
|
|
216
|
+
return getDetailValue(input, 'path', 'file_path');
|
|
564
217
|
}
|
|
565
218
|
function extractToolName(event) {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
219
|
+
const details = event.details || {};
|
|
220
|
+
const value = details.tool;
|
|
221
|
+
return typeof value === 'string' ? value.toLowerCase() : '';
|
|
569
222
|
}
|
|
570
223
|
function pushUnique(list, value, maxItems = 4) {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
224
|
+
const normalized = sanitizePromptText(value);
|
|
225
|
+
if (!normalized)
|
|
226
|
+
return;
|
|
227
|
+
if (list.includes(normalized))
|
|
228
|
+
return;
|
|
229
|
+
if (list.length < maxItems)
|
|
230
|
+
list.push(normalized);
|
|
575
231
|
}
|
|
576
232
|
function pushSecurityFinding(findings, finding) {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
233
|
+
const key = `${finding.severity}:${finding.signal}:${finding.evidence}`;
|
|
234
|
+
const exists = findings.some((item) => `${item.severity}:${item.signal}:${item.evidence}` === key);
|
|
235
|
+
if (!exists)
|
|
236
|
+
findings.push(finding);
|
|
580
237
|
}
|
|
581
238
|
function detectSecurityFindings(findings, sourceText, rules) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
239
|
+
const evidence = sanitizePromptText(sourceText).slice(0, 140);
|
|
240
|
+
if (!evidence)
|
|
241
|
+
return;
|
|
242
|
+
for (const rule of rules) {
|
|
243
|
+
if (!rule.pattern.test(sourceText))
|
|
244
|
+
continue;
|
|
245
|
+
pushSecurityFinding(findings, {
|
|
246
|
+
severity: rule.severity,
|
|
247
|
+
signal: rule.signal,
|
|
248
|
+
evidence,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
592
251
|
}
|
|
593
252
|
function hasPattern(text, patterns) {
|
|
594
|
-
|
|
253
|
+
return patterns.some((pattern) => pattern.test(text));
|
|
595
254
|
}
|
|
596
255
|
function summarizeActivity(reads, writes, searches, commands, tests, errors) {
|
|
597
|
-
|
|
256
|
+
return `reads=${reads}, writes=${writes}, searches=${searches}, commands=${commands}, tests=${tests}, errors=${errors}`;
|
|
598
257
|
}
|
|
599
258
|
function summarizeClosingEvidence(assessment) {
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
259
|
+
if (assessment.progressSignals.length > 0)
|
|
260
|
+
return assessment.progressSignals[0];
|
|
261
|
+
if (assessment.driftSignals.length > 0)
|
|
262
|
+
return assessment.driftSignals[0];
|
|
263
|
+
return assessment.activitySummary;
|
|
603
264
|
}
|
|
604
265
|
function createClosingLine(assessment) {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
266
|
+
const evidence = summarizeClosingEvidence(assessment);
|
|
267
|
+
const dir = assessment.direction === 'on-track' ? 'on track' : assessment.direction;
|
|
268
|
+
const sec = assessment.security !== 'clean' ? ` Security is ${assessment.security}.` : '';
|
|
269
|
+
return `${evidence} — looks ${dir} with ${assessment.confidence} confidence.${sec}`;
|
|
609
270
|
}
|
|
610
271
|
function extractRecentlyUsedJudgments(recentCommentary) {
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
272
|
+
const found = new Set();
|
|
273
|
+
for (const text of recentCommentary.slice(-5)) {
|
|
274
|
+
const upper = String(text || '').toUpperCase();
|
|
275
|
+
for (const phrase of JUDGMENT_PHRASES) {
|
|
276
|
+
if (upper.includes(phrase))
|
|
277
|
+
found.add(phrase);
|
|
278
|
+
}
|
|
616
279
|
}
|
|
617
|
-
|
|
618
|
-
return Array.from(found).slice(0, 4);
|
|
280
|
+
return Array.from(found).slice(0, 4);
|
|
619
281
|
}
|
|
620
282
|
function buildCommentaryAssessment(events, sessionTitle) {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
283
|
+
let reads = 0;
|
|
284
|
+
let writes = 0;
|
|
285
|
+
let searches = 0;
|
|
286
|
+
let commands = 0;
|
|
287
|
+
let tests = 0;
|
|
288
|
+
let deploys = 0;
|
|
289
|
+
let errors = 0;
|
|
290
|
+
const touchedFiles = [];
|
|
291
|
+
const progressSignals = [];
|
|
292
|
+
const driftSignals = [];
|
|
293
|
+
const securityFindings = [];
|
|
294
|
+
for (const event of events) {
|
|
295
|
+
const summary = sanitizePromptText(event.summary);
|
|
296
|
+
const summaryLower = summary.toLowerCase();
|
|
297
|
+
const toolName = extractToolName(event);
|
|
298
|
+
const command = extractCommand(event);
|
|
299
|
+
const touchedPath = extractTouchedPath(event);
|
|
300
|
+
if (event.type === 'tool_call') {
|
|
301
|
+
if (summaryLower.startsWith('reading ') || toolName.includes('read'))
|
|
302
|
+
reads += 1;
|
|
303
|
+
if (summaryLower.startsWith('writing ')
|
|
304
|
+
|| summaryLower.startsWith('editing ')
|
|
305
|
+
|| toolName.includes('write')
|
|
306
|
+
|| toolName.includes('edit')
|
|
307
|
+
|| toolName.includes('apply_diff')) {
|
|
308
|
+
writes += 1;
|
|
309
|
+
}
|
|
310
|
+
if (summaryLower.startsWith('searching ')
|
|
311
|
+
|| summaryLower.startsWith('finding files')
|
|
312
|
+
|| toolName.includes('grep')
|
|
313
|
+
|| toolName.includes('glob')) {
|
|
314
|
+
searches += 1;
|
|
315
|
+
}
|
|
316
|
+
if (command) {
|
|
317
|
+
commands += 1;
|
|
318
|
+
if (hasPattern(command, TEST_COMMAND_PATTERNS))
|
|
319
|
+
tests += 1;
|
|
320
|
+
if (hasPattern(command, DEPLOY_COMMAND_PATTERNS))
|
|
321
|
+
deploys += 1;
|
|
322
|
+
detectSecurityFindings(securityFindings, command, SECURITY_COMMAND_RULES);
|
|
323
|
+
}
|
|
324
|
+
if (touchedPath) {
|
|
325
|
+
pushUnique(touchedFiles, touchedPath, 3);
|
|
326
|
+
detectSecurityFindings(securityFindings, touchedPath, SECURITY_PATH_RULES);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const output = typeof event.details?.output === 'string' ? event.details.output : '';
|
|
330
|
+
const combinedText = `${summary}\n${output}`;
|
|
331
|
+
if ((event.type === 'message' || event.type === 'tool_result') && ERROR_SIGNAL_PATTERN.test(combinedText)) {
|
|
332
|
+
errors += 1;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (writes > 0)
|
|
336
|
+
pushUnique(progressSignals, `${writes} write/edit actions executed`);
|
|
337
|
+
if (tests > 0)
|
|
338
|
+
pushUnique(progressSignals, `${tests} test commands detected`);
|
|
339
|
+
if (deploys > 0)
|
|
340
|
+
pushUnique(progressSignals, `${deploys} deploy/release commands detected`);
|
|
341
|
+
if (touchedFiles.length > 0)
|
|
342
|
+
pushUnique(progressSignals, `touched files: ${touchedFiles.join(', ')}`);
|
|
343
|
+
if (reads >= 4 && writes === 0)
|
|
344
|
+
pushUnique(driftSignals, `${reads} reads with no code edits`);
|
|
345
|
+
if (commands >= 6 && writes === 0 && tests === 0) {
|
|
346
|
+
pushUnique(driftSignals, `${commands} commands executed without visible code/test progress`);
|
|
347
|
+
}
|
|
348
|
+
if (errors >= 2)
|
|
349
|
+
pushUnique(driftSignals, `${errors} error/failure signals seen in outputs`);
|
|
350
|
+
if (deploys > 0 && tests === 0)
|
|
351
|
+
pushUnique(driftSignals, 'deploy/release command seen without test evidence');
|
|
352
|
+
if (progressSignals.length === 0 && events.length >= 3) {
|
|
353
|
+
pushUnique(driftSignals, 'no concrete progress signal captured in this batch');
|
|
354
|
+
}
|
|
355
|
+
let direction = 'on-track';
|
|
356
|
+
if (errors >= 3 && writes === 0 && tests === 0) {
|
|
357
|
+
direction = 'blocked';
|
|
358
|
+
}
|
|
359
|
+
else if (driftSignals.length > 0 && writes === 0 && tests === 0) {
|
|
360
|
+
direction = 'drifting';
|
|
656
361
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
${output}`;
|
|
660
|
-
if ((event.type === "message" || event.type === "tool_result") && ERROR_SIGNAL_PATTERN.test(combinedText)) {
|
|
661
|
-
errors += 1;
|
|
362
|
+
else if (progressSignals.length === 0) {
|
|
363
|
+
direction = 'drifting';
|
|
662
364
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
const confidence = events.length >= 8 || evidenceScore >= 3 ? "high" : events.length >= 4 || evidenceScore >= 1 ? "medium" : "low";
|
|
687
|
-
const security = securityFindings.some((finding) => finding.severity === "high") ? "alert" : securityFindings.length > 0 ? "watch" : "clean";
|
|
688
|
-
if (sessionTitle) {
|
|
689
|
-
pushUnique(progressSignals, `session goal/title: ${sessionTitle}`, 5);
|
|
690
|
-
}
|
|
691
|
-
return {
|
|
692
|
-
direction,
|
|
693
|
-
confidence,
|
|
694
|
-
security,
|
|
695
|
-
activitySummary: summarizeActivity(reads, writes, searches, commands, tests, errors),
|
|
696
|
-
progressSignals,
|
|
697
|
-
driftSignals,
|
|
698
|
-
securityFindings: securityFindings.slice(0, 3)
|
|
699
|
-
};
|
|
365
|
+
const evidenceScore = writes + tests + deploys + (errors > 0 ? 1 : 0);
|
|
366
|
+
const confidence = events.length >= 8 || evidenceScore >= 3
|
|
367
|
+
? 'high'
|
|
368
|
+
: events.length >= 4 || evidenceScore >= 1
|
|
369
|
+
? 'medium'
|
|
370
|
+
: 'low';
|
|
371
|
+
const security = securityFindings.some((finding) => finding.severity === 'high')
|
|
372
|
+
? 'alert'
|
|
373
|
+
: securityFindings.length > 0
|
|
374
|
+
? 'watch'
|
|
375
|
+
: 'clean';
|
|
376
|
+
if (sessionTitle) {
|
|
377
|
+
pushUnique(progressSignals, `session goal/title: ${sessionTitle}`, 5);
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
direction,
|
|
381
|
+
confidence,
|
|
382
|
+
security,
|
|
383
|
+
activitySummary: summarizeActivity(reads, writes, searches, commands, tests, errors),
|
|
384
|
+
progressSignals,
|
|
385
|
+
driftSignals,
|
|
386
|
+
securityFindings: securityFindings.slice(0, 3),
|
|
387
|
+
};
|
|
700
388
|
}
|
|
701
389
|
function applyAssessmentSignals(commentary, assessment) {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
}
|
|
719
|
-
return out;
|
|
390
|
+
let out = String(commentary || '').trim();
|
|
391
|
+
if (!out) {
|
|
392
|
+
out = 'Agent completed actions in this session.';
|
|
393
|
+
}
|
|
394
|
+
const hasSecuritySignal = /\bsecurity\b|\bsecurity alert\b|\bsecurity watch\b/i.test(out);
|
|
395
|
+
if (!hasSecuritySignal && assessment.security !== 'clean') {
|
|
396
|
+
const top = assessment.securityFindings[0];
|
|
397
|
+
const label = assessment.security === 'alert' ? 'SECURITY ALERT' : 'SECURITY WATCH';
|
|
398
|
+
const reason = top ? `${top.signal} (${top.evidence})` : 'Risky behavior detected in this batch.';
|
|
399
|
+
out = `${label}: ${reason}\n${out}`;
|
|
400
|
+
}
|
|
401
|
+
const hasClosingLine = /\bverdict\b|\bon.?track\b|\bdrifting\b|\bblocked\b|\bmomentum\b/i.test(out);
|
|
402
|
+
if (!hasClosingLine) {
|
|
403
|
+
out = `${out}\n${createClosingLine(assessment)}`;
|
|
404
|
+
}
|
|
405
|
+
return out;
|
|
720
406
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
}
|
|
741
|
-
setModel(model) {
|
|
742
|
-
this.model = model;
|
|
743
|
-
this.emit("model-changed", model);
|
|
744
|
-
}
|
|
745
|
-
getModel() {
|
|
746
|
-
return this.model;
|
|
747
|
-
}
|
|
748
|
-
setFocus(focus) {
|
|
749
|
-
this.userFocus = focus;
|
|
750
|
-
}
|
|
751
|
-
getFocus() {
|
|
752
|
-
return this.userFocus;
|
|
753
|
-
}
|
|
754
|
-
setPreset(preset) {
|
|
755
|
-
const next = Object.prototype.hasOwnProperty.call(PRESET_INSTRUCTIONS, preset) ? preset : "auto";
|
|
756
|
-
this.preset = next;
|
|
757
|
-
this.emit("preset-changed", this.preset);
|
|
758
|
-
}
|
|
759
|
-
getPreset() {
|
|
760
|
-
return this.preset;
|
|
761
|
-
}
|
|
762
|
-
setFormatStyles(styleIds) {
|
|
763
|
-
const next = normalizeFormatStyles(Array.isArray(styleIds) ? styleIds : []);
|
|
764
|
-
if (sameFormatStyles(next, this.enabledFormatStyleIds)) return;
|
|
765
|
-
this.enabledFormatStyleIds = next;
|
|
766
|
-
if (this.lastFormatStyleId && !this.enabledFormatStyleIds.includes(this.lastFormatStyleId)) {
|
|
767
|
-
this.lastFormatStyleId = null;
|
|
407
|
+
class CommentaryEngine extends events_1.EventEmitter {
|
|
408
|
+
sessions = new Map();
|
|
409
|
+
flushQueue = [];
|
|
410
|
+
queuedSessions = new Set();
|
|
411
|
+
generating = false;
|
|
412
|
+
model = types_1.DEFAULT_MODEL;
|
|
413
|
+
userFocus = '';
|
|
414
|
+
preset = 'auto';
|
|
415
|
+
providers = {
|
|
416
|
+
anthropic: new anthropic_1.AnthropicProvider(),
|
|
417
|
+
openai: new openai_1.OpenAIProvider(),
|
|
418
|
+
};
|
|
419
|
+
keyResolver;
|
|
420
|
+
paused = false;
|
|
421
|
+
enabledFormatStyleIds = DEFAULT_FORMAT_STYLE_IDS.slice();
|
|
422
|
+
lastFormatStyleId = null;
|
|
423
|
+
constructor(keyResolver) {
|
|
424
|
+
super();
|
|
425
|
+
this.keyResolver = keyResolver;
|
|
768
426
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
return this.enabledFormatStyleIds.slice();
|
|
773
|
-
}
|
|
774
|
-
pause() {
|
|
775
|
-
this.paused = true;
|
|
776
|
-
}
|
|
777
|
-
resume() {
|
|
778
|
-
this.paused = false;
|
|
779
|
-
}
|
|
780
|
-
isPaused() {
|
|
781
|
-
return this.paused;
|
|
782
|
-
}
|
|
783
|
-
addEvent(event) {
|
|
784
|
-
if (this.paused) return;
|
|
785
|
-
if (event.type === "tool_result" || event.type === "meta") return;
|
|
786
|
-
const key = this.sessionKey(event);
|
|
787
|
-
const state = this.ensureSessionState(key);
|
|
788
|
-
state.events.push(event);
|
|
789
|
-
if (state.events.length >= MAX_BATCH_SIZE) {
|
|
790
|
-
this.requestFlush(key, true);
|
|
791
|
-
return;
|
|
427
|
+
setModel(model) {
|
|
428
|
+
this.model = model;
|
|
429
|
+
this.emit('model-changed', model);
|
|
792
430
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
if (!state.maxTimer) {
|
|
796
|
-
state.maxTimer = setTimeout(() => this.requestFlush(key, true), MAX_WAIT_MS);
|
|
431
|
+
getModel() {
|
|
432
|
+
return this.model;
|
|
797
433
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
return `${event.agent}:${event.sessionId}`;
|
|
801
|
-
}
|
|
802
|
-
ensureSessionState(key) {
|
|
803
|
-
if (!this.sessions.has(key)) {
|
|
804
|
-
this.sessions.set(key, {
|
|
805
|
-
events: [],
|
|
806
|
-
idleTimer: null,
|
|
807
|
-
maxTimer: null,
|
|
808
|
-
recentCommentary: []
|
|
809
|
-
});
|
|
434
|
+
setFocus(focus) {
|
|
435
|
+
this.userFocus = focus;
|
|
810
436
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
clearSessionTimers(state) {
|
|
814
|
-
if (state.idleTimer) {
|
|
815
|
-
clearTimeout(state.idleTimer);
|
|
816
|
-
state.idleTimer = null;
|
|
437
|
+
getFocus() {
|
|
438
|
+
return this.userFocus;
|
|
817
439
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
440
|
+
setPreset(preset) {
|
|
441
|
+
const next = Object.prototype.hasOwnProperty.call(PRESET_INSTRUCTIONS, preset)
|
|
442
|
+
? preset
|
|
443
|
+
: 'auto';
|
|
444
|
+
this.preset = next;
|
|
445
|
+
this.emit('preset-changed', this.preset);
|
|
821
446
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
const state = this.sessions.get(key);
|
|
825
|
-
if (!state) return;
|
|
826
|
-
this.clearSessionTimers(state);
|
|
827
|
-
if (state.events.length === 0) return;
|
|
828
|
-
if (!force && state.events.length < MIN_BATCH_SIZE) {
|
|
829
|
-
state.idleTimer = setTimeout(() => this.requestFlush(key, true), IDLE_TIMEOUT_MS);
|
|
830
|
-
return;
|
|
447
|
+
getPreset() {
|
|
448
|
+
return this.preset;
|
|
831
449
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
}
|
|
840
|
-
async processFlushQueue() {
|
|
841
|
-
if (this.generating) return;
|
|
842
|
-
while (this.flushQueue.length > 0) {
|
|
843
|
-
const key = this.flushQueue.shift();
|
|
844
|
-
this.queuedSessions.delete(key);
|
|
845
|
-
const state = this.sessions.get(key);
|
|
846
|
-
if (!state || state.events.length === 0) continue;
|
|
847
|
-
const batch = state.events.splice(0);
|
|
848
|
-
this.generating = true;
|
|
849
|
-
try {
|
|
850
|
-
await this.generateCommentary(batch, state);
|
|
851
|
-
} catch (err) {
|
|
852
|
-
this.emit("error", err);
|
|
853
|
-
} finally {
|
|
854
|
-
this.generating = false;
|
|
855
|
-
}
|
|
856
|
-
if (state.events.length > 0) {
|
|
857
|
-
if (state.events.length >= MIN_BATCH_SIZE) {
|
|
858
|
-
this.enqueueFlush(key);
|
|
859
|
-
} else if (!state.idleTimer) {
|
|
860
|
-
state.idleTimer = setTimeout(() => this.requestFlush(key, true), IDLE_TIMEOUT_MS);
|
|
450
|
+
setFormatStyles(styleIds) {
|
|
451
|
+
const next = normalizeFormatStyles(Array.isArray(styleIds) ? styleIds : []);
|
|
452
|
+
if (sameFormatStyles(next, this.enabledFormatStyleIds))
|
|
453
|
+
return;
|
|
454
|
+
this.enabledFormatStyleIds = next;
|
|
455
|
+
if (this.lastFormatStyleId && !this.enabledFormatStyleIds.includes(this.lastFormatStyleId)) {
|
|
456
|
+
this.lastFormatStyleId = null;
|
|
861
457
|
}
|
|
862
|
-
|
|
458
|
+
this.emit('format-styles-changed', this.enabledFormatStyleIds.slice());
|
|
863
459
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
const enabled = this.enabledFormatStyleIds.length > 0 ? this.enabledFormatStyleIds : DEFAULT_FORMAT_STYLE_IDS;
|
|
867
|
-
const pool = this.lastFormatStyleId && enabled.length > 1 ? enabled.filter((styleId) => styleId !== this.lastFormatStyleId) : enabled;
|
|
868
|
-
const pickedStyleId = pool[Math.floor(Math.random() * pool.length)];
|
|
869
|
-
this.lastFormatStyleId = pickedStyleId;
|
|
870
|
-
return FORMAT_TEMPLATES[pickedStyleId];
|
|
871
|
-
}
|
|
872
|
-
async generateCommentary(events, state) {
|
|
873
|
-
if (events.length === 0) return;
|
|
874
|
-
const modelConfig = MODELS.find((m) => m.id === this.model);
|
|
875
|
-
if (!modelConfig) {
|
|
876
|
-
this.emit("error", new Error(`Unknown model: ${this.model}`));
|
|
877
|
-
return;
|
|
460
|
+
getFormatStyles() {
|
|
461
|
+
return this.enabledFormatStyleIds.slice();
|
|
878
462
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
(
|
|
904
|
-
|
|
463
|
+
pause() {
|
|
464
|
+
this.paused = true;
|
|
465
|
+
}
|
|
466
|
+
resume() {
|
|
467
|
+
this.paused = false;
|
|
468
|
+
}
|
|
469
|
+
isPaused() {
|
|
470
|
+
return this.paused;
|
|
471
|
+
}
|
|
472
|
+
addEvent(event) {
|
|
473
|
+
if (this.paused)
|
|
474
|
+
return;
|
|
475
|
+
// Skip noisy internal events
|
|
476
|
+
if (event.type === 'tool_result' || event.type === 'meta')
|
|
477
|
+
return;
|
|
478
|
+
const key = this.sessionKey(event);
|
|
479
|
+
const state = this.ensureSessionState(key);
|
|
480
|
+
state.events.push(event);
|
|
481
|
+
// Hard cap — flush immediately
|
|
482
|
+
if (state.events.length >= MAX_BATCH_SIZE) {
|
|
483
|
+
this.requestFlush(key, true);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
// Reset idle timer on each new event (wait for activity to settle)
|
|
487
|
+
if (state.idleTimer)
|
|
488
|
+
clearTimeout(state.idleTimer);
|
|
489
|
+
state.idleTimer = setTimeout(() => this.requestFlush(key, false), IDLE_TIMEOUT_MS);
|
|
490
|
+
// Start max-wait timer on first event in batch
|
|
491
|
+
if (!state.maxTimer) {
|
|
492
|
+
state.maxTimer = setTimeout(() => this.requestFlush(key, true), MAX_WAIT_MS);
|
|
905
493
|
}
|
|
906
|
-
);
|
|
907
|
-
const rawOutput = full || streamedRaw;
|
|
908
|
-
entry.commentary = applyAssessmentSignals(
|
|
909
|
-
sanitizeGeneratedCommentary(rawOutput),
|
|
910
|
-
assessment
|
|
911
|
-
);
|
|
912
|
-
if (entry.commentary) {
|
|
913
|
-
this.emit("commentary-chunk", { entry, chunk: entry.commentary });
|
|
914
|
-
}
|
|
915
|
-
this.emit("commentary-done", entry);
|
|
916
|
-
state.recentCommentary.push(entry.commentary);
|
|
917
|
-
if (state.recentCommentary.length > 5) {
|
|
918
|
-
state.recentCommentary.shift();
|
|
919
|
-
}
|
|
920
|
-
} catch (err) {
|
|
921
|
-
entry.commentary = "Commentary unavailable right now.";
|
|
922
|
-
this.emit("commentary-chunk", { entry, chunk: entry.commentary });
|
|
923
|
-
this.emit("commentary-done", entry);
|
|
924
|
-
this.emit("error", err);
|
|
925
494
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
let prompt = SYSTEM_PROMPT;
|
|
929
|
-
prompt += `
|
|
930
|
-
|
|
931
|
-
Format for this message:
|
|
932
|
-
${this.pickFormat()}`;
|
|
933
|
-
if (this.preset !== "auto") {
|
|
934
|
-
prompt += `
|
|
935
|
-
|
|
936
|
-
Tone preset:
|
|
937
|
-
${PRESET_INSTRUCTIONS[this.preset]}`;
|
|
495
|
+
sessionKey(event) {
|
|
496
|
+
return `${event.agent}:${event.sessionId}`;
|
|
938
497
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
498
|
+
ensureSessionState(key) {
|
|
499
|
+
if (!this.sessions.has(key)) {
|
|
500
|
+
this.sessions.set(key, {
|
|
501
|
+
events: [],
|
|
502
|
+
idleTimer: null,
|
|
503
|
+
maxTimer: null,
|
|
504
|
+
recentCommentary: [],
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
return this.sessions.get(key);
|
|
943
508
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
`;
|
|
509
|
+
clearSessionTimers(state) {
|
|
510
|
+
if (state.idleTimer) {
|
|
511
|
+
clearTimeout(state.idleTimer);
|
|
512
|
+
state.idleTimer = null;
|
|
513
|
+
}
|
|
514
|
+
if (state.maxTimer) {
|
|
515
|
+
clearTimeout(state.maxTimer);
|
|
516
|
+
state.maxTimer = null;
|
|
517
|
+
}
|
|
954
518
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
519
|
+
requestFlush(key, force) {
|
|
520
|
+
const state = this.sessions.get(key);
|
|
521
|
+
if (!state)
|
|
522
|
+
return;
|
|
523
|
+
this.clearSessionTimers(state);
|
|
524
|
+
if (state.events.length === 0)
|
|
525
|
+
return;
|
|
526
|
+
// Need minimum events for meaningful commentary unless force-flushing.
|
|
527
|
+
if (!force && state.events.length < MIN_BATCH_SIZE) {
|
|
528
|
+
state.idleTimer = setTimeout(() => this.requestFlush(key, true), IDLE_TIMEOUT_MS);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
this.enqueueFlush(key);
|
|
532
|
+
}
|
|
533
|
+
enqueueFlush(key) {
|
|
534
|
+
if (this.queuedSessions.has(key))
|
|
535
|
+
return;
|
|
536
|
+
this.queuedSessions.add(key);
|
|
537
|
+
this.flushQueue.push(key);
|
|
538
|
+
this.processFlushQueue();
|
|
539
|
+
}
|
|
540
|
+
async processFlushQueue() {
|
|
541
|
+
if (this.generating)
|
|
542
|
+
return;
|
|
543
|
+
while (this.flushQueue.length > 0) {
|
|
544
|
+
const key = this.flushQueue.shift();
|
|
545
|
+
this.queuedSessions.delete(key);
|
|
546
|
+
const state = this.sessions.get(key);
|
|
547
|
+
if (!state || state.events.length === 0)
|
|
548
|
+
continue;
|
|
549
|
+
const batch = state.events.splice(0);
|
|
550
|
+
this.generating = true;
|
|
551
|
+
try {
|
|
552
|
+
await this.generateCommentary(batch, state);
|
|
553
|
+
}
|
|
554
|
+
catch (err) {
|
|
555
|
+
this.emit('error', err);
|
|
556
|
+
}
|
|
557
|
+
finally {
|
|
558
|
+
this.generating = false;
|
|
559
|
+
}
|
|
560
|
+
// Events may have arrived while we were generating.
|
|
561
|
+
if (state.events.length > 0) {
|
|
562
|
+
if (state.events.length >= MIN_BATCH_SIZE) {
|
|
563
|
+
this.enqueueFlush(key);
|
|
564
|
+
}
|
|
565
|
+
else if (!state.idleTimer) {
|
|
566
|
+
state.idleTimer = setTimeout(() => this.requestFlush(key, true), IDLE_TIMEOUT_MS);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
966
570
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
571
|
+
pickFormat() {
|
|
572
|
+
const enabled = this.enabledFormatStyleIds.length > 0
|
|
573
|
+
? this.enabledFormatStyleIds
|
|
574
|
+
: DEFAULT_FORMAT_STYLE_IDS;
|
|
575
|
+
const pool = this.lastFormatStyleId && enabled.length > 1
|
|
576
|
+
? enabled.filter((styleId) => styleId !== this.lastFormatStyleId)
|
|
577
|
+
: enabled;
|
|
578
|
+
const pickedStyleId = pool[Math.floor(Math.random() * pool.length)];
|
|
579
|
+
this.lastFormatStyleId = pickedStyleId;
|
|
580
|
+
return FORMAT_TEMPLATES[pickedStyleId];
|
|
970
581
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
582
|
+
async generateCommentary(events, state) {
|
|
583
|
+
if (events.length === 0)
|
|
584
|
+
return;
|
|
585
|
+
const modelConfig = types_1.MODELS.find(m => m.id === this.model);
|
|
586
|
+
if (!modelConfig) {
|
|
587
|
+
this.emit('error', new Error(`Unknown model: ${this.model}`));
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const apiKey = '';
|
|
591
|
+
const systemPrompt = this.buildSystemPrompt();
|
|
592
|
+
const latest = events[events.length - 1];
|
|
593
|
+
const assessment = buildCommentaryAssessment(events, latest.sessionTitle);
|
|
594
|
+
const userPrompt = this.buildUserPrompt(events, state.recentCommentary, assessment);
|
|
595
|
+
const provider = this.providers[modelConfig.provider];
|
|
596
|
+
const entry = {
|
|
597
|
+
timestamp: Date.now(),
|
|
598
|
+
sessionId: latest.sessionId,
|
|
599
|
+
projectName: latest.projectName,
|
|
600
|
+
sessionTitle: latest.sessionTitle,
|
|
601
|
+
agent: latest.agent,
|
|
602
|
+
source: latest.source,
|
|
603
|
+
eventSummary: events.map(e => sanitizePromptText(e.summary)).join(' → '),
|
|
604
|
+
commentary: '',
|
|
605
|
+
};
|
|
606
|
+
this.emit('commentary-start', entry);
|
|
607
|
+
try {
|
|
608
|
+
let streamedRaw = '';
|
|
609
|
+
const full = await provider.generate(systemPrompt, userPrompt, apiKey || '', this.model, (chunk) => {
|
|
610
|
+
streamedRaw += chunk;
|
|
611
|
+
});
|
|
612
|
+
const rawOutput = full || streamedRaw;
|
|
613
|
+
entry.commentary = applyAssessmentSignals(sanitizeGeneratedCommentary(rawOutput), assessment);
|
|
614
|
+
if (entry.commentary) {
|
|
615
|
+
this.emit('commentary-chunk', { entry, chunk: entry.commentary });
|
|
616
|
+
}
|
|
617
|
+
this.emit('commentary-done', entry);
|
|
618
|
+
// Track recent commentary per session to avoid repetition.
|
|
619
|
+
state.recentCommentary.push(entry.commentary);
|
|
620
|
+
if (state.recentCommentary.length > 5) {
|
|
621
|
+
state.recentCommentary.shift();
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
catch (err) {
|
|
625
|
+
entry.commentary = 'Commentary unavailable right now.';
|
|
626
|
+
this.emit('commentary-chunk', { entry, chunk: entry.commentary });
|
|
627
|
+
this.emit('commentary-done', entry);
|
|
628
|
+
this.emit('error', err);
|
|
629
|
+
}
|
|
984
630
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
631
|
+
buildSystemPrompt() {
|
|
632
|
+
let prompt = SYSTEM_PROMPT;
|
|
633
|
+
// Add random format template
|
|
634
|
+
prompt += `\n\nFormat for this message:\n${this.pickFormat()}`;
|
|
635
|
+
if (this.preset !== 'auto') {
|
|
636
|
+
prompt += `\n\nTone preset:\n${PRESET_INSTRUCTIONS[this.preset]}`;
|
|
637
|
+
}
|
|
638
|
+
if (this.userFocus.trim()) {
|
|
639
|
+
prompt += `\n\nAdditional user instruction: ${this.userFocus}`;
|
|
640
|
+
}
|
|
641
|
+
return prompt;
|
|
993
642
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
643
|
+
buildUserPrompt(events, recentCommentary, assessment) {
|
|
644
|
+
const latest = events[events.length - 1];
|
|
645
|
+
const lines = events.map(e => ` ${e.type}: ${sanitizePromptText(e.summary)}`);
|
|
646
|
+
let prompt = `Session: ${latest.agent}/${latest.projectName}\n`;
|
|
647
|
+
if (latest.sessionTitle) {
|
|
648
|
+
prompt += `Session title: ${sanitizePromptText(latest.sessionTitle)}\n`;
|
|
649
|
+
}
|
|
650
|
+
prompt += `Actions (${events.length}):\n${lines.join('\n')}`;
|
|
651
|
+
prompt += `\n\nDirection context:\n`;
|
|
652
|
+
prompt += `- Activity snapshot: ${assessment.activitySummary}\n`;
|
|
653
|
+
if (assessment.progressSignals.length > 0) {
|
|
654
|
+
prompt += `- Progress signals: ${assessment.progressSignals.join(' | ')}\n`;
|
|
655
|
+
}
|
|
656
|
+
if (assessment.driftSignals.length > 0) {
|
|
657
|
+
prompt += `- Drift/block signals: ${assessment.driftSignals.join(' | ')}\n`;
|
|
658
|
+
}
|
|
659
|
+
prompt += `- Initial direction estimate from raw signals: ${assessment.direction} (${assessment.confidence} confidence)\n`;
|
|
660
|
+
prompt += `\nSecurity auto-check:\n`;
|
|
661
|
+
prompt += `- Security state: ${assessment.security}\n`;
|
|
662
|
+
if (assessment.securityFindings.length > 0) {
|
|
663
|
+
prompt += assessment.securityFindings
|
|
664
|
+
.map((finding) => `- ${finding.severity.toUpperCase()}: ${finding.signal} (${finding.evidence})`)
|
|
665
|
+
.join('\n');
|
|
666
|
+
prompt += '\n';
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
prompt += `- No explicit security flags in this batch.\n`;
|
|
670
|
+
}
|
|
671
|
+
// Include recent commentary so the LLM doesn't repeat itself
|
|
672
|
+
if (recentCommentary.length > 0) {
|
|
673
|
+
prompt += `\n\nPrevious commentary (don't repeat):\n`;
|
|
674
|
+
prompt += recentCommentary
|
|
675
|
+
.slice(-3)
|
|
676
|
+
.map(c => `- ${sanitizePromptText(c).slice(0, 80)}...`)
|
|
677
|
+
.join('\n');
|
|
678
|
+
prompt += `\nUse different judgment wording than these previous lines.`;
|
|
679
|
+
}
|
|
680
|
+
const recentlyUsed = extractRecentlyUsedJudgments(recentCommentary);
|
|
681
|
+
if (recentlyUsed.length > 0) {
|
|
682
|
+
prompt += `\nAvoid reusing these reaction phrases: ${recentlyUsed.join(', ')}.`;
|
|
683
|
+
}
|
|
684
|
+
prompt += `\n\nSummarize only this session's actions. Plain language, with your reaction.`;
|
|
685
|
+
prompt += `\nClose with one sentence about direction and momentum — weave in confidence and security naturally.`;
|
|
686
|
+
prompt += `\nNever mention IDs/logs/prompts/traces or internal data-collection steps.`;
|
|
687
|
+
prompt += `\nNever say "I", "I'll", "we", or any future plan.`;
|
|
688
|
+
return prompt;
|
|
998
689
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
Summarize only this session's actions. Plain language, with your reaction.`;
|
|
1002
|
-
prompt += `
|
|
1003
|
-
Close with one sentence about direction and momentum \u2014 weave in confidence and security naturally.`;
|
|
1004
|
-
prompt += `
|
|
1005
|
-
Never mention IDs/logs/prompts/traces or internal data-collection steps.`;
|
|
1006
|
-
prompt += `
|
|
1007
|
-
Never say "I", "I'll", "we", or any future plan.`;
|
|
1008
|
-
return prompt;
|
|
1009
|
-
}
|
|
1010
|
-
};
|
|
690
|
+
}
|
|
691
|
+
exports.CommentaryEngine = CommentaryEngine;
|
|
1011
692
|
function redactSessionIdentifiers(text) {
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
693
|
+
let out = text;
|
|
694
|
+
for (const pattern of SESSION_ID_PATTERNS) {
|
|
695
|
+
out = out.replace(pattern, '[session]');
|
|
696
|
+
}
|
|
697
|
+
return out;
|
|
1017
698
|
}
|
|
1018
699
|
function sanitizePromptText(text) {
|
|
1019
|
-
|
|
700
|
+
return redactSessionIdentifiers(String(text || ''))
|
|
701
|
+
.replace(/\s+/g, ' ')
|
|
702
|
+
.trim();
|
|
1020
703
|
}
|
|
1021
704
|
function sanitizeGeneratedCommentary(text) {
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
705
|
+
let out = redactSessionIdentifiers(String(text || ''));
|
|
706
|
+
out = out.replace(/\blocal session logs?\b/gi, 'recent actions');
|
|
707
|
+
out = out.replace(/\bsession logs?\b/gi, 'actions');
|
|
708
|
+
out = out.replace(/\bsession id\b/gi, 'session');
|
|
709
|
+
// Strip any "Verdict:" label the LLM generates anywhere in the text — keep the text after it
|
|
710
|
+
out = out.replace(/\bverdict\s*:\s*/gi, '');
|
|
711
|
+
const blockedLine = /\b(i['’]?ll|i will|we(?:'ll| will)?)\b.*\b(pull|fetch|read|inspect|scan|parse|load|check)\b.*\b(session|log|trace|jsonl|prompt|history)\b/i;
|
|
712
|
+
const keptLines = out.split('\n').filter(line => !blockedLine.test(line.trim()));
|
|
713
|
+
out = keptLines.join('\n').trim();
|
|
714
|
+
if (!out) {
|
|
715
|
+
return 'Agent completed actions in this session.';
|
|
716
|
+
}
|
|
717
|
+
return out;
|
|
1034
718
|
}
|
|
1035
|
-
|
|
1036
|
-
0 && (module.exports = {
|
|
1037
|
-
CommentaryEngine,
|
|
1038
|
-
applyAssessmentSignals,
|
|
1039
|
-
buildCommentaryAssessment,
|
|
1040
|
-
sanitizeGeneratedCommentary
|
|
1041
|
-
});
|
|
719
|
+
//# sourceMappingURL=commentary.js.map
|