@sentry/warden 0.44.0 → 0.46.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 (98) 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 +3 -1
  14. package/dist/cli/main.d.ts.map +1 -1
  15. package/dist/cli/main.js +297 -36
  16. package/dist/cli/main.js.map +1 -1
  17. package/dist/cli/output/tasks.d.ts.map +1 -1
  18. package/dist/cli/output/tasks.js +1 -0
  19. package/dist/cli/output/tasks.js.map +1 -1
  20. package/dist/config/loader.d.ts +3 -1
  21. package/dist/config/loader.d.ts.map +1 -1
  22. package/dist/config/loader.js +19 -0
  23. package/dist/config/loader.js.map +1 -1
  24. package/dist/config/schema.d.ts +47 -0
  25. package/dist/config/schema.d.ts.map +1 -1
  26. package/dist/config/schema.js +43 -0
  27. package/dist/config/schema.js.map +1 -1
  28. package/dist/index.d.ts +5 -3
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +2 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/output/dedup.d.ts.map +1 -1
  33. package/dist/output/dedup.js +2 -0
  34. package/dist/output/dedup.js.map +1 -1
  35. package/dist/sdk/analyze.d.ts.map +1 -1
  36. package/dist/sdk/analyze.js +3 -2
  37. package/dist/sdk/analyze.js.map +1 -1
  38. package/dist/sdk/extract.d.ts +2 -0
  39. package/dist/sdk/extract.d.ts.map +1 -1
  40. package/dist/sdk/extract.js +2 -0
  41. package/dist/sdk/extract.js.map +1 -1
  42. package/dist/sdk/json-output.d.ts +2 -0
  43. package/dist/sdk/json-output.d.ts.map +1 -1
  44. package/dist/sdk/json-output.js +1 -0
  45. package/dist/sdk/json-output.js.map +1 -1
  46. package/dist/sdk/local.d.ts +6 -0
  47. package/dist/sdk/local.d.ts.map +1 -1
  48. package/dist/sdk/local.js +78 -2
  49. package/dist/sdk/local.js.map +1 -1
  50. package/dist/sdk/offline.d.ts +13 -0
  51. package/dist/sdk/offline.d.ts.map +1 -0
  52. package/dist/sdk/offline.js +22 -0
  53. package/dist/sdk/offline.js.map +1 -0
  54. package/dist/sdk/post-process.d.ts +1 -1
  55. package/dist/sdk/post-process.d.ts.map +1 -1
  56. package/dist/sdk/post-process.js +2 -1
  57. package/dist/sdk/post-process.js.map +1 -1
  58. package/dist/sdk/prompt.d.ts +1 -1
  59. package/dist/sdk/prompt.d.ts.map +1 -1
  60. package/dist/sdk/prompt.js +3 -1
  61. package/dist/sdk/prompt.js.map +1 -1
  62. package/dist/sdk/runner.d.ts +1 -1
  63. package/dist/sdk/runner.d.ts.map +1 -1
  64. package/dist/sdk/runner.js.map +1 -1
  65. package/dist/sdk/runtimes/claude.d.ts.map +1 -1
  66. package/dist/sdk/runtimes/claude.js.map +1 -1
  67. package/dist/sdk/runtimes/pi.d.ts.map +1 -1
  68. package/dist/sdk/runtimes/pi.js +149 -5
  69. package/dist/sdk/runtimes/pi.js.map +1 -1
  70. package/dist/sdk/runtimes/types.d.ts +2 -0
  71. package/dist/sdk/runtimes/types.d.ts.map +1 -1
  72. package/dist/sdk/types.d.ts +4 -0
  73. package/dist/sdk/types.d.ts.map +1 -1
  74. package/dist/service/client.d.ts +12 -0
  75. package/dist/service/client.d.ts.map +1 -0
  76. package/dist/service/client.js +35 -0
  77. package/dist/service/client.js.map +1 -0
  78. package/dist/service/findings.d.ts +337 -0
  79. package/dist/service/findings.d.ts.map +1 -0
  80. package/dist/service/findings.js +194 -0
  81. package/dist/service/findings.js.map +1 -0
  82. package/dist/service/index.d.ts +8 -0
  83. package/dist/service/index.d.ts.map +1 -0
  84. package/dist/service/index.js +6 -0
  85. package/dist/service/index.js.map +1 -0
  86. package/dist/service/memory.d.ts +4 -0
  87. package/dist/service/memory.d.ts.map +1 -0
  88. package/dist/service/memory.js +12 -0
  89. package/dist/service/memory.js.map +1 -0
  90. package/dist/service/options.d.ts +31 -0
  91. package/dist/service/options.d.ts.map +1 -0
  92. package/dist/service/options.js +51 -0
  93. package/dist/service/options.js.map +1 -0
  94. package/dist/service/projection.d.ts +43 -0
  95. package/dist/service/projection.d.ts.map +1 -0
  96. package/dist/service/projection.js +155 -0
  97. package/dist/service/projection.js.map +1 -0
  98. 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;
@@ -532,6 +537,8 @@ export function mergeSkillRunnerOptions(base, overrides) {
532
537
  merged.runtime = overrides.runtime;
533
538
  if (overrides.auxiliaryModel !== undefined)
534
539
  merged.auxiliaryModel = overrides.auxiliaryModel;
540
+ if (overrides.auxiliaryEffort !== undefined)
541
+ merged.auxiliaryEffort = overrides.auxiliaryEffort;
535
542
  if (overrides.synthesisModel !== undefined)
536
543
  merged.synthesisModel = overrides.synthesisModel;
537
544
  if (overrides.auxiliaryMaxRetries !== undefined) {
@@ -558,9 +565,15 @@ function loadOptionalConfig(options, repoPath) {
558
565
  : repoPath
559
566
  ? resolve(repoPath, 'warden.toml')
560
567
  : null;
561
- return configPath && existsSync(configPath)
568
+ const config = configPath && existsSync(configPath)
562
569
  ? loadWardenConfigFile(configPath)
563
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));
564
577
  }
565
578
  /**
566
579
  * Find the first Pi runner option using a model ID that is not provider/model.
@@ -576,11 +589,12 @@ function reportInvalidPiModelSelector(reporter, invalid) {
576
589
  reporter.tip('Set a Pi model selector such as anthropic/claude-sonnet-4-6.');
577
590
  }
578
591
  function emitInvalidPiModelSelectorRunLog(repoPath, options, invalid) {
579
- emitEmptyRunLog(repoPath, options, {
592
+ const error = {
580
593
  code: 'invalid_model_selector',
581
594
  message: invalidPiModelSelectorMessage(invalid),
582
595
  timestamp: new Date().toISOString(),
583
- });
596
+ };
597
+ return { run: emitEmptyRunLog(repoPath, options, error), error };
584
598
  }
585
599
  function verifyClaudeAuthForRun(args) {
586
600
  const { apiKey, pathToClaudeCodeExecutable, reporter, repoPath, options } = args;
@@ -589,17 +603,17 @@ function verifyClaudeAuthForRun(args) {
589
603
  }
590
604
  try {
591
605
  verifyAuth({ apiKey, pathToClaudeCodeExecutable });
592
- return true;
606
+ return undefined;
593
607
  }
594
608
  catch (error) {
595
609
  const message = error.message;
596
610
  reporter.error(message);
597
- emitEmptyRunLog(repoPath, options, {
611
+ const skillError = {
598
612
  code: 'auth_failed',
599
613
  message,
600
614
  timestamp: new Date().toISOString(),
601
- });
602
- return false;
615
+ };
616
+ return { run: emitEmptyRunLog(repoPath, options, skillError), error: skillError };
603
617
  }
604
618
  }
605
619
  function renderSkillRunHeader(args) {
@@ -619,7 +633,7 @@ async function createDirectSkillTask(args) {
619
633
  try {
620
634
  skill = await resolveSkillAsync(spec.skill, repoPath, {
621
635
  remote: spec.remote,
622
- offline: options.offline,
636
+ offline: isWardenOffline(),
623
637
  });
624
638
  }
625
639
  catch (error) {
@@ -792,6 +806,146 @@ async function outputResultsAndHandleFixes(processed, options, reporter, runLog,
792
806
  }
793
807
  return 0;
794
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
+ }
795
949
  /** Run one or more skills against an already constructed review context. */
796
950
  export async function runSkills(context, options, reporter) {
797
951
  const cwd = process.cwd();
@@ -800,6 +954,7 @@ export async function runSkills(context, options, reporter) {
800
954
  const apiKey = getAnthropicApiKey();
801
955
  const repoPath = findRepoPath(cwd);
802
956
  const config = loadOptionalConfig(options, repoPath);
957
+ const service = resolveCliService(options, config, reporter);
803
958
  const defaultModel = resolveCliDefaultModel(config, options.model);
804
959
  const defaultAuxiliaryModel = resolveCliDefaultAuxiliaryModel(config);
805
960
  const defaultSynthesisModel = resolveCliDefaultSynthesisModel(config);
@@ -827,6 +982,7 @@ export async function runSkills(context, options, reporter) {
827
982
  effort: options.effort ?? match?.effort ?? defaultEffort,
828
983
  runtime: options.runtime ?? match?.runtime ?? config?.defaults?.runtime ?? 'pi',
829
984
  auxiliaryModel: match?.auxiliaryModel ?? defaultAuxiliaryModel,
985
+ auxiliaryEffort: match?.auxiliaryEffort ?? config?.defaults?.auxiliary?.effort,
830
986
  synthesisModel: match?.synthesisModel ?? defaultSynthesisModel,
831
987
  auxiliaryMaxRetries: match?.auxiliaryMaxRetries ??
832
988
  config?.defaults?.auxiliary?.maxRetries ??
@@ -856,6 +1012,7 @@ export async function runSkills(context, options, reporter) {
856
1012
  runtime: options.runtime ?? t.runtime,
857
1013
  effort: options.effort ?? t.effort,
858
1014
  auxiliaryModel: t.auxiliaryModel,
1015
+ auxiliaryEffort: t.auxiliaryEffort,
859
1016
  synthesisModel: t.synthesisModel,
860
1017
  auxiliaryMaxRetries: t.auxiliaryMaxRetries,
861
1018
  verifyFindings: t.verifyFindings,
@@ -869,22 +1026,39 @@ export async function runSkills(context, options, reporter) {
869
1026
  emitRunMetric();
870
1027
  // Handle case where no skills to run
871
1028
  if (skillsToRun.length === 0) {
872
- emitEmptyRunLog(repoPath ?? cwd, options);
1029
+ const run = emitEmptyRunLog(repoPath ?? cwd, options);
1030
+ await publishCliEmptyRun({ service, context, run, reporter });
873
1031
  if (!options.json) {
874
1032
  reporter.warning('No triggers matched for the changed files');
875
1033
  reporter.tip('Specify a skill explicitly: warden <target> --skill <name>');
876
1034
  }
877
1035
  return 0;
878
1036
  }
879
- if (needsClaudeAuth(skillsToRun) && !verifyClaudeAuthForRun({
880
- apiKey,
881
- pathToClaudeCodeExecutable,
882
- reporter,
883
- repoPath: repoPath ?? cwd,
884
- options,
885
- })) {
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
+ });
886
1054
  return 1;
887
1055
  }
1056
+ const recall = await recallCliMemory({
1057
+ service,
1058
+ context,
1059
+ skills: skillsToRun.map((skill) => skill.skill),
1060
+ });
1061
+ const recalledMemories = recall?.memories ?? [];
888
1062
  // Build skill tasks
889
1063
  // Model precedence: defaults.agent.model > defaults.model > CLI flag > WARDEN_MODEL env var > SDK default
890
1064
  // sdkModel is undefined when no model is explicitly configured (lets SDK use its default).
@@ -897,6 +1071,7 @@ export async function runSkills(context, options, reporter) {
897
1071
  runtime: defaultRuntime,
898
1072
  pathToClaudeCodeExecutable,
899
1073
  auxiliaryModel: defaultAuxiliaryModel,
1074
+ auxiliaryEffort: config?.defaults?.auxiliary?.effort,
900
1075
  synthesisModel: defaultSynthesisModel,
901
1076
  abortController,
902
1077
  maxTurns: config?.defaults?.agent?.maxTurns ?? config?.defaults?.maxTurns,
@@ -910,6 +1085,7 @@ export async function runSkills(context, options, reporter) {
910
1085
  config?.defaults?.auxiliaryMaxRetries,
911
1086
  verifyFindings: config?.defaults?.verification?.enabled !== false,
912
1087
  captureTraces: options.traces,
1088
+ historicalEvidence: renderHistoricalMemory(recalledMemories),
913
1089
  };
914
1090
  const specs = skillsToRun.map(({ skill, remote, filters, ...skillOptions }) => ({
915
1091
  name: skill,
@@ -922,7 +1098,8 @@ export async function runSkills(context, options, reporter) {
922
1098
  const invalidModelSelector = findInvalidPiModelSelector(specs);
923
1099
  if (invalidModelSelector) {
924
1100
  reportInvalidPiModelSelector(reporter, invalidModelSelector);
925
- emitInvalidPiModelSelectorRunLog(repoPath ?? cwd, options, invalidModelSelector);
1101
+ const failure = emitInvalidPiModelSelectorRunLog(repoPath ?? cwd, options, invalidModelSelector);
1102
+ await publishCliEarlyFailure({ service, context, ...failure, skills: skillsToRun, reporter });
926
1103
  return 1;
927
1104
  }
928
1105
  let tasks;
@@ -939,11 +1116,13 @@ export async function runSkills(context, options, reporter) {
939
1116
  catch (error) {
940
1117
  const message = error instanceof Error ? error.message : String(error);
941
1118
  reporter.error(message);
942
- emitEmptyRunLog(repoPath ?? cwd, options, {
1119
+ const skillError = {
943
1120
  code: 'unknown',
944
1121
  message,
945
1122
  timestamp: new Date().toISOString(),
946
- });
1123
+ };
1124
+ const run = emitEmptyRunLog(repoPath ?? cwd, options, skillError);
1125
+ await publishCliEarlyFailure({ service, context, run, error: skillError, skills: skillsToRun, reporter });
947
1126
  return 1;
948
1127
  }
949
1128
  // Open the run's JSONL log before launching skills so `warden runs
@@ -986,7 +1165,19 @@ export async function runSkills(context, options, reporter) {
986
1165
  const totalDuration = Date.now() - startTime;
987
1166
  const effectiveMinConfidence = options.minConfidence ?? config?.defaults?.minConfidence ?? 'medium';
988
1167
  const processed = processTaskResults(results, options.reportOn, effectiveMinConfidence);
989
- 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;
990
1181
  }
991
1182
  async function runFileMode(filePatterns, options, reporter) {
992
1183
  const cwd = process.cwd();
@@ -1058,6 +1249,7 @@ async function runGitRefMode(gitRef, options, reporter) {
1058
1249
  // Load config to get defaultBranch if available
1059
1250
  const configPath = resolveConfigPath(options, repoPath);
1060
1251
  const config = existsSync(configPath) ? loadWardenConfigFile(configPath) : null;
1252
+ applyWardenOfflinePolicy(options, config);
1061
1253
  // Build context from local git
1062
1254
  reporter.startContext(`Analyzing changes from ${gitRef}...`);
1063
1255
  const context = buildLocalEventContext({
@@ -1103,6 +1295,8 @@ async function runConfigMode(options, reporter) {
1103
1295
  }
1104
1296
  // Load config
1105
1297
  const config = loadWardenConfigFile(configPath);
1298
+ applyWardenOfflinePolicy(options, config);
1299
+ const service = resolveCliService(options, config, reporter);
1106
1300
  // Build context from local git. By default, mirror PR-style analysis:
1107
1301
  // compare the configured/default branch merge base to HEAD.
1108
1302
  const localReviewBase = resolveLocalReviewBase(config.defaults?.defaultBranch, repoPath);
@@ -1123,7 +1317,8 @@ async function runConfigMode(options, reporter) {
1123
1317
  return 1;
1124
1318
  }
1125
1319
  if (pullRequest.files.length === 0) {
1126
- emitEmptyRunLog(repoPath, options);
1320
+ const run = emitEmptyRunLog(repoPath, options);
1321
+ await publishCliEmptyRun({ service, context, run, reporter });
1127
1322
  if (!options.json) {
1128
1323
  if (options.staged) {
1129
1324
  reporter.renderEmptyState('No staged changes found');
@@ -1156,7 +1351,8 @@ async function runConfigMode(options, reporter) {
1156
1351
  }
1157
1352
  const triggersToRun = [...seen.values()];
1158
1353
  if (triggersToRun.length === 0) {
1159
- emitEmptyRunLog(repoPath, options);
1354
+ const run = emitEmptyRunLog(repoPath, options);
1355
+ await publishCliEmptyRun({ service, context, run, reporter });
1160
1356
  if (!options.json) {
1161
1357
  reporter.blank();
1162
1358
  if (options.skill) {
@@ -1176,15 +1372,35 @@ async function runConfigMode(options, reporter) {
1176
1372
  const triggerRuntimes = triggersToRun.map((trigger) => ({
1177
1373
  runtime: options.runtime ?? trigger.runtime,
1178
1374
  }));
1179
- if (needsClaudeAuth(triggerRuntimes) && !verifyClaudeAuthForRun({
1180
- apiKey,
1181
- pathToClaudeCodeExecutable,
1182
- reporter,
1183
- repoPath,
1184
- options,
1185
- })) {
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
+ });
1186
1396
  return 1;
1187
1397
  }
1398
+ const recall = await recallCliMemory({
1399
+ service,
1400
+ context,
1401
+ skills: triggersToRun.map((trigger) => trigger.skill),
1402
+ });
1403
+ const recalledMemories = recall?.memories ?? [];
1188
1404
  // Build trigger tasks
1189
1405
  const effectiveMinConfidence = options.minConfidence ?? config.defaults?.minConfidence ?? 'medium';
1190
1406
  const specs = triggersToRun.map((trigger) => ({
@@ -1203,6 +1419,7 @@ async function runConfigMode(options, reporter) {
1203
1419
  pathToClaudeCodeExecutable,
1204
1420
  effort: options.effort ?? trigger.effort,
1205
1421
  auxiliaryModel: trigger.auxiliaryModel,
1422
+ auxiliaryEffort: trigger.auxiliaryEffort,
1206
1423
  synthesisModel: trigger.synthesisModel,
1207
1424
  abortController,
1208
1425
  maxTurns: trigger.maxTurns,
@@ -1213,12 +1430,24 @@ async function runConfigMode(options, reporter) {
1213
1430
  auxiliaryMaxRetries: trigger.auxiliaryMaxRetries,
1214
1431
  verifyFindings: trigger.verifyFindings,
1215
1432
  captureTraces: options.traces,
1433
+ historicalEvidence: renderHistoricalMemory(recalledMemories),
1216
1434
  },
1217
1435
  }));
1218
1436
  const invalidModelSelector = findInvalidPiModelSelector(specs);
1219
1437
  if (invalidModelSelector) {
1220
1438
  reportInvalidPiModelSelector(reporter, invalidModelSelector);
1221
- 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
+ });
1222
1451
  return 1;
1223
1452
  }
1224
1453
  let tasks;
@@ -1235,10 +1464,23 @@ async function runConfigMode(options, reporter) {
1235
1464
  catch (error) {
1236
1465
  const message = error instanceof Error ? error.message : String(error);
1237
1466
  reporter.error(message);
1238
- emitEmptyRunLog(repoPath, options, {
1467
+ const skillError = {
1239
1468
  code: 'unknown',
1240
1469
  message,
1241
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,
1242
1484
  });
1243
1485
  return 1;
1244
1486
  }
@@ -1285,7 +1527,19 @@ async function runConfigMode(options, reporter) {
1285
1527
  // Process results and output
1286
1528
  const totalDuration = Date.now() - startTime;
1287
1529
  const processed = processTaskResults(results, options.reportOn, effectiveMinConfidence);
1288
- 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;
1289
1543
  }
1290
1544
  async function runDirectSkillMode(options, reporter) {
1291
1545
  const cwd = process.cwd();
@@ -1301,6 +1555,7 @@ async function runDirectSkillMode(options, reporter) {
1301
1555
  // Load config to get defaultBranch if available
1302
1556
  const configPath = resolveConfigPath(options, repoPath);
1303
1557
  const config = existsSync(configPath) ? loadWardenConfigFile(configPath) : null;
1558
+ applyWardenOfflinePolicy(options, config);
1304
1559
  // Build context from local git. By default, mirror PR-style analysis:
1305
1560
  // compare the configured/default branch merge base to HEAD.
1306
1561
  const localReviewBase = resolveLocalReviewBase(config?.defaults?.defaultBranch, repoPath);
@@ -1385,7 +1640,7 @@ async function runCommand(options, reporter) {
1385
1640
  }
1386
1641
  /** Parse CLI input, dispatch the selected command, and perform shutdown cleanup. */
1387
1642
  export async function main() {
1388
- const { command, options, helpTarget, setupAppOptions, runsOptions } = parseCliArgs();
1643
+ const { command, options, helpTarget, setupAppOptions, runsOptions, serviceOptions } = parseCliArgs();
1389
1644
  if (command === 'help') {
1390
1645
  showHelp(helpTarget);
1391
1646
  process.exit(0);
@@ -1449,6 +1704,12 @@ export async function main() {
1449
1704
  process.exit(1);
1450
1705
  }
1451
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);
1452
1713
  case 'build':
1453
1714
  return runBuild(options, reporter, { abortController, interrupted });
1454
1715
  case 'improve':