@sentry/warden 0.45.0 → 0.46.1

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 (74) hide show
  1. package/dist/cli/args.d.ts +15 -1
  2. package/dist/cli/args.d.ts.map +1 -1
  3. package/dist/cli/args.js +53 -0
  4. package/dist/cli/args.js.map +1 -1
  5. package/dist/cli/commands/service.d.ts +340 -0
  6. package/dist/cli/commands/service.d.ts.map +1 -0
  7. package/dist/cli/commands/service.js +99 -0
  8. package/dist/cli/commands/service.js.map +1 -0
  9. package/dist/cli/help.d.ts +1 -1
  10. package/dist/cli/help.d.ts.map +1 -1
  11. package/dist/cli/help.js +49 -1
  12. package/dist/cli/help.js.map +1 -1
  13. package/dist/cli/main.d.ts +2 -0
  14. package/dist/cli/main.d.ts.map +1 -1
  15. package/dist/cli/main.js +291 -36
  16. package/dist/cli/main.js.map +1 -1
  17. package/dist/config/loader.d.ts.map +1 -1
  18. package/dist/config/loader.js +16 -0
  19. package/dist/config/loader.js.map +1 -1
  20. package/dist/config/schema.d.ts +23 -0
  21. package/dist/config/schema.d.ts.map +1 -1
  22. package/dist/config/schema.js +41 -0
  23. package/dist/config/schema.js.map +1 -1
  24. package/dist/index.d.ts +5 -3
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +2 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/sdk/analyze.js +1 -1
  29. package/dist/sdk/analyze.js.map +1 -1
  30. package/dist/sdk/local.d.ts +6 -0
  31. package/dist/sdk/local.d.ts.map +1 -1
  32. package/dist/sdk/local.js +78 -2
  33. package/dist/sdk/local.js.map +1 -1
  34. package/dist/sdk/offline.d.ts +13 -0
  35. package/dist/sdk/offline.d.ts.map +1 -0
  36. package/dist/sdk/offline.js +22 -0
  37. package/dist/sdk/offline.js.map +1 -0
  38. package/dist/sdk/prompt.d.ts +1 -1
  39. package/dist/sdk/prompt.d.ts.map +1 -1
  40. package/dist/sdk/prompt.js +3 -1
  41. package/dist/sdk/prompt.js.map +1 -1
  42. package/dist/sdk/runner.d.ts +1 -1
  43. package/dist/sdk/runner.d.ts.map +1 -1
  44. package/dist/sdk/runner.js.map +1 -1
  45. package/dist/sdk/runtimes/pi.d.ts.map +1 -1
  46. package/dist/sdk/runtimes/pi.js +148 -5
  47. package/dist/sdk/runtimes/pi.js.map +1 -1
  48. package/dist/sdk/types.d.ts +2 -0
  49. package/dist/sdk/types.d.ts.map +1 -1
  50. package/dist/service/client.d.ts +12 -0
  51. package/dist/service/client.d.ts.map +1 -0
  52. package/dist/service/client.js +35 -0
  53. package/dist/service/client.js.map +1 -0
  54. package/dist/service/findings.d.ts +337 -0
  55. package/dist/service/findings.d.ts.map +1 -0
  56. package/dist/service/findings.js +194 -0
  57. package/dist/service/findings.js.map +1 -0
  58. package/dist/service/index.d.ts +8 -0
  59. package/dist/service/index.d.ts.map +1 -0
  60. package/dist/service/index.js +6 -0
  61. package/dist/service/index.js.map +1 -0
  62. package/dist/service/memory.d.ts +4 -0
  63. package/dist/service/memory.d.ts.map +1 -0
  64. package/dist/service/memory.js +12 -0
  65. package/dist/service/memory.js.map +1 -0
  66. package/dist/service/options.d.ts +31 -0
  67. package/dist/service/options.d.ts.map +1 -0
  68. package/dist/service/options.js +51 -0
  69. package/dist/service/options.js.map +1 -0
  70. package/dist/service/projection.d.ts +43 -0
  71. package/dist/service/projection.d.ts.map +1 -0
  72. package/dist/service/projection.js +155 -0
  73. package/dist/service/projection.js.map +1 -0
  74. package/package.json +3 -2
