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
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import path from "node:path";
3
+ import { requireOutputFileSystem } from "../config/output-filesystem.js";
3
4
 
4
5
  import { createMultiProviderApiClient } from "../ai/client.js";
5
6
  import { serializeProxyError } from "../ai/proxy.js";
@@ -376,6 +377,7 @@ export function createIsolatedPersonaContext({
376
377
  runId = "",
377
378
  rootPath = ".",
378
379
  artifactDir = "",
380
+ outputFs,
379
381
  provider = null,
380
382
  budget = {},
381
383
  onEvent = null,
@@ -384,6 +386,7 @@ export function createIsolatedPersonaContext({
384
386
  isolation = DEFAULT_ISOLATION_MODE,
385
387
  eventContext = null,
386
388
  } = {}) {
389
+ if (artifactDir) requireOutputFileSystem(outputFs);
387
390
  const normalizedAgent = normalizeAuditPersona(agent || {});
388
391
  const resolvedRootPath = path.resolve(String(rootPath || "."));
389
392
  const resolvedRunId =
@@ -399,6 +402,7 @@ export function createIsolatedPersonaContext({
399
402
  budget: loopBudget,
400
403
  runId: resolvedRunId,
401
404
  artifactDir,
405
+ outputFs,
402
406
  onEvent,
403
407
  });
404
408
  const emitter = (event, payload = {}) => {
@@ -701,6 +705,7 @@ async function runSinglePersonaAgenticLoop({
701
705
  sharedContext = null,
702
706
  hybridContext = null,
703
707
  artifactDir = "",
708
+ outputFs,
704
709
  provider = null,
705
710
  budget = {},
706
711
  maxTurns = null,
@@ -722,6 +727,7 @@ async function runSinglePersonaAgenticLoop({
722
727
  runId: requestedRunId,
723
728
  rootPath: resolvedRootPath,
724
729
  artifactDir,
730
+ outputFs,
725
731
  provider,
726
732
  budget,
727
733
  onEvent,
@@ -1053,6 +1059,7 @@ function summarizeSwarmStatus(results = []) {
1053
1059
  }
1054
1060
 
1055
1061
  export async function runPersonaAgenticLoop(options = {}) {
1062
+ if (options.artifactDir) requireOutputFileSystem(options.outputFs);
1056
1063
  const {
1057
1064
  agent,
1058
1065
  rootPath,
@@ -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
 
4
4
  function normalizeString(value) {
@@ -87,15 +87,17 @@ export function compareAuditReports(baseReport = {}, candidateReport = {}) {
87
87
  }
88
88
 
89
89
  export async function writeAuditComparisonArtifact({
90
+ outputFs,
90
91
  baseReport = {},
91
92
  candidateReport = {},
92
93
  outputDirectory = "",
93
94
  } = {}) {
95
+ requireOutputFileSystem(outputFs);
94
96
  const resolvedOutputDirectory = path.resolve(String(outputDirectory || "."));
95
97
  const comparison = compareAuditReports(baseReport, candidateReport);
96
98
  const fileName = `AUDIT_COMPARISON_${comparison.baseRunId}_vs_${comparison.candidateRunId}.json`;
97
99
  const outputPath = path.join(resolvedOutputDirectory, fileName);
98
- await fsp.writeFile(outputPath, `${JSON.stringify(comparison, null, 2)}\n`, "utf-8");
100
+ await outputFs.writeFile(outputPath, `${JSON.stringify(comparison, null, 2)}\n`, "utf-8");
99
101
  return {
100
102
  comparison,
101
103
  outputPath,
package/src/auth/gate.js CHANGED
@@ -17,6 +17,8 @@ import { isAuthenticated } from "./credential-destinations.js";
17
17
  * - auth login, auth status, auth --help
18
18
  * - --help, --version, -h, -v
19
19
  * - config (read-only config inspection)
20
+ * - skills install/list (local packaged files only)
21
+ * - judge check (validates a local verdict file; sends nothing)
20
22
  *
21
23
  * All other commands require a valid session.
22
24
  * If not authenticated, prints instructions and exits.
@@ -31,7 +33,7 @@ const AUTH_BYPASS_COMMANDS = new Set([
31
33
  "-v",
32
34
  ]);
33
35
 
34
- // Commands that work without auth (read-only, no API calls)
36
+ // Commands that work without auth (local only, no API calls)
35
37
  const NO_AUTH_REQUIRED = new Set([
36
38
  "config", // local config inspection
37
39
  ]);
@@ -331,6 +333,25 @@ function isAuthenticatedSessionValid(session) {
331
333
  return true;
332
334
  }
333
335
 
336
+ /**
337
+ * A help request a newcomer can make before signing in (`sl session --help`, `sl session say --help`): the help flag
338
+ * is the LAST argument and every argument before it is a plain word (command, subcommand, positional). Then no option
339
+ * can take "--help" as its value (`--message-file --help` stays gated), so Commander prints help and runs nothing.
340
+ * The caller applies it only to Commander-dispatched commands. Tokens are judged exactly as Commander will see them,
341
+ * never trimmed: `" --help"` is message text to Commander, so it must not skip the gate either.
342
+ *
343
+ * @param {string[]} args - CLI arguments (after normalization)
344
+ * @returns {boolean}
345
+ */
346
+ export function isHelpOnlyInvocation(args = []) {
347
+ const list = Array.isArray(args) ? args : [];
348
+ const last = list[list.length - 1];
349
+ if (last !== "--help" && last !== "-h") {
350
+ return false;
351
+ }
352
+ return list.slice(0, -1).every((arg) => typeof arg === "string" && arg !== "" && !arg.startsWith("-"));
353
+ }
354
+
334
355
  /**
335
356
  * Check if the current command requires authentication.
336
357
  * Returns true if auth is required but user is not logged in.
@@ -349,6 +370,16 @@ export async function checkAuthGate(args) {
349
370
  return { authenticated: true, session: null, bypassReason: "no_auth_required", failureReason: null };
350
371
  }
351
372
 
373
+ // These two commands only inspect/copy packaged files. Future skills subcommands stay gated.
374
+ if (args[0] === "skills" && (args[1] === "install" || args[1] === "list")) {
375
+ return { authenticated: true, session: null, bypassReason: "local_skills", failureReason: null };
376
+ }
377
+
378
+ // Validating a verdict file is local; `judge verdict` sends, so it stays gated.
379
+ if (args[0] === "judge" && args[1] === "check") {
380
+ return { authenticated: true, session: null, bypassReason: "local_judge_check", failureReason: null };
381
+ }
382
+
352
383
  if (isSessionNoAuthCommand(args)) {
353
384
  return { authenticated: true, session: null, bypassReason: "session_no_auth_required", failureReason: null };
354
385
  }
@@ -19,6 +19,19 @@ const SESSION_WARNING_ALLOWED_FIELDS = new Set([
19
19
  ]);
20
20
  const emittedSessionWarningKeys = new Set();
21
21
  let keytarClientOverrideForTests;
22
+ let sessionOperationTail = Promise.resolve();
23
+
24
+ // A store operation spans metadata, keyring and file publication/cleanup. Reads
25
+ // can migrate plaintext too. Keep that transaction in order within this module;
26
+ // serializing only rename would still permit stale migrations and logout races.
27
+ // One queue also covers path aliases and keyring accounts shared across homes.
28
+ // Other processes still rely on the existing key publication and bounded retries.
29
+ function runSessionOperation(operation) {
30
+ const result = sessionOperationTail.then(operation);
31
+ // A failed operation must not poison the queue or retain returned credentials.
32
+ sessionOperationTail = result.then(() => undefined, () => undefined);
33
+ return result;
34
+ }
22
35
 
23
36
  function nowIso() {
24
37
  return new Date().toISOString();
@@ -600,6 +613,11 @@ async function tryDecryptFileToken({ metadata, homeDir }) {
600
613
  * }>}
601
614
  */
602
615
  export async function readStoredSession({ homeDir } = {}) {
616
+ const options = { homeDir: resolveHomeDir(homeDir) };
617
+ return runSessionOperation(() => readStoredSessionUnlocked(options));
618
+ }
619
+
620
+ async function readStoredSessionUnlocked({ homeDir } = {}) {
603
621
  const { filePath, metadata } = await readMetadata({ homeDir });
604
622
  if (!metadata) {
605
623
  return null;
@@ -711,6 +729,11 @@ export async function readStoredSession({ homeDir } = {}) {
711
729
  * }>}
712
730
  */
713
731
  export async function readStoredSessionMetadata({ homeDir } = {}) {
732
+ const options = { homeDir: resolveHomeDir(homeDir) };
733
+ return runSessionOperation(() => readStoredSessionMetadataUnlocked(options));
734
+ }
735
+
736
+ async function readStoredSessionMetadataUnlocked({ homeDir } = {}) {
714
737
  const { filePath, metadata } = await readMetadata({ homeDir });
715
738
  if (!metadata) {
716
739
  return null;
@@ -776,6 +799,15 @@ export async function writeStoredSession(
776
799
  aidenid = null,
777
800
  } = {},
778
801
  { homeDir } = {}
802
+ ) {
803
+ const session = { apiUrl, token, tokenId, tokenPrefix, tokenExpiresAt, user, aidenid };
804
+ const options = { homeDir: resolveHomeDir(homeDir) };
805
+ return runSessionOperation(() => writeStoredSessionUnlocked(session, options));
806
+ }
807
+
808
+ async function writeStoredSessionUnlocked(
809
+ { apiUrl, token, tokenId, tokenPrefix, tokenExpiresAt, user, aidenid },
810
+ { homeDir }
779
811
  ) {
780
812
  const normalizedApiUrl = String(apiUrl || "").trim();
781
813
  const normalizedToken = String(token || "").trim();
@@ -838,6 +870,11 @@ export async function writeStoredSession(
838
870
  * @returns {Promise<{ filePath: string, hadSession: boolean }>}
839
871
  */
840
872
  export async function clearStoredSession({ homeDir } = {}) {
873
+ const options = { homeDir: resolveHomeDir(homeDir) };
874
+ return runSessionOperation(() => clearStoredSessionUnlocked(options));
875
+ }
876
+
877
+ async function clearStoredSessionUnlocked({ homeDir } = {}) {
841
878
  const { filePath, metadata } = await readMetadata({ homeDir });
842
879
  if (metadata && metadata.storage === "keyring") {
843
880
  const keytar = await loadKeytarClient({ allowImplicit: true });
@@ -30,7 +30,7 @@
30
30
  * refused with an Error, which the gate reports as cannot-verify.
31
31
  */
32
32
 
33
- import { ghCommitSha, ghRefSegment, ghRepoSlug } from "../net/gh-api-path.js";
33
+ import { ghCheckRunsApiPath, ghCompareApiPath, ghRepoSlug } from "../net/gh-api-path.js";
34
34
 
35
35
  /** Shape a resolver must return. Anything else is treated as indeterminate. */
36
36
  const YES = true;
@@ -74,8 +74,7 @@ export function createEvidenceResolver({ run, probeDeployedSha } = {}) {
74
74
  }
75
75
 
76
76
  async function shaOnBranch({ repo, sha, branch }) {
77
- const basehead = `${ghRefSegment(branch, { label: "branch" })}...${ghCommitSha(sha)}`;
78
- const res = await run(["api", `repos/${ghRepoSlug(repo)}/compare/${basehead}`, "--jq", ".status"]);
77
+ const res = await run(["api", ghCompareApiPath({ repo, branch, sha }), "--jq", ".status"]);
79
78
  if (!res || !res.ok) return UNKNOWN;
80
79
  const status = String(res.stdout ?? "").trim();
81
80
  // GitHub's compare status, base=branch head=sha:
@@ -87,7 +86,7 @@ export function createEvidenceResolver({ run, probeDeployedSha } = {}) {
87
86
  }
88
87
 
89
88
  async function checkConclusive({ repo, ref, check }) {
90
- const res = await run(["api", `repos/${ghRepoSlug(repo)}/commits/${ghRefSegment(ref)}/check-runs`, "--jq", ".check_runs"]);
89
+ const res = await run(["api", ghCheckRunsApiPath({ repo, ref }), "--jq", ".check_runs"]);
91
90
  if (!res || !res.ok) return UNKNOWN;
92
91
  const runs = parseJson(res.stdout);
93
92
  if (!Array.isArray(runs)) return UNKNOWN;
package/src/cli.js CHANGED
@@ -5,6 +5,16 @@ import { CLI_VERSION, runLegacyCliWithErrorHandling } from "./legacy-cli.js";
5
5
  import { restrictProgramLookupToPath } from "./program-lookup.js";
6
6
 
7
7
  const COMMAND_REGISTRARS = {
8
+ skills: {
9
+ loader: () => import("./commands/skills.js"),
10
+ exportName: "registerSkillsCommand",
11
+ needsLegacy: false,
12
+ },
13
+ judge: {
14
+ loader: () => import("./commands/judge.js"),
15
+ exportName: "registerJudgeCommand",
16
+ needsLegacy: false,
17
+ },
8
18
  init: {
9
19
  loader: () => import("./commands/init.js"),
10
20
  exportName: "registerInitCommand",
@@ -262,9 +272,11 @@ export async function runCli(rawArgs = process.argv.slice(2)) {
262
272
  // Normalize slash commands (/omargate → omargate, /audit → audit, etc.)
263
273
  const normalizedArgs = normalizeSlashArgs(rawArgs);
264
274
 
265
- // Auth gate — require login for all commands except auth/help/version/config
266
- const { checkAuthGate, printAuthRequired } = await import("./auth/gate.js");
267
- const authResult = await checkAuthGate(normalizedArgs);
275
+ // Auth gate — require login for all commands except auth/help/version/config. Help for a Commander command prints
276
+ // without login too: Commander prints it and runs nothing (a newcomer reads `sl session --help` first).
277
+ const { checkAuthGate, isHelpOnlyInvocation, printAuthRequired } = await import("./auth/gate.js");
278
+ const helpOnly = !shouldBypassCommander(normalizedArgs) && isHelpOnlyInvocation(normalizedArgs);
279
+ const authResult = helpOnly ? { authenticated: true } : await checkAuthGate(normalizedArgs);
268
280
  if (!authResult.authenticated) {
269
281
  printAuthRequired(authResult.failureReason);
270
282
  return;
@@ -272,6 +284,14 @@ export async function runCli(rawArgs = process.argv.slice(2)) {
272
284
 
273
285
  if (shouldBypassCommander(normalizedArgs)) {
274
286
  await runLegacyCliWithErrorHandling(normalizedArgs);
287
+ if (["--help", "-h", "help"].includes(normalizedArgs[0])) {
288
+ console.log("\nAgent Skills:");
289
+ console.log(" sl skills install [names...] [--dir <path>] [--force] [--json] Install bundled Senti skills (all, or the named ones)");
290
+ console.log(" sl skills list [--dir <path>] [--json] Show bundled and installed versions/hashes");
291
+ console.log("\nJudge Loop:");
292
+ console.log(" sl judge check --file <verdict.json> [--json] Validate a verdict offline and preview the message");
293
+ console.log(" sl judge verdict <sessionId> --agent <id> --file <verdict.json> Send the verdict to the coder");
294
+ }
275
295
  return;
276
296
  }
277
297
 
@@ -22,7 +22,7 @@ import {
22
22
  recordProvisionedIdentity,
23
23
  updateIdentityStatus,
24
24
  } from "../../ai/identity-store.js";
25
- import { resolveOutputRoot } from "../../config/service.js";
25
+ import { resolveOutputRoot, resolveOutputContext } from "../../config/service.js";
26
26
  import {
27
27
  delay,
28
28
  hasExtractionSignal,
@@ -145,7 +145,7 @@ legalHold
145
145
  .action(async (identityId, options, command) => {
146
146
  const emitJson = shouldEmitJson(options, command);
147
147
  const targetPath = path.resolve(process.cwd(), String(options.path || "."));
148
- const outputRoot = await resolveOutputRoot({
148
+ const { outputRoot, outputFs } = await resolveOutputContext({
149
149
  cwd: targetPath,
150
150
  outputDirOverride: options.outputDir,
151
151
  env: process.env,
@@ -172,7 +172,7 @@ legalHold
172
172
  status,
173
173
  underHold,
174
174
  identity: identityRecord,
175
- });
175
+ }, outputFs);
176
176
 
177
177
  const payload = {
178
178
  command: "ai identity legal-hold status",
@@ -204,7 +204,7 @@ identity
204
204
  .action(async (options, command) => {
205
205
  const emitJson = shouldEmitJson(options, command);
206
206
  const targetPath = path.resolve(process.cwd(), String(options.path || "."));
207
- const outputRoot = await resolveOutputRoot({
207
+ const { outputRoot, outputFs } = await resolveOutputContext({
208
208
  cwd: targetPath,
209
209
  outputDirOverride: options.outputDir,
210
210
  env: process.env,
@@ -223,7 +223,7 @@ identity
223
223
  staleCount: staleScan.stale.length,
224
224
  totalCount: staleScan.identities.length,
225
225
  identities,
226
- });
226
+ }, outputFs);
227
227
 
228
228
  const payload = {
229
229
  command: "ai identity audit",
@@ -269,7 +269,7 @@ identity
269
269
  throw new Error("At least one tag is required via --tags.");
270
270
  }
271
271
  const targetPath = path.resolve(process.cwd(), String(options.path || "."));
272
- const outputRoot = await resolveOutputRoot({
272
+ const { outputRoot, outputFs } = await resolveOutputContext({
273
273
  cwd: targetPath,
274
274
  outputDirOverride: options.outputDir,
275
275
  env: process.env,
@@ -293,7 +293,7 @@ identity
293
293
  candidateIdentityIds: candidates.map((item) => item.identityId),
294
294
  blockedIdentityIds: blocked.map((item) => item.identityId),
295
295
  eligibleIdentityIds: eligible.map((item) => item.identityId),
296
- });
296
+ }, outputFs);
297
297
 
298
298
  if (!options.execute) {
299
299
  const payload = {
@@ -345,6 +345,7 @@ identity
345
345
  revokeResponse = revokeExecution.response || null;
346
346
  }
347
347
  const updated = await updateIdentityStatus({
348
+ outputFs,
348
349
  outputRoot,
349
350
  identityId: identityRecord.identityId,
350
351
  status: "SQUASHED",
@@ -379,7 +380,7 @@ identity
379
380
  updated: updates,
380
381
  apiCalled: canCallApi,
381
382
  credentialsMissing: resolvedCredentials.missing,
382
- });
383
+ }, outputFs);
383
384
 
384
385
  const payload = {
385
386
  command: "ai identity kill-all",
@@ -426,7 +427,7 @@ identity
426
427
  .action(async (identityId, options, command) => {
427
428
  const emitJson = shouldEmitJson(options, command);
428
429
  const targetPath = path.resolve(process.cwd(), String(options.path || "."));
429
- const outputRoot = await resolveOutputRoot({
430
+ const { outputRoot, outputFs } = await resolveOutputContext({
430
431
  cwd: targetPath,
431
432
  outputDirOverride: options.outputDir,
432
433
  env: process.env,
@@ -441,7 +442,7 @@ identity
441
442
  apiUrl,
442
443
  idempotencyKey,
443
444
  identityId,
444
- });
445
+ }, outputFs);
445
446
 
446
447
  const { registryPath, identity: trackedIdentity } = await getIdentityById({
447
448
  outputRoot,
@@ -472,7 +473,7 @@ identity
472
473
  credentialsMissing: resolvedCredentials.missing,
473
474
  trackedIdentity,
474
475
  curlPreview: [
475
- `curl -X POST ${apiUrl}/v1/identities/${urlPathSegment(identityId, { label: "identityId" })}/revoke \\`,
476
+ `curl -X POST ${apiUrl}/v1/identities/${urlPathSegment(identityId, { label: "identityId" })}/squash \\`,
476
477
  ` -H \"Authorization: Bearer $AIDENID_API_KEY\" \\`,
477
478
  ` -H \"X-Org-Id: $AIDENID_ORG_ID\" \\`,
478
479
  ` -H \"X-Project-Id: $AIDENID_PROJECT_ID\" \\`,
@@ -517,10 +518,11 @@ identity
517
518
  apiUrl,
518
519
  idempotencyKey,
519
520
  response: execution.response,
520
- });
521
+ }, outputFs);
521
522
 
522
523
  const revokedAt = String(execution.response?.revokedAt || "").trim() || new Date().toISOString();
523
524
  const updated = await updateIdentityStatus({
525
+ outputFs,
524
526
  outputRoot,
525
527
  identityId,
526
528
  status: String(execution.response?.status || "REVOKED"),
@@ -576,7 +578,7 @@ identity
576
578
  .action(async (parentIdentityId, options, command) => {
577
579
  const emitJson = shouldEmitJson(options, command);
578
580
  const targetPath = path.resolve(process.cwd(), String(options.path || "."));
579
- const outputRoot = await resolveOutputRoot({
581
+ const { outputRoot, outputFs } = await resolveOutputContext({
580
582
  cwd: targetPath,
581
583
  outputDirOverride: options.outputDir,
582
584
  env: process.env,
@@ -617,7 +619,7 @@ identity
617
619
  idempotencyKey,
618
620
  parentIdentityId,
619
621
  payload,
620
- });
622
+ }, outputFs);
621
623
 
622
624
  const resolvedCredentials = await resolveAidenIdCredentials({
623
625
  apiKey: options.apiKey,
@@ -695,9 +697,10 @@ identity
695
697
  idempotencyKey,
696
698
  parentIdentityId,
697
699
  response: execution.response,
698
- });
700
+ }, outputFs);
699
701
 
700
702
  const registryUpdate = await recordProvisionedIdentity({
703
+ outputFs,
701
704
  outputRoot,
702
705
  response: execution.response || {},
703
706
  context: {
@@ -758,7 +761,7 @@ identity
758
761
  .action(async (identityId, options, command) => {
759
762
  const emitJson = shouldEmitJson(options, command);
760
763
  const targetPath = path.resolve(process.cwd(), String(options.path || "."));
761
- const outputRoot = await resolveOutputRoot({
764
+ const { outputRoot, outputFs } = await resolveOutputContext({
762
765
  cwd: targetPath,
763
766
  outputDirOverride: options.outputDir,
764
767
  env: process.env,
@@ -796,7 +799,7 @@ identity
796
799
  generatedAt: new Date().toISOString(),
797
800
  identityId,
798
801
  response: execution.response,
799
- });
802
+ }, outputFs);
800
803
 
801
804
  const rows = renderLineageRows({
802
805
  nodes: execution.nodes,
@@ -843,7 +846,7 @@ identity
843
846
  .action(async (identityId, options, command) => {
844
847
  const emitJson = shouldEmitJson(options, command);
845
848
  const targetPath = path.resolve(process.cwd(), String(options.path || "."));
846
- const outputRoot = await resolveOutputRoot({
849
+ const { outputRoot, outputFs } = await resolveOutputContext({
847
850
  cwd: targetPath,
848
851
  outputDirOverride: options.outputDir,
849
852
  env: process.env,
@@ -865,7 +868,7 @@ identity
865
868
  apiUrl,
866
869
  idempotencyKey,
867
870
  identityId,
868
- });
871
+ }, outputFs);
869
872
 
870
873
  const resolvedCredentials = await resolveAidenIdCredentials({
871
874
  apiKey: options.apiKey,
@@ -937,12 +940,13 @@ identity
937
940
  idempotencyKey,
938
941
  identityId,
939
942
  response: execution.response,
940
- });
943
+ }, outputFs);
941
944
 
942
945
  const updateResults = await Promise.all(
943
946
  execution.revokedIdentityIds.map(async (revokedIdentityId) => {
944
947
  try {
945
948
  const updated = await updateIdentityStatus({
949
+ outputFs,
946
950
  outputRoot,
947
951
  identityId: revokedIdentityId,
948
952
  status: "SQUASHED",
@@ -1003,7 +1007,7 @@ identity
1003
1007
  .action(async (identityId, options, command) => {
1004
1008
  const emitJson = shouldEmitJson(options, command);
1005
1009
  const targetPath = path.resolve(process.cwd(), String(options.path || "."));
1006
- const outputRoot = await resolveOutputRoot({
1010
+ const { outputRoot, outputFs } = await resolveOutputContext({
1007
1011
  cwd: targetPath,
1008
1012
  outputDirOverride: options.outputDir,
1009
1013
  env: process.env,
@@ -1048,7 +1052,7 @@ identity
1048
1052
  cursor: String(options.cursor || "").trim() || null,
1049
1053
  limit,
1050
1054
  response: execution.response,
1051
- });
1055
+ }, outputFs);
1052
1056
 
1053
1057
  const payload = {
1054
1058
  command: "ai identity events",
@@ -1096,7 +1100,7 @@ identity
1096
1100
  .action(async (identityId, options, command) => {
1097
1101
  const emitJson = shouldEmitJson(options, command);
1098
1102
  const targetPath = path.resolve(process.cwd(), String(options.path || "."));
1099
- const outputRoot = await resolveOutputRoot({
1103
+ const { outputRoot, outputFs } = await resolveOutputContext({
1100
1104
  cwd: targetPath,
1101
1105
  outputDirOverride: options.outputDir,
1102
1106
  env: process.env,
@@ -1149,7 +1153,7 @@ identity
1149
1153
  latestEvent,
1150
1154
  extraction: latestExtraction.extraction,
1151
1155
  extractionResponse: latestExtraction.response,
1152
- });
1156
+ }, outputFs);
1153
1157
 
1154
1158
  const payload = {
1155
1159
  command: "ai identity latest",
@@ -1207,7 +1211,7 @@ identity
1207
1211
  .action(async (identityId, options, command) => {
1208
1212
  const emitJson = shouldEmitJson(options, command);
1209
1213
  const targetPath = path.resolve(process.cwd(), String(options.path || "."));
1210
- const outputRoot = await resolveOutputRoot({
1214
+ const { outputRoot, outputFs } = await resolveOutputContext({
1211
1215
  cwd: targetPath,
1212
1216
  outputDirOverride: options.outputDir,
1213
1217
  env: process.env,
@@ -1309,7 +1313,7 @@ identity
1309
1313
  extraction: lastExtraction,
1310
1314
  },
1311
1315
  };
1312
- await writeArtifact(outputPath, artifactPayload);
1316
+ await writeArtifact(outputPath, artifactPayload, outputFs);
1313
1317
 
1314
1318
  if (!success) {
1315
1319
  throw new Error(