sentinelayer-cli 0.46.5 → 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 (133) hide show
  1. package/README.md +47 -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 +32 -1
  26. package/src/auth/session-store.js +37 -0
  27. package/src/board/evidence-resolvers.js +3 -4
  28. package/src/cli.js +23 -3
  29. package/src/commands/ai/identity-lifecycle.js +30 -26
  30. package/src/commands/ai/provision-governance.js +32 -24
  31. package/src/commands/ai/shared.js +6 -4
  32. package/src/commands/audit.js +14 -12
  33. package/src/commands/board.js +2 -2
  34. package/src/commands/chat.js +8 -6
  35. package/src/commands/judge.js +125 -0
  36. package/src/commands/legacy-args.js +1 -0
  37. package/src/commands/mcp.js +45 -52
  38. package/src/commands/omargate.js +2 -1
  39. package/src/commands/plugin.js +11 -3
  40. package/src/commands/review.js +9 -4
  41. package/src/commands/scan.js +7 -7
  42. package/src/commands/session.js +516 -30
  43. package/src/commands/skills.js +45 -0
  44. package/src/commands/watch.js +9 -7
  45. package/src/config/output-filesystem.js +156 -0
  46. package/src/config/schema.js +4 -2
  47. package/src/config/service.js +40 -4
  48. package/src/cost/history.js +15 -9
  49. package/src/daemon/artifact-lineage.js +15 -12
  50. package/src/daemon/assignment-ledger.js +31 -25
  51. package/src/daemon/ast-drift.js +7 -3
  52. package/src/daemon/budget-governor.js +26 -20
  53. package/src/daemon/error-worker.js +28 -22
  54. package/src/daemon/fix-cycle.js +5 -4
  55. package/src/daemon/hybrid-mapper.js +19 -16
  56. package/src/daemon/ingest-refresh.js +4 -0
  57. package/src/daemon/jira-lifecycle.js +21 -17
  58. package/src/daemon/operator-control.js +18 -15
  59. package/src/daemon/reliability-lane.js +18 -15
  60. package/src/daemon/scope-engine.js +12 -8
  61. package/src/daemon/watchdog.js +15 -12
  62. package/src/engram/build-state.js +24 -3
  63. package/src/engram/capability-index.js +367 -0
  64. package/src/engram/compile-graph.js +1063 -0
  65. package/src/engram/contracts/action-receipt.js +82 -0
  66. package/src/engram/contracts/capability-index.js +259 -0
  67. package/src/engram/contracts/capability.js +377 -0
  68. package/src/engram/contracts/content-hash.js +8 -0
  69. package/src/engram/contracts/evidence.js +258 -0
  70. package/src/engram/contracts/graph.js +594 -0
  71. package/src/engram/contracts/identity.js +8 -0
  72. package/src/engram/contracts/index.js +149 -0
  73. package/src/engram/contracts/media-asset.js +85 -0
  74. package/src/engram/contracts/money.js +249 -0
  75. package/src/engram/contracts/source-snapshot.js +57 -0
  76. package/src/engram/contracts/task-context-pack.js +114 -0
  77. package/src/engram/contracts/temporal-segment.js +72 -0
  78. package/src/engram/contracts/versions.js +96 -0
  79. package/src/engram/extract-html.js +692 -0
  80. package/src/engram/graph-store.js +877 -0
  81. package/src/engram/index.js +98 -14
  82. package/src/engram/ingest-site.js +132 -0
  83. package/src/engram/live-state.js +1180 -0
  84. package/src/engram/namespace.js +34 -1
  85. package/src/engram/retrieve-site.js +753 -0
  86. package/src/engram/safe-reader.js +597 -0
  87. package/src/engram/source-intake.js +251 -0
  88. package/src/engram/store.js +227 -9
  89. package/src/ingest/engine.js +21 -28
  90. package/src/judge/verdict.js +305 -0
  91. package/src/legacy-cli.js +96 -19
  92. package/src/mcp/bridge-inputs.js +25 -4
  93. package/src/mcp/cli-command-tools.js +70 -0
  94. package/src/mcp/registry.js +17 -13
  95. package/src/mcp/session-stdio-server.js +297 -19
  96. package/src/memory/blackboard.js +5 -4
  97. package/src/net/gh-api-path.js +35 -9
  98. package/src/net/test-egress-guard.js +16 -4
  99. package/src/plugin/manifest.js +12 -8
  100. package/src/review/ai-review.js +110 -10
  101. package/src/review/investor-dd-devtestbot.js +17 -6
  102. package/src/review/investor-dd-orchestrator.js +21 -13
  103. package/src/review/local-review.js +26 -16
  104. package/src/review/omargate-cache.js +5 -5
  105. package/src/review/omargate-orchestrator.js +89 -17
  106. package/src/review/replay.js +9 -4
  107. package/src/review/report.js +22 -13
  108. package/src/review/typesafe-severity.js +201 -0
  109. package/src/session/admission-auth.js +34 -7
  110. package/src/session/admission.js +110 -15
  111. package/src/session/attachment-summary.js +77 -0
  112. package/src/session/edit-summary.js +19 -0
  113. package/src/session/invitations.js +65 -1
  114. package/src/session/mentions.js +17 -9
  115. package/src/session/message-edits.js +5 -4
  116. package/src/session/preview.js +10 -2
  117. package/src/session/recap.js +3 -2
  118. package/src/session/room-files.js +519 -0
  119. package/src/session/route-classes.js +22 -8
  120. package/src/session/setup-guides.js +4 -0
  121. package/src/session/standup.js +192 -0
  122. package/src/session/sync.js +229 -10
  123. package/src/session/tickets.js +41 -0
  124. package/src/session/transcript.js +2 -1
  125. package/src/session/wake/resolve-target.js +7 -3
  126. package/src/skills/listener-runtime.cjs +92 -0
  127. package/src/skills/service.js +207 -0
  128. package/src/swarm/factory.js +5 -5
  129. package/src/swarm/pentest.js +9 -9
  130. package/src/swarm/report.js +4 -4
  131. package/src/swarm/runtime.js +15 -12
  132. package/src/swarm/scenario-dsl.js +7 -5
  133. package/src/telemetry/ledger.js +16 -7