package/dist/cli/main.js CHANGED
@@ -1,16 +1,18 @@
1
1
  import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
- import { join, relative, resolve } from 'node:path';
2
+ import { extname, join, relative, resolve } from 'node:path';
3
3
  import { config as dotenvConfig } from 'dotenv';
4
4
  import { Sentry, flushSentry, setRepositoryScope, emitRunMetric, getTraceId, initSentry } from '../sentry.js';
5
5
  import { emptyToUndefined, loadWardenConfigFile, resolveSkillConfigs } from '../config/loader.js';
6
6
  import { verifyAuth } from '../sdk/runner.js';
7
7
  import { findInvalidPiModelSelector as findInvalidPiModelSelectorTarget, invalidPiModelSelectorMessage, } from '../sdk/runtimes/model-selectors.js';
8
+ import { configureWardenOffline, isWardenOffline } from '../sdk/offline.js';
8
9
  import { mapExtractionErrorCode } from '../sdk/errors.js';
9
10
  import { aggregateAuxiliaryUsageAttribution, mergeAuxiliaryUsage } from '../sdk/usage.js';
10
11
  import { resolveSkillAsync, SkillLoaderError } from '../skills/loader.js';
11
12
  import { matchTrigger, filterContextByPaths, shouldFail, countFindingsAtOrAbove } from '../triggers/matcher.js';
12
13
  import { filterFindings } from '../types/index.js';
13
- import { DEFAULT_CONCURRENCY, getAnthropicApiKey } from '../utils/index.js';
14
+ import { DEFAULT_CONCURRENCY, getAnthropicApiKey, getVersion } from '../utils/index.js';
15
+ import { buildServiceRunEnvelope, publishRunFailOpen, recallMemoryFailOpen, renderHistoricalMemory, resolveServiceOptions, } from '../service/index.js';
14
16
  import { isRepoRelativePath, normalizePath, resolveConfigInput } from '../utils/path.js';
15
17
  import { parseCliArgs, showVersion, classifyTargets, expandTargetFileReferences } from './args.js';
16
18
  import { showHelp } from './help.js';
@@ -26,6 +28,7 @@ import { runSetupApp } from './commands/setup-app.js';
26
28
  import { runSync } from './commands/sync.js';
27
29
  import { runRuns } from './commands/runs.js';
28
30
  import { runBuild, runImprove } from './commands/build.js';
31
+ import { runServiceCommand } from './commands/service.js';
29
32
  import { generatedSkillDefinitionRootExists, resolveGeneratedSkillTarget, } from '../skill-builder/definition.js';
30
33
  /**
31
34
  * Global abort controller for graceful shutdown on SIGINT.
@@ -95,6 +98,7 @@ function resolveLocalReviewBase(configDefaultBranch, repoPath) {
95
98
  function emitEmptyRunLog(repoPath, options, error) {
96
99
  const runId = generateRunId();
97
100
  const timestamp = new Date();
101
+ const traceId = getTraceId();
98
102
  let headSha;
99
103
  try {
100
104
  headSha = getHeadSha(repoPath);
@@ -104,7 +108,7 @@ function emitEmptyRunLog(repoPath, options, error) {
104
108
  }
105
109
  const content = renderJsonlString([], 0, {
106
110
  runId,
107
- traceId: getTraceId(),
111
+ traceId,
108
112
  timestamp,
109
113
  headSha,
110
114
  error,
@@ -129,6 +133,7 @@ function emitEmptyRunLog(repoPath, options, error) {
129
133
  if (options.json) {
130
134
  process.stdout.write(content);
131
135
  }
136
+ return { runId, timestamp, traceId, headSha };
132
137
  }
133
138
  function initializeRunLog(args) {
134
139
  const { repoPath, runId, timestamp, traceId, headSha, model, outputPath, reporter, startTime } = args;
@@ -560,9 +565,15 @@ function loadOptionalConfig(options, repoPath) {
560
565
  : repoPath
561
566
  ? resolve(repoPath, 'warden.toml')
562
567
  : null;
563
- return configPath && existsSync(configPath)
568
+ const config = configPath && existsSync(configPath)
564
569
  ? loadWardenConfigFile(configPath)
565
570
  : null;
571
+ applyWardenOfflinePolicy(options, config);
572
+ return config;
573
+ }
574
+ /** Apply durable config and CLI offline intent for Warden network access. */
575
+ function applyWardenOfflinePolicy(options, config) {
576
+ configureWardenOffline(Boolean(options.offline || config?.defaults?.offline));
566
577
  }
