@harness-engineering/orchestrator 0.4.6 → 0.5.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.
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import { Issue, AgentEvent, WorkflowConfig, TokenUsage, ConcernSignal, ScopeTier, EscalationConfig, RoutingDecision, Result, WorkflowDefinition, WorkspaceConfig, HooksConfig, AgentBackend, SessionStartParams, AgentSession, AgentError, TurnParams, TurnResult, BackendDef, RoutingConfig, RoutingUseCase, ContainerConfig, SecretConfig, AgentConfig, TokenScope, AuthToken, AuthTokenPublic } from '@harness-engineering/types';
2
- import { IssueTrackerClient, Issue as Issue$1, TrackerConfig, CacheMetricsRecorder } from '@harness-engineering/core';
3
- import { EnrichedSpec, ComplexityScore, SimulationResult, IntelligencePipeline, WeightedRecommendation } from '@harness-engineering/intelligence';
1
+ import { Issue, AgentEvent, WorkflowConfig, TokenUsage, ConcernSignal, ScopeTier, EscalationConfig, RoutingDecision, Result, WorkflowDefinition, WorkspaceConfig, HooksConfig, AgentBackend, SessionStartParams, AgentSession, AgentError, TurnParams, TurnResult, BackendDef, RoutingConfig, RoutingUseCase, ContainerConfig, SecretConfig, AgentConfig, TokenScope, AuthToken, AuthTokenPublic, IndexedFileKind, SessionSearchResult, ReindexStats, SessionSummarizationConfig, SessionSummary, SessionSummaryMeta, SessionsConfig, GatewayEvent, NotificationEnvelope, NotificationDeliveryResult, NotificationSinkConfig, NotificationsConfig } from '@harness-engineering/types';
2
+ import { IssueTrackerClient, Issue as Issue$1, TrackerConfig, CacheMetricsRecorder, ArchiveHooks } from '@harness-engineering/core';
3
+ import { EnrichedSpec, ComplexityScore, SimulationResult, IntelligencePipeline, WeightedRecommendation, AnalysisProvider } from '@harness-engineering/intelligence';
4
4
  import { GraphStore } from '@harness-engineering/graph';
5
5
  import { execFile } from 'node:child_process';
6
6
  import { EventEmitter } from 'node:events';
@@ -1117,6 +1117,15 @@ interface RunResult {
1117
1117
  prUpdated: boolean;
1118
1118
  /** Error message if status is 'failure' */
1119
1119
  error?: string;
1120
+ /**
1121
+ * Cumulative agent spend in USD for this run (Hermes Phase 5).
1122
+ *
1123
+ * Always present (defaults to 0). Populated by the
1124
+ * `CostCeilingMonitor` from per-turn `TokenUsage` × `ModelPricing`.
1125
+ * When the run aborted on cost-ceiling exceed, `status === 'failure'`
1126
+ * and `error === 'cost_ceiling_exceeded'`.
1127
+ */
1128
+ costUsd?: number;
1120
1129
  }
1121
1130
  /**
1122
1131
  * Schedule entry for a single task, used in MaintenanceStatus.
@@ -1226,6 +1235,8 @@ declare class Orchestrator extends EventEmitter {
1226
1235
  private cacheMetrics?;
1227
1236
  private otlpExporter?;
1228
1237
  private telemetryFanoutOff?;
1238
+ private notificationsRegistry?;
1239
+ private notificationFanoutOff?;
1229
1240
  private orchestratorIdPromise;
1230
1241
  private recorder;
1231
1242
  private intelligenceRunner;
@@ -1341,6 +1352,15 @@ declare class Orchestrator extends EventEmitter {
1341
1352
  * Informs the state machine that an agent worker has exited.
1342
1353
  */
1343
1354
  private emitWorkerExit;
1355
+ /**
1356
+ * Hermes Phase 3: wire in-process notification sinks against the
1357
+ * orchestrator's event bus (`this`). A misconfigured sink (unknown kind,
1358
+ * missing env var) logs + skips rather than breaking startup — the
1359
+ * hardened doctor (`harness doctor`) surfaces the gap. Sinks subscribe
1360
+ * to the same topics as `wireWebhookFanout`; a slow Slack call cannot
1361
+ * block webhook delivery because the two paths fan out independently.
1362
+ */
1363
+ private setupNotifications;
1344
1364
  /**
1345
1365
  * Stops execution for a specific issue.
1346
1366
  *
@@ -1697,4 +1717,267 @@ declare class WebhookQueue {
1697
1717
  close(): void;
1698
1718
  }
1699
1719
 
1700
- export { type AgentUpdateEvent, AnalysisArchive, type AnalysisRecord, type ApplyEventResult, type ArtifactPresence, type AttemptStats, BackendRouter, type BackendRouterOptions, type BaseRefFallbackEvent, ClaimManager, type ClaimManagerConfig, type CleanWorkspaceEffect, type CreateTokenInput, type CreateTokenResult, type DispatchEffect, type EmitLogEffect, type EscalateEffect, type ExecFileFn$1 as ExecFileFn, type Highlight, type HighlightsInfo, InteractionQueue, type LinearGraphQLExtension, LinearGraphQLStub, type LiveSession, MAX_ATTEMPTS, type MigrationResult, MockBackend, ORCHESTRATOR_IDENTITY_FILE, Orchestrator, OrchestratorBackendFactory, type OrchestratorBackendFactoryOptions, type OrchestratorContext, type OrchestratorEvent, type OrchestratorState, PRDetector, type PRDetectorLogger, type PendingInteraction, PromptRenderer, type PublishedIndex, type QueueInsertInput, type QueueRow, type QueueStats, RETRY_DELAYS_MS, type RateLimitSnapshot as RateLimitComputeSnapshot, type RateLimitConfig, type RateLimitSnapshot$1 as RateLimitSnapshot, type ReleaseClaimEffect, type RetryEntry, type RetryFiredEvent, RoadmapTrackerAdapter, type RunAttemptPhase, type RunningEntry, type ScheduleRetryEffect, type SideEffect, type StallDetectedEvent, type StopEffect, type StreamManifest, StreamRecorder, type SyncMainOptions, type SyncMainResult, type SyncSkipReason, type TickEvent, TokenStore, type TokenTotals, type TriageConfig, type TriageDecision, type TriageSignals, type TriageSkill, type UpdateTokensEffect, WebhookQueue, type WorkerExitEvent, WorkflowLoader, WorkspaceHooks, WorkspaceManager, type WorkspaceManagerOptions, applyEvent, artifactPresenceFromIssue, calculateRetryDelay, canDispatch, computeRateLimitDelay, createBackend, createEmptyState, detectScopeTier, extractHighlights, extractTitlePrefix, getAvailableSlots, getDefaultConfig, getPerStateCount, isEligible, launchTUI, loadPublishedIndex, migrateAgentConfig, reconcile, renderAnalysisComment, renderPRComment, resolveEscalationConfig, resolveOrchestratorId, routeIssue, savePublishedIndex, selectCandidates, sortCandidates, syncMain, triageIssue, validateWorkflowConfig };
1720
+ interface IndexedDoc {
1721
+ sessionId: string;
1722
+ archived: boolean;
1723
+ fileKind: IndexedFileKind;
1724
+ /** Path relative to project root, posix-style. */
1725
+ path: string;
1726
+ mtimeMs: number;
1727
+ body: string;
1728
+ }
1729
+ interface SearchOptions {
1730
+ limit?: number;
1731
+ archivedOnly?: boolean;
1732
+ fileKinds?: IndexedFileKind[];
1733
+ /** Maximum bytes to retain per doc body (defaults 256 KiB; longer bodies are truncated with a marker). */
1734
+ maxBytesPerBody?: number;
1735
+ }
1736
+ /**
1737
+ * Convert a user-typed query string into a safe FTS5 expression.
1738
+ *
1739
+ * If the caller's query already contains explicit FTS5 syntax markers (double
1740
+ * quotes, parens, asterisk, caret, plus, the literal words AND/OR/NOT or a
1741
+ * `column:` selector) it is passed through unchanged so power users keep the
1742
+ * full FTS5 grammar.
1743
+ *
1744
+ * Otherwise each whitespace-separated token is wrapped as an FTS5 phrase so
1745
+ * characters like `-`, `:` and `*` inside the token are treated as content
1746
+ * (not operators), and the tokens are implicitly AND-joined by FTS5.
1747
+ *
1748
+ * Without this, `idx.search('token-aleph')` is parsed by FTS5 as
1749
+ * `token NOT aleph` and fails with `no such column: aleph`.
1750
+ */
1751
+ declare function normalizeFts5Query(query: string): string;
1752
+ /**
1753
+ * Filesystem path of the search-index sqlite file for a given project root.
1754
+ * Stable so consumers (cleanup tools, doctor, gitignore guards) can locate it.
1755
+ */
1756
+ declare function searchIndexPath(projectPath: string): string;
1757
+ declare class SqliteSearchIndex {
1758
+ private readonly db;
1759
+ private readonly upsertStmt;
1760
+ private readonly removeSessionStmt;
1761
+ private readonly totalStmt;
1762
+ constructor(dbPath: string);
1763
+ upsertSessionDoc(doc: IndexedDoc): void;
1764
+ removeSession(sessionId: string): number;
1765
+ /**
1766
+ * Drop all `archived=1` rows. Used by `reindexFromArchive` before a full
1767
+ * re-walk. Live (archived=0) rows are preserved.
1768
+ */
1769
+ resetArchived(): void;
1770
+ /** Total rows currently indexed (across both live and archived). */
1771
+ totalIndexed(): number;
1772
+ /**
1773
+ * Ranked FTS5 query. Returns BM25-sorted matches. The `query` is passed to
1774
+ * FTS5 as-is; FTS5 syntax (phrases with quotes, AND/OR/NOT, `column:term`)
1775
+ * is therefore the user-facing language. Errors from malformed queries
1776
+ * surface as thrown `SqliteError` so the CLI can catch + render them.
1777
+ */
1778
+ search(query: string, opts?: SearchOptions): SessionSearchResult;
1779
+ close(): void;
1780
+ }
1781
+ /** Open (or create) the project's search index. Idempotent. */
1782
+ declare function openSearchIndex(projectPath: string): SqliteSearchIndex;
1783
+ /**
1784
+ * Walk a session/archive directory and upsert one row per existing file_kind.
1785
+ * Used by both the archive hook (`indexArchivedSession`) and `reindexFromArchive`.
1786
+ *
1787
+ * Bodies larger than `maxBytesPerBody` are truncated with a marker so the index
1788
+ * does not bloat on pathological session files.
1789
+ */
1790
+ declare function indexSessionDirectory(idx: SqliteSearchIndex, args: {
1791
+ sessionId: string;
1792
+ sessionDir: string;
1793
+ archived: boolean;
1794
+ projectPath: string;
1795
+ /** Subset of file_kinds to consider (defaults to all). */
1796
+ fileKinds?: IndexedFileKind[];
1797
+ maxBytesPerBody?: number;
1798
+ }): {
1799
+ docsWritten: number;
1800
+ };
1801
+ /**
1802
+ * Drop and rebuild the `archived=1` portion of the index from
1803
+ * `.harness/archive/sessions/<slug-date>/`. Idempotent.
1804
+ *
1805
+ * Each subdirectory is treated as one session whose id is the basename.
1806
+ */
1807
+ declare function reindexFromArchive(projectPath: string, opts?: {
1808
+ fileKinds?: IndexedFileKind[];
1809
+ maxBytesPerBody?: number;
1810
+ }): ReindexStats;
1811
+
1812
+ interface SummarizeContext {
1813
+ /** Path to the archived session directory, e.g. .harness/archive/sessions/foo-2026-05-16. */
1814
+ archiveDir: string;
1815
+ /** Resolved AnalysisProvider — caller skips this step when no provider is available. */
1816
+ provider: AnalysisProvider;
1817
+ /** Optional session summary config, defaults applied when fields are missing. */
1818
+ config?: SessionSummarizationConfig | undefined;
1819
+ /** Optional logger; falls back to console.warn for diagnostics. */
1820
+ logger?: {
1821
+ warn?: (msg: string, meta?: Record<string, unknown>) => void;
1822
+ };
1823
+ /** When true, on provider error a stub `llm-summary.md` is still written. Default true. */
1824
+ writeStubOnError?: boolean;
1825
+ }
1826
+ interface SummarizeResult {
1827
+ summary: SessionSummary;
1828
+ meta: SessionSummaryMeta;
1829
+ filePath: string;
1830
+ }
1831
+ /** Approximate token cap via char count; conservative because tokens average ~4 chars. */
1832
+ declare function truncateForBudget(text: string, inputBudgetTokens: number): string;
1833
+ /** Render the structured summary as the `llm-summary.md` markdown payload. */
1834
+ declare function renderLlmSummaryMarkdown(summary: SessionSummary, meta: SessionSummaryMeta): string;
1835
+ /**
1836
+ * Summarise a single archived session via the provided AnalysisProvider.
1837
+ *
1838
+ * On success: writes `llm-summary.md` into the archive directory and returns
1839
+ * the structured summary + metadata.
1840
+ *
1841
+ * On provider error: optionally writes a stub `llm-summary.md` so callers can
1842
+ * still detect that summarization was attempted, then returns `Err`.
1843
+ *
1844
+ * Empty / missing input corpus is returned as `Err` and never produces a file.
1845
+ */
1846
+ declare function summarizeArchivedSession(ctx: SummarizeContext): Promise<Result<SummarizeResult, Error>>;
1847
+ /** Resolve whether summarization should run for the given config. */
1848
+ declare function isSummaryEnabled(config?: SessionSummarizationConfig): boolean;
1849
+
1850
+ /**
1851
+ * Session archive hook bundle.
1852
+ *
1853
+ * `buildArchiveHooks()` returns an `ArchiveHooks` implementation that wires
1854
+ * `summarizeArchivedSession()` + `indexSessionDirectory()` together so the
1855
+ * core `archiveSession()` lifecycle invokes both after a successful move.
1856
+ *
1857
+ * Both steps are individually wrapped in try/catch — failure of either does
1858
+ * not propagate up the call stack. Spec: §"Risks" treats summary + index
1859
+ * failure as non-fatal.
1860
+ */
1861
+
1862
+ interface BuildArchiveHooksOptions {
1863
+ /** Absolute path to the project root (contains `.harness/`). */
1864
+ projectPath: string;
1865
+ /** Optional AnalysisProvider — summarization is skipped when omitted. */
1866
+ provider?: AnalysisProvider | undefined;
1867
+ /** Optional sessions config slice. */
1868
+ config?: SessionsConfig | undefined;
1869
+ /** Optional logger; falls back to console.warn. */
1870
+ logger?: HookLogger | undefined;
1871
+ }
1872
+ interface HookLogger {
1873
+ warn?: (msg: string, meta?: Record<string, unknown>) => void;
1874
+ }
1875
+ /**
1876
+ * Construct the `ArchiveHooks` impl. Always returns a working hook bundle —
1877
+ * missing provider or disabled config simply skips that step.
1878
+ */
1879
+ declare function buildArchiveHooks(opts: BuildArchiveHooksOptions): ArchiveHooks;
1880
+
1881
+ /**
1882
+ * Wrap a `GatewayEvent` into a platform-agnostic `NotificationEnvelope`.
1883
+ * Used when a sink has `wrap_response: true` in its config. Unknown event
1884
+ * types fall back to a generic title/summary so newly-emitted events do
1885
+ * not require a code change to be deliverable.
1886
+ */
1887
+ declare function wrapAsEnvelope(event: GatewayEvent): NotificationEnvelope;
1888
+
1889
+ /**
1890
+ * Payload passed to `NotificationSink.deliver`. `wrapped` discriminates
1891
+ * which member of the union `payload` is. Sinks branch on `wrapped`
1892
+ * rather than runtime-detecting the shape.
1893
+ */
1894
+ interface NotificationSinkDeliverInput {
1895
+ payload: GatewayEvent | NotificationEnvelope;
1896
+ wrapped: boolean;
1897
+ }
1898
+ /**
1899
+ * Hermes Phase 3 sink contract.
1900
+ *
1901
+ * Sinks subscribe to the orchestrator event bus via `wireNotificationSinks`
1902
+ * and deliver each filtered event to a destination (chat channel, webhook
1903
+ * URL, etc.). Delivery is best-effort: no retry, no persistence.
1904
+ *
1905
+ * Sinks MUST be idempotent w.r.t. their own delivery semantics — the bus
1906
+ * may emit the same logical state transition more than once during testing
1907
+ * or recovery. Sinks should not assume one-shot semantics.
1908
+ */
1909
+ interface NotificationSink {
1910
+ /** Stable id used in config + CLI; lowercase, kebab-case. */
1911
+ readonly id: string;
1912
+ /** Sink kind literal (matches `NotificationSinkKind`). */
1913
+ readonly kind: string;
1914
+ /** One-shot delivery. Returns Ok on 2xx, Err on any other outcome. */
1915
+ deliver(input: NotificationSinkDeliverInput): Promise<NotificationDeliveryResult>;
1916
+ /** Optional teardown hook called on orchestrator stop. */
1917
+ dispose?(): Promise<void>;
1918
+ }
1919
+
1920
+ interface SlackSinkOptions {
1921
+ id: string;
1922
+ webhookUrl: string;
1923
+ fetchImpl?: typeof fetch;
1924
+ timeoutMs?: number;
1925
+ }
1926
+ /**
1927
+ * Slack sink shipped with Hermes Phase 3. Uses incoming-webhook URLs only;
1928
+ * OAuth + bot tokens are intentionally out of scope (spec D3). Sends one
1929
+ * HTTP POST per delivery and never retries — retries are the operator's
1930
+ * call via the Phase 0 webhook fanout if they need durable delivery.
1931
+ */
1932
+ declare class SlackSink implements NotificationSink {
1933
+ readonly kind = "slack";
1934
+ readonly id: string;
1935
+ private readonly webhookUrl;
1936
+ private readonly fetchImpl;
1937
+ private readonly timeoutMs;
1938
+ constructor(opts: SlackSinkOptions);
1939
+ deliver(input: NotificationSinkDeliverInput): Promise<NotificationDeliveryResult>;
1940
+ private renderEnvelope;
1941
+ private renderRawEvent;
1942
+ }
1943
+
1944
+ interface RegistryEntry {
1945
+ config: NotificationSinkConfig;
1946
+ adapter: NotificationSink;
1947
+ }
1948
+ interface FromConfigOptions {
1949
+ env: NodeJS.ProcessEnv;
1950
+ /** Optional per-sink fetch override (testing). */
1951
+ fetchImpl?: typeof fetch;
1952
+ }
1953
+ /**
1954
+ * Surfaced when a sink config refers to an unknown kind, or its env-var
1955
+ * secret cannot be resolved. Carrying the sinkId helps the doctor + CLI
1956
+ * print operator-actionable messages.
1957
+ */
1958
+ declare class SinkConfigError extends Error {
1959
+ readonly sinkId: string;
1960
+ constructor(sinkId: string, message: string);
1961
+ }
1962
+ /**
1963
+ * In-memory registry of configured notification sinks. Built once at
1964
+ * orchestrator startup from `harness.config.json` `notifications.sinks[]`.
1965
+ * Disposed on orchestrator stop.
1966
+ */
1967
+ declare class SinkRegistry {
1968
+ private readonly entries;
1969
+ private constructor();
1970
+ static fromConfig(config: NotificationsConfig, options: FromConfigOptions): SinkRegistry;
1971
+ list(): readonly RegistryEntry[];
1972
+ get(id: string): RegistryEntry | null;
1973
+ ids(): string[];
1974
+ dispose(): Promise<void>;
1975
+ }
1976
+
1977
+ interface WireParams {
1978
+ bus: EventEmitter;
1979
+ registry: SinkRegistry;
1980
+ }
1981
+ declare function wireNotificationSinks({ bus, registry }: WireParams): () => void;
1982
+
1983
+ export { type AgentUpdateEvent, AnalysisArchive, type AnalysisRecord, type ApplyEventResult, type ArtifactPresence, type AttemptStats, BackendRouter, type BackendRouterOptions, type BaseRefFallbackEvent, type BuildArchiveHooksOptions, ClaimManager, type ClaimManagerConfig, type CleanWorkspaceEffect, type CreateTokenInput, type CreateTokenResult, type DispatchEffect, type EmitLogEffect, type EscalateEffect, type ExecFileFn$1 as ExecFileFn, type FromConfigOptions, type Highlight, type HighlightsInfo, type IndexedDoc, InteractionQueue, type LinearGraphQLExtension, LinearGraphQLStub, type LiveSession, MAX_ATTEMPTS, type MigrationResult, MockBackend, type NotificationSink, type NotificationSinkDeliverInput, ORCHESTRATOR_IDENTITY_FILE, Orchestrator, OrchestratorBackendFactory, type OrchestratorBackendFactoryOptions, type OrchestratorContext, type OrchestratorEvent, type OrchestratorState, PRDetector, type PRDetectorLogger, type PendingInteraction, PromptRenderer, type PublishedIndex, type QueueInsertInput, type QueueRow, type QueueStats, RETRY_DELAYS_MS, type RateLimitSnapshot as RateLimitComputeSnapshot, type RateLimitConfig, type RateLimitSnapshot$1 as RateLimitSnapshot, type RegistryEntry, type ReleaseClaimEffect, type RetryEntry, type RetryFiredEvent, RoadmapTrackerAdapter, type RunAttemptPhase, type RunningEntry, type ScheduleRetryEffect, type SearchOptions, type SideEffect, SinkConfigError, SinkRegistry, SlackSink, type SlackSinkOptions, SqliteSearchIndex, type StallDetectedEvent, type StopEffect, type StreamManifest, StreamRecorder, type SummarizeContext, type SummarizeResult, type SyncMainOptions, type SyncMainResult, type SyncSkipReason, type TickEvent, TokenStore, type TokenTotals, type TriageConfig, type TriageDecision, type TriageSignals, type TriageSkill, type UpdateTokensEffect, WebhookQueue, type WorkerExitEvent, WorkflowLoader, WorkspaceHooks, WorkspaceManager, type WorkspaceManagerOptions, applyEvent, artifactPresenceFromIssue, buildArchiveHooks, calculateRetryDelay, canDispatch, computeRateLimitDelay, createBackend, createEmptyState, detectScopeTier, extractHighlights, extractTitlePrefix, getAvailableSlots, getDefaultConfig, getPerStateCount, indexSessionDirectory, isEligible, isSummaryEnabled, launchTUI, loadPublishedIndex, migrateAgentConfig, normalizeFts5Query, openSearchIndex, reconcile, reindexFromArchive, renderAnalysisComment, renderLlmSummaryMarkdown, renderPRComment, resolveEscalationConfig, resolveOrchestratorId, routeIssue, savePublishedIndex, searchIndexPath, selectCandidates, sortCandidates, summarizeArchivedSession, syncMain, triageIssue, truncateForBudget, validateWorkflowConfig, wireNotificationSinks, wrapAsEnvelope };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { Issue, AgentEvent, WorkflowConfig, TokenUsage, ConcernSignal, ScopeTier, EscalationConfig, RoutingDecision, Result, WorkflowDefinition, WorkspaceConfig, HooksConfig, AgentBackend, SessionStartParams, AgentSession, AgentError, TurnParams, TurnResult, BackendDef, RoutingConfig, RoutingUseCase, ContainerConfig, SecretConfig, AgentConfig, TokenScope, AuthToken, AuthTokenPublic } from '@harness-engineering/types';
2
- import { IssueTrackerClient, Issue as Issue$1, TrackerConfig, CacheMetricsRecorder } from '@harness-engineering/core';
3
- import { EnrichedSpec, ComplexityScore, SimulationResult, IntelligencePipeline, WeightedRecommendation } from '@harness-engineering/intelligence';
1
+ import { Issue, AgentEvent, WorkflowConfig, TokenUsage, ConcernSignal, ScopeTier, EscalationConfig, RoutingDecision, Result, WorkflowDefinition, WorkspaceConfig, HooksConfig, AgentBackend, SessionStartParams, AgentSession, AgentError, TurnParams, TurnResult, BackendDef, RoutingConfig, RoutingUseCase, ContainerConfig, SecretConfig, AgentConfig, TokenScope, AuthToken, AuthTokenPublic, IndexedFileKind, SessionSearchResult, ReindexStats, SessionSummarizationConfig, SessionSummary, SessionSummaryMeta, SessionsConfig, GatewayEvent, NotificationEnvelope, NotificationDeliveryResult, NotificationSinkConfig, NotificationsConfig } from '@harness-engineering/types';
2
+ import { IssueTrackerClient, Issue as Issue$1, TrackerConfig, CacheMetricsRecorder, ArchiveHooks } from '@harness-engineering/core';
3
+ import { EnrichedSpec, ComplexityScore, SimulationResult, IntelligencePipeline, WeightedRecommendation, AnalysisProvider } from '@harness-engineering/intelligence';
4
4
  import { GraphStore } from '@harness-engineering/graph';
5
5
  import { execFile } from 'node:child_process';
6
6
  import { EventEmitter } from 'node:events';
@@ -1117,6 +1117,15 @@ interface RunResult {
1117
1117
  prUpdated: boolean;
1118
1118
  /** Error message if status is 'failure' */
1119
1119
  error?: string;
1120
+ /**
1121
+ * Cumulative agent spend in USD for this run (Hermes Phase 5).
1122
+ *
1123
+ * Always present (defaults to 0). Populated by the
1124
+ * `CostCeilingMonitor` from per-turn `TokenUsage` × `ModelPricing`.
1125
+ * When the run aborted on cost-ceiling exceed, `status === 'failure'`
1126
+ * and `error === 'cost_ceiling_exceeded'`.
1127
+ */
1128
+ costUsd?: number;
1120
1129
  }
1121
1130
  /**
1122
1131
  * Schedule entry for a single task, used in MaintenanceStatus.
@@ -1226,6 +1235,8 @@ declare class Orchestrator extends EventEmitter {
1226
1235
  private cacheMetrics?;
1227
1236
  private otlpExporter?;
1228
1237
  private telemetryFanoutOff?;
1238
+ private notificationsRegistry?;
1239
+ private notificationFanoutOff?;
1229
1240
  private orchestratorIdPromise;
1230
1241
  private recorder;
1231
1242
  private intelligenceRunner;
@@ -1341,6 +1352,15 @@ declare class Orchestrator extends EventEmitter {
1341
1352
  * Informs the state machine that an agent worker has exited.
1342
1353
  */
1343
1354
  private emitWorkerExit;
1355
+ /**
1356
+ * Hermes Phase 3: wire in-process notification sinks against the
1357
+ * orchestrator's event bus (`this`). A misconfigured sink (unknown kind,
1358
+ * missing env var) logs + skips rather than breaking startup — the
1359
+ * hardened doctor (`harness doctor`) surfaces the gap. Sinks subscribe
1360
+ * to the same topics as `wireWebhookFanout`; a slow Slack call cannot
1361
+ * block webhook delivery because the two paths fan out independently.
1362
+ */
1363
+ private setupNotifications;
1344
1364
  /**
1345
1365
  * Stops execution for a specific issue.
1346
1366
  *
@@ -1697,4 +1717,267 @@ declare class WebhookQueue {
1697
1717
  close(): void;
1698
1718
  }
1699
1719
 
1700
- export { type AgentUpdateEvent, AnalysisArchive, type AnalysisRecord, type ApplyEventResult, type ArtifactPresence, type AttemptStats, BackendRouter, type BackendRouterOptions, type BaseRefFallbackEvent, ClaimManager, type ClaimManagerConfig, type CleanWorkspaceEffect, type CreateTokenInput, type CreateTokenResult, type DispatchEffect, type EmitLogEffect, type EscalateEffect, type ExecFileFn$1 as ExecFileFn, type Highlight, type HighlightsInfo, InteractionQueue, type LinearGraphQLExtension, LinearGraphQLStub, type LiveSession, MAX_ATTEMPTS, type MigrationResult, MockBackend, ORCHESTRATOR_IDENTITY_FILE, Orchestrator, OrchestratorBackendFactory, type OrchestratorBackendFactoryOptions, type OrchestratorContext, type OrchestratorEvent, type OrchestratorState, PRDetector, type PRDetectorLogger, type PendingInteraction, PromptRenderer, type PublishedIndex, type QueueInsertInput, type QueueRow, type QueueStats, RETRY_DELAYS_MS, type RateLimitSnapshot as RateLimitComputeSnapshot, type RateLimitConfig, type RateLimitSnapshot$1 as RateLimitSnapshot, type ReleaseClaimEffect, type RetryEntry, type RetryFiredEvent, RoadmapTrackerAdapter, type RunAttemptPhase, type RunningEntry, type ScheduleRetryEffect, type SideEffect, type StallDetectedEvent, type StopEffect, type StreamManifest, StreamRecorder, type SyncMainOptions, type SyncMainResult, type SyncSkipReason, type TickEvent, TokenStore, type TokenTotals, type TriageConfig, type TriageDecision, type TriageSignals, type TriageSkill, type UpdateTokensEffect, WebhookQueue, type WorkerExitEvent, WorkflowLoader, WorkspaceHooks, WorkspaceManager, type WorkspaceManagerOptions, applyEvent, artifactPresenceFromIssue, calculateRetryDelay, canDispatch, computeRateLimitDelay, createBackend, createEmptyState, detectScopeTier, extractHighlights, extractTitlePrefix, getAvailableSlots, getDefaultConfig, getPerStateCount, isEligible, launchTUI, loadPublishedIndex, migrateAgentConfig, reconcile, renderAnalysisComment, renderPRComment, resolveEscalationConfig, resolveOrchestratorId, routeIssue, savePublishedIndex, selectCandidates, sortCandidates, syncMain, triageIssue, validateWorkflowConfig };
1720
+ interface IndexedDoc {
1721
+ sessionId: string;
1722
+ archived: boolean;
1723
+ fileKind: IndexedFileKind;
1724
+ /** Path relative to project root, posix-style. */
1725
+ path: string;
1726
+ mtimeMs: number;
1727
+ body: string;
1728
+ }
1729
+ interface SearchOptions {
1730
+ limit?: number;
1731
+ archivedOnly?: boolean;
1732
+ fileKinds?: IndexedFileKind[];
1733
+ /** Maximum bytes to retain per doc body (defaults 256 KiB; longer bodies are truncated with a marker). */
1734
+ maxBytesPerBody?: number;
1735
+ }
1736
+ /**
1737
+ * Convert a user-typed query string into a safe FTS5 expression.
1738
+ *
1739
+ * If the caller's query already contains explicit FTS5 syntax markers (double
1740
+ * quotes, parens, asterisk, caret, plus, the literal words AND/OR/NOT or a
1741
+ * `column:` selector) it is passed through unchanged so power users keep the
1742
+ * full FTS5 grammar.
1743
+ *
1744
+ * Otherwise each whitespace-separated token is wrapped as an FTS5 phrase so
1745
+ * characters like `-`, `:` and `*` inside the token are treated as content
1746
+ * (not operators), and the tokens are implicitly AND-joined by FTS5.
1747
+ *
1748
+ * Without this, `idx.search('token-aleph')` is parsed by FTS5 as
1749
+ * `token NOT aleph` and fails with `no such column: aleph`.
1750
+ */
1751
+ declare function normalizeFts5Query(query: string): string;
1752
+ /**
1753
+ * Filesystem path of the search-index sqlite file for a given project root.
1754
+ * Stable so consumers (cleanup tools, doctor, gitignore guards) can locate it.
1755
+ */
1756
+ declare function searchIndexPath(projectPath: string): string;
1757
+ declare class SqliteSearchIndex {
1758
+ private readonly db;
1759
+ private readonly upsertStmt;
1760
+ private readonly removeSessionStmt;
1761
+ private readonly totalStmt;
1762
+ constructor(dbPath: string);
1763
+ upsertSessionDoc(doc: IndexedDoc): void;
1764
+ removeSession(sessionId: string): number;
1765
+ /**
1766
+ * Drop all `archived=1` rows. Used by `reindexFromArchive` before a full
1767
+ * re-walk. Live (archived=0) rows are preserved.
1768
+ */
1769
+ resetArchived(): void;
1770
+ /** Total rows currently indexed (across both live and archived). */
1771
+ totalIndexed(): number;
1772
+ /**
1773
+ * Ranked FTS5 query. Returns BM25-sorted matches. The `query` is passed to
1774
+ * FTS5 as-is; FTS5 syntax (phrases with quotes, AND/OR/NOT, `column:term`)
1775
+ * is therefore the user-facing language. Errors from malformed queries
1776
+ * surface as thrown `SqliteError` so the CLI can catch + render them.
1777
+ */
1778
+ search(query: string, opts?: SearchOptions): SessionSearchResult;
1779
+ close(): void;
1780
+ }
1781
+ /** Open (or create) the project's search index. Idempotent. */
1782
+ declare function openSearchIndex(projectPath: string): SqliteSearchIndex;
1783
+ /**
1784
+ * Walk a session/archive directory and upsert one row per existing file_kind.
1785
+ * Used by both the archive hook (`indexArchivedSession`) and `reindexFromArchive`.
1786
+ *
1787
+ * Bodies larger than `maxBytesPerBody` are truncated with a marker so the index
1788
+ * does not bloat on pathological session files.
1789
+ */
1790
+ declare function indexSessionDirectory(idx: SqliteSearchIndex, args: {
1791
+ sessionId: string;
1792
+ sessionDir: string;
1793
+ archived: boolean;
1794
+ projectPath: string;
1795
+ /** Subset of file_kinds to consider (defaults to all). */
1796
+ fileKinds?: IndexedFileKind[];
1797
+ maxBytesPerBody?: number;
1798
+ }): {
1799
+ docsWritten: number;
1800
+ };
1801
+ /**
1802
+ * Drop and rebuild the `archived=1` portion of the index from
1803
+ * `.harness/archive/sessions/<slug-date>/`. Idempotent.
1804
+ *
1805
+ * Each subdirectory is treated as one session whose id is the basename.
1806
+ */
1807
+ declare function reindexFromArchive(projectPath: string, opts?: {
1808
+ fileKinds?: IndexedFileKind[];
1809
+ maxBytesPerBody?: number;
1810
+ }): ReindexStats;
1811
+
1812
+ interface SummarizeContext {
1813
+ /** Path to the archived session directory, e.g. .harness/archive/sessions/foo-2026-05-16. */
1814
+ archiveDir: string;
1815
+ /** Resolved AnalysisProvider — caller skips this step when no provider is available. */
1816
+ provider: AnalysisProvider;
1817
+ /** Optional session summary config, defaults applied when fields are missing. */
1818
+ config?: SessionSummarizationConfig | undefined;
1819
+ /** Optional logger; falls back to console.warn for diagnostics. */
1820
+ logger?: {
1821
+ warn?: (msg: string, meta?: Record<string, unknown>) => void;
1822
+ };
1823
+ /** When true, on provider error a stub `llm-summary.md` is still written. Default true. */
1824
+ writeStubOnError?: boolean;
1825
+ }
1826
+ interface SummarizeResult {
1827
+ summary: SessionSummary;
1828
+ meta: SessionSummaryMeta;
1829
+ filePath: string;
1830
+ }
1831
+ /** Approximate token cap via char count; conservative because tokens average ~4 chars. */
1832
+ declare function truncateForBudget(text: string, inputBudgetTokens: number): string;
1833
+ /** Render the structured summary as the `llm-summary.md` markdown payload. */
1834
+ declare function renderLlmSummaryMarkdown(summary: SessionSummary, meta: SessionSummaryMeta): string;
1835
+ /**
1836
+ * Summarise a single archived session via the provided AnalysisProvider.
1837
+ *
1838
+ * On success: writes `llm-summary.md` into the archive directory and returns
1839
+ * the structured summary + metadata.
1840
+ *
1841
+ * On provider error: optionally writes a stub `llm-summary.md` so callers can
1842
+ * still detect that summarization was attempted, then returns `Err`.
1843
+ *
1844
+ * Empty / missing input corpus is returned as `Err` and never produces a file.
1845
+ */
1846
+ declare function summarizeArchivedSession(ctx: SummarizeContext): Promise<Result<SummarizeResult, Error>>;
1847
+ /** Resolve whether summarization should run for the given config. */
1848
+ declare function isSummaryEnabled(config?: SessionSummarizationConfig): boolean;
1849
+
1850
+ /**
1851
+ * Session archive hook bundle.
1852
+ *
1853
+ * `buildArchiveHooks()` returns an `ArchiveHooks` implementation that wires
1854
+ * `summarizeArchivedSession()` + `indexSessionDirectory()` together so the
1855
+ * core `archiveSession()` lifecycle invokes both after a successful move.
1856
+ *
1857
+ * Both steps are individually wrapped in try/catch — failure of either does
1858
+ * not propagate up the call stack. Spec: §"Risks" treats summary + index
1859
+ * failure as non-fatal.
1860
+ */
1861
+
1862
+ interface BuildArchiveHooksOptions {
1863
+ /** Absolute path to the project root (contains `.harness/`). */
1864
+ projectPath: string;
1865
+ /** Optional AnalysisProvider — summarization is skipped when omitted. */
1866
+ provider?: AnalysisProvider | undefined;
1867
+ /** Optional sessions config slice. */
1868
+ config?: SessionsConfig | undefined;
1869
+ /** Optional logger; falls back to console.warn. */
1870
+ logger?: HookLogger | undefined;
1871
+ }
1872
+ interface HookLogger {
1873
+ warn?: (msg: string, meta?: Record<string, unknown>) => void;
1874
+ }
1875
+ /**
1876
+ * Construct the `ArchiveHooks` impl. Always returns a working hook bundle —
1877
+ * missing provider or disabled config simply skips that step.
1878
+ */
1879
+ declare function buildArchiveHooks(opts: BuildArchiveHooksOptions): ArchiveHooks;
1880
+
1881
+ /**
1882
+ * Wrap a `GatewayEvent` into a platform-agnostic `NotificationEnvelope`.
1883
+ * Used when a sink has `wrap_response: true` in its config. Unknown event
1884
+ * types fall back to a generic title/summary so newly-emitted events do
1885
+ * not require a code change to be deliverable.
1886
+ */
1887
+ declare function wrapAsEnvelope(event: GatewayEvent): NotificationEnvelope;
1888
+
1889
+ /**
1890
+ * Payload passed to `NotificationSink.deliver`. `wrapped` discriminates
1891
+ * which member of the union `payload` is. Sinks branch on `wrapped`
1892
+ * rather than runtime-detecting the shape.
1893
+ */
1894
+ interface NotificationSinkDeliverInput {
1895
+ payload: GatewayEvent | NotificationEnvelope;
1896
+ wrapped: boolean;
1897
+ }
1898
+ /**
1899
+ * Hermes Phase 3 sink contract.
1900
+ *
1901
+ * Sinks subscribe to the orchestrator event bus via `wireNotificationSinks`
1902
+ * and deliver each filtered event to a destination (chat channel, webhook
1903
+ * URL, etc.). Delivery is best-effort: no retry, no persistence.
1904
+ *
1905
+ * Sinks MUST be idempotent w.r.t. their own delivery semantics — the bus
1906
+ * may emit the same logical state transition more than once during testing
1907
+ * or recovery. Sinks should not assume one-shot semantics.
1908
+ */
1909
+ interface NotificationSink {
1910
+ /** Stable id used in config + CLI; lowercase, kebab-case. */
1911
+ readonly id: string;
1912
+ /** Sink kind literal (matches `NotificationSinkKind`). */
1913
+ readonly kind: string;
1914
+ /** One-shot delivery. Returns Ok on 2xx, Err on any other outcome. */
1915
+ deliver(input: NotificationSinkDeliverInput): Promise<NotificationDeliveryResult>;
1916
+ /** Optional teardown hook called on orchestrator stop. */
1917
+ dispose?(): Promise<void>;
1918
+ }
1919
+
1920
+ interface SlackSinkOptions {
1921
+ id: string;
1922
+ webhookUrl: string;
1923
+ fetchImpl?: typeof fetch;
1924
+ timeoutMs?: number;
1925
+ }
1926
+ /**
1927
+ * Slack sink shipped with Hermes Phase 3. Uses incoming-webhook URLs only;
1928
+ * OAuth + bot tokens are intentionally out of scope (spec D3). Sends one
1929
+ * HTTP POST per delivery and never retries — retries are the operator's
1930
+ * call via the Phase 0 webhook fanout if they need durable delivery.
1931
+ */
1932
+ declare class SlackSink implements NotificationSink {
1933
+ readonly kind = "slack";
1934
+ readonly id: string;
1935
+ private readonly webhookUrl;
1936
+ private readonly fetchImpl;
1937
+ private readonly timeoutMs;
1938
+ constructor(opts: SlackSinkOptions);
1939
+ deliver(input: NotificationSinkDeliverInput): Promise<NotificationDeliveryResult>;
1940
+ private renderEnvelope;
1941
+ private renderRawEvent;
1942
+ }
1943
+
1944
+ interface RegistryEntry {
1945
+ config: NotificationSinkConfig;
1946
+ adapter: NotificationSink;
1947
+ }
1948
+ interface FromConfigOptions {
1949
+ env: NodeJS.ProcessEnv;
1950
+ /** Optional per-sink fetch override (testing). */
1951
+ fetchImpl?: typeof fetch;
1952
+ }
1953
+ /**
1954
+ * Surfaced when a sink config refers to an unknown kind, or its env-var
1955
+ * secret cannot be resolved. Carrying the sinkId helps the doctor + CLI
1956
+ * print operator-actionable messages.
1957
+ */
1958
+ declare class SinkConfigError extends Error {
1959
+ readonly sinkId: string;
1960
+ constructor(sinkId: string, message: string);
1961
+ }
1962
+ /**
1963
+ * In-memory registry of configured notification sinks. Built once at
1964
+ * orchestrator startup from `harness.config.json` `notifications.sinks[]`.
1965
+ * Disposed on orchestrator stop.
1966
+ */
1967
+ declare class SinkRegistry {
1968
+ private readonly entries;
1969
+ private constructor();
1970
+ static fromConfig(config: NotificationsConfig, options: FromConfigOptions): SinkRegistry;
1971
+ list(): readonly RegistryEntry[];
1972
+ get(id: string): RegistryEntry | null;
1973
+ ids(): string[];
1974
+ dispose(): Promise<void>;
1975
+ }
1976
+
1977
+ interface WireParams {
1978
+ bus: EventEmitter;
1979
+ registry: SinkRegistry;
1980
+ }
1981
+ declare function wireNotificationSinks({ bus, registry }: WireParams): () => void;
1982
+
1983
+ export { type AgentUpdateEvent, AnalysisArchive, type AnalysisRecord, type ApplyEventResult, type ArtifactPresence, type AttemptStats, BackendRouter, type BackendRouterOptions, type BaseRefFallbackEvent, type BuildArchiveHooksOptions, ClaimManager, type ClaimManagerConfig, type CleanWorkspaceEffect, type CreateTokenInput, type CreateTokenResult, type DispatchEffect, type EmitLogEffect, type EscalateEffect, type ExecFileFn$1 as ExecFileFn, type FromConfigOptions, type Highlight, type HighlightsInfo, type IndexedDoc, InteractionQueue, type LinearGraphQLExtension, LinearGraphQLStub, type LiveSession, MAX_ATTEMPTS, type MigrationResult, MockBackend, type NotificationSink, type NotificationSinkDeliverInput, ORCHESTRATOR_IDENTITY_FILE, Orchestrator, OrchestratorBackendFactory, type OrchestratorBackendFactoryOptions, type OrchestratorContext, type OrchestratorEvent, type OrchestratorState, PRDetector, type PRDetectorLogger, type PendingInteraction, PromptRenderer, type PublishedIndex, type QueueInsertInput, type QueueRow, type QueueStats, RETRY_DELAYS_MS, type RateLimitSnapshot as RateLimitComputeSnapshot, type RateLimitConfig, type RateLimitSnapshot$1 as RateLimitSnapshot, type RegistryEntry, type ReleaseClaimEffect, type RetryEntry, type RetryFiredEvent, RoadmapTrackerAdapter, type RunAttemptPhase, type RunningEntry, type ScheduleRetryEffect, type SearchOptions, type SideEffect, SinkConfigError, SinkRegistry, SlackSink, type SlackSinkOptions, SqliteSearchIndex, type StallDetectedEvent, type StopEffect, type StreamManifest, StreamRecorder, type SummarizeContext, type SummarizeResult, type SyncMainOptions, type SyncMainResult, type SyncSkipReason, type TickEvent, TokenStore, type TokenTotals, type TriageConfig, type TriageDecision, type TriageSignals, type TriageSkill, type UpdateTokensEffect, WebhookQueue, type WorkerExitEvent, WorkflowLoader, WorkspaceHooks, WorkspaceManager, type WorkspaceManagerOptions, applyEvent, artifactPresenceFromIssue, buildArchiveHooks, calculateRetryDelay, canDispatch, computeRateLimitDelay, createBackend, createEmptyState, detectScopeTier, extractHighlights, extractTitlePrefix, getAvailableSlots, getDefaultConfig, getPerStateCount, indexSessionDirectory, isEligible, isSummaryEnabled, launchTUI, loadPublishedIndex, migrateAgentConfig, normalizeFts5Query, openSearchIndex, reconcile, reindexFromArchive, renderAnalysisComment, renderLlmSummaryMarkdown, renderPRComment, resolveEscalationConfig, resolveOrchestratorId, routeIssue, savePublishedIndex, searchIndexPath, selectCandidates, sortCandidates, summarizeArchivedSession, syncMain, triageIssue, truncateForBudget, validateWorkflowConfig, wireNotificationSinks, wrapAsEnvelope };