@@ -24,7 +24,7 @@ import {
24
24
  } from "../../ai/domain-target-store.js";
25
25
  import { listSites, recordTemporarySite } from "../../ai/site-store.js";
26
26
  import { getIdentityById, recordProvisionedIdentity } from "../../ai/identity-store.js";
27
- import { resolveOutputRoot } from "../../config/service.js";
27
+ import { resolveOutputRoot, resolveOutputContext } from "../../config/service.js";
28
28
  import { fetchAidenIdCredentials, resolveActiveAuthSession } from "../../auth/service.js";
29
29
  import {
30
30
  buildCurlPreview,
@@ -76,7 +76,7 @@ async function provisionEmailAction(options, command) {
76
76
  extractionTypes: options.extractionTypes,
77
77
  });
78
78
 
79
- const outputRoot = await resolveOutputRoot({
79
+ const { outputRoot, outputFs } = await resolveOutputContext({
80
80
  cwd: targetPath,
81
81
  outputDirOverride: options.outputDir,
82
82
  env: process.env,
@@ -90,7 +90,7 @@ async function provisionEmailAction(options, command) {
90
90
  apiUrl,
91
91
  idempotencyKey,
92
92
  payload,
93
- });
93
+ }, outputFs);
94
94
 
95
95
  const session = await resolveActiveAuthSession({ cwd: process.cwd(), env: process.env, autoRotate: false }).catch(
96
96
  () => null,
@@ -170,10 +170,11 @@ async function provisionEmailAction(options, command) {
170
170
  apiUrl,
171
171
  idempotencyKey,
172
172
  response: execution.response,
173
- });
173
+ }, outputFs);
174
174
 
175
175
  const responseIdentity = execution.response || {};