567
578
  /**
568
579
  * Find the first Pi runner option using a model ID that is not provider/model.
@@ -578,11 +589,12 @@ function reportInvalidPiModelSelector(reporter, invalid) {
578
589
  reporter.tip('Set a Pi model selector such as anthropic/claude-sonnet-4-6.');
579
590
  }
580
591
  function emitInvalidPiModelSelectorRunLog(repoPath, options, invalid) {
581
- emitEmptyRunLog(repoPath, options, {
592
+ const error = {
582
593
  code: 'invalid_model_selector',
583
594
  message: invalidPiModelSelectorMessage(invalid),
584
595
  timestamp: new Date().toISOString(),
585
- });
596
+ };
597
+ return { run: emitEmptyRunLog(repoPath, options, error), error };
586
598
  }
587
599
  function verifyClaudeAuthForRun(args) {
588
600
  const { apiKey, pathToClaudeCodeExecutable, reporter, repoPath, options } = args;
@@ -591,17 +603,17 @@ function verifyClaudeAuthForRun(args) {
591
603
  }
592
604
  try {
593
605
  verifyAuth({ apiKey, pathToClaudeCodeExecutable });
594
- return true;
606
+ return undefined;
595
607
  }
596
608
  catch (error) {
597
609
  const message = error.message;
598
610
  reporter.error(message);
599
- emitEmptyRunLog(repoPath, options, {
611
+ const skillError = {
600
612
  code: 'auth_failed',
601
613
  message,
602
614
  timestamp: new Date().toISOString(),
603
- });
604
- return false;
615
+ };
616
+ return { run: emitEmptyRunLog(repoPath, options, skillError), error: skillError };
605
617
  }
606
618
  }
607
619
  function renderSkillRunHeader(args) {
@@ -621,7 +633,7 @@ async function createDirectSkillTask(args) {
621
633
  try {
622
634
  skill = await resolveSkillAsync(spec.skill, repoPath, {
623
635
  remote: spec.remote,
624
- offline: options.offline,
636
+ offline: isWardenOffline(),
625
637
  });
626
638
  }
627
639
  catch (error) {
@@ -794,6 +806,146 @@ async function outputResultsAndHandleFixes(processed, options, reporter, runLog,
794
806
  }
795
807
  return 0;
796
808
  }
809
+ function resolveCliService(options, config, reporter) {
810
+ return resolveServiceOptions({
811
+ explicit: {
812
+ url: options.serviceUrl,
813
+ data: options.serviceData,
814
+ memory: options.serviceMemory,
815
+ timeoutMs: options.serviceTimeoutMs,
816
+ disabled: options.noService,
817
+ },
818
+ config: config?.service,
819
+ onWarning: (message) => reporter.warning(message),
820
+ });
821
+ }
822
+ /** Convert the finalized CLI exit status into its service run outcome. */
823
+ export function cliRunOutcome(exitCode) {
824
+ if (exitCode === 130)
825
+ return 'cancelled';
826
+ if (exitCode === 0)
827
+ return 'success';
828
+ return 'failure';
829
+ }
830
+ async function publishCliRun(args) {
831
+ if (!args.service)
832
+ return;
833
+ const service = args.service;
834
+ const repository = args.context.repository;
835
+ await publishRunFailOpen(service, {
836
+ clientRunId: args.runLog.baseRun.runId,
837
+ build: () => buildServiceRunEnvelope({
838
+ service,
839
+ clientRunId: args.runLog.baseRun.runId,
840
+ source: 'cli',
841
+ wardenVersion: getVersion(),
842
+ startedAt: new Date(args.runLog.baseRun.timestamp),
843
+ completedAt: new Date(new Date(args.runLog.baseRun.timestamp).getTime() + args.totalDuration),
844
+ outcome: cliRunOutcome(args.exitCode),
845
+ repository: {
846
+ provider: 'local',
847
+ owner: repository.owner,
848
+ name: repository.name,
849
+ fullName: repository.fullName,
850
+ },
851
+ reports: args.processed.reports.map((report, index) => ({
852
+ executionId: `${index + 1}:${report.skill}`,
853
+ report,
854
+ })),
855
+ ...(args.recalledMemories?.length ? { recalledMemories: [...args.recalledMemories] } : {}),
856
+ ...(args.memoryRecallId ? { memoryRecallId: args.memoryRecallId } : {}),
857
+ ...(args.runLog.baseRun.traceId ? { traceId: args.runLog.baseRun.traceId } : {}),
858
+ ...(args.runLog.baseRun.headSha ? { headSha: args.runLog.baseRun.headSha } : {}),
859
+ event: args.context.eventType,
860
+ }),
861
+ }, (message) => args.reporter.warning(message));
862
+ }
863
+ async function publishCliEarlyFailure(args) {
864
+ if (!args.service)
865
+ return;
866
+ const service = args.service;
867
+ const completedAt = new Date();
868
+ const repository = args.context.repository;
869
+ await publishRunFailOpen(service, {
870
+ clientRunId: args.run.runId,
871
+ build: () => buildServiceRunEnvelope({
872
+ service: { ...service, data: 'metrics', memory: false },
873
+ clientRunId: args.run.runId,
874
+ source: 'cli',
875
+ wardenVersion: getVersion(),
876
+ startedAt: args.run.timestamp,
877
+ completedAt,
878
+ outcome: 'failure',
879
+ repository: {
880
+ provider: 'local',
881
+ owner: repository.owner,
882
+ name: repository.name,
883
+ fullName: repository.fullName,
884
+ },
885
+ reports: args.skills.map((skill, index) => ({
886
+ executionId: `${index + 1}:${skill.skill}`,
887
+ report: {
888
+ skill: skill.skill,
889
+ summary: 'Skill did not start',
890
+ findings: [],
891
+ ...(skill.model ? { model: skill.model } : {}),
892
+ ...(skill.runtime ? { runtime: skill.runtime } : {}),
893
+ durationMs: Math.max(0, completedAt.getTime() - args.run.timestamp.getTime()),
894
+ error: args.error,
895
+ },
896
+ })),
897
+ ...(args.run.traceId ? { traceId: args.run.traceId } : {}),
898
+ ...(args.run.headSha ? { headSha: args.run.headSha } : {}),
899
+ event: args.context.eventType,
900
+ }),
901
+ }, (message) => args.reporter.warning(message));
902
+ }
903
+ async function publishCliEmptyRun(args) {
904
+ if (!args.service)
905
+ return;
906
+ const service = args.service;
907
+ const repository = args.context.repository;
908
+ await publishRunFailOpen(service, {
909
+ clientRunId: args.run.runId,
910
+ build: () => buildServiceRunEnvelope({
911
+ service,
912
+ clientRunId: args.run.runId,
913
+ source: 'cli',
914
+ wardenVersion: getVersion(),
915
+ startedAt: args.run.timestamp,
916
+ completedAt: new Date(),
917
+ outcome: 'skipped',
918
+ repository: {
919
+ provider: 'local',
920
+ owner: repository.owner,
921
+ name: repository.name,
922
+ fullName: repository.fullName,
923
+ },
924
+ reports: [],
925
+ ...(args.run.traceId ? { traceId: args.run.traceId } : {}),
926
+ ...(args.run.headSha ? { headSha: args.run.headSha } : {}),
927
+ event: args.context.eventType,
928
+ }),
929
+ }, (message) => args.reporter.warning(message));
930
+ }
931
+ async function recallCliMemory(args) {
932
+ if (!args.service?.memory)
933
+ return undefined;
934
+ const paths = args.context.pullRequest?.files.map((file) => file.filename) ?? [];
935
+ return recallMemoryFailOpen(args.service, {
936
+ protocolVersion: 1,
937
+ clientRecallId: generateRunId(),
938
+ repository: {
939
+ provider: 'local',
940
+ owner: args.context.repository.owner,
941
+ name: args.context.repository.name,
942
+ fullName: args.context.repository.fullName,
943
+ },
944
+ skills: [...args.skills],
945
+ languages: [...new Set(paths.map((path) => extname(path).slice(1)).filter(Boolean))],
946
+ paths,
947
+ });
948
+ }
797
949
  /** Run one or more skills against an already constructed review context. */
