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
@@ -12,6 +12,7 @@
12
12
  // ast-parser-layer.js so we don't spawn Python for the JS/TS majority.
13
13
 
14
14
  import fsp from "node:fs/promises";
15
+ import { requireOutputFileSystem } from "../config/output-filesystem.js";
15
16
  import path from "node:path";
16
17
  import process from "node:process";
17
18
 
@@ -452,24 +453,27 @@ export async function detectAstDrift({
452
453
  }
453
454
 
454
455
  export async function writeAstSnapshot({
456
+ outputFs,
455
457
  rootPath = process.cwd(),
456
458
  snapshotPath = "",
457
459
  snapshot,
458
460
  } = {}) {
461
+ requireOutputFileSystem(outputFs);
459
462
  if (!snapshot || typeof snapshot !== "object") {
460
463
  throw new Error("snapshot is required.");
461
464
  }
462
465
  const resolvedSnapshotPath = snapshotPath
463
466
  ? path.resolve(rootPath, snapshotPath)
464
467
  : path.join(rootPath, DEFAULT_AST_SNAPSHOT_RELATIVE_PATH);
465
- await fsp.mkdir(path.dirname(resolvedSnapshotPath), { recursive: true });
468
+ await outputFs.assertFilePath(resolvedSnapshotPath);
469
+ await outputFs.mkdir(path.dirname(resolvedSnapshotPath), { recursive: true });
466
470
  const tmpPath = `${resolvedSnapshotPath}.${process.pid}.${Date.now()}.tmp`;
467
- await fsp.writeFile(
471
+ await outputFs.writeFile(
468
472
  tmpPath,
469
473
  `${JSON.stringify(snapshot, null, 2)}\n`,
470
474
  "utf-8"
471
475
  );
472
- await fsp.rename(tmpPath, resolvedSnapshotPath);
476
+ await outputFs.rename(tmpPath, resolvedSnapshotPath);
473
477
  return resolvedSnapshotPath;
474
478
  }
475
479
 
@@ -1,6 +1,7 @@
1
1
  import fsp from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
+ import { bindOutputFileSystem, requireOutputFileSystem } from "../config/output-filesystem.js";
4
5
  import { resolveAssignmentLedgerStorage } from "./assignment-ledger.js";
5
6
  import { resolveErrorDaemonStorage } from "./error-worker.js";
6
7
 
@@ -179,14 +180,16 @@ async function loadJsonFile(filePath, defaultFactory) {
179
180
  }
180
181
  }
181
182
 
182
- async function writeJsonFile(filePath, payload = {}) {
183
- await fsp.mkdir(path.dirname(filePath), { recursive: true });
184
- await fsp.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
183
+ async function writeJsonFile(filePath, payload = {}, outputFs) {
184
+ requireOutputFileSystem(outputFs);
185
+ await outputFs.mkdir(path.dirname(filePath), { recursive: true });
186
+ await outputFs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
185
187
  }
186
188
 
187
- async function appendEvent(filePath, payload = {}) {
188
- await fsp.mkdir(path.dirname(filePath), { recursive: true });
189
- await fsp.appendFile(filePath, `${JSON.stringify(payload)}\n`, "utf-8");
189
+ async function appendEvent(filePath, payload = {}, outputFs) {
190
+ requireOutputFileSystem(outputFs);
191
+ await outputFs.mkdir(path.dirname(filePath), { recursive: true });
192
+ await outputFs.appendFile(filePath, `${JSON.stringify(payload)}\n`, "utf-8");
190
193
  }
191
194
 
192
195
  async function loadQueue(queuePath, nowIso = new Date().toISOString()) {
@@ -202,12 +205,13 @@ async function loadQueue(queuePath, nowIso = new Date().toISOString()) {
202
205
  };
203
206
  }
204
207
 
205
- async function writeQueue(queuePath, queue = {}, nowIso = new Date().toISOString()) {
208
+ async function writeQueue(queuePath, queue = {}, nowIso = new Date().toISOString(), outputFs) {
209
+ requireOutputFileSystem(outputFs);
206
210
  await writeJsonFile(queuePath, {
207
211
  schemaVersion: QUEUE_SCHEMA_VERSION,
208
212
  generatedAt: normalizeIsoTimestamp(nowIso, nowIso),
209
213
  items: Array.isArray(queue.items) ? queue.items : [],
210
- });
214
+ }, outputFs);
211
215
  }
212
216
 
213
217
  async function loadAssignmentLedger(ledgerPath, nowIso = new Date().toISOString()) {
@@ -218,12 +222,13 @@ async function loadAssignmentLedger(ledgerPath, nowIso = new Date().toISOString(
218
222
  }));
219
223
  }
220
224
 
221
- async function writeAssignmentLedger(ledgerPath, ledger = {}, nowIso = new Date().toISOString()) {
225
+ async function writeAssignmentLedger(ledgerPath, ledger = {}, nowIso = new Date().toISOString(), outputFs) {
226
+ requireOutputFileSystem(outputFs);
222
227
  await writeJsonFile(ledgerPath, {
223
228
  ...ledger,
224
229
  generatedAt: normalizeIsoTimestamp(nowIso, nowIso),
225
230
  assignments: Array.isArray(ledger.assignments) ? ledger.assignments : [],
226
- });
231
+ }, outputFs);
227
232
  }