176
176
  const registryUpdate = await recordProvisionedIdentity({
177
+ outputFs,
177
178
  outputRoot,
178
179
  response: execution.response || {},
179
180
  context: {
@@ -259,7 +260,7 @@ domain
259
260
  .action(async (domainName, options, command) => {
260
261
  const emitJson = shouldEmitJson(options, command);
261
262
  const targetPath = path.resolve(process.cwd(), String(options.path || "."));
262
- const outputRoot = await resolveOutputRoot({
263
+ const { outputRoot, outputFs } = await resolveOutputContext({
263
264
  cwd: targetPath,
264
265
  outputDirOverride: options.outputDir,
265
266
  env: process.env,
@@ -287,7 +288,7 @@ domain
287
288
  apiUrl,
288
289
  idempotencyKey,
289
290
  payload,
290
- });
291
+ }, outputFs);
291
292
 
292
293
  const resolvedCredentials = await resolveAidenIdCredentials({
293
294
  apiKey: options.apiKey,
@@ -355,10 +356,11 @@ domain
355
356
  apiUrl,
356
357
  idempotencyKey,
357
358
  response: execution.response,
358
- });
359
+ }, outputFs);
359
360
 
360
361
  const proofResponse = execution.response || {};
361
362
  const registryUpdate = await recordDomainProofResponse({
363
+ outputFs,
362
364
  outputRoot,
363
365
  domain: proofResponse.domain || {},
364
366
  proof: proofResponse,
@@ -416,7 +418,7 @@ domain
416
418
  .action(async (domainId, options, command) => {
417
419
  const emitJson = shouldEmitJson(options, command);
418
420
  const targetPath = path.resolve(process.cwd(), String(options.path || "."));
419
- const outputRoot = await resolveOutputRoot({
421
+ const { outputRoot, outputFs } = await resolveOutputContext({
420
422
  cwd: targetPath,
421
423
  outputDirOverride: options.outputDir,
422
424
  env: process.env,
@@ -451,7 +453,7 @@ domain
451
453
  idempotencyKey,
452
454
  domainId,
453
455
  payload,
454
- });
456
+ }, outputFs);
455
457
 
456
458
  const resolvedCredentials = await resolveAidenIdCredentials({
457
459
  apiKey: options.apiKey,
@@ -511,10 +513,11 @@ domain
511
513
  idempotencyKey,
512
514
  domainId,
513
515
  response: execution.response,
514
- });
516
+ }, outputFs);
515
517
 
516
518
  const proofResponse = execution.response || {};
517
519
  const registryUpdate = await recordDomainProofResponse({
520
+ outputFs,
518
521
  outputRoot,
519
522
  domain: proofResponse.domain || {},
520
523
  proof: proofResponse,
@@ -571,7 +574,7 @@ domain
571
574
  .action(async (domainId, options, command) => {
572
575
  const emitJson = shouldEmitJson(options, command);
573
576
  const targetPath = path.resolve(process.cwd(), String(options.path || "."));
574
- const outputRoot = await resolveOutputRoot({
577
+ const { outputRoot, outputFs } = await resolveOutputContext({
575
578
  cwd: targetPath,
576
579
  outputDirOverride: options.outputDir,
577
580
  env: process.env,
@@ -593,7 +596,7 @@ domain
593
596
  idempotencyKey,
594
597
  domainId,
595
598
  payload,
596
- });
599
+ }, outputFs);
597
600
 
598
601
  const resolvedCredentials = await resolveAidenIdCredentials({
599
602
  apiKey: options.apiKey,
@@ -653,9 +656,10 @@ domain
653
656
  idempotencyKey,
654
657
  domainId,
655
658
  response: execution.response,
656
- });
659
+ }, outputFs);
657
660
 
658
661
  const registryUpdate = await recordDomainProofResponse({
662
+ outputFs,
659
663
  outputRoot,
660
664
  domain: execution.response || {},
661
665
  proof: {},
@@ -712,7 +716,7 @@ target
712
716
  .action(async (host, options, command) => {
713
717
  const emitJson = shouldEmitJson(options, command);
714
718
  const targetPath = path.resolve(process.cwd(), String(options.path || "."));
715
- const outputRoot = await resolveOutputRoot({
719
+ const { outputRoot, outputFs } = await resolveOutputContext({
716
720
  cwd: targetPath,
717
721
  outputDirOverride: options.outputDir,
718
722
  env: process.env,
@@ -745,7 +749,7 @@ target
745
749
  apiUrl,
746
750
  idempotencyKey,
747
751
  payload,
748
- });
752
+ }, outputFs);
749
753
 
750
754
  const resolvedCredentials = await resolveAidenIdCredentials({
751
755
  apiKey: options.apiKey,
@@ -802,10 +806,11 @@ target
802
806
  apiUrl,
803
807
  idempotencyKey,
804
808
  response: execution.response,
805
- });
809
+ }, outputFs);
806
810
 
807
811
  const proofResponse = execution.response || {};
808
812
  const registryUpdate = await recordTargetProofResponse({
813
+ outputFs,
809
814
  outputRoot,
810
815
  target: proofResponse.target || {},
811
816
  proof: proofResponse,
@@ -862,7 +867,7 @@ target
862
867
  .action(async (targetId, options, command) => {
863
868
  const emitJson = shouldEmitJson(options, command);
864
869
  const targetPath = path.resolve(process.cwd(), String(options.path || "."));
865
- const outputRoot = await resolveOutputRoot({
870
+ const { outputRoot, outputFs } = await resolveOutputContext({
866
871
  cwd: targetPath,
867
872
  outputDirOverride: options.outputDir,
868
873
  env: process.env,
@@ -897,7 +902,7 @@ target
897
902
  idempotencyKey,
898
903
  targetId,
899
904
  payload,
900
- });
905
+ }, outputFs);
901
906
 
902
907
  const resolvedCredentials = await resolveAidenIdCredentials({
903
908
  apiKey: options.apiKey,
@@ -957,10 +962,11 @@ target
957
962
  idempotencyKey,
958
963
  targetId,
959
964
  response: execution.response,
960
- });
965
+ }, outputFs);
961
966
 
962
967
  const proofResponse = execution.response || {};
963
968
  const registryUpdate = await recordTargetProofResponse({
969
+ outputFs,
964
970
  outputRoot,
965
971
  target: proofResponse.target || {},
966
972
  proof: proofResponse,
@@ -1012,7 +1018,7 @@ target
1012
1018
  .action(async (targetId, options, command) => {
1013
1019
  const emitJson = shouldEmitJson(options, command);
1014
1020
  const targetPath = path.resolve(process.cwd(), String(options.path || "."));
1015
- const outputRoot = await resolveOutputRoot({
1021
+ const { outputRoot, outputFs } = await resolveOutputContext({
1016
1022
  cwd: targetPath,
1017
1023
  outputDirOverride: options.outputDir,
1018
1024
  env: process.env,
@@ -1041,9 +1047,10 @@ target
1041
1047
  generatedAt: new Date().toISOString(),
1042
1048
  targetId,
1043
1049
  response: execution.response,
1044
- });
1050
+ }, outputFs);
1045
1051
 
1046
1052
  const registryUpdate = await recordTargetProofResponse({
1053
+ outputFs,
1047
1054
  outputRoot,
1048
1055
  target: execution.response || {},
1049
1056
  proof: {},
@@ -1098,7 +1105,7 @@ site
1098
1105
  .action(async (identityId, options, command) => {
1099
1106
  const emitJson = shouldEmitJson(options, command);
1100
1107
  const targetPath = path.resolve(process.cwd(), String(options.path || "."));
1101
- const outputRoot = await resolveOutputRoot({
1108
+ const { outputRoot, outputFs } = await resolveOutputContext({
1102
1109
  cwd: targetPath,
1103
1110
  outputDirOverride: options.outputDir,
1104
1111
  env: process.env,
@@ -1132,7 +1139,7 @@ site
1132
1139
  apiUrl,
1133
1140
  idempotencyKey,
1134
1141
  payload,
1135
- });
1142
+ }, outputFs);
1136
1143
 
1137
1144
  const resolvedCredentials = await resolveAidenIdCredentials({
1138
1145
  apiKey: options.apiKey,
@@ -1190,9 +1197,10 @@ site
1190
1197
  apiUrl,
1191
1198
  idempotencyKey,
1192
1199
  response: execution.response,
1193
- });
1200
+ }, outputFs);
1194
1201
 
1195
1202
  const registryUpdate = await recordTemporarySite({
1203
+ outputFs,
1196
1204
  outputRoot,
1197
1205
  site: execution.response || {},
1198
1206
  context: {
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import fsp from "node:fs/promises";
2
+
3
3
  import path from "node:path";
4
+ import { requireOutputFileSystem } from "../../config/output-filesystem.js";
4
5
 
5
6
  // Shared helper utilities for ai command modules.
6
7
 
@@ -128,9 +129,10 @@ export function renderLineageRows(lineage = {}) {
128
129
  });
129
130
  }
130
131
 
131
- export async function writeArtifact(filePath, payload) {
132
- await fsp.mkdir(path.dirname(filePath), { recursive: true });
133
- await fsp.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
132
+ export async function writeArtifact(filePath, payload, outputFs) {
133
+ requireOutputFileSystem(outputFs);
134
+ await outputFs.mkdir(path.dirname(filePath), { recursive: true });
135
+ await outputFs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
134
136
  }
135
137
 
136
138
  export function buildCurlPreview({ apiUrl, idempotencyKey, requestPath }) {
@@ -7,7 +7,7 @@ import { runAuditOrchestrator } from "../audit/orchestrator.js";
7
7
  import { loadAuditRunReport, resolveAuditRunDirectory, writeDdPackage } from "../audit/package.js";
8
8
  import { writeAuditComparisonArtifact } from "../audit/replay.js";
9
9
  import { loadAuditRegistry, selectAuditAgents } from "../audit/registry.js";
10
- import { resolveOutputRoot } from "../config/service.js";
10
+ import { resolveOutputContext } from "../config/service.js";
11
11
  import { createAgentEvent } from "../events/schema.js";
12
12
  import { createAuditSessionReporter, resolveAuditSessionId } from "../session/audit-reporter.js";
13
13
  import { resolveActionOptions } from "./action-options.js";
@@ -217,7 +217,7 @@ export function registerAuditCommand(program, invokeLegacy) {
217
217
  const actionOptions = resolveActionOptions(options, command);
218
218
  const emitJson = shouldEmitJson(actionOptions, command);
219
219
  const targetPath = path.resolve(process.cwd(), String(actionOptions.path || "."));
220
- const outputRoot = await resolveOutputRoot({
220
+ const { outputRoot, outputFs } = await resolveOutputContext({
221
221
  cwd: targetPath,
222
222
  outputDirOverride: actionOptions.outputDir,
223
223
  env: process.env,
@@ -228,6 +228,7 @@ export function registerAuditCommand(program, invokeLegacy) {
228
228
  });
229
229
  const { report } = await loadAuditRunReport(runDirectory);
230
230
  const ddPackage = await writeDdPackage({
231
+ outputFs,
231
232
  report,
232
233
  runDirectory,
233
234
  });
@@ -269,7 +270,7 @@ export function registerAuditCommand(program, invokeLegacy) {
269
270
  const actionOptions = resolveActionOptions(options, command);
270
271
  const emitJson = shouldEmitJson(actionOptions, command);
271
272
  const initialTargetPath = path.resolve(process.cwd(), String(actionOptions.path || "."));
272
- const outputRoot = await resolveOutputRoot({
273
+ const { outputRoot } = await resolveOutputContext({
273
274
  cwd: initialTargetPath,
274
275
  outputDirOverride: actionOptions.outputDir,
275
276
  env: process.env,
@@ -306,6 +307,7 @@ export function registerAuditCommand(program, invokeLegacy) {
306
307
  baseReport,
307
308
  candidateReport: replayResult,
308
309
  outputDirectory: replayResult.runDirectory,
310
+ outputFs: replayResult.outputFs,
309
311
  });
310
312
 
311
313
  const payload = {
@@ -347,7 +349,7 @@ export function registerAuditCommand(program, invokeLegacy) {
347
349
  const actionOptions = resolveActionOptions(options, command);
348
350
  const emitJson = shouldEmitJson(actionOptions, command);
349
351
  const targetPath = path.resolve(process.cwd(), String(actionOptions.path || "."));
350
- const outputRoot = await resolveOutputRoot({
352
+ const { outputRoot, outputFs } = await resolveOutputContext({
351
353
  cwd: targetPath,
352
354
  outputDirOverride: actionOptions.outputDir,
353
355
  env: process.env,
@@ -366,6 +368,7 @@ export function registerAuditCommand(program, invokeLegacy) {
366
368
  baseReport,
367
369
  candidateReport,
368
370
  outputDirectory: candidateRunDirectory,
371
+ outputFs,
369
372
  });
370
373
 
371
374
  const payload = {
@@ -869,14 +872,12 @@ export function registerAuditCommand(program, invokeLegacy) {
869
872
  const { julesAuditLoop } = await import("../agents/jules/loop.js");
870
873
  const { JULES_DEFINITION } = await import("../agents/jules/config/definition.js");
871
874
  const { collectCodebaseIngest, generateCodebaseIngest } = await import("../ingest/engine.js");
872
- const { resolveOutputRoot: resolveOut } = await import("../config/service.js");
873
875
  const { createBlackboard } = await import("../memory/blackboard.js");
874
876
  const { runDeterministicReviewPipeline } = await import("../review/local-review.js");
875
- const fsp = await import("node:fs/promises");
876
877
 
877
- const outputRoot = await resolveOut({ targetPath, outputDir: actionOptions.outputDir });
878
+ const { outputRoot, outputFs } = await resolveOutputContext({ cwd: targetPath, outputDirOverride: actionOptions.outputDir });
878
879
  const artifactDir = path.join(outputRoot, "reports", "jules-" + Date.now());
879
- await fsp.mkdir(artifactDir, { recursive: true });
880
+ await outputFs.mkdir(artifactDir, { recursive: true });
880
881
 
881
882
  // Build event handler
882
883
  const onEvent = buildEventHandler(emitStream, emitJson, JULES_DEFINITION);
@@ -888,7 +889,7 @@ export function registerAuditCommand(program, invokeLegacy) {
888
889
  let ingest;
889
890
  try {
890
891
  ingest = await generateCodebaseIngest({
891
- rootPath: targetPath, outputDir: outputRoot,
892
+ rootPath: targetPath, outputDir: actionOptions.outputDir,
892
893
  refresh: Boolean(actionOptions.refresh),
893
894
  });
894
895
  } catch {
@@ -907,6 +908,7 @@ export function registerAuditCommand(program, invokeLegacy) {
907
908
  targetPath,
908
909
  mode: "full",
909
910
  outputDir: artifactDir,
911
+ outputFs,
910
912
  });
911
913
  emitProgress(onEvent, JULES_DEFINITION,
912
914
  "Baseline complete: P0=" + (omarBaseline?.summary?.P0 || 0) +
@@ -1085,21 +1087,21 @@ export function registerAuditCommand(program, invokeLegacy) {
1085
1087
  signature: JULES_DEFINITION.signature,
1086
1088
  };
1087
1089
 
1088
- await fsp.writeFile(
1090
+ await outputFs.writeFile(
1089
1091
  path.join(artifactDir, "JULES_AUDIT.json"),
1090
1092
  JSON.stringify(reportPayload, null, 2),
1091
1093
  );
1092
1094
 
1093
1095
  // Write baseline artifact if it exists
1094
1096
  if (omarBaseline) {
1095
- await fsp.writeFile(
1097
+ await outputFs.writeFile(
1096
1098
  path.join(artifactDir, "OMAR_BASELINE.json"),
1097
1099
  JSON.stringify(omarBaseline, null, 2),
1098
1100
  );
1099
1101
  }
1100
1102
 
1101
1103
  // Write reconciliation artifact
1102
- await fsp.writeFile(
1104
+ await outputFs.writeFile(
1103
1105
  path.join(artifactDir, "RECONCILIATION.json"),
1104
1106
  JSON.stringify(reconciliation, null, 2),
1105
1107
  );
@@ -104,10 +104,10 @@ export function registerBoardCommand(program) {
104
104
  .description("Check whether a ticket's evidence holds (done | not-done | cannot-verify)")
105
105
  .requiredOption("--file <path>", "Ticket JSON file with an `evidence` array")
106
106
  .option("--json", "Emit machine-readable output")
107
- .action(async (options) => {
107
+ .action(async (options, command) => {
108
108
  const code = await runBoardCheck({
109
109
  file: path.resolve(process.cwd(), String(options.file)),
110
- emitJson: Boolean(options.json),
110
+ emitJson: Boolean(options.json || command.optsWithGlobals().json),
111
111
  });
112
112
  process.exitCode = code;
113
113
  });
@@ -1,4 +1,4 @@
1
- import fsp from "node:fs/promises";
1
+ import { requireOutputFileSystem } from "../config/output-filesystem.js";
2
2
  import path from "node:path";
3
3
  import process from "node:process";
4
4
 
@@ -10,7 +10,7 @@ import {
10
10
  resolveProvider,
11
11
  } from "../ai/client.js";
12
12
  import { recordCliLlmSessionUsage, usageNumber } from "../billing/llm-session-usage.js";
13
- import { resolveOutputRoot } from "../config/service.js";
13
+ import { resolveOutputContext } from "../config/service.js";
14
14
  import { estimateModelCost } from "../cost/tracker.js";
15
15
  import { estimateTokens } from "../cost/tokenizer.js";
16
16
  import { normalizeSessionId } from "../session/paths.js";
@@ -43,10 +43,11 @@ async function readPromptFromStdin() {
43
43
  });
44
44
  }
45
45
 
46
- async function appendTranscriptEntries({ transcriptPath, entries } = {}) {
47
- await fsp.mkdir(path.dirname(transcriptPath), { recursive: true });
46
+ async function appendTranscriptEntries({ transcriptPath, entries, outputFs } = {}) {
47
+ requireOutputFileSystem(outputFs);
48
+ await outputFs.mkdir(path.dirname(transcriptPath), { recursive: true });
48
49
  const rows = entries.map((entry) => JSON.stringify(entry)).join("\n");
49
- await fsp.appendFile(transcriptPath, `${rows}\n`, "utf-8");
50
+ await outputFs.appendFile(transcriptPath, `${rows}\n`, "utf-8");
50
51
  }
51
52
 
52
53
  export function registerChatCommand(program) {
@@ -77,7 +78,7 @@ export function registerChatCommand(program) {
77
78
  }
78
79
 
79
80
  const targetPath = path.resolve(process.cwd(), String(options.path || "."));
80
- const outputRoot = await resolveOutputRoot({
81
+ const { outputRoot, outputFs } = await resolveOutputContext({
81
82
  cwd: targetPath,
82
83
  outputDirOverride: options.outputDir,
83
84
  env: process.env,
@@ -159,6 +160,7 @@ export function registerChatCommand(program) {
159
160
  });
160
161
 
161
162
  await appendTranscriptEntries({
163
+ outputFs,
162
164
  transcriptPath,
163
165
  entries: [
164
166
  {
@@ -0,0 +1,125 @@
1
+ import fsp from "node:fs/promises";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+
5
+ import { formatJudgeVerdictErrors, renderJudgeVerdictMessage, validateJudgeVerdict } from "../judge/verdict.js";
6
+
7
+ function shouldEmitJson(options, command) {
8
+ if (options && options.json) return true;
9
+ return typeof command?.optsWithGlobals === "function" ? Boolean(command.optsWithGlobals().json) : false;
10
+ }
11
+
12
+ async function readStdinText() {
13
+ const chunks = [];
14
+ for await (const chunk of process.stdin) chunks.push(chunk);
15
+ return Buffer.concat(chunks).toString("utf8");
16
+ }
17
+
18
+ async function readVerdictDocument(options) {
19
+ if (options.file && options.stdin) throw new Error("Use --file or --stdin, not both.");
20
+ if (!options.file && !options.stdin) throw new Error("Give the verdict with --file <path> or --stdin.");
21
+ const raw = options.stdin
22
+ ? await readStdinText()
23
+ : await fsp.readFile(path.resolve(process.cwd(), String(options.file)), "utf8");
24
+ try {
25
+ return JSON.parse(raw);
26
+ } catch (error) {
27
+ throw new Error(`The verdict is not valid JSON: ${error.message}`);
28
+ }
29
+ }
30
+
31
+ function report(emitJson, payload, text) {
32
+ if (emitJson) {
33
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
34
+ } else {
35
+ process.stdout.write(`${text}\n`);
36
+ }
37
+ }
38
+
39
+ function fail(emitJson, command, error) {
40
+ const message = error && error.message ? error.message : String(error);
41
+ if (emitJson) {
42
+ process.stdout.write(`${JSON.stringify({ command, ok: false, reason: message }, null, 2)}\n`);
43
+ } else {
44
+ process.stderr.write(`${command} failed: ${message}\n`);
45
+ }
46
+ process.exitCode = 1;
47
+ }
48
+
49
+ export function registerJudgeCommand(program) {
50
+ const judge = program
51
+ .command("judge")
52
+ .description("Judge a coding agent's work and send the verdict back (LLM-as-a-judge loop)");
53
+
54
+ judge
55
+ .command("check")
56
+ .description("Validate a judge-verdict offline and print the message the coder will receive")
57
+ .option("--file <path>", "Verdict JSON file")
58
+ .option("--stdin", "Read the verdict JSON from stdin")
59
+ .option("--escalate-to <handle...>", "Human handle(s) to mention when the verdict escalates")
60
+ .option("--json", "Emit machine-readable output")
61
+ .action(async (options, command) => {
62
+ const emitJson = shouldEmitJson(options, command);
63
+ try {
64
+ const checked = validateJudgeVerdict(await readVerdictDocument(options));
65
+ if (!checked.ok) {
66
+ report(emitJson, { command: "judge check", ok: false, errors: checked.errors },
67
+ `Invalid verdict: ${formatJudgeVerdictErrors(checked.errors)}`);
68
+ process.exitCode = 1;
69
+ return;
70
+ }
71
+ const message = renderJudgeVerdictMessage(checked.verdict, { escalateTo: options.escalateTo || [] });
72
+ report(emitJson, { command: "judge check", ok: true, verdict: checked.verdict, message }, message);
73
+ } catch (error) {
74
+ fail(emitJson, "judge check", error);
75
+ }
76
+ });
77
+
78
+ judge
79
+ .command("verdict <sessionId>")
80
+ .description("Send a judge-verdict to the room: the coder is mentioned and woken, and the verdict is recorded")
81
+ .option("--agent <id>", "Your judge agent id (default: SENTINELAYER_AGENT_ID)")
82
+ .option("--file <path>", "Verdict JSON file")
83
+ .option("--stdin", "Read the verdict JSON from stdin")
84
+ .option("--via <transport>", "auto (the API route when the server has it, else a room message), api, or room", "auto")
85
+ .option("--escalate-to <handle...>", "Human handle(s) to mention when the verdict escalates")
86
+ .option("--path <path>", "Workspace path for the session", ".")
87
+ .option("--dry-run", "Validate and print the message without sending it")
88
+ .option("--json", "Emit machine-readable output")
89
+ .action(async (sessionId, options, command) => {
90
+ const emitJson = shouldEmitJson(options, command);
91
+ try {
92
+ const verdict = await readVerdictDocument(options);
93
+ const agentId = String(options.agent || process.env.SENTINELAYER_AGENT_ID || "").trim();
94
+ if (!agentId) throw new Error("Name your judge agent with --agent <id> or SENTINELAYER_AGENT_ID.");
95
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
96
+ // One implementation for the CLI and MCP: the native judge_verdict tool, which already runs
97
+ // under the admission policy every local session tool uses.
98
+ const { createSessionMcpToolHandlers } = await import("../mcp/session-stdio-server.js");
99
+ const handlers = createSessionMcpToolHandlers({ targetPath });
100
+ const result = await handlers.judge_verdict({
101
+ sessionId,
102
+ agentId,
103
+ verdict,
104
+ via: options.via,
105
+ escalateTo: options.escalateTo || [],
106
+ dryRun: Boolean(options.dryRun),
107
+ });
108
+ const payload = { command: "judge verdict", ...result };
109
+ if (!result.ok) {
110
+ const why = result.errors ? formatJudgeVerdictErrors(result.errors) : result.reason;
111
+ report(emitJson, payload, `Verdict not sent: ${why}`);
112
+ process.exitCode = 1;
113
+ return;
114
+ }
115
+ if (result.dryRun) {
116
+ report(emitJson, payload, result.message);
117
+ return;
118
+ }
119
+ report(emitJson, payload,
120
+ `Verdict ${result.verdict.verdict} (round ${result.verdict.round}) sent to @${result.verdict.coder} via ${result.transport}.`);
121
+ } catch (error) {
122
+ fail(emitJson, "judge verdict", error);
123
+ }
124
+ });
125
+ }
@@ -57,6 +57,7 @@ export function buildLegacyArgs(baseArgs, { commandOptions = {}, command } = {})
57
57
  appendPassthroughFlag(args, "--max-runtime-minutes", commandOptions.maxRuntimeMinutes);
58
58
  appendPassthroughFlag(args, "--model", commandOptions.model);
59
59
  appendPassthroughFlag(args, "--provider", commandOptions.provider);
60
+ if (commandOptions.severityClassifier && commandOptions.severityClassifier !== "off") appendPassthroughFlag(args, "--severity-classifier", commandOptions.severityClassifier);
60
61
  appendPassthroughFlag(args, "--reuse-omargate", commandOptions.reuseOmargate);
61
62
  appendPassthroughFlag(args, "--scope-mode", commandOptions.scopeMode);
62
63
  appendBooleanFlag(args, "--diff", commandOptions.diff);