798
950
  export async function runSkills(context, options, reporter) {
799
951
  const cwd = process.cwd();
@@ -802,6 +954,7 @@ export async function runSkills(context, options, reporter) {
802
954
  const apiKey = getAnthropicApiKey();
803
955
  const repoPath = findRepoPath(cwd);
804
956
  const config = loadOptionalConfig(options, repoPath);
957
+ const service = resolveCliService(options, config, reporter);
805
958
  const defaultModel = resolveCliDefaultModel(config, options.model);
806
959
  const defaultAuxiliaryModel = resolveCliDefaultAuxiliaryModel(config);
807
960
  const defaultSynthesisModel = resolveCliDefaultSynthesisModel(config);
@@ -873,22 +1026,39 @@ export async function runSkills(context, options, reporter) {
873
1026
  emitRunMetric();
874
1027
  // Handle case where no skills to run
875
1028
  if (skillsToRun.length === 0) {
876
- emitEmptyRunLog(repoPath ?? cwd, options);
1029
+ const run = emitEmptyRunLog(repoPath ?? cwd, options);
1030
+ await publishCliEmptyRun({ service, context, run, reporter });
877
1031
  if (!options.json) {
878
1032
  reporter.warning('No triggers matched for the changed files');
879
1033
  reporter.tip('Specify a skill explicitly: warden <target> --skill <name>');
880
1034
  }
881
1035
  return 0;
882
1036
  }
883
- if (needsClaudeAuth(skillsToRun) && !verifyClaudeAuthForRun({
884
- apiKey,
885
- pathToClaudeCodeExecutable,
886
- reporter,
887
- repoPath: repoPath ?? cwd,
888
- options,
889
- })) {
1037
+ const authFailure = needsClaudeAuth(skillsToRun)
1038
+ ? verifyClaudeAuthForRun({
1039
+ apiKey,
1040
+ pathToClaudeCodeExecutable,
1041
+ reporter,
1042
+ repoPath: repoPath ?? cwd,
1043
+ options,
1044
+ })
1045
+ : undefined;
1046
+ if (authFailure) {
1047
+ await publishCliEarlyFailure({
1048
+ service,
1049
+ context,
1050
+ ...authFailure,
1051
+ skills: skillsToRun,
1052
+ reporter,
1053
+ });
890
1054
  return 1;
891
1055
  }
1056
+ const recall = await recallCliMemory({
1057
+ service,
1058
+ context,
1059
+ skills: skillsToRun.map((skill) => skill.skill),
1060
+ });
1061
+ const recalledMemories = recall?.memories ?? [];
892
1062
  // Build skill tasks
893
1063
  // Model precedence: defaults.agent.model > defaults.model > CLI flag > WARDEN_MODEL env var > SDK default
894
1064
  // sdkModel is undefined when no model is explicitly configured (lets SDK use its default).
@@ -915,6 +1085,7 @@ export async function runSkills(context, options, reporter) {
915
1085
  config?.defaults?.auxiliaryMaxRetries,
916
1086
  verifyFindings: config?.defaults?.verification?.enabled !== false,
917
1087
  captureTraces: options.traces,
1088
+ historicalEvidence: renderHistoricalMemory(recalledMemories),
918
1089
  };
919
1090
  const specs = skillsToRun.map(({ skill, remote, filters, ...skillOptions }) => ({
920
1091
  name: skill,
@@ -927,7 +1098,8 @@ export async function runSkills(context, options, reporter) {
927
1098
  const invalidModelSelector = findInvalidPiModelSelector(specs);
928
1099
  if (invalidModelSelector) {
929
1100
  reportInvalidPiModelSelector(reporter, invalidModelSelector);
930
- emitInvalidPiModelSelectorRunLog(repoPath ?? cwd, options, invalidModelSelector);
1101
+ const failure = emitInvalidPiModelSelectorRunLog(repoPath ?? cwd, options, invalidModelSelector);
1102
+ await publishCliEarlyFailure({ service, context, ...failure, skills: skillsToRun, reporter });
931
1103
  return 1;
932
1104
  }
933
1105
  let tasks;
@@ -944,11 +1116,13 @@ export async function runSkills(context, options, reporter) {
944
1116
  catch (error) {
945
1117
  const message = error instanceof Error ? error.message : String(error);
946
1118
  reporter.error(message);
947
- emitEmptyRunLog(repoPath ?? cwd, options, {
1119
+ const skillError = {
948
1120
  code: 'unknown',
949
1121
  message,
950
1122
  timestamp: new Date().toISOString(),
951
- });
1123
+ };
1124
+ const run = emitEmptyRunLog(repoPath ?? cwd, options, skillError);
1125
+ await publishCliEarlyFailure({ service, context, run, error: skillError, skills: skillsToRun, reporter });
952
1126
  return 1;
953
1127
  }
954
1128
  // Open the run's JSONL log before launching skills so `warden runs
@@ -991,7 +1165,19 @@ export async function runSkills(context, options, reporter) {
991
1165
  const totalDuration = Date.now() - startTime;
992
1166
  const effectiveMinConfidence = options.minConfidence ?? config?.defaults?.minConfidence ?? 'medium';
993
1167
  const processed = processTaskResults(results, options.reportOn, effectiveMinConfidence);
994
- return outputResultsAndHandleFixes(processed, options, reporter, runLog, totalDuration, failFastController?.signal.aborted);
1168
+ const exitCode = await outputResultsAndHandleFixes(processed, options, reporter, runLog, totalDuration, failFastController?.signal.aborted);
1169
+ await publishCliRun({
1170
+ service,
1171
+ context,
1172
+ processed,
1173
+ runLog,
1174
+ totalDuration,
1175
+ exitCode,
1176
+ reporter,
1177
+ recalledMemories,
1178
+ memoryRecallId: recall?.clientRecallId,
1179
+ });
1180
+ return exitCode;
995
1181
  }
996
1182
  async function runFileMode(filePatterns, options, reporter) {
997
1183
  const cwd = process.cwd();
@@ -1063,6 +1249,7 @@ async function runGitRefMode(gitRef, options, reporter) {
1063
1249
  // Load config to get defaultBranch if available
1064
1250
  const configPath = resolveConfigPath(options, repoPath);
1065
1251
  const config = existsSync(configPath) ? loadWardenConfigFile(configPath) : null;
1252
+ applyWardenOfflinePolicy(options, config);
1066
1253
  // Build context from local git
1067
1254
  reporter.startContext(`Analyzing changes from ${gitRef}...`);
1068
1255
  const context = buildLocalEventContext({
@@ -1108,6 +1295,8 @@ async function runConfigMode(options, reporter) {
1108
1295
  }
1109
1296
  // Load config
1110
1297
  const config = loadWardenConfigFile(configPath);
1298
+ applyWardenOfflinePolicy(options, config);
1299
+ const service = resolveCliService(options, config, reporter);
1111
1300
  // Build context from local git. By default, mirror PR-style analysis:
1112
1301
  // compare the configured/default branch merge base to HEAD.
1113
1302
  const localReviewBase = resolveLocalReviewBase(config.defaults?.defaultBranch, repoPath);
@@ -1128,7 +1317,8 @@ async function runConfigMode(options, reporter) {
1128
1317
  return 1;
1129
1318
  }
1130
1319
  if (pullRequest.files.length === 0) {
1131
- emitEmptyRunLog(repoPath, options);
1320
+ const run = emitEmptyRunLog(repoPath, options);
1321
+ await publishCliEmptyRun({ service, context, run, reporter });
1132
1322
  if (!options.json) {
1133
1323
  if (options.staged) {
1134
1324
  reporter.renderEmptyState('No staged changes found');
@@ -1161,7 +1351,8 @@ async function runConfigMode(options, reporter) {
1161
1351
  }
1162
1352
  const triggersToRun = [...seen.values()];
1163
1353
  if (triggersToRun.length === 0) {
1164
- emitEmptyRunLog(repoPath, options);
1354
+ const run = emitEmptyRunLog(repoPath, options);
1355
+ await publishCliEmptyRun({ service, context, run, reporter });
1165
1356
  if (!options.json) {
1166
1357
  reporter.blank();
1167
1358
  if (options.skill) {
@@ -1181,15 +1372,35 @@ async function runConfigMode(options, reporter) {
1181
1372
  const triggerRuntimes = triggersToRun.map((trigger) => ({
1182
1373
  runtime: options.runtime ?? trigger.runtime,
1183
1374
  }));
1184
- if (needsClaudeAuth(triggerRuntimes) && !verifyClaudeAuthForRun({
1185
- apiKey,
1186
- pathToClaudeCodeExecutable,
1187
- reporter,
1188
- repoPath,
1189
- options,
1190
- })) {
1375
+ const authFailure = needsClaudeAuth(triggerRuntimes)
1376
+ ? verifyClaudeAuthForRun({
1377
+ apiKey,
1378
+ pathToClaudeCodeExecutable,
1379
+ reporter,
1380
+ repoPath,
1381
+ options,
1382
+ })
1383
+ : undefined;
1384
+ if (authFailure) {
1385
+ await publishCliEarlyFailure({
1386
+ service,
1387
+ context,
1388
+ ...authFailure,
1389
+ skills: triggersToRun.map((trigger) => ({
1390
+ skill: trigger.skill,
1391
+ model: trigger.model,
1392
+ runtime: options.runtime ?? trigger.runtime,
1393
+ })),
1394
+ reporter,
1395
+ });
1191
1396
  return 1;
1192
1397
  }
1398
+ const recall = await recallCliMemory({
1399
+ service,
1400
+ context,
1401
+ skills: triggersToRun.map((trigger) => trigger.skill),
1402
+ });
1403
+ const recalledMemories = recall?.memories ?? [];
1193
1404
  // Build trigger tasks
1194
1405
  const effectiveMinConfidence = options.minConfidence ?? config.defaults?.minConfidence ?? 'medium';
1195
1406
  const specs = triggersToRun.map((trigger) => ({
@@ -1219,12 +1430,24 @@ async function runConfigMode(options, reporter) {
1219
1430
  auxiliaryMaxRetries: trigger.auxiliaryMaxRetries,
1220
1431
  verifyFindings: trigger.verifyFindings,
1221
1432
  captureTraces: options.traces,
1433
+ historicalEvidence: renderHistoricalMemory(recalledMemories),
1222
1434
  },
1223
1435
  }));
1224
1436
  const invalidModelSelector = findInvalidPiModelSelector(specs);
1225
1437
  if (invalidModelSelector) {
1226
1438
  reportInvalidPiModelSelector(reporter, invalidModelSelector);
1227
- emitInvalidPiModelSelectorRunLog(repoPath, options, invalidModelSelector);
1439
+ const failure = emitInvalidPiModelSelectorRunLog(repoPath, options, invalidModelSelector);
1440
+ await publishCliEarlyFailure({
1441
+ service,
1442
+ context,
1443
+ ...failure,
1444
+ skills: triggersToRun.map((trigger) => ({
1445
+ skill: trigger.skill,
1446
+ model: trigger.model,
1447
+ runtime: options.runtime ?? trigger.runtime,
1448
+ })),
1449
+ reporter,
1450
+ });
1228
1451
  return 1;
1229
1452
  }
1230
1453
  let tasks;
@@ -1241,10 +1464,23 @@ async function runConfigMode(options, reporter) {
1241
1464
  catch (error) {
1242
1465
  const message = error instanceof Error ? error.message : String(error);
1243
1466
  reporter.error(message);
1244
- emitEmptyRunLog(repoPath, options, {
1467
+ const skillError = {
1245
1468
  code: 'unknown',
1246
1469
  message,
1247
1470
  timestamp: new Date().toISOString(),
1471
+ };
1472
+ const run = emitEmptyRunLog(repoPath, options, skillError);
1473
+ await publishCliEarlyFailure({
1474
+ service,
1475
+ context,
1476
+ run,
1477
+ error: skillError,
1478
+ skills: triggersToRun.map((trigger) => ({
1479
+ skill: trigger.skill,
1480
+ model: trigger.model,
1481
+ runtime: options.runtime ?? trigger.runtime,
1482
+ })),
1483
+ reporter,
1248
1484
  });
1249
1485
  return 1;
1250
1486
  }
@@ -1291,7 +1527,19 @@ async function runConfigMode(options, reporter) {
1291
1527
  // Process results and output
1292
1528
  const totalDuration = Date.now() - startTime;
1293
1529
  const processed = processTaskResults(results, options.reportOn, effectiveMinConfidence);
1294
- return outputResultsAndHandleFixes(processed, options, reporter, runLog, totalDuration, failFastController?.signal.aborted);
1530
+ const exitCode = await outputResultsAndHandleFixes(processed, options, reporter, runLog, totalDuration, failFastController?.signal.aborted);
1531
+ await publishCliRun({
1532
+ service,
1533
+ context,
1534
+ processed,
1535
+ runLog,
1536
+ totalDuration,
1537
+ exitCode,
1538
+ reporter,
1539
+ recalledMemories,
1540
+ memoryRecallId: recall?.clientRecallId,
1541
+ });
1542
+ return exitCode;
1295
1543
  }
1296
1544
  async function runDirectSkillMode(options, reporter) {
1297
1545
  const cwd = process.cwd();
@@ -1307,6 +1555,7 @@ async function runDirectSkillMode(options, reporter) {
1307
1555
  // Load config to get defaultBranch if available
1308
1556
  const configPath = resolveConfigPath(options, repoPath);
1309
1557
  const config = existsSync(configPath) ? loadWardenConfigFile(configPath) : null;
1558
+ applyWardenOfflinePolicy(options, config);
1310
1559
  // Build context from local git. By default, mirror PR-style analysis:
1311
1560
  // compare the configured/default branch merge base to HEAD.
1312
1561
  const localReviewBase = resolveLocalReviewBase(config?.defaults?.defaultBranch, repoPath);
@@ -1391,7 +1640,7 @@ async function runCommand(options, reporter) {
1391
1640
  }
1392
1641
  /** Parse CLI input, dispatch the selected command, and perform shutdown cleanup. */
1393
1642
  export async function main() {
1394
- const { command, options, helpTarget, setupAppOptions, runsOptions } = parseCliArgs();
1643
+ const { command, options, helpTarget, setupAppOptions, runsOptions, serviceOptions } = parseCliArgs();
1395
1644
  if (command === 'help') {
1396
1645
  showHelp(helpTarget);
1397
1646
  process.exit(0);
@@ -1455,6 +1704,12 @@ export async function main() {
1455
1704
  process.exit(1);
1456
1705
  }
1457
1706
  return runRuns(runsOptions, options, reporter);
1707
+ case 'service':
1708
+ if (!serviceOptions) {
1709
+ reporter.error('Missing service command options');
1710
+ return 1;
1711
+ }
1712
+ return runServiceCommand(serviceOptions, options, reporter);
1458
1713
  case 'build':
1459
1714
  return runBuild(options, reporter, { abortController, interrupted });
1460
1715
  case 'improve':