228
233
 
229
234
  export function evaluateDaemonBudget({
@@ -371,12 +376,12 @@ export async function resolveBudgetGovernorStorage({
371
376
  env,
372
377
  homeDir,
373
378
  });
374
- return {
379
+ return bindOutputFileSystem({
375
380
  ...daemonStorage,
376
381
  budgetStatePath: path.join(daemonStorage.baseDir, "budget-state.json"),
377
382
  budgetEventsPath: path.join(daemonStorage.baseDir, "budget-events.ndjson"),
378
383
  budgetRunsDir: path.join(daemonStorage.baseDir, "budget-runs"),
379
- };
384
+ }, daemonStorage.outputFs);
380
385
  }
381
386
 
382
387
  async function loadBudgetState(filePath, nowIso = new Date().toISOString()) {
@@ -392,7 +397,8 @@ async function loadBudgetState(filePath, nowIso = new Date().toISOString()) {
392
397
  };
393
398
  }
394
399
 
395
- async function writeBudgetState(filePath, state = {}, nowIso = new Date().toISOString()) {
400
+ async function writeBudgetState(filePath, state = {}, nowIso = new Date().toISOString(), outputFs) {
401
+ requireOutputFileSystem(outputFs);
396
402
  const normalized = {
397
403
  schemaVersion: BUDGET_STATE_SCHEMA_VERSION,
398
404
  generatedAt: normalizeIsoTimestamp(nowIso, nowIso),
@@ -402,7 +408,7 @@ async function writeBudgetState(filePath, state = {}, nowIso = new Date().toISOS
402
408
  .filter((record) => record.workItemId)
403
409
  : [],
404
410
  };
405
- await writeJsonFile(filePath, normalized);
411
+ await writeJsonFile(filePath, normalized, outputFs);
406
412
  return normalized;
407
413
  }
408
414
 
@@ -529,9 +535,9 @@ export async function applyDaemonBudgetCheck({
529
535
  }
530
536
 
531
537
  const [savedState] = await Promise.all([
532
- writeBudgetState(storage.budgetStatePath, budgetState, normalizedNow),
533
- writeQueue(storage.queuePath, queue, normalizedNow),
534
- writeAssignmentLedger(assignmentStorage.ledgerPath, assignmentLedger, normalizedNow),
538
+ writeBudgetState(storage.budgetStatePath, budgetState, normalizedNow, storage.outputFs),
539
+ writeQueue(storage.queuePath, queue, normalizedNow, storage.outputFs),
540
+ writeAssignmentLedger(assignmentStorage.ledgerPath, assignmentLedger, normalizedNow, assignmentStorage.outputFs),
535
541
  ]);
536
542
 
537
543
  await appendEvent(storage.budgetEventsPath, {
@@ -543,9 +549,9 @@ export async function applyDaemonBudgetCheck({
543
549
  warningCodes: evaluation.warnings.map((item) => item.code),
544
550
  stopCodes: evaluation.stopReasons.map((item) => item.code),
545
551
  quarantineUntil: evaluation.quarantineUntil,
546
- });
552
+ }, storage.outputFs);
547
553
 
548
- await fsp.mkdir(storage.budgetRunsDir, { recursive: true });
554
+ await storage.outputFs.mkdir(storage.budgetRunsDir, { recursive: true });
549
555
  const runId = `budget-check-${normalizedNow.replace(/[:.]/g, "-")}-${String(
550
556
  budgetState.records.length
551
557
  ).padStart(4, "0")}`;
@@ -562,7 +568,7 @@ export async function applyDaemonBudgetCheck({
562
568
  usage: evaluation.usage,
563
569
  quarantineStartedAt: evaluation.quarantineStartedAt,
564
570
  quarantineUntil: evaluation.quarantineUntil,
565
- });
571
+ }, storage.outputFs);
566
572
 
567
573
  return {
568
574
  ...storage,
@@ -2,7 +2,8 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import fsp from "node:fs/promises";
3
3
  import path from "node:path";
4
4
 
5
- import { resolveOutputRoot } from "../config/service.js";
5
+ import { bindOutputFileSystem, requireOutputFileSystem } from "../config/output-filesystem.js";
6
+ import { resolveOutputContext } from "../config/service.js";
6
7
  import { createAgentEvent } from "../events/schema.js";
7
8
  import { appendToStream } from "../session/stream.js";
8
9
 
@@ -290,7 +291,8 @@ async function loadQueueFile(queuePath, nowIso = new Date().toISOString()) {
290
291
  }
291
292
  }
292
293
 
293
- async function writeQueueFile(queuePath, queue = {}, nowIso = new Date().toISOString()) {
294
+ async function writeQueueFile(queuePath, queue = {}, nowIso = new Date().toISOString(), outputFs) {
295
+ requireOutputFileSystem(outputFs);
294
296
  const normalized = {
295
297
  schemaVersion: QUEUE_SCHEMA_VERSION,
296
298
  generatedAt: normalizeIsoTimestamp(nowIso, nowIso),
@@ -300,8 +302,8 @@ async function writeQueueFile(queuePath, queue = {}, nowIso = new Date().toISOSt
300
302
  .filter((item) => normalizeString(item.fingerprint))
301
303
  : [],
302
304
  };
303
- await fsp.mkdir(path.dirname(queuePath), { recursive: true });
304
- await fsp.writeFile(queuePath, `${JSON.stringify(normalized, null, 2)}\n`, "utf-8");
305
+ await outputFs.mkdir(path.dirname(queuePath), { recursive: true });
306
+ await outputFs.writeFile(queuePath, `${JSON.stringify(normalized, null, 2)}\n`, "utf-8");
305
307
  return normalized;
306
308
  }
307
309
 
@@ -327,7 +329,8 @@ async function loadWorkerStateFile(statePath, nowIso = new Date().toISOString())
327
329
  }
328
330
  }
329
331
 
330
- async function writeWorkerStateFile(statePath, state = {}, nowIso = new Date().toISOString()) {
332
+ async function writeWorkerStateFile(statePath, state = {}, nowIso = new Date().toISOString(), outputFs) {
333
+ requireOutputFileSystem(outputFs);
331
334
  const normalized = {
332
335
  schemaVersion: STATE_SCHEMA_VERSION,
333
336
  updatedAt: normalizeIsoTimestamp(nowIso, nowIso),
@@ -338,8 +341,8 @@ async function writeWorkerStateFile(statePath, state = {}, nowIso = new Date().t
338
341
  totalQueuedItems: normalizeNonNegativeInteger(state.totalQueuedItems, 0),
339
342
  totalDedupeHits: normalizeNonNegativeInteger(state.totalDedupeHits, 0),
340
343
  };
341
- await fsp.mkdir(path.dirname(statePath), { recursive: true });
342
- await fsp.writeFile(statePath, `${JSON.stringify(normalized, null, 2)}\n`, "utf-8");
344
+ await outputFs.mkdir(path.dirname(statePath), { recursive: true });
345
+ await outputFs.writeFile(statePath, `${JSON.stringify(normalized, null, 2)}\n`, "utf-8");
343
346
  return normalized;
344
347
  }
345
348
 
@@ -427,15 +430,16 @@ export async function resolveErrorDaemonStorage({
427
430
  env,
428
431
  homeDir,
429
432
  } = {}) {
430
- const outputRoot = await resolveOutputRoot({
433
+ const { outputRoot, outputFs } = await resolveOutputContext({
431
434
  cwd: path.resolve(String(targetPath || ".")),
432
435
  outputDirOverride: outputDir,
433
436
  env,
434
437
  homeDir,
435
438
  });
439
+ requireOutputFileSystem(outputFs);
436
440
  const observabilityRoot = path.join(outputRoot, "observability");
437
441
  const baseDir = path.join(observabilityRoot, "error-daemon");
438
- return {
442
+ return bindOutputFileSystem({
439
443
  outputRoot,
440
444
  observabilityRoot,
441
445
  baseDir,
@@ -445,7 +449,7 @@ export async function resolveErrorDaemonStorage({
445
449
  intakeDir: path.join(baseDir, "intake"),
446
450
  runsDir: path.join(baseDir, "runs"),
447
451
  sweepsDir: path.join(baseDir, "sweeps"),
448
- };
452
+ }, outputFs);
449
453
  }
450
454
 
451
455
  export async function appendAdminErrorEvent({
@@ -464,15 +468,15 @@ export async function appendAdminErrorEvent({
464
468
  homeDir,
465
469
  });
466
470
 
467
- await fsp.mkdir(storage.baseDir, { recursive: true });
468
- await fsp.appendFile(storage.streamPath, `${JSON.stringify(normalizedEvent)}\n`, "utf-8");
471
+ await storage.outputFs.mkdir(storage.baseDir, { recursive: true });
472
+ await storage.outputFs.appendFile(storage.streamPath, `${JSON.stringify(normalizedEvent)}\n`, "utf-8");
469
473
 
470
- await fsp.mkdir(storage.intakeDir, { recursive: true });
474
+ await storage.outputFs.mkdir(storage.intakeDir, { recursive: true });
471
475
  const intakePath = path.join(
472
476
  storage.intakeDir,
473
477
  `intake-${stableTimestampForFile(new Date(normalizedEvent.recordedAt))}-${normalizedEvent.eventId.slice(0, 8)}.json`
474
478
  );
475
- await fsp.writeFile(
479
+ await storage.outputFs.writeFile(
476
480
  intakePath,
477
481
  `${JSON.stringify(
478
482
  {
@@ -506,9 +510,10 @@ async function writeWorkItemIntakeArtifact(
506
510
  nowIso = new Date().toISOString(),
507
511
  } = {}
508
512
  ) {
513
+ requireOutputFileSystem(storage.outputFs);
509
514
  const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
510
515
  const intakePath = resolveWorkItemIntakePath(storage, queueItem, normalizedNow);
511
- await fsp.mkdir(path.dirname(intakePath), { recursive: true });
516
+ await storage.outputFs.mkdir(path.dirname(intakePath), { recursive: true });
512
517
  const normalizedRequestIds = normalizeRequestIds(queueItem.requestIds || []);
513
518
  const payload = {
514
519
  schemaVersion: "1.0.0",
@@ -537,7 +542,7 @@ async function writeWorkItemIntakeArtifact(
537
542
  status: queueItem.status,
538
543
  message: queueItem.message,
539
544
  };
540
- await fsp.writeFile(intakePath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
545
+ await storage.outputFs.writeFile(intakePath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
541
546
  return {
542
547
  intakePath,
543
548
  payload,
@@ -696,7 +701,7 @@ export async function runErrorDaemonWorker({
696
701
  }
697
702
 
698
703
  queue.generatedAt = normalizedNow;
699
- const savedQueue = await writeQueueFile(storage.queuePath, queue, normalizedNow);
704
+ const savedQueue = await writeQueueFile(storage.queuePath, queue, normalizedNow, storage.outputFs);
700
705
 
701
706
  const savedState = await writeWorkerStateFile(
702
707
  storage.statePath,
@@ -710,7 +715,8 @@ export async function runErrorDaemonWorker({
710
715
  totalQueuedItems: savedQueue.items.length,
711
716
  totalDedupeHits: state.totalDedupeHits + dedupedCount,
712
717
  },
713
- normalizedNow
718
+ normalizedNow,
719
+ storage.outputFs
714
720
  );
715
721
 
716
722
  const intakeArtifacts = [];
@@ -759,10 +765,10 @@ export async function runErrorDaemonWorker({
759
765
  }
760
766
  }
761
767
 
762
- await fsp.mkdir(storage.runsDir, { recursive: true });
768
+ await storage.outputFs.mkdir(storage.runsDir, { recursive: true });
763
769
  const runId = `error-daemon-run-${stableTimestampForFile(new Date(normalizedNow))}-${randomUUID().slice(0, 8)}`;
764
770
  const runPath = path.join(storage.runsDir, `${runId}.json`);
765
- await fsp.writeFile(
771
+ await storage.outputFs.writeFile(
766
772
  runPath,
767
773
  `${JSON.stringify(
768
774
  {
@@ -893,9 +899,9 @@ export async function scheduledErrorSweep({
893
899
  })),
894
900
  };
895
901
 
896
- await fsp.mkdir(storage.sweepsDir, { recursive: true });
902
+ await storage.outputFs.mkdir(storage.sweepsDir, { recursive: true });
897
903
  const sweepPath = path.join(storage.sweepsDir, `${runId}.json`);
898
- await fsp.writeFile(sweepPath, `${JSON.stringify(digest, null, 2)}\n`, "utf-8");
904
+ await storage.outputFs.writeFile(sweepPath, `${JSON.stringify(digest, null, 2)}\n`, "utf-8");
899
905
 
900
906
  let sweepEvent = null;
901
907
  if (normalizedSessionId) {
@@ -1,6 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import path from "node:path";
3
- import fsp from "node:fs/promises";
3
+ import { requireOutputFileSystem } from "../config/output-filesystem.js";
4
4
  import { startJiraLifecycle, commentJiraIssue, transitionJiraIssue } from "./jira-lifecycle.js";
5
5
  import { claimAssignment, heartbeatAssignment, releaseAssignment } from "./assignment-ledger.js";
6
6
  import { createAgentEvent } from "../events/schema.js";
@@ -23,7 +23,8 @@ const HEARTBEAT_INTERVAL_MS = 300000;
23
23
  const OMAR_POLL_INTERVAL_MS = 15000;
24
24
  const OMAR_POLL_MAX_ATTEMPTS = 40; // 10 minutes max wait
25
25
 
26
- export async function runFixCycle({ workItemId, workItem, rootPath, scopeMap, findings, onEvent, agentIdentity }) {
26
+ export async function runFixCycle({ workItemId, workItem, rootPath, scopeMap, findings, onEvent, agentIdentity, outputFs }) {
27
+ requireOutputFileSystem(outputFs);
27
28
  const agentDef = agentIdentity || { id: "unknown", persona: "Unknown Agent", color: "white", avatar: "", signature: "" };
28
29
  const emit = (ev, pl) => {
29
30
  if (onEvent) onEvent(createAgentEvent({
@@ -40,7 +41,7 @@ export async function runFixCycle({ workItemId, workItem, rootPath, scopeMap, fi
40
41
  };
41
42
 
42
43
  const artDir = path.join(rootPath, ".sentinelayer", "observability", "fixes", workItemId);
43
- await fsp.mkdir(artDir, { recursive: true });
44
+ await outputFs.mkdir(artDir, { recursive: true });
44
45
 
45
46
  let jiraKey = null;
46
47
  let prNumber = null;
@@ -202,7 +203,7 @@ export async function runFixCycle({ workItemId, workItem, rootPath, scopeMap, fi
202
203
  findingsAddressed: findings?.length || 0,
203
204
  signature: agentDef.signature,
204
205
  };
205
- await fsp.writeFile(
206
+ await outputFs.writeFile(
206
207
  path.join(artDir, "fix-result.json"),
207
208
  JSON.stringify(result, null, 2),
208
209
  );
@@ -1,6 +1,7 @@
1
1
  import fsp from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
+ import { bindOutputFileSystem, requireOutputFileSystem } from "../config/output-filesystem.js";
4
5
  import { parseAstModuleSpecifiers } from "./ast-parser-layer.js";
5
6
  import { buildCallgraphOverlay } from "./callgraph-overlay.js";
6
7
  import { collectCodebaseIngest } from "../ingest/engine.js";
@@ -110,14 +111,16 @@ async function readJsonFile(filePath, defaultFactory) {
110
111
  }
111
112
  }
112
113
 
113
- async function writeJsonFile(filePath, payload = {}) {
114
- await fsp.mkdir(path.dirname(filePath), { recursive: true });
115
- await fsp.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
114
+ async function writeJsonFile(filePath, payload = {}, outputFs) {
115
+ requireOutputFileSystem(outputFs);
116
+ await outputFs.mkdir(path.dirname(filePath), { recursive: true });
117
+ await outputFs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
116
118
  }
117
119
 
118
- async function appendJsonLine(filePath, payload = {}) {
119
- await fsp.mkdir(path.dirname(filePath), { recursive: true });
120
- await fsp.appendFile(filePath, `${JSON.stringify(payload)}\n`, "utf-8");
120
+ async function appendJsonLine(filePath, payload = {}, outputFs) {
121
+ requireOutputFileSystem(outputFs);
122
+ await outputFs.mkdir(path.dirname(filePath), { recursive: true });
123
+ await outputFs.appendFile(filePath, `${JSON.stringify(payload)}\n`, "utf-8");
121
124
  }
122
125
 
123
126
  function tokenizeWorkItem(queueItem = {}) {
@@ -364,7 +367,7 @@ export async function resolveHybridMappingStorage({
364
367
  homeDir,
365
368
  });
366
369
  const mappingDir = path.join(daemonStorage.baseDir, "mapping");
367
- return {
370
+ return bindOutputFileSystem({
368
371
  ...daemonStorage,
369
372
  mappingDir,
370
373
  mapIndexPath: path.join(mappingDir, "hybrid-map-index.json"),
@@ -373,7 +376,7 @@ export async function resolveHybridMappingStorage({
373
376
  handoffIndexPath: path.join(mappingDir, "hybrid-handoff-index.json"),
374
377
  handoffEventsPath: path.join(mappingDir, "hybrid-handoff-events.ndjson"),
375
378
  handoffRunsDir: path.join(mappingDir, "handoffs"),
376
- };
379
+ }, daemonStorage.outputFs);
377
380
  }
378
381
 
379
382
  export async function buildHybridScopeMap({
@@ -577,8 +580,8 @@ export async function buildHybridScopeMap({
577
580
  },
578
581
  };
579
582
 
580
- await fsp.mkdir(storage.mapRunsDir, { recursive: true });
581
- await writeJsonFile(runPath, runPayload);
583
+ await storage.outputFs.mkdir(storage.mapRunsDir, { recursive: true });
584
+ await writeJsonFile(runPath, runPayload, storage.outputFs);
582
585
 
583
586
  const rawIndex = await readJsonFile(storage.mapIndexPath, () => createInitialMapIndex(normalizedNow));
584
587
  const index = normalizeMapIndex(rawIndex, normalizedNow);
@@ -600,7 +603,7 @@ export async function buildHybridScopeMap({
600
603
  ...index.maps.filter((entry) => entry.runId !== runId),
601
604
  ].slice(0, 2000);
602
605
  await Promise.all([
603
- writeJsonFile(storage.mapIndexPath, index),
606
+ writeJsonFile(storage.mapIndexPath, index, storage.outputFs),
604
607
  appendJsonLine(storage.mapEventsPath, {
605
608
  timestamp: normalizedNow,
606
609
  eventType: "hybrid_scope_map",
@@ -618,7 +621,7 @@ export async function buildHybridScopeMap({
618
621
  callGraphParsedFileCount: callGraphOverlay.summary.parsedFileCount,
619
622
  callGraphFallbackParsedFileCount: callGraphOverlay.summary.fallbackParsedFileCount,
620
623
  callGraphParseErrorCount: callGraphOverlay.summary.parseErrorCount,
621
- }),
624
+ }, storage.outputFs),
622
625
  ]);
623
626
 
624
627
  return {
@@ -791,8 +794,8 @@ export async function buildHybridHandoffPackage({
791
794
  },
792
795
  files: handoffFiles,
793
796
  };
794
- await fsp.mkdir(storage.handoffRunsDir, { recursive: true });
795
- await writeJsonFile(handoffPath, payload);
797
+ await storage.outputFs.mkdir(storage.handoffRunsDir, { recursive: true });
798
+ await writeJsonFile(handoffPath, payload, storage.outputFs);
796
799
 
797
800
  const rawIndex = await readJsonFile(storage.handoffIndexPath, () => createInitialHandoffIndex(normalizedNow));
798
801
  const index = normalizeHandoffIndex(rawIndex, normalizedNow);
@@ -812,7 +815,7 @@ export async function buildHybridHandoffPackage({
812
815
  ].slice(0, 2000);
813
816
 
814
817
  await Promise.all([
815
- writeJsonFile(storage.handoffIndexPath, index),
818
+ writeJsonFile(storage.handoffIndexPath, index, storage.outputFs),
816
819
  appendJsonLine(storage.handoffEventsPath, {
817
820
  timestamp: normalizedNow,
818
821
  eventType: "hybrid_handoff_package",
@@ -822,7 +825,7 @@ export async function buildHybridHandoffPackage({
822
825
  assignee: normalizedAssignee,
823
826
  scopedFileCount: handoffFiles.length,
824
827
  estimatedInputTokens: budgets.estimatedInputTokens,
825
- }),
828
+ }, storage.outputFs),
826
829
  ]);
827
830
 
828
831
  return {
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { resolveOutputContext } from "../config/service.js";
3
4
  import { collectCodebaseIngest, generateCodebaseIngest } from "../ingest/engine.js";
4
5
  import { createAgentEvent } from "../events/schema.js";
5
6
  import {
@@ -74,6 +75,7 @@ export function detectIngestDrift(targetPath) {
74
75
  * @returns {Promise<{ refreshed: boolean, reason: string, durationMs: number }>}
75
76
  */
76
77
  export async function refreshIngestIfNeeded(targetPath, options = {}) {
78
+ const { outputFs } = await resolveOutputContext({ cwd: path.resolve(targetPath), outputDirOverride: ".sentinelayer" });
77
79
  const maxDurationMs = options.maxDurationMs || DEFAULT_MAX_DURATION_MS;
78
80
  const driftMode = options.driftMode === "ast" ? "ast" : "file-count";
79
81
  let astOutcome = null;
@@ -137,6 +139,7 @@ export async function refreshIngestIfNeeded(targetPath, options = {}) {
137
139
  let ingest;
138
140
  try {
139
141
  ingest = await generateCodebaseIngest({
142
+ outputFs,
140
143
  rootPath: targetPath,
141
144
  outputDir: path.join(targetPath, ".sentinelayer"),
142
145
  refresh: true,
@@ -152,6 +155,7 @@ export async function refreshIngestIfNeeded(targetPath, options = {}) {
152
155
  if (driftMode === "ast" && astOutcome?.currentSnapshot) {
153
156
  try {
154
157
  await writeAstSnapshot({
158
+ outputFs,
155
159
  rootPath: targetPath,
156
160
  snapshot: astOutcome.currentSnapshot,
157
161
  });
@@ -1,6 +1,7 @@
1
1
  import fsp from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
+ import { bindOutputFileSystem, requireOutputFileSystem } from "../config/output-filesystem.js";
4
5
  import { createAgentEvent } from "../events/schema.js";
5
6
  import { appendToStream } from "../session/stream.js";
6
7
  import { resolveAssignmentLedgerStorage } from "./assignment-ledger.js";
@@ -165,14 +166,16 @@ async function loadJsonFile(filePath, defaultFactory) {
165
166
  }
166
167
  }
167
168
 
168
- async function writeJsonFile(filePath, payload = {}) {
169
- await fsp.mkdir(path.dirname(filePath), { recursive: true });
170
- await fsp.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
169
+ async function writeJsonFile(filePath, payload = {}, outputFs) {
170
+ requireOutputFileSystem(outputFs);
171
+ await outputFs.mkdir(path.dirname(filePath), { recursive: true });
172
+ await outputFs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
171
173
  }
172
174
 
173
- async function appendEvent(filePath, payload = {}) {
174
- await fsp.mkdir(path.dirname(filePath), { recursive: true });
175
- await fsp.appendFile(filePath, `${JSON.stringify(payload)}\n`, "utf-8");
175
+ async function appendEvent(filePath, payload = {}, outputFs) {
176
+ requireOutputFileSystem(outputFs);
177
+ await outputFs.mkdir(path.dirname(filePath), { recursive: true });
178
+ await outputFs.appendFile(filePath, `${JSON.stringify(payload)}\n`, "utf-8");
176
179
  }
177
180
 
178
181
  async function loadQueue(queuePath, nowIso = new Date().toISOString()) {
@@ -200,7 +203,8 @@ async function loadLifecycle(lifecyclePath, nowIso = new Date().toISOString()) {
200
203
  };
201
204
  }
202
205
 
203
- async function writeLifecycle(lifecyclePath, lifecycle = {}, nowIso = new Date().toISOString()) {
206
+ async function writeLifecycle(lifecyclePath, lifecycle = {}, nowIso = new Date().toISOString(), outputFs) {
207
+ requireOutputFileSystem(outputFs);
204
208
  const normalized = {
205
209
  schemaVersion: LIFECYCLE_SCHEMA_VERSION,
206
210
  generatedAt: normalizeIsoTimestamp(nowIso, nowIso),
@@ -211,7 +215,7 @@ async function writeLifecycle(lifecyclePath, lifecycle = {}, nowIso = new Date()
211
215
  .filter((issue) => issue.workItemId && issue.issueKey)
212
216
  : [],
213
217
  };
214
- await writeJsonFile(lifecyclePath, normalized);
218
+ await writeJsonFile(lifecyclePath, normalized, outputFs);
215
219
  return normalized;
216
220
  }
217
221
 
@@ -284,7 +288,7 @@ async function syncIssueKeyToAssignment({
284
288
  ...rawLedger,
285
289
  generatedAt: nowIso,
286
290
  assignments,
287
- });
291
+ }, assignmentStorage.outputFs);
288
292
  }
289
293
 
290
294
  export async function resolveJiraLifecycleStorage({
@@ -299,11 +303,11 @@ export async function resolveJiraLifecycleStorage({
299
303
  env,
300
304
  homeDir,
301
305
  });
302
- return {
306
+ return bindOutputFileSystem({
303
307
  ...daemonStorage,
304
308
  lifecyclePath: path.join(daemonStorage.baseDir, "jira-lifecycle.json"),
305
309
  eventsPath: path.join(daemonStorage.baseDir, "jira-events.ndjson"),
306
- };
310
+ }, daemonStorage.outputFs);
307
311
  }
308
312
 
309
313
  export async function emitJiraLifecycleEvent(
@@ -453,7 +457,7 @@ export async function openJiraIssue({
453
457
  normalizedNow
454
458
  );
455
459
  lifecycle.issues.push(issue);
456
- const savedLifecycle = await writeLifecycle(storage.lifecyclePath, lifecycle, normalizedNow);
460
+ const savedLifecycle = await writeLifecycle(storage.lifecyclePath, lifecycle, normalizedNow, storage.outputFs);
457
461
  await appendEvent(storage.eventsPath, {
458
462
  timestamp: normalizedNow,
459
463
  eventType: "create_issue",
@@ -461,7 +465,7 @@ export async function openJiraIssue({
461
465
  workItemId: normalizedWorkItemId,
462
466
  issueKey: normalizedIssueKey,
463
467
  status: "OPEN",
464
- });
468
+ }, storage.outputFs);
465
469
  await syncIssueKeyToAssignment({
466
470
  targetPath,
467
471
  outputDir,
@@ -536,7 +540,7 @@ export async function commentJiraIssue({
536
540
  existing.latestCommentAt = normalizedNow;
537
541
  existing.updatedAt = normalizedNow;
538
542
  lifecycle.issues[issueIndex] = normalizeIssue(existing, normalizedNow);
539
- const savedLifecycle = await writeLifecycle(storage.lifecyclePath, lifecycle, normalizedNow);
543
+ const savedLifecycle = await writeLifecycle(storage.lifecyclePath, lifecycle, normalizedNow, storage.outputFs);
540
544
  await appendEvent(storage.eventsPath, {
541
545
  timestamp: normalizedNow,
542
546
  eventType: "comment",
@@ -545,7 +549,7 @@ export async function commentJiraIssue({
545
549
  issueKey: existing.issueKey,
546
550
  type: comment.type,
547
551
  message: comment.message,
548
- });
552
+ }, storage.outputFs);
549
553
  if (normalizedSessionId) {
550
554
  await emitJiraLifecycleEvent(normalizedSessionId, {
551
555
  phase: comment.type === "plan" ? "plan_comment" : "checkpoint",
@@ -613,7 +617,7 @@ export async function transitionJiraIssue({
613
617
  existing.transitions = [...existing.transitions, transition];
614
618
  existing.updatedAt = normalizedNow;
615
619
  lifecycle.issues[issueIndex] = normalizeIssue(existing, normalizedNow);
616
- const savedLifecycle = await writeLifecycle(storage.lifecyclePath, lifecycle, normalizedNow);
620
+ const savedLifecycle = await writeLifecycle(storage.lifecyclePath, lifecycle, normalizedNow, storage.outputFs);
617
621
  await appendEvent(storage.eventsPath, {
618
622
  timestamp: normalizedNow,
619
623
  eventType: "transition",
@@ -623,7 +627,7 @@ export async function transitionJiraIssue({
623
627
  from: transition.from,
624
628
  to: transition.to,
625
629
  reason: transition.reason,
626
- });
630
+ }, storage.outputFs);
627
631
  if (normalizedSessionId) {
628
632
  await emitJiraLifecycleEvent(normalizedSessionId, {
629
633
  phase: resolveTransitionPhase(nextStatus),