sentinelayer-cli 0.46.4 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -3
- package/package.json +12 -7
- package/skills/omar-gate-setup/SKILL.md +103 -0
- package/skills/senti-agent-identity/SKILL.md +80 -0
- package/skills/senti-judge/SKILL.md +139 -0
- package/skills/senti-room-notifications/SKILL.md +73 -0
- package/skills/senti-room-notifications/listen-wrapper.cjs +150 -0
- package/skills/senti-room-notifications/package.json +1 -0
- package/skills/senti-room-notifications/test-classifier.js +109 -0
- package/skills/senti-room-notifications/test-receipt.js +54 -0
- package/skills/sentinelayer-cli/SKILL.md +222 -0
- package/src/agents/devtestbot/runner.js +22 -11
- package/src/agents/devtestbot/tool.js +24 -11
- package/src/agents/jules/fix-cycle.js +7 -1
- package/src/agents/jules/tools/runtime-audit.js +17 -12
- package/src/agents/shared-tools/dispatch-core.js +10 -5
- package/src/ai/aidenid.js +4 -2
- package/src/ai/domain-target-store.js +11 -5
- package/src/ai/identity-store.js +11 -5
- package/src/ai/site-store.js +8 -4
- package/src/audit/orchestrator.js +21 -14
- package/src/audit/package.js +6 -4
- package/src/audit/persona-loop.js +7 -0
- package/src/audit/replay.js +4 -2
- package/src/auth/gate.js +46 -8
- package/src/auth/service.js +34 -15
- package/src/auth/session-store.js +105 -47
- package/src/board/evidence-resolvers.js +3 -4
- package/src/cli.js +23 -3
- package/src/commands/ai/identity-lifecycle.js +30 -26
- package/src/commands/ai/provision-governance.js +32 -24
- package/src/commands/ai/shared.js +6 -4
- package/src/commands/audit.js +14 -12
- package/src/commands/auth.js +24 -4
- package/src/commands/board.js +2 -2
- package/src/commands/chat.js +8 -6
- package/src/commands/judge.js +125 -0
- package/src/commands/legacy-args.js +1 -0
- package/src/commands/mcp.js +45 -52
- package/src/commands/omargate.js +2 -1
- package/src/commands/plugin.js +11 -3
- package/src/commands/review.js +9 -4
- package/src/commands/scan.js +7 -7
- package/src/commands/session.js +607 -57
- package/src/commands/skills.js +45 -0
- package/src/commands/watch.js +9 -7
- package/src/config/output-filesystem.js +156 -0
- package/src/config/schema.js +4 -2
- package/src/config/service.js +40 -4
- package/src/cost/history.js +15 -9
- package/src/daemon/artifact-lineage.js +15 -12
- package/src/daemon/assignment-ledger.js +31 -25
- package/src/daemon/ast-drift.js +7 -3
- package/src/daemon/budget-governor.js +26 -20
- package/src/daemon/error-worker.js +28 -22
- package/src/daemon/fix-cycle.js +5 -4
- package/src/daemon/hybrid-mapper.js +19 -16
- package/src/daemon/ingest-refresh.js +4 -0
- package/src/daemon/jira-lifecycle.js +21 -17
- package/src/daemon/operator-control.js +18 -15
- package/src/daemon/reliability-lane.js +18 -15
- package/src/daemon/scope-engine.js +12 -8
- package/src/daemon/watchdog.js +15 -12
- package/src/engram/build-state.js +24 -3
- package/src/engram/capability-index.js +367 -0
- package/src/engram/compile-graph.js +1063 -0
- package/src/engram/contracts/action-receipt.js +82 -0
- package/src/engram/contracts/capability-index.js +259 -0
- package/src/engram/contracts/capability.js +377 -0
- package/src/engram/contracts/content-hash.js +8 -0
- package/src/engram/contracts/evidence.js +258 -0
- package/src/engram/contracts/graph.js +594 -0
- package/src/engram/contracts/identity.js +8 -0
- package/src/engram/contracts/index.js +149 -0
- package/src/engram/contracts/media-asset.js +85 -0
- package/src/engram/contracts/money.js +249 -0
- package/src/engram/contracts/source-snapshot.js +57 -0
- package/src/engram/contracts/task-context-pack.js +114 -0
- package/src/engram/contracts/temporal-segment.js +72 -0
- package/src/engram/contracts/versions.js +96 -0
- package/src/engram/extract-html.js +692 -0
- package/src/engram/graph-store.js +877 -0
- package/src/engram/index.js +98 -14
- package/src/engram/ingest-site.js +132 -0
- package/src/engram/live-state.js +1180 -0
- package/src/engram/namespace.js +34 -1
- package/src/engram/retrieve-site.js +753 -0
- package/src/engram/safe-reader.js +597 -0
- package/src/engram/source-intake.js +251 -0
- package/src/engram/store.js +227 -9
- package/src/ingest/engine.js +21 -28
- package/src/judge/verdict.js +305 -0
- package/src/legacy-cli.js +96 -19
- package/src/mcp/bridge-inputs.js +28 -4
- package/src/mcp/cli-command-tools.js +70 -0
- package/src/mcp/registry.js +17 -13
- package/src/mcp/session-stdio-server.js +297 -19
- package/src/memory/blackboard.js +5 -4
- package/src/net/gh-api-path.js +35 -9
- package/src/net/test-egress-guard.js +16 -4
- package/src/plugin/manifest.js +12 -8
- package/src/review/ai-review.js +110 -10
- package/src/review/investor-dd-devtestbot.js +17 -6
- package/src/review/investor-dd-orchestrator.js +21 -13
- package/src/review/local-review.js +26 -16
- package/src/review/omargate-cache.js +5 -5
- package/src/review/omargate-orchestrator.js +89 -17
- package/src/review/replay.js +9 -4
- package/src/review/report.js +22 -13
- package/src/review/typesafe-severity.js +201 -0
- package/src/session/admission-auth.js +34 -7
- package/src/session/admission.js +322 -71
- package/src/session/agent-access.js +137 -0
- package/src/session/agent-registry.js +2 -1
- package/src/session/attachment-summary.js +77 -0
- package/src/session/edit-summary.js +19 -0
- package/src/session/invitations.js +65 -1
- package/src/session/mentions.js +17 -9
- package/src/session/message-edits.js +5 -4
- package/src/session/preview.js +10 -2
- package/src/session/recap.js +3 -2
- package/src/session/room-files.js +519 -0
- package/src/session/route-classes.js +22 -8
- package/src/session/setup-guides.js +4 -0
- package/src/session/standup.js +192 -0
- package/src/session/sync.js +229 -10
- package/src/session/tickets.js +41 -0
- package/src/session/transcript.js +2 -1
- package/src/session/wake/resolve-target.js +7 -3
- package/src/skills/listener-runtime.cjs +92 -0
- package/src/skills/service.js +207 -0
- package/src/swarm/factory.js +5 -5
- package/src/swarm/pentest.js +9 -9
- package/src/swarm/report.js +4 -4
- package/src/swarm/runtime.js +15 -12
- package/src/swarm/scenario-dsl.js +7 -5
- package/src/telemetry/ledger.js +16 -7
package/src/plugin/manifest.js
CHANGED
|
@@ -3,7 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { ZodError, z } from "zod";
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { resolveOutputContext } from "../config/service.js";
|
|
7
|
+
import { requireOutputFileSystem } from "../config/output-filesystem.js";
|
|
7
8
|
|
|
8
9
|
export const PLUGIN_MANIFEST_SCHEMA_VERSION = "1.0.0";
|
|
9
10
|
export const PLUGIN_MANIFEST_KIND = "sentinelayer.cli.plugin";
|
|
@@ -284,11 +285,13 @@ export function stringifyJson(value) {
|
|
|
284
285
|
return `${JSON.stringify(value, null, 2)}\n`;
|
|
285
286
|
}
|
|
286
287
|
|
|
287
|
-
export async function writeJsonFile(filePath, value, { force = false } = {}) {
|
|
288
|
+
export async function writeJsonFile(filePath, value, { force = false, outputFs } = {}) {
|
|
289
|
+
requireOutputFileSystem(outputFs);
|
|
288
290
|
const resolvedPath = path.resolve(filePath);
|
|
291
|
+
await outputFs.assertPath(resolvedPath);
|
|
289
292
|
if (!force) {
|
|
290
293
|
try {
|
|
291
|
-
await
|
|
294
|
+
await outputFs.access(resolvedPath);
|
|
292
295
|
throw new Error(`File already exists: ${resolvedPath}. Use --force to overwrite.`);
|
|
293
296
|
} catch (error) {
|
|
294
297
|
if (error && typeof error === "object" && error.code === "ENOENT") {
|
|
@@ -301,8 +304,8 @@ export async function writeJsonFile(filePath, value, { force = false } = {}) {
|
|
|
301
304
|
}
|
|
302
305
|
}
|
|
303
306
|
|
|
304
|
-
await
|
|
305
|
-
await
|
|
307
|
+
await outputFs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
308
|
+
await outputFs.writeFile(resolvedPath, stringifyJson(value), "utf-8");
|
|
306
309
|
return resolvedPath;
|
|
307
310
|
}
|
|
308
311
|
|
|
@@ -315,8 +318,8 @@ export async function readJsonFile(filePath) {
|
|
|
315
318
|
};
|
|
316
319
|
}
|
|
317
320
|
|
|
318
|
-
export async function resolvePluginsDirectoryPath({ cwd, outputDir, env } = {}) {
|
|
319
|
-
const outputRoot = await
|
|
321
|
+
export async function resolvePluginsDirectoryPath({ cwd, outputDir, env, outputContext } = {}) {
|
|
322
|
+
const { outputRoot } = outputContext || await resolveOutputContext({
|
|
320
323
|
cwd,
|
|
321
324
|
outputDirOverride: outputDir,
|
|
322
325
|
env,
|
|
@@ -324,11 +327,12 @@ export async function resolvePluginsDirectoryPath({ cwd, outputDir, env } = {})
|
|
|
324
327
|
return path.join(outputRoot, "plugins");
|
|
325
328
|
}
|
|
326
329
|
|
|
327
|
-
export async function resolveDefaultPluginManifestPath({ cwd, outputDir, env, pluginId } = {}) {
|
|
330
|
+
export async function resolveDefaultPluginManifestPath({ cwd, outputDir, env, pluginId, outputContext } = {}) {
|
|
328
331
|
const pluginsRoot = await resolvePluginsDirectoryPath({
|
|
329
332
|
cwd,
|
|
330
333
|
outputDir,
|
|
331
334
|
env,
|
|
335
|
+
outputContext,
|
|
332
336
|
});
|
|
333
337
|
const normalizedId = normalizePluginId(pluginId);
|
|
334
338
|
return path.join(pluginsRoot, normalizedId, "plugin.json");
|
package/src/review/ai-review.js
CHANGED
|
@@ -10,11 +10,12 @@ import {
|
|
|
10
10
|
import { computeProviderCost } from "../billing/price-book.js";
|
|
11
11
|
import { buildBillingRunId, buildCallIdempotencyKey, sanitizeBillingMetadata } from "../billing/ledger-entry.js";
|
|
12
12
|
import { recordSessionUsage } from "../billing/session-usage.js";
|
|
13
|
-
import { loadConfig, userConfigValues } from "../config/service.js";
|
|
13
|
+
import { loadConfig, resolveOutputContext, userConfigValues } from "../config/service.js";
|
|
14
14
|
import { evaluateBudget } from "../cost/budget.js";
|
|
15
|
-
import { appendCostEntry, summarizeCostHistory } from "../cost/history.js";
|
|
15
|
+
import { appendCostEntry, summarizeCostHistory, loadCostHistory } from "../cost/history.js";
|
|
16
16
|
import { estimateTokens } from "../cost/tokenizer.js";
|
|
17
|
-
import {
|
|
17
|
+
import { evaluateSeverityClassification, normalizeSeverityClassifier, withSeverityEvaluationQueue, severityResourceTotals, parseSeverityEvaluationMaxCost, TYPESAFE_SEVERITY_MODEL, SEVERITY_DICTIONARY_VERSION } from "./typesafe-severity.js";
|
|
18
|
+
import { appendRunEvent, loadRunEvents, deriveStopClassFromBudget } from "../telemetry/ledger.js";
|
|
18
19
|
|
|
19
20
|
const AI_SEVERITIES = new Set(["P0", "P1", "P2", "P3"]);
|
|
20
21
|
const DEFAULT_AI_MAX_FINDINGS = 20;
|
|
@@ -199,13 +200,51 @@ function summarizeFindings(findings = []) {
|
|
|
199
200
|
};
|
|
200
201
|
}
|
|
201
202
|
|
|
203
|
+
// Normalization remains permissive for legacy reports. Completion requires a
|
|
204
|
+
// structured review, so error envelopes and invented defaults cannot prove it.
|
|
205
|
+
function validateReviewResponse(text) {
|
|
206
|
+
const raw = String(text || "").trim();
|
|
207
|
+
const fenced = raw.match(/^```(?:json)?\s*([\s\S]*?)```\s*$/i);
|
|
208
|
+
let parsed;
|
|
209
|
+
try {
|
|
210
|
+
parsed = JSON.parse(fenced ? fenced[1] : raw);
|
|
211
|
+
} catch {
|
|
212
|
+
return { valid: false, reason: "unstructured_response" };
|
|
213
|
+
}
|
|
214
|
+
if (Array.isArray(parsed)) parsed = { findings: parsed };
|
|
215
|
+
if (!parsed || typeof parsed !== "object") return { valid: false, reason: "invalid_review_envelope" };
|
|
216
|
+
const hasError = (value) => value !== undefined && value !== null && value !== false && value !== "" &&
|
|
217
|
+
!(Array.isArray(value) && value.length === 0);
|
|
218
|
+
const invalidStatus = Object.hasOwn(parsed, "status") &&
|
|
219
|
+
(typeof parsed.status !== "string" || !["ok", "success", "complete", "completed", "passed"].includes(parsed.status.trim().toLowerCase()));
|
|
220
|
+
if (hasError(parsed.error) || hasError(parsed.errors) || hasError(parsed.refusal) || invalidStatus ||
|
|
221
|
+
parsed.ok === false || parsed.success === false || parsed.complete === false) {
|
|
222
|
+
return { valid: false, reason: "error_response" };
|
|
223
|
+
}
|
|
224
|
+
if (!Array.isArray(parsed.findings)) return { valid: false, reason: "missing_or_invalid_findings" };
|
|
225
|
+
const malformed = parsed.findings.some((finding) => {
|
|
226
|
+
if (!finding || typeof finding !== "object" || Array.isArray(finding)) return true;
|
|
227
|
+
const message = finding.title || finding.message;
|
|
228
|
+
const line = typeof finding.line === "string" && /^\d+$/.test(finding.line) ? Number(finding.line) : finding.line;
|
|
229
|
+
return typeof message !== "string" || !message.trim() ||
|
|
230
|
+
typeof finding.file !== "string" || !finding.file.trim() ||
|
|
231
|
+
typeof finding.severity !== "string" || !AI_SEVERITIES.has(finding.severity.trim().toUpperCase()) ||
|
|
232
|
+
!Number.isSafeInteger(line) || line < 1 ||
|
|
233
|
+
(finding.confidence !== undefined && finding.confidence !== null &&
|
|
234
|
+
(typeof finding.confidence !== "number" || !Number.isFinite(finding.confidence) || finding.confidence < 0 || finding.confidence > 1));
|
|
235
|
+
});
|
|
236
|
+
return { valid: !malformed, reason: malformed ? "malformed_findings" : "structured_review" };
|
|
237
|
+
}
|
|
238
|
+
|
|
202
239
|
export function parseAiReviewResponse({ text, maxFindings = DEFAULT_AI_MAX_FINDINGS } = {}) {
|
|
203
240
|
const parsed = extractJsonPayload(text);
|
|
241
|
+
const reviewValidity = validateReviewResponse(text);
|
|
204
242
|
const normalizedMaxFindings = Math.max(1, Math.floor(Number(maxFindings || DEFAULT_AI_MAX_FINDINGS)));
|
|
205
243
|
|
|
206
244
|
if (!parsed) {
|
|
207
245
|
return {
|
|
208
246
|
parser: "fallback_text",
|
|
247
|
+
reviewValidity,
|
|
209
248
|
summary:
|
|
210
249
|
sanitizeExcerpt(text) || "AI response could not be parsed as JSON; no structured findings extracted.",
|
|
211
250
|
findings: [],
|
|
@@ -229,6 +268,7 @@ export function parseAiReviewResponse({ text, maxFindings = DEFAULT_AI_MAX_FINDI
|
|
|
229
268
|
|
|
230
269
|
return {
|
|
231
270
|
parser: "json",
|
|
271
|
+
reviewValidity,
|
|
232
272
|
summary: summary || "Structured AI response parsed successfully.",
|
|
233
273
|
findings,
|
|
234
274
|
};
|
|
@@ -355,12 +395,14 @@ function composeAiReviewMarkdown({
|
|
|
355
395
|
runId,
|
|
356
396
|
mode,
|
|
357
397
|
parser,
|
|
398
|
+
reviewValidity,
|
|
358
399
|
summary,
|
|
359
400
|
provider,
|
|
360
401
|
model,
|
|
361
402
|
dryRun,
|
|
362
403
|
findings = [],
|
|
363
404
|
usage,
|
|
405
|
+
severityClassification = null,
|
|
364
406
|
combinedSummary,
|
|
365
407
|
} = {}) {
|
|
366
408
|
const findingLines =
|
|
@@ -374,7 +416,8 @@ function composeAiReviewMarkdown({
|
|
|
374
416
|
(finding.userImpact ? ` user_impact: ${finding.userImpact}\n` : "") +
|
|
375
417
|
(finding.trafficLight ? ` traffic_light: ${finding.trafficLight}\n` : "") +
|
|
376
418
|
` suggested_fix: ${finding.suggestedFix}` +
|
|
377
|
-
(finding.confidence === null ? "" : `\n confidence: ${finding.confidence.toFixed(2)}`)
|
|
419
|
+
(finding.confidence === null ? "" : `\n confidence: ${finding.confidence.toFixed(2)}`) +
|
|
420
|
+
(finding.severityClassification ? `\n severity_evaluation: ${finding.severityClassification.status} / ${finding.severityClassification.reason}; candidate=${finding.severityClassification.severity || "unverified"}; category=${finding.severityClassification.category || "unverified"}; authority=evaluation_only` : "")
|
|
378
421
|
)
|
|
379
422
|
.join("\n")
|
|
380
423
|
: "- none";
|
|
@@ -389,12 +432,14 @@ function composeAiReviewMarkdown({
|
|
|
389
432
|
`Model: ${model}`,
|
|
390
433
|
`Dry run: ${dryRun ? "yes" : "no"}`,
|
|
391
434
|
`Parser: ${parser}`,
|
|
435
|
+
`Structured review: ${reviewValidity?.valid ? "valid" : "incomplete"} (${reviewValidity?.reason || "unknown"})`,
|
|
392
436
|
"",
|
|
437
|
+
...(severityClassification ? [`TypeSafe classification evaluation: ${severityClassification.status} / ${severityClassification.reason}; schema validation only, original gate severities unchanged.`, `Classifier usage: ${severityClassification.usage ? JSON.stringify(severityClassification.usage) : "unknown/not submitted (not zero cost)"}; attempted=${severityClassification.attempted}`, ""] : []),
|
|
393
438
|
"Summary:",
|
|
394
439
|
`- ${summary || "No summary provided."}`,
|
|
395
440
|
`- Combined findings: P0=${combinedSummary.P0} P1=${combinedSummary.P1} P2=${combinedSummary.P2} P3=${combinedSummary.P3}`,
|
|
396
441
|
`- Blocking: ${combinedSummary.blocking ? "yes" : "no"}`,
|
|
397
|
-
`- Usage: input_tokens=${usage.inputTokens} output_tokens=${usage.outputTokens} cost_usd=${usage.costUsd.toFixed(6)} duration_ms=${usage.durationMs}`,
|
|
442
|
+
`- ${severityClassification ? "Generation usage" : "Usage"}: input_tokens=${usage.inputTokens} output_tokens=${usage.outputTokens} cost_usd=${usage.costUsd.toFixed(6)} duration_ms=${usage.durationMs}`,
|
|
398
443
|
"",
|
|
399
444
|
"AI Findings:",
|
|
400
445
|
findingLines,
|
|
@@ -486,6 +531,7 @@ export async function runAiReviewLayer({
|
|
|
486
531
|
warningThresholdPercent = 80,
|
|
487
532
|
systemPrompt = "",
|
|
488
533
|
dryRun = false,
|
|
534
|
+
severityClassifier = "off",
|
|
489
535
|
requireUsageLedger = false,
|
|
490
536
|
usageRecorder = recordSessionUsage,
|
|
491
537
|
sourceCommand = "review",
|
|
@@ -496,8 +542,11 @@ export async function runAiReviewLayer({
|
|
|
496
542
|
usageFailureLabel = "Review AI",
|
|
497
543
|
env = process.env,
|
|
498
544
|
} = {}) {
|
|
545
|
+
normalizeSeverityClassifier(severityClassifier);
|
|
546
|
+
if (severityClassifier === "typesafe") maxCostUsd = parseSeverityEvaluationMaxCost(maxCostUsd, 1);
|
|
499
547
|
const normalizedTargetPath = path.resolve(String(targetPath || "."));
|
|
500
548
|
const normalizedRunDirectory = path.resolve(String(runDirectory || "."));
|
|
549
|
+
const { outputFs } = await resolveOutputContext({ cwd: normalizedTargetPath, outputDirOverride: outputDir, env });
|
|
501
550
|
const normalizedMode = normalizeString(mode) || "full";
|
|
502
551
|
const normalizedMaxFindings = Math.max(
|
|
503
552
|
1,
|
|
@@ -624,7 +673,7 @@ export async function runAiReviewLayer({
|
|
|
624
673
|
noProgressStreak: 0,
|
|
625
674
|
};
|
|
626
675
|
|
|
627
|
-
|
|
676
|
+
let budget = evaluateBudget({
|
|
628
677
|
sessionSummary,
|
|
629
678
|
maxCostUsd: parseNonNegativeNumber(maxCostUsd, "maxCostUsd"),
|
|
630
679
|
maxOutputTokens: parseNonNegativeNumber(maxOutputTokens, "maxOutputTokens"),
|
|
@@ -718,6 +767,51 @@ export async function runAiReviewLayer({
|
|
|
718
767
|
}
|
|
719
768
|
}
|
|
720
769
|
|
|
770
|
+
let severityClassification = null;
|
|
771
|
+
if (severityClassifier === "typesafe") {
|
|
772
|
+
const historyOptions = { targetPath: normalizedTargetPath, outputDirOverride: outputDir };
|
|
773
|
+
severityClassification = await withSeverityEvaluationQueue(`${appendedCost.filePath}:${normalizedSessionId}`, async () => {
|
|
774
|
+
let reservation = false; const attemptId = crypto.randomUUID();
|
|
775
|
+
const event = (phase, metadata) => appendRunEvent(historyOptions, {
|
|
776
|
+
sessionId: normalizedSessionId, runId: normalizedRunId,
|
|
777
|
+
eventType: phase === "reserved" ? "tool_call" : "run_step",
|
|
778
|
+
metadata: { layer: "severity_classification_evaluation", version: 1, attemptId, phase, costKnown: false, outputTokensKnown: false, ...metadata },
|
|
779
|
+
});
|
|
780
|
+
let classifierBudget = budget;
|
|
781
|
+
try {
|
|
782
|
+
const [{ events }, current] = await Promise.all([loadRunEvents(historyOptions), loadCostHistory(historyOptions)]);
|
|
783
|
+
const baseline = summarizeCostHistory(current.history).sessions.find((r) => r.sessionId === normalizedSessionId) || sessionSummary;
|
|
784
|
+
const extra = severityResourceTotals(events, normalizedSessionId);
|
|
785
|
+
classifierBudget = evaluateBudget({ ...budget.limits, sessionSummary: { ...baseline, toolCalls: baseline.toolCalls + extra.toolCalls, durationMs: baseline.durationMs + extra.durationMs } });
|
|
786
|
+
if (extra.unknownRuntime && budget.limits.maxRuntimeMs > 0) return { version: 1, mode: "typesafe", authority: "evaluation_only", verification: "response_schema_only", status: "unverified", reason: "prior_runtime_unknown", requestedModel: TYPESAFE_SEVERITY_MODEL, dictionaryVersion: SEVERITY_DICTIONARY_VERSION, attempted: false, durationMs: 0, usage: null, findings: parsed.findings.map(() => ({ status: "unverified", reason: "prior_runtime_unknown", severity: null, category: null })) };
|
|
787
|
+
} catch {
|
|
788
|
+
return { version: 1, mode: "typesafe", authority: "evaluation_only", status: "unverified", reason: "accounting_unavailable", requestedModel: TYPESAFE_SEVERITY_MODEL, dictionaryVersion: SEVERITY_DICTIONARY_VERSION, attempted: false, durationMs: 0, usage: null, findings: parsed.findings.map(() => ({ status: "unverified", reason: "accounting_unavailable", severity: null, category: null })) };
|
|
789
|
+
}
|
|
790
|
+
let evaluated = await evaluateSeverityClassification({
|
|
791
|
+
mode: severityClassifier, findings: parsed.findings, env, dryRun,
|
|
792
|
+
requireUsageLedger: normalizedRequireUsageLedger, budget: classifierBudget,
|
|
793
|
+
beforeRequest: async () => { await event("reserved", { reservedRequests: 1 }); reservation = true; },
|
|
794
|
+
});
|
|
795
|
+
if (reservation) {
|
|
796
|
+
try { await event("completed", { actualRequests: evaluated.attempted ? 1 : 0, durationMs: evaluated.durationMs, status: evaluated.status, reason: evaluated.reason, costKnown: false, outputTokensKnown: Boolean(evaluated.usage), reportedUsage: evaluated.usage }); }
|
|
797
|
+
catch { evaluated = { ...evaluated, status: "unverified", reason: "accounting_unavailable" }; }
|
|
798
|
+
}
|
|
799
|
+
const resourcesAfter = evaluateBudget({ ...classifierBudget.limits, sessionSummary: {
|
|
800
|
+
costUsd: classifierBudget.usage.costUsd + (evaluated.usage?.listPriceEstimateUsd || 0),
|
|
801
|
+
outputTokens: classifierBudget.usage.outputTokens + (evaluated.usage?.outputTokens || 0),
|
|
802
|
+
durationMs: classifierBudget.usage.runtimeMs + (reservation ? evaluated.durationMs : 0),
|
|
803
|
+
toolCalls: classifierBudget.usage.toolCalls + (reservation ? 1 : 0),
|
|
804
|
+
noProgressStreak: classifierBudget.usage.noProgressStreak,
|
|
805
|
+
} });
|
|
806
|
+
if (evaluated.attempted && !evaluated.usage) resourcesAfter.classificationUsageUnknown = true;
|
|
807
|
+
return { ...evaluated, resourceAccounting: { reservedRequests: reservation ? 1 : 0, actualRequests: evaluated.attempted ? 1 : 0, durationMs: evaluated.durationMs, budgetAfter: resourcesAfter } };
|
|
808
|
+
});
|
|
809
|
+
if (severityClassification.resourceAccounting?.budgetAfter) budget = severityClassification.resourceAccounting.budgetAfter;
|
|
810
|
+
aiFindings.forEach((finding, index) => {
|
|
811
|
+
finding.severityClassification = { ...severityClassification.findings[index], authority: "evaluation_only", dictionaryVersion: severityClassification.dictionaryVersion, model: severityClassification.requestedModel };
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
|
|
721
815
|
let stopTelemetry = null;
|
|
722
816
|
if (budget.blocking) {
|
|
723
817
|
stopTelemetry = await appendRunEvent(
|
|
@@ -755,7 +849,7 @@ export async function runAiReviewLayer({
|
|
|
755
849
|
);
|
|
756
850
|
}
|
|
757
851
|
|
|
758
|
-
await
|
|
852
|
+
await outputFs.mkdir(normalizedRunDirectory, { recursive: true });
|
|
759
853
|
const promptPath = path.join(normalizedRunDirectory, "REVIEW_AI_PROMPT.txt");
|
|
760
854
|
const reportMarkdownPath = path.join(normalizedRunDirectory, "REVIEW_AI.md");
|
|
761
855
|
const reportJsonPath = path.join(normalizedRunDirectory, "REVIEW_AI.json");
|
|
@@ -773,6 +867,7 @@ export async function runAiReviewLayer({
|
|
|
773
867
|
runId: normalizedRunId,
|
|
774
868
|
mode: normalizedMode,
|
|
775
869
|
parser: parsed.parser,
|
|
870
|
+
reviewValidity: parsed.reviewValidity,
|
|
776
871
|
summary: parsed.summary,
|
|
777
872
|
provider: resolvedProvider,
|
|
778
873
|
model: resolvedModel,
|
|
@@ -786,6 +881,7 @@ export async function runAiReviewLayer({
|
|
|
786
881
|
aiSummary,
|
|
787
882
|
combinedSummary,
|
|
788
883
|
findings: aiFindings,
|
|
884
|
+
...(severityClassification ? { severityClassification } : {}),
|
|
789
885
|
};
|
|
790
886
|
|
|
791
887
|
const reportMarkdown = composeAiReviewMarkdown({
|
|
@@ -793,22 +889,26 @@ export async function runAiReviewLayer({
|
|
|
793
889
|
runId: normalizedRunId,
|
|
794
890
|
mode: normalizedMode,
|
|
795
891
|
parser: parsed.parser,
|
|
892
|
+
reviewValidity: parsed.reviewValidity,
|
|
796
893
|
summary: parsed.summary,
|
|
797
894
|
provider: resolvedProvider,
|
|
798
895
|
model: resolvedModel,
|
|
799
896
|
dryRun: Boolean(dryRun),
|
|
800
897
|
findings: aiFindings,
|
|
801
898
|
usage,
|
|
899
|
+
severityClassification,
|
|
802
900
|
combinedSummary,
|
|
803
901
|
});
|
|
804
902
|
|
|
805
|
-
await
|
|
806
|
-
await
|
|
807
|
-
await
|
|
903
|
+
await outputFs.writeFile(promptPath, `${prompt}\n`, "utf-8");
|
|
904
|
+
await outputFs.writeFile(reportMarkdownPath, `${reportMarkdown.trim()}\n`, "utf-8");
|
|
905
|
+
await outputFs.writeFile(reportJsonPath, `${JSON.stringify(reportPayload, null, 2)}\n`, "utf-8");
|
|
808
906
|
|
|
809
907
|
return {
|
|
810
908
|
parser: parsed.parser,
|
|
909
|
+
reviewValidity: parsed.reviewValidity,
|
|
811
910
|
summary: parsed.summary,
|
|
911
|
+
...(severityClassification ? { severityClassification } : {}),
|
|
812
912
|
findings: aiFindings,
|
|
813
913
|
aiSummary,
|
|
814
914
|
combinedSummary,
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
* writes an explicit evidence-gap artifact bundle.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import fsp from "node:fs/promises";
|
|
11
10
|
import path from "node:path";
|
|
12
11
|
import crypto from "node:crypto";
|
|
13
12
|
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { requireOutputFileSystem } from "../config/output-filesystem.js";
|
|
14
14
|
|
|
15
15
|
import { recordProvisionedIdentity } from "../ai/identity-store.js";
|
|
16
16
|
import { runDevTestBotSession } from "../agents/devtestbot/tool.js";
|
|
@@ -27,9 +27,10 @@ export const DEVTESTBOT_PHASE_DEFAULT_SCOPE = "smoke";
|
|
|
27
27
|
export const DEVTESTBOT_PHASE_DEFAULT_SWARMS = 1;
|
|
28
28
|
export const DEVTESTBOT_PHASE_DEFAULT_PER_SWARM_BUDGET_USD = 0.25;
|
|
29
29
|
|
|
30
|
-
async function
|
|
31
|
-
|
|
32
|
-
await
|
|
30
|
+
async function writeJsonWithFs(outputFs, filePath, obj) {
|
|
31
|
+
requireOutputFileSystem(outputFs);
|
|
32
|
+
await outputFs.mkdir(path.dirname(filePath), { recursive: true });
|
|
33
|
+
await outputFs.writeFile(filePath, JSON.stringify(obj, null, 2), "utf-8");
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
function normalizeString(value) {
|
|
@@ -347,14 +348,17 @@ function makeSyntheticIdentityResponse({ runId, index }) {
|
|
|
347
348
|
}
|
|
348
349
|
|
|
349
350
|
export async function provisionDevTestBotIdentities({
|
|
351
|
+
outputFs,
|
|
350
352
|
outputRoot,
|
|
351
353
|
runId,
|
|
352
354
|
count,
|
|
353
355
|
provisionIdentity = null,
|
|
354
356
|
onEvent = () => {},
|
|
355
357
|
} = {}) {
|
|
358
|
+
requireOutputFileSystem(outputFs);
|
|
356
359
|
if (!outputRoot) throw new TypeError("provisionDevTestBotIdentities requires outputRoot");
|
|
357
360
|
if (!runId) throw new TypeError("provisionDevTestBotIdentities requires runId");
|
|
361
|
+
await outputFs.assertPath(outputRoot);
|
|
358
362
|
const total = clampInt(count, { min: 0, max: DEVTESTBOT_PHASE_MAX_CONCURRENT, fallback: 0 });
|
|
359
363
|
const identities = [];
|
|
360
364
|
|
|
@@ -378,6 +382,7 @@ export async function provisionDevTestBotIdentities({
|
|
|
378
382
|
response = makeSyntheticIdentityResponse({ runId, index });
|
|
379
383
|
}
|
|
380
384
|
const recorded = await recordProvisionedIdentity({
|
|
385
|
+
outputFs,
|
|
381
386
|
outputRoot,
|
|
382
387
|
response,
|
|
383
388
|
context: {
|
|
@@ -459,6 +464,7 @@ function safeErrorMessage(error) {
|
|
|
459
464
|
}
|
|
460
465
|
|
|
461
466
|
export async function runDevTestBotPhase({
|
|
467
|
+
outputFs,
|
|
462
468
|
runId,
|
|
463
469
|
rootPath,
|
|
464
470
|
outputRoot,
|
|
@@ -470,11 +476,15 @@ export async function runDevTestBotPhase({
|
|
|
470
476
|
options = {},
|
|
471
477
|
onEvent = () => {},
|
|
472
478
|
} = {}) {
|
|
479
|
+
requireOutputFileSystem(outputFs);
|
|
480
|
+
const writeJson = (...args) => writeJsonWithFs(outputFs, ...args);
|
|
473
481
|
if (!runId) throw new TypeError("runDevTestBotPhase requires runId");
|
|
474
482
|
if (!rootPath) throw new TypeError("runDevTestBotPhase requires rootPath");
|
|
475
483
|
if (!outputRoot) throw new TypeError("runDevTestBotPhase requires outputRoot");
|
|
476
484
|
if (!runRoot) throw new TypeError("runDevTestBotPhase requires runRoot");
|
|
477
485
|
if (!artifactDir) throw new TypeError("runDevTestBotPhase requires artifactDir");
|
|
486
|
+
const phaseRoot = path.join(runRoot, "devtestbot");
|
|
487
|
+
await Promise.all([outputRoot, phaseRoot, artifactDir].map((destination) => outputFs.assertPath(destination)));
|
|
478
488
|
|
|
479
489
|
const normalized = normalizePhaseOptions(options);
|
|
480
490
|
const phaseStartedAt = Date.now();
|
|
@@ -485,8 +495,7 @@ export async function runDevTestBotPhase({
|
|
|
485
495
|
budget,
|
|
486
496
|
options: normalized,
|
|
487
497
|
});
|
|
488
|
-
|
|
489
|
-
await fsp.mkdir(phaseRoot, { recursive: true });
|
|
498
|
+
await outputFs.mkdir(phaseRoot, { recursive: true });
|
|
490
499
|
|
|
491
500
|
onEvent({
|
|
492
501
|
type: "phase_start",
|
|
@@ -551,6 +560,7 @@ export async function runDevTestBotPhase({
|
|
|
551
560
|
}
|
|
552
561
|
|
|
553
562
|
const identities = await provisionDevTestBotIdentities({
|
|
563
|
+
outputFs,
|
|
554
564
|
outputRoot,
|
|
555
565
|
runId,
|
|
556
566
|
count: plan.identityCount,
|
|
@@ -612,6 +622,7 @@ export async function runDevTestBotPhase({
|
|
|
612
622
|
execute: plan.execute,
|
|
613
623
|
recordVideo: plan.recordVideo,
|
|
614
624
|
}, {
|
|
625
|
+
outputFs,
|
|
615
626
|
targetPath: rootPath,
|
|
616
627
|
outputRoot,
|
|
617
628
|
runId,
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
import fsp from "node:fs/promises";
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import crypto from "node:crypto";
|
|
25
|
+
import { resolveOutputContext } from "../config/service.js";
|
|
26
|
+
import { requireOutputFileSystem } from "../config/output-filesystem.js";
|
|
25
27
|
|
|
26
28
|
import { routeFilesToPersonas, summarizeRouting } from "./investor-dd-file-router.js";
|
|
27
29
|
import { runAllPersonas } from "./investor-dd-persona-runner.js";
|
|
@@ -160,9 +162,10 @@ async function collectFileMetrics(rootPath, files) {
|
|
|
160
162
|
return metrics;
|
|
161
163
|
}
|
|
162
164
|
|
|
163
|
-
async function
|
|
164
|
-
|
|
165
|
-
await
|
|
165
|
+
async function writeJsonWithFs(outputFs, filePath, obj) {
|
|
166
|
+
requireOutputFileSystem(outputFs);
|
|
167
|
+
await outputFs.mkdir(path.dirname(filePath), { recursive: true });
|
|
168
|
+
await outputFs.writeFile(filePath, JSON.stringify(obj, null, 2), "utf-8");
|
|
166
169
|
}
|
|
167
170
|
|
|
168
171
|
function sha256(text) {
|
|
@@ -341,17 +344,20 @@ export async function runInvestorDd({
|
|
|
341
344
|
|
|
342
345
|
const runId = `investor-dd-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
|
|
343
346
|
const resolvedBudget = resolveInvestorDdBudget(budgetOptions);
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
:
|
|
347
|
+
const explicitOutputDir = String(outputDir || "").trim();
|
|
348
|
+
const { outputRoot, outputFs } = await resolveOutputContext({
|
|
349
|
+
cwd: rootPath,
|
|
350
|
+
outputDirOverride: explicitOutputDir || ".sentinelayer",
|
|
351
|
+
});
|
|
352
|
+
const writeJson = (...args) => writeJsonWithFs(outputFs, ...args);
|
|
353
|
+
const artifactBase = explicitOutputDir
|
|
354
|
+
? path.join(outputRoot, runId, INVESTOR_DD_ARTIFACT_SUBDIR)
|
|
355
|
+
: path.join(outputRoot, "runs", runId, INVESTOR_DD_ARTIFACT_SUBDIR);
|
|
347
356
|
const runRoot = path.dirname(artifactBase);
|
|
348
|
-
|
|
349
|
-
? path.resolve(outputDir)
|
|
350
|
-
: path.resolve(rootPath, ".sentinelayer");
|
|
351
|
-
await fsp.mkdir(artifactBase, { recursive: true });
|
|
357
|
+
await outputFs.mkdir(artifactBase, { recursive: true });
|
|
352
358
|
|
|
353
359
|
const streamPath = path.join(artifactBase, "stream.ndjson");
|
|
354
|
-
const streamHandle = await
|
|
360
|
+
const streamHandle = await outputFs.open(streamPath, "w");
|
|
355
361
|
let streamClosePromise = null;
|
|
356
362
|
|
|
357
363
|
const closeStream = () => {
|
|
@@ -410,6 +416,7 @@ export async function runInvestorDd({
|
|
|
410
416
|
maxRuntimeMs: resolvedBudget.maxRuntimeMinutes * 60_000,
|
|
411
417
|
});
|
|
412
418
|
const runResult = await runAllPersonas({
|
|
419
|
+
outputFs,
|
|
413
420
|
routing,
|
|
414
421
|
rootPath,
|
|
415
422
|
budget: budgetState,
|
|
@@ -434,6 +441,7 @@ export async function runInvestorDd({
|
|
|
434
441
|
});
|
|
435
442
|
|
|
436
443
|
devTestBotPhase = await runDevTestBotPhase({
|
|
444
|
+
outputFs,
|
|
437
445
|
runId,
|
|
438
446
|
rootPath,
|
|
439
447
|
outputRoot,
|
|
@@ -539,7 +547,7 @@ export async function runInvestorDd({
|
|
|
539
547
|
|
|
540
548
|
const markdown = buildSummaryMarkdown({ runId, summary, routing, byPersona });
|
|
541
549
|
const reportPath = path.join(artifactBase, "report.md");
|
|
542
|
-
await
|
|
550
|
+
await outputFs.writeFile(reportPath, markdown, "utf-8");
|
|
543
551
|
|
|
544
552
|
const htmlReport = renderInvestorDdHtml({
|
|
545
553
|
runId,
|
|
@@ -549,7 +557,7 @@ export async function runInvestorDd({
|
|
|
549
557
|
findings,
|
|
550
558
|
compliance: compliance ? compliance.packs : null,
|
|
551
559
|
});
|
|
552
|
-
await
|
|
560
|
+
await outputFs.writeFile(path.join(artifactBase, "report.html"), htmlReport, "utf-8");
|
|
553
561
|
|
|
554
562
|
emit({
|
|
555
563
|
type: "investor_dd_complete",
|
|
@@ -4,7 +4,8 @@ import fsp from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
6
|
import { buildScrubbedEnv } from "../agents/shared-tools/shell.js";
|
|
7
|
-
import {
|
|
7
|
+
import { resolveOutputContext } from "../config/service.js";
|
|
8
|
+
import { bindOutputFileSystem, requireOutputFileSystem } from "../config/output-filesystem.js";
|
|
8
9
|
import { resolveCodebaseIngest } from "../ingest/engine.js";
|
|
9
10
|
import { runSpecBindingChecks } from "./spec-binding.js";
|
|
10
11
|
|
|
@@ -852,9 +853,10 @@ export function staticCheckEnv(env = process.env) {
|
|
|
852
853
|
return { ...buildScrubbedEnv(env), CI: "1", FORCE_COLOR: "0" };
|
|
853
854
|
}
|
|
854
855
|
|
|
855
|
-
async function executeStaticCheck({ check, targetPath, runDir } = {}) {
|
|
856
|
+
async function executeStaticCheck({ check, targetPath, runDir, outputFs } = {}) {
|
|
857
|
+
requireOutputFileSystem(outputFs);
|
|
856
858
|
const checksDir = path.join(runDir, "checks");
|
|
857
|
-
await
|
|
859
|
+
await outputFs.mkdir(checksDir, { recursive: true });
|
|
858
860
|
|
|
859
861
|
const startedAt = Date.now();
|
|
860
862
|
const result = spawnSync(check.command, check.args, {
|
|
@@ -869,8 +871,8 @@ async function executeStaticCheck({ check, targetPath, runDir } = {}) {
|
|
|
869
871
|
const stderr = String(result.stderr || "");
|
|
870
872
|
const stdoutPath = path.join(checksDir, `${check.id}.stdout.log`);
|
|
871
873
|
const stderrPath = path.join(checksDir, `${check.id}.stderr.log`);
|
|
872
|
-
await
|
|
873
|
-
await
|
|
874
|
+
await outputFs.writeFile(stdoutPath, stdout, "utf-8");
|
|
875
|
+
await outputFs.writeFile(stderrPath, stderr, "utf-8");
|
|
874
876
|
|
|
875
877
|
if (result.error && result.error.code === "ENOENT") {
|
|
876
878
|
return {
|
|
@@ -933,7 +935,8 @@ async function executeStaticCheck({ check, targetPath, runDir } = {}) {
|
|
|
933
935
|
|
|
934
936
|
export const __localReviewForTests = Object.freeze({ executeStaticCheck });
|
|
935
937
|
|
|
936
|
-
async function runStaticAnalysisLayer({ targetPath, ingest, runDir, maxFindings = MAX_FINDINGS } = {}) {
|
|
938
|
+
async function runStaticAnalysisLayer({ targetPath, ingest, runDir, outputFs, maxFindings = MAX_FINDINGS } = {}) {
|
|
939
|
+
requireOutputFileSystem(outputFs);
|
|
937
940
|
const checks = buildStaticChecks(ingest);
|
|
938
941
|
const results = [];
|
|
939
942
|
const findings = [];
|
|
@@ -943,6 +946,7 @@ async function runStaticAnalysisLayer({ targetPath, ingest, runDir, maxFindings
|
|
|
943
946
|
check,
|
|
944
947
|
targetPath,
|
|
945
948
|
runDir,
|
|
949
|
+
outputFs,
|
|
946
950
|
});
|
|
947
951
|
results.push(result);
|
|
948
952
|
|
|
@@ -1145,12 +1149,13 @@ ${formatDeterministicFindingsMarkdown(result.findings)}
|
|
|
1145
1149
|
`;
|
|
1146
1150
|
}
|
|
1147
1151
|
|
|
1148
|
-
async function writeDeterministicReviewArtifacts({ result, runDir } = {}) {
|
|
1152
|
+
async function writeDeterministicReviewArtifacts({ result, runDir, outputFs } = {}) {
|
|
1153
|
+
requireOutputFileSystem(outputFs);
|
|
1149
1154
|
const markdownPath = path.join(runDir, "REVIEW_DETERMINISTIC.md");
|
|
1150
1155
|
const jsonPath = path.join(runDir, "REVIEW_DETERMINISTIC.json");
|
|
1151
1156
|
|
|
1152
|
-
await
|
|
1153
|
-
await
|
|
1157
|
+
await outputFs.writeFile(markdownPath, `${buildDeterministicReviewMarkdown(result).trim()}\n`, "utf-8");
|
|
1158
|
+
await outputFs.writeFile(jsonPath, `${JSON.stringify(result, null, 2)}\n`, "utf-8");
|
|
1154
1159
|
|
|
1155
1160
|
return {
|
|
1156
1161
|
markdownPath,
|
|
@@ -1159,6 +1164,7 @@ async function writeDeterministicReviewArtifacts({ result, runDir } = {}) {
|
|
|
1159
1164
|
}
|
|
1160
1165
|
|
|
1161
1166
|
export async function runDeterministicReviewPipeline({
|
|
1167
|
+
outputFs: inheritedOutputFs,
|
|
1162
1168
|
targetPath,
|
|
1163
1169
|
mode = "full",
|
|
1164
1170
|
outputDir = "",
|
|
@@ -1170,17 +1176,19 @@ export async function runDeterministicReviewPipeline({
|
|
|
1170
1176
|
allowedModes: ["full", "diff", "staged"],
|
|
1171
1177
|
});
|
|
1172
1178
|
|
|
1173
|
-
const outputRoot = await
|
|
1179
|
+
const { outputRoot, outputFs: resolvedOutputFs } = await resolveOutputContext({
|
|
1174
1180
|
cwd: normalizedTargetPath,
|
|
1175
1181
|
outputDirOverride: outputDir,
|
|
1176
1182
|
});
|
|
1183
|
+
const outputFs = inheritedOutputFs === undefined ? resolvedOutputFs : requireOutputFileSystem(inheritedOutputFs);
|
|
1177
1184
|
const runId = `review-${formatTimestampForFile()}-${randomUUID().slice(0, 8)}`;
|
|
1178
1185
|
const runDir = path.join(outputRoot, "reviews", runId);
|
|
1179
|
-
await
|
|
1186
|
+
await outputFs.mkdir(runDir, { recursive: true });
|
|
1180
1187
|
|
|
1181
1188
|
const ingestResolution = await resolveCodebaseIngest({
|
|
1182
1189
|
rootPath: normalizedTargetPath,
|
|
1183
1190
|
outputDir,
|
|
1191
|
+
outputFs,
|
|
1184
1192
|
refresh: Boolean(refreshIngest),
|
|
1185
1193
|
});
|
|
1186
1194
|
const ingest = ingestResolution.ingest;
|
|
@@ -1221,6 +1229,7 @@ export async function runDeterministicReviewPipeline({
|
|
|
1221
1229
|
targetPath: normalizedTargetPath,
|
|
1222
1230
|
ingest,
|
|
1223
1231
|
runDir,
|
|
1232
|
+
outputFs,
|
|
1224
1233
|
maxFindings: remainingBudget,
|
|
1225
1234
|
});
|
|
1226
1235
|
|
|
@@ -1302,14 +1311,15 @@ export async function runDeterministicReviewPipeline({
|
|
|
1302
1311
|
const artifacts = await writeDeterministicReviewArtifacts({
|
|
1303
1312
|
result,
|
|
1304
1313
|
runDir,
|
|
1314
|
+
outputFs,
|
|
1305
1315
|
});
|
|
1306
1316
|
|
|
1307
1317
|
return {
|
|
1308
1318
|
...result,
|
|
1309
|
-
artifacts: {
|
|
1319
|
+
artifacts: bindOutputFileSystem({
|
|
1310
1320
|
runDirectory: runDir,
|
|
1311
1321
|
...artifacts,
|
|
1312
|
-
},
|
|
1322
|
+
}, outputFs),
|
|
1313
1323
|
};
|
|
1314
1324
|
}
|
|
1315
1325
|
|
|
@@ -1363,13 +1373,13 @@ export async function writeReviewReport({
|
|
|
1363
1373
|
outputDir = "",
|
|
1364
1374
|
reportMarkdown,
|
|
1365
1375
|
} = {}) {
|
|
1366
|
-
const outputRoot = await
|
|
1376
|
+
const { outputRoot, outputFs } = await resolveOutputContext({
|
|
1367
1377
|
cwd: targetPath,
|
|
1368
1378
|
outputDirOverride: outputDir,
|
|
1369
1379
|
});
|
|
1370
1380
|
const reportDir = path.join(outputRoot, "reports");
|
|
1371
|
-
await
|
|
1381
|
+
await outputFs.mkdir(reportDir, { recursive: true });
|
|
1372
1382
|
const reportPath = path.join(reportDir, `review-scan-${mode}-${formatTimestampForFile()}.md`);
|
|
1373
|
-
await
|
|
1383
|
+
await outputFs.writeFile(reportPath, `${String(reportMarkdown || "").trim()}\n`, "utf-8");
|
|
1374
1384
|
return reportPath;
|
|
1375
1385
|
}
|