sentinelayer-cli 0.24.0 → 0.25.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/package.json +1 -1
- package/src/audit/agents/architecture.js +4 -1
- package/src/audit/agents/documentation.js +4 -4
- package/src/audit/agents/path-classification.js +120 -0
- package/src/audit/agents/performance.js +4 -1
- package/src/audit/agents/testing.js +5 -7
- package/src/commands/action-options.js +27 -0
- package/src/commands/audit.js +84 -71
- package/src/commands/review.js +47 -40
- package/src/commands/session.js +85 -7
- package/src/coord/events-log.js +1 -1
- package/src/legacy-cli.js +3 -0
- package/src/session/invitations.js +241 -0
- package/src/session/stream.js +31 -2
package/package.json
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isAuditSourceFile } from "./path-classification.js";
|
|
2
|
+
|
|
1
3
|
function normalizeString(value) {
|
|
2
4
|
return String(value || "").trim();
|
|
3
5
|
}
|
|
@@ -21,6 +23,7 @@ function summarizeSeverity(findings = []) {
|
|
|
21
23
|
function buildHotspots(ingest = {}) {
|
|
22
24
|
const indexed = Array.isArray(ingest.indexedFiles?.files) ? ingest.indexedFiles.files : [];
|
|
23
25
|
return indexed
|
|
26
|
+
.filter((file) => isAuditSourceFile(file))
|
|
24
27
|
.filter((file) => Number(file.loc || 0) >= 300)
|
|
25
28
|
.sort((left, right) => Number(right.loc || 0) - Number(left.loc || 0))
|
|
26
29
|
.slice(0, 20)
|
|
@@ -50,7 +53,7 @@ function deriveArchitectureFindings({ findings = [], hotspots = [] } = {}) {
|
|
|
50
53
|
continue;
|
|
51
54
|
}
|
|
52
55
|
derived.push({
|
|
53
|
-
severity:
|
|
56
|
+
severity: "P2",
|
|
54
57
|
file: hotspot.path,
|
|
55
58
|
line: 1,
|
|
56
59
|
message: `Large architectural hotspot detected (${hotspot.loc} LOC).`,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isAuditSourceFile } from "./path-classification.js";
|
|
2
|
+
|
|
1
3
|
function normalizeString(value) {
|
|
2
4
|
return String(value || "").trim();
|
|
3
5
|
}
|
|
@@ -26,9 +28,7 @@ function buildDocumentationInventory(ingest = {}) {
|
|
|
26
28
|
language: file.language,
|
|
27
29
|
}));
|
|
28
30
|
const docFiles = normalized.filter((file) => /(^|\/)(docs?|guides?|adr|readme)/i.test(file.path));
|
|
29
|
-
const codeFiles = normalized.filter(
|
|
30
|
-
(file) => file.loc > 0 && !/(^|\/)(docs?|guides?|adr|readme)/i.test(file.path)
|
|
31
|
-
);
|
|
31
|
+
const codeFiles = normalized.filter((file) => file.loc > 0 && isAuditSourceFile(file));
|
|
32
32
|
const docDensity = codeFiles.length > 0 ? docFiles.length / codeFiles.length : 0;
|
|
33
33
|
const undocumentedHotspots = codeFiles
|
|
34
34
|
.filter((file) => file.loc >= 320)
|
|
@@ -42,7 +42,7 @@ function buildDocumentationInventory(ingest = {}) {
|
|
|
42
42
|
path: file.path,
|
|
43
43
|
loc: file.loc,
|
|
44
44
|
language: file.language,
|
|
45
|
-
severity:
|
|
45
|
+
severity: "P2",
|
|
46
46
|
}));
|
|
47
47
|
|
|
48
48
|
return {
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
2
|
+
".c",
|
|
3
|
+
".cc",
|
|
4
|
+
".cpp",
|
|
5
|
+
".cs",
|
|
6
|
+
".css",
|
|
7
|
+
".go",
|
|
8
|
+
".graphql",
|
|
9
|
+
".h",
|
|
10
|
+
".hpp",
|
|
11
|
+
".java",
|
|
12
|
+
".js",
|
|
13
|
+
".jsx",
|
|
14
|
+
".kt",
|
|
15
|
+
".mjs",
|
|
16
|
+
".php",
|
|
17
|
+
".py",
|
|
18
|
+
".rb",
|
|
19
|
+
".rs",
|
|
20
|
+
".scss",
|
|
21
|
+
".sql",
|
|
22
|
+
".svelte",
|
|
23
|
+
".swift",
|
|
24
|
+
".tf",
|
|
25
|
+
".ts",
|
|
26
|
+
".tsx",
|
|
27
|
+
".vue",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const SOURCE_LANGUAGES = new Set([
|
|
31
|
+
"c",
|
|
32
|
+
"c++",
|
|
33
|
+
"c#",
|
|
34
|
+
"css",
|
|
35
|
+
"dockerfile",
|
|
36
|
+
"go",
|
|
37
|
+
"graphql",
|
|
38
|
+
"java",
|
|
39
|
+
"javascript",
|
|
40
|
+
"jsx",
|
|
41
|
+
"kotlin",
|
|
42
|
+
"php",
|
|
43
|
+
"python",
|
|
44
|
+
"ruby",
|
|
45
|
+
"rust",
|
|
46
|
+
"scss",
|
|
47
|
+
"sql",
|
|
48
|
+
"svelte",
|
|
49
|
+
"swift",
|
|
50
|
+
"terraform",
|
|
51
|
+
"typescript",
|
|
52
|
+
"tsx",
|
|
53
|
+
"vue",
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const LOCKFILE_NAMES = new Set([
|
|
57
|
+
"cargo.lock",
|
|
58
|
+
"composer.lock",
|
|
59
|
+
"go.sum",
|
|
60
|
+
"package-lock.json",
|
|
61
|
+
"pipfile.lock",
|
|
62
|
+
"pnpm-lock.yaml",
|
|
63
|
+
"poetry.lock",
|
|
64
|
+
"yarn.lock",
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
function normalizePath(value) {
|
|
68
|
+
return String(value || "").replace(/\\/g, "/").replace(/^\.\/+/, "").toLowerCase();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function basename(filePath) {
|
|
72
|
+
const normalized = normalizePath(filePath);
|
|
73
|
+
return normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function extension(filePath) {
|
|
77
|
+
const name = basename(filePath);
|
|
78
|
+
const dot = name.lastIndexOf(".");
|
|
79
|
+
return dot > 0 ? name.slice(dot) : "";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function isAuditTestPath(filePath) {
|
|
83
|
+
const normalized = normalizePath(filePath);
|
|
84
|
+
return /(^|\/)(__tests__|tests?|specs?)(\/|$)|\.(test|spec)\.[^.\/]+$/i.test(normalized);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isAuditSupportPath(filePath) {
|
|
88
|
+
const normalized = normalizePath(filePath);
|
|
89
|
+
if (!normalized) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (LOCKFILE_NAMES.has(basename(normalized))) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
if (/(^|\/)(node_modules|vendor|fixtures?|__fixtures__|mocks?|__mocks__|snapshots?|__snapshots__)(\/|$)/i.test(normalized)) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
if (/(^|\/)(generated|__generated__|auto-generated|coverage|dist|build|out|\.next|\.nuxt|\.turbo|\.cache)(\/|$)/i.test(normalized)) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
if (/^(docs?|guides?|adr|tasks?|\.github|\.vscode|\.husky|public)(\/|$)/i.test(normalized)) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
if (/(\.min|\.bundle|generated)\.[^.\/]+$/i.test(normalized)) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function isAuditSourceFile(file = {}) {
|
|
111
|
+
const filePath = typeof file === "string" ? file : file.path;
|
|
112
|
+
const normalized = normalizePath(filePath);
|
|
113
|
+
if (!normalized || isAuditTestPath(normalized) || isAuditSupportPath(normalized)) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
if (SOURCE_EXTENSIONS.has(extension(normalized))) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
return SOURCE_LANGUAGES.has(String(file.language || "").trim().toLowerCase());
|
|
120
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isAuditSourceFile } from "./path-classification.js";
|
|
2
|
+
|
|
1
3
|
function normalizeString(value) {
|
|
2
4
|
return String(value || "").trim();
|
|
3
5
|
}
|
|
@@ -21,6 +23,7 @@ function summarizeSeverity(findings = []) {
|
|
|
21
23
|
function buildRuntimeHotspots(ingest = {}) {
|
|
22
24
|
const indexed = Array.isArray(ingest.indexedFiles?.files) ? ingest.indexedFiles.files : [];
|
|
23
25
|
return indexed
|
|
26
|
+
.filter((file) => isAuditSourceFile(file))
|
|
24
27
|
.filter((file) => Number(file.loc || 0) >= 280)
|
|
25
28
|
.sort((left, right) => Number(right.loc || 0) - Number(left.loc || 0))
|
|
26
29
|
.slice(0, 20)
|
|
@@ -28,7 +31,7 @@ function buildRuntimeHotspots(ingest = {}) {
|
|
|
28
31
|
path: toPosixPath(file.path),
|
|
29
32
|
loc: Number(file.loc || 0),
|
|
30
33
|
language: file.language,
|
|
31
|
-
severity:
|
|
34
|
+
severity: "P2",
|
|
32
35
|
}));
|
|
33
36
|
}
|
|
34
37
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isAuditSourceFile, isAuditTestPath } from "./path-classification.js";
|
|
2
|
+
|
|
1
3
|
function normalizeString(value) {
|
|
2
4
|
return String(value || "").trim();
|
|
3
5
|
}
|
|
@@ -18,10 +20,6 @@ function summarizeSeverity(findings = []) {
|
|
|
18
20
|
return summary;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
function isTestPath(filePath) {
|
|
22
|
-
return /(^|\/)(__tests__|tests?|spec)(\/|$)|\.(test|spec)\.[^.]+$/i.test(filePath);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
23
|
function buildCoverageInventory(ingest = {}) {
|
|
26
24
|
const indexedFiles = Array.isArray(ingest.indexedFiles?.files) ? ingest.indexedFiles.files : [];
|
|
27
25
|
const normalized = indexedFiles.map((file) => ({
|
|
@@ -30,8 +28,8 @@ function buildCoverageInventory(ingest = {}) {
|
|
|
30
28
|
loc: Number(file.loc || 0),
|
|
31
29
|
}));
|
|
32
30
|
|
|
33
|
-
const testFiles = normalized.filter((file) =>
|
|
34
|
-
const nonTestCodeFiles = normalized.filter((file) =>
|
|
31
|
+
const testFiles = normalized.filter((file) => isAuditTestPath(file.path));
|
|
32
|
+
const nonTestCodeFiles = normalized.filter((file) => isAuditSourceFile(file) && file.loc > 0);
|
|
35
33
|
const ratio = nonTestCodeFiles.length > 0 ? testFiles.length / nonTestCodeFiles.length : 0;
|
|
36
34
|
const likelyGaps = nonTestCodeFiles
|
|
37
35
|
.filter((file) => file.loc >= 260)
|
|
@@ -45,7 +43,7 @@ function buildCoverageInventory(ingest = {}) {
|
|
|
45
43
|
path: file.path,
|
|
46
44
|
loc: file.loc,
|
|
47
45
|
language: file.language,
|
|
48
|
-
severity:
|
|
46
|
+
severity: "P2",
|
|
49
47
|
}));
|
|
50
48
|
|
|
51
49
|
return {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function resolveActionOptions(options = {}, command = null) {
|
|
2
|
+
const localOptions = options && typeof options.opts === "function" ? options.opts() : { ...options };
|
|
3
|
+
const parent = command?.parent;
|
|
4
|
+
if (!parent || typeof parent.opts !== "function") {
|
|
5
|
+
return localOptions;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const parentOptions = parent.opts();
|
|
9
|
+
const merged = {
|
|
10
|
+
...parentOptions,
|
|
11
|
+
...localOptions,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
for (const [key, value] of Object.entries(parentOptions)) {
|
|
15
|
+
const parentSource =
|
|
16
|
+
typeof parent.getOptionValueSource === "function" ? parent.getOptionValueSource(key) : null;
|
|
17
|
+
const localSource =
|
|
18
|
+
typeof command?.getOptionValueSource === "function" ? command.getOptionValueSource(key) : null;
|
|
19
|
+
const parentWasProvided = parentSource && parentSource !== "default";
|
|
20
|
+
const localIsOnlyDefault = localSource === "default" || localOptions[key] === undefined;
|
|
21
|
+
if (parentWasProvided && localIsOnlyDefault) {
|
|
22
|
+
merged[key] = value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return merged;
|
|
27
|
+
}
|
package/src/commands/audit.js
CHANGED
|
@@ -10,6 +10,7 @@ import { loadAuditRegistry, selectAuditAgents } from "../audit/registry.js";
|
|
|
10
10
|
import { resolveOutputRoot } from "../config/service.js";
|
|
11
11
|
import { createAgentEvent } from "../events/schema.js";
|
|
12
12
|
import { createAuditSessionReporter, resolveAuditSessionId } from "../session/audit-reporter.js";
|
|
13
|
+
import { resolveActionOptions } from "./action-options.js";
|
|
13
14
|
import { buildLegacyArgs } from "./legacy-args.js";
|
|
14
15
|
|
|
15
16
|
function shouldEmitJson(options, command) {
|
|
@@ -213,16 +214,17 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
213
214
|
.option("--output-dir <path>", "Optional artifact output root override")
|
|
214
215
|
.option("--json", "Emit machine-readable output")
|
|
215
216
|
.action(async (options, command) => {
|
|
216
|
-
const
|
|
217
|
-
const
|
|
217
|
+
const actionOptions = resolveActionOptions(options, command);
|
|
218
|
+
const emitJson = shouldEmitJson(actionOptions, command);
|
|
219
|
+
const targetPath = path.resolve(process.cwd(), String(actionOptions.path || "."));
|
|
218
220
|
const outputRoot = await resolveOutputRoot({
|
|
219
221
|
cwd: targetPath,
|
|
220
|
-
outputDirOverride:
|
|
222
|
+
outputDirOverride: actionOptions.outputDir,
|
|
221
223
|
env: process.env,
|
|
222
224
|
});
|
|
223
225
|
const runDirectory = await resolveAuditRunDirectory({
|
|
224
226
|
outputRoot,
|
|
225
|
-
runId:
|
|
227
|
+
runId: actionOptions.runId,
|
|
226
228
|
});
|
|
227
229
|
const { report } = await loadAuditRunReport(runDirectory);
|
|
228
230
|
const ddPackage = await writeDdPackage({
|
|
@@ -264,11 +266,12 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
264
266
|
.option("--refresh", "Refresh CODEBASE_INGEST before replay")
|
|
265
267
|
.option("--json", "Emit machine-readable output")
|
|
266
268
|
.action(async (runId, options, command) => {
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
+
const actionOptions = resolveActionOptions(options, command);
|
|
270
|
+
const emitJson = shouldEmitJson(actionOptions, command);
|
|
271
|
+
const initialTargetPath = path.resolve(process.cwd(), String(actionOptions.path || "."));
|
|
269
272
|
const outputRoot = await resolveOutputRoot({
|
|
270
273
|
cwd: initialTargetPath,
|
|
271
|
-
outputDirOverride:
|
|
274
|
+
outputDirOverride: actionOptions.outputDir,
|
|
272
275
|
env: process.env,
|
|
273
276
|
});
|
|
274
277
|
const baseRunDirectory = await resolveAuditRunDirectory({
|
|
@@ -276,12 +279,12 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
276
279
|
runId,
|
|
277
280
|
});
|
|
278
281
|
const { report: baseReport } = await loadAuditRunReport(baseRunDirectory);
|
|
279
|
-
const targetPath =
|
|
280
|
-
? path.resolve(process.cwd(), String(
|
|
282
|
+
const targetPath = actionOptions.path
|
|
283
|
+
? path.resolve(process.cwd(), String(actionOptions.path || "."))
|
|
281
284
|
: path.resolve(String(baseReport.targetPath || "."));
|
|
282
285
|
|
|
283
286
|
const registry = await loadAuditRegistry({
|
|
284
|
-
registryFile:
|
|
287
|
+
registryFile: actionOptions.registryFile,
|
|
285
288
|
});
|
|
286
289
|
const selected = selectAuditAgents(registry.agents, (baseReport.selectedAgents || []).join(","));
|
|
287
290
|
if (selected.selected.length === 0) {
|
|
@@ -292,9 +295,9 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
292
295
|
targetPath,
|
|
293
296
|
agents: selected.selected,
|
|
294
297
|
maxParallel: Number(baseReport.maxParallel || 1),
|
|
295
|
-
outputDir:
|
|
298
|
+
outputDir: actionOptions.outputDir,
|
|
296
299
|
dryRun: Boolean(baseReport.dryRun),
|
|
297
|
-
refreshIngest: Boolean(
|
|
300
|
+
refreshIngest: Boolean(actionOptions.refresh),
|
|
298
301
|
isolation: baseReport.isolation || "strict",
|
|
299
302
|
seedFromDeterministic: baseReport.seedFromDeterministic !== false,
|
|
300
303
|
});
|
|
@@ -341,11 +344,12 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
341
344
|
.option("--output-dir <path>", "Optional artifact output root override")
|
|
342
345
|
.option("--json", "Emit machine-readable output")
|
|
343
346
|
.action(async (baseRunId, candidateRunId, options, command) => {
|
|
344
|
-
const
|
|
345
|
-
const
|
|
347
|
+
const actionOptions = resolveActionOptions(options, command);
|
|
348
|
+
const emitJson = shouldEmitJson(actionOptions, command);
|
|
349
|
+
const targetPath = path.resolve(process.cwd(), String(actionOptions.path || "."));
|
|
346
350
|
const outputRoot = await resolveOutputRoot({
|
|
347
351
|
cwd: targetPath,
|
|
348
|
-
outputDirOverride:
|
|
352
|
+
outputDirOverride: actionOptions.outputDir,
|
|
349
353
|
env: process.env,
|
|
350
354
|
});
|
|
351
355
|
const baseRunDirectory = await resolveAuditRunDirectory({
|
|
@@ -388,9 +392,10 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
388
392
|
.option("--registry-file <path>", "Optional custom audit registry file")
|
|
389
393
|
.option("--json", "Emit machine-readable output")
|
|
390
394
|
.action(async (options, command) => {
|
|
391
|
-
const
|
|
395
|
+
const actionOptions = resolveActionOptions(options, command);
|
|
396
|
+
const emitJson = shouldEmitJson(actionOptions, command);
|
|
392
397
|
const registry = await loadAuditRegistry({
|
|
393
|
-
registryFile:
|
|
398
|
+
registryFile: actionOptions.registryFile,
|
|
394
399
|
});
|
|
395
400
|
const payload = {
|
|
396
401
|
command: "audit registry",
|
|
@@ -422,10 +427,11 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
422
427
|
.option("--dry-run", "Skip deterministic baseline and run security planning only")
|
|
423
428
|
.option("--json", "Emit machine-readable output")
|
|
424
429
|
.action(async (options, command) => {
|
|
425
|
-
const
|
|
426
|
-
const
|
|
430
|
+
const actionOptions = resolveActionOptions(options, command);
|
|
431
|
+
const emitJson = shouldEmitJson(actionOptions, command);
|
|
432
|
+
const targetPath = path.resolve(process.cwd(), String(actionOptions.path || "."));
|
|
427
433
|
const registry = await loadAuditRegistry({
|
|
428
|
-
registryFile:
|
|
434
|
+
registryFile: actionOptions.registryFile,
|
|
429
435
|
});
|
|
430
436
|
const selected = selectAuditAgents(registry.agents, "security");
|
|
431
437
|
if (selected.selected.length !== 1) {
|
|
@@ -436,9 +442,9 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
436
442
|
targetPath,
|
|
437
443
|
agents: selected.selected,
|
|
438
444
|
maxParallel: 1,
|
|
439
|
-
outputDir:
|
|
440
|
-
dryRun: Boolean(
|
|
441
|
-
refreshIngest: Boolean(
|
|
445
|
+
outputDir: actionOptions.outputDir,
|
|
446
|
+
dryRun: Boolean(actionOptions.dryRun),
|
|
447
|
+
refreshIngest: Boolean(actionOptions.refresh),
|
|
442
448
|
});
|
|
443
449
|
const securityAgent = result.agentResults.find((agent) => agent.agentId === "security") || null;
|
|
444
450
|
|
|
@@ -486,10 +492,11 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
486
492
|
.option("--dry-run", "Skip deterministic baseline and run architecture planning only")
|
|
487
493
|
.option("--json", "Emit machine-readable output")
|
|
488
494
|
.action(async (options, command) => {
|
|
489
|
-
const
|
|
490
|
-
const
|
|
495
|
+
const actionOptions = resolveActionOptions(options, command);
|
|
496
|
+
const emitJson = shouldEmitJson(actionOptions, command);
|
|
497
|
+
const targetPath = path.resolve(process.cwd(), String(actionOptions.path || "."));
|
|
491
498
|
const registry = await loadAuditRegistry({
|
|
492
|
-
registryFile:
|
|
499
|
+
registryFile: actionOptions.registryFile,
|
|
493
500
|
});
|
|
494
501
|
const selected = selectAuditAgents(registry.agents, "architecture");
|
|
495
502
|
if (selected.selected.length !== 1) {
|
|
@@ -500,9 +507,9 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
500
507
|
targetPath,
|
|
501
508
|
agents: selected.selected,
|
|
502
509
|
maxParallel: 1,
|
|
503
|
-
outputDir:
|
|
504
|
-
dryRun: Boolean(
|
|
505
|
-
refreshIngest: Boolean(
|
|
510
|
+
outputDir: actionOptions.outputDir,
|
|
511
|
+
dryRun: Boolean(actionOptions.dryRun),
|
|
512
|
+
refreshIngest: Boolean(actionOptions.refresh),
|
|
506
513
|
});
|
|
507
514
|
const architectureAgent =
|
|
508
515
|
result.agentResults.find((agent) => agent.agentId === "architecture") || null;
|
|
@@ -551,10 +558,11 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
551
558
|
.option("--dry-run", "Skip deterministic baseline and run testing planning only")
|
|
552
559
|
.option("--json", "Emit machine-readable output")
|
|
553
560
|
.action(async (options, command) => {
|
|
554
|
-
const
|
|
555
|
-
const
|
|
561
|
+
const actionOptions = resolveActionOptions(options, command);
|
|
562
|
+
const emitJson = shouldEmitJson(actionOptions, command);
|
|
563
|
+
const targetPath = path.resolve(process.cwd(), String(actionOptions.path || "."));
|
|
556
564
|
const registry = await loadAuditRegistry({
|
|
557
|
-
registryFile:
|
|
565
|
+
registryFile: actionOptions.registryFile,
|
|
558
566
|
});
|
|
559
567
|
const selected = selectAuditAgents(registry.agents, "testing");
|
|
560
568
|
if (selected.selected.length !== 1) {
|
|
@@ -565,9 +573,9 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
565
573
|
targetPath,
|
|
566
574
|
agents: selected.selected,
|
|
567
575
|
maxParallel: 1,
|
|
568
|
-
outputDir:
|
|
569
|
-
dryRun: Boolean(
|
|
570
|
-
refreshIngest: Boolean(
|
|
576
|
+
outputDir: actionOptions.outputDir,
|
|
577
|
+
dryRun: Boolean(actionOptions.dryRun),
|
|
578
|
+
refreshIngest: Boolean(actionOptions.refresh),
|
|
571
579
|
});
|
|
572
580
|
const testingAgent = result.agentResults.find((agent) => agent.agentId === "testing") || null;
|
|
573
581
|
|
|
@@ -615,10 +623,11 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
615
623
|
.option("--dry-run", "Skip deterministic baseline and run performance planning only")
|
|
616
624
|
.option("--json", "Emit machine-readable output")
|
|
617
625
|
.action(async (options, command) => {
|
|
618
|
-
const
|
|
619
|
-
const
|
|
626
|
+
const actionOptions = resolveActionOptions(options, command);
|
|
627
|
+
const emitJson = shouldEmitJson(actionOptions, command);
|
|
628
|
+
const targetPath = path.resolve(process.cwd(), String(actionOptions.path || "."));
|
|
620
629
|
const registry = await loadAuditRegistry({
|
|
621
|
-
registryFile:
|
|
630
|
+
registryFile: actionOptions.registryFile,
|
|
622
631
|
});
|
|
623
632
|
const selected = selectAuditAgents(registry.agents, "performance");
|
|
624
633
|
if (selected.selected.length !== 1) {
|
|
@@ -629,9 +638,9 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
629
638
|
targetPath,
|
|
630
639
|
agents: selected.selected,
|
|
631
640
|
maxParallel: 1,
|
|
632
|
-
outputDir:
|
|
633
|
-
dryRun: Boolean(
|
|
634
|
-
refreshIngest: Boolean(
|
|
641
|
+
outputDir: actionOptions.outputDir,
|
|
642
|
+
dryRun: Boolean(actionOptions.dryRun),
|
|
643
|
+
refreshIngest: Boolean(actionOptions.refresh),
|
|
635
644
|
});
|
|
636
645
|
const performanceAgent =
|
|
637
646
|
result.agentResults.find((agent) => agent.agentId === "performance") || null;
|
|
@@ -680,10 +689,11 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
680
689
|
.option("--dry-run", "Skip deterministic baseline and run compliance planning only")
|
|
681
690
|
.option("--json", "Emit machine-readable output")
|
|
682
691
|
.action(async (options, command) => {
|
|
683
|
-
const
|
|
684
|
-
const
|
|
692
|
+
const actionOptions = resolveActionOptions(options, command);
|
|
693
|
+
const emitJson = shouldEmitJson(actionOptions, command);
|
|
694
|
+
const targetPath = path.resolve(process.cwd(), String(actionOptions.path || "."));
|
|
685
695
|
const registry = await loadAuditRegistry({
|
|
686
|
-
registryFile:
|
|
696
|
+
registryFile: actionOptions.registryFile,
|
|
687
697
|
});
|
|
688
698
|
const selected = selectAuditAgents(registry.agents, "compliance");
|
|
689
699
|
if (selected.selected.length !== 1) {
|
|
@@ -694,9 +704,9 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
694
704
|
targetPath,
|
|
695
705
|
agents: selected.selected,
|
|
696
706
|
maxParallel: 1,
|
|
697
|
-
outputDir:
|
|
698
|
-
dryRun: Boolean(
|
|
699
|
-
refreshIngest: Boolean(
|
|
707
|
+
outputDir: actionOptions.outputDir,
|
|
708
|
+
dryRun: Boolean(actionOptions.dryRun),
|
|
709
|
+
refreshIngest: Boolean(actionOptions.refresh),
|
|
700
710
|
});
|
|
701
711
|
const complianceAgent =
|
|
702
712
|
result.agentResults.find((agent) => agent.agentId === "compliance") || null;
|
|
@@ -745,10 +755,11 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
745
755
|
.option("--dry-run", "Skip deterministic baseline and run documentation planning only")
|
|
746
756
|
.option("--json", "Emit machine-readable output")
|
|
747
757
|
.action(async (options, command) => {
|
|
748
|
-
const
|
|
749
|
-
const
|
|
758
|
+
const actionOptions = resolveActionOptions(options, command);
|
|
759
|
+
const emitJson = shouldEmitJson(actionOptions, command);
|
|
760
|
+
const targetPath = path.resolve(process.cwd(), String(actionOptions.path || "."));
|
|
750
761
|
const registry = await loadAuditRegistry({
|
|
751
|
-
registryFile:
|
|
762
|
+
registryFile: actionOptions.registryFile,
|
|
752
763
|
});
|
|
753
764
|
const selected = selectAuditAgents(registry.agents, "documentation");
|
|
754
765
|
if (selected.selected.length !== 1) {
|
|
@@ -759,9 +770,9 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
759
770
|
targetPath,
|
|
760
771
|
agents: selected.selected,
|
|
761
772
|
maxParallel: 1,
|
|
762
|
-
outputDir:
|
|
763
|
-
dryRun: Boolean(
|
|
764
|
-
refreshIngest: Boolean(
|
|
773
|
+
outputDir: actionOptions.outputDir,
|
|
774
|
+
dryRun: Boolean(actionOptions.dryRun),
|
|
775
|
+
refreshIngest: Boolean(actionOptions.refresh),
|
|
765
776
|
});
|
|
766
777
|
const documentationAgent =
|
|
767
778
|
result.agentResults.find((agent) => agent.agentId === "documentation") || null;
|
|
@@ -808,8 +819,9 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
808
819
|
.option("--reuse-omargate <runId>", "Reuse deterministic findings from an OmarGate run id or latest")
|
|
809
820
|
.option("--json", "Emit machine-readable output")
|
|
810
821
|
.action(async (options, command) => {
|
|
822
|
+
const actionOptions = resolveActionOptions(options, command);
|
|
811
823
|
const legacyArgs = buildLegacyArgs(["/audit"], {
|
|
812
|
-
commandOptions:
|
|
824
|
+
commandOptions: actionOptions,
|
|
813
825
|
command,
|
|
814
826
|
});
|
|
815
827
|
await invokeLegacy(legacyArgs);
|
|
@@ -848,9 +860,10 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
848
860
|
.option("--output-dir <path>", "Artifact output root override")
|
|
849
861
|
.option("--json", "Emit machine-readable final output")
|
|
850
862
|
.action(async (options, command) => {
|
|
851
|
-
const
|
|
852
|
-
const
|
|
853
|
-
const
|
|
863
|
+
const actionOptions = resolveActionOptions(options, command);
|
|
864
|
+
const emitJson = shouldEmitJson(actionOptions, command);
|
|
865
|
+
const emitStream = Boolean(actionOptions.stream);
|
|
866
|
+
const targetPath = path.resolve(process.cwd(), String(actionOptions.path || "."));
|
|
854
867
|
|
|
855
868
|
// Lazy-load modules
|
|
856
869
|
const { julesAuditLoop } = await import("../agents/jules/loop.js");
|
|
@@ -861,7 +874,7 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
861
874
|
const { runDeterministicReviewPipeline } = await import("../review/local-review.js");
|
|
862
875
|
const fsp = await import("node:fs/promises");
|
|
863
876
|
|
|
864
|
-
const outputRoot = await resolveOut({ targetPath, outputDir:
|
|
877
|
+
const outputRoot = await resolveOut({ targetPath, outputDir: actionOptions.outputDir });
|
|
865
878
|
const artifactDir = path.join(outputRoot, "reports", "jules-" + Date.now());
|
|
866
879
|
await fsp.mkdir(artifactDir, { recursive: true });
|
|
867
880
|
|
|
@@ -876,7 +889,7 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
876
889
|
try {
|
|
877
890
|
ingest = await generateCodebaseIngest({
|
|
878
891
|
rootPath: targetPath, outputDir: outputRoot,
|
|
879
|
-
refresh: Boolean(
|
|
892
|
+
refresh: Boolean(actionOptions.refresh),
|
|
880
893
|
});
|
|
881
894
|
} catch {
|
|
882
895
|
ingest = await collectCodebaseIngest({ rootPath: targetPath });
|
|
@@ -887,7 +900,7 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
887
900
|
|
|
888
901
|
// ── [2] OMAR BASELINE (blind — Jules won't see until reconciliation) ──
|
|
889
902
|
let omarBaseline = null;
|
|
890
|
-
if (!
|
|
903
|
+
if (!actionOptions.skipBaseline) {
|
|
891
904
|
emitProgress(onEvent, JULES_DEFINITION, "Running Omar 7-layer deterministic baseline...");
|
|
892
905
|
try {
|
|
893
906
|
omarBaseline = await runDeterministicReviewPipeline({
|
|
@@ -925,14 +938,14 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
925
938
|
|
|
926
939
|
// ── [5] PROVIDER ──────────────────────────────────────────────
|
|
927
940
|
const providerConfig = {};
|
|
928
|
-
if (
|
|
929
|
-
if (
|
|
930
|
-
if (
|
|
941
|
+
if (actionOptions.provider) providerConfig.provider = actionOptions.provider;
|
|
942
|
+
if (actionOptions.model) providerConfig.model = actionOptions.model;
|
|
943
|
+
if (actionOptions.apiKey) providerConfig.apiKey = actionOptions.apiKey;
|
|
931
944
|
|
|
932
945
|
// ── [6] SYSTEM PROMPT (full production prompt) ─────────────────
|
|
933
946
|
const { buildJulesProductionPrompt } = await import("../agents/jules/config/system-prompt.js");
|
|
934
947
|
const systemPrompt = buildJulesProductionPrompt({
|
|
935
|
-
mode:
|
|
948
|
+
mode: actionOptions.mode,
|
|
936
949
|
framework: ingest?.frameworks?.[0] || "unknown",
|
|
937
950
|
componentCount: ingest?.indexedFiles?.files?.filter(
|
|
938
951
|
f => /\.(tsx|jsx|vue|svelte)$/.test(f.path || ""),
|
|
@@ -952,14 +965,14 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
952
965
|
blackboard,
|
|
953
966
|
memory: memoryIndex,
|
|
954
967
|
budget: {
|
|
955
|
-
maxCostUsd: parseFloat(
|
|
968
|
+
maxCostUsd: parseFloat(actionOptions.maxCost) || 5.0,
|
|
956
969
|
maxOutputTokens: JULES_DEFINITION.budget.maxOutputTokens,
|
|
957
970
|
maxRuntimeMs: JULES_DEFINITION.budget.maxRuntimeMs,
|
|
958
971
|
maxToolCalls: JULES_DEFINITION.budget.maxToolCalls,
|
|
959
972
|
},
|
|
960
973
|
provider: providerConfig,
|
|
961
|
-
mode:
|
|
962
|
-
maxTurns: parseInt(
|
|
974
|
+
mode: actionOptions.mode,
|
|
975
|
+
maxTurns: parseInt(actionOptions.maxTurns) || 25,
|
|
963
976
|
onEvent,
|
|
964
977
|
});
|
|
965
978
|
|
|
@@ -973,8 +986,8 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
973
986
|
|
|
974
987
|
// ── [7.5] RUNTIME AUDIT (if --url provided or URL detected) ────
|
|
975
988
|
let runtimeResult = null;
|
|
976
|
-
const runtimeUrl =
|
|
977
|
-
if (runtimeUrl || !
|
|
989
|
+
const runtimeUrl = actionOptions.url || null;
|
|
990
|
+
if (runtimeUrl || !actionOptions.skipRuntime) {
|
|
978
991
|
emitProgress(onEvent, JULES_DEFINITION, "Checking for deployed URL...");
|
|
979
992
|
try {
|
|
980
993
|
const { runtimeAudit: runRT } = await import("../agents/jules/tools/runtime-audit.js");
|
|
@@ -1057,7 +1070,7 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
1057
1070
|
command: "audit frontend",
|
|
1058
1071
|
persona: JULES_DEFINITION.persona,
|
|
1059
1072
|
targetPath,
|
|
1060
|
-
mode:
|
|
1073
|
+
mode: actionOptions.mode,
|
|
1061
1074
|
framework: ingest?.frameworks?.[0] || "unknown",
|
|
1062
1075
|
findings: allFindings,
|
|
1063
1076
|
summary: { total: allFindings.length, ...severityCounts, blocking: severityCounts.P0 > 0 || severityCounts.P1 > 0 },
|
|
@@ -1109,7 +1122,7 @@ export function registerAuditCommand(program, invokeLegacy) {
|
|
|
1109
1122
|
const syncResult = await syncRunToDashboard({
|
|
1110
1123
|
command: "audit frontend",
|
|
1111
1124
|
persona: JULES_DEFINITION.persona,
|
|
1112
|
-
mode:
|
|
1125
|
+
mode: actionOptions.mode,
|
|
1113
1126
|
framework: ingest?.frameworks?.[0] || "unknown",
|
|
1114
1127
|
summary: { total: allFindings.length, ...severityCounts, blocking: severityCounts.P0 > 0 || severityCounts.P1 > 0 },
|
|
1115
1128
|
usage: report?.usage || {},
|
package/src/commands/review.js
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
writeReviewComparisonArtifact,
|
|
26
26
|
writeReviewRunContext,
|
|
27
27
|
} from "../review/replay.js";
|
|
28
|
+
import { resolveActionOptions } from "./action-options.js";
|
|
28
29
|
|
|
29
30
|
function shouldEmitJson(options, command) {
|
|
30
31
|
const local = Boolean(options && options.json);
|
|
@@ -236,16 +237,17 @@ function registerVerdictCommand(review, verdict) {
|
|
|
236
237
|
.option("--actor <id>", "Operator identifier for audit trail", "")
|
|
237
238
|
.option("--json", "Emit machine-readable output")
|
|
238
239
|
.action(async (findingId, options, command) => {
|
|
239
|
-
const
|
|
240
|
-
const
|
|
240
|
+
const actionOptions = resolveActionOptions(options, command);
|
|
241
|
+
const emitJson = shouldEmitJson(actionOptions, command);
|
|
242
|
+
const targetPath = resolveTargetPath(".", actionOptions);
|
|
241
243
|
const result = await recordReviewDecision({
|
|
242
244
|
targetPath,
|
|
243
|
-
runId:
|
|
244
|
-
outputDir:
|
|
245
|
+
runId: actionOptions.runId,
|
|
246
|
+
outputDir: actionOptions.outputDir,
|
|
245
247
|
findingId,
|
|
246
248
|
verdict,
|
|
247
|
-
note:
|
|
248
|
-
actor:
|
|
249
|
+
note: actionOptions.note,
|
|
250
|
+
actor: actionOptions.actor,
|
|
249
251
|
env: process.env,
|
|
250
252
|
});
|
|
251
253
|
|
|
@@ -374,12 +376,13 @@ export function registerReviewCommand(program) {
|
|
|
374
376
|
.option("--output-dir <path>", "Optional artifact output root override")
|
|
375
377
|
.option("--json", "Emit machine-readable output")
|
|
376
378
|
.action(async (options, command) => {
|
|
377
|
-
const
|
|
378
|
-
const
|
|
379
|
+
const actionOptions = resolveActionOptions(options, command);
|
|
380
|
+
const emitJson = shouldEmitJson(actionOptions, command);
|
|
381
|
+
const targetPath = resolveTargetPath(".", actionOptions);
|
|
379
382
|
const loaded = await loadUnifiedReviewReport({
|
|
380
383
|
targetPath,
|
|
381
|
-
runId:
|
|
382
|
-
outputDir:
|
|
384
|
+
runId: actionOptions.runId,
|
|
385
|
+
outputDir: actionOptions.outputDir,
|
|
383
386
|
env: process.env,
|
|
384
387
|
});
|
|
385
388
|
|
|
@@ -412,14 +415,15 @@ export function registerReviewCommand(program) {
|
|
|
412
415
|
.option("--output-file <path>", "Optional custom export output path")
|
|
413
416
|
.option("--json", "Emit machine-readable output")
|
|
414
417
|
.action(async (options, command) => {
|
|
415
|
-
const
|
|
416
|
-
const
|
|
418
|
+
const actionOptions = resolveActionOptions(options, command);
|
|
419
|
+
const emitJson = shouldEmitJson(actionOptions, command);
|
|
420
|
+
const targetPath = resolveTargetPath(".", actionOptions);
|
|
417
421
|
const exported = await exportUnifiedReviewReport({
|
|
418
422
|
targetPath,
|
|
419
|
-
runId:
|
|
420
|
-
outputDir:
|
|
421
|
-
format:
|
|
422
|
-
outputFile:
|
|
423
|
+
runId: actionOptions.runId,
|
|
424
|
+
outputDir: actionOptions.outputDir,
|
|
425
|
+
format: actionOptions.format,
|
|
426
|
+
outputFile: actionOptions.outputFile,
|
|
423
427
|
env: process.env,
|
|
424
428
|
});
|
|
425
429
|
|
|
@@ -453,12 +457,13 @@ export function registerReviewCommand(program) {
|
|
|
453
457
|
.option("--ai-dry-run", "Force AI dry-run during replay")
|
|
454
458
|
.option("--json", "Emit machine-readable output")
|
|
455
459
|
.action(async (runId, options, command) => {
|
|
456
|
-
const
|
|
457
|
-
const
|
|
460
|
+
const actionOptions = resolveActionOptions(options, command);
|
|
461
|
+
const emitJson = shouldEmitJson(actionOptions, command);
|
|
462
|
+
const lookupPath = resolveTargetPath(".", actionOptions);
|
|
458
463
|
const source = await loadUnifiedReviewReport({
|
|
459
464
|
targetPath: lookupPath,
|
|
460
465
|
runId,
|
|
461
|
-
outputDir:
|
|
466
|
+
outputDir: actionOptions.outputDir,
|
|
462
467
|
env: process.env,
|
|
463
468
|
});
|
|
464
469
|
|
|
@@ -470,20 +475,20 @@ export function registerReviewCommand(program) {
|
|
|
470
475
|
}
|
|
471
476
|
|
|
472
477
|
const invocation = sourceContext?.context?.invocation || {};
|
|
473
|
-
const replayTargetPath =
|
|
474
|
-
? resolveTargetPath(".",
|
|
478
|
+
const replayTargetPath = actionOptions.path
|
|
479
|
+
? resolveTargetPath(".", actionOptions)
|
|
475
480
|
: path.resolve(sourceContext?.context?.targetPath || source.report.targetPath || lookupPath);
|
|
476
|
-
const aiEnabled =
|
|
481
|
+
const aiEnabled = actionOptions.noAi ? false : Boolean(invocation.aiEnabled);
|
|
477
482
|
|
|
478
483
|
const replayOutcome = await executeReviewRun({
|
|
479
484
|
targetPath: replayTargetPath,
|
|
480
485
|
mode: sourceContext?.context?.mode || source.report.mode || "full",
|
|
481
|
-
outputDir:
|
|
482
|
-
specFile:
|
|
483
|
-
refreshIngest: Boolean(
|
|
486
|
+
outputDir: actionOptions.outputDir || invocation.outputDir || "",
|
|
487
|
+
specFile: actionOptions.spec || invocation.specFile || "",
|
|
488
|
+
refreshIngest: Boolean(actionOptions.refresh || invocation.refreshIngest),
|
|
484
489
|
aiConfig: {
|
|
485
490
|
enable: aiEnabled,
|
|
486
|
-
aiDryRun: Boolean(
|
|
491
|
+
aiDryRun: Boolean(actionOptions.aiDryRun || invocation.aiDryRun),
|
|
487
492
|
provider: invocation.provider,
|
|
488
493
|
model: invocation.model,
|
|
489
494
|
sessionId: invocation.sessionId,
|
|
@@ -542,18 +547,19 @@ export function registerReviewCommand(program) {
|
|
|
542
547
|
.option("--output-file <path>", "Optional custom output path for comparison JSON")
|
|
543
548
|
.option("--json", "Emit machine-readable output")
|
|
544
549
|
.action(async (baseRunId, candidateRunId, options, command) => {
|
|
545
|
-
const
|
|
546
|
-
const
|
|
550
|
+
const actionOptions = resolveActionOptions(options, command);
|
|
551
|
+
const emitJson = shouldEmitJson(actionOptions, command);
|
|
552
|
+
const targetPath = resolveTargetPath(".", actionOptions);
|
|
547
553
|
const base = await loadUnifiedReviewReport({
|
|
548
554
|
targetPath,
|
|
549
555
|
runId: baseRunId,
|
|
550
|
-
outputDir:
|
|
556
|
+
outputDir: actionOptions.outputDir,
|
|
551
557
|
env: process.env,
|
|
552
558
|
});
|
|
553
559
|
const candidate = await loadUnifiedReviewReport({
|
|
554
560
|
targetPath,
|
|
555
561
|
runId: candidateRunId,
|
|
556
|
-
outputDir:
|
|
562
|
+
outputDir: actionOptions.outputDir,
|
|
557
563
|
env: process.env,
|
|
558
564
|
});
|
|
559
565
|
|
|
@@ -566,8 +572,8 @@ export function registerReviewCommand(program) {
|
|
|
566
572
|
});
|
|
567
573
|
|
|
568
574
|
let outputPath = artifact.artifactPath;
|
|
569
|
-
if (
|
|
570
|
-
outputPath = path.resolve(targetPath, String(
|
|
575
|
+
if (actionOptions.outputFile) {
|
|
576
|
+
outputPath = path.resolve(targetPath, String(actionOptions.outputFile));
|
|
571
577
|
await fsp.mkdir(path.dirname(outputPath), { recursive: true });
|
|
572
578
|
await fsp.writeFile(outputPath, `${JSON.stringify(artifact.payload, null, 2)}\n`, "utf-8");
|
|
573
579
|
}
|
|
@@ -611,13 +617,14 @@ export function registerReviewCommand(program) {
|
|
|
611
617
|
.option("--refresh", "Refresh CODEBASE_INGEST prior to compatibility scan")
|
|
612
618
|
.option("--json", "Emit machine-readable output")
|
|
613
619
|
.action(async (options, command) => {
|
|
614
|
-
const
|
|
615
|
-
const
|
|
620
|
+
const actionOptions = resolveActionOptions(options, command);
|
|
621
|
+
const emitJson = shouldEmitJson(actionOptions, command);
|
|
622
|
+
const targetPath = resolveTargetPath(".", actionOptions);
|
|
616
623
|
let ingestRefresh = null;
|
|
617
|
-
if (
|
|
624
|
+
if (actionOptions.refresh) {
|
|
618
625
|
const ingestResolution = await resolveCodebaseIngest({
|
|
619
626
|
rootPath: targetPath,
|
|
620
|
-
outputDir:
|
|
627
|
+
outputDir: actionOptions.outputDir,
|
|
621
628
|
refresh: true,
|
|
622
629
|
});
|
|
623
630
|
ingestRefresh = {
|
|
@@ -630,13 +637,13 @@ export function registerReviewCommand(program) {
|
|
|
630
637
|
contentHash: ingestResolution.fingerprint?.contentHash || "",
|
|
631
638
|
};
|
|
632
639
|
}
|
|
633
|
-
const mode = resolveModeFromOptions(
|
|
634
|
-
defaultMode: String(
|
|
640
|
+
const mode = resolveModeFromOptions(actionOptions, {
|
|
641
|
+
defaultMode: String(actionOptions.mode || "full").trim().toLowerCase() || "full",
|
|
635
642
|
});
|
|
636
643
|
const scan = await runLocalReviewScan({
|
|
637
644
|
targetPath,
|
|
638
645
|
mode,
|
|
639
|
-
specFile:
|
|
646
|
+
specFile: actionOptions.spec,
|
|
640
647
|
});
|
|
641
648
|
|
|
642
649
|
const report = `# Local Review Scan
|
|
@@ -660,7 +667,7 @@ ${formatFindingsMarkdown(scan.findings)}
|
|
|
660
667
|
const reportPath = await writeReviewReport({
|
|
661
668
|
targetPath: scan.targetPath,
|
|
662
669
|
mode: scan.mode,
|
|
663
|
-
outputDir:
|
|
670
|
+
outputDir: actionOptions.outputDir,
|
|
664
671
|
reportMarkdown: report,
|
|
665
672
|
});
|
|
666
673
|
|
package/src/commands/session.js
CHANGED
|
@@ -62,6 +62,11 @@ import {
|
|
|
62
62
|
updateSessionTitle,
|
|
63
63
|
} from "../session/store.js";
|
|
64
64
|
import { fetchSessionListeners, formatListenerLine } from "../session/listeners.js";
|
|
65
|
+
import {
|
|
66
|
+
acceptSessionInvitation,
|
|
67
|
+
normalizeSessionOnboarding,
|
|
68
|
+
writeSessionOnboardingBrief,
|
|
69
|
+
} from "../session/invitations.js";
|
|
65
70
|
import { postFirstSentiMessage } from "../session/first-message.js";
|
|
66
71
|
import { createListenerHostWake } from "../session/wake/listen-wake.js";
|
|
67
72
|
import { appendToStream, readStream, tailStream } from "../session/stream.js";
|
|
@@ -124,6 +129,13 @@ function normalizeString(value) {
|
|
|
124
129
|
return String(value || "").trim();
|
|
125
130
|
}
|
|
126
131
|
|
|
132
|
+
function optionWasSetByCli(command, optionName) {
|
|
133
|
+
if (!command || typeof command.getOptionValueSource !== "function") {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
return command.getOptionValueSource(optionName) === "cli";
|
|
137
|
+
}
|
|
138
|
+
|
|
127
139
|
const SESSION_SAY_CONFIRM_ATTEMPTS = 3;
|
|
128
140
|
const SESSION_SAY_CONFIRM_DELAY_MS = 250;
|
|
129
141
|
const SESSION_SAY_CONFIRM_PAGE_LIMIT = 200;
|
|
@@ -2719,6 +2731,9 @@ export function registerSessionCommand(program) {
|
|
|
2719
2731
|
"--agent <id>",
|
|
2720
2732
|
"Granted agent id to emit an agent_join event as. Behaves like post-agent for human/placeholder ids — those are recorded in the local registry only.",
|
|
2721
2733
|
)
|
|
2734
|
+
.option("--invite-token <token>", "Invitation token to accept before joining the session")
|
|
2735
|
+
.option("--seat-key <key>", "Reserved session seat key to claim while accepting an invite")
|
|
2736
|
+
.option("--idempotency-key <key>", "Explicit idempotency key for invite acceptance")
|
|
2722
2737
|
.option("--role <role>", "Agent role: coder, reviewer, tester, observer", "coder")
|
|
2723
2738
|
.option("--model <model>", "Agent model hint", "cli")
|
|
2724
2739
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
@@ -2729,12 +2744,35 @@ export function registerSessionCommand(program) {
|
|
|
2729
2744
|
throw new Error("session id is required.");
|
|
2730
2745
|
}
|
|
2731
2746
|
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
2747
|
+
const explicitAgent = normalizeString(options.agent);
|
|
2748
|
+
const legacyName = normalizeString(options.name);
|
|
2749
|
+
const inviteToken = normalizeString(options.inviteToken);
|
|
2750
|
+
let invitationAccept = null;
|
|
2751
|
+
let invitationAcceptResult = null;
|
|
2752
|
+
if (inviteToken) {
|
|
2753
|
+
invitationAccept = await acceptSessionInvitation(normalizedSessionId, {
|
|
2754
|
+
targetPath,
|
|
2755
|
+
invitationToken: inviteToken,
|
|
2756
|
+
seatKey: options.seatKey,
|
|
2757
|
+
agentId: explicitAgent || legacyName,
|
|
2758
|
+
idempotencyKey: options.idempotencyKey,
|
|
2759
|
+
});
|
|
2760
|
+
invitationAcceptResult =
|
|
2761
|
+
invitationAccept?.result && typeof invitationAccept.result === "object"
|
|
2762
|
+
? invitationAccept.result
|
|
2763
|
+
: null;
|
|
2764
|
+
}
|
|
2732
2765
|
|
|
2733
2766
|
// PR #483 contract: verify the session exists and the caller has access
|
|
2734
2767
|
// BEFORE materializing local cache state. Without this we'd silently
|
|
2735
2768
|
// create a phantom local NDJSON for a session that's archived or owned
|
|
2736
2769
|
// by another tenant — which is the bug Carter reported when asking for
|
|
2737
2770
|
// a clean "share an id from web → join in CLI" flow.
|
|
2771
|
+
//
|
|
2772
|
+
// Invitation acceptance is the only exception to the old ordering:
|
|
2773
|
+
// before the invite is accepted the user may legitimately receive 403,
|
|
2774
|
+
// so --invite-token performs the guarded accept mutation first and this
|
|
2775
|
+
// verification proves membership immediately afterward.
|
|
2738
2776
|
const verification = await verifyRemoteSession(normalizedSessionId, { targetPath });
|
|
2739
2777
|
if (!verification.ok) {
|
|
2740
2778
|
if (verification.status === 404 || verification.reason === "not_found") {
|
|
@@ -2766,17 +2804,26 @@ export function registerSessionCommand(program) {
|
|
|
2766
2804
|
targetPath,
|
|
2767
2805
|
});
|
|
2768
2806
|
|
|
2769
|
-
const
|
|
2770
|
-
|
|
2807
|
+
const acceptedOnboarding = normalizeSessionOnboarding(
|
|
2808
|
+
invitationAcceptResult?.onboarding,
|
|
2809
|
+
invitationAcceptResult?.claimedSeat,
|
|
2810
|
+
);
|
|
2811
|
+
const acceptedAgentId = normalizeString(acceptedOnboarding?.agentId);
|
|
2812
|
+
const agentSeed = explicitAgent || acceptedAgentId || legacyName;
|
|
2771
2813
|
const resolvedAgentId = await defaultAgentId(agentSeed, targetPath);
|
|
2772
|
-
const
|
|
2814
|
+
const roleWasExplicit = optionWasSetByCli(command, "role");
|
|
2815
|
+
const role =
|
|
2816
|
+
normalizeString(roleWasExplicit ? options.role : acceptedOnboarding?.role || options.role) ||
|
|
2817
|
+
"coder";
|
|
2773
2818
|
const model = normalizeString(options.model) || "cli";
|
|
2819
|
+
const hasConcreteAgentIdentity = Boolean(explicitAgent || acceptedAgentId);
|
|
2774
2820
|
|
|
2775
2821
|
// `registerAgent` already writes the canonical `agent_join` event to the
|
|
2776
2822
|
// local NDJSON and best-effort relays it to /events via appendToStream
|
|
2777
2823
|
// → syncSessionEventToApi. That gives us the exact `post-agent` parity
|
|
2778
|
-
// the spec calls for when `--agent <granted>`
|
|
2779
|
-
//
|
|
2824
|
+
// the spec calls for when `--agent <granted>` or an accepted reserved
|
|
2825
|
+
// seat provides a concrete agent id. We don't double-emit; we record
|
|
2826
|
+
// whether a durable agent identity path was used
|
|
2780
2827
|
// so the JSON output can advertise it to callers (and tests).
|
|
2781
2828
|
const joined = await registerAgent(normalizedSessionId, {
|
|
2782
2829
|
targetPath,
|
|
@@ -2784,15 +2831,21 @@ export function registerSessionCommand(program) {
|
|
|
2784
2831
|
model,
|
|
2785
2832
|
role,
|
|
2786
2833
|
trackProcessExit: false,
|
|
2787
|
-
awaitRemoteSync:
|
|
2834
|
+
awaitRemoteSync: hasConcreteAgentIdentity,
|
|
2788
2835
|
});
|
|
2789
2836
|
const agentJoinRelayed =
|
|
2790
2837
|
joined.emittedJoinEvent !== false &&
|
|
2791
|
-
|
|
2838
|
+
hasConcreteAgentIdentity &&
|
|
2792
2839
|
Boolean(resolvedAgentId) &&
|
|
2793
2840
|
resolvedAgentId !== "cli-user" &&
|
|
2794
2841
|
resolvedAgentId !== "unknown" &&
|
|
2795
2842
|
!resolvedAgentId.startsWith("human-");
|
|
2843
|
+
const onboardingBrief = await writeSessionOnboardingBrief(normalizedSessionId, {
|
|
2844
|
+
targetPath,
|
|
2845
|
+
onboarding: acceptedOnboarding,
|
|
2846
|
+
claimedSeat: invitationAcceptResult?.claimedSeat || null,
|
|
2847
|
+
agentId: joined.agentId,
|
|
2848
|
+
});
|
|
2796
2849
|
|
|
2797
2850
|
const eventCount = Number(remoteSession.eventCount ?? remoteSession.events ?? 0);
|
|
2798
2851
|
const agents = Array.isArray(remoteSession.agents) ? remoteSession.agents : [];
|
|
@@ -2824,6 +2877,21 @@ export function registerSessionCommand(program) {
|
|
|
2824
2877
|
agentCount: Number.isFinite(agentCount) ? agentCount : 0,
|
|
2825
2878
|
lastActivityAt: lastActivityIso || null,
|
|
2826
2879
|
agentJoinRelayed,
|
|
2880
|
+
invitationAccepted: Boolean(invitationAcceptResult?.ok || invitationAccept),
|
|
2881
|
+
invitationAccept: invitationAccept
|
|
2882
|
+
? {
|
|
2883
|
+
idempotencyKey: invitationAccept.idempotencyKey,
|
|
2884
|
+
claimedSeat: invitationAcceptResult?.claimedSeat || null,
|
|
2885
|
+
onboarding: acceptedOnboarding,
|
|
2886
|
+
capacity: invitationAcceptResult?.capacity || null,
|
|
2887
|
+
}
|
|
2888
|
+
: null,
|
|
2889
|
+
onboardingGuide: onboardingBrief
|
|
2890
|
+
? {
|
|
2891
|
+
markdownPath: onboardingBrief.markdownPath,
|
|
2892
|
+
jsonPath: onboardingBrief.jsonPath,
|
|
2893
|
+
}
|
|
2894
|
+
: null,
|
|
2827
2895
|
};
|
|
2828
2896
|
if (shouldEmitJson(options, command)) {
|
|
2829
2897
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -2836,7 +2904,17 @@ export function registerSessionCommand(program) {
|
|
|
2836
2904
|
`Joined session ${titleLabel} (${normalizedSessionId}) — ${payload.eventCount} events, ${payload.agentCount} agents, last activity ${ageLabel}`,
|
|
2837
2905
|
),
|
|
2838
2906
|
);
|
|
2907
|
+
if (payload.invitationAccepted) {
|
|
2908
|
+
const seat = payload.invitationAccept?.claimedSeat || {};
|
|
2909
|
+
const seatLabel = normalizeString(seat.seatKey)
|
|
2910
|
+
? `; claimed ${normalizeString(seat.seatType) || "reserved"} seat ${seat.seatKey}`
|
|
2911
|
+
: "";
|
|
2912
|
+
console.log(pc.green(`Invitation accepted${seatLabel}`));
|
|
2913
|
+
}
|
|
2839
2914
|
console.log(pc.gray(`agent=${joined.agentId} role=${joined.role} model=${joined.model}`));
|
|
2915
|
+
if (payload.onboardingGuide?.markdownPath) {
|
|
2916
|
+
console.log(pc.gray(`onboarding=${payload.onboardingGuide.markdownPath}`));
|
|
2917
|
+
}
|
|
2840
2918
|
});
|
|
2841
2919
|
|
|
2842
2920
|
session
|
package/src/coord/events-log.js
CHANGED
package/src/legacy-cli.js
CHANGED
|
@@ -2294,6 +2294,9 @@ ${renderCoordinationNumberedList()}
|
|
|
2294
2294
|
- Finding: \`sl session say <id> "finding: [P2] missing rate limit on POST /api/auth/login"\`
|
|
2295
2295
|
- Help: \`sl session say <id> "help: unexpected change in package.json - who modified it?"\`
|
|
2296
2296
|
- Done: \`sl session say <id> "done: PR merged, auth hardening complete"\`
|
|
2297
|
+
|
|
2298
|
+
## Reserved Seat Onboarding
|
|
2299
|
+
- If \`sl session join\` returns \`onboardingGuide.markdownPath\`, read that file before posting; it contains your reserved-seat SOUL instructions, role, and first actions.
|
|
2297
2300
|
`;
|
|
2298
2301
|
}
|
|
2299
2302
|
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { createHmac, randomUUID } from "node:crypto";
|
|
2
|
+
import fsp from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
|
|
6
|
+
import { requestJsonMutation } from "../auth/http.js";
|
|
7
|
+
import { resolveActiveAuthSession } from "../auth/service.js";
|
|
8
|
+
import { renderCoordinationBulletList } from "./coordination-guidance.js";
|
|
9
|
+
import { resolveSessionPaths } from "./paths.js";
|
|
10
|
+
|
|
11
|
+
export const SESSION_INVITATION_ACCEPT_ROUTE_ID =
|
|
12
|
+
"POST /api/v1/sessions/{session_id}/invitations/accept";
|
|
13
|
+
export const SESSION_MUTATION_ORIGIN = "https://sentinelayer.com";
|
|
14
|
+
|
|
15
|
+
function normalizeString(value) {
|
|
16
|
+
return String(value || "").trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function safeFileToken(value, fallback = "onboarding") {
|
|
20
|
+
const normalized = normalizeString(value)
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
23
|
+
.replace(/^-+|-+$/g, "");
|
|
24
|
+
return (normalized || fallback).slice(0, 80);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function canonicalSessionRef(value) {
|
|
28
|
+
const raw = normalizeString(value);
|
|
29
|
+
if (!raw) return "unavailable";
|
|
30
|
+
return raw.toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createSessionMutationCsrfToken({
|
|
34
|
+
bearerToken,
|
|
35
|
+
sessionId,
|
|
36
|
+
routeId,
|
|
37
|
+
idempotencyKey,
|
|
38
|
+
} = {}) {
|
|
39
|
+
const secret = normalizeString(bearerToken);
|
|
40
|
+
if (!secret) return "";
|
|
41
|
+
const message = [
|
|
42
|
+
"session-mutation-csrf:v1",
|
|
43
|
+
canonicalSessionRef(sessionId),
|
|
44
|
+
normalizeString(routeId),
|
|
45
|
+
normalizeString(idempotencyKey),
|
|
46
|
+
].join("\0");
|
|
47
|
+
return createHmac("sha256", secret).update(message, "utf-8").digest("hex");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createSessionMutationHeaders({
|
|
51
|
+
bearerToken,
|
|
52
|
+
sessionId,
|
|
53
|
+
routeId,
|
|
54
|
+
idempotencyKey,
|
|
55
|
+
origin = SESSION_MUTATION_ORIGIN,
|
|
56
|
+
} = {}) {
|
|
57
|
+
const token = normalizeString(bearerToken);
|
|
58
|
+
const normalizedOrigin = normalizeString(origin) || SESSION_MUTATION_ORIGIN;
|
|
59
|
+
return {
|
|
60
|
+
Authorization: `Bearer ${token}`,
|
|
61
|
+
Origin: normalizedOrigin,
|
|
62
|
+
"Sec-Fetch-Site": "same-site",
|
|
63
|
+
"X-Sentinelayer-Session-Mutation": "session-mutation",
|
|
64
|
+
"X-CSRF-Token": createSessionMutationCsrfToken({
|
|
65
|
+
bearerToken: token,
|
|
66
|
+
sessionId,
|
|
67
|
+
routeId,
|
|
68
|
+
idempotencyKey,
|
|
69
|
+
}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createSessionMutationIdempotencyKey(operationName = "invitation-accept") {
|
|
74
|
+
const operation = normalizeString(operationName)
|
|
75
|
+
.toLowerCase()
|
|
76
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
77
|
+
.replace(/^-+|-+$/g, "")
|
|
78
|
+
.slice(0, 32) || "session-mutation";
|
|
79
|
+
return `sl-cli-${operation}-${randomUUID()}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function acceptSessionInvitation(
|
|
83
|
+
sessionId,
|
|
84
|
+
{
|
|
85
|
+
invitationToken,
|
|
86
|
+
seatKey = "",
|
|
87
|
+
agentId = "",
|
|
88
|
+
targetPath = process.cwd(),
|
|
89
|
+
idempotencyKey = "",
|
|
90
|
+
origin = SESSION_MUTATION_ORIGIN,
|
|
91
|
+
resolveAuthSession = resolveActiveAuthSession,
|
|
92
|
+
requestMutation = requestJsonMutation,
|
|
93
|
+
} = {},
|
|
94
|
+
) {
|
|
95
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
96
|
+
const normalizedInvitationToken = normalizeString(invitationToken);
|
|
97
|
+
if (!normalizedSessionId) {
|
|
98
|
+
throw new Error("session id is required.");
|
|
99
|
+
}
|
|
100
|
+
if (!normalizedInvitationToken) {
|
|
101
|
+
throw new Error("invite token is required.");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const auth = await resolveAuthSession({
|
|
105
|
+
cwd: targetPath,
|
|
106
|
+
env: process.env,
|
|
107
|
+
autoRotate: false,
|
|
108
|
+
});
|
|
109
|
+
if (!auth?.token || !auth?.apiUrl) {
|
|
110
|
+
throw new Error("Not authenticated. Run `sl auth login` first.");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const resolvedIdempotencyKey =
|
|
114
|
+
normalizeString(idempotencyKey) || createSessionMutationIdempotencyKey("session-invite-accept");
|
|
115
|
+
const apiUrl = normalizeString(auth.apiUrl).replace(/\/+$/, "");
|
|
116
|
+
const body = {
|
|
117
|
+
token: normalizedInvitationToken,
|
|
118
|
+
};
|
|
119
|
+
const normalizedSeatKey = normalizeString(seatKey);
|
|
120
|
+
const normalizedAgentId = normalizeString(agentId);
|
|
121
|
+
if (normalizedSeatKey) body.seatKey = normalizedSeatKey;
|
|
122
|
+
if (normalizedAgentId) body.agentId = normalizedAgentId;
|
|
123
|
+
|
|
124
|
+
const result = await requestMutation(
|
|
125
|
+
`${apiUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}/invitations/accept`,
|
|
126
|
+
{
|
|
127
|
+
method: "POST",
|
|
128
|
+
operationName: "session.invitation_accept",
|
|
129
|
+
idempotencyKey: resolvedIdempotencyKey,
|
|
130
|
+
headers: createSessionMutationHeaders({
|
|
131
|
+
bearerToken: auth.token,
|
|
132
|
+
sessionId: normalizedSessionId,
|
|
133
|
+
routeId: SESSION_INVITATION_ACCEPT_ROUTE_ID,
|
|
134
|
+
idempotencyKey: resolvedIdempotencyKey,
|
|
135
|
+
origin,
|
|
136
|
+
}),
|
|
137
|
+
body,
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
idempotencyKey: resolvedIdempotencyKey,
|
|
143
|
+
result,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function normalizeSessionOnboarding(onboarding = {}, claimedSeat = null) {
|
|
148
|
+
const onboardingSource =
|
|
149
|
+
onboarding && typeof onboarding === "object" && !Array.isArray(onboarding) ? onboarding : {};
|
|
150
|
+
const seatSource =
|
|
151
|
+
claimedSeat && typeof claimedSeat === "object" && !Array.isArray(claimedSeat) ? claimedSeat : {};
|
|
152
|
+
const normalized = {
|
|
153
|
+
seatKey: normalizeString(onboardingSource.seatKey) || normalizeString(seatSource.seatKey) || null,
|
|
154
|
+
agentId:
|
|
155
|
+
normalizeString(onboardingSource.agentId) ||
|
|
156
|
+
normalizeString(seatSource.claimedAgentId) ||
|
|
157
|
+
normalizeString(seatSource.agentId) ||
|
|
158
|
+
null,
|
|
159
|
+
displayName:
|
|
160
|
+
normalizeString(onboardingSource.displayName) || normalizeString(seatSource.displayName) || null,
|
|
161
|
+
role: normalizeString(onboardingSource.role) || normalizeString(seatSource.role) || null,
|
|
162
|
+
instructions:
|
|
163
|
+
normalizeString(onboardingSource.instructions) || normalizeString(seatSource.instructions) || null,
|
|
164
|
+
};
|
|
165
|
+
return Object.values(normalized).some(Boolean) ? normalized : null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function renderOnboardingMarkdown({ sessionId, onboarding, claimedSeat, agentId }) {
|
|
169
|
+
const lines = [
|
|
170
|
+
"# Senti Session Onboarding",
|
|
171
|
+
"",
|
|
172
|
+
`Session: ${sessionId}`,
|
|
173
|
+
`Agent: ${normalizeString(onboarding.agentId) || normalizeString(agentId) || "unknown"}`,
|
|
174
|
+
];
|
|
175
|
+
if (onboarding.displayName) lines.push(`Display name: ${onboarding.displayName}`);
|
|
176
|
+
if (onboarding.role) lines.push(`Role: ${onboarding.role}`);
|
|
177
|
+
if (onboarding.seatKey) lines.push(`Seat key: ${onboarding.seatKey}`);
|
|
178
|
+
if (claimedSeat?.seatType) lines.push(`Seat type: ${normalizeString(claimedSeat.seatType)}`);
|
|
179
|
+
lines.push("", "## SOUL Instructions", "");
|
|
180
|
+
lines.push(onboarding.instructions || "No seat-specific instructions were provided.");
|
|
181
|
+
lines.push("", "## Session Etiquette", "", renderCoordinationBulletList());
|
|
182
|
+
lines.push(
|
|
183
|
+
"",
|
|
184
|
+
"## First Actions",
|
|
185
|
+
"",
|
|
186
|
+
`- Read recent room context: \`sl session read ${sessionId} --remote --tail 50 --json\``,
|
|
187
|
+
`- Start a quiet listener: \`sl session listen --session ${sessionId} --agent <your-name> --interval 60 --active-interval 5 --emit ndjson --no-presence\``,
|
|
188
|
+
`- Post a short plan before editing: \`sl session say ${sessionId} \"plan: <scope>; files: <paths>\"\``,
|
|
189
|
+
"- Review `AGENTS.md` / `CLAUDE.md`, then keep `tasks/todo.md` current for work status.",
|
|
190
|
+
"- Add any Carter correction pattern to `tasks/lessons.md` before continuing the next slice.",
|
|
191
|
+
);
|
|
192
|
+
return `${lines.join("\n")}\n`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function writeSessionOnboardingBrief(
|
|
196
|
+
sessionId,
|
|
197
|
+
{
|
|
198
|
+
onboarding,
|
|
199
|
+
claimedSeat = null,
|
|
200
|
+
agentId = "",
|
|
201
|
+
targetPath = process.cwd(),
|
|
202
|
+
} = {},
|
|
203
|
+
) {
|
|
204
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
205
|
+
const normalizedOnboarding = normalizeSessionOnboarding(onboarding, claimedSeat);
|
|
206
|
+
if (!normalizedSessionId || !normalizedOnboarding) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
const paths = resolveSessionPaths(normalizedSessionId, { targetPath });
|
|
210
|
+
const owner = safeFileToken(normalizedOnboarding.agentId || agentId || normalizedOnboarding.seatKey);
|
|
211
|
+
const dir = path.join(paths.sentiDir, "onboarding");
|
|
212
|
+
const jsonPath = path.join(dir, `${owner}.json`);
|
|
213
|
+
const markdownPath = path.join(dir, `${owner}.md`);
|
|
214
|
+
const payload = {
|
|
215
|
+
schemaVersion: "1.0.0",
|
|
216
|
+
sessionId: normalizedSessionId,
|
|
217
|
+
agentId: normalizeString(agentId) || normalizedOnboarding.agentId || null,
|
|
218
|
+
onboarding: normalizedOnboarding,
|
|
219
|
+
claimedSeat: claimedSeat && typeof claimedSeat === "object" ? claimedSeat : null,
|
|
220
|
+
writtenAt: new Date().toISOString(),
|
|
221
|
+
};
|
|
222
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
223
|
+
await Promise.all([
|
|
224
|
+
fsp.writeFile(jsonPath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8"),
|
|
225
|
+
fsp.writeFile(
|
|
226
|
+
markdownPath,
|
|
227
|
+
renderOnboardingMarkdown({
|
|
228
|
+
sessionId: normalizedSessionId,
|
|
229
|
+
onboarding: normalizedOnboarding,
|
|
230
|
+
claimedSeat,
|
|
231
|
+
agentId,
|
|
232
|
+
}),
|
|
233
|
+
"utf-8",
|
|
234
|
+
),
|
|
235
|
+
]);
|
|
236
|
+
return {
|
|
237
|
+
jsonPath,
|
|
238
|
+
markdownPath,
|
|
239
|
+
onboarding: normalizedOnboarding,
|
|
240
|
+
};
|
|
241
|
+
}
|
package/src/session/stream.js
CHANGED
|
@@ -15,6 +15,10 @@ const DEFAULT_LOCK_POLL_MS = 25;
|
|
|
15
15
|
const DEFAULT_MAX_STREAM_EVENTS = 10_000;
|
|
16
16
|
const DEFAULT_REMOTE_REFRESH_TTL_SECONDS = 24 * 60 * 60;
|
|
17
17
|
const REMOTE_REFRESH_TIMEOUT_MS = 2_500;
|
|
18
|
+
const METADATA_WRITE_RETRY_ATTEMPTS = 5;
|
|
19
|
+
const METADATA_WRITE_RETRY_DELAY_MS = 50;
|
|
20
|
+
|
|
21
|
+
let metadataWriteCounter = 0;
|
|
18
22
|
|
|
19
23
|
function normalizeString(value) {
|
|
20
24
|
return String(value || "").trim();
|
|
@@ -68,11 +72,36 @@ async function readSessionMetadata(paths) {
|
|
|
68
72
|
}
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
function isRetryableMetadataWriteError(error) {
|
|
76
|
+
const code = error && typeof error === "object" ? error.code : "";
|
|
77
|
+
return code === "EPERM" || code === "EACCES" || code === "EBUSY";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function replaceMetadataFile(tmpPath, metadataPath) {
|
|
81
|
+
for (let attempt = 1; attempt <= METADATA_WRITE_RETRY_ATTEMPTS; attempt += 1) {
|
|
82
|
+
try {
|
|
83
|
+
await fsp.rename(tmpPath, metadataPath);
|
|
84
|
+
return;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (!isRetryableMetadataWriteError(error) || attempt >= METADATA_WRITE_RETRY_ATTEMPTS) {
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
await sleep(METADATA_WRITE_RETRY_DELAY_MS * attempt);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
71
94
|
async function writeSessionMetadata(paths, metadata = {}) {
|
|
72
95
|
await fsp.mkdir(paths.sessionDir, { recursive: true });
|
|
73
|
-
|
|
96
|
+
metadataWriteCounter = (metadataWriteCounter + 1) % Number.MAX_SAFE_INTEGER;
|
|
97
|
+
const tmpPath = `${paths.metadataPath}.${process.pid}.${Date.now()}.${metadataWriteCounter}.tmp`;
|
|
74
98
|
await fsp.writeFile(tmpPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf-8");
|
|
75
|
-
|
|
99
|
+
try {
|
|
100
|
+
await replaceMetadataFile(tmpPath, paths.metadataPath);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
await fsp.rm(tmpPath, { force: true }).catch(() => {});
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
76
105
|
}
|
|
77
106
|
|
|
78
107
|
function remoteStatusAllowsLocalRefresh(remoteSession = {}) {
|