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
@@ -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);
@@ -34,7 +34,8 @@ import { runMcpStdioServer } from "../mcp/session-stdio-server.js";
34
34
  import { runMcpDoctorProbes } from "../mcp/doctor.js";
35
35
  import { runHostedMcpSmoke } from "../mcp/smoke.js";
36
36
  import { resolveActiveAuthSession } from "../auth/service.js";
37
- import { resolveOutputRoot } from "../config/service.js";
37
+ import { resolveOutputContext, resolveOutputRoot } from "../config/service.js";
38
+ import { createOutputFileSystem } from "../config/output-filesystem.js";
38
39
 
39
40
  function shouldEmitJson(options, command) {
40
41
  const local = Boolean(options && options.json);
@@ -51,6 +52,27 @@ function normalizeOutputPath(rawValue, fallbackPath) {
51
52
  return path.resolve(process.cwd(), candidate);
52
53
  }
53
54
 
55
+ async function resolveWriteDestination(options, resolveDefaultPath, { suffix, ...pathOptions } = {}) {
56
+ if (String(options.path || "").trim()) {
57
+ const { outputRoot: outputPath, outputFs } = await resolveOutputContext({
58
+ cwd: process.cwd(),
59
+ outputDirOverride: options.path,
60
+ env: process.env,
61
+ });
62
+ return { outputPath, outputFs };
63
+ }
64
+ const outputContext = await resolveOutputContext({
65
+ cwd: process.cwd(),
66
+ outputDirOverride: options.outputDir,
67
+ env: process.env,
68
+ });
69
+ const defaultPath = await resolveDefaultPath({ ...pathOptions, outputContext });
70
+ return {
71
+ outputPath: suffix ? defaultPath.replace(".schema", suffix) : defaultPath,
72
+ outputFs: outputContext.outputFs,
73
+ };
74
+ }
75
+
54
76
  function zodIssueSummary(error) {
55
77
  if (!(error instanceof ZodError)) {
56
78
  return String(error instanceof Error ? error.message : error);
@@ -390,14 +412,9 @@ Docs: https://github.com/mrrCarter/create-sentinelayer/blob/main/docs/mcp.md`,
390
412
  .option("--force", "Overwrite destination file if it already exists")
391
413
  .option("--json", "Emit machine-readable output")
392
414
  .action(async (options, command) => {
393
- const defaultPath = await resolveDefaultMcpOutputPath({
394
- cwd: process.cwd(),
395
- outputDir: options.outputDir,
396
- env: process.env,
397
- });
398
- const outputPath = normalizeOutputPath(options.path, defaultPath);
415
+ const { outputPath, outputFs } = await resolveWriteDestination(options, resolveDefaultMcpOutputPath);
399
416
  const schemaPayload = buildMcpToolRegistrySchema();
400
- const writtenPath = await writeJsonFile(outputPath, schemaPayload, { force: Boolean(options.force) });
417
+ const writtenPath = await writeJsonFile(outputPath, schemaPayload, { force: Boolean(options.force), outputFs });
401
418
 
402
419
  const payload = {
403
420
  command: "mcp schema write",
@@ -420,15 +437,10 @@ Docs: https://github.com/mrrCarter/create-sentinelayer/blob/main/docs/mcp.md`,
420
437
  .option("--force", "Overwrite destination file if it already exists")
421
438
  .option("--json", "Emit machine-readable output")
422
439
  .action(async (options, command) => {
423
- const defaultPath = await resolveDefaultMcpOutputPath({
424
- cwd: process.cwd(),
425
- outputDir: options.outputDir,
426
- env: process.env,
427
- });
428
- const outputPath = normalizeOutputPath(options.path, defaultPath.replace(".schema", ".aidenid-template"));
440
+ const { outputPath, outputFs } = await resolveWriteDestination(options, resolveDefaultMcpOutputPath, { suffix: ".aidenid-template" });
429
441
  const template = buildAidenIdRegistryTemplate();
430
442
  validateMcpToolRegistry(template);
431
- const writtenPath = await writeJsonFile(outputPath, template, { force: Boolean(options.force) });
443
+ const writtenPath = await writeJsonFile(outputPath, template, { force: Boolean(options.force), outputFs });
432
444
  const payload = {
433
445
  command: "mcp registry init-aidenid",
434
446
  outputPath: writtenPath,
@@ -456,17 +468,12 @@ Docs: https://github.com/mrrCarter/create-sentinelayer/blob/main/docs/mcp.md`,
456
468
  .option("--force", "Overwrite destination file if it already exists")
457
469
  .option("--json", "Emit machine-readable output")
458
470
  .action(async (options, command) => {
459
- const defaultPath = await resolveDefaultAidenIdAdapterContractPath({
460
- cwd: process.cwd(),
461
- outputDir: options.outputDir,
462
- env: process.env,
463
- });
464
- const outputPath = normalizeOutputPath(options.path, defaultPath);
471
+ const { outputPath, outputFs } = await resolveWriteDestination(options, resolveDefaultAidenIdAdapterContractPath);
465
472
  const template = buildAidenIdProvisioningAdapterTemplate({
466
473
  registryFile: options.registryFile,
467
474
  });
468
475
  validateAidenIdAdapterContract(template);
469
- const writtenPath = await writeJsonFile(outputPath, template, { force: Boolean(options.force) });
476
+ const writtenPath = await writeJsonFile(outputPath, template, { force: Boolean(options.force), outputFs });
470
477
  const payload = {
471
478
  command: "mcp registry init-aidenid-adapter",
472
479
  outputPath: writtenPath,
@@ -497,15 +504,10 @@ Docs: https://github.com/mrrCarter/create-sentinelayer/blob/main/docs/mcp.md`,
497
504
  .option("--force", "Overwrite destination file if it already exists")
498
505
  .option("--json", "Emit machine-readable output")
499
506
  .action(async (options, command) => {
500
- const defaultPath = await resolveDefaultMcpOutputPath({
501
- cwd: process.cwd(),
502
- outputDir: options.outputDir,
503
- env: process.env,
504
- });
505
- const outputPath = normalizeOutputPath(options.path, defaultPath.replace(".schema", outputSuffix));
507
+ const { outputPath, outputFs } = await resolveWriteDestination(options, resolveDefaultMcpOutputPath, { suffix: outputSuffix });
506
508
  const template = buildSentinelayerSessionRegistryTemplate();
507
509
  validateMcpToolRegistry(template);
508
- const writtenPath = await writeJsonFile(outputPath, template, { force: Boolean(options.force) });
510
+ const writtenPath = await writeJsonFile(outputPath, template, { force: Boolean(options.force), outputFs });
509
511
  const payload = {
510
512
  command: payloadCommand,
511
513
  outputPath: writtenPath,
@@ -553,17 +555,12 @@ Docs: https://github.com/mrrCarter/create-sentinelayer/blob/main/docs/mcp.md`,
553
555
  .option("--force", "Overwrite destination file if it already exists")
554
556
  .option("--json", "Emit machine-readable output")
555
557
  .action(async (options, command) => {
556
- const defaultPath = await resolveDefaultHostedSentiSessionConnectorContractPath({
557
- cwd: process.cwd(),
558
- outputDir: options.outputDir,
559
- env: process.env,
560
- });
561
- const outputPath = normalizeOutputPath(options.path, defaultPath);
558
+ const { outputPath, outputFs } = await resolveWriteDestination(options, resolveDefaultHostedSentiSessionConnectorContractPath);
562
559
  const template = buildHostedSentiSessionConnectorContract({
563
560
  localRegistryFile: options.registryFile,
564
561
  });
565
562
  validateHostedSentiSessionConnectorContract(template);
566
- const writtenPath = await writeJsonFile(outputPath, template, { force: Boolean(options.force) });
563
+ const writtenPath = await writeJsonFile(outputPath, template, { force: Boolean(options.force), outputFs });
567
564
  const payload = {
568
565
  command: "mcp registry init-hosted-session-connector",
569
566
  outputPath: writtenPath,
@@ -588,15 +585,10 @@ Docs: https://github.com/mrrCarter/create-sentinelayer/blob/main/docs/mcp.md`,
588
585
  .option("--force", "Overwrite destination file if it already exists")
589
586
  .option("--json", "Emit machine-readable output")
590
587
  .action(async (options, command) => {
591
- const defaultPath = await resolveDefaultMcpOutputPath({
592
- cwd: process.cwd(),
593
- outputDir: options.outputDir,
594
- env: process.env,
595
- });
596
- const outputPath = normalizeOutputPath(options.path, defaultPath.replace(".schema", ".cli-tools"));
588
+ const { outputPath, outputFs } = await resolveWriteDestination(options, resolveDefaultMcpOutputPath, { suffix: ".cli-tools" });
597
589
  const template = await buildSentinelayerCliRegistryTemplate();
598
590
  validateMcpToolRegistry(template);
599
- const writtenPath = await writeJsonFile(outputPath, template, { force: Boolean(options.force) });
591
+ const writtenPath = await writeJsonFile(outputPath, template, { force: Boolean(options.force), outputFs });
600
592
  const payload = {
601
593
  command: "mcp registry init-cli",
602
594
  outputPath: writtenPath,
@@ -805,19 +797,13 @@ Docs: https://github.com/mrrCarter/create-sentinelayer/blob/main/docs/mcp.md`,
805
797
  const serverId = String(options.id || "")
806
798
  .trim()
807
799
  .toLowerCase();
808
- const defaultPath = await resolveDefaultMcpServerConfigPath({
809
- cwd: process.cwd(),
810
- outputDir: options.outputDir,
811
- env: process.env,
812
- serverId,
813
- });
814
- const outputPath = normalizeOutputPath(options.path, defaultPath);
800
+ const { outputPath, outputFs } = await resolveWriteDestination(options, resolveDefaultMcpServerConfigPath, { serverId });
815
801
  const template = buildMcpServerConfigTemplate({
816
802
  serverId,
817
803
  registryFile: options.registryFile,
818
804
  });
819
805
  const config = validateMcpServerConfig(template);
820
- const writtenPath = await writeJsonFile(outputPath, config, { force: Boolean(options.force) });
806
+ const writtenPath = await writeJsonFile(outputPath, config, { force: Boolean(options.force), outputFs });
821
807
 
822
808
  const payload = {
823
809
  command: "mcp server init",
@@ -924,7 +910,14 @@ Docs: https://github.com/mrrCarter/create-sentinelayer/blob/main/docs/mcp.md`,
924
910
  serverId,
925
911
  serverConfigFile: String(options.serverConfig || "").trim(),
926
912
  });
927
- const writtenPath = await writeJsonFile(outputPath, bridgePayload, { force: Boolean(options.force) });
913
+ const outputFs = String(options.path || "").trim()
914
+ ? (await resolveOutputContext({
915
+ cwd: process.cwd(),
916
+ outputDirOverride: options.path,
917
+ env: process.env,
918
+ })).outputFs
919
+ : createOutputFileSystem({ workspaceRoot: process.cwd() });
920
+ const writtenPath = await writeJsonFile(outputPath, bridgePayload, { force: Boolean(options.force), outputFs });
928
921
 
929
922
  const payload = {
930
923
  command: "mcp bridge init-vscode",
@@ -19,7 +19,8 @@ export function registerOmarGateCommand(program, invokeLegacy) {
19
19
  .option("--max-parallel <n>", "Max concurrent persona calls (default: 4)")
20
20
  .option("--model <id>", "LLM model override (default: gpt-5.3-codex)")
21
21
  .option("--provider <name>", "LLM provider: sentinelayer, openai, anthropic")
22
- .option("--max-cost <usd>", "Maximum AI layer cost in USD (default: 5.0)")
22
+ .option("--max-cost <usd>", "Reported AI cost stop threshold in USD (default: 5.0), not a pre-dispatch spending cap; with TypeSafe evaluation only, explicit 0 means NO monetary cap")
23
+ .option("--severity-classifier <mode>", "Opt-in classification evaluation: off|typesafe; sends generated finding evidence to TypeSafe, never changes gate severities (positive cost caps skip)", "off")
23
24
  .option("--notify-session <session-id>", "Write Omar Deep billing-grade usage entries into this Senti session (default: auto-detect active Senti session)")
24
25
  .option("--require-usage-ledger", "Fail if Omar Deep AI calls cannot write billing-grade session_usage")
25
26
  .option("--persona <csv>", "Only run these personas (comma-separated IDs); unknown IDs are dropped + warned")
@@ -2,6 +2,7 @@ import path from "node:path";
2
2
  import process from "node:process";
3
3
 
4
4
  import pc from "picocolors";
5
+ import { resolveOutputContext } from "../config/service.js";
5
6
 
6
7
  import {
7
8
  buildPluginManifestTemplate,
@@ -61,11 +62,15 @@ export function registerPluginCommand(program) {
61
62
  const pluginId = normalizePluginId(options.id);
62
63
  const packType = normalizePluginPackType(options.packType);
63
64
  const stage = normalizePluginLoadStage(options.stage);
64
- const defaultPath = await resolveDefaultPluginManifestPath({
65
+ const explicitPath = String(options.path || "").trim();
66
+ const outputContext = await resolveOutputContext({
65
67
  cwd: process.cwd(),
66
- outputDir: options.outputDir,
68
+ outputDirOverride: explicitPath || options.outputDir,
67
69
  env: process.env,
70
+ });
71
+ const defaultPath = explicitPath ? undefined : await resolveDefaultPluginManifestPath({
68
72
  pluginId,
73
+ outputContext,
69
74
  });
70
75
  const outputPath = normalizeOutputPath(options.path, defaultPath);
71
76
  const template = buildPluginManifestTemplate({
@@ -74,7 +79,10 @@ export function registerPluginCommand(program) {
74
79
  stage,
75
80
  });
76
81
  const manifest = validatePluginManifest(template);
77
- const writtenPath = await writeJsonFile(outputPath, manifest, { force: Boolean(options.force) });
82
+ const writtenPath = await writeJsonFile(outputPath, manifest, {
83
+ force: Boolean(options.force),
84
+ outputFs: outputContext.outputFs,
85
+ });
78
86
 
79
87
  const payload = {
80
88
  command: "plugin init",
@@ -1,4 +1,3 @@
1
- import fsp from "node:fs/promises";
2
1
  import path from "node:path";
3
2
  import process from "node:process";
4
3
 
@@ -26,6 +25,7 @@ import {
26
25
  writeReviewRunContext,
27
26
  } from "../review/replay.js";
28
27
  import { resolveActionOptions } from "./action-options.js";
28
+ import { resolveOutputContext } from "../config/service.js";
29
29
 
30
30
  function shouldEmitJson(options, command) {
31
31
  const local = Boolean(options && options.json);
@@ -134,12 +134,14 @@ async function executeReviewRun({
134
134
  specFile,
135
135
  });
136
136
  const unifiedArtifacts = await writeUnifiedReviewArtifacts({
137
+ outputFs: deterministic.artifacts.outputFs,
137
138
  runDirectory: deterministic.artifacts.runDirectory,
138
139
  report: unified.report,
139
140
  markdown: unified.markdown,
140
141
  });
141
142
 
142
143
  const contextWrite = await writeReviewRunContext({
144
+ outputFs: deterministic.artifacts.outputFs,
143
145
  runDirectory: deterministic.artifacts.runDirectory,
144
146
  runId: deterministic.runId,
145
147
  targetPath: deterministic.targetPath,
@@ -511,6 +513,7 @@ export function registerReviewCommand(program) {
511
513
  const comparison = compareUnifiedReports(source.report, replayOutcome.unified.report);
512
514
  const artifact = await writeReviewComparisonArtifact({
513
515
  runDirectory: replayOutcome.deterministic.artifacts.runDirectory,
516
+ outputFs: replayOutcome.deterministic.artifacts.outputFs,
514
517
  baseRunId: source.report.runId,
515
518
  candidateRunId: replayOutcome.unified.report.runId,
516
519
  comparison,
@@ -571,6 +574,7 @@ export function registerReviewCommand(program) {
571
574
  const comparison = compareUnifiedReports(base.report, candidate.report);
572
575
  const artifact = await writeReviewComparisonArtifact({
573
576
  runDirectory: candidate.runDirectory,
577
+ outputFs: candidate.outputFs,
574
578
  baseRunId: base.report.runId,
575
579
  candidateRunId: candidate.report.runId,
576
580
  comparison,
@@ -578,9 +582,10 @@ export function registerReviewCommand(program) {
578
582
 
579
583
  let outputPath = artifact.artifactPath;
580
584
  if (actionOptions.outputFile) {
581
- outputPath = path.resolve(targetPath, String(actionOptions.outputFile));
582
- await fsp.mkdir(path.dirname(outputPath), { recursive: true });
583
- await fsp.writeFile(outputPath, `${JSON.stringify(artifact.payload, null, 2)}\n`, "utf-8");
585
+ const outputContext = await resolveOutputContext({ cwd: targetPath, outputDirOverride: String(actionOptions.outputFile) });
586
+ outputPath = outputContext.outputRoot;
587
+ await outputContext.outputFs.mkdir(path.dirname(outputPath), { recursive: true });
588
+ await outputContext.outputFs.writeFile(outputPath, `${JSON.stringify(artifact.payload, null, 2)}\n`, "utf-8");
584
589
  }
585
590
 
586
591
  const payload = {
@@ -12,7 +12,7 @@ import {
12
12
  resolveProvider,
13
13
  } from "../ai/client.js";
14
14
  import { recordCliLlmSessionUsage, usageNumber } from "../billing/llm-session-usage.js";
15
- import { loadConfig, resolveOutputRoot, userConfigValues } from "../config/service.js";
15
+ import { loadConfig, resolveOutputContext, userConfigValues } from "../config/service.js";
16
16
  import { evaluateBudget } from "../cost/budget.js";
17
17
  import { appendCostEntry, summarizeCostHistory } from "../cost/history.js";
18
18
  import { estimateModelCost } from "../cost/tracker.js";
@@ -292,15 +292,15 @@ async function resolvePreScanReportPath({
292
292
  } = {}) {
293
293
  const explicit = String(outputFile || "").trim();
294
294
  if (explicit) {
295
- return path.resolve(targetPath, explicit);
295
+ return { reportPath: path.resolve(targetPath, explicit), outputFs: fsp };
296
296
  }
297
297
 
298
- const outputRoot = await resolveOutputRoot({
298
+ const { outputRoot, outputFs } = await resolveOutputContext({
299
299
  cwd: targetPath,
300
300
  outputDirOverride,
301
301
  env: process.env,
302
302
  });
303
- return path.join(outputRoot, "reports", `scan-precheck-${createTimestampToken()}.md`);
303
+ return { reportPath: path.join(outputRoot, "reports", `scan-precheck-${createTimestampToken()}.md`), outputFs };
304
304
  }
305
305
 
306
306
  function printAiPreScanSummary({ reportPath, ai }) {
@@ -610,13 +610,13 @@ export function registerScanCommand(program) {
610
610
  model: response.model,
611
611
  aiMarkdown,
612
612
  });
613
- const reportPath = await resolvePreScanReportPath({
613
+ const { reportPath, outputFs } = await resolvePreScanReportPath({
614
614
  targetPath,
615
615
  outputDirOverride: options.outputDir,
616
616
  outputFile: options.outputFile,
617
617
  });
618
- await fsp.mkdir(path.dirname(reportPath), { recursive: true });
619
- await fsp.writeFile(reportPath, reportMarkdown, "utf-8");
618
+ await outputFs.mkdir(path.dirname(reportPath), { recursive: true });
619
+ await outputFs.writeFile(reportPath, reportMarkdown, "utf-8");
620
620
 
621
621
  const estimatedInputTokens = estimateTokens(prompt, { model: response.model });
622
622
  const estimatedOutputTokens = estimateTokens(aiMarkdown, { model: response.model });