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.
Files changed (137) hide show
  1. package/README.md +63 -3
  2. package/package.json +12 -7
  3. package/skills/omar-gate-setup/SKILL.md +103 -0
  4. package/skills/senti-agent-identity/SKILL.md +80 -0
  5. package/skills/senti-judge/SKILL.md +139 -0
  6. package/skills/senti-room-notifications/SKILL.md +73 -0
  7. package/skills/senti-room-notifications/listen-wrapper.cjs +150 -0
  8. package/skills/senti-room-notifications/package.json +1 -0
  9. package/skills/senti-room-notifications/test-classifier.js +109 -0
  10. package/skills/senti-room-notifications/test-receipt.js +54 -0
  11. package/skills/sentinelayer-cli/SKILL.md +222 -0
  12. package/src/agents/devtestbot/runner.js +22 -11
  13. package/src/agents/devtestbot/tool.js +24 -11
  14. package/src/agents/jules/fix-cycle.js +7 -1
  15. package/src/agents/jules/tools/runtime-audit.js +17 -12
  16. package/src/agents/shared-tools/dispatch-core.js +10 -5
  17. package/src/ai/aidenid.js +4 -2
  18. package/src/ai/domain-target-store.js +11 -5
  19. package/src/ai/identity-store.js +11 -5
  20. package/src/ai/site-store.js +8 -4
  21. package/src/audit/orchestrator.js +21 -14
  22. package/src/audit/package.js +6 -4
  23. package/src/audit/persona-loop.js +7 -0
  24. package/src/audit/replay.js +4 -2
  25. package/src/auth/gate.js +46 -8
  26. package/src/auth/service.js +34 -15
  27. package/src/auth/session-store.js +105 -47
  28. package/src/board/evidence-resolvers.js +3 -4
  29. package/src/cli.js +23 -3
  30. package/src/commands/ai/identity-lifecycle.js +30 -26
  31. package/src/commands/ai/provision-governance.js +32 -24
  32. package/src/commands/ai/shared.js +6 -4
  33. package/src/commands/audit.js +14 -12
  34. package/src/commands/auth.js +24 -4
  35. package/src/commands/board.js +2 -2
  36. package/src/commands/chat.js +8 -6
  37. package/src/commands/judge.js +125 -0
  38. package/src/commands/legacy-args.js +1 -0
  39. package/src/commands/mcp.js +45 -52
  40. package/src/commands/omargate.js +2 -1
  41. package/src/commands/plugin.js +11 -3
  42. package/src/commands/review.js +9 -4
  43. package/src/commands/scan.js +7 -7
  44. package/src/commands/session.js +607 -57
  45. package/src/commands/skills.js +45 -0
  46. package/src/commands/watch.js +9 -7
  47. package/src/config/output-filesystem.js +156 -0
  48. package/src/config/schema.js +4 -2
  49. package/src/config/service.js +40 -4
  50. package/src/cost/history.js +15 -9
  51. package/src/daemon/artifact-lineage.js +15 -12
  52. package/src/daemon/assignment-ledger.js +31 -25
  53. package/src/daemon/ast-drift.js +7 -3
  54. package/src/daemon/budget-governor.js +26 -20
  55. package/src/daemon/error-worker.js +28 -22
  56. package/src/daemon/fix-cycle.js +5 -4
  57. package/src/daemon/hybrid-mapper.js +19 -16
  58. package/src/daemon/ingest-refresh.js +4 -0
  59. package/src/daemon/jira-lifecycle.js +21 -17
  60. package/src/daemon/operator-control.js +18 -15
  61. package/src/daemon/reliability-lane.js +18 -15
  62. package/src/daemon/scope-engine.js +12 -8
  63. package/src/daemon/watchdog.js +15 -12
  64. package/src/engram/build-state.js +24 -3
  65. package/src/engram/capability-index.js +367 -0
  66. package/src/engram/compile-graph.js +1063 -0
  67. package/src/engram/contracts/action-receipt.js +82 -0
  68. package/src/engram/contracts/capability-index.js +259 -0
  69. package/src/engram/contracts/capability.js +377 -0
  70. package/src/engram/contracts/content-hash.js +8 -0
  71. package/src/engram/contracts/evidence.js +258 -0
  72. package/src/engram/contracts/graph.js +594 -0
  73. package/src/engram/contracts/identity.js +8 -0
  74. package/src/engram/contracts/index.js +149 -0
  75. package/src/engram/contracts/media-asset.js +85 -0
  76. package/src/engram/contracts/money.js +249 -0
  77. package/src/engram/contracts/source-snapshot.js +57 -0
  78. package/src/engram/contracts/task-context-pack.js +114 -0
  79. package/src/engram/contracts/temporal-segment.js +72 -0
  80. package/src/engram/contracts/versions.js +96 -0
  81. package/src/engram/extract-html.js +692 -0
  82. package/src/engram/graph-store.js +877 -0
  83. package/src/engram/index.js +98 -14
  84. package/src/engram/ingest-site.js +132 -0
  85. package/src/engram/live-state.js +1180 -0
  86. package/src/engram/namespace.js +34 -1
  87. package/src/engram/retrieve-site.js +753 -0
  88. package/src/engram/safe-reader.js +597 -0
  89. package/src/engram/source-intake.js +251 -0
  90. package/src/engram/store.js +227 -9
  91. package/src/ingest/engine.js +21 -28
  92. package/src/judge/verdict.js +305 -0
  93. package/src/legacy-cli.js +96 -19
  94. package/src/mcp/bridge-inputs.js +28 -4
  95. package/src/mcp/cli-command-tools.js +70 -0
  96. package/src/mcp/registry.js +17 -13
  97. package/src/mcp/session-stdio-server.js +297 -19
  98. package/src/memory/blackboard.js +5 -4
  99. package/src/net/gh-api-path.js +35 -9
  100. package/src/net/test-egress-guard.js +16 -4
  101. package/src/plugin/manifest.js +12 -8
  102. package/src/review/ai-review.js +110 -10
  103. package/src/review/investor-dd-devtestbot.js +17 -6
  104. package/src/review/investor-dd-orchestrator.js +21 -13
  105. package/src/review/local-review.js +26 -16
  106. package/src/review/omargate-cache.js +5 -5
  107. package/src/review/omargate-orchestrator.js +89 -17
  108. package/src/review/replay.js +9 -4
  109. package/src/review/report.js +22 -13
  110. package/src/review/typesafe-severity.js +201 -0
  111. package/src/session/admission-auth.js +34 -7
  112. package/src/session/admission.js +322 -71
  113. package/src/session/agent-access.js +137 -0
  114. package/src/session/agent-registry.js +2 -1
  115. package/src/session/attachment-summary.js +77 -0
  116. package/src/session/edit-summary.js +19 -0
  117. package/src/session/invitations.js +65 -1
  118. package/src/session/mentions.js +17 -9
  119. package/src/session/message-edits.js +5 -4
  120. package/src/session/preview.js +10 -2
  121. package/src/session/recap.js +3 -2
  122. package/src/session/room-files.js +519 -0
  123. package/src/session/route-classes.js +22 -8
  124. package/src/session/setup-guides.js +4 -0
  125. package/src/session/standup.js +192 -0
  126. package/src/session/sync.js +229 -10
  127. package/src/session/tickets.js +41 -0
  128. package/src/session/transcript.js +2 -1
  129. package/src/session/wake/resolve-target.js +7 -3
  130. package/src/skills/listener-runtime.cjs +92 -0
  131. package/src/skills/service.js +207 -0
  132. package/src/swarm/factory.js +5 -5
  133. package/src/swarm/pentest.js +9 -9
  134. package/src/swarm/report.js +4 -4
  135. package/src/swarm/runtime.js +15 -12
  136. package/src/swarm/scenario-dsl.js +7 -5
  137. package/src/telemetry/ledger.js +16 -7
@@ -1,7 +1,7 @@
1
1
  import fsp from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
- import { resolveOutputRoot } from "../config/service.js";
4
+ import { resolveOutputRoot, resolveOutputContext } from "../config/service.js";
5
5
 
6
6
  const CACHE_SCHEMA_VERSION = "1.0.0";
7
7
  const CACHE_KIND = "omargate-deterministic-cache";
@@ -178,7 +178,7 @@ export async function writeOmarGateDeterministicCache({
178
178
  reportPath = "",
179
179
  } = {}) {
180
180
  const normalizedTargetPath = normalizeTargetPath(targetPath);
181
- const outputRoot = await resolveOutputRoot({
181
+ const { outputRoot, outputFs } = await resolveOutputContext({
182
182
  cwd: normalizedTargetPath,
183
183
  outputDirOverride: outputDir,
184
184
  env: process.env,
@@ -191,7 +191,7 @@ export async function writeOmarGateDeterministicCache({
191
191
  const runDirectory = path.join(outputRoot, "runs", normalizedRunId);
192
192
  const artifactPath = path.join(runDirectory, "deterministic.json");
193
193
  const latestPath = path.join(outputRoot, "runs", LATEST_INDEX_NAME);
194
- await fsp.mkdir(runDirectory, { recursive: true });
194
+ await outputFs.mkdir(runDirectory, { recursive: true });
195
195
 
196
196
  const cache = {
197
197
  schemaVersion: CACHE_SCHEMA_VERSION,
@@ -212,8 +212,8 @@ export async function writeOmarGateDeterministicCache({
212
212
  reportPath: normalizeString(reportPath),
213
213
  },
214
214
  };
215
- await fsp.writeFile(artifactPath, `${JSON.stringify(cache, null, 2)}\n`, "utf-8");
216
- await fsp.writeFile(
215
+ await outputFs.writeFile(artifactPath, `${JSON.stringify(cache, null, 2)}\n`, "utf-8");
216
+ await outputFs.writeFile(
217
217
  latestPath,
218
218
  `${JSON.stringify(
219
219
  {
@@ -9,6 +9,7 @@ import { randomUUID } from "node:crypto";
9
9
  import path from "node:path";
10
10
 
11
11
  import { runAiReviewLayer } from "./ai-review.js";
12
+ import { summarizeSeverityEvaluations, normalizeSeverityClassifier, parseSeverityEvaluationMaxCost } from "./typesafe-severity.js";
12
13
  import { buildPersonaReviewPrompt, PERSONA_IDS } from "./persona-prompts.js";
13
14
  import { resolveFilteredPersonas, resolveScanMode } from "./scan-modes.js";
14
15
  import { reconcileReviewFindings } from "./report.js";
@@ -388,6 +389,13 @@ function resolveAiResultCostUsd(result = {}) {
388
389
  );
389
390
  }
390
391
 
392
+ function incompleteReviewReason(result = {}) {
393
+ if (result.reviewValidity?.valid === false || result.parser === "fallback_text" || !Array.isArray(result.findings)) {
394
+ return "invalid_review_response";
395
+ }
396
+ return result.budget?.blocking === true ? "budget_blocked" : null;
397
+ }
398
+
391
399
  function buildAiCallEvidence(result = {}, { dryRun = false } = {}) {
392
400
  if (dryRun) {
393
401
  return {
@@ -462,11 +470,15 @@ function buildPersonaHealthFindings(personaHealth = {}) {
462
470
  const aiCallEvidence = personaHealth.aiCallEvidence || {};
463
471
  const okCount = Number(personaHealth.ok || 0);
464
472
  const unverifiedOkCount = Number(personaHealth.unverifiedOk || 0);
473
+ const skippedCount = Number(personaHealth.skipped || 0);
474
+ const incompleteCount = Number(personaHealth.incomplete || 0);
465
475
  const allPersonasFailed = total > 0 && errorCount === total;
466
476
  const severity = allPersonasFailed ? "P0" : "P1";
467
477
  let message = `Omar AI coverage degraded: ${errorCount}/${total} dispatched personas errored (${Math.round(errorRatio * 100)}%).`;
468
478
  if (allPersonasFailed) {
469
479
  message = `Omar AI coverage failed closed: all ${total} dispatched personas errored before producing findings.`;
480
+ } else if (skippedCount > 0 || incompleteCount > 0) {
481
+ message = `Omar AI coverage incomplete: ${skippedCount}/${total} requested personas were skipped and ${incompleteCount}/${total} returned incomplete partition, budget, or structured-response coverage.`;
470
482
  } else if (errorCount === 0 && unverifiedOkCount > 0) {
471
483
  message = `Omar AI coverage failed closed: ${unverifiedOkCount}/${okCount} successful personas lacked token-bearing usage, billing-ledger, or priced-call evidence.`;
472
484
  }
@@ -484,12 +496,13 @@ function buildPersonaHealthFindings(personaHealth = {}) {
484
496
  confidenceFloor: 0,
485
497
  sources: ["orchestrator"],
486
498
  remediation:
487
- "Fix SentinelLayer managed LLM proxy/provider credentials or run with --no-ai when deterministic-only coverage is intentional.",
499
+ "Resolve the reported provider, budget, or usage-evidence failure and rerun every requested persona and partition. Partial coverage cannot satisfy a full AI review gate.",
488
500
  evidence: warnings,
489
501
  personaHealth: {
490
502
  ok: Number(personaHealth.ok || 0),
491
503
  error: errorCount,
492
- skipped: Number(personaHealth.skipped || 0),
504
+ skipped: skippedCount,
505
+ incomplete: incompleteCount,
493
506
  total,
494
507
  errorRatio,
495
508
  totalCostUsd,
@@ -562,6 +575,7 @@ async function runOmarPersonaSwarm({
562
575
  outputDir,
563
576
  provider,
564
577
  model,
578
+ severityClassifier = "off",
565
579
  perPersonaCost,
566
580
  dryRun,
567
581
  onEvent,
@@ -670,6 +684,7 @@ async function runOmarPersonaSwarm({
670
684
  outputDir,
671
685
  provider: provider || undefined,
672
686
  model: model || undefined,
687
+ ...(severityClassifier === "typesafe" ? { severityClassifier } : {}),
673
688
  sessionId: usageSessionId || `${subagentRunId}-ai`,
674
689
  maxCostUsd: budget.maxCostUsd,
675
690
  systemPrompt,
@@ -717,6 +732,7 @@ async function runOmarPersonaSwarm({
717
732
  });
718
733
  const costUsd = resolveAiResultCostUsd(result);
719
734
  const aiCallEvidence = buildAiCallEvidence(result, { dryRun });
735
+ const incompleteReason = incompleteReviewReason(result);
720
736
 
721
737
  if (onEvent) {
722
738
  for (const finding of findings) {
@@ -739,6 +755,7 @@ async function runOmarPersonaSwarm({
739
755
  partitionCount: partitions.length,
740
756
  fileCount: scopedFiles.length,
741
757
  findings: findings.length,
758
+ status: incompleteReason ? "incomplete" : "ok",
742
759
  summary: result?.summary || summarizeFindings(findings),
743
760
  costUsd,
744
761
  durationMs: Date.now() - subagentStart,
@@ -753,7 +770,7 @@ async function runOmarPersonaSwarm({
753
770
  }
754
771
 
755
772
  return {
756
- status: "ok",
773
+ status: incompleteReason ? "incomplete" : "ok",
757
774
  subagentIndex,
758
775
  agentId: subagentIdentity.id,
759
776
  files: scopedFiles,
@@ -761,8 +778,11 @@ async function runOmarPersonaSwarm({
761
778
  summary: result?.summary || summarizeFindings(findings),
762
779
  costUsd,
763
780
  aiCallEvidence,
781
+ reason: incompleteReason,
782
+ reviewValidity: result.reviewValidity || null,
764
783
  model: result?.model || model || null,
765
784
  billing: result?.billing || null,
785
+ ...(severityClassifier === "typesafe" ? { severityClassification: result?.severityClassification || null } : {}),
766
786
  artifacts: result?.artifacts || null,
767
787
  durationMs: Date.now() - subagentStart,
768
788
  };
@@ -827,6 +847,14 @@ async function runOmarPersonaSwarm({
827
847
  const summary = summarizeFindings(findings);
828
848
  const okCount = settledSubagents.filter((result) => result.status === "ok").length;
829
849
  const errorCount = settledSubagents.filter((result) => result.status === "error").length;
850
+ const completedCount = settledSubagents.filter((result) =>
851
+ result.status === "ok" && (dryRun || Number(result.aiCallEvidence?.confirmedCalls || 0) > 0)
852
+ ).length;
853
+ const coverage = {
854
+ requested: partitions.length,
855
+ completed: completedCount,
856
+ incomplete: partitions.length - completedCount,
857
+ };
830
858
  const usageLedgerErrors = settledSubagents.filter((result) =>
831
859
  result.status === "error" && isUsageLedgerFailure(result.error)
832
860
  );
@@ -875,6 +903,7 @@ async function runOmarPersonaSwarm({
875
903
  subagentCount: settledSubagents.length,
876
904
  ok: okCount,
877
905
  error: errorCount,
906
+ coverage,
878
907
  findings: findings.length,
879
908
  summary,
880
909
  totalCostUsd,
@@ -892,20 +921,22 @@ async function runOmarPersonaSwarm({
892
921
 
893
922
  return {
894
923
  personaId,
895
- status: okCount > 0 ? "ok" : "error",
924
+ ...(severityClassifier === "typesafe" ? { severityClassification: summarizeSeverityEvaluations(settledSubagents) } : {}),
925
+ status: coverage.incomplete === 0 ? "ok" : errorCount === partitions.length ? "error" : "incomplete",
896
926
  findings,
897
927
  summary,
898
928
  costUsd: totalCostUsd,
899
929
  aiCallEvidence,
900
930
  model: model || null,
901
931
  durationMs: Date.now() - startedAt,
902
- error: okCount > 0 ? null : settledSubagents.find((result) => result.error)?.error || null,
932
+ error: coverage.incomplete === 0 ? null : settledSubagents.find((result) => result.error)?.error || "Requested partition coverage is incomplete.",
903
933
  swarm: {
904
934
  runId: swarmRunId,
905
935
  decision,
906
936
  subagentCount: settledSubagents.length,
907
937
  ok: okCount,
908
938
  error: errorCount,
939
+ coverage,
909
940
  partitionSizes: partitions.map((files) => files.length),
910
941
  blackboardEntries: blackboard.length,
911
942
  subagents: settledSubagents.map((result) => ({
@@ -919,6 +950,9 @@ async function runOmarPersonaSwarm({
919
950
  billing: result.billing || null,
920
951
  durationMs: result.durationMs || 0,
921
952
  error: result.error || null,
953
+ reason: result.reason || null,
954
+ reviewValidity: result.reviewValidity || null,
955
+ verified: dryRun ? null : Number(result.aiCallEvidence?.confirmedCalls || 0) > 0,
922
956
  artifacts: result.artifacts || null,
923
957
  })),
924
958
  },
@@ -934,7 +968,7 @@ async function runOmarPersonaSwarm({
934
968
  * @param {number} [options.maxParallel] - Max concurrent persona calls (default 4)
935
969
  * @param {string} [options.provider] - LLM provider override
936
970
  * @param {string} [options.model] - LLM model override
937
- * @param {number} [options.maxCostUsd] - Global cost ceiling (default 5.0)
971
+ * @param {number} [options.maxCostUsd] - Completed-persona cost stop threshold (default 5.0), not a pre-dispatch spending cap
938
972
  * @param {boolean} [options.dryRun] - Dry-run mode (no LLM calls)
939
973
  * @param {string} [options.outputDir] - Output directory override
940
974
  * @param {object} [options.deterministic] - Deterministic scan results
@@ -951,6 +985,7 @@ export async function runOmarGateOrchestrator({
951
985
  maxParallel = 4,
952
986
  provider = "",
953
987
  model = "",
988
+ severityClassifier = "off",
954
989
  maxCostUsd = 5.0,
955
990
  dryRun = false,
956
991
  outputDir = "",
@@ -963,6 +998,8 @@ export async function runOmarGateOrchestrator({
963
998
  usageRecorder = undefined,
964
999
  aiReviewRunner = runAiReviewLayer,
965
1000
  } = {}) {
1001
+ normalizeSeverityClassifier(severityClassifier);
1002
+ if (severityClassifier === "typesafe") maxCostUsd = parseSeverityEvaluationMaxCost(maxCostUsd, 5);
966
1003
  const runId = `omargate-${Date.now()}-${randomUUID().slice(0, 8)}`;
967
1004
  const startTime = Date.now();
968
1005
 
@@ -980,6 +1017,12 @@ export async function runOmarGateOrchestrator({
980
1017
  const { mode, personas: basePersonas } = resolved;
981
1018
  const droppedPersonas = resolved.dropped || [];
982
1019
  const unknownPersonas = resolved.unknown || [];
1020
+ if (unknownPersonas.length > 0) {
1021
+ throw new Error("Unknown requested Omar persona IDs; the requested review cannot be completed.");
1022
+ }
1023
+ if (basePersonas.length === 0) {
1024
+ throw new Error("Requested Omar persona roster is empty; AI coverage cannot be completed.");
1025
+ }
983
1026
 
984
1027
  if (onEvent && (droppedPersonas.length > 0 || unknownPersonas.length > 0)) {
985
1028
  onEvent(createAgentEvent({
@@ -1003,6 +1046,9 @@ export async function runOmarGateOrchestrator({
1003
1046
  manualInclude: Array.isArray(includeOnly) && includeOnly.length > 0,
1004
1047
  });
1005
1048
  const personas = personaRouting.effectivePersonas;
1049
+ if (personas.length === 0) {
1050
+ throw new Error("Effective Omar persona roster is empty; AI coverage cannot be completed.");
1051
+ }
1006
1052
 
1007
1053
  if (onEvent && personaRouting.enabled) {
1008
1054
  onEvent(createAgentEvent({
@@ -1052,7 +1098,8 @@ export async function runOmarGateOrchestrator({
1052
1098
  const detFindings = deterministic?.findings || [];
1053
1099
 
1054
1100
  // Per-persona cost budget = global / effective persona count (with minimum floor).
1055
- const perPersonaCost = personas.length > 0 ? Math.max(0.25, maxCostUsd / personas.length) : 0;
1101
+ const evaluationWithoutCostCap = severityClassifier === "typesafe" && maxCostUsd === 0;
1102
+ const perPersonaCost = evaluationWithoutCostCap ? 0 : personas.length > 0 ? Math.max(0.25, maxCostUsd / personas.length) : 0;
1056
1103
  let runningCostUsd = 0;
1057
1104
 
1058
1105
  const personaResults = await runWithConcurrency(personas, maxParallel, async (personaId) => {
@@ -1067,7 +1114,7 @@ export async function runOmarGateOrchestrator({
1067
1114
  };
1068
1115
 
1069
1116
  // Global budget check — skip remaining personas if exhausted
1070
- if (runningCostUsd >= maxCostUsd) {
1117
+ if (!evaluationWithoutCostCap && runningCostUsd >= maxCostUsd) {
1071
1118
  if (onEvent) {
1072
1119
  onEvent(createAgentEvent({
1073
1120
  event: "persona_skipped",
@@ -1128,6 +1175,7 @@ export async function runOmarGateOrchestrator({
1128
1175
  outputDir,
1129
1176
  provider,
1130
1177
  model,
1178
+ severityClassifier,
1131
1179
  perPersonaCost,
1132
1180
  dryRun,
1133
1181
  onEvent,
@@ -1151,6 +1199,7 @@ export async function runOmarGateOrchestrator({
1151
1199
  identity,
1152
1200
  error: swarmResult.error || "all subagents failed",
1153
1201
  swarm: swarmResult.swarm,
1202
+ status: swarmResult.status,
1154
1203
  },
1155
1204
  runId,
1156
1205
  }));
@@ -1163,6 +1212,7 @@ export async function runOmarGateOrchestrator({
1163
1212
  identity,
1164
1213
  findings: swarmResult.findings.length,
1165
1214
  summary: swarmResult.summary,
1215
+ status: swarmResult.status,
1166
1216
  costUsd: personaCost,
1167
1217
  durationMs: Date.now() - personaStart,
1168
1218
  swarm: swarmResult.swarm,
@@ -1203,6 +1253,7 @@ export async function runOmarGateOrchestrator({
1203
1253
  outputDir,
1204
1254
  provider: provider || undefined,
1205
1255
  model: model || undefined,
1256
+ ...(severityClassifier === "typesafe" ? { severityClassifier } : {}),
1206
1257
  sessionId: usageSessionId || `${runId}-${personaId}-ai`,
1207
1258
  maxCostUsd: perPersonaCost,
1208
1259
  systemPrompt,
@@ -1231,6 +1282,7 @@ export async function runOmarGateOrchestrator({
1231
1282
  }));
1232
1283
  const personaCost = resolveAiResultCostUsd(result);
1233
1284
  const aiCallEvidence = buildAiCallEvidence(result, { dryRun });
1285
+ const incompleteReason = incompleteReviewReason(result);
1234
1286
 
1235
1287
  if (onEvent) {
1236
1288
  for (const finding of findings) {
@@ -1262,6 +1314,7 @@ export async function runOmarGateOrchestrator({
1262
1314
  personaId,
1263
1315
  identity,
1264
1316
  findings: findings.length,
1317
+ status: incompleteReason ? "incomplete" : "ok",
1265
1318
  summary: result?.summary || {},
1266
1319
  costUsd: personaCost,
1267
1320
  durationMs: Date.now() - personaStart,
@@ -1274,13 +1327,16 @@ export async function runOmarGateOrchestrator({
1274
1327
 
1275
1328
  return {
1276
1329
  personaId,
1277
- status: "ok",
1330
+ status: incompleteReason ? "incomplete" : "ok",
1278
1331
  findings,
1279
1332
  summary: result?.summary || { P0: 0, P1: 0, P2: 0, P3: 0 },
1280
1333
  costUsd: personaCost,
1281
1334
  aiCallEvidence,
1335
+ reason: incompleteReason,
1336
+ reviewValidity: result.reviewValidity || null,
1282
1337
  model: result?.model || model || null,
1283
1338
  billing: result?.billing || null,
1339
+ ...(severityClassifier === "typesafe" ? { severityClassification: result?.severityClassification || null } : {}),
1284
1340
  artifacts: result?.artifacts || null,
1285
1341
  durationMs: Date.now() - personaStart,
1286
1342
  };
@@ -1366,6 +1422,7 @@ export async function runOmarGateOrchestrator({
1366
1422
  // billing can still carry token-bearing usage and provider-cost receipts.
1367
1423
  const personaErrorCount = settled.filter((r) => r.status === "error").length;
1368
1424
  const personaSkippedCount = settled.filter((r) => r.status === "skipped").length;
1425
+ const personaIncompleteCount = settled.filter((r) => r.status === "incomplete").length;
1369
1426
  const personaOkCount = settled.filter((r) => r.status === "ok").length;
1370
1427
  const personaVerifiedOkCount = settled.filter(
1371
1428
  (r) => r.status === "ok" && Number(r.aiCallEvidence?.confirmedCalls || 0) > 0
@@ -1373,34 +1430,44 @@ export async function runOmarGateOrchestrator({
1373
1430
  const personaUnverifiedOkCount = personaOkCount - personaVerifiedOkCount;
1374
1431
  const totalPersonas = settled.length;
1375
1432
  const errorRatio = totalPersonas > 0 ? personaErrorCount / totalPersonas : 0;
1376
- const aiCoverageHealthy =
1377
- totalPersonas === 0 ||
1378
- (personaOkCount > 0 && personaUnverifiedOkCount === 0 && errorRatio < 0.5 && !dryRun);
1433
+ const aiCoverageHealthy = personaOkCount === totalPersonas &&
1434
+ (dryRun || personaUnverifiedOkCount === 0);
1435
+ const coverage = {
1436
+ status: aiCoverageHealthy ? (dryRun ? "simulated" : "complete") : "incomplete",
1437
+ requestedPersonas: totalPersonas,
1438
+ completedPersonas: dryRun ? personaOkCount : personaVerifiedOkCount,
1439
+ };
1379
1440
 
1380
1441
  const personaHealth = {
1381
1442
  ok: personaOkCount,
1382
1443
  error: personaErrorCount,
1383
1444
  skipped: personaSkippedCount,
1445
+ incomplete: personaIncompleteCount,
1384
1446
  total: totalPersonas,
1385
1447
  verifiedOk: personaVerifiedOkCount,
1386
1448
  unverifiedOk: personaUnverifiedOkCount,
1387
1449
  errorRatio,
1388
1450
  totalCostUsd: totalCost,
1389
1451
  aiCallEvidence,
1390
- healthy: aiCoverageHealthy || dryRun,
1452
+ healthy: aiCoverageHealthy,
1391
1453
  warnings: [],
1392
1454
  };
1393
- if (!dryRun && totalPersonas > 0) {
1394
- if (personaOkCount === 0 && personaErrorCount > 0) {
1455
+ if (totalPersonas > 0) {
1456
+ if (personaErrorCount === totalPersonas) {
1395
1457
  personaHealth.warnings.push(
1396
1458
  `ALL ${totalPersonas} personas errored. AI coverage is ZERO. Re-check auth (sl auth login) or LLM proxy config.`
1397
1459
  );
1398
- } else if (errorRatio >= 0.5) {
1460
+ } else if (personaErrorCount > 0) {
1399
1461
  personaHealth.warnings.push(
1400
1462
  `${personaErrorCount}/${totalPersonas} personas errored (${Math.round(errorRatio * 100)}%). AI coverage is degraded.`
1401
1463
  );
1402
1464
  }
1403
- if (personaUnverifiedOkCount > 0) {
1465
+ if (personaSkippedCount > 0 || personaIncompleteCount > 0) {
1466
+ personaHealth.warnings.push(
1467
+ `${personaSkippedCount} requested personas skipped; ${personaIncompleteCount} returned incomplete partition, budget, or structured-response coverage. Full AI coverage is incomplete.`
1468
+ );
1469
+ }
1470
+ if (!dryRun && personaUnverifiedOkCount > 0) {
1404
1471
  personaHealth.warnings.push(
1405
1472
  `${personaUnverifiedOkCount}/${personaOkCount} successful personas lacked token-bearing usage, billing-ledger, or priced-call evidence.`
1406
1473
  );
@@ -1416,6 +1483,7 @@ export async function runOmarGateOrchestrator({
1416
1483
  const result = {
1417
1484
  runId,
1418
1485
  mode,
1486
+ ...(severityClassifier === "typesafe" ? { severityClassification: summarizeSeverityEvaluations(settled) } : {}),
1419
1487
  basePersonas,
1420
1488
  roster,
1421
1489
  personaRouting,
@@ -1430,11 +1498,15 @@ export async function runOmarGateOrchestrator({
1430
1498
  durationMs: r.durationMs,
1431
1499
  model: r.model || null,
1432
1500
  billing: r.billing || null,
1501
+ ...(severityClassifier === "typesafe" ? { severityClassification: r.severityClassification || null } : {}),
1433
1502
  error: r.error || null,
1503
+ reason: r.reason || null,
1504
+ reviewValidity: r.reviewValidity || null,
1434
1505
  swarm: r.swarm || null,
1435
1506
  artifacts: r.artifacts || null,
1436
1507
  })),
1437
1508
  personaHealth,
1509
+ coverage,
1438
1510
  findings: reconciledFindings,
1439
1511
  findingsBySource: {
1440
1512
  deterministic: detFindings.length,
@@ -1,3 +1,4 @@
1
+ import { requireOutputFileSystem } from "../config/output-filesystem.js";
1
2
  import { spawnSync } from "node:child_process";
2
3
  import fsp from "node:fs/promises";
3
4
  import path from "node:path";
@@ -74,6 +75,7 @@ function normalizeInvocation(invocation = {}) {
74
75
  }
75
76
 
76
77
  export async function writeReviewRunContext({
78
+ outputFs,
77
79
  runDirectory,
78
80
  runId,
79
81
  targetPath,
@@ -81,6 +83,7 @@ export async function writeReviewRunContext({
81
83
  invocation = {},
82
84
  replay = {},
83
85
  } = {}) {
86
+ requireOutputFileSystem(outputFs);
84
87
  const normalizedRunDirectory = path.resolve(String(runDirectory || "."));
85
88
  const context = {
86
89
  schemaVersion: "1.0.0",
@@ -96,8 +99,8 @@ export async function writeReviewRunContext({
96
99
  },
97
100
  };
98
101
  const contextPath = path.join(normalizedRunDirectory, REVIEW_RUN_CONTEXT_FILE);
99
- await fsp.mkdir(normalizedRunDirectory, { recursive: true });
100
- await fsp.writeFile(contextPath, `${JSON.stringify(context, null, 2)}\n`, "utf-8");
102
+ await outputFs.mkdir(normalizedRunDirectory, { recursive: true });
103
+ await outputFs.writeFile(contextPath, `${JSON.stringify(context, null, 2)}\n`, "utf-8");
101
104
  return {
102
105
  context,
103
106
  contextPath,
@@ -208,11 +211,13 @@ function sanitizeRunId(value) {
208
211
  }
209
212
 
210
213
  export async function writeReviewComparisonArtifact({
214
+ outputFs,
211
215
  runDirectory,
212
216
  baseRunId,
213
217
  candidateRunId,
214
218
  comparison,
215
219
  } = {}) {
220
+ requireOutputFileSystem(outputFs);
216
221
  const normalizedRunDirectory = path.resolve(String(runDirectory || "."));
217
222
  const fileName = `REVIEW_COMPARISON_${sanitizeRunId(baseRunId)}_vs_${sanitizeRunId(
218
223
  candidateRunId
@@ -225,8 +230,8 @@ export async function writeReviewComparisonArtifact({
225
230
  candidateRunId: normalizeString(candidateRunId),
226
231
  comparison,
227
232
  };
228
- await fsp.mkdir(normalizedRunDirectory, { recursive: true });
229
- await fsp.writeFile(artifactPath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
233
+ await outputFs.mkdir(normalizedRunDirectory, { recursive: true });
234
+ await outputFs.writeFile(artifactPath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
230
235
  return {
231
236
  artifactPath,
232
237
  payload,
@@ -4,7 +4,8 @@ import fsp from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
 
7
- import { resolveOutputRoot } from "../config/service.js";
7
+ import { resolveOutputRoot, resolveOutputContext } from "../config/service.js";
8
+ import { bindOutputFileSystem, requireOutputFileSystem } from "../config/output-filesystem.js";
8
9
 
9
10
  const REVIEW_REPORT_JSON = "REVIEW_REPORT.json";
10
11
  const REVIEW_REPORT_MD = "REVIEW_REPORT.md";
@@ -523,16 +524,18 @@ export async function buildUnifiedReviewReport({
523
524
  }
524
525
 
525
526
  export async function writeUnifiedReviewArtifacts({
527
+ outputFs,
526
528
  runDirectory,
527
529
  report,
528
530
  markdown,
529
531
  } = {}) {
532
+ requireOutputFileSystem(outputFs);
530
533
  const normalizedRunDirectory = path.resolve(String(runDirectory || "."));
531
- await fsp.mkdir(normalizedRunDirectory, { recursive: true });
534
+ await outputFs.mkdir(normalizedRunDirectory, { recursive: true });
532
535
  const reportJsonPath = path.join(normalizedRunDirectory, REVIEW_REPORT_JSON);
533
536
  const reportMarkdownPath = path.join(normalizedRunDirectory, REVIEW_REPORT_MD);
534
- await fsp.writeFile(reportJsonPath, `${JSON.stringify(report, null, 2)}\n`, "utf-8");
535
- await fsp.writeFile(reportMarkdownPath, `${String(markdown || "").trim()}\n`, "utf-8");
537
+ await outputFs.writeFile(reportJsonPath, `${JSON.stringify(report, null, 2)}\n`, "utf-8");
538
+ await outputFs.writeFile(reportMarkdownPath, `${String(markdown || "").trim()}\n`, "utf-8");
536
539
  return {
537
540
  reportJsonPath,
538
541
  reportMarkdownPath,
@@ -617,6 +620,7 @@ export async function loadUnifiedReviewReport({
617
620
  outputDir = "",
618
621
  env,
619
622
  } = {}) {
623
+ const { outputFs } = await resolveOutputContext({ cwd: path.resolve(String(targetPath || ".")), outputDirOverride: outputDir, env });
620
624
  const runDirectory = await resolveRunDirectory({ targetPath, runId, outputDir, env });
621
625
  const reportJsonPath = path.join(runDirectory, REVIEW_REPORT_JSON);
622
626
  const reportMarkdownPath = path.join(runDirectory, REVIEW_REPORT_MD);
@@ -624,14 +628,14 @@ export async function loadUnifiedReviewReport({
624
628
  const decisionsPath = path.join(runDirectory, REVIEW_DECISIONS_JSON);
625
629
  const decisions = await loadDecisions(decisionsPath);
626
630
  const resolvedReport = applyDecisionsToReport(report, decisions.decisions);
627
- return {
631
+ return bindOutputFileSystem({
628
632
  runDirectory,
629
633
  reportJsonPath,
630
634
  reportMarkdownPath,
631
635
  decisionsPath,
632
636
  decisions,
633
637
  report: resolvedReport,
634
- };
638
+ }, outputFs);
635
639
  }
636
640
 
637
641
  export async function recordReviewDecision({
@@ -659,6 +663,7 @@ export async function recordReviewDecision({
659
663
  outputDir,
660
664
  env,
661
665
  });
666
+ const { outputFs } = loaded;
662
667
  const finding = (loaded.report.findings || []).find((item) => item.findingId === normalizedFindingId);
663
668
  if (!finding) {
664
669
  throw new Error(`Finding '${normalizedFindingId}' not found in run ${loaded.report.runId}.`);
@@ -678,12 +683,12 @@ export async function recordReviewDecision({
678
683
  [normalizedFindingId]: decision,
679
684
  },
680
685
  };
681
- await fsp.writeFile(loaded.decisionsPath, `${JSON.stringify(nextDecisions, null, 2)}\n`, "utf-8");
686
+ await outputFs.writeFile(loaded.decisionsPath, `${JSON.stringify(nextDecisions, null, 2)}\n`, "utf-8");
682
687
 
683
688
  const nextReport = applyDecisionsToReport(loaded.report, nextDecisions.decisions);
684
689
  const nextMarkdown = composeReportMarkdown(nextReport);
685
- await fsp.writeFile(loaded.reportJsonPath, `${JSON.stringify(nextReport, null, 2)}\n`, "utf-8");
686
- await fsp.writeFile(loaded.reportMarkdownPath, `${nextMarkdown.trim()}\n`, "utf-8");
690
+ await outputFs.writeFile(loaded.reportJsonPath, `${JSON.stringify(nextReport, null, 2)}\n`, "utf-8");
691
+ await outputFs.writeFile(loaded.reportMarkdownPath, `${nextMarkdown.trim()}\n`, "utf-8");
687
692
 
688
693
  return {
689
694
  runId: nextReport.runId,
@@ -821,10 +826,14 @@ export async function exportUnifiedReviewReport({
821
826
  outputDir,
822
827
  env,
823
828
  });
824
- const resolvedOutputPath = normalizeString(outputFile)
825
- ? path.resolve(path.resolve(String(targetPath || ".")), outputFile)
829
+ const explicitOutput = normalizeString(outputFile)
830
+ ? await resolveOutputContext({ cwd: path.resolve(String(targetPath || ".")), outputDirOverride: outputFile, env })
831
+ : null;
832
+ const resolvedOutputPath = explicitOutput
833
+ ? explicitOutput.outputRoot
826
834
  : resolveDefaultExportPath(loaded.runDirectory, normalizedFormat);
827
835
 
836
+ const outputFs = requireOutputFileSystem(explicitOutput ? explicitOutput.outputFs : loaded.outputFs);
828
837
  let serialized = "";
829
838
  if (normalizedFormat === "json") {
830
839
  serialized = `${JSON.stringify(loaded.report, null, 2)}\n`;
@@ -836,8 +845,8 @@ export async function exportUnifiedReviewReport({
836
845
  serialized = `${buildGithubAnnotations(loaded.report)}\n`;
837
846
  }
838
847
 
839
- await fsp.mkdir(path.dirname(resolvedOutputPath), { recursive: true });
840
- await fsp.writeFile(resolvedOutputPath, serialized, "utf-8");
848
+ await outputFs.mkdir(path.dirname(resolvedOutputPath), { recursive: true });
849
+ await outputFs.writeFile(resolvedOutputPath, serialized, "utf-8");
841
850
 
842
851
  return {
843
852
  runId: loaded.report.runId,