sentinelayer-cli 0.8.11 → 0.8.12
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 +4 -4
- package/src/agents/jules/stream.js +2 -12
- package/src/audit/orchestrator.js +471 -114
- package/src/audit/persona-loop.js +1342 -0
- package/src/audit/registry.js +58 -2
- package/src/commands/audit.js +42 -1
- package/src/commands/legacy-args.js +28 -1
- package/src/commands/session.js +80 -20
- package/src/cost/history.js +41 -21
- package/src/events/schema.js +27 -1
- package/src/legacy-cli.js +76 -1
- package/src/review/omargate-cache.js +285 -0
- package/src/review/omargate-orchestrator.js +586 -3
- package/src/review/report.js +128 -2
- package/src/session/senti-naming.js +36 -0
- package/src/session/sync.js +23 -0
package/src/audit/registry.js
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
import fsp from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
+
export const DEFAULT_AUDIT_AGENT_TOOLS = Object.freeze([
|
|
5
|
+
"FileRead",
|
|
6
|
+
"Grep",
|
|
7
|
+
"Glob",
|
|
8
|
+
"Shell",
|
|
9
|
+
"FileEdit",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const TOOL_NAME_ALIASES = Object.freeze({
|
|
13
|
+
read: "FileRead",
|
|
14
|
+
file_read: "FileRead",
|
|
15
|
+
"file-read": "FileRead",
|
|
16
|
+
fileread: "FileRead",
|
|
17
|
+
grep: "Grep",
|
|
18
|
+
glob: "Glob",
|
|
19
|
+
shell: "Shell",
|
|
20
|
+
file_edit: "FileEdit",
|
|
21
|
+
"file-edit": "FileEdit",
|
|
22
|
+
fileedit: "FileEdit",
|
|
23
|
+
dispatch: "Dispatch",
|
|
24
|
+
});
|
|
25
|
+
|
|
4
26
|
const BUILTIN_AUDIT_AGENTS = Object.freeze([
|
|
5
27
|
{
|
|
6
28
|
id: "security",
|
|
@@ -173,16 +195,40 @@ function normalizeString(value) {
|
|
|
173
195
|
return String(value || "").trim();
|
|
174
196
|
}
|
|
175
197
|
|
|
198
|
+
function normalizeToolName(value) {
|
|
199
|
+
const raw = normalizeString(value);
|
|
200
|
+
if (!raw) {
|
|
201
|
+
return "";
|
|
202
|
+
}
|
|
203
|
+
const alias = TOOL_NAME_ALIASES[raw.toLowerCase()];
|
|
204
|
+
if (alias) {
|
|
205
|
+
return alias;
|
|
206
|
+
}
|
|
207
|
+
return raw;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function normalizeAuditAgentTools(tools = [], { useDefaultWhenEmpty = false } = {}) {
|
|
211
|
+
const normalized = Array.isArray(tools)
|
|
212
|
+
? tools.map((item) => normalizeToolName(item)).filter(Boolean)
|
|
213
|
+
: [];
|
|
214
|
+
const unique = [...new Set(normalized)];
|
|
215
|
+
if (unique.length > 0) {
|
|
216
|
+
return unique;
|
|
217
|
+
}
|
|
218
|
+
return useDefaultWhenEmpty ? [...DEFAULT_AUDIT_AGENT_TOOLS] : [];
|
|
219
|
+
}
|
|
220
|
+
|
|
176
221
|
function normalizeAgentId(value) {
|
|
177
222
|
return normalizeString(value).toLowerCase();
|
|
178
223
|
}
|
|
179
224
|
|
|
180
225
|
function normalizeAgentRecord(record = {}) {
|
|
226
|
+
const hasToolOverride = Object.prototype.hasOwnProperty.call(record, "tools");
|
|
181
227
|
return {
|
|
182
228
|
id: normalizeAgentId(record.id),
|
|
183
229
|
persona: normalizeString(record.persona),
|
|
184
230
|
domain: normalizeString(record.domain),
|
|
185
|
-
tools:
|
|
231
|
+
tools: normalizeAuditAgentTools(record.tools, { useDefaultWhenEmpty: !hasToolOverride }),
|
|
186
232
|
permissionMode: normalizeString(record.permissionMode || "plan") || "plan",
|
|
187
233
|
maxTurns: Math.max(1, Math.floor(Number(record.maxTurns || 1))),
|
|
188
234
|
confidenceFloor: Math.max(0, Math.min(1, Number(record.confidenceFloor || 0))),
|
|
@@ -206,6 +252,11 @@ function mergeRegistry(builtinAgents = [], overrideAgents = []) {
|
|
|
206
252
|
continue;
|
|
207
253
|
}
|
|
208
254
|
const existing = byId.get(normalized.id) || {};
|
|
255
|
+
if (!Object.prototype.hasOwnProperty.call(override, "tools")) {
|
|
256
|
+
normalized.tools = Array.isArray(existing.tools) && existing.tools.length > 0
|
|
257
|
+
? [...existing.tools]
|
|
258
|
+
: [...DEFAULT_AUDIT_AGENT_TOOLS];
|
|
259
|
+
}
|
|
209
260
|
byId.set(normalized.id, {
|
|
210
261
|
...existing,
|
|
211
262
|
...normalized,
|
|
@@ -223,7 +274,12 @@ function parseAgentFilter(rawValue) {
|
|
|
223
274
|
}
|
|
224
275
|
|
|
225
276
|
export function listBuiltinAuditAgents() {
|
|
226
|
-
return BUILTIN_AUDIT_AGENTS.map((agent) =>
|
|
277
|
+
return BUILTIN_AUDIT_AGENTS.map((agent) =>
|
|
278
|
+
normalizeAgentRecord({
|
|
279
|
+
...agent,
|
|
280
|
+
tools: DEFAULT_AUDIT_AGENT_TOOLS,
|
|
281
|
+
})
|
|
282
|
+
);
|
|
227
283
|
}
|
|
228
284
|
|
|
229
285
|
export async function loadAuditRegistry({ registryFile = "" } = {}) {
|
package/src/commands/audit.js
CHANGED
|
@@ -26,6 +26,14 @@ function parseMaxParallel(rawValue) {
|
|
|
26
26
|
return Math.floor(normalized);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function parseIsolationMode(rawValue) {
|
|
30
|
+
const normalized = String(rawValue || "strict").trim().toLowerCase();
|
|
31
|
+
if (normalized === "strict" || normalized === "relaxed") {
|
|
32
|
+
return normalized;
|
|
33
|
+
}
|
|
34
|
+
throw new Error("isolation must be one of: strict, relaxed.");
|
|
35
|
+
}
|
|
36
|
+
|
|
29
37
|
function printAuditSummary(result) {
|
|
30
38
|
console.log(pc.bold("Audit orchestrator complete"));
|
|
31
39
|
console.log(pc.gray(`Run: ${result.runId}`));
|
|
@@ -34,6 +42,11 @@ function printAuditSummary(result) {
|
|
|
34
42
|
if (result.sharedMemory?.artifactPath) {
|
|
35
43
|
console.log(pc.gray(`Shared memory: ${result.sharedMemory.artifactPath}`));
|
|
36
44
|
}
|
|
45
|
+
if (result.omargateReuse?.used) {
|
|
46
|
+
console.log(pc.gray(`Reused OmarGate run: ${result.omargateReuse.runId}`));
|
|
47
|
+
} else if (result.omargateReuse?.requested) {
|
|
48
|
+
console.log(pc.gray(`OmarGate reuse unavailable: ${result.omargateReuse.reason || "not_found"}`));
|
|
49
|
+
}
|
|
37
50
|
if (result.ddPackage?.executiveSummaryPath) {
|
|
38
51
|
console.log(pc.gray(`DD package: ${result.ddPackage.executiveSummaryPath}`));
|
|
39
52
|
}
|
|
@@ -51,6 +64,15 @@ function printAuditSummary(result) {
|
|
|
51
64
|
console.log(`Agents: ${result.agentResults.length}, max_parallel=${result.maxParallel}`);
|
|
52
65
|
}
|
|
53
66
|
|
|
67
|
+
function buildAuditOrchestratorEventHandler(emitStream) {
|
|
68
|
+
if (!emitStream) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return (evt) => {
|
|
72
|
+
console.log(JSON.stringify(evt));
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
54
76
|
export function registerAuditCommand(program, invokeLegacy) {
|
|
55
77
|
const audit = program
|
|
56
78
|
.command("audit")
|
|
@@ -63,9 +85,14 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
63
85
|
.option("--output-dir <path>", "Optional artifact output root override")
|
|
64
86
|
.option("--refresh", "Refresh CODEBASE_INGEST before running audit")
|
|
65
87
|
.option("--dry-run", "Skip deterministic baseline and run orchestration planning only")
|
|
88
|
+
.option("--isolation <mode>", "Persona isolation mode: strict | relaxed", "strict")
|
|
89
|
+
.option("--no-seed-from-deterministic", "Run personas without deterministic baseline or specialist seed findings")
|
|
90
|
+
.option("--reuse-omargate <runId>", "Reuse deterministic findings from an OmarGate run id or latest")
|
|
91
|
+
.option("--stream", "Emit NDJSON agent events to stdout")
|
|
66
92
|
.option("--json", "Emit machine-readable output")
|
|
67
93
|
.action(async (targetPathArg, options, command) => {
|
|
68
94
|
const emitJson = shouldEmitJson(options, command);
|
|
95
|
+
const emitStream = Boolean(options.stream);
|
|
69
96
|
const targetPath = path.resolve(process.cwd(), String(options.path || targetPathArg || "."));
|
|
70
97
|
const registry = await loadAuditRegistry({
|
|
71
98
|
registryFile: options.registryFile,
|
|
@@ -85,6 +112,10 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
85
112
|
outputDir: options.outputDir,
|
|
86
113
|
dryRun: Boolean(options.dryRun),
|
|
87
114
|
refreshIngest: Boolean(options.refresh),
|
|
115
|
+
isolation: parseIsolationMode(options.isolation),
|
|
116
|
+
seedFromDeterministic: options.seedFromDeterministic !== false,
|
|
117
|
+
reuseOmarGate: options.reuseOmargate,
|
|
118
|
+
onEvent: buildAuditOrchestratorEventHandler(emitStream),
|
|
88
119
|
});
|
|
89
120
|
|
|
90
121
|
const payload = {
|
|
@@ -99,6 +130,11 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
99
130
|
registryFile: registry.registryFile,
|
|
100
131
|
selectedAgents: result.selectedAgents,
|
|
101
132
|
maxParallel: result.maxParallel,
|
|
133
|
+
isolation: result.isolation,
|
|
134
|
+
seedFromDeterministic: result.seedFromDeterministic !== false,
|
|
135
|
+
omargateReuse: result.omargateReuse || null,
|
|
136
|
+
reusedOmarGateRunId: result.omargateReuse?.used ? result.omargateReuse.runId : "",
|
|
137
|
+
reusedOmarGateDeterministicPath: result.omargateReuse?.used ? result.omargateReuse.artifactPath : "",
|
|
102
138
|
summary: result.summary,
|
|
103
139
|
agentCount: result.agentResults.length,
|
|
104
140
|
sharedMemoryPath: result.sharedMemory?.artifactPath || "",
|
|
@@ -112,7 +148,7 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
112
148
|
|
|
113
149
|
if (emitJson) {
|
|
114
150
|
console.log(JSON.stringify(payload, null, 2));
|
|
115
|
-
} else {
|
|
151
|
+
} else if (!emitStream) {
|
|
116
152
|
printAuditSummary(result);
|
|
117
153
|
}
|
|
118
154
|
|
|
@@ -211,6 +247,8 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
211
247
|
outputDir: options.outputDir,
|
|
212
248
|
dryRun: Boolean(baseReport.dryRun),
|
|
213
249
|
refreshIngest: Boolean(options.refresh),
|
|
250
|
+
isolation: baseReport.isolation || "strict",
|
|
251
|
+
seedFromDeterministic: baseReport.seedFromDeterministic !== false,
|
|
214
252
|
});
|
|
215
253
|
|
|
216
254
|
const comparison = await writeAuditComparisonArtifact({
|
|
@@ -231,6 +269,8 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
231
269
|
deterministicEquivalent: comparison.comparison.deterministicEquivalent,
|
|
232
270
|
addedCount: comparison.comparison.addedCount,
|
|
233
271
|
removedCount: comparison.comparison.removedCount,
|
|
272
|
+
isolation: replayResult.isolation,
|
|
273
|
+
seedFromDeterministic: replayResult.seedFromDeterministic !== false,
|
|
234
274
|
ingestRefresh: replayResult.ingest?.refresh || null,
|
|
235
275
|
};
|
|
236
276
|
|
|
@@ -717,6 +757,7 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
717
757
|
.description("Compatibility mode: run legacy local readiness + policy audit")
|
|
718
758
|
.option("--path <path>", "Target repository path")
|
|
719
759
|
.option("--output-dir <path>", "Artifact root for report output")
|
|
760
|
+
.option("--reuse-omargate <runId>", "Reuse deterministic findings from an OmarGate run id or latest")
|
|
720
761
|
.option("--json", "Emit machine-readable output")
|
|
721
762
|
.action(async (options, command) => {
|
|
722
763
|
const legacyArgs = buildLegacyArgs(["/audit"], {
|
|
@@ -20,12 +20,24 @@ function appendOutputDirFlag(args, maybeOutputDir) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function appendPassthroughFlag(args, flagName, maybeValue) {
|
|
23
|
-
const value =
|
|
23
|
+
const value = maybeValue === undefined || maybeValue === null ? "" : String(maybeValue).trim();
|
|
24
24
|
if (value) {
|
|
25
25
|
args.push(flagName, value);
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function appendBooleanFlag(args, flagName, maybeValue) {
|
|
30
|
+
if (Boolean(maybeValue)) {
|
|
31
|
+
args.push(flagName);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function appendNegatedBooleanFlag(args, flagName, maybeValue) {
|
|
36
|
+
if (maybeValue === false) {
|
|
37
|
+
args.push(flagName);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
29
41
|
export function buildLegacyArgs(baseArgs, { commandOptions = {}, command } = {}) {
|
|
30
42
|
const args = [...baseArgs];
|
|
31
43
|
appendPathFlag(args, commandOptions.path);
|
|
@@ -33,6 +45,21 @@ export function buildLegacyArgs(baseArgs, { commandOptions = {}, command } = {})
|
|
|
33
45
|
if (wantsJsonOutput(commandOptions, command)) {
|
|
34
46
|
args.push("--json");
|
|
35
47
|
}
|
|
48
|
+
appendNegatedBooleanFlag(args, "--no-ai", commandOptions.ai);
|
|
49
|
+
appendBooleanFlag(args, "--ai-dry-run", commandOptions.aiDryRun);
|
|
50
|
+
appendBooleanFlag(args, "--stream", commandOptions.stream);
|
|
51
|
+
appendBooleanFlag(args, "--dry-run", commandOptions.dryRun);
|
|
52
|
+
appendNegatedBooleanFlag(args, "--no-email", commandOptions.email);
|
|
53
|
+
appendNegatedBooleanFlag(args, "--no-dashboard", commandOptions.dashboard);
|
|
54
|
+
appendPassthroughFlag(args, "--scan-mode", commandOptions.scanMode);
|
|
55
|
+
appendPassthroughFlag(args, "--max-parallel", commandOptions.maxParallel);
|
|
56
|
+
appendPassthroughFlag(args, "--max-cost", commandOptions.maxCost);
|
|
57
|
+
appendPassthroughFlag(args, "--max-runtime-minutes", commandOptions.maxRuntimeMinutes);
|
|
58
|
+
appendPassthroughFlag(args, "--model", commandOptions.model);
|
|
59
|
+
appendPassthroughFlag(args, "--provider", commandOptions.provider);
|
|
60
|
+
appendPassthroughFlag(args, "--reuse-omargate", commandOptions.reuseOmargate);
|
|
61
|
+
appendPassthroughFlag(args, "--notify-email", commandOptions.notifyEmail);
|
|
62
|
+
appendPassthroughFlag(args, "--notify-session", commandOptions.notifySession);
|
|
36
63
|
// Omar Gate per-persona filter flags (A-CLI-1).
|
|
37
64
|
appendPassthroughFlag(args, "--persona", commandOptions.persona);
|
|
38
65
|
appendPassthroughFlag(args, "--skip-persona", commandOptions.skipPersona);
|
package/src/commands/session.js
CHANGED
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
} from "../session/sync.js";
|
|
58
58
|
import { hydrateSessionFromRemote } from "../session/remote-hydrate.js";
|
|
59
59
|
import { mergeLiveSources } from "../session/live-source.js";
|
|
60
|
+
import { deriveSessionTitle } from "../session/senti-naming.js";
|
|
60
61
|
import {
|
|
61
62
|
buildDashboardUrl,
|
|
62
63
|
buildTemplateLaunchPlan,
|
|
@@ -302,30 +303,80 @@ export function registerSessionCommand(program) {
|
|
|
302
303
|
3600,
|
|
303
304
|
);
|
|
304
305
|
|
|
305
|
-
// Auto-resume:
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
306
|
+
// Auto-resume: prefer an existing active session for this codebase
|
|
307
|
+
// over minting a new one. We check both local filesystem state and the
|
|
308
|
+
// remote registry — local-only resume meant a fresh checkout / second
|
|
309
|
+
// machine would orphan the room each time, exactly the mess Carter
|
|
310
|
+
// surfaced ("all of them look like one chat re-created").
|
|
311
|
+
//
|
|
312
|
+
// Order:
|
|
313
|
+
// 1. Local session for the same targetPath inside the reuse window.
|
|
314
|
+
// 2. Remote active session whose codebasePath matches the absolute
|
|
315
|
+
// targetPath, sorted by last activity. We fold these into the
|
|
316
|
+
// candidate pool so a session minted on another machine can be
|
|
317
|
+
// rejoined rather than duplicated.
|
|
318
|
+
// `--force-new` opts back into the old "always mint" behavior.
|
|
310
319
|
let resumed = null;
|
|
311
320
|
if (!options.forceNew) {
|
|
321
|
+
const cutoffMs = Date.now() - reuseWindowSeconds * 1000;
|
|
322
|
+
const candidates = [];
|
|
312
323
|
try {
|
|
313
324
|
const active = await listActiveSessions({ targetPath });
|
|
314
|
-
const
|
|
315
|
-
const candidates = active.filter((entry) => {
|
|
325
|
+
for (const entry of active) {
|
|
316
326
|
const createdMs = Date.parse(entry.createdAt || "");
|
|
317
|
-
|
|
327
|
+
if (Number.isFinite(createdMs) && createdMs >= cutoffMs) {
|
|
328
|
+
candidates.push({ ...entry, _source: "local" });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
} catch {
|
|
332
|
+
/* local lookup failure is non-fatal */
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
const remote = await listSessionsFromApi({
|
|
336
|
+
targetPath,
|
|
337
|
+
includeArchived: false,
|
|
338
|
+
limit: 50,
|
|
318
339
|
});
|
|
319
|
-
|
|
340
|
+
if (remote && remote.ok) {
|
|
341
|
+
const normalizedTarget = String(targetPath).toLowerCase();
|
|
342
|
+
for (const entry of remote.sessions || []) {
|
|
343
|
+
const codebase = String(entry.codebasePath || "").toLowerCase();
|
|
344
|
+
if (!codebase || codebase !== normalizedTarget) continue;
|
|
345
|
+
if (entry.archiveStatus && entry.archiveStatus !== "active") continue;
|
|
346
|
+
const lastMs = Date.parse(entry.lastActivityAt || entry.createdAt || "");
|
|
347
|
+
if (Number.isFinite(lastMs) && lastMs >= cutoffMs) {
|
|
348
|
+
candidates.push({
|
|
349
|
+
sessionId: entry.sessionId,
|
|
350
|
+
createdAt: entry.createdAt,
|
|
351
|
+
lastActivityAt: entry.lastActivityAt,
|
|
352
|
+
expiresAt: entry.expiresAt,
|
|
353
|
+
status: entry.status || "active",
|
|
354
|
+
template: entry.templateName || null,
|
|
355
|
+
title: entry.title || null,
|
|
356
|
+
_source: "remote",
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
/* remote lookup failure is non-fatal */
|
|
363
|
+
}
|
|
364
|
+
if (candidates.length > 0) {
|
|
365
|
+
// Prefer the most recent activity. Local + remote may name the
|
|
366
|
+
// same session; dedupe on sessionId before picking.
|
|
367
|
+
const seen = new Set();
|
|
368
|
+
const deduped = [];
|
|
369
|
+
for (const entry of candidates) {
|
|
370
|
+
if (seen.has(entry.sessionId)) continue;
|
|
371
|
+
seen.add(entry.sessionId);
|
|
372
|
+
deduped.push(entry);
|
|
373
|
+
}
|
|
374
|
+
deduped.sort((a, b) =>
|
|
320
375
|
String(b.lastActivityAt || b.createdAt || "").localeCompare(
|
|
321
376
|
String(a.lastActivityAt || a.createdAt || ""),
|
|
322
377
|
),
|
|
323
378
|
);
|
|
324
|
-
|
|
325
|
-
resumed = candidates[0];
|
|
326
|
-
}
|
|
327
|
-
} catch (error) {
|
|
328
|
-
// listActiveSessions failure is non-fatal; fall through to fresh create.
|
|
379
|
+
resumed = deduped[0];
|
|
329
380
|
}
|
|
330
381
|
}
|
|
331
382
|
|
|
@@ -358,11 +409,19 @@ export function registerSessionCommand(program) {
|
|
|
358
409
|
const durationMs = Date.now() - startedAt;
|
|
359
410
|
const launchPlan = template ? buildTemplateLaunchPlan(created.sessionId, template) : [];
|
|
360
411
|
const dashboardUrl = buildDashboardUrl(created.sessionId);
|
|
412
|
+
// Default the title to a stable codebase+date slug so the web sidebar
|
|
413
|
+
// never fills with anonymous "<null>" rows. The caller can still
|
|
414
|
+
// override with --title. We skip the auto-title for resumed sessions
|
|
415
|
+
// because the room already has a name we don't want to clobber.
|
|
361
416
|
const titleArg = normalizeString(options.title);
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
417
|
+
const autoTitle = !resumed && !titleArg ? deriveSessionTitle(targetPath) : "";
|
|
418
|
+
const effectiveTitle = titleArg || autoTitle;
|
|
419
|
+
|
|
420
|
+
// If a title needs to land on the dashboard, push it. We always push
|
|
421
|
+
// when the caller passed --title, AND we push the auto-derived title
|
|
422
|
+
// for fresh (non-resumed) sessions so the room is never anonymous on
|
|
423
|
+
// the web. Best-effort, non-blocking.
|
|
424
|
+
if (effectiveTitle) {
|
|
366
425
|
void (async () => {
|
|
367
426
|
try {
|
|
368
427
|
const session = await resolveActiveAuthSession({
|
|
@@ -378,7 +437,7 @@ export function registerSessionCommand(program) {
|
|
|
378
437
|
method: "POST",
|
|
379
438
|
operationName: "session.set_title",
|
|
380
439
|
headers: { Authorization: `Bearer ${session.token}` },
|
|
381
|
-
body: { title:
|
|
440
|
+
body: { title: effectiveTitle },
|
|
382
441
|
},
|
|
383
442
|
);
|
|
384
443
|
} catch (_error) {
|
|
@@ -405,7 +464,8 @@ export function registerSessionCommand(program) {
|
|
|
405
464
|
launchPlan,
|
|
406
465
|
dashboardUrl,
|
|
407
466
|
resumed: Boolean(resumed),
|
|
408
|
-
title:
|
|
467
|
+
title: effectiveTitle || null,
|
|
468
|
+
titleAuto: Boolean(autoTitle && !titleArg),
|
|
409
469
|
};
|
|
410
470
|
|
|
411
471
|
// Best-effort admin visibility sync. Session creation remains local-first.
|
package/src/cost/history.js
CHANGED
|
@@ -7,6 +7,7 @@ import { rollupUsage } from "./tracker.js";
|
|
|
7
7
|
|
|
8
8
|
const HISTORY_VERSION = 1;
|
|
9
9
|
const HISTORY_FILE_NAME = "cost-history.json";
|
|
10
|
+
const costHistoryWriteQueues = new Map();
|
|
10
11
|
|
|
11
12
|
function normalizeNumber(value, field) {
|
|
12
13
|
const normalized = Number(value || 0);
|
|
@@ -76,6 +77,14 @@ export async function resolveCostHistoryPath({
|
|
|
76
77
|
|
|
77
78
|
export async function loadCostHistory(options = {}) {
|
|
78
79
|
const filePath = await resolveCostHistoryPath(options);
|
|
80
|
+
const history = await readCostHistoryFile(filePath);
|
|
81
|
+
return {
|
|
82
|
+
filePath,
|
|
83
|
+
history,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function readCostHistoryFile(filePath) {
|
|
79
88
|
try {
|
|
80
89
|
const raw = await fsp.readFile(filePath, "utf-8");
|
|
81
90
|
const parsed = JSON.parse(raw);
|
|
@@ -83,20 +92,14 @@ export async function loadCostHistory(options = {}) {
|
|
|
83
92
|
throw new Error("Invalid cost history payload.");
|
|
84
93
|
}
|
|
85
94
|
return {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
version: Number(parsed.version || HISTORY_VERSION),
|
|
89
|
-
entries: parsed.entries,
|
|
90
|
-
},
|
|
95
|
+
version: Number(parsed.version || HISTORY_VERSION),
|
|
96
|
+
entries: parsed.entries,
|
|
91
97
|
};
|
|
92
98
|
} catch (error) {
|
|
93
99
|
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
94
100
|
return {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
version: HISTORY_VERSION,
|
|
98
|
-
entries: [],
|
|
99
|
-
},
|
|
101
|
+
version: HISTORY_VERSION,
|
|
102
|
+
entries: [],
|
|
100
103
|
};
|
|
101
104
|
}
|
|
102
105
|
throw error;
|
|
@@ -114,17 +117,34 @@ export async function saveCostHistory({ filePath, history }) {
|
|
|
114
117
|
|
|
115
118
|
export async function appendCostEntry(options = {}, entry = {}) {
|
|
116
119
|
const normalizedEntry = normalizeEntry(entry);
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
filePath,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
120
|
+
const filePath = await resolveCostHistoryPath(options);
|
|
121
|
+
return withCostHistoryWriteQueue(filePath, async () => {
|
|
122
|
+
const history = await readCostHistoryFile(filePath);
|
|
123
|
+
const nextHistory = {
|
|
124
|
+
version: HISTORY_VERSION,
|
|
125
|
+
entries: [...history.entries, normalizedEntry],
|
|
126
|
+
};
|
|
127
|
+
await saveCostHistory({ filePath, history: nextHistory });
|
|
128
|
+
return {
|
|
129
|
+
filePath,
|
|
130
|
+
entry: normalizedEntry,
|
|
131
|
+
history: nextHistory,
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function withCostHistoryWriteQueue(filePath, fn) {
|
|
137
|
+
const previous = costHistoryWriteQueues.get(filePath) || Promise.resolve();
|
|
138
|
+
const next = previous.catch(() => {}).then(fn);
|
|
139
|
+
const queued = next.catch(() => {});
|
|
140
|
+
costHistoryWriteQueues.set(filePath, queued);
|
|
141
|
+
try {
|
|
142
|
+
return await next;
|
|
143
|
+
} finally {
|
|
144
|
+
if (costHistoryWriteQueues.get(filePath) === queued) {
|
|
145
|
+
costHistoryWriteQueues.delete(filePath);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
128
148
|
}
|
|
129
149
|
|
|
130
150
|
function summarizeSessionEntries(entries) {
|
package/src/events/schema.js
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
const AGENT_EVENT_STREAM = "sl_event";
|
|
2
2
|
const LEGACY_AGENT_ID = "legacy-emitter";
|
|
3
|
+
const AGENT_EVENT_TYPES = Object.freeze([
|
|
4
|
+
"agent_start",
|
|
5
|
+
"agent_complete",
|
|
6
|
+
"agent_abort",
|
|
7
|
+
"agent_error",
|
|
8
|
+
"progress",
|
|
9
|
+
"heartbeat",
|
|
10
|
+
"tool_call",
|
|
11
|
+
"tool_result",
|
|
12
|
+
"finding",
|
|
13
|
+
"reasoning",
|
|
14
|
+
"budget_warning",
|
|
15
|
+
"budget_stop",
|
|
16
|
+
"swarm_start",
|
|
17
|
+
"swarm_complete",
|
|
18
|
+
"phase_start",
|
|
19
|
+
"phase_complete",
|
|
20
|
+
"orchestrator_start",
|
|
21
|
+
"dispatch",
|
|
22
|
+
"reconcile_start",
|
|
23
|
+
"reconcile_complete",
|
|
24
|
+
"orchestrator_complete",
|
|
25
|
+
"convergence_expansion",
|
|
26
|
+
"coverage_gap",
|
|
27
|
+
"llm_error",
|
|
28
|
+
]);
|
|
3
29
|
|
|
4
30
|
function isPlainObject(value) {
|
|
5
31
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -187,4 +213,4 @@ export function validateAgentEvent(evt, options = {}) {
|
|
|
187
213
|
return Boolean(normalizeAgentEvent(evt, options));
|
|
188
214
|
}
|
|
189
215
|
|
|
190
|
-
export { AGENT_EVENT_STREAM };
|
|
216
|
+
export { AGENT_EVENT_STREAM, AGENT_EVENT_TYPES };
|