@harness-engineering/orchestrator 0.8.4 → 0.9.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.
package/dist/index.js CHANGED
@@ -34,13 +34,18 @@ __export(index_exports, {
34
34
  BUILT_IN_TASKS: () => BUILT_IN_TASKS,
35
35
  BackendDefSchema: () => BackendDefSchema,
36
36
  BackendRouter: () => BackendRouter,
37
+ CheckScriptRunner: () => CheckScriptRunner,
37
38
  ClaimManager: () => ClaimManager,
38
39
  GateNotReadyError: () => GateNotReadyError,
39
40
  GateRunError: () => GateRunError,
40
41
  InteractionQueue: () => InteractionQueue,
42
+ LinearGraphQLClient: () => LinearGraphQLClient,
41
43
  LinearGraphQLStub: () => LinearGraphQLStub,
42
44
  LocalModelResolver: () => LocalModelResolver,
45
+ MAINTENANCE_CHECK_MAX_BUFFER: () => MAINTENANCE_CHECK_MAX_BUFFER,
46
+ MAINTENANCE_CHECK_TIMEOUT_MS: () => MAINTENANCE_CHECK_TIMEOUT_MS,
43
47
  MAX_ATTEMPTS: () => MAX_ATTEMPTS,
48
+ MaintenanceReporter: () => MaintenanceReporter,
44
49
  MockBackend: () => MockBackend,
45
50
  ORCHESTRATOR_IDENTITY_FILE: () => ORCHESTRATOR_IDENTITY_FILE,
46
51
  Orchestrator: () => Orchestrator,
@@ -58,6 +63,7 @@ __export(index_exports, {
58
63
  SqliteSearchIndex: () => SqliteSearchIndex,
59
64
  StreamRecorder: () => StreamRecorder,
60
65
  TaskOutputStore: () => TaskOutputStore,
66
+ TaskRunner: () => TaskRunner,
61
67
  TokenStore: () => TokenStore,
62
68
  WebhookQueue: () => WebhookQueue,
63
69
  WorkflowLoader: () => WorkflowLoader,
@@ -68,7 +74,9 @@ __export(index_exports, {
68
74
  buildArchiveHooks: () => buildArchiveHooks,
69
75
  calculateRetryDelay: () => calculateRetryDelay,
70
76
  canDispatch: () => canDispatch,
77
+ classifyCheckExecutionFailure: () => classifyCheckExecutionFailure,
71
78
  computeRateLimitDelay: () => computeRateLimitDelay,
79
+ createAgentDispatcher: () => createAgentDispatcher,
72
80
  createBackend: () => createBackend,
73
81
  createEmptyState: () => createEmptyState,
74
82
  crossFieldRoutingIssues: () => crossFieldRoutingIssues,
@@ -80,22 +88,27 @@ __export(index_exports, {
80
88
  emitProposalApproved: () => emitProposalApproved,
81
89
  emitProposalCreated: () => emitProposalCreated,
82
90
  emitProposalRejected: () => emitProposalRejected,
91
+ explicitFindingsCount: () => explicitFindingsCount,
83
92
  extractHighlights: () => extractHighlights,
84
93
  extractTitlePrefix: () => extractTitlePrefix,
85
94
  getAvailableSlots: () => getAvailableSlots,
86
95
  getDefaultConfig: () => getDefaultConfig,
87
96
  getPerStateCount: () => getPerStateCount,
88
97
  indexSessionDirectory: () => indexSessionDirectory,
98
+ isCheckTimeoutError: () => isCheckTimeoutError,
89
99
  isEligible: () => isEligible,
90
100
  isSummaryEnabled: () => isSummaryEnabled,
91
101
  launchTUI: () => launchTUI,
92
102
  loadPublishedIndex: () => loadPublishedIndex,
103
+ makeBackendResolver: () => makeBackendResolver,
93
104
  migrateAgentConfig: () => migrateAgentConfig,
94
105
  normalizeFts5Query: () => normalizeFts5Query,
106
+ normalizeHarnessCommand: () => normalizeHarnessCommand,
95
107
  normalizeLocalModel: () => normalizeLocalModel,
96
108
  openSearchIndex: () => openSearchIndex,
97
109
  promote: () => promote,
98
110
  reconcile: () => reconcile,
111
+ recoverFindingsCount: () => recoverFindingsCount,
99
112
  reindexFromArchive: () => reindexFromArchive,
100
113
  renderAnalysisComment: () => renderAnalysisComment,
101
114
  renderLlmSummaryMarkdown: () => renderLlmSummaryMarkdown,
@@ -105,9 +118,11 @@ __export(index_exports, {
105
118
  routeIssue: () => routeIssue,
106
119
  routingWarnings: () => routingWarnings,
107
120
  runGate: () => runGate,
121
+ runHarnessCheck: () => runHarnessCheck,
108
122
  savePublishedIndex: () => savePublishedIndex,
109
123
  searchIndexPath: () => searchIndexPath,
110
124
  selectCandidates: () => selectCandidates,
125
+ selectTasks: () => selectTasks,
111
126
  sortCandidates: () => sortCandidates,
112
127
  summarizeArchivedSession: () => summarizeArchivedSession,
113
128
  syncMain: () => syncMain,
@@ -2327,7 +2342,6 @@ var WorkflowLoader = class {
2327
2342
  };
2328
2343
 
2329
2344
  // src/tracker/adapters/roadmap.ts
2330
- var fs8 = __toESM(require("fs/promises"));
2331
2345
  var import_node_crypto2 = require("crypto");
2332
2346
  var import_core = require("@harness-engineering/core");
2333
2347
  var import_types4 = require("@harness-engineering/types");
@@ -2358,8 +2372,7 @@ var RoadmapTrackerAdapter = class {
2358
2372
  async fetchIssuesByStates(stateNames) {
2359
2373
  try {
2360
2374
  if (!this.config.filePath) return (0, import_types4.Err)(new Error("Missing filePath"));
2361
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2362
- const roadmapResult = (0, import_core.parseRoadmap)(content);
2375
+ const roadmapResult = await this.loadRoadmap();
2363
2376
  if (!roadmapResult.ok) return roadmapResult;
2364
2377
  const issues = [];
2365
2378
  for (const milestone of roadmapResult.value.milestones) {
@@ -2390,17 +2403,19 @@ var RoadmapTrackerAdapter = class {
2390
2403
  if (!terminal) {
2391
2404
  return (0, import_types4.Err)(new Error("Tracker config has no terminalStates; cannot mark complete"));
2392
2405
  }
2393
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2394
- const roadmapResult = (0, import_core.parseRoadmap)(content);
2406
+ const store = this.resolveStore();
2407
+ const roadmapResult = await store.load();
2395
2408
  if (!roadmapResult.ok) return roadmapResult;
2396
2409
  const roadmap = roadmapResult.value;
2410
+ const before = structuredClone(roadmap);
2397
2411
  const target = this.findFeatureById(roadmap.milestones, issueId);
2398
2412
  if (!target) return (0, import_types4.Ok)(void 0);
2399
2413
  const normalizedTerminal = this.config.terminalStates.map((s) => s.toLowerCase());
2400
2414
  if (normalizedTerminal.includes(target.status.toLowerCase())) return (0, import_types4.Ok)(void 0);
2401
2415
  const now = (/* @__PURE__ */ new Date()).toISOString();
2402
2416
  (0, import_core.setStatus)(roadmap, target, terminal, now.slice(0, 10));
2403
- await fs8.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2417
+ const persisted = await (0, import_core.applyRoadmapDiff)(store, before, roadmap);
2418
+ if (!persisted.ok) return persisted;
2404
2419
  return (0, import_types4.Ok)(void 0);
2405
2420
  } catch (error) {
2406
2421
  return (0, import_types4.Err)(error instanceof Error ? error : new Error(String(error)));
@@ -2414,10 +2429,11 @@ var RoadmapTrackerAdapter = class {
2414
2429
  async claimIssue(issueId, orchestratorId) {
2415
2430
  try {
2416
2431
  if (!this.config.filePath) return (0, import_types4.Err)(new Error("Missing filePath"));
2417
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2418
- const roadmapResult = (0, import_core.parseRoadmap)(content);
2432
+ const store = this.resolveStore();
2433
+ const roadmapResult = await store.load();
2419
2434
  if (!roadmapResult.ok) return roadmapResult;
2420
2435
  const roadmap = roadmapResult.value;
2436
+ const before = structuredClone(roadmap);
2421
2437
  const target = this.findFeatureById(roadmap.milestones, issueId);
2422
2438
  if (!target) return (0, import_types4.Ok)(void 0);
2423
2439
  if (!(0, import_core.isClaimableBy)(target, orchestratorId)) {
@@ -2429,7 +2445,8 @@ var RoadmapTrackerAdapter = class {
2429
2445
  const now = (/* @__PURE__ */ new Date()).toISOString();
2430
2446
  (0, import_core.claim)(roadmap, target, orchestratorId, now.slice(0, 10));
2431
2447
  target.updatedAt = now;
2432
- await fs8.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2448
+ const persisted = await (0, import_core.applyRoadmapDiff)(store, before, roadmap);
2449
+ if (!persisted.ok) return persisted;
2433
2450
  return (0, import_types4.Ok)(void 0);
2434
2451
  } catch (error) {
2435
2452
  return (0, import_types4.Err)(error instanceof Error ? error : new Error(String(error)));
@@ -2446,10 +2463,11 @@ var RoadmapTrackerAdapter = class {
2446
2463
  if (!activeState) {
2447
2464
  return (0, import_types4.Err)(new Error("Tracker config has no activeStates; cannot release"));
2448
2465
  }
2449
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2450
- const roadmapResult = (0, import_core.parseRoadmap)(content);
2466
+ const store = this.resolveStore();
2467
+ const roadmapResult = await store.load();
2451
2468
  if (!roadmapResult.ok) return roadmapResult;
2452
2469
  const roadmap = roadmapResult.value;
2470
+ const before = structuredClone(roadmap);
2453
2471
  const target = this.findFeatureById(roadmap.milestones, issueId);
2454
2472
  if (!target) return (0, import_types4.Ok)(void 0);
2455
2473
  if (this.config.activeStates.includes(target.status) && target.assignee === null) {
@@ -2458,12 +2476,26 @@ var RoadmapTrackerAdapter = class {
2458
2476
  const now = (/* @__PURE__ */ new Date()).toISOString();
2459
2477
  (0, import_core.setStatus)(roadmap, target, activeState, now.slice(0, 10));
2460
2478
  target.updatedAt = null;
2461
- await fs8.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2479
+ const persisted = await (0, import_core.applyRoadmapDiff)(store, before, roadmap);
2480
+ if (!persisted.ok) return persisted;
2462
2481
  return (0, import_types4.Ok)(void 0);
2463
2482
  } catch (error) {
2464
2483
  return (0, import_types4.Err)(error instanceof Error ? error : new Error(String(error)));
2465
2484
  }
2466
2485
  }
2486
+ /**
2487
+ * Resolve the roadmap store anchored on the configured aggregate file path.
2488
+ * The shard backend is chosen when a sibling `roadmap.d/` exists next to the
2489
+ * file; otherwise the monolith file is read/written whole. Callers guard
2490
+ * `this.config.filePath` before invoking.
2491
+ */
2492
+ resolveStore() {
2493
+ return (0, import_core.resolveRoadmapStoreForFile)({ roadmapPath: this.config.filePath });
2494
+ }
2495
+ /** Load the roadmap via the store (sharded or monolith), returning a Result. */
2496
+ loadRoadmap() {
2497
+ return this.resolveStore().load();
2498
+ }
2467
2499
  findFeatureById(milestones, issueId) {
2468
2500
  for (const milestone of milestones) {
2469
2501
  for (const feature of milestone.features) {
@@ -2480,8 +2512,7 @@ var RoadmapTrackerAdapter = class {
2480
2512
  async fetchIssueStatesByIds(issueIds) {
2481
2513
  try {
2482
2514
  if (!this.config.filePath) return (0, import_types4.Err)(new Error("Missing filePath"));
2483
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2484
- const roadmapResult = (0, import_core.parseRoadmap)(content);
2515
+ const roadmapResult = await this.loadRoadmap();
2485
2516
  if (!roadmapResult.ok) return roadmapResult;
2486
2517
  const issueMap = /* @__PURE__ */ new Map();
2487
2518
  for (const milestone of roadmapResult.value.milestones) {
@@ -2537,6 +2568,69 @@ var RoadmapTrackerAdapter = class {
2537
2568
 
2538
2569
  // src/tracker/extensions/linear.ts
2539
2570
  var import_types5 = require("@harness-engineering/types");
2571
+ var LINEAR_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql";
2572
+ function errorMessage(err) {
2573
+ return err instanceof Error ? err.message : String(err);
2574
+ }
2575
+ var LinearGraphQLClient = class {
2576
+ apiKey;
2577
+ endpoint;
2578
+ fetchFn;
2579
+ constructor(opts) {
2580
+ this.apiKey = opts.apiKey;
2581
+ this.endpoint = opts.endpoint ?? LINEAR_GRAPHQL_ENDPOINT;
2582
+ this.fetchFn = opts.fetchFn ?? globalThis.fetch;
2583
+ }
2584
+ async query(query, variables) {
2585
+ const sent = await this.sendRequest(query, variables);
2586
+ if (!sent.ok) return sent;
2587
+ const res = sent.value;
2588
+ if (!res.ok) {
2589
+ return (0, import_types5.Err)(await this.httpError(res));
2590
+ }
2591
+ const parsed = await this.parseEnvelope(res);
2592
+ if (!parsed.ok) return parsed;
2593
+ const envelope = parsed.value;
2594
+ const graphqlError = envelopeError(envelope);
2595
+ if (graphqlError) return (0, import_types5.Err)(graphqlError);
2596
+ return (0, import_types5.Ok)(envelope.data ?? {});
2597
+ }
2598
+ /** POST the operation to Linear, normalizing transport throws into an `Err`. */
2599
+ async sendRequest(query, variables) {
2600
+ try {
2601
+ const res = await this.fetchFn(this.endpoint, {
2602
+ method: "POST",
2603
+ headers: {
2604
+ Authorization: this.apiKey,
2605
+ "Content-Type": "application/json"
2606
+ },
2607
+ body: JSON.stringify({ query, variables: variables ?? {} })
2608
+ });
2609
+ return (0, import_types5.Ok)(res);
2610
+ } catch (err) {
2611
+ return (0, import_types5.Err)(new Error(`Linear GraphQL request failed: ${errorMessage(err)}`));
2612
+ }
2613
+ }
2614
+ /** Build the error for a non-2xx HTTP response, including a truncated body. */
2615
+ async httpError(res) {
2616
+ const body = await res.text().catch(() => "");
2617
+ const detail = body ? `: ${body.slice(0, 500)}` : "";
2618
+ return new Error(`Linear GraphQL HTTP ${res.status}${detail}`);
2619
+ }
2620
+ /** Parse the JSON envelope, normalizing parse failures into an `Err`. */
2621
+ async parseEnvelope(res) {
2622
+ try {
2623
+ return (0, import_types5.Ok)(await res.json());
2624
+ } catch (err) {
2625
+ return (0, import_types5.Err)(new Error(`Linear GraphQL response was not valid JSON: ${errorMessage(err)}`));
2626
+ }
2627
+ }
2628
+ };
2629
+ function envelopeError(envelope) {
2630
+ if (!envelope.errors || envelope.errors.length === 0) return void 0;
2631
+ const message = envelope.errors.map((e) => e.message ?? "unknown error").join("; ");
2632
+ return new Error(`Linear GraphQL error: ${message}`);
2633
+ }
2540
2634
  var LinearGraphQLStub = class {
2541
2635
  async query(query, _variables) {
2542
2636
  console.log("Linear GraphQL query (stub):", query);
@@ -2545,7 +2639,7 @@ var LinearGraphQLStub = class {
2545
2639
  };
2546
2640
 
2547
2641
  // src/workspace/manager.ts
2548
- var fs9 = __toESM(require("fs/promises"));
2642
+ var fs8 = __toESM(require("fs/promises"));
2549
2643
  var path8 = __toESM(require("path"));
2550
2644
  var import_node_child_process2 = require("child_process");
2551
2645
  var import_node_util2 = require("util");
@@ -2585,7 +2679,7 @@ var WorkspaceManager = class _WorkspaceManager {
2585
2679
  async getRepoRoot() {
2586
2680
  if (this.repoRoot) return this.repoRoot;
2587
2681
  const root = path8.resolve(this.config.root);
2588
- await fs9.mkdir(root, { recursive: true });
2682
+ await fs8.mkdir(root, { recursive: true });
2589
2683
  const stdout = await this.git(["rev-parse", "--show-toplevel"], root);
2590
2684
  this.repoRoot = stdout.trim();
2591
2685
  return this.repoRoot;
@@ -2598,21 +2692,21 @@ var WorkspaceManager = class _WorkspaceManager {
2598
2692
  try {
2599
2693
  const workspacePath = path8.resolve(this.resolvePath(identifier));
2600
2694
  try {
2601
- await fs9.access(path8.join(workspacePath, ".git"));
2695
+ await fs8.access(path8.join(workspacePath, ".git"));
2602
2696
  const repoRoot2 = await this.getRepoRoot();
2603
2697
  try {
2604
2698
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
2605
2699
  } catch {
2606
- await fs9.rm(workspacePath, { recursive: true, force: true });
2700
+ await fs8.rm(workspacePath, { recursive: true, force: true });
2607
2701
  }
2608
2702
  } catch {
2609
2703
  try {
2610
- await fs9.access(workspacePath);
2704
+ await fs8.access(workspacePath);
2611
2705
  const repoRoot2 = await this.getRepoRoot();
2612
2706
  try {
2613
2707
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
2614
2708
  } catch {
2615
- await fs9.rm(workspacePath, { recursive: true, force: true });
2709
+ await fs8.rm(workspacePath, { recursive: true, force: true });
2616
2710
  }
2617
2711
  } catch {
2618
2712
  }
@@ -2665,13 +2759,13 @@ var WorkspaceManager = class _WorkspaceManager {
2665
2759
  }
2666
2760
  const src = path8.join(repoRoot, rel);
2667
2761
  try {
2668
- await fs9.access(src);
2762
+ await fs8.access(src);
2669
2763
  } catch {
2670
2764
  continue;
2671
2765
  }
2672
2766
  const dest = path8.join(workspacePath, rel);
2673
2767
  try {
2674
- await fs9.cp(src, dest, { recursive: true, force: true });
2768
+ await fs8.cp(src, dest, { recursive: true, force: true });
2675
2769
  } catch {
2676
2770
  }
2677
2771
  }
@@ -2747,7 +2841,7 @@ var WorkspaceManager = class _WorkspaceManager {
2747
2841
  async exists(identifier) {
2748
2842
  try {
2749
2843
  const workspacePath = this.resolvePath(identifier);
2750
- await fs9.access(workspacePath);
2844
+ await fs8.access(workspacePath);
2751
2845
  return true;
2752
2846
  } catch {
2753
2847
  return false;
@@ -2762,7 +2856,7 @@ var WorkspaceManager = class _WorkspaceManager {
2762
2856
  try {
2763
2857
  const workspacePath = path8.resolve(this.resolvePath(identifier));
2764
2858
  try {
2765
- await fs9.access(path8.join(workspacePath, ".git"));
2859
+ await fs8.access(path8.join(workspacePath, ".git"));
2766
2860
  } catch {
2767
2861
  return null;
2768
2862
  }
@@ -2873,7 +2967,7 @@ var WorkspaceManager = class _WorkspaceManager {
2873
2967
  const repoRoot = await this.getRepoRoot();
2874
2968
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot);
2875
2969
  } catch {
2876
- await fs9.rm(workspacePath, { recursive: true, force: true });
2970
+ await fs8.rm(workspacePath, { recursive: true, force: true });
2877
2971
  }
2878
2972
  return (0, import_types6.Ok)(void 0);
2879
2973
  } catch (error) {
@@ -3014,8 +3108,8 @@ var PromptRenderer = class {
3014
3108
  // src/orchestrator.ts
3015
3109
  var import_node_events = require("events");
3016
3110
  var path21 = __toESM(require("path"));
3017
- var import_node_crypto16 = require("crypto");
3018
- var import_core14 = require("@harness-engineering/core");
3111
+ var import_node_crypto15 = require("crypto");
3112
+ var import_core15 = require("@harness-engineering/core");
3019
3113
 
3020
3114
  // src/core/stall-detector.ts
3021
3115
  function detectStalledIssues(running, nowMs, stallTimeoutMs) {
@@ -3591,7 +3685,7 @@ var CompletionHandler = class {
3591
3685
  };
3592
3686
 
3593
3687
  // src/orchestrator.ts
3594
- var import_core15 = require("@harness-engineering/core");
3688
+ var import_core16 = require("@harness-engineering/core");
3595
3689
 
3596
3690
  // src/tracker/adapters/github-issues-issue-tracker.ts
3597
3691
  var import_types9 = require("@harness-engineering/types");
@@ -4724,18 +4818,18 @@ var AnthropicBackend = class {
4724
4818
  usage
4725
4819
  };
4726
4820
  } catch (err) {
4727
- const errorMessage = err instanceof Error ? err.message : "Anthropic request failed";
4821
+ const errorMessage2 = err instanceof Error ? err.message : "Anthropic request failed";
4728
4822
  yield {
4729
4823
  type: "error",
4730
4824
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4731
- content: errorMessage,
4825
+ content: errorMessage2,
4732
4826
  sessionId: session.sessionId
4733
4827
  };
4734
4828
  return {
4735
4829
  success: false,
4736
4830
  sessionId: session.sessionId,
4737
4831
  usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
4738
- error: errorMessage
4832
+ error: errorMessage2
4739
4833
  };
4740
4834
  }
4741
4835
  }
@@ -4826,18 +4920,18 @@ var OpenAIBackend = class {
4826
4920
  }
4827
4921
  }
4828
4922
  } catch (err) {
4829
- const errorMessage = err instanceof Error ? err.message : "OpenAI request failed";
4923
+ const errorMessage2 = err instanceof Error ? err.message : "OpenAI request failed";
4830
4924
  yield {
4831
4925
  type: "error",
4832
4926
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4833
- content: errorMessage,
4927
+ content: errorMessage2,
4834
4928
  sessionId: session.sessionId
4835
4929
  };
4836
4930
  return {
4837
4931
  success: false,
4838
4932
  sessionId: session.sessionId,
4839
4933
  usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
4840
- error: errorMessage
4934
+ error: errorMessage2
4841
4935
  };
4842
4936
  }
4843
4937
  const usage = {
@@ -4948,11 +5042,11 @@ var GeminiBackend = class {
4948
5042
  }
4949
5043
  }
4950
5044
  } catch (err) {
4951
- const errorMessage = err instanceof Error ? err.message : "Gemini request failed";
5045
+ const errorMessage2 = err instanceof Error ? err.message : "Gemini request failed";
4952
5046
  yield {
4953
5047
  type: "error",
4954
5048
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4955
- content: errorMessage,
5049
+ content: errorMessage2,
4956
5050
  sessionId: session.sessionId
4957
5051
  };
4958
5052
  return {
@@ -4965,7 +5059,7 @@ var GeminiBackend = class {
4965
5059
  cacheCreationTokens,
4966
5060
  cacheReadTokens
4967
5061
  },
4968
- error: errorMessage
5062
+ error: errorMessage2
4969
5063
  };
4970
5064
  }
4971
5065
  const usage = {
@@ -5091,18 +5185,18 @@ var LocalBackend = class {
5091
5185
  }
5092
5186
  }
5093
5187
  } catch (err) {
5094
- const errorMessage = err instanceof Error ? err.message : "Local backend request failed";
5188
+ const errorMessage2 = err instanceof Error ? err.message : "Local backend request failed";
5095
5189
  yield {
5096
5190
  type: "error",
5097
5191
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5098
- content: errorMessage,
5192
+ content: errorMessage2,
5099
5193
  sessionId: session.sessionId
5100
5194
  };
5101
5195
  return {
5102
5196
  success: false,
5103
5197
  sessionId: session.sessionId,
5104
5198
  usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
5105
- error: errorMessage
5199
+ error: errorMessage2
5106
5200
  };
5107
5201
  }
5108
5202
  const usage = { inputTokens, outputTokens, totalTokens };
@@ -5438,30 +5532,8 @@ var SshBackend = class {
5438
5532
  config;
5439
5533
  spawnImpl;
5440
5534
  constructor(config) {
5441
- if (!config.host || typeof config.host !== "string") {
5442
- throw new Error("SshBackend: `host` is required");
5443
- }
5444
- if (FORBIDDEN_HOST_CHARS.test(config.host) || config.host.startsWith("-")) {
5445
- throw new Error(
5446
- `SshBackend: invalid host '${config.host}' (contains shell metacharacters or starts with '-')`
5447
- );
5448
- }
5449
- if (!config.remoteCommand || typeof config.remoteCommand !== "string") {
5450
- throw new Error("SshBackend: `remoteCommand` is required");
5451
- }
5452
- if (config.user !== void 0 && /[\s;&|`$]/.test(config.user)) {
5453
- throw new Error(`SshBackend: invalid user '${config.user}'`);
5454
- }
5455
- this.config = {
5456
- host: config.host,
5457
- remoteCommand: config.remoteCommand,
5458
- sshBinary: config.sshBinary ?? "ssh",
5459
- sshOptions: config.sshOptions ?? [],
5460
- timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
5461
- ...config.user !== void 0 ? { user: config.user } : {},
5462
- ...config.port !== void 0 ? { port: config.port } : {},
5463
- ...config.identityFile !== void 0 ? { identityFile: config.identityFile } : {}
5464
- };
5535
+ validateSshConfig(config);
5536
+ this.config = normalizeSshConfig(config);
5465
5537
  this.spawnImpl = config.spawnImpl ?? import_node_child_process5.spawn;
5466
5538
  }
5467
5539
  /**
@@ -5613,6 +5685,34 @@ var SshBackend = class {
5613
5685
  });
5614
5686
  }
5615
5687
  };
5688
+ function validateSshConfig(config) {
5689
+ if (!config.host || typeof config.host !== "string") {
5690
+ throw new Error("SshBackend: `host` is required");
5691
+ }
5692
+ if (FORBIDDEN_HOST_CHARS.test(config.host) || config.host.startsWith("-")) {
5693
+ throw new Error(
5694
+ `SshBackend: invalid host '${config.host}' (contains shell metacharacters or starts with '-')`
5695
+ );
5696
+ }
5697
+ if (!config.remoteCommand || typeof config.remoteCommand !== "string") {
5698
+ throw new Error("SshBackend: `remoteCommand` is required");
5699
+ }
5700
+ if (config.user !== void 0 && /[\s;&|`$]/.test(config.user)) {
5701
+ throw new Error(`SshBackend: invalid user '${config.user}'`);
5702
+ }
5703
+ }
5704
+ function normalizeSshConfig(config) {
5705
+ return {
5706
+ host: config.host,
5707
+ remoteCommand: config.remoteCommand,
5708
+ sshBinary: config.sshBinary ?? "ssh",
5709
+ sshOptions: config.sshOptions ?? [],
5710
+ timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
5711
+ ...config.user !== void 0 ? { user: config.user } : {},
5712
+ ...config.port !== void 0 ? { port: config.port } : {},
5713
+ ...config.identityFile !== void 0 ? { identityFile: config.identityFile } : {}
5714
+ };
5715
+ }
5616
5716
  function errResult(sessionId, message) {
5617
5717
  return {
5618
5718
  success: false,
@@ -5719,23 +5819,8 @@ var OciServerlessBackend = class extends ServerlessBackend {
5719
5819
  envSource;
5720
5820
  constructor(config) {
5721
5821
  super();
5722
- if (!config.image || typeof config.image !== "string") {
5723
- throw new Error("OciServerlessBackend: `image` is required");
5724
- }
5725
- if (FORBIDDEN_IMAGE_CHARS.test(config.image) || config.image.startsWith("-")) {
5726
- throw new Error(
5727
- `OciServerlessBackend: invalid image '${config.image}' (contains shell metacharacters or starts with '-')`
5728
- );
5729
- }
5730
- this.config = {
5731
- image: config.image,
5732
- pullPolicy: config.pullPolicy ?? "if-not-present",
5733
- runtime: config.runtime ?? "docker",
5734
- envPassthrough: config.envPassthrough ?? [],
5735
- timeoutMs: config.timeoutMs ?? DEFAULT_OCI_TIMEOUT_MS,
5736
- extraArgs: sanitizeExtraArgs(config.extraArgs),
5737
- ...config.registry !== void 0 ? { registry: config.registry } : {}
5738
- };
5822
+ validateOciImage(config.image);
5823
+ this.config = resolveOciConfig(config);
5739
5824
  this.spawnImpl = config.spawnImpl ?? import_node_child_process6.spawn;
5740
5825
  this.envSource = config.envSource ?? process.env;
5741
5826
  }
@@ -5898,6 +5983,27 @@ var OciServerlessBackend = class extends ServerlessBackend {
5898
5983
  });
5899
5984
  }
5900
5985
  };
5986
+ function validateOciImage(image) {
5987
+ if (!image || typeof image !== "string") {
5988
+ throw new Error("OciServerlessBackend: `image` is required");
5989
+ }
5990
+ if (FORBIDDEN_IMAGE_CHARS.test(image) || image.startsWith("-")) {
5991
+ throw new Error(
5992
+ `OciServerlessBackend: invalid image '${image}' (contains shell metacharacters or starts with '-')`
5993
+ );
5994
+ }
5995
+ }
5996
+ function resolveOciConfig(config) {
5997
+ return {
5998
+ image: config.image,
5999
+ pullPolicy: config.pullPolicy ?? "if-not-present",
6000
+ runtime: config.runtime ?? "docker",
6001
+ envPassthrough: config.envPassthrough ?? [],
6002
+ timeoutMs: config.timeoutMs ?? DEFAULT_OCI_TIMEOUT_MS,
6003
+ extraArgs: sanitizeExtraArgs(config.extraArgs),
6004
+ ...config.registry !== void 0 ? { registry: config.registry } : {}
6005
+ };
6006
+ }
5901
6007
  function sanitizeExtraArgs(extraArgs) {
5902
6008
  if (!extraArgs) return [];
5903
6009
  return extraArgs.filter((arg) => !BLOCKED_DOCKER_FLAGS.some((flag) => arg.startsWith(flag)));
@@ -5969,78 +6075,96 @@ function makeGetModel(model) {
5969
6075
  if (Array.isArray(model) && model.length > 0) return () => model[0] ?? null;
5970
6076
  return () => null;
5971
6077
  }
6078
+ function createClaudeBackend(def, options) {
6079
+ return new ClaudeBackend(def.command ?? "claude", {
6080
+ ...options.cacheMetrics ? { cacheMetrics: options.cacheMetrics } : {}
6081
+ });
6082
+ }
6083
+ function createAnthropicBackend(def) {
6084
+ return new AnthropicBackend({
6085
+ model: def.model,
6086
+ ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
6087
+ });
6088
+ }
6089
+ function createOpenAIBackend(def) {
6090
+ return new OpenAIBackend({
6091
+ model: def.model,
6092
+ ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
6093
+ });
6094
+ }
6095
+ function createGeminiBackend(def) {
6096
+ return new GeminiBackend({
6097
+ model: def.model,
6098
+ ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
6099
+ });
6100
+ }
6101
+ function createLocalBackend(def) {
6102
+ const isArray = Array.isArray(def.model);
6103
+ return new LocalBackend({
6104
+ endpoint: def.endpoint,
6105
+ ...typeof def.model === "string" ? { model: def.model } : {},
6106
+ ...isArray ? { getModel: makeGetModel(def.model) } : {},
6107
+ ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
6108
+ ...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
6109
+ });
6110
+ }
6111
+ function createPiBackend(def) {
6112
+ const isArray = Array.isArray(def.model);
6113
+ return new PiBackend({
6114
+ endpoint: def.endpoint,
6115
+ ...typeof def.model === "string" ? { model: def.model } : {},
6116
+ ...isArray ? { getModel: makeGetModel(def.model) } : {},
6117
+ ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
6118
+ ...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
6119
+ });
6120
+ }
6121
+ function createSshBackend(def) {
6122
+ return new SshBackend({
6123
+ host: def.host,
6124
+ remoteCommand: def.remoteCommand,
6125
+ ...def.user !== void 0 ? { user: def.user } : {},
6126
+ ...def.port !== void 0 ? { port: def.port } : {},
6127
+ ...def.identityFile !== void 0 ? { identityFile: def.identityFile } : {},
6128
+ ...def.sshOptions !== void 0 ? { sshOptions: def.sshOptions } : {},
6129
+ ...def.sshBinary !== void 0 ? { sshBinary: def.sshBinary } : {}
6130
+ });
6131
+ }
6132
+ function createServerlessBackend(def) {
6133
+ switch (def.adapter) {
6134
+ case "oci":
6135
+ return new OciServerlessBackend({
6136
+ image: def.image,
6137
+ ...def.registry !== void 0 ? { registry: def.registry } : {},
6138
+ ...def.pullPolicy !== void 0 ? { pullPolicy: def.pullPolicy } : {},
6139
+ ...def.envPassthrough !== void 0 ? { envPassthrough: def.envPassthrough } : {},
6140
+ ...def.runtime !== void 0 ? { runtime: def.runtime } : {}
6141
+ });
6142
+ default: {
6143
+ const exhaustive = def.adapter;
6144
+ throw new Error(`createBackend: unknown serverless adapter ${JSON.stringify(exhaustive)}`);
6145
+ }
6146
+ }
6147
+ }
5972
6148
  function createBackend(def, options = {}) {
5973
6149
  switch (def.type) {
5974
6150
  case "mock":
5975
6151
  return new MockBackend();
5976
6152
  case "claude":
5977
- return new ClaudeBackend(def.command ?? "claude", {
5978
- ...options.cacheMetrics ? { cacheMetrics: options.cacheMetrics } : {}
5979
- });
6153
+ return createClaudeBackend(def, options);
5980
6154
  case "anthropic":
5981
- return new AnthropicBackend({
5982
- model: def.model,
5983
- ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
5984
- });
6155
+ return createAnthropicBackend(def);
5985
6156
  case "openai":
5986
- return new OpenAIBackend({
5987
- model: def.model,
5988
- ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
5989
- });
6157
+ return createOpenAIBackend(def);
5990
6158
  case "gemini":
5991
- return new GeminiBackend({
5992
- model: def.model,
5993
- ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
5994
- });
5995
- case "local": {
5996
- const isArray = Array.isArray(def.model);
5997
- return new LocalBackend({
5998
- endpoint: def.endpoint,
5999
- ...typeof def.model === "string" ? { model: def.model } : {},
6000
- ...isArray ? { getModel: makeGetModel(def.model) } : {},
6001
- ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
6002
- ...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
6003
- });
6004
- }
6005
- case "pi": {
6006
- const isArray = Array.isArray(def.model);
6007
- return new PiBackend({
6008
- endpoint: def.endpoint,
6009
- ...typeof def.model === "string" ? { model: def.model } : {},
6010
- ...isArray ? { getModel: makeGetModel(def.model) } : {},
6011
- ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
6012
- ...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
6013
- });
6014
- }
6015
- case "ssh": {
6016
- return new SshBackend({
6017
- host: def.host,
6018
- remoteCommand: def.remoteCommand,
6019
- ...def.user !== void 0 ? { user: def.user } : {},
6020
- ...def.port !== void 0 ? { port: def.port } : {},
6021
- ...def.identityFile !== void 0 ? { identityFile: def.identityFile } : {},
6022
- ...def.sshOptions !== void 0 ? { sshOptions: def.sshOptions } : {},
6023
- ...def.sshBinary !== void 0 ? { sshBinary: def.sshBinary } : {}
6024
- });
6025
- }
6026
- case "serverless": {
6027
- switch (def.adapter) {
6028
- case "oci":
6029
- return new OciServerlessBackend({
6030
- image: def.image,
6031
- ...def.registry !== void 0 ? { registry: def.registry } : {},
6032
- ...def.pullPolicy !== void 0 ? { pullPolicy: def.pullPolicy } : {},
6033
- ...def.envPassthrough !== void 0 ? { envPassthrough: def.envPassthrough } : {},
6034
- ...def.runtime !== void 0 ? { runtime: def.runtime } : {}
6035
- });
6036
- default: {
6037
- const exhaustive = def.adapter;
6038
- throw new Error(
6039
- `createBackend: unknown serverless adapter ${JSON.stringify(exhaustive)}`
6040
- );
6041
- }
6042
- }
6043
- }
6159
+ return createGeminiBackend(def);
6160
+ case "local":
6161
+ return createLocalBackend(def);
6162
+ case "pi":
6163
+ return createPiBackend(def);
6164
+ case "ssh":
6165
+ return createSshBackend(def);
6166
+ case "serverless":
6167
+ return createServerlessBackend(def);
6044
6168
  default: {
6045
6169
  const exhaustive = def;
6046
6170
  throw new Error(`createBackend: unknown backend type ${JSON.stringify(exhaustive)}`);
@@ -6511,6 +6635,96 @@ var OrchestratorBackendFactory = class {
6511
6635
  }
6512
6636
  };
6513
6637
 
6638
+ // src/agent/backend-resolver.ts
6639
+ function makeBackendResolver(backends) {
6640
+ return (backendName) => {
6641
+ const def = backends?.[backendName];
6642
+ return def ? createBackend(def) : null;
6643
+ };
6644
+ }
6645
+
6646
+ // src/maintenance/agent-dispatcher.ts
6647
+ function syntheticIssue(branch, skill) {
6648
+ return {
6649
+ id: `maintenance:${branch}`,
6650
+ identifier: branch,
6651
+ title: `Maintenance: ${skill}`,
6652
+ description: null,
6653
+ priority: null,
6654
+ state: "in_progress",
6655
+ branchName: branch,
6656
+ url: null,
6657
+ labels: ["maintenance"],
6658
+ blockedBy: [],
6659
+ spec: null,
6660
+ plans: [],
6661
+ createdAt: null,
6662
+ updatedAt: null,
6663
+ externalId: null
6664
+ };
6665
+ }
6666
+ function buildPrompt(skill, promptContext) {
6667
+ const instruction = `Run the \`${skill}\` skill to detect and fix issues in this repository. Apply the fixes and commit each logical change. If there is nothing to fix, make no commits.`;
6668
+ return promptContext ? `${promptContext}
6669
+
6670
+ ${instruction}` : instruction;
6671
+ }
6672
+ function headOf(git2, cwd) {
6673
+ try {
6674
+ return git2(["rev-parse", "HEAD"], cwd) || null;
6675
+ } catch {
6676
+ return null;
6677
+ }
6678
+ }
6679
+ function createAgentDispatcher(deps) {
6680
+ const maxTurns = deps.maxTurns ?? 10;
6681
+ const dispatch = async (skill, branch, backendName, cwd, options) => {
6682
+ const backend = deps.resolveBackend(backendName);
6683
+ if (!backend) {
6684
+ deps.logger.warn(`Maintenance agent dispatch skipped: unknown backend "${backendName}"`, {
6685
+ skill,
6686
+ branch
6687
+ });
6688
+ return { producedCommits: false, fixed: 0 };
6689
+ }
6690
+ const before = headOf(deps.git, cwd);
6691
+ const runner = new AgentRunner(backend, { maxTurns });
6692
+ const session = runner.runSession(
6693
+ syntheticIssue(branch, skill),
6694
+ cwd,
6695
+ buildPrompt(skill, options?.promptContext)
6696
+ );
6697
+ let step = await session.next();
6698
+ while (!step.done) step = await session.next();
6699
+ const after = headOf(deps.git, cwd);
6700
+ let fixed = 0;
6701
+ if (after && after !== before) {
6702
+ if (before) {
6703
+ try {
6704
+ fixed = parseInt(deps.git(["rev-list", "--count", `${before}..${after}`], cwd), 10) || 0;
6705
+ } catch {
6706
+ fixed = 1;
6707
+ }
6708
+ } else {
6709
+ fixed = 1;
6710
+ }
6711
+ }
6712
+ const producedCommits = fixed > 0;
6713
+ deps.logger.info("Maintenance agent dispatch complete", {
6714
+ skill,
6715
+ branch,
6716
+ backendName,
6717
+ producedCommits,
6718
+ fixed
6719
+ });
6720
+ return { producedCommits, fixed };
6721
+ };
6722
+ return { dispatch };
6723
+ }
6724
+
6725
+ // src/orchestrator.ts
6726
+ var import_node_child_process13 = require("child_process");
6727
+
6514
6728
  // src/agent/intelligence-factory.ts
6515
6729
  var import_intelligence3 = require("@harness-engineering/intelligence");
6516
6730
  var import_graph = require("@harness-engineering/graph");
@@ -7012,7 +7226,7 @@ function handleV1InteractionsResolveRoute(req, res, queue) {
7012
7226
 
7013
7227
  // src/server/routes/plans.ts
7014
7228
  var import_zod5 = require("zod");
7015
- var fs10 = __toESM(require("fs/promises"));
7229
+ var fs9 = __toESM(require("fs/promises"));
7016
7230
  var path11 = __toESM(require("path"));
7017
7231
  var PlanWriteSchema = import_zod5.z.object({
7018
7232
  filename: import_zod5.z.string().min(1),
@@ -7041,9 +7255,9 @@ function handlePlansRoute(req, res, plansDir) {
7041
7255
  );
7042
7256
  return;
7043
7257
  }
7044
- await fs10.mkdir(plansDir, { recursive: true });
7258
+ await fs9.mkdir(plansDir, { recursive: true });
7045
7259
  const filePath = path11.join(plansDir, basename3);
7046
- await fs10.writeFile(filePath, parsed.content, "utf-8");
7260
+ await fs9.writeFile(filePath, parsed.content, "utf-8");
7047
7261
  res.writeHead(201, { "Content-Type": "application/json" });
7048
7262
  res.end(JSON.stringify({ ok: true, filename: basename3 }));
7049
7263
  } catch {
@@ -7418,7 +7632,6 @@ function handleAnalyzeRoute(req, res, pipeline) {
7418
7632
  }
7419
7633
 
7420
7634
  // src/server/routes/roadmap-actions.ts
7421
- var fs11 = __toESM(require("fs/promises"));
7422
7635
  var path12 = __toESM(require("path"));
7423
7636
  var import_core7 = require("@harness-engineering/core");
7424
7637
  var import_zod8 = require("zod");
@@ -7500,13 +7713,14 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
7500
7713
  sendJSON2(res, 400, { error: "Title must not contain newlines or markdown headings" });
7501
7714
  return;
7502
7715
  }
7503
- const content = await fs11.readFile(roadmapPath, "utf-8");
7504
- const roadmapResult = (0, import_core7.parseRoadmap)(content);
7505
- if (!roadmapResult.ok) {
7716
+ const store = (0, import_core7.resolveRoadmapStoreForFile)({ roadmapPath });
7717
+ const loaded = await store.load();
7718
+ if (!loaded.ok) {
7506
7719
  sendJSON2(res, 500, { error: "Failed to parse roadmap file" });
7507
7720
  return;
7508
7721
  }
7509
- const roadmap = roadmapResult.value;
7722
+ const roadmap = loaded.value;
7723
+ const before = structuredClone(roadmap);
7510
7724
  let backlog = roadmap.milestones.find((m) => m.isBacklog);
7511
7725
  if (!backlog) {
7512
7726
  backlog = { name: "Backlog", isBacklog: true, features: [] };
@@ -7529,10 +7743,11 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
7529
7743
  updatedAt: null
7530
7744
  });
7531
7745
  roadmap.frontmatter.lastManualEdit = (/* @__PURE__ */ new Date()).toISOString();
7532
- const tmpPath = roadmapPath + ".tmp";
7533
- const serialized = (0, import_core7.serializeRoadmap)(roadmap);
7534
- await fs11.writeFile(tmpPath, serialized, "utf-8");
7535
- await fs11.rename(tmpPath, roadmapPath);
7746
+ const persisted = await (0, import_core7.applyRoadmapDiff)(store, before, roadmap);
7747
+ if (!persisted.ok) {
7748
+ sendJSON2(res, 500, { error: persisted.error.message });
7749
+ return;
7750
+ }
7536
7751
  sendJSON2(res, 201, { ok: true, featureName: parsed.title });
7537
7752
  } catch (err) {
7538
7753
  const msg = err instanceof Error ? err.message : "Failed to append to roadmap";
@@ -7798,7 +8013,6 @@ function handleV1JobsMaintenanceRoute(req, res, deps) {
7798
8013
  }
7799
8014
 
7800
8015
  // src/server/routes/v1/events-sse.ts
7801
- var import_node_crypto8 = require("crypto");
7802
8016
  var SSE_TOPICS = [
7803
8017
  "state_change",
7804
8018
  "agent_event",
@@ -7814,8 +8028,55 @@ var SSE_TOPICS = [
7814
8028
  "webhook.subscription.deleted"
7815
8029
  ];
7816
8030
  var HEARTBEAT_MS = 15e3;
7817
- function newEventId() {
7818
- return `evt_${(0, import_node_crypto8.randomBytes)(8).toString("hex")}`;
8031
+ var DEFAULT_REPLAY_BUFFER = 1024;
8032
+ var SseEventLog = class {
8033
+ seq = 0;
8034
+ buffer = [];
8035
+ subscribers = /* @__PURE__ */ new Set();
8036
+ constructor(bus, cap = DEFAULT_REPLAY_BUFFER) {
8037
+ this.cap = cap;
8038
+ for (const topic of SSE_TOPICS) {
8039
+ bus.on(topic, (data) => this.record(topic, data));
8040
+ }
8041
+ }
8042
+ cap;
8043
+ record(topic, data) {
8044
+ const event = { id: ++this.seq, topic, data };
8045
+ this.buffer.push(event);
8046
+ if (this.buffer.length > this.cap) this.buffer.shift();
8047
+ for (const fn of this.subscribers) fn(event);
8048
+ }
8049
+ /** Highest assigned id (0 when nothing has been recorded yet). */
8050
+ currentSeq() {
8051
+ return this.seq;
8052
+ }
8053
+ /** Buffered events strictly newer than `lastId`, oldest-first. */
8054
+ replayFrom(lastId) {
8055
+ return this.buffer.filter((e) => e.id > lastId);
8056
+ }
8057
+ /** Register a live listener; returns an unsubscribe function. */
8058
+ subscribe(fn) {
8059
+ this.subscribers.add(fn);
8060
+ return () => {
8061
+ this.subscribers.delete(fn);
8062
+ };
8063
+ }
8064
+ };
8065
+ var logsByBus = /* @__PURE__ */ new WeakMap();
8066
+ function getSseEventLog(bus) {
8067
+ let log = logsByBus.get(bus);
8068
+ if (!log) {
8069
+ log = new SseEventLog(bus);
8070
+ logsByBus.set(bus, log);
8071
+ }
8072
+ return log;
8073
+ }
8074
+ function parseLastEventId(req) {
8075
+ const raw = req.headers["last-event-id"];
8076
+ const value = Array.isArray(raw) ? raw[0] : raw;
8077
+ if (value === void 0) return null;
8078
+ const n = Number(value);
8079
+ return Number.isInteger(n) && n >= 0 ? n : null;
7819
8080
  }
7820
8081
  function handleV1EventsSseRoute(req, res, bus) {
7821
8082
  if (req.method !== "GET" || req.url !== "/api/v1/events") return false;
@@ -7827,22 +8088,28 @@ function handleV1EventsSseRoute(req, res, bus) {
7827
8088
  res.write(`: harness gateway SSE \u2014 connected at ${(/* @__PURE__ */ new Date()).toISOString()}
7828
8089
 
7829
8090
  `);
7830
- const listeners = [];
7831
- for (const topic of SSE_TOPICS) {
7832
- const fn = (data) => {
7833
- try {
7834
- const frame = `event: ${topic}
7835
- data: ${JSON.stringify(data)}
7836
- id: ${newEventId()}
8091
+ const log = getSseEventLog(bus);
8092
+ const send = (e) => {
8093
+ try {
8094
+ res.write(`event: ${e.topic}
8095
+ data: ${JSON.stringify(e.data)}
8096
+ id: ${e.id}
7837
8097
 
7838
- `;
7839
- res.write(frame);
7840
- } catch {
7841
- }
7842
- };
7843
- bus.on(topic, fn);
7844
- listeners.push({ topic, fn });
8098
+ `);
8099
+ } catch {
8100
+ }
8101
+ };
8102
+ const lastId = parseLastEventId(req);
8103
+ let replayedThrough = lastId ?? log.currentSeq();
8104
+ if (lastId !== null) {
8105
+ for (const e of log.replayFrom(lastId)) {
8106
+ send(e);
8107
+ replayedThrough = e.id;
8108
+ }
7845
8109
  }
8110
+ const unsubscribe = log.subscribe((e) => {
8111
+ if (e.id > replayedThrough) send(e);
8112
+ });
7846
8113
  const heartbeat = setInterval(() => {
7847
8114
  try {
7848
8115
  res.write(": heartbeat\n\n");
@@ -7852,7 +8119,7 @@ id: ${newEventId()}
7852
8119
  heartbeat.unref();
7853
8120
  const cleanup = () => {
7854
8121
  clearInterval(heartbeat);
7855
- for (const { topic, fn } of listeners) bus.removeListener(topic, fn);
8122
+ unsubscribe();
7856
8123
  };
7857
8124
  res.on("close", cleanup);
7858
8125
  res.on("finish", cleanup);
@@ -8161,7 +8428,7 @@ async function runGate(projectPath, proposalId) {
8161
8428
  }
8162
8429
 
8163
8430
  // src/proposals/promote.ts
8164
- var fs12 = __toESM(require("fs"));
8431
+ var fs10 = __toESM(require("fs"));
8165
8432
  var path13 = __toESM(require("path"));
8166
8433
  var import_yaml4 = require("yaml");
8167
8434
  var import_core9 = require("@harness-engineering/core");
@@ -8183,7 +8450,7 @@ function skillDir(projectPath, name) {
8183
8450
  }
8184
8451
  function readIfExists(p) {
8185
8452
  try {
8186
- return fs12.readFileSync(p, "utf-8");
8453
+ return fs10.readFileSync(p, "utf-8");
8187
8454
  } catch {
8188
8455
  return null;
8189
8456
  }
@@ -8229,15 +8496,15 @@ function assertGateReady(proposal) {
8229
8496
  }
8230
8497
  async function promoteNewSkill(projectPath, proposal) {
8231
8498
  const target = skillDir(projectPath, proposal.content.name);
8232
- if (fs12.existsSync(target)) {
8499
+ if (fs10.existsSync(target)) {
8233
8500
  throw new PromotionError(
8234
8501
  `a catalog skill already exists at ${target}; use a refinement proposal to update it`
8235
8502
  );
8236
8503
  }
8237
- fs12.mkdirSync(target, { recursive: true });
8504
+ fs10.mkdirSync(target, { recursive: true });
8238
8505
  const yamlOut = injectProvenanceIntoYaml(proposal.content.skillYaml ?? "", proposal.id);
8239
- fs12.writeFileSync(path13.join(target, "skill.yaml"), yamlOut);
8240
- fs12.writeFileSync(path13.join(target, "SKILL.md"), proposal.content.skillMd ?? "");
8506
+ fs10.writeFileSync(path13.join(target, "skill.yaml"), yamlOut);
8507
+ fs10.writeFileSync(path13.join(target, "SKILL.md"), proposal.content.skillMd ?? "");
8241
8508
  return { skillPath: target };
8242
8509
  }
8243
8510
  async function promoteRefinement(projectPath, proposal) {
@@ -8245,7 +8512,7 @@ async function promoteRefinement(projectPath, proposal) {
8245
8512
  throw new PromotionError("refinement proposal is missing targetSkill");
8246
8513
  }
8247
8514
  const target = skillDir(projectPath, proposal.targetSkill);
8248
- if (!fs12.existsSync(target)) {
8515
+ if (!fs10.existsSync(target)) {
8249
8516
  throw new PromotionError(
8250
8517
  `target skill ${proposal.targetSkill} does not exist at ${target}; cannot refine`
8251
8518
  );
@@ -8258,7 +8525,7 @@ async function promoteRefinement(projectPath, proposal) {
8258
8525
  "no metadata changes detected; check that the reviewer applied the proposed diff before approving"
8259
8526
  );
8260
8527
  }
8261
- fs12.writeFileSync(yamlPath, after);
8528
+ fs10.writeFileSync(yamlPath, after);
8262
8529
  return { skillPath: target };
8263
8530
  }
8264
8531
  async function promote(projectPath, proposalId, decidedBy) {
@@ -8696,7 +8963,7 @@ function handleV1RoutingRoute(req, res, deps) {
8696
8963
  }
8697
8964
 
8698
8965
  // src/server/routes/sessions.ts
8699
- var fs13 = __toESM(require("fs/promises"));
8966
+ var fs11 = __toESM(require("fs/promises"));
8700
8967
  var path14 = __toESM(require("path"));
8701
8968
  var import_zod15 = require("zod");
8702
8969
  var SessionCreateSchema = import_zod15.z.object({
@@ -8717,12 +8984,12 @@ function extractSessionId(url) {
8717
8984
  }
8718
8985
  async function handleList2(res, sessionsDir) {
8719
8986
  try {
8720
- const entries = await fs13.readdir(sessionsDir, { withFileTypes: true });
8987
+ const entries = await fs11.readdir(sessionsDir, { withFileTypes: true });
8721
8988
  const sessions = [];
8722
8989
  for (const entry of entries) {
8723
8990
  if (!entry.isDirectory()) continue;
8724
8991
  try {
8725
- const content = await fs13.readFile(
8992
+ const content = await fs11.readFile(
8726
8993
  path14.join(sessionsDir, entry.name, "session.json"),
8727
8994
  "utf-8"
8728
8995
  );
@@ -8748,7 +9015,7 @@ async function handleGet2(res, id, sessionsDir) {
8748
9015
  return;
8749
9016
  }
8750
9017
  try {
8751
- const content = await fs13.readFile(path14.join(sessionsDir, id, "session.json"), "utf-8");
9018
+ const content = await fs11.readFile(path14.join(sessionsDir, id, "session.json"), "utf-8");
8752
9019
  jsonResponse(res, 200, JSON.parse(content));
8753
9020
  } catch (err) {
8754
9021
  if (err.code === "ENOENT") {
@@ -8772,8 +9039,8 @@ async function handleCreate(req, res, sessionsDir) {
8772
9039
  return;
8773
9040
  }
8774
9041
  const sessionDir = path14.join(sessionsDir, session.sessionId);
8775
- await fs13.mkdir(sessionDir, { recursive: true });
8776
- await fs13.writeFile(path14.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
9042
+ await fs11.mkdir(sessionDir, { recursive: true });
9043
+ await fs11.writeFile(path14.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
8777
9044
  jsonResponse(res, 200, { ok: true });
8778
9045
  } catch {
8779
9046
  jsonResponse(res, 500, { error: "Failed to save session" });
@@ -8789,8 +9056,8 @@ async function handleUpdate(req, res, url, sessionsDir) {
8789
9056
  const body = await readBody(req);
8790
9057
  const updates = import_zod15.z.record(import_zod15.z.unknown()).parse(JSON.parse(body));
8791
9058
  const sessionFilePath = path14.join(sessionsDir, id, "session.json");
8792
- const current = JSON.parse(await fs13.readFile(sessionFilePath, "utf-8"));
8793
- await fs13.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
9059
+ const current = JSON.parse(await fs11.readFile(sessionFilePath, "utf-8"));
9060
+ await fs11.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
8794
9061
  jsonResponse(res, 200, { ok: true });
8795
9062
  } catch {
8796
9063
  jsonResponse(res, 500, { error: "Failed to update session" });
@@ -8803,7 +9070,7 @@ async function handleDelete(res, url, sessionsDir) {
8803
9070
  jsonResponse(res, 400, { error: "Missing or invalid sessionId" });
8804
9071
  return;
8805
9072
  }
8806
- await fs13.rm(path14.join(sessionsDir, id), { recursive: true, force: true });
9073
+ await fs11.rm(path14.join(sessionsDir, id), { recursive: true, force: true });
8807
9074
  jsonResponse(res, 200, { ok: true });
8808
9075
  } catch {
8809
9076
  jsonResponse(res, 500, { error: "Failed to delete session" });
@@ -9047,7 +9314,7 @@ function handleLocalModelsRoute(req, res, getStatuses) {
9047
9314
  }
9048
9315
 
9049
9316
  // src/server/static.ts
9050
- var fs14 = __toESM(require("fs"));
9317
+ var fs12 = __toESM(require("fs"));
9051
9318
  var path15 = __toESM(require("path"));
9052
9319
  var MIME_TYPES = {
9053
9320
  ".html": "text/html; charset=utf-8",
@@ -9077,11 +9344,11 @@ function handleStaticFile(req, res, dashboardDir) {
9077
9344
  if (!resolved.startsWith(path15.resolve(dashboardDir))) {
9078
9345
  return serveFile(path15.join(dashboardDir, "index.html"), res);
9079
9346
  }
9080
- if (fs14.existsSync(resolved) && fs14.statSync(resolved).isFile()) {
9347
+ if (fs12.existsSync(resolved) && fs12.statSync(resolved).isFile()) {
9081
9348
  return serveFile(resolved, res);
9082
9349
  }
9083
9350
  const indexPath = path15.join(dashboardDir, "index.html");
9084
- if (fs14.existsSync(indexPath)) {
9351
+ if (fs12.existsSync(indexPath)) {
9085
9352
  return serveFile(indexPath, res);
9086
9353
  }
9087
9354
  return false;
@@ -9090,7 +9357,7 @@ function serveFile(filePath, res) {
9090
9357
  const ext = path15.extname(filePath).toLowerCase();
9091
9358
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
9092
9359
  try {
9093
- const content = fs14.readFileSync(filePath);
9360
+ const content = fs12.readFileSync(filePath);
9094
9361
  res.writeHead(200, { "Content-Type": contentType });
9095
9362
  res.end(content);
9096
9363
  return true;
@@ -9100,7 +9367,7 @@ function serveFile(filePath, res) {
9100
9367
  }
9101
9368
 
9102
9369
  // src/server/plan-watcher.ts
9103
- var fs15 = __toESM(require("fs"));
9370
+ var fs13 = __toESM(require("fs"));
9104
9371
  var path16 = __toESM(require("path"));
9105
9372
  var PlanWatcher = class {
9106
9373
  plansDir;
@@ -9115,11 +9382,11 @@ var PlanWatcher = class {
9115
9382
  * Creates the directory if it does not exist.
9116
9383
  */
9117
9384
  start() {
9118
- fs15.mkdirSync(this.plansDir, { recursive: true });
9119
- this.watcher = fs15.watch(this.plansDir, (eventType, filename) => {
9385
+ fs13.mkdirSync(this.plansDir, { recursive: true });
9386
+ this.watcher = fs13.watch(this.plansDir, (eventType, filename) => {
9120
9387
  if (eventType === "rename" && filename && filename.endsWith(".md")) {
9121
9388
  const filePath = path16.join(this.plansDir, filename);
9122
- if (fs15.existsSync(filePath)) {
9389
+ if (fs13.existsSync(filePath)) {
9123
9390
  void this.handleNewPlan(filename);
9124
9391
  }
9125
9392
  }
@@ -9150,7 +9417,7 @@ var PlanWatcher = class {
9150
9417
  };
9151
9418
 
9152
9419
  // src/auth/tokens.ts
9153
- var import_node_crypto9 = require("crypto");
9420
+ var import_node_crypto8 = require("crypto");
9154
9421
  var import_promises = require("fs/promises");
9155
9422
  var import_node_path = require("path");
9156
9423
  var import_bcryptjs = __toESM(require("bcryptjs"));
@@ -9158,10 +9425,10 @@ var import_types26 = require("@harness-engineering/types");
9158
9425
  var BCRYPT_ROUNDS = 12;
9159
9426
  var LEGACY_ENV_ID = "tok_legacy_env";
9160
9427
  function genId() {
9161
- return `tok_${(0, import_node_crypto9.randomBytes)(8).toString("hex")}`;
9428
+ return `tok_${(0, import_node_crypto8.randomBytes)(8).toString("hex")}`;
9162
9429
  }
9163
9430
  function genSecret() {
9164
- return (0, import_node_crypto9.randomBytes)(24).toString("base64url");
9431
+ return (0, import_node_crypto8.randomBytes)(24).toString("base64url");
9165
9432
  }
9166
9433
  function parseToken(raw) {
9167
9434
  const dot = raw.indexOf(".");
@@ -9192,7 +9459,7 @@ var TokenStore = class {
9192
9459
  }
9193
9460
  async persist(records) {
9194
9461
  await (0, import_promises.mkdir)((0, import_node_path.dirname)(this.path), { recursive: true });
9195
- const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${(0, import_node_crypto9.randomBytes)(4).toString("hex")}`;
9462
+ const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${(0, import_node_crypto8.randomBytes)(4).toString("hex")}`;
9196
9463
  await (0, import_promises.writeFile)(tmp, JSON.stringify(records, null, 2), "utf8");
9197
9464
  await (0, import_promises.rename)(tmp, this.path);
9198
9465
  this.cache = records;
@@ -9261,7 +9528,7 @@ var TokenStore = class {
9261
9528
  const a = Buffer.from(presented);
9262
9529
  const b = Buffer.from(envValue);
9263
9530
  if (a.length !== b.length) return null;
9264
- if (!(0, import_node_crypto9.timingSafeEqual)(a, b)) return null;
9531
+ if (!(0, import_node_crypto8.timingSafeEqual)(a, b)) return null;
9265
9532
  return {
9266
9533
  id: LEGACY_ENV_ID,
9267
9534
  name: "legacy-env",
@@ -9444,26 +9711,41 @@ function hasScope(held, required) {
9444
9711
  if (held.includes("admin")) return true;
9445
9712
  return held.includes(required);
9446
9713
  }
9447
- function requiredScopeForRoute(method, path24) {
9448
- const bridgeScope = requiredBridgeScope(method, path24);
9449
- if (bridgeScope) return bridgeScope;
9714
+ function exactScopeForRoute(method, path24) {
9450
9715
  if (path24 === "/api/v1/auth/token" && method === "POST") return "admin";
9451
9716
  if (path24 === "/api/v1/auth/tokens" && method === "GET") return "admin";
9452
9717
  if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path24) && method === "DELETE") return "admin";
9453
9718
  if ((path24 === "/api/state" || path24 === "/api/v1/state") && method === "GET") return "read-status";
9454
- if (path24.startsWith("/api/interactions")) return "resolve-interaction";
9455
- if (path24.startsWith("/api/plans")) return "read-status";
9456
- if (path24.startsWith("/api/analyze") || path24.startsWith("/api/analyses")) return "read-status";
9457
- if (path24.startsWith("/api/roadmap-actions")) return "modify-roadmap";
9458
- if (path24.startsWith("/api/dispatch-actions")) return "trigger-job";
9459
- if (path24.startsWith("/api/local-model") || path24.startsWith("/api/local-models"))
9460
- return "read-status";
9461
- if (path24.startsWith("/api/maintenance")) return "trigger-job";
9462
- if (path24.startsWith("/api/streams")) return "read-status";
9463
- if (path24.startsWith("/api/sessions")) return "read-status";
9464
- if (path24 === "/api/chat" || path24.startsWith("/api/chat-proxy")) return "trigger-job";
9465
9719
  return null;
9466
9720
  }
9721
+ var PREFIX_SCOPES = [
9722
+ ["/api/interactions", "resolve-interaction"],
9723
+ ["/api/plans", "read-status"],
9724
+ ["/api/analyze", "read-status"],
9725
+ ["/api/analyses", "read-status"],
9726
+ ["/api/roadmap-actions", "modify-roadmap"],
9727
+ ["/api/dispatch-actions", "trigger-job"],
9728
+ ["/api/local-model", "read-status"],
9729
+ ["/api/local-models", "read-status"],
9730
+ ["/api/maintenance", "trigger-job"],
9731
+ ["/api/streams", "read-status"],
9732
+ ["/api/sessions", "read-status"],
9733
+ ["/api/chat-proxy", "trigger-job"]
9734
+ ];
9735
+ function prefixScopeForPath(path24) {
9736
+ if (path24 === "/api/chat") return "trigger-job";
9737
+ for (const [prefix, scope] of PREFIX_SCOPES) {
9738
+ if (path24.startsWith(prefix)) return scope;
9739
+ }
9740
+ return null;
9741
+ }
9742
+ function requiredScopeForRoute(method, path24) {
9743
+ const bridgeScope = requiredBridgeScope(method, path24);
9744
+ if (bridgeScope) return bridgeScope;
9745
+ const exactScope = exactScopeForRoute(method, path24);
9746
+ if (exactScope) return exactScope;
9747
+ return prefixScopeForPath(path24);
9748
+ }
9467
9749
 
9468
9750
  // src/server/http.ts
9469
9751
  var RATE_LIMIT = Number(process.env["HARNESS_RATE_LIMIT"]) || 100;
@@ -9878,15 +10160,15 @@ var OrchestratorServer = class {
9878
10160
  };
9879
10161
 
9880
10162
  // src/gateway/webhooks/store.ts
9881
- var import_node_crypto11 = require("crypto");
10163
+ var import_node_crypto10 = require("crypto");
9882
10164
  var import_promises3 = require("fs/promises");
9883
10165
  var import_node_path3 = require("path");
9884
10166
  var import_types28 = require("@harness-engineering/types");
9885
10167
 
9886
10168
  // src/gateway/webhooks/signer.ts
9887
- var import_node_crypto10 = require("crypto");
10169
+ var import_node_crypto9 = require("crypto");
9888
10170
  function sign(secret, body) {
9889
- const hex = (0, import_node_crypto10.createHmac)("sha256", secret).update(body).digest("hex");
10171
+ const hex = (0, import_node_crypto9.createHmac)("sha256", secret).update(body).digest("hex");
9890
10172
  return `sha256=${hex}`;
9891
10173
  }
9892
10174
  function eventMatches(pattern, type) {
@@ -9905,10 +10187,10 @@ function eventMatches(pattern, type) {
9905
10187
 
9906
10188
  // src/gateway/webhooks/store.ts
9907
10189
  function genId2() {
9908
- return `whk_${(0, import_node_crypto11.randomBytes)(8).toString("hex")}`;
10190
+ return `whk_${(0, import_node_crypto10.randomBytes)(8).toString("hex")}`;
9909
10191
  }
9910
10192
  function genSecret2() {
9911
- return (0, import_node_crypto11.randomBytes)(32).toString("base64url");
10193
+ return (0, import_node_crypto10.randomBytes)(32).toString("base64url");
9912
10194
  }
9913
10195
  var WebhookStore = class {
9914
10196
  constructor(path24) {
@@ -9933,7 +10215,7 @@ var WebhookStore = class {
9933
10215
  return this.cache;
9934
10216
  }
9935
10217
  async persist(records) {
9936
- const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${(0, import_node_crypto11.randomBytes)(4).toString("hex")}`;
10218
+ const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${(0, import_node_crypto10.randomBytes)(4).toString("hex")}`;
9937
10219
  try {
9938
10220
  await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(this.path), { recursive: true });
9939
10221
  await (0, import_promises3.writeFile)(tmp, JSON.stringify(records, null, 2), { encoding: "utf8", mode: 384 });
@@ -9977,7 +10259,7 @@ var WebhookStore = class {
9977
10259
  };
9978
10260
 
9979
10261
  // src/gateway/webhooks/delivery.ts
9980
- var import_node_crypto12 = require("crypto");
10262
+ var import_node_crypto11 = require("crypto");
9981
10263
 
9982
10264
  // src/gateway/webhooks/queue.ts
9983
10265
  var import_better_sqlite3 = __toESM(require("better-sqlite3"));
@@ -10187,7 +10469,7 @@ var WebhookDelivery = class {
10187
10469
  enqueue(sub, event) {
10188
10470
  const payload = JSON.stringify(event);
10189
10471
  this.queue.insert({
10190
- id: `dlv_${(0, import_node_crypto12.randomBytes)(8).toString("hex")}`,
10472
+ id: `dlv_${(0, import_node_crypto11.randomBytes)(8).toString("hex")}`,
10191
10473
  subscriptionId: sub.id,
10192
10474
  eventType: event.type,
10193
10475
  payload
@@ -10295,7 +10577,7 @@ var WebhookDelivery = class {
10295
10577
  };
10296
10578
 
10297
10579
  // src/gateway/webhooks/events.ts
10298
- var import_node_crypto13 = require("crypto");
10580
+ var import_node_crypto12 = require("crypto");
10299
10581
  var WEBHOOK_TOPICS = [
10300
10582
  "interaction.created",
10301
10583
  "interaction.resolved",
@@ -10310,8 +10592,8 @@ var WEBHOOK_TOPICS = [
10310
10592
  "proposal.approved",
10311
10593
  "proposal.rejected"
10312
10594
  ];
10313
- function newEventId2() {
10314
- return `evt_${(0, import_node_crypto13.randomBytes)(8).toString("hex")}`;
10595
+ function newEventId() {
10596
+ return `evt_${(0, import_node_crypto12.randomBytes)(8).toString("hex")}`;
10315
10597
  }
10316
10598
  function wireWebhookFanout({ bus, store, delivery }) {
10317
10599
  const handlers = [];
@@ -10322,7 +10604,7 @@ function wireWebhookFanout({ bus, store, delivery }) {
10322
10604
  const subs = await store.listForEvent(eventType);
10323
10605
  if (subs.length === 0) return;
10324
10606
  const event = {
10325
- id: newEventId2(),
10607
+ id: newEventId(),
10326
10608
  type: eventType,
10327
10609
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10328
10610
  data
@@ -10341,7 +10623,7 @@ function wireWebhookFanout({ bus, store, delivery }) {
10341
10623
  }
10342
10624
 
10343
10625
  // src/gateway/telemetry/fanout.ts
10344
- var import_node_crypto14 = require("crypto");
10626
+ var import_node_crypto13 = require("crypto");
10345
10627
  var import_core12 = require("@harness-engineering/core");
10346
10628
  var TOPICS = {
10347
10629
  MAINTENANCE_STARTED: "maintenance:started",
@@ -10364,14 +10646,14 @@ var SPAN_NAME = {
10364
10646
  [TOPICS.DISPATCH_DECISION]: "dispatch_decision",
10365
10647
  [TOPICS.SKILL_INVOCATION]: "skill_invocation"
10366
10648
  };
10367
- function newEventId3() {
10368
- return `evt_${(0, import_node_crypto14.randomBytes)(8).toString("hex")}`;
10649
+ function newEventId2() {
10650
+ return `evt_${(0, import_node_crypto13.randomBytes)(8).toString("hex")}`;
10369
10651
  }
10370
10652
  function newTraceId() {
10371
- return (0, import_node_crypto14.randomBytes)(16).toString("hex");
10653
+ return (0, import_node_crypto13.randomBytes)(16).toString("hex");
10372
10654
  }
10373
10655
  function newSpanId() {
10374
- return (0, import_node_crypto14.randomBytes)(8).toString("hex");
10656
+ return (0, import_node_crypto13.randomBytes)(8).toString("hex");
10375
10657
  }
10376
10658
  function nowNs() {
10377
10659
  return BigInt(Date.now()) * 1000000n;
@@ -10422,79 +10704,102 @@ function buildAttributes(payload, extras = {}) {
10422
10704
  }
10423
10705
  return attrs;
10424
10706
  }
10707
+ function extractIds(payload) {
10708
+ const correlationId = typeof payload["correlationId"] === "string" ? payload["correlationId"] : void 0;
10709
+ const taskId = typeof payload["taskId"] === "string" ? payload["taskId"] : void 0;
10710
+ return { correlationId, taskId };
10711
+ }
10712
+ function resolveActiveRun(registry, correlationId, taskId) {
10713
+ return registry.resolve({
10714
+ ...correlationId !== void 0 ? { correlationId } : {},
10715
+ ...taskId !== void 0 ? { taskId } : {}
10716
+ });
10717
+ }
10718
+ function openMaintenanceSpan(registry, correlationId, taskId) {
10719
+ const traceId = newTraceId();
10720
+ const spanId = newSpanId();
10721
+ const key = correlationId ?? taskId ?? `run_${spanId}`;
10722
+ registry.open(key, { traceId, spanId });
10723
+ return { traceId, spanId };
10724
+ }
10725
+ function closeMaintenanceSpan(registry, topic, correlationId, taskId) {
10726
+ const existing = resolveActiveRun(registry, correlationId, taskId);
10727
+ const traceId = existing?.traceId ?? newTraceId();
10728
+ const spanId = existing?.spanId ?? newSpanId();
10729
+ const statusCode = topic === TOPICS.MAINTENANCE_ERROR ? 2 : 1;
10730
+ const key = correlationId ?? taskId ?? "";
10731
+ if (key) registry.close(key);
10732
+ return { traceId, spanId, statusCode };
10733
+ }
10734
+ function childSpan(registry, correlationId, taskId) {
10735
+ const parent = resolveActiveRun(registry, correlationId, taskId);
10736
+ const plan = {
10737
+ traceId: parent?.traceId ?? newTraceId(),
10738
+ spanId: newSpanId()
10739
+ };
10740
+ if (parent?.spanId !== void 0) plan.parentSpanId = parent.spanId;
10741
+ return plan;
10742
+ }
10743
+ function planSpan(registry, topic, correlationId, taskId) {
10744
+ if (topic === TOPICS.MAINTENANCE_STARTED) {
10745
+ return openMaintenanceSpan(registry, correlationId, taskId);
10746
+ }
10747
+ if (topic === TOPICS.MAINTENANCE_COMPLETED || topic === TOPICS.MAINTENANCE_ERROR) {
10748
+ return closeMaintenanceSpan(registry, topic, correlationId, taskId);
10749
+ }
10750
+ return childSpan(registry, correlationId, taskId);
10751
+ }
10752
+ function buildSpan(topic, plan, payload) {
10753
+ const startNs = nowNs();
10754
+ return {
10755
+ traceId: plan.traceId,
10756
+ spanId: plan.spanId,
10757
+ ...plan.parentSpanId !== void 0 ? { parentSpanId: plan.parentSpanId } : {},
10758
+ name: SPAN_NAME[topic],
10759
+ kind: import_core12.SpanKind.INTERNAL,
10760
+ startTimeNs: startNs,
10761
+ endTimeNs: startNs,
10762
+ attributes: buildAttributes(payload, { "harness.topic": topic }),
10763
+ ...plan.statusCode !== void 0 ? { statusCode: plan.statusCode } : {}
10764
+ };
10765
+ }
10766
+ function buildGatewayEvent(topic, payload, correlationId) {
10767
+ return {
10768
+ id: newEventId2(),
10769
+ type: TELEMETRY_TYPE[topic],
10770
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10771
+ data: payload,
10772
+ ...correlationId !== void 0 ? { correlationId } : {}
10773
+ };
10774
+ }
10775
+ async function enqueueToMatchingSubs(store, webhookDelivery, event) {
10776
+ const subs = await store.listForEvent(event.type);
10777
+ if (subs.length === 0) return;
10778
+ const filtered = subs.filter((sub) => sub.events.some((p) => eventMatches(p, event.type)));
10779
+ for (const sub of filtered) {
10780
+ webhookDelivery.enqueue(sub, event);
10781
+ }
10782
+ }
10783
+ function makeTelemetryHandler(ctx, topic) {
10784
+ return (data) => {
10785
+ const payload = data ?? {};
10786
+ const { correlationId, taskId } = extractIds(payload);
10787
+ const plan = planSpan(ctx.registry, topic, correlationId, taskId);
10788
+ ctx.exporter.push(buildSpan(topic, plan, payload));
10789
+ void enqueueToMatchingSubs(
10790
+ ctx.store,
10791
+ ctx.webhookDelivery,
10792
+ buildGatewayEvent(topic, payload, correlationId)
10793
+ );
10794
+ };
10795
+ }
10425
10796
  function wireTelemetryFanout(params) {
10426
10797
  const { bus, exporter, webhookDelivery, store } = params;
10427
10798
  const registry = new ActiveRunRegistry();
10428
10799
  const handlers = [];
10429
- const enqueueToMatchingSubs = async (event) => {
10430
- const subs = await store.listForEvent(event.type);
10431
- if (subs.length === 0) return;
10432
- const filtered = subs.filter((sub) => sub.events.some((p) => eventMatches(p, event.type)));
10433
- for (const sub of filtered) {
10434
- webhookDelivery.enqueue(sub, event);
10435
- }
10436
- };
10437
- const makeHandler = (topic) => (data) => {
10438
- const payload = data ?? {};
10439
- const correlationId = typeof payload["correlationId"] === "string" ? payload["correlationId"] : void 0;
10440
- const taskId = typeof payload["taskId"] === "string" ? payload["taskId"] : void 0;
10441
- let traceId;
10442
- let spanId;
10443
- let parentSpanId;
10444
- let statusCode;
10445
- if (topic === TOPICS.MAINTENANCE_STARTED) {
10446
- traceId = newTraceId();
10447
- spanId = newSpanId();
10448
- const key = correlationId ?? taskId ?? `run_${spanId}`;
10449
- registry.open(key, { traceId, spanId });
10450
- } else if (topic === TOPICS.MAINTENANCE_COMPLETED || topic === TOPICS.MAINTENANCE_ERROR) {
10451
- const existing = registry.resolve({
10452
- ...correlationId !== void 0 ? { correlationId } : {},
10453
- ...taskId !== void 0 ? { taskId } : {}
10454
- });
10455
- if (existing !== void 0) {
10456
- traceId = existing.traceId;
10457
- spanId = existing.spanId;
10458
- } else {
10459
- traceId = newTraceId();
10460
- spanId = newSpanId();
10461
- }
10462
- statusCode = topic === TOPICS.MAINTENANCE_ERROR ? 2 : 1;
10463
- const key = correlationId ?? taskId ?? "";
10464
- if (key) registry.close(key);
10465
- } else {
10466
- const parent = registry.resolve({
10467
- ...correlationId !== void 0 ? { correlationId } : {},
10468
- ...taskId !== void 0 ? { taskId } : {}
10469
- });
10470
- traceId = parent?.traceId ?? newTraceId();
10471
- spanId = newSpanId();
10472
- parentSpanId = parent?.spanId;
10473
- }
10474
- const startNs = nowNs();
10475
- const span = {
10476
- traceId,
10477
- spanId,
10478
- ...parentSpanId !== void 0 ? { parentSpanId } : {},
10479
- name: SPAN_NAME[topic],
10480
- kind: import_core12.SpanKind.INTERNAL,
10481
- startTimeNs: startNs,
10482
- endTimeNs: startNs,
10483
- attributes: buildAttributes(payload, { "harness.topic": topic }),
10484
- ...statusCode !== void 0 ? { statusCode } : {}
10485
- };
10486
- exporter.push(span);
10487
- const gatewayEvent = {
10488
- id: newEventId3(),
10489
- type: TELEMETRY_TYPE[topic],
10490
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10491
- data: payload,
10492
- ...correlationId !== void 0 ? { correlationId } : {}
10493
- };
10494
- void enqueueToMatchingSubs(gatewayEvent);
10495
- };
10800
+ const ctx = { registry, exporter, webhookDelivery, store };
10496
10801
  for (const topic of Object.values(TOPICS)) {
10497
- const fn = makeHandler(topic);
10802
+ const fn = makeTelemetryHandler(ctx, topic);
10498
10803
  bus.on(topic, fn);
10499
10804
  handlers.push({ topic, fn });
10500
10805
  }
@@ -10679,7 +10984,7 @@ function buildSlackSink(config, options) {
10679
10984
  }
10680
10985
 
10681
10986
  // src/notifications/events.ts
10682
- var import_node_crypto15 = require("crypto");
10987
+ var import_node_crypto14 = require("crypto");
10683
10988
 
10684
10989
  // src/notifications/envelope.ts
10685
10990
  function asObj(data) {
@@ -10810,8 +11115,8 @@ var NOTIFICATION_TOPICS = [
10810
11115
  "proposal.approved",
10811
11116
  "proposal.rejected"
10812
11117
  ];
10813
- function newEventId4() {
10814
- return `evt_${(0, import_node_crypto15.randomBytes)(8).toString("hex")}`;
11118
+ function newEventId3() {
11119
+ return `evt_${(0, import_node_crypto14.randomBytes)(8).toString("hex")}`;
10815
11120
  }
10816
11121
  function dispatchToEntry(bus, entry, event) {
10817
11122
  const eventType = event.type;
@@ -10846,7 +11151,7 @@ function wireNotificationSinks({ bus, registry }) {
10846
11151
  const entries = registry.list();
10847
11152
  if (entries.length === 0) return;
10848
11153
  const event = {
10849
- id: newEventId4(),
11154
+ id: newEventId3(),
10850
11155
  type: eventType,
10851
11156
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10852
11157
  data
@@ -10864,7 +11169,7 @@ function wireNotificationSinks({ bus, registry }) {
10864
11169
  }
10865
11170
 
10866
11171
  // src/orchestrator.ts
10867
- var import_core16 = require("@harness-engineering/core");
11172
+ var import_core17 = require("@harness-engineering/core");
10868
11173
 
10869
11174
  // src/logging/logger.ts
10870
11175
  var StructuredLogger = class {
@@ -10949,6 +11254,37 @@ async function scanWorkspaceConfig(workspacePath) {
10949
11254
  return { exitCode: (0, import_core13.computeScanExitCode)(results), results };
10950
11255
  }
10951
11256
 
11257
+ // src/core/lane-persistence.ts
11258
+ var import_core14 = require("@harness-engineering/core");
11259
+ var import_types29 = require("@harness-engineering/types");
11260
+ var SIGNAL_TO_LANE = {
11261
+ claim: "claimed",
11262
+ dispatch: "in_progress",
11263
+ success: "in_review",
11264
+ failure: "blocked",
11265
+ abandon: "canceled"
11266
+ };
11267
+ function mapOrchestratorLane(signal) {
11268
+ return SIGNAL_TO_LANE[signal];
11269
+ }
11270
+ async function persistLane(projectPath, issueId, signal) {
11271
+ try {
11272
+ const reg = await import_core14.eventSourcing.registerTask(projectPath, issueId, []);
11273
+ if (!reg.ok) return reg;
11274
+ return await import_core14.eventSourcing.transitionLane(projectPath, issueId, mapOrchestratorLane(signal));
11275
+ } catch (err) {
11276
+ return (0, import_types29.Err)(err instanceof Error ? err : new Error(String(err)));
11277
+ }
11278
+ }
11279
+ async function readPersistedLanes(projectPath) {
11280
+ try {
11281
+ const snap = await import_core14.eventSourcing.readSnapshot(projectPath);
11282
+ return snap.ok ? snap.value.lanes : { tasks: {} };
11283
+ } catch {
11284
+ return { tasks: {} };
11285
+ }
11286
+ }
11287
+
10952
11288
  // src/maintenance/task-registry.ts
10953
11289
  var BUILT_IN_TASKS = [
10954
11290
  // --- Mechanical-AI ---
@@ -11012,7 +11348,13 @@ var BUILT_IN_TASKS = [
11012
11348
  description: "Detect and fix cross-check violations",
11013
11349
  schedule: "0 6 * * 1",
11014
11350
  branch: "harness-maint/cross-check-fixes",
11015
- checkCommand: ["validate-cross-check"],
11351
+ // `harness cross-check` is a dedicated read-only CLI subcommand that surfaces
11352
+ // JUST cross-artifact consistency (plan→implementation coverage + staleness),
11353
+ // mirroring the `validate_cross_check` MCP tool's core (`runCrossCheck`)
11354
+ // WITHOUT running the full `harness validate` suite. It prints a parseable
11355
+ // `Cross-check: N issues` line and exits 0 (clean) / 1 (N issues), so the
11356
+ // maintenance runner reports real results instead of an honest `failure`.
11357
+ checkCommand: ["cross-check"],
11016
11358
  fixSkill: "harness-cross-check-fix"
11017
11359
  },
11018
11360
  // --- Pure-AI ---
@@ -11071,7 +11413,10 @@ var BUILT_IN_TASKS = [
11071
11413
  description: "Assess overall project health",
11072
11414
  schedule: "0 6 * * *",
11073
11415
  branch: null,
11074
- checkCommand: ["assess_project"]
11416
+ // `assess_project` is an MCP tool name, not a CLI subcommand. The CLI
11417
+ // composite project-health report is `harness insights` (health, entropy,
11418
+ // decay, attention, impact) — a read-only report that records metrics.
11419
+ checkCommand: ["insights"]
11075
11420
  },
11076
11421
  {
11077
11422
  id: "stale-constraints",
@@ -11079,7 +11424,14 @@ var BUILT_IN_TASKS = [
11079
11424
  description: "Detect stale architectural constraints",
11080
11425
  schedule: "0 2 1 * *",
11081
11426
  branch: null,
11082
- checkCommand: ["detect_stale_constraints"]
11427
+ // `harness stale-constraints` is a dedicated read-only CLI subcommand that
11428
+ // surfaces the `detect_stale_constraints` MCP tool's core in-process. It is
11429
+ // precondition-gated on the knowledge graph: with no graph it emits the
11430
+ // "No knowledge graph found. Run `harness scan` first." signature and exits
11431
+ // non-zero, which the runner classifies as `skipped` (not failure). With a
11432
+ // graph it prints a parseable `Stale constraints: N findings` line and exits
11433
+ // 0 (clean) / 1 (N stale), recorded as report-only metrics.
11434
+ checkCommand: ["stale-constraints"]
11083
11435
  },
11084
11436
  {
11085
11437
  id: "graph-refresh",
@@ -11112,7 +11464,8 @@ var BUILT_IN_TASKS = [
11112
11464
  description: "Clean up stale orchestrator sessions",
11113
11465
  schedule: "0 0 * * *",
11114
11466
  branch: null,
11115
- checkCommand: ["cleanup-sessions"]
11467
+ checkCommand: ["cleanup-sessions"],
11468
+ excludeFromHumanSweep: true
11116
11469
  },
11117
11470
  {
11118
11471
  id: "perf-baselines",
@@ -11120,7 +11473,8 @@ var BUILT_IN_TASKS = [
11120
11473
  description: "Update performance baselines",
11121
11474
  schedule: "0 7 * * *",
11122
11475
  branch: null,
11123
- checkCommand: ["perf", "baselines", "update"]
11476
+ checkCommand: ["perf", "baselines", "update"],
11477
+ excludeFromHumanSweep: true
11124
11478
  },
11125
11479
  {
11126
11480
  id: "main-sync",
@@ -11128,7 +11482,8 @@ var BUILT_IN_TASKS = [
11128
11482
  description: "Fast-forward local default branch from origin",
11129
11483
  schedule: "*/15 * * * *",
11130
11484
  branch: null,
11131
- checkCommand: ["harness", "sync-main", "--json"]
11485
+ checkCommand: ["harness", "sync-main", "--json"],
11486
+ excludeFromHumanSweep: true
11132
11487
  },
11133
11488
  // Hermes Phase 4 — one-shot backfill that stamps `provenance: user-authored`
11134
11489
  // on every existing catalog skill. Schedule is Feb 31 (a date that never
@@ -11141,7 +11496,8 @@ var BUILT_IN_TASKS = [
11141
11496
  description: "Backfill provenance: user-authored on every existing skill (one-shot, idempotent)",
11142
11497
  schedule: "0 0 31 2 *",
11143
11498
  branch: null,
11144
- checkCommand: ["backfill-skill-provenance"]
11499
+ checkCommand: ["backfill-skill-provenance"],
11500
+ excludeFromHumanSweep: true
11145
11501
  }
11146
11502
  ];
11147
11503
 
@@ -11208,6 +11564,17 @@ function cronMatchesNow(expression, now) {
11208
11564
  const daysOfWeek = parseField(dowField, 0, 6);
11209
11565
  return minutes.has(minute) && hours.has(hour) && daysOfMonth.has(dayOfMonth) && months.has(month) && daysOfWeek.has(dayOfWeek);
11210
11566
  }
11567
+ function cronMatchesDate(expression, date) {
11568
+ const fields = expression.trim().split(/\s+/);
11569
+ if (fields.length !== 5) {
11570
+ throw new Error(`Invalid cron expression: expected 5 fields, got ${fields.length}`);
11571
+ }
11572
+ const [, , domField, monthField, dowField] = fields;
11573
+ const daysOfMonth = parseField(domField, 1, 31);
11574
+ const months = parseField(monthField, 1, 12);
11575
+ const daysOfWeek = parseField(dowField, 0, 6);
11576
+ return daysOfMonth.has(date.getDate()) && months.has(date.getMonth() + 1) && daysOfWeek.has(date.getDay());
11577
+ }
11211
11578
 
11212
11579
  // src/maintenance/scheduler.ts
11213
11580
  var MaintenanceScheduler = class {
@@ -11449,15 +11816,15 @@ var MaintenanceScheduler = class {
11449
11816
  };
11450
11817
 
11451
11818
  // src/maintenance/leader-elector.ts
11452
- var import_types29 = require("@harness-engineering/types");
11819
+ var import_types30 = require("@harness-engineering/types");
11453
11820
  var SingleProcessLeaderElector = class {
11454
11821
  async electLeader() {
11455
- return (0, import_types29.Ok)("claimed");
11822
+ return (0, import_types30.Ok)("claimed");
11456
11823
  }
11457
11824
  };
11458
11825
 
11459
11826
  // src/maintenance/reporter.ts
11460
- var fs16 = __toESM(require("fs"));
11827
+ var fs14 = __toESM(require("fs"));
11461
11828
  var path18 = __toESM(require("path"));
11462
11829
  var import_zod17 = require("zod");
11463
11830
  var RunResultSchema = import_zod17.z.object({
@@ -11493,9 +11860,9 @@ var MaintenanceReporter = class {
11493
11860
  */
11494
11861
  async load() {
11495
11862
  try {
11496
- await fs16.promises.mkdir(this.persistDir, { recursive: true });
11863
+ await fs14.promises.mkdir(this.persistDir, { recursive: true });
11497
11864
  const filePath = path18.join(this.persistDir, "history.json");
11498
- const data = await fs16.promises.readFile(filePath, "utf-8");
11865
+ const data = await fs14.promises.readFile(filePath, "utf-8");
11499
11866
  const parsed = import_zod17.z.array(RunResultSchema).safeParse(JSON.parse(data));
11500
11867
  if (parsed.success) {
11501
11868
  this.history = parsed.data.slice(0, MAX_HISTORY);
@@ -11529,9 +11896,9 @@ var MaintenanceReporter = class {
11529
11896
  */
11530
11897
  async persist() {
11531
11898
  try {
11532
- await fs16.promises.mkdir(this.persistDir, { recursive: true });
11899
+ await fs14.promises.mkdir(this.persistDir, { recursive: true });
11533
11900
  const filePath = path18.join(this.persistDir, "history.json");
11534
- await fs16.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
11901
+ await fs14.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
11535
11902
  } catch (err) {
11536
11903
  this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
11537
11904
  }
@@ -11571,20 +11938,20 @@ var TaskRunner = class {
11571
11938
  * @param origin - Hermes Phase 2 trigger-source tag; defaults to `'cron'`
11572
11939
  * when called from the scheduler path.
11573
11940
  */
11574
- async run(task, origin = "cron") {
11941
+ async run(task, origin = "cron", mode = "fix") {
11575
11942
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
11576
11943
  let result;
11577
11944
  let captured;
11578
11945
  try {
11579
11946
  switch (task.type) {
11580
11947
  case "mechanical-ai": {
11581
- const out = await this.runMechanicalAI(task, startedAt);
11948
+ const out = await this.runMechanicalAI(task, startedAt, mode);
11582
11949
  result = out.result;
11583
11950
  captured = out.captured;
11584
11951
  break;
11585
11952
  }
11586
11953
  case "pure-ai":
11587
- result = await this.runPureAI(task, startedAt);
11954
+ result = await this.runPureAI(task, startedAt, mode);
11588
11955
  break;
11589
11956
  case "report-only": {
11590
11957
  const out = await this.runReportOnly(task, startedAt);
@@ -11593,7 +11960,7 @@ var TaskRunner = class {
11593
11960
  break;
11594
11961
  }
11595
11962
  case "housekeeping": {
11596
- const out = await this.runHousekeeping(task, startedAt);
11963
+ const out = await this.runHousekeeping(task, startedAt, mode);
11597
11964
  result = out.result;
11598
11965
  captured = out.captured;
11599
11966
  break;
@@ -11657,7 +12024,8 @@ var TaskRunner = class {
11657
12024
  findings: r2.findings,
11658
12025
  stdout: r2.output,
11659
12026
  stderr: r2.stderr,
11660
- structured: r2.structured ? r2.structured : null
12027
+ structured: r2.structured ? r2.structured : null,
12028
+ executionFailed: false
11661
12029
  };
11662
12030
  }
11663
12031
  if (!task.checkCommand || task.checkCommand.length === 0) {
@@ -11669,7 +12037,8 @@ var TaskRunner = class {
11669
12037
  findings: r.findings,
11670
12038
  stdout: r.output,
11671
12039
  stderr: "",
11672
- structured: null
12040
+ structured: null,
12041
+ executionFailed: r.executionFailed ?? false
11673
12042
  };
11674
12043
  }
11675
12044
  /**
@@ -11694,7 +12063,7 @@ var TaskRunner = class {
11694
12063
  * only if fixable findings exist; persist captured stdout/stderr/context
11695
12064
  * via the output store on the way out.
11696
12065
  */
11697
- async runMechanicalAI(task, startedAt) {
12066
+ async runMechanicalAI(task, startedAt, mode = "fix") {
11698
12067
  if (!task.fixSkill) {
11699
12068
  return wrap(this.failureResult(task.id, startedAt, "mechanical-ai task missing fixSkill"));
11700
12069
  }
@@ -11716,6 +12085,27 @@ var TaskRunner = class {
11716
12085
  } catch (err) {
11717
12086
  return wrap(this.failureResult(task.id, startedAt, String(err)));
11718
12087
  }
12088
+ if (check.executionFailed) {
12089
+ const cls = classifyCheckExecutionFailure(`${check.stdout}
12090
+ ${check.stderr}`);
12091
+ if (cls.kind === "unrunnable") {
12092
+ return wrap(
12093
+ this.failureResult(task.id, startedAt, checkExecutionError(check)),
12094
+ captureFromCheck(check)
12095
+ );
12096
+ }
12097
+ if (cls.kind === "precondition") {
12098
+ return wrap(
12099
+ this.skippedResult(task.id, startedAt, cls.reason ?? "precondition not met"),
12100
+ captureFromCheck(check)
12101
+ );
12102
+ }
12103
+ check = {
12104
+ ...check,
12105
+ executionFailed: false,
12106
+ findings: check.findings > 0 ? check.findings : recoverFindingsCount(check.stdout)
12107
+ };
12108
+ }
11719
12109
  const promptContext = await this.composePromptContext(task);
11720
12110
  const baseCaptured = {
11721
12111
  stdout: check.stdout,
@@ -11724,13 +12114,14 @@ var TaskRunner = class {
11724
12114
  ...promptContext ? { context: promptContext } : {}
11725
12115
  };
11726
12116
  const wakeAgentExplicitlyFalse = check.structured !== null && typeof check.structured === "object" && check.structured.wakeAgent === false;
11727
- if (check.findings === 0 || wakeAgentExplicitlyFalse) {
12117
+ if (check.findings === 0 || wakeAgentExplicitlyFalse || mode === "report") {
12118
+ const reportedWithFindings = mode === "report" && check.findings > 0;
11728
12119
  return {
11729
12120
  result: {
11730
12121
  taskId: task.id,
11731
12122
  startedAt,
11732
12123
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
11733
- status: "no-issues",
12124
+ status: reportedWithFindings ? "success" : "no-issues",
11734
12125
  findings: check.findings,
11735
12126
  fixed: 0,
11736
12127
  prUrl: null,
@@ -11800,7 +12191,19 @@ var TaskRunner = class {
11800
12191
  /**
11801
12192
  * Pure-AI: always dispatch agent with configured skill.
11802
12193
  */
11803
- async runPureAI(task, startedAt) {
12194
+ async runPureAI(task, startedAt, mode = "fix") {
12195
+ if (mode === "report") {
12196
+ return {
12197
+ taskId: task.id,
12198
+ startedAt,
12199
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
12200
+ status: "no-issues",
12201
+ findings: 0,
12202
+ fixed: 0,
12203
+ prUrl: null,
12204
+ prUpdated: false
12205
+ };
12206
+ }
11804
12207
  if (!task.fixSkill) {
11805
12208
  return this.failureResult(task.id, startedAt, "pure-ai task missing fixSkill");
11806
12209
  }
@@ -11874,6 +12277,27 @@ var TaskRunner = class {
11874
12277
  return wrap(this.failureResult(task.id, startedAt, String(err)));
11875
12278
  }
11876
12279
  const parsed = parseStatusLine(check.stdout);
12280
+ if (parsed === null && check.executionFailed) {
12281
+ const cls = classifyCheckExecutionFailure(`${check.stdout}
12282
+ ${check.stderr}`);
12283
+ if (cls.kind === "unrunnable") {
12284
+ return wrap(
12285
+ this.failureResult(task.id, startedAt, checkExecutionError(check)),
12286
+ captureFromCheck(check)
12287
+ );
12288
+ }
12289
+ if (cls.kind === "precondition") {
12290
+ return wrap(
12291
+ this.skippedResult(task.id, startedAt, cls.reason ?? "precondition not met"),
12292
+ captureFromCheck(check)
12293
+ );
12294
+ }
12295
+ check = {
12296
+ ...check,
12297
+ executionFailed: false,
12298
+ findings: check.findings > 0 ? check.findings : recoverFindingsCount(check.stdout)
12299
+ };
12300
+ }
11877
12301
  const status = parsed?.status ?? "success";
11878
12302
  const findings = parsed === null ? check.findings : typeof parsed.candidatesFound === "number" ? parsed.candidatesFound : 0;
11879
12303
  const result = {
@@ -11907,7 +12331,20 @@ var TaskRunner = class {
11907
12331
  * Hermes Phase 2: a `checkScript` may replace `checkCommand` for housekeeping
11908
12332
  * tasks; the runner falls through to the same JSON-status parsing path.
11909
12333
  */
11910
- async runHousekeeping(task, startedAt) {
12334
+ async runHousekeeping(task, startedAt, mode = "fix") {
12335
+ if (mode === "report") {
12336
+ return wrap({
12337
+ taskId: task.id,
12338
+ startedAt,
12339
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
12340
+ status: "skipped",
12341
+ findings: 0,
12342
+ fixed: 0,
12343
+ prUrl: null,
12344
+ prUpdated: false,
12345
+ error: "skipped in report mode: housekeeping may mutate and report runs are read-only"
12346
+ });
12347
+ }
11911
12348
  if (!task.checkCommand && !task.checkScript) {
11912
12349
  return wrap(
11913
12350
  this.failureResult(
@@ -11974,46 +12411,133 @@ var TaskRunner = class {
11974
12411
  error
11975
12412
  };
11976
12413
  }
12414
+ /**
12415
+ * A precondition-gated check (e.g. `predict` with <3 snapshots, or a
12416
+ * graph-backed check before `harness scan`). The command is correctly
12417
+ * configured; the repo just lacks the state it needs. Reported as `skipped`
12418
+ * — distinct from a hard `failure` — carrying the refusal line as the reason
12419
+ * (surfaced in the run-report summary column). ADR 0050.
12420
+ */
12421
+ skippedResult(taskId, startedAt, reason) {
12422
+ return {
12423
+ taskId,
12424
+ startedAt,
12425
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
12426
+ status: "skipped",
12427
+ findings: 0,
12428
+ fixed: 0,
12429
+ prUrl: null,
12430
+ prUpdated: false,
12431
+ error: reason
12432
+ };
12433
+ }
11977
12434
  };
11978
12435
  function wrap(result, captured) {
11979
12436
  return captured ? { result, captured } : { result };
11980
12437
  }
12438
+ function checkExecutionError(check) {
12439
+ const detail = (check.stderr || check.stdout || "").trim();
12440
+ return detail ? `check command failed to execute: ${detail}` : "check command failed to execute";
12441
+ }
12442
+ function captureFromCheck(check) {
12443
+ return { stdout: check.stdout, stderr: check.stderr, structured: check.structured };
12444
+ }
12445
+ var PRECONDITION_PATTERNS = [
12446
+ /requires at least \d+ snapshots?/i,
12447
+ /Run ["'`]?harness snapshot/i,
12448
+ /no knowledge graph found/i,
12449
+ /Run ["'`]?harness scan/i,
12450
+ /no graph (?:available|found)/i
12451
+ ];
12452
+ var UNRUNNABLE_PATTERNS = [
12453
+ /unknown command/i,
12454
+ /unknown option/i,
12455
+ /\bENOENT\b/,
12456
+ /command not found/i,
12457
+ /cannot find module/i,
12458
+ // A timed-out check did not complete — it is a hard failure, never a
12459
+ // "ran-no-count" success. The runners synthesize this message on SIGTERM.
12460
+ /timed out/i,
12461
+ /\bETIMEDOUT\b/
12462
+ ];
12463
+ var TIMEOUT_SIGNATURE = /check timed out after \d+\s*ms/i;
12464
+ function explicitFindingsCount(output) {
12465
+ const after = output.match(/(?:findings?|issues?|violations?|errors?)\s*[:=]\s*(\d+)/i);
12466
+ if (after) return parseInt(after[1], 10);
12467
+ const before = output.match(/(\d+)\s+(?:findings?|issues?|violations?|errors?)\b/i);
12468
+ if (before) return parseInt(before[1], 10);
12469
+ return null;
12470
+ }
12471
+ function recoverFindingsCount(output) {
12472
+ return explicitFindingsCount(output) ?? 1;
12473
+ }
12474
+ function firstMeaningfulLine(output) {
12475
+ const line = output.split("\n").map((l) => l.trim()).find(Boolean);
12476
+ if (!line) return "precondition not met";
12477
+ return line.replace(/^[x✗✓!-]\s*/u, "").trim();
12478
+ }
12479
+ var CLASSIFY_HEAD_LINES = 3;
12480
+ function classifyCheckExecutionFailure(output) {
12481
+ const text = (output ?? "").trim();
12482
+ if (text.length === 0) return { kind: "unrunnable" };
12483
+ if (TIMEOUT_SIGNATURE.test(text)) return { kind: "unrunnable" };
12484
+ if (explicitFindingsCount(text) !== null) return { kind: "ran-no-count" };
12485
+ const head = text.split("\n").filter((l) => l.trim()).slice(0, CLASSIFY_HEAD_LINES).join("\n");
12486
+ for (const re of PRECONDITION_PATTERNS) {
12487
+ if (re.test(head)) return { kind: "precondition", reason: firstMeaningfulLine(text) };
12488
+ }
12489
+ for (const re of UNRUNNABLE_PATTERNS) {
12490
+ if (re.test(head)) return { kind: "unrunnable" };
12491
+ }
12492
+ return { kind: "ran-no-count" };
12493
+ }
11981
12494
  function parseStatusLine(output) {
11982
12495
  const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
11983
12496
  for (let i = lines.length - 1; i >= 0; i--) {
11984
- const line = lines[i];
11985
- if (!line || !line.startsWith("{") || !line.endsWith("}")) continue;
11986
- try {
11987
- const obj = JSON.parse(line);
11988
- const s = obj.status;
11989
- if (s === "success" || s === "skipped" || s === "failure" || s === "no-issues") {
11990
- const parsed = { status: s, rawStatus: s };
11991
- if (typeof obj.candidatesFound === "number") {
11992
- parsed.candidatesFound = obj.candidatesFound;
11993
- }
11994
- if (typeof obj.error === "string") {
11995
- parsed.error = obj.error;
11996
- }
11997
- if (typeof obj.reason === "string") {
11998
- parsed.reason = obj.reason;
11999
- }
12000
- if (typeof obj.detail === "string" && !parsed.error) {
12001
- parsed.error = `${parsed.reason ?? "skipped"}: ${obj.detail}`;
12002
- }
12003
- return parsed;
12004
- }
12005
- if (s === "updated" || s === "no-op") {
12006
- return { status: "success", rawStatus: s };
12007
- }
12008
- if (s === "error") {
12009
- const message = typeof obj.message === "string" ? obj.message : "unknown error";
12010
- return { status: "failure", error: message, rawStatus: "error" };
12011
- }
12012
- } catch {
12013
- }
12497
+ const parsed = parseStatusObjectLine(lines[i]);
12498
+ if (parsed) return parsed;
12499
+ }
12500
+ return null;
12501
+ }
12502
+ function parseStatusObjectLine(line) {
12503
+ if (!line || !line.startsWith("{") || !line.endsWith("}")) return null;
12504
+ let obj;
12505
+ try {
12506
+ obj = JSON.parse(line);
12507
+ } catch {
12508
+ return null;
12509
+ }
12510
+ return classifyStatusObject(obj);
12511
+ }
12512
+ var PHASE_STATUSES = /* @__PURE__ */ new Set(["success", "skipped", "failure", "no-issues"]);
12513
+ var SYNC_SUCCESS_STATUSES = /* @__PURE__ */ new Set(["updated", "no-op"]);
12514
+ function classifyStatusObject(obj) {
12515
+ const s = obj.status;
12516
+ if (typeof s !== "string") return null;
12517
+ if (PHASE_STATUSES.has(s)) return buildPhaseStatus(s, obj);
12518
+ if (SYNC_SUCCESS_STATUSES.has(s)) return { status: "success", rawStatus: s };
12519
+ if (s === "error") {
12520
+ const message = typeof obj.message === "string" ? obj.message : "unknown error";
12521
+ return { status: "failure", error: message, rawStatus: "error" };
12014
12522
  }
12015
12523
  return null;
12016
12524
  }
12525
+ function buildPhaseStatus(status, obj) {
12526
+ const parsed = { status, rawStatus: status };
12527
+ if (typeof obj.candidatesFound === "number") {
12528
+ parsed.candidatesFound = obj.candidatesFound;
12529
+ }
12530
+ if (typeof obj.error === "string") {
12531
+ parsed.error = obj.error;
12532
+ }
12533
+ if (typeof obj.reason === "string") {
12534
+ parsed.reason = obj.reason;
12535
+ }
12536
+ if (typeof obj.detail === "string" && !parsed.error) {
12537
+ parsed.error = `${parsed.reason ?? "skipped"}: ${obj.detail}`;
12538
+ }
12539
+ return parsed;
12540
+ }
12017
12541
 
12018
12542
  // src/maintenance/check-script-runner.ts
12019
12543
  var import_node_child_process11 = require("child_process");
@@ -12126,8 +12650,49 @@ function heuristicResult(stdout, stderr, exitedAbnormally) {
12126
12650
  };
12127
12651
  }
12128
12652
 
12653
+ // src/maintenance/check-runner.ts
12654
+ var import_node_child_process12 = require("child_process");
12655
+ var import_node_util4 = require("util");
12656
+ var MAINTENANCE_CHECK_MAX_BUFFER = 64 * 1024 * 1024;
12657
+ var MAINTENANCE_CHECK_TIMEOUT_MS = 3e5;
12658
+ var FINDINGS_RE = /(\d+)\s+(?:finding|issue|violation|error)/i;
12659
+ var nodeExecFileAsync = (0, import_node_util4.promisify)(import_node_child_process12.execFile);
12660
+ function isCheckTimeoutError(e) {
12661
+ return e.killed === true || e.signal === "SIGTERM" || e.code === "ETIMEDOUT";
12662
+ }
12663
+ async function runHarnessCheck(spawn7, cwd, opts = {}) {
12664
+ const execFileAsync2 = opts.execFileAsync ?? nodeExecFileAsync;
12665
+ const timeoutMs = opts.timeoutMs ?? MAINTENANCE_CHECK_TIMEOUT_MS;
12666
+ const maxBuffer = opts.maxBuffer ?? MAINTENANCE_CHECK_MAX_BUFFER;
12667
+ try {
12668
+ const { stdout } = await execFileAsync2(spawn7.file, spawn7.args, {
12669
+ cwd,
12670
+ timeout: timeoutMs,
12671
+ maxBuffer
12672
+ });
12673
+ const text = String(stdout);
12674
+ const m = text.match(FINDINGS_RE);
12675
+ const findings = m ? parseInt(m[1], 10) : 0;
12676
+ return { passed: findings === 0, findings, output: text, executionFailed: false };
12677
+ } catch (err) {
12678
+ const e = err;
12679
+ let output = [e.stdout, e.stderr].map((v) => v == null ? "" : String(v)).filter(Boolean).join("\n");
12680
+ if (isCheckTimeoutError(e)) {
12681
+ const note = `check timed out after ${timeoutMs}ms`;
12682
+ output = output.trim() ? `${output}
12683
+ ${note}` : note;
12684
+ return { passed: false, findings: 0, output, executionFailed: true };
12685
+ }
12686
+ const m = output.match(FINDINGS_RE);
12687
+ if (m) {
12688
+ return { passed: false, findings: parseInt(m[1], 10), output, executionFailed: false };
12689
+ }
12690
+ return { passed: false, findings: 0, output, executionFailed: true };
12691
+ }
12692
+ }
12693
+
12129
12694
  // src/maintenance/output-store.ts
12130
- var fs17 = __toESM(require("fs"));
12695
+ var fs15 = __toESM(require("fs"));
12131
12696
  var path20 = __toESM(require("path"));
12132
12697
  var DEFAULT_RETENTION = {
12133
12698
  runs: 50,
@@ -12168,13 +12733,13 @@ var TaskOutputStore = class {
12168
12733
  async write(taskId, entry, retention) {
12169
12734
  this.ensureSafeTaskId(taskId);
12170
12735
  const dir = this.dirFor(taskId);
12171
- await fs17.promises.mkdir(dir, { recursive: true });
12736
+ await fs15.promises.mkdir(dir, { recursive: true });
12172
12737
  const fileName = `${sanitizeIso(entry.completedAt || (/* @__PURE__ */ new Date()).toISOString())}.json`;
12173
12738
  const filePath = path20.join(dir, fileName);
12174
12739
  const tmpPath = `${filePath}.tmp`;
12175
12740
  const payload = JSON.stringify(entry, null, 2);
12176
- await fs17.promises.writeFile(tmpPath, payload, "utf-8");
12177
- await fs17.promises.rename(tmpPath, filePath);
12741
+ await fs15.promises.writeFile(tmpPath, payload, "utf-8");
12742
+ await fs15.promises.rename(tmpPath, filePath);
12178
12743
  try {
12179
12744
  await this.applyRetention(taskId, retention);
12180
12745
  } catch (err) {
@@ -12225,7 +12790,7 @@ var TaskOutputStore = class {
12225
12790
  }
12226
12791
  async readEntry(filePath) {
12227
12792
  try {
12228
- const buf = await fs17.promises.readFile(filePath, "utf-8");
12793
+ const buf = await fs15.promises.readFile(filePath, "utf-8");
12229
12794
  const parsed = JSON.parse(buf);
12230
12795
  return parsed;
12231
12796
  } catch {
@@ -12247,7 +12812,7 @@ var TaskOutputStore = class {
12247
12812
  const toRemove = /* @__PURE__ */ new Set([...overflow, ...aged]);
12248
12813
  for (const name of toRemove) {
12249
12814
  try {
12250
- await fs17.promises.unlink(path20.join(dir, name));
12815
+ await fs15.promises.unlink(path20.join(dir, name));
12251
12816
  } catch {
12252
12817
  }
12253
12818
  }
@@ -12256,7 +12821,7 @@ var TaskOutputStore = class {
12256
12821
  async function listJsonFilesDescending(dir) {
12257
12822
  let names;
12258
12823
  try {
12259
- names = await fs17.promises.readdir(dir);
12824
+ names = await fs15.promises.readdir(dir);
12260
12825
  } catch {
12261
12826
  return [];
12262
12827
  }
@@ -12365,7 +12930,7 @@ var fallbackLogger3 = {
12365
12930
  };
12366
12931
 
12367
12932
  // src/maintenance/custom-task-validator.ts
12368
- var import_types30 = require("@harness-engineering/types");
12933
+ var import_types31 = require("@harness-engineering/types");
12369
12934
  var ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
12370
12935
  var REQUIRED_FIELDS_BY_TYPE = {
12371
12936
  "mechanical-ai": ["branch", "fixSkill"],
@@ -12375,7 +12940,7 @@ var REQUIRED_FIELDS_BY_TYPE = {
12375
12940
  };
12376
12941
  function validateCustomTasks(customTasks, builtIns, deps = {}) {
12377
12942
  const errors = [];
12378
- if (!customTasks) return (0, import_types30.Ok)(void 0);
12943
+ if (!customTasks) return (0, import_types31.Ok)(void 0);
12379
12944
  const builtInIds = new Set(builtIns.map((t) => t.id));
12380
12945
  const customIds = Object.keys(customTasks);
12381
12946
  const allIds = /* @__PURE__ */ new Set([...builtInIds, ...customIds]);
@@ -12385,7 +12950,7 @@ function validateCustomTasks(customTasks, builtIns, deps = {}) {
12385
12950
  validateOne(id, task, builtInIds, allIds, deps, errors);
12386
12951
  }
12387
12952
  detectCycles(customTasks, builtIns, errors);
12388
- return errors.length === 0 ? (0, import_types30.Ok)(void 0) : (0, import_types30.Err)(errors);
12953
+ return errors.length === 0 ? (0, import_types31.Ok)(void 0) : (0, import_types31.Err)(errors);
12389
12954
  }
12390
12955
  function validateOne(id, task, builtInIds, allIds, deps, errors) {
12391
12956
  const prefix = `customTasks.${id}`;
@@ -12574,6 +13139,11 @@ function deriveSeedPaths(config) {
12574
13139
  const roadmapPath = config.tracker.kind === "roadmap" && config.tracker.filePath ? config.tracker.filePath : "docs/roadmap.md";
12575
13140
  return [".harness/proposals", roadmapPath];
12576
13141
  }
13142
+ function normalizeHarnessCommand(command) {
13143
+ if (command.length === 0) return [];
13144
+ if (command[0] === "harness") return command;
13145
+ return ["harness", ...command];
13146
+ }
12577
13147
  var Orchestrator = class extends import_node_events.EventEmitter {
12578
13148
  state;
12579
13149
  config;
@@ -12700,6 +13270,11 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12700
13270
  abortControllers = /* @__PURE__ */ new Map();
12701
13271
  /** Guards against overlapping ticks when a tick takes longer than the polling interval */
12702
13272
  tickInProgress = false;
13273
+ /** Phase 4 (DLane-5): lanes read back from the durable log on first tick, for
13274
+ * observability only — NOT fed into reconciliation. Empty until the first tick. */
13275
+ persistedLanes = { tasks: {} };
13276
+ /** Ensures the lane read-back diagnostic runs at most once (first tick). */
13277
+ laneReadbackDone = false;
12703
13278
  /** Timestamp of the last stale branch sweep (at most once per hour) */
12704
13279
  lastBranchSweepMs = 0;
12705
13280
  /** Current tick-phase activity visible to the dashboard */
@@ -12773,7 +13348,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12773
13348
  this.localResolvers.set(name, new LocalModelResolver(resolverOpts));
12774
13349
  }
12775
13350
  }
12776
- this.cacheMetrics = new import_core16.CacheMetricsRecorder();
13351
+ this.cacheMetrics = new import_core17.CacheMetricsRecorder();
12777
13352
  if (this.config.agent.backends !== void 0 && Object.keys(this.config.agent.backends).length > 0) {
12778
13353
  const sandboxPolicy = this.config.agent.sandboxPolicy === "docker" ? "docker" : "none";
12779
13354
  const firstBackendName = Object.keys(this.config.agent.backends)[0];
@@ -12857,22 +13432,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12857
13432
  });
12858
13433
  webhookDelivery.start();
12859
13434
  this.setupNotifications(config.notifications);
12860
- const otlpCfg = config.telemetry?.export?.otlp;
12861
- if (otlpCfg) {
12862
- this.otlpExporter = new import_core16.OTLPExporter({
12863
- endpoint: otlpCfg.endpoint,
12864
- ...otlpCfg.enabled !== void 0 ? { enabled: otlpCfg.enabled } : {},
12865
- ...otlpCfg.headers !== void 0 ? { headers: otlpCfg.headers } : {},
12866
- ...otlpCfg.flushIntervalMs !== void 0 ? { flushIntervalMs: otlpCfg.flushIntervalMs } : {},
12867
- ...otlpCfg.batchSize !== void 0 ? { batchSize: otlpCfg.batchSize } : {}
12868
- });
12869
- this.telemetryFanoutOff = wireTelemetryFanout({
12870
- bus: this,
12871
- exporter: this.otlpExporter,
12872
- webhookDelivery,
12873
- store: webhookStore
12874
- });
12875
- }
13435
+ this.setupTelemetryExport(config, webhookStore, webhookDelivery);
12876
13436
  this.server = new OrchestratorServer(this, config.server.port, {
12877
13437
  interactionQueue: this.interactionQueue,
12878
13438
  webhooks: {
@@ -12895,24 +13455,8 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12895
13455
  analysisArchive: this.analysisArchive,
12896
13456
  roadmapPath: config.tracker.filePath ?? null,
12897
13457
  dispatchAdHoc: this.dispatchAdHoc.bind(this),
12898
- getLocalModelStatus: () => {
12899
- const first = this.localResolvers.values().next();
12900
- return first.done ? null : first.value.getStatus();
12901
- },
12902
- getLocalModelStatuses: () => {
12903
- const backends = this.config.agent.backends ?? {};
12904
- const out = [];
12905
- for (const [name, resolver] of this.localResolvers) {
12906
- const def = backends[name];
12907
- if (!def || def.type !== "local" && def.type !== "pi") continue;
12908
- out.push({
12909
- ...resolver.getStatus(),
12910
- backendName: name,
12911
- endpoint: def.endpoint
12912
- });
12913
- }
12914
- return out;
12915
- }
13458
+ getLocalModelStatus: () => this.getFirstLocalModelStatus(),
13459
+ getLocalModelStatuses: () => this.buildLocalModelStatuses()
12916
13460
  });
12917
13461
  this.server.setRecorder(this.recorder);
12918
13462
  this.interactionQueue.onPush((interaction) => {
@@ -12920,6 +13464,56 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12920
13464
  });
12921
13465
  }
12922
13466
  }
13467
+ /**
13468
+ * Phase 5: construct the OTLP/HTTP trace exporter and wire telemetry fanout.
13469
+ * Only fires when the operator configures `telemetry.export.otlp` in
13470
+ * harness.config.json. Extracted from the server-init block in the
13471
+ * constructor to keep that block's cyclomatic complexity under threshold.
13472
+ */
13473
+ setupTelemetryExport(config, webhookStore, webhookDelivery) {
13474
+ const otlpCfg = config.telemetry?.export?.otlp;
13475
+ if (!otlpCfg) return;
13476
+ this.otlpExporter = new import_core17.OTLPExporter({
13477
+ endpoint: otlpCfg.endpoint,
13478
+ ...otlpCfg.enabled !== void 0 ? { enabled: otlpCfg.enabled } : {},
13479
+ ...otlpCfg.headers !== void 0 ? { headers: otlpCfg.headers } : {},
13480
+ ...otlpCfg.flushIntervalMs !== void 0 ? { flushIntervalMs: otlpCfg.flushIntervalMs } : {},
13481
+ ...otlpCfg.batchSize !== void 0 ? { batchSize: otlpCfg.batchSize } : {}
13482
+ });
13483
+ this.telemetryFanoutOff = wireTelemetryFanout({
13484
+ bus: this,
13485
+ exporter: this.otlpExporter,
13486
+ webhookDelivery,
13487
+ store: webhookStore
13488
+ });
13489
+ }
13490
+ /**
13491
+ * Deprecated alias for /api/v1/local-model/status (Spec 1 endpoint retained
13492
+ * as a compat shim per spec line 35; superseded by getLocalModelStatuses for
13493
+ * the multi-local UI). Returns the first-registered resolver's status.
13494
+ */
13495
+ getFirstLocalModelStatus() {
13496
+ const first = this.localResolvers.values().next();
13497
+ return first.done ? null : first.value.getStatus();
13498
+ }
13499
+ /**
13500
+ * SC38: build NamedLocalModelStatus[] from each registered resolver, tagged
13501
+ * with its backendName + endpoint from the config.
13502
+ */
13503
+ buildLocalModelStatuses() {
13504
+ const backends = this.config.agent.backends ?? {};
13505
+ const out = [];
13506
+ for (const [name, resolver] of this.localResolvers) {
13507
+ const def = backends[name];
13508
+ if (!def || def.type !== "local" && def.type !== "pi") continue;
13509
+ out.push({
13510
+ ...resolver.getStatus(),
13511
+ backendName: name,
13512
+ endpoint: def.endpoint
13513
+ });
13514
+ }
13515
+ return out;
13516
+ }
12923
13517
  createTracker() {
12924
13518
  if (this.config.tracker.kind === "github-issues") {
12925
13519
  const trackerCfg = {
@@ -12928,7 +13522,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12928
13522
  ...this.config.tracker.apiKey ? { token: this.config.tracker.apiKey } : {},
12929
13523
  ...this.config.tracker.endpoint ? { apiBase: this.config.tracker.endpoint } : {}
12930
13524
  };
12931
- const clientResult = (0, import_core15.createTrackerClient)(trackerCfg);
13525
+ const clientResult = (0, import_core16.createTrackerClient)(trackerCfg);
12932
13526
  if (!clientResult.ok) throw clientResult.error;
12933
13527
  return new GitHubIssuesIssueTrackerAdapter(clientResult.value, this.config.tracker);
12934
13528
  }
@@ -12946,48 +13540,30 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12946
13540
  const logger = this.logger;
12947
13541
  const checkRunner = {
12948
13542
  run: async (command, cwd) => {
12949
- const { execFile: execFile7 } = await import("child_process");
12950
- const { promisify: promisify5 } = await import("util");
12951
- const execFileAsync2 = promisify5(execFile7);
12952
- const [cmd, ...args] = command;
12953
- if (!cmd) return { passed: true, findings: 0, output: "" };
12954
- try {
12955
- const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
12956
- const findingsMatch = stdout.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
12957
- const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 0;
12958
- return { passed: findings === 0, findings, output: stdout };
12959
- } catch (err) {
12960
- const error = err;
12961
- const output = [error.stdout, error.stderr].filter(Boolean).join("\n");
12962
- const findingsMatch = output.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
12963
- const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 1;
12964
- return { passed: false, findings, output };
12965
- }
12966
- }
12967
- };
12968
- const agentDispatcher = {
12969
- dispatch: async (skill, branch, backendName, cwd) => {
12970
- logger.info(
12971
- "Maintenance agent dispatcher invoked (stub \u2014 skill dispatch integration pending)",
12972
- {
12973
- skill,
12974
- branch,
12975
- backendName,
12976
- cwd
12977
- }
12978
- );
12979
- return { producedCommits: false, fixed: 0 };
13543
+ const [cmd, ...args] = normalizeHarnessCommand(command);
13544
+ if (!cmd) return { passed: true, findings: 0, output: "", executionFailed: false };
13545
+ return runHarnessCheck({ file: cmd, args }, cwd);
12980
13546
  }
12981
13547
  };
13548
+ const resolveBackend2 = makeBackendResolver(this.getBackends());
13549
+ const agentDispatcher = createAgentDispatcher({
13550
+ resolveBackend: resolveBackend2,
13551
+ git: (args, cwd) => (0, import_node_child_process13.execFileSync)("git", args, { cwd, encoding: "utf-8" }).toString().trim(),
13552
+ logger
13553
+ });
12982
13554
  const commandExecutor = {
12983
13555
  exec: async (command, cwd) => {
12984
13556
  const { execFile: execFile7 } = await import("child_process");
12985
- const { promisify: promisify5 } = await import("util");
12986
- const execFileAsync2 = promisify5(execFile7);
12987
- const [cmd, ...args] = command;
13557
+ const { promisify: promisify6 } = await import("util");
13558
+ const execFileAsync2 = promisify6(execFile7);
13559
+ const [cmd, ...args] = normalizeHarnessCommand(command);
12988
13560
  if (!cmd) return { stdout: "" };
12989
13561
  try {
12990
- const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
13562
+ const { stdout } = await execFileAsync2(cmd, args, {
13563
+ cwd,
13564
+ timeout: MAINTENANCE_CHECK_TIMEOUT_MS,
13565
+ maxBuffer: MAINTENANCE_CHECK_MAX_BUFFER
13566
+ });
12991
13567
  return { stdout: String(stdout) };
12992
13568
  } catch (err) {
12993
13569
  logger.warn("Maintenance command execution failed", {
@@ -13122,6 +13698,10 @@ ${messages}`);
13122
13698
  async asyncTick() {
13123
13699
  await this.ensureClaimManager();
13124
13700
  await this.intelligenceRunner.loadPersistedData();
13701
+ if (!this.laneReadbackDone) {
13702
+ this.laneReadbackDone = true;
13703
+ await this.readBackPersistedLanes();
13704
+ }
13125
13705
  const nowMs = Date.now();
13126
13706
  this.setTickActivity("fetching", "Polling tracker for candidates");
13127
13707
  const candidatesResult = await this.tracker.fetchCandidateIssues();
@@ -13274,12 +13854,48 @@ ${messages}`);
13274
13854
  break;
13275
13855
  case "escalate":
13276
13856
  await this.handleEscalation(effect);
13857
+ await this.persistLaneSafe(effect.issueId, "abandon");
13277
13858
  break;
13278
13859
  case "claim":
13279
13860
  await this.handleClaimEffect(effect);
13280
13861
  break;
13281
13862
  }
13282
13863
  }
13864
+ /**
13865
+ * Phase 4 (DLane-5): persist an orchestrator lane transition to the durable
13866
+ * core event log at the effect boundary. NEVER throws — `persistLane` returns
13867
+ * an `Err` Result on failure, which is logged and swallowed here so a
13868
+ * lane-persistence failure can never break orchestrator dispatch.
13869
+ */
13870
+ async persistLaneSafe(issueId, signal) {
13871
+ const r = await persistLane(this.projectRoot, issueId, signal);
13872
+ if (!r.ok) {
13873
+ this.logger.warn(`lane persist failed for ${issueId} (${signal}): ${r.error.message}`);
13874
+ }
13875
+ }
13876
+ /**
13877
+ * Phase 4 (DLane-5): read persisted task lanes back from the durable log and
13878
+ * log a one-line summary. Stores the projection on `this.persistedLanes` for
13879
+ * observability. Read-only — never feeds reconciliation, never throws.
13880
+ */
13881
+ async readBackPersistedLanes() {
13882
+ const lanes = await readPersistedLanes(this.projectRoot);
13883
+ this.persistedLanes = lanes;
13884
+ const entries = Object.keys(lanes.tasks).map((id) => `${id}:${lanes.tasks[id]?.lane}`);
13885
+ const nonTerminal = entries.filter((e) => !e.endsWith(":done") && !e.endsWith(":canceled"));
13886
+ this.logger.info(
13887
+ `Lane read-back on startup: ${entries.length} persisted task(s), ${nonTerminal.length} non-terminal`,
13888
+ { nonTerminal }
13889
+ );
13890
+ }
13891
+ /**
13892
+ * Phase 4 (DLane-5): the task lanes most recently read back from the durable
13893
+ * log on startup, exposed for external observability. Read-only — a fresh
13894
+ * `{ tasks: {} }` until the first tick's read-back has run.
13895
+ */
13896
+ getPersistedLanes() {
13897
+ return this.persistedLanes;
13898
+ }
13283
13899
  /**
13284
13900
  * Guards workspace cleanup by checking whether the agent pushed a branch
13285
13901
  * that does not yet have a pull request. If so, the worktree is preserved
@@ -13312,7 +13928,7 @@ ${messages}`);
13312
13928
  { issueId }
13313
13929
  );
13314
13930
  await this.interactionQueue.push({
13315
- id: `interaction-${(0, import_node_crypto16.randomUUID)()}`,
13931
+ id: `interaction-${(0, import_node_crypto15.randomUUID)()}`,
13316
13932
  issueId,
13317
13933
  type: "needs-human",
13318
13934
  reasons: [`Agent pushed branch "${branch}" but did not create a PR. Worktree preserved.`],
@@ -13388,7 +14004,7 @@ ${messages}`);
13388
14004
  { issueId: effect.issueId }
13389
14005
  );
13390
14006
  await this.interactionQueue.push({
13391
- id: `interaction-${(0, import_node_crypto16.randomUUID)()}`,
14007
+ id: `interaction-${(0, import_node_crypto15.randomUUID)()}`,
13392
14008
  issueId: effect.issueId,
13393
14009
  type: "needs-human",
13394
14010
  reasons: effect.reasons,
@@ -13466,6 +14082,7 @@ ${messages}`);
13466
14082
  }
13467
14083
  return;
13468
14084
  }
14085
+ await this.persistLaneSafe(effect.issue.id, "claim");
13469
14086
  await this.postClaimComment(effect.issue);
13470
14087
  await this.dispatchIssue(effect.issue, effect.attempt, effect.backend);
13471
14088
  }
@@ -13484,12 +14101,12 @@ ${messages}`);
13484
14101
  async postLifecycleComment(identifier, externalId, event) {
13485
14102
  try {
13486
14103
  if (!externalId) return;
13487
- const trackerConfig = (0, import_core15.loadTrackerSyncConfig)(this.projectRoot);
14104
+ const trackerConfig = (0, import_core16.loadTrackerSyncConfig)(this.projectRoot);
13488
14105
  if (!trackerConfig) return;
13489
14106
  const token = process.env.GITHUB_TOKEN;
13490
14107
  if (!token) return;
13491
14108
  const orchestratorId = await this.orchestratorIdPromise;
13492
- const adapter = new import_core15.GitHubIssuesSyncAdapter({ token, config: trackerConfig });
14109
+ const adapter = new import_core16.GitHubIssuesSyncAdapter({ token, config: trackerConfig });
13493
14110
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
13494
14111
  const actionMap = {
13495
14112
  claimed: "Dispatching agent for autonomous execution",
@@ -13526,6 +14143,7 @@ ${messages}`);
13526
14143
  this.logger.info(`Dispatching issue: ${issue.identifier} (attempt ${attempt})`, {
13527
14144
  issueId: issue.id
13528
14145
  });
14146
+ await this.persistLaneSafe(issue.id, "dispatch");
13529
14147
  try {
13530
14148
  const workspaceResult = await this.workspace.ensureWorkspace(issue.identifier);
13531
14149
  if (!workspaceResult.ok) throw workspaceResult.error;
@@ -13562,7 +14180,7 @@ ${messages}`);
13562
14180
  ...f.line !== void 0 ? { line: f.line } : {}
13563
14181
  }))
13564
14182
  );
13565
- (0, import_core14.writeTaint)(
14183
+ (0, import_core15.writeTaint)(
13566
14184
  workspacePath,
13567
14185
  issue.id,
13568
14186
  "Medium-severity injection patterns found in workspace config files",
@@ -13734,6 +14352,7 @@ ${messages}`);
13734
14352
  * Informs the state machine that an agent worker has exited.
13735
14353
  */
13736
14354
  async emitWorkerExit(issueId, reason, attempt, error) {
14355
+ await this.persistLaneSafe(issueId, reason === "normal" ? "success" : "failure");
13737
14356
  await this.completionHandler.handleWorkerExit(
13738
14357
  issueId,
13739
14358
  reason,
@@ -14269,11 +14888,11 @@ function launchTUI(orchestrator) {
14269
14888
  }
14270
14889
 
14271
14890
  // src/maintenance/sync-main.ts
14272
- var import_node_child_process12 = require("child_process");
14273
- var import_node_util4 = require("util");
14891
+ var import_node_child_process14 = require("child_process");
14892
+ var import_node_util5 = require("util");
14274
14893
  var DEFAULT_TIMEOUT_MS3 = 6e4;
14275
14894
  async function git(execFileFn, args, cwd, timeoutMs) {
14276
- const exec = (0, import_node_util4.promisify)(execFileFn);
14895
+ const exec = (0, import_node_util5.promisify)(execFileFn);
14277
14896
  const { stdout, stderr } = await exec("git", args, { cwd, timeout: timeoutMs });
14278
14897
  return { stdout: String(stdout), stderr: String(stderr) };
14279
14898
  }
@@ -14335,7 +14954,7 @@ async function isAncestor(execFileFn, a, b, cwd, timeoutMs) {
14335
14954
  }
14336
14955
  }
14337
14956
  async function syncMain(repoRoot, opts = {}) {
14338
- const execFileFn = opts.execFileFn ?? import_node_child_process12.execFile;
14957
+ const execFileFn = opts.execFileFn ?? import_node_child_process14.execFile;
14339
14958
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
14340
14959
  try {
14341
14960
  const originRef = await resolveOriginDefault(execFileFn, repoRoot, timeoutMs);
@@ -14412,11 +15031,59 @@ async function syncMain(repoRoot, opts = {}) {
14412
15031
  }
14413
15032
  }
14414
15033
 
15034
+ // src/maintenance/overdue.ts
15035
+ var LOOKBACK_DAYS = 366;
15036
+ function previousFireTime(schedule, now) {
15037
+ let day = new Date(now.getFullYear(), now.getMonth(), now.getDate());
15038
+ for (let d = 0; d <= LOOKBACK_DAYS; d++) {
15039
+ if (cronMatchesDate(schedule, day)) {
15040
+ const dayStartMs = day.getTime();
15041
+ const scanStart = d === 0 ? new Date(
15042
+ now.getFullYear(),
15043
+ now.getMonth(),
15044
+ now.getDate(),
15045
+ now.getHours(),
15046
+ now.getMinutes()
15047
+ ) : new Date(day.getFullYear(), day.getMonth(), day.getDate(), 23, 59);
15048
+ for (let ms = scanStart.getTime(); ms >= dayStartMs; ms -= 6e4) {
15049
+ const candidate = new Date(ms);
15050
+ if (cronMatchesNow(schedule, candidate)) return candidate;
15051
+ }
15052
+ }
15053
+ day = new Date(day.getFullYear(), day.getMonth(), day.getDate() - 1);
15054
+ }
15055
+ return null;
15056
+ }
15057
+ function isSatisfyingRun(r) {
15058
+ return r.status === "success" || r.status === "no-issues";
15059
+ }
15060
+ function isOverdue(task, history, now) {
15061
+ const satisfyingRuns = history.filter((r) => r.taskId === task.id && isSatisfyingRun(r));
15062
+ const fire = previousFireTime(task.schedule, now);
15063
+ if (fire === null) return satisfyingRuns.length === 0;
15064
+ const fireMs = fire.getTime();
15065
+ const satisfied = satisfyingRuns.some((r) => new Date(r.completedAt).getTime() >= fireMs);
15066
+ return !satisfied;
15067
+ }
15068
+ function selectTasks(tasks, history, filter) {
15069
+ const eligible = tasks.filter((t) => t.excludeFromHumanSweep !== true);
15070
+ switch (filter.mode) {
15071
+ case "all":
15072
+ return eligible;
15073
+ case "ids": {
15074
+ const wanted = new Set(filter.ids ?? []);
15075
+ return eligible.filter((t) => wanted.has(t.id));
15076
+ }
15077
+ case "overdue":
15078
+ return eligible.filter((t) => isOverdue(t, history, filter.now));
15079
+ }
15080
+ }
15081
+
14415
15082
  // src/sessions/search-index.ts
14416
- var fs18 = __toESM(require("fs"));
15083
+ var fs16 = __toESM(require("fs"));
14417
15084
  var path22 = __toESM(require("path"));
14418
15085
  var import_better_sqlite32 = __toESM(require("better-sqlite3"));
14419
- var import_types31 = require("@harness-engineering/types");
15086
+ var import_types32 = require("@harness-engineering/types");
14420
15087
  var SEARCH_INDEX_FILE = "search-index.sqlite";
14421
15088
  var SCHEMA_SQL2 = `
14422
15089
  CREATE TABLE IF NOT EXISTS session_docs (
@@ -14474,7 +15141,7 @@ var SqliteSearchIndex = class {
14474
15141
  removeSessionStmt;
14475
15142
  totalStmt;
14476
15143
  constructor(dbPath) {
14477
- fs18.mkdirSync(path22.dirname(dbPath), { recursive: true });
15144
+ fs16.mkdirSync(path22.dirname(dbPath), { recursive: true });
14478
15145
  this.db = new import_better_sqlite32.default(dbPath);
14479
15146
  this.db.pragma("journal_mode = WAL");
14480
15147
  this.db.pragma("synchronous = NORMAL");
@@ -14574,18 +15241,18 @@ function openSearchIndex(projectPath) {
14574
15241
  return new SqliteSearchIndex(searchIndexPath(projectPath));
14575
15242
  }
14576
15243
  function indexSessionDirectory(idx, args) {
14577
- const kinds = args.fileKinds ?? [...import_types31.INDEXED_FILE_KINDS];
15244
+ const kinds = args.fileKinds ?? [...import_types32.INDEXED_FILE_KINDS];
14578
15245
  const cap = args.maxBytesPerBody ?? 256 * 1024;
14579
15246
  let docsWritten = 0;
14580
15247
  for (const kind of kinds) {
14581
15248
  const fileName = FILE_KIND_TO_FILENAME[kind];
14582
15249
  const filePath = path22.join(args.sessionDir, fileName);
14583
- if (!fs18.existsSync(filePath)) continue;
14584
- let body = fs18.readFileSync(filePath, "utf8");
15250
+ if (!fs16.existsSync(filePath)) continue;
15251
+ let body = fs16.readFileSync(filePath, "utf8");
14585
15252
  if (Buffer.byteLength(body, "utf8") > cap) {
14586
15253
  body = body.slice(0, cap) + "\n\n[TRUNCATED]";
14587
15254
  }
14588
- const stat = fs18.statSync(filePath);
15255
+ const stat = fs16.statSync(filePath);
14589
15256
  const relPath = path22.relative(args.projectPath, filePath).replaceAll("\\", "/");
14590
15257
  idx.upsertSessionDoc({
14591
15258
  sessionId: args.sessionId,
@@ -14607,8 +15274,8 @@ function reindexFromArchive(projectPath, opts = {}) {
14607
15274
  idx.resetArchived();
14608
15275
  let sessionsIndexed = 0;
14609
15276
  let docsWritten = 0;
14610
- if (fs18.existsSync(archiveBase)) {
14611
- const entries = fs18.readdirSync(archiveBase, { withFileTypes: true });
15277
+ if (fs16.existsSync(archiveBase)) {
15278
+ const entries = fs16.readdirSync(archiveBase, { withFileTypes: true });
14612
15279
  for (const entry of entries) {
14613
15280
  if (!entry.isDirectory()) continue;
14614
15281
  const sessionDir = path22.join(archiveBase, entry.name);
@@ -14631,10 +15298,10 @@ function reindexFromArchive(projectPath, opts = {}) {
14631
15298
  }
14632
15299
 
14633
15300
  // src/sessions/summarize.ts
14634
- var fs19 = __toESM(require("fs"));
15301
+ var fs17 = __toESM(require("fs"));
14635
15302
  var path23 = __toESM(require("path"));
14636
- var import_types32 = require("@harness-engineering/types");
14637
15303
  var import_types33 = require("@harness-engineering/types");
15304
+ var import_types34 = require("@harness-engineering/types");
14638
15305
  var LLM_SUMMARY_FILE = "llm-summary.md";
14639
15306
  var SUMMARY_INPUT_FILES = [
14640
15307
  { filename: "summary.md", kind: "summary" },
@@ -14661,9 +15328,9 @@ function readInputCorpus(archiveDir) {
14661
15328
  const parts = [];
14662
15329
  for (const { filename, kind } of SUMMARY_INPUT_FILES) {
14663
15330
  const p = path23.join(archiveDir, filename);
14664
- if (!fs19.existsSync(p)) continue;
15331
+ if (!fs17.existsSync(p)) continue;
14665
15332
  try {
14666
- const content = fs19.readFileSync(p, "utf8");
15333
+ const content = fs17.readFileSync(p, "utf8");
14667
15334
  if (content.trim().length === 0) continue;
14668
15335
  parts.push(`## FILE: ${kind}
14669
15336
 
@@ -14725,17 +15392,17 @@ status: failed
14725
15392
 
14726
15393
  - reason: ${reason}
14727
15394
  `;
14728
- fs19.writeFileSync(filePath, body, "utf8");
15395
+ fs17.writeFileSync(filePath, body, "utf8");
14729
15396
  return filePath;
14730
15397
  }
14731
15398
  async function summarizeArchivedSession(ctx) {
14732
15399
  const writeStubOnError = ctx.writeStubOnError ?? true;
14733
- if (!fs19.existsSync(ctx.archiveDir)) {
14734
- return (0, import_types33.Err)(new Error(`archive directory not found: ${ctx.archiveDir}`));
15400
+ if (!fs17.existsSync(ctx.archiveDir)) {
15401
+ return (0, import_types34.Err)(new Error(`archive directory not found: ${ctx.archiveDir}`));
14735
15402
  }
14736
15403
  const corpus = readInputCorpus(ctx.archiveDir);
14737
15404
  if (corpus.trim().length === 0) {
14738
- return (0, import_types33.Err)(new Error(`no summary input files found in ${ctx.archiveDir}`));
15405
+ return (0, import_types34.Err)(new Error(`no summary input files found in ${ctx.archiveDir}`));
14739
15406
  }
14740
15407
  const inputBudgetTokens = ctx.config?.inputBudgetTokens ?? DEFAULT_INPUT_BUDGET_TOKENS;
14741
15408
  const truncated = truncateForBudget(corpus, inputBudgetTokens);
@@ -14744,7 +15411,7 @@ async function summarizeArchivedSession(ctx) {
14744
15411
  const analyzeOpts = {
14745
15412
  prompt,
14746
15413
  systemPrompt: SYSTEM_PROMPT,
14747
- responseSchema: import_types32.SessionSummarySchema,
15414
+ responseSchema: import_types33.SessionSummarySchema,
14748
15415
  ...ctx.config?.model && { model: ctx.config.model }
14749
15416
  };
14750
15417
  let response;
@@ -14768,11 +15435,11 @@ async function summarizeArchivedSession(ctx) {
14768
15435
  } catch {
14769
15436
  }
14770
15437
  }
14771
- return (0, import_types33.Err)(
15438
+ return (0, import_types34.Err)(
14772
15439
  new Error(`session summary failed: ${reason}` + (stubPath ? ` (stub: ${stubPath})` : ""))
14773
15440
  );
14774
15441
  }
14775
- const parsed = import_types32.SessionSummarySchema.safeParse(response.result);
15442
+ const parsed = import_types33.SessionSummarySchema.safeParse(response.result);
14776
15443
  if (!parsed.success) {
14777
15444
  const reason = `schema validation failed: ${parsed.error.message}`;
14778
15445
  ctx.logger?.warn?.("session summary: invalid provider payload", { reason });
@@ -14782,7 +15449,7 @@ async function summarizeArchivedSession(ctx) {
14782
15449
  } catch {
14783
15450
  }
14784
15451
  }
14785
- return (0, import_types33.Err)(new Error(reason));
15452
+ return (0, import_types34.Err)(new Error(reason));
14786
15453
  }
14787
15454
  const meta = {
14788
15455
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -14793,8 +15460,8 @@ async function summarizeArchivedSession(ctx) {
14793
15460
  };
14794
15461
  const filePath = path23.join(ctx.archiveDir, LLM_SUMMARY_FILE);
14795
15462
  const body = renderLlmSummaryMarkdown(parsed.data, meta);
14796
- fs19.writeFileSync(filePath, body, "utf8");
14797
- return (0, import_types33.Ok)({ summary: parsed.data, meta, filePath });
15463
+ fs17.writeFileSync(filePath, body, "utf8");
15464
+ return (0, import_types34.Ok)({ summary: parsed.data, meta, filePath });
14798
15465
  }
14799
15466
  function isSummaryEnabled(config) {
14800
15467
  if (!config) return false;
@@ -14874,13 +15541,18 @@ function buildArchiveHooks(opts) {
14874
15541
  BUILT_IN_TASKS,
14875
15542
  BackendDefSchema,
14876
15543
  BackendRouter,
15544
+ CheckScriptRunner,
14877
15545
  ClaimManager,
14878
15546
  GateNotReadyError,
14879
15547
  GateRunError,
14880
15548
  InteractionQueue,
15549
+ LinearGraphQLClient,
14881
15550
  LinearGraphQLStub,
14882
15551
  LocalModelResolver,
15552
+ MAINTENANCE_CHECK_MAX_BUFFER,
15553
+ MAINTENANCE_CHECK_TIMEOUT_MS,
14883
15554
  MAX_ATTEMPTS,
15555
+ MaintenanceReporter,
14884
15556
  MockBackend,
14885
15557
  ORCHESTRATOR_IDENTITY_FILE,
14886
15558
  Orchestrator,
@@ -14898,6 +15570,7 @@ function buildArchiveHooks(opts) {
14898
15570
  SqliteSearchIndex,
14899
15571
  StreamRecorder,
14900
15572
  TaskOutputStore,
15573
+ TaskRunner,
14901
15574
  TokenStore,
14902
15575
  WebhookQueue,
14903
15576
  WorkflowLoader,
@@ -14908,7 +15581,9 @@ function buildArchiveHooks(opts) {
14908
15581
  buildArchiveHooks,
14909
15582
  calculateRetryDelay,
14910
15583
  canDispatch,
15584
+ classifyCheckExecutionFailure,
14911
15585
  computeRateLimitDelay,
15586
+ createAgentDispatcher,
14912
15587
  createBackend,
14913
15588
  createEmptyState,
14914
15589
  crossFieldRoutingIssues,
@@ -14920,22 +15595,27 @@ function buildArchiveHooks(opts) {
14920
15595
  emitProposalApproved,
14921
15596
  emitProposalCreated,
14922
15597
  emitProposalRejected,
15598
+ explicitFindingsCount,
14923
15599
  extractHighlights,
14924
15600
  extractTitlePrefix,
14925
15601
  getAvailableSlots,
14926
15602
  getDefaultConfig,
14927
15603
  getPerStateCount,
14928
15604
  indexSessionDirectory,
15605
+ isCheckTimeoutError,
14929
15606
  isEligible,
14930
15607
  isSummaryEnabled,
14931
15608
  launchTUI,
14932
15609
  loadPublishedIndex,
15610
+ makeBackendResolver,
14933
15611
  migrateAgentConfig,
14934
15612
  normalizeFts5Query,
15613
+ normalizeHarnessCommand,
14935
15614
  normalizeLocalModel,
14936
15615
  openSearchIndex,
14937
15616
  promote,
14938
15617
  reconcile,
15618
+ recoverFindingsCount,
14939
15619
  reindexFromArchive,
14940
15620
  renderAnalysisComment,
14941
15621
  renderLlmSummaryMarkdown,
@@ -14945,9 +15625,11 @@ function buildArchiveHooks(opts) {
14945
15625
  routeIssue,
14946
15626
  routingWarnings,
14947
15627
  runGate,
15628
+ runHarnessCheck,
14948
15629
  savePublishedIndex,
14949
15630
  searchIndexPath,
14950
15631
  selectCandidates,
15632
+ selectTasks,
14951
15633
  sortCandidates,
14952
15634
  summarizeArchivedSession,
14953
15635
  syncMain,