@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.mjs CHANGED
@@ -2209,11 +2209,10 @@ var WorkflowLoader = class {
2209
2209
  };
2210
2210
 
2211
2211
  // src/tracker/adapters/roadmap.ts
2212
- import * as fs8 from "fs/promises";
2213
2212
  import { createHash as createHash2 } from "crypto";
2214
2213
  import {
2215
- parseRoadmap,
2216
- serializeRoadmap,
2214
+ resolveRoadmapStoreForFile,
2215
+ applyRoadmapDiff,
2217
2216
  claim as claimFeature,
2218
2217
  setStatus as setFeatureStatus,
2219
2218
  isClaimableBy
@@ -2249,8 +2248,7 @@ var RoadmapTrackerAdapter = class {
2249
2248
  async fetchIssuesByStates(stateNames) {
2250
2249
  try {
2251
2250
  if (!this.config.filePath) return Err3(new Error("Missing filePath"));
2252
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2253
- const roadmapResult = parseRoadmap(content);
2251
+ const roadmapResult = await this.loadRoadmap();
2254
2252
  if (!roadmapResult.ok) return roadmapResult;
2255
2253
  const issues = [];
2256
2254
  for (const milestone of roadmapResult.value.milestones) {
@@ -2281,17 +2279,19 @@ var RoadmapTrackerAdapter = class {
2281
2279
  if (!terminal) {
2282
2280
  return Err3(new Error("Tracker config has no terminalStates; cannot mark complete"));
2283
2281
  }
2284
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2285
- const roadmapResult = parseRoadmap(content);
2282
+ const store = this.resolveStore();
2283
+ const roadmapResult = await store.load();
2286
2284
  if (!roadmapResult.ok) return roadmapResult;
2287
2285
  const roadmap = roadmapResult.value;
2286
+ const before = structuredClone(roadmap);
2288
2287
  const target = this.findFeatureById(roadmap.milestones, issueId);
2289
2288
  if (!target) return Ok4(void 0);
2290
2289
  const normalizedTerminal = this.config.terminalStates.map((s) => s.toLowerCase());
2291
2290
  if (normalizedTerminal.includes(target.status.toLowerCase())) return Ok4(void 0);
2292
2291
  const now = (/* @__PURE__ */ new Date()).toISOString();
2293
2292
  setFeatureStatus(roadmap, target, terminal, now.slice(0, 10));
2294
- await fs8.writeFile(this.config.filePath, serializeRoadmap(roadmap), "utf-8");
2293
+ const persisted = await applyRoadmapDiff(store, before, roadmap);
2294
+ if (!persisted.ok) return persisted;
2295
2295
  return Ok4(void 0);
2296
2296
  } catch (error) {
2297
2297
  return Err3(error instanceof Error ? error : new Error(String(error)));
@@ -2305,10 +2305,11 @@ var RoadmapTrackerAdapter = class {
2305
2305
  async claimIssue(issueId, orchestratorId) {
2306
2306
  try {
2307
2307
  if (!this.config.filePath) return Err3(new Error("Missing filePath"));
2308
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2309
- const roadmapResult = parseRoadmap(content);
2308
+ const store = this.resolveStore();
2309
+ const roadmapResult = await store.load();
2310
2310
  if (!roadmapResult.ok) return roadmapResult;
2311
2311
  const roadmap = roadmapResult.value;
2312
+ const before = structuredClone(roadmap);
2312
2313
  const target = this.findFeatureById(roadmap.milestones, issueId);
2313
2314
  if (!target) return Ok4(void 0);
2314
2315
  if (!isClaimableBy(target, orchestratorId)) {
@@ -2320,7 +2321,8 @@ var RoadmapTrackerAdapter = class {
2320
2321
  const now = (/* @__PURE__ */ new Date()).toISOString();
2321
2322
  claimFeature(roadmap, target, orchestratorId, now.slice(0, 10));
2322
2323
  target.updatedAt = now;
2323
- await fs8.writeFile(this.config.filePath, serializeRoadmap(roadmap), "utf-8");
2324
+ const persisted = await applyRoadmapDiff(store, before, roadmap);
2325
+ if (!persisted.ok) return persisted;
2324
2326
  return Ok4(void 0);
2325
2327
  } catch (error) {
2326
2328
  return Err3(error instanceof Error ? error : new Error(String(error)));
@@ -2337,10 +2339,11 @@ var RoadmapTrackerAdapter = class {
2337
2339
  if (!activeState) {
2338
2340
  return Err3(new Error("Tracker config has no activeStates; cannot release"));
2339
2341
  }
2340
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2341
- const roadmapResult = parseRoadmap(content);
2342
+ const store = this.resolveStore();
2343
+ const roadmapResult = await store.load();
2342
2344
  if (!roadmapResult.ok) return roadmapResult;
2343
2345
  const roadmap = roadmapResult.value;
2346
+ const before = structuredClone(roadmap);
2344
2347
  const target = this.findFeatureById(roadmap.milestones, issueId);
2345
2348
  if (!target) return Ok4(void 0);
2346
2349
  if (this.config.activeStates.includes(target.status) && target.assignee === null) {
@@ -2349,12 +2352,26 @@ var RoadmapTrackerAdapter = class {
2349
2352
  const now = (/* @__PURE__ */ new Date()).toISOString();
2350
2353
  setFeatureStatus(roadmap, target, activeState, now.slice(0, 10));
2351
2354
  target.updatedAt = null;
2352
- await fs8.writeFile(this.config.filePath, serializeRoadmap(roadmap), "utf-8");
2355
+ const persisted = await applyRoadmapDiff(store, before, roadmap);
2356
+ if (!persisted.ok) return persisted;
2353
2357
  return Ok4(void 0);
2354
2358
  } catch (error) {
2355
2359
  return Err3(error instanceof Error ? error : new Error(String(error)));
2356
2360
  }
2357
2361
  }
2362
+ /**
2363
+ * Resolve the roadmap store anchored on the configured aggregate file path.
2364
+ * The shard backend is chosen when a sibling `roadmap.d/` exists next to the
2365
+ * file; otherwise the monolith file is read/written whole. Callers guard
2366
+ * `this.config.filePath` before invoking.
2367
+ */
2368
+ resolveStore() {
2369
+ return resolveRoadmapStoreForFile({ roadmapPath: this.config.filePath });
2370
+ }
2371
+ /** Load the roadmap via the store (sharded or monolith), returning a Result. */
2372
+ loadRoadmap() {
2373
+ return this.resolveStore().load();
2374
+ }
2358
2375
  findFeatureById(milestones, issueId) {
2359
2376
  for (const milestone of milestones) {
2360
2377
  for (const feature of milestone.features) {
@@ -2371,8 +2388,7 @@ var RoadmapTrackerAdapter = class {
2371
2388
  async fetchIssueStatesByIds(issueIds) {
2372
2389
  try {
2373
2390
  if (!this.config.filePath) return Err3(new Error("Missing filePath"));
2374
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2375
- const roadmapResult = parseRoadmap(content);
2391
+ const roadmapResult = await this.loadRoadmap();
2376
2392
  if (!roadmapResult.ok) return roadmapResult;
2377
2393
  const issueMap = /* @__PURE__ */ new Map();
2378
2394
  for (const milestone of roadmapResult.value.milestones) {
@@ -2427,7 +2443,70 @@ var RoadmapTrackerAdapter = class {
2427
2443
  };
2428
2444
 
2429
2445
  // src/tracker/extensions/linear.ts
2430
- import { Ok as Ok5 } from "@harness-engineering/types";
2446
+ import { Ok as Ok5, Err as Err4 } from "@harness-engineering/types";
2447
+ var LINEAR_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql";
2448
+ function errorMessage(err) {
2449
+ return err instanceof Error ? err.message : String(err);
2450
+ }
2451
+ var LinearGraphQLClient = class {
2452
+ apiKey;
2453
+ endpoint;
2454
+ fetchFn;
2455
+ constructor(opts) {
2456
+ this.apiKey = opts.apiKey;
2457
+ this.endpoint = opts.endpoint ?? LINEAR_GRAPHQL_ENDPOINT;
2458
+ this.fetchFn = opts.fetchFn ?? globalThis.fetch;
2459
+ }
2460
+ async query(query, variables) {
2461
+ const sent = await this.sendRequest(query, variables);
2462
+ if (!sent.ok) return sent;
2463
+ const res = sent.value;
2464
+ if (!res.ok) {
2465
+ return Err4(await this.httpError(res));
2466
+ }
2467
+ const parsed = await this.parseEnvelope(res);
2468
+ if (!parsed.ok) return parsed;
2469
+ const envelope = parsed.value;
2470
+ const graphqlError = envelopeError(envelope);
2471
+ if (graphqlError) return Err4(graphqlError);
2472
+ return Ok5(envelope.data ?? {});
2473
+ }
2474
+ /** POST the operation to Linear, normalizing transport throws into an `Err`. */
2475
+ async sendRequest(query, variables) {
2476
+ try {
2477
+ const res = await this.fetchFn(this.endpoint, {
2478
+ method: "POST",
2479
+ headers: {
2480
+ Authorization: this.apiKey,
2481
+ "Content-Type": "application/json"
2482
+ },
2483
+ body: JSON.stringify({ query, variables: variables ?? {} })
2484
+ });
2485
+ return Ok5(res);
2486
+ } catch (err) {
2487
+ return Err4(new Error(`Linear GraphQL request failed: ${errorMessage(err)}`));
2488
+ }
2489
+ }
2490
+ /** Build the error for a non-2xx HTTP response, including a truncated body. */
2491
+ async httpError(res) {
2492
+ const body = await res.text().catch(() => "");
2493
+ const detail = body ? `: ${body.slice(0, 500)}` : "";
2494
+ return new Error(`Linear GraphQL HTTP ${res.status}${detail}`);
2495
+ }
2496
+ /** Parse the JSON envelope, normalizing parse failures into an `Err`. */
2497
+ async parseEnvelope(res) {
2498
+ try {
2499
+ return Ok5(await res.json());
2500
+ } catch (err) {
2501
+ return Err4(new Error(`Linear GraphQL response was not valid JSON: ${errorMessage(err)}`));
2502
+ }
2503
+ }
2504
+ };
2505
+ function envelopeError(envelope) {
2506
+ if (!envelope.errors || envelope.errors.length === 0) return void 0;
2507
+ const message = envelope.errors.map((e) => e.message ?? "unknown error").join("; ");
2508
+ return new Error(`Linear GraphQL error: ${message}`);
2509
+ }
2431
2510
  var LinearGraphQLStub = class {
2432
2511
  async query(query, _variables) {
2433
2512
  console.log("Linear GraphQL query (stub):", query);
@@ -2436,11 +2515,11 @@ var LinearGraphQLStub = class {
2436
2515
  };
2437
2516
 
2438
2517
  // src/workspace/manager.ts
2439
- import * as fs9 from "fs/promises";
2518
+ import * as fs8 from "fs/promises";
2440
2519
  import * as path8 from "path";
2441
2520
  import { execFile as execFile2 } from "child_process";
2442
2521
  import { promisify as promisify2 } from "util";
2443
- import { Ok as Ok6, Err as Err4 } from "@harness-engineering/types";
2522
+ import { Ok as Ok6, Err as Err5 } from "@harness-engineering/types";
2444
2523
  var WorkspaceManager = class _WorkspaceManager {
2445
2524
  config;
2446
2525
  /** Absolute path to the git repository root (resolved lazily). */
@@ -2476,7 +2555,7 @@ var WorkspaceManager = class _WorkspaceManager {
2476
2555
  async getRepoRoot() {
2477
2556
  if (this.repoRoot) return this.repoRoot;
2478
2557
  const root = path8.resolve(this.config.root);
2479
- await fs9.mkdir(root, { recursive: true });
2558
+ await fs8.mkdir(root, { recursive: true });
2480
2559
  const stdout = await this.git(["rev-parse", "--show-toplevel"], root);
2481
2560
  this.repoRoot = stdout.trim();
2482
2561
  return this.repoRoot;
@@ -2489,21 +2568,21 @@ var WorkspaceManager = class _WorkspaceManager {
2489
2568
  try {
2490
2569
  const workspacePath = path8.resolve(this.resolvePath(identifier));
2491
2570
  try {
2492
- await fs9.access(path8.join(workspacePath, ".git"));
2571
+ await fs8.access(path8.join(workspacePath, ".git"));
2493
2572
  const repoRoot2 = await this.getRepoRoot();
2494
2573
  try {
2495
2574
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
2496
2575
  } catch {
2497
- await fs9.rm(workspacePath, { recursive: true, force: true });
2576
+ await fs8.rm(workspacePath, { recursive: true, force: true });
2498
2577
  }
2499
2578
  } catch {
2500
2579
  try {
2501
- await fs9.access(workspacePath);
2580
+ await fs8.access(workspacePath);
2502
2581
  const repoRoot2 = await this.getRepoRoot();
2503
2582
  try {
2504
2583
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
2505
2584
  } catch {
2506
- await fs9.rm(workspacePath, { recursive: true, force: true });
2585
+ await fs8.rm(workspacePath, { recursive: true, force: true });
2507
2586
  }
2508
2587
  } catch {
2509
2588
  }
@@ -2515,7 +2594,7 @@ var WorkspaceManager = class _WorkspaceManager {
2515
2594
  await this.seedWorkspace(workspacePath, repoRoot);
2516
2595
  return Ok6(workspacePath);
2517
2596
  } catch (error) {
2518
- return Err4(error instanceof Error ? error : new Error(String(error)));
2597
+ return Err5(error instanceof Error ? error : new Error(String(error)));
2519
2598
  }
2520
2599
  }
2521
2600
  /**
@@ -2556,13 +2635,13 @@ var WorkspaceManager = class _WorkspaceManager {
2556
2635
  }
2557
2636
  const src = path8.join(repoRoot, rel);
2558
2637
  try {
2559
- await fs9.access(src);
2638
+ await fs8.access(src);
2560
2639
  } catch {
2561
2640
  continue;
2562
2641
  }
2563
2642
  const dest = path8.join(workspacePath, rel);
2564
2643
  try {
2565
- await fs9.cp(src, dest, { recursive: true, force: true });
2644
+ await fs8.cp(src, dest, { recursive: true, force: true });
2566
2645
  } catch {
2567
2646
  }
2568
2647
  }
@@ -2638,7 +2717,7 @@ var WorkspaceManager = class _WorkspaceManager {
2638
2717
  async exists(identifier) {
2639
2718
  try {
2640
2719
  const workspacePath = this.resolvePath(identifier);
2641
- await fs9.access(workspacePath);
2720
+ await fs8.access(workspacePath);
2642
2721
  return true;
2643
2722
  } catch {
2644
2723
  return false;
@@ -2653,7 +2732,7 @@ var WorkspaceManager = class _WorkspaceManager {
2653
2732
  try {
2654
2733
  const workspacePath = path8.resolve(this.resolvePath(identifier));
2655
2734
  try {
2656
- await fs9.access(path8.join(workspacePath, ".git"));
2735
+ await fs8.access(path8.join(workspacePath, ".git"));
2657
2736
  } catch {
2658
2737
  return null;
2659
2738
  }
@@ -2764,18 +2843,18 @@ var WorkspaceManager = class _WorkspaceManager {
2764
2843
  const repoRoot = await this.getRepoRoot();
2765
2844
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot);
2766
2845
  } catch {
2767
- await fs9.rm(workspacePath, { recursive: true, force: true });
2846
+ await fs8.rm(workspacePath, { recursive: true, force: true });
2768
2847
  }
2769
2848
  return Ok6(void 0);
2770
2849
  } catch (error) {
2771
- return Err4(error instanceof Error ? error : new Error(String(error)));
2850
+ return Err5(error instanceof Error ? error : new Error(String(error)));
2772
2851
  }
2773
2852
  }
2774
2853
  };
2775
2854
 
2776
2855
  // src/workspace/hooks.ts
2777
2856
  import { spawn } from "child_process";
2778
- import { Ok as Ok7, Err as Err5 } from "@harness-engineering/types";
2857
+ import { Ok as Ok7, Err as Err6 } from "@harness-engineering/types";
2779
2858
  var WorkspaceHooks = class {
2780
2859
  config;
2781
2860
  constructor(config) {
@@ -2802,19 +2881,19 @@ var WorkspaceHooks = class {
2802
2881
  });
2803
2882
  const timeout = setTimeout(() => {
2804
2883
  child.kill();
2805
- resolve8(Err5(new Error(`Hook ${hookName} timed out after ${this.config.timeoutMs}ms`)));
2884
+ resolve8(Err6(new Error(`Hook ${hookName} timed out after ${this.config.timeoutMs}ms`)));
2806
2885
  }, this.config.timeoutMs);
2807
2886
  child.on("exit", (code) => {
2808
2887
  clearTimeout(timeout);
2809
2888
  if (code === 0) {
2810
2889
  resolve8(Ok7(void 0));
2811
2890
  } else {
2812
- resolve8(Err5(new Error(`Hook ${hookName} failed with exit code ${code}`)));
2891
+ resolve8(Err6(new Error(`Hook ${hookName} failed with exit code ${code}`)));
2813
2892
  }
2814
2893
  });
2815
2894
  child.on("error", (error) => {
2816
2895
  clearTimeout(timeout);
2817
- resolve8(Err5(error));
2896
+ resolve8(Err6(error));
2818
2897
  });
2819
2898
  });
2820
2899
  }
@@ -3494,7 +3573,7 @@ import {
3494
3573
  } from "@harness-engineering/core";
3495
3574
 
3496
3575
  // src/tracker/adapters/github-issues-issue-tracker.ts
3497
- import { Ok as Ok9, Err as Err6 } from "@harness-engineering/types";
3576
+ import { Ok as Ok9, Err as Err7 } from "@harness-engineering/types";
3498
3577
  var GitHubIssuesIssueTrackerAdapter = class {
3499
3578
  client;
3500
3579
  config;
@@ -3509,12 +3588,12 @@ var GitHubIssuesIssueTrackerAdapter = class {
3509
3588
  const r = await this.client.fetchByStatus(
3510
3589
  stateNames
3511
3590
  );
3512
- if (!r.ok) return Err6(r.error);
3591
+ if (!r.ok) return Err7(r.error);
3513
3592
  return Ok9(r.value.map((f) => this.mapTrackedToIssue(f)));
3514
3593
  }
3515
3594
  async fetchIssueStatesByIds(issueIds) {
3516
3595
  const r = await this.client.fetchAll();
3517
- if (!r.ok) return Err6(r.error);
3596
+ if (!r.ok) return Err7(r.error);
3518
3597
  const wanted = new Set(issueIds);
3519
3598
  const out = /* @__PURE__ */ new Map();
3520
3599
  for (const f of r.value.features) {
@@ -3524,17 +3603,17 @@ var GitHubIssuesIssueTrackerAdapter = class {
3524
3603
  }
3525
3604
  async claimIssue(issueId, orchestratorId) {
3526
3605
  const r = await this.client.claim(issueId, orchestratorId);
3527
- if (!r.ok) return Err6(r.error);
3606
+ if (!r.ok) return Err7(r.error);
3528
3607
  return Ok9(void 0);
3529
3608
  }
3530
3609
  async releaseIssue(issueId) {
3531
3610
  const r = await this.client.release(issueId);
3532
- if (!r.ok) return Err6(r.error);
3611
+ if (!r.ok) return Err7(r.error);
3533
3612
  return Ok9(void 0);
3534
3613
  }
3535
3614
  async markIssueComplete(issueId) {
3536
3615
  const r = await this.client.complete(issueId);
3537
- if (!r.ok) return Err6(r.error);
3616
+ if (!r.ok) return Err7(r.error);
3538
3617
  return Ok9(void 0);
3539
3618
  }
3540
3619
  /**
@@ -4185,14 +4264,14 @@ import * as readline from "readline";
4185
4264
  import { randomUUID as randomUUID2 } from "crypto";
4186
4265
  import {
4187
4266
  Ok as Ok10,
4188
- Err as Err7
4267
+ Err as Err8
4189
4268
  } from "@harness-engineering/types";
4190
4269
  function resolveExitCode(code, command, resolve8) {
4191
4270
  if (code === 0) {
4192
4271
  resolve8(Ok10(void 0));
4193
4272
  } else {
4194
4273
  resolve8(
4195
- Err7({
4274
+ Err8({
4196
4275
  category: "agent_not_found",
4197
4276
  message: `Claude command '${command}' not found or failed`
4198
4277
  })
@@ -4200,7 +4279,7 @@ function resolveExitCode(code, command, resolve8) {
4200
4279
  }
4201
4280
  }
4202
4281
  function resolveSpawnError(command, resolve8) {
4203
- resolve8(Err7({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
4282
+ resolve8(Err8({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
4204
4283
  }
4205
4284
  var JUST_PAST_GRACE_MS = 5 * 6e4;
4206
4285
  var PRIMARY_LIMIT_RE = /You[\u2019']ve hit your limit.*resets\s+(\d{1,2}(?::\d{2})?\s*(?:am|pm))\s*\(([^)]+)\)/i;
@@ -4550,7 +4629,7 @@ var ClaudeBackend = class {
4550
4629
  import Anthropic from "@anthropic-ai/sdk";
4551
4630
  import {
4552
4631
  Ok as Ok11,
4553
- Err as Err8
4632
+ Err as Err9
4554
4633
  } from "@harness-engineering/types";
4555
4634
  import { AnthropicCacheAdapter } from "@harness-engineering/core";
4556
4635
  var AnthropicBackend = class {
@@ -4569,7 +4648,7 @@ var AnthropicBackend = class {
4569
4648
  }
4570
4649
  async startSession(params) {
4571
4650
  if (!this.config.apiKey) {
4572
- return Err8({
4651
+ return Err9({
4573
4652
  category: "agent_not_found",
4574
4653
  message: "ANTHROPIC_API_KEY is not set"
4575
4654
  });
@@ -4630,18 +4709,18 @@ var AnthropicBackend = class {
4630
4709
  usage
4631
4710
  };
4632
4711
  } catch (err) {
4633
- const errorMessage = err instanceof Error ? err.message : "Anthropic request failed";
4712
+ const errorMessage2 = err instanceof Error ? err.message : "Anthropic request failed";
4634
4713
  yield {
4635
4714
  type: "error",
4636
4715
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4637
- content: errorMessage,
4716
+ content: errorMessage2,
4638
4717
  sessionId: session.sessionId
4639
4718
  };
4640
4719
  return {
4641
4720
  success: false,
4642
4721
  sessionId: session.sessionId,
4643
4722
  usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
4644
- error: errorMessage
4723
+ error: errorMessage2
4645
4724
  };
4646
4725
  }
4647
4726
  }
@@ -4650,7 +4729,7 @@ var AnthropicBackend = class {
4650
4729
  }
4651
4730
  async healthCheck() {
4652
4731
  if (!this.config.apiKey) {
4653
- return Err8({
4732
+ return Err9({
4654
4733
  category: "response_error",
4655
4734
  message: "ANTHROPIC_API_KEY is not set"
4656
4735
  });
@@ -4663,7 +4742,7 @@ var AnthropicBackend = class {
4663
4742
  import OpenAI from "openai";
4664
4743
  import {
4665
4744
  Ok as Ok12,
4666
- Err as Err9
4745
+ Err as Err10
4667
4746
  } from "@harness-engineering/types";
4668
4747
  import { OpenAICacheAdapter } from "@harness-engineering/core";
4669
4748
  var OpenAIBackend = class {
@@ -4681,7 +4760,7 @@ var OpenAIBackend = class {
4681
4760
  }
4682
4761
  async startSession(params) {
4683
4762
  if (!this.config.apiKey) {
4684
- return Err9({
4763
+ return Err10({
4685
4764
  category: "agent_not_found",
4686
4765
  message: "OPENAI_API_KEY is not set"
4687
4766
  });
@@ -4735,18 +4814,18 @@ var OpenAIBackend = class {
4735
4814
  }
4736
4815
  }
4737
4816
  } catch (err) {
4738
- const errorMessage = err instanceof Error ? err.message : "OpenAI request failed";
4817
+ const errorMessage2 = err instanceof Error ? err.message : "OpenAI request failed";
4739
4818
  yield {
4740
4819
  type: "error",
4741
4820
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4742
- content: errorMessage,
4821
+ content: errorMessage2,
4743
4822
  sessionId: session.sessionId
4744
4823
  };
4745
4824
  return {
4746
4825
  success: false,
4747
4826
  sessionId: session.sessionId,
4748
4827
  usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
4749
- error: errorMessage
4828
+ error: errorMessage2
4750
4829
  };
4751
4830
  }
4752
4831
  const usage = {
@@ -4776,7 +4855,7 @@ var OpenAIBackend = class {
4776
4855
  await this.client.models.list();
4777
4856
  return Ok12(void 0);
4778
4857
  } catch (err) {
4779
- return Err9({
4858
+ return Err10({
4780
4859
  category: "response_error",
4781
4860
  message: err instanceof Error ? err.message : "OpenAI health check failed"
4782
4861
  });
@@ -4788,7 +4867,7 @@ var OpenAIBackend = class {
4788
4867
  import { GoogleGenAI } from "@google/genai";
4789
4868
  import {
4790
4869
  Ok as Ok13,
4791
- Err as Err10
4870
+ Err as Err11
4792
4871
  } from "@harness-engineering/types";
4793
4872
  import { GeminiCacheAdapter } from "@harness-engineering/core";
4794
4873
  var GeminiBackend = class {
@@ -4804,7 +4883,7 @@ var GeminiBackend = class {
4804
4883
  }
4805
4884
  async startSession(params) {
4806
4885
  if (!this.config.apiKey) {
4807
- return Err10({
4886
+ return Err11({
4808
4887
  category: "agent_not_found",
4809
4888
  message: "GEMINI_API_KEY is not set"
4810
4889
  });
@@ -4860,11 +4939,11 @@ var GeminiBackend = class {
4860
4939
  }
4861
4940
  }
4862
4941
  } catch (err) {
4863
- const errorMessage = err instanceof Error ? err.message : "Gemini request failed";
4942
+ const errorMessage2 = err instanceof Error ? err.message : "Gemini request failed";
4864
4943
  yield {
4865
4944
  type: "error",
4866
4945
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4867
- content: errorMessage,
4946
+ content: errorMessage2,
4868
4947
  sessionId: session.sessionId
4869
4948
  };
4870
4949
  return {
@@ -4877,7 +4956,7 @@ var GeminiBackend = class {
4877
4956
  cacheCreationTokens,
4878
4957
  cacheReadTokens
4879
4958
  },
4880
- error: errorMessage
4959
+ error: errorMessage2
4881
4960
  };
4882
4961
  }
4883
4962
  const usage = {
@@ -4904,7 +4983,7 @@ var GeminiBackend = class {
4904
4983
  }
4905
4984
  async healthCheck() {
4906
4985
  if (!this.config.apiKey) {
4907
- return Err10({
4986
+ return Err11({
4908
4987
  category: "agent_not_found",
4909
4988
  message: "GEMINI_API_KEY is not set"
4910
4989
  });
@@ -4913,7 +4992,7 @@ var GeminiBackend = class {
4913
4992
  new GoogleGenAI({ apiKey: this.config.apiKey });
4914
4993
  return Ok13(void 0);
4915
4994
  } catch (err) {
4916
- return Err10({
4995
+ return Err11({
4917
4996
  category: "response_error",
4918
4997
  message: err instanceof Error ? err.message : "Gemini health check failed"
4919
4998
  });
@@ -4925,7 +5004,7 @@ var GeminiBackend = class {
4925
5004
  import OpenAI2 from "openai";
4926
5005
  import {
4927
5006
  Ok as Ok14,
4928
- Err as Err11
5007
+ Err as Err12
4929
5008
  } from "@harness-engineering/types";
4930
5009
  var DEFAULT_TIMEOUT_MS = 9e4;
4931
5010
  var LocalBackend = class {
@@ -4952,7 +5031,7 @@ var LocalBackend = class {
4952
5031
  if (this.getModel) {
4953
5032
  const candidate = this.getModel();
4954
5033
  if (candidate === null) {
4955
- return Err11({
5034
+ return Err12({
4956
5035
  category: "agent_not_found",
4957
5036
  message: "No local model available; check dashboard for details."
4958
5037
  });
@@ -5006,18 +5085,18 @@ var LocalBackend = class {
5006
5085
  }
5007
5086
  }
5008
5087
  } catch (err) {
5009
- const errorMessage = err instanceof Error ? err.message : "Local backend request failed";
5088
+ const errorMessage2 = err instanceof Error ? err.message : "Local backend request failed";
5010
5089
  yield {
5011
5090
  type: "error",
5012
5091
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5013
- content: errorMessage,
5092
+ content: errorMessage2,
5014
5093
  sessionId: session.sessionId
5015
5094
  };
5016
5095
  return {
5017
5096
  success: false,
5018
5097
  sessionId: session.sessionId,
5019
5098
  usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
5020
- error: errorMessage
5099
+ error: errorMessage2
5021
5100
  };
5022
5101
  }
5023
5102
  const usage = { inputTokens, outputTokens, totalTokens };
@@ -5041,7 +5120,7 @@ var LocalBackend = class {
5041
5120
  await this.client.models.list();
5042
5121
  return Ok14(void 0);
5043
5122
  } catch (err) {
5044
- return Err11({
5123
+ return Err12({
5045
5124
  category: "response_error",
5046
5125
  message: err instanceof Error ? err.message : "Local backend health check failed"
5047
5126
  });
@@ -5053,7 +5132,7 @@ var LocalBackend = class {
5053
5132
  import { randomUUID as randomUUID3 } from "crypto";
5054
5133
  import {
5055
5134
  Ok as Ok15,
5056
- Err as Err12
5135
+ Err as Err13
5057
5136
  } from "@harness-engineering/types";
5058
5137
  var SILENT_EVENTS = /* @__PURE__ */ new Set([
5059
5138
  "turn_end",
@@ -5171,7 +5250,7 @@ var PiBackend = class {
5171
5250
  if (this.config.getModel) {
5172
5251
  const candidate = this.config.getModel();
5173
5252
  if (candidate === null) {
5174
- return Err12({
5253
+ return Err13({
5175
5254
  category: "agent_not_found",
5176
5255
  message: "No local model available; check dashboard for details."
5177
5256
  });
@@ -5201,7 +5280,7 @@ var PiBackend = class {
5201
5280
  };
5202
5281
  return Ok15(session);
5203
5282
  } catch (err) {
5204
- return Err12({
5283
+ return Err13({
5205
5284
  category: "response_error",
5206
5285
  message: `Failed to create pi session: ${err instanceof Error ? err.message : String(err)}`
5207
5286
  });
@@ -5338,7 +5417,7 @@ var PiBackend = class {
5338
5417
  await import("@earendil-works/pi-coding-agent");
5339
5418
  return Ok15(void 0);
5340
5419
  } catch (err) {
5341
- return Err12({
5420
+ return Err13({
5342
5421
  category: "agent_not_found",
5343
5422
  message: `Pi SDK not available: ${err instanceof Error ? err.message : String(err)}`
5344
5423
  });
@@ -5350,7 +5429,7 @@ var PiBackend = class {
5350
5429
  import { spawn as spawn3 } from "child_process";
5351
5430
  import {
5352
5431
  Ok as Ok16,
5353
- Err as Err13
5432
+ Err as Err14
5354
5433
  } from "@harness-engineering/types";
5355
5434
  var DEFAULT_TIMEOUT_MS2 = 9e4;
5356
5435
  var FORBIDDEN_HOST_CHARS = /[;&|`$()\n\r<>]/;
@@ -5359,30 +5438,8 @@ var SshBackend = class {
5359
5438
  config;
5360
5439
  spawnImpl;
5361
5440
  constructor(config) {
5362
- if (!config.host || typeof config.host !== "string") {
5363
- throw new Error("SshBackend: `host` is required");
5364
- }
5365
- if (FORBIDDEN_HOST_CHARS.test(config.host) || config.host.startsWith("-")) {
5366
- throw new Error(
5367
- `SshBackend: invalid host '${config.host}' (contains shell metacharacters or starts with '-')`
5368
- );
5369
- }
5370
- if (!config.remoteCommand || typeof config.remoteCommand !== "string") {
5371
- throw new Error("SshBackend: `remoteCommand` is required");
5372
- }
5373
- if (config.user !== void 0 && /[\s;&|`$]/.test(config.user)) {
5374
- throw new Error(`SshBackend: invalid user '${config.user}'`);
5375
- }
5376
- this.config = {
5377
- host: config.host,
5378
- remoteCommand: config.remoteCommand,
5379
- sshBinary: config.sshBinary ?? "ssh",
5380
- sshOptions: config.sshOptions ?? [],
5381
- timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
5382
- ...config.user !== void 0 ? { user: config.user } : {},
5383
- ...config.port !== void 0 ? { port: config.port } : {},
5384
- ...config.identityFile !== void 0 ? { identityFile: config.identityFile } : {}
5385
- };
5441
+ validateSshConfig(config);
5442
+ this.config = normalizeSshConfig(config);
5386
5443
  this.spawnImpl = config.spawnImpl ?? spawn3;
5387
5444
  }
5388
5445
  /**
@@ -5497,7 +5554,7 @@ var SshBackend = class {
5497
5554
  });
5498
5555
  } catch (err) {
5499
5556
  resolve8(
5500
- Err13({
5557
+ Err14({
5501
5558
  category: "agent_not_found",
5502
5559
  message: err instanceof Error ? err.message : "failed to spawn ssh"
5503
5560
  })
@@ -5520,7 +5577,7 @@ var SshBackend = class {
5520
5577
  resolve8(Ok16(void 0));
5521
5578
  } else {
5522
5579
  resolve8(
5523
- Err13({
5580
+ Err14({
5524
5581
  category: "agent_not_found",
5525
5582
  message: `ssh health check failed (exit=${code ?? "null"}): ${stderr.slice(0, 500)}`
5526
5583
  })
@@ -5529,11 +5586,39 @@ var SshBackend = class {
5529
5586
  });
5530
5587
  child.on("error", (err) => {
5531
5588
  clearTimeout(timer);
5532
- resolve8(Err13({ category: "agent_not_found", message: err.message }));
5589
+ resolve8(Err14({ category: "agent_not_found", message: err.message }));
5533
5590
  });
5534
5591
  });
5535
5592
  }
5536
5593
  };
5594
+ function validateSshConfig(config) {
5595
+ if (!config.host || typeof config.host !== "string") {
5596
+ throw new Error("SshBackend: `host` is required");
5597
+ }
5598
+ if (FORBIDDEN_HOST_CHARS.test(config.host) || config.host.startsWith("-")) {
5599
+ throw new Error(
5600
+ `SshBackend: invalid host '${config.host}' (contains shell metacharacters or starts with '-')`
5601
+ );
5602
+ }
5603
+ if (!config.remoteCommand || typeof config.remoteCommand !== "string") {
5604
+ throw new Error("SshBackend: `remoteCommand` is required");
5605
+ }
5606
+ if (config.user !== void 0 && /[\s;&|`$]/.test(config.user)) {
5607
+ throw new Error(`SshBackend: invalid user '${config.user}'`);
5608
+ }
5609
+ }
5610
+ function normalizeSshConfig(config) {
5611
+ return {
5612
+ host: config.host,
5613
+ remoteCommand: config.remoteCommand,
5614
+ sshBinary: config.sshBinary ?? "ssh",
5615
+ sshOptions: config.sshOptions ?? [],
5616
+ timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
5617
+ ...config.user !== void 0 ? { user: config.user } : {},
5618
+ ...config.port !== void 0 ? { port: config.port } : {},
5619
+ ...config.identityFile !== void 0 ? { identityFile: config.identityFile } : {}
5620
+ };
5621
+ }
5537
5622
  function errResult(sessionId, message) {
5538
5623
  return {
5539
5624
  success: false,
@@ -5591,7 +5676,7 @@ function waitForExit(child) {
5591
5676
  import { spawn as spawn4 } from "child_process";
5592
5677
  import {
5593
5678
  Ok as Ok17,
5594
- Err as Err14
5679
+ Err as Err15
5595
5680
  } from "@harness-engineering/types";
5596
5681
  var ServerlessBackend = class {
5597
5682
  handles = /* @__PURE__ */ new Map();
@@ -5643,23 +5728,8 @@ var OciServerlessBackend = class extends ServerlessBackend {
5643
5728
  envSource;
5644
5729
  constructor(config) {
5645
5730
  super();
5646
- if (!config.image || typeof config.image !== "string") {
5647
- throw new Error("OciServerlessBackend: `image` is required");
5648
- }
5649
- if (FORBIDDEN_IMAGE_CHARS.test(config.image) || config.image.startsWith("-")) {
5650
- throw new Error(
5651
- `OciServerlessBackend: invalid image '${config.image}' (contains shell metacharacters or starts with '-')`
5652
- );
5653
- }
5654
- this.config = {
5655
- image: config.image,
5656
- pullPolicy: config.pullPolicy ?? "if-not-present",
5657
- runtime: config.runtime ?? "docker",
5658
- envPassthrough: config.envPassthrough ?? [],
5659
- timeoutMs: config.timeoutMs ?? DEFAULT_OCI_TIMEOUT_MS,
5660
- extraArgs: sanitizeExtraArgs(config.extraArgs),
5661
- ...config.registry !== void 0 ? { registry: config.registry } : {}
5662
- };
5731
+ validateOciImage(config.image);
5732
+ this.config = resolveOciConfig(config);
5663
5733
  this.spawnImpl = config.spawnImpl ?? spawn4;
5664
5734
  this.envSource = config.envSource ?? process.env;
5665
5735
  }
@@ -5690,7 +5760,7 @@ var OciServerlessBackend = class extends ServerlessBackend {
5690
5760
  if (!result.ok) return result;
5691
5761
  const id = result.value.trim().split(/\s+/)[0] ?? "";
5692
5762
  if (!id) {
5693
- return Err14({
5763
+ return Err15({
5694
5764
  category: "response_error",
5695
5765
  message: "OciServerlessBackend: empty container id from runtime"
5696
5766
  });
@@ -5750,7 +5820,7 @@ var OciServerlessBackend = class extends ServerlessBackend {
5750
5820
  }
5751
5821
  async teardown(handle) {
5752
5822
  if (handle.adapter !== this.name) {
5753
- return Err14({
5823
+ return Err15({
5754
5824
  category: "response_error",
5755
5825
  message: `handle adapter mismatch: got '${handle.adapter}', expected '${this.name}'`
5756
5826
  });
@@ -5781,7 +5851,7 @@ var OciServerlessBackend = class extends ServerlessBackend {
5781
5851
  });
5782
5852
  } catch (err) {
5783
5853
  resolve8(
5784
- Err14({
5854
+ Err15({
5785
5855
  category: "agent_not_found",
5786
5856
  message: err instanceof Error ? err.message : "failed to spawn runtime"
5787
5857
  })
@@ -5808,7 +5878,7 @@ var OciServerlessBackend = class extends ServerlessBackend {
5808
5878
  resolve8(Ok17(stdout));
5809
5879
  } else {
5810
5880
  resolve8(
5811
- Err14({
5881
+ Err15({
5812
5882
  category: "response_error",
5813
5883
  message: `runtime '${binary} ${args.join(" ")}' exited ${code ?? "null"}: ${stderr.slice(0, 500)}`
5814
5884
  })
@@ -5817,11 +5887,32 @@ var OciServerlessBackend = class extends ServerlessBackend {
5817
5887
  });
5818
5888
  child.on("error", (err) => {
5819
5889
  clearTimeout(timer);
5820
- resolve8(Err14({ category: "agent_not_found", message: err.message }));
5890
+ resolve8(Err15({ category: "agent_not_found", message: err.message }));
5821
5891
  });
5822
5892
  });
5823
5893
  }
5824
5894
  };
5895
+ function validateOciImage(image) {
5896
+ if (!image || typeof image !== "string") {
5897
+ throw new Error("OciServerlessBackend: `image` is required");
5898
+ }
5899
+ if (FORBIDDEN_IMAGE_CHARS.test(image) || image.startsWith("-")) {
5900
+ throw new Error(
5901
+ `OciServerlessBackend: invalid image '${image}' (contains shell metacharacters or starts with '-')`
5902
+ );
5903
+ }
5904
+ }
5905
+ function resolveOciConfig(config) {
5906
+ return {
5907
+ image: config.image,
5908
+ pullPolicy: config.pullPolicy ?? "if-not-present",
5909
+ runtime: config.runtime ?? "docker",
5910
+ envPassthrough: config.envPassthrough ?? [],
5911
+ timeoutMs: config.timeoutMs ?? DEFAULT_OCI_TIMEOUT_MS,
5912
+ extraArgs: sanitizeExtraArgs(config.extraArgs),
5913
+ ...config.registry !== void 0 ? { registry: config.registry } : {}
5914
+ };
5915
+ }
5825
5916
  function sanitizeExtraArgs(extraArgs) {
5826
5917
  if (!extraArgs) return [];
5827
5918
  return extraArgs.filter((arg) => !BLOCKED_DOCKER_FLAGS.some((flag) => arg.startsWith(flag)));
@@ -5893,78 +5984,96 @@ function makeGetModel(model) {
5893
5984
  if (Array.isArray(model) && model.length > 0) return () => model[0] ?? null;
5894
5985
  return () => null;
5895
5986
  }
5987
+ function createClaudeBackend(def, options) {
5988
+ return new ClaudeBackend(def.command ?? "claude", {
5989
+ ...options.cacheMetrics ? { cacheMetrics: options.cacheMetrics } : {}
5990
+ });
5991
+ }
5992
+ function createAnthropicBackend(def) {
5993
+ return new AnthropicBackend({
5994
+ model: def.model,
5995
+ ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
5996
+ });
5997
+ }
5998
+ function createOpenAIBackend(def) {
5999
+ return new OpenAIBackend({
6000
+ model: def.model,
6001
+ ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
6002
+ });
6003
+ }
6004
+ function createGeminiBackend(def) {
6005
+ return new GeminiBackend({
6006
+ model: def.model,
6007
+ ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
6008
+ });
6009
+ }
6010
+ function createLocalBackend(def) {
6011
+ const isArray = Array.isArray(def.model);
6012
+ return new LocalBackend({
6013
+ endpoint: def.endpoint,
6014
+ ...typeof def.model === "string" ? { model: def.model } : {},
6015
+ ...isArray ? { getModel: makeGetModel(def.model) } : {},
6016
+ ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
6017
+ ...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
6018
+ });
6019
+ }
6020
+ function createPiBackend(def) {
6021
+ const isArray = Array.isArray(def.model);
6022
+ return new PiBackend({
6023
+ endpoint: def.endpoint,
6024
+ ...typeof def.model === "string" ? { model: def.model } : {},
6025
+ ...isArray ? { getModel: makeGetModel(def.model) } : {},
6026
+ ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
6027
+ ...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
6028
+ });
6029
+ }
6030
+ function createSshBackend(def) {
6031
+ return new SshBackend({
6032
+ host: def.host,
6033
+ remoteCommand: def.remoteCommand,
6034
+ ...def.user !== void 0 ? { user: def.user } : {},
6035
+ ...def.port !== void 0 ? { port: def.port } : {},
6036
+ ...def.identityFile !== void 0 ? { identityFile: def.identityFile } : {},
6037
+ ...def.sshOptions !== void 0 ? { sshOptions: def.sshOptions } : {},
6038
+ ...def.sshBinary !== void 0 ? { sshBinary: def.sshBinary } : {}
6039
+ });
6040
+ }
6041
+ function createServerlessBackend(def) {
6042
+ switch (def.adapter) {
6043
+ case "oci":
6044
+ return new OciServerlessBackend({
6045
+ image: def.image,
6046
+ ...def.registry !== void 0 ? { registry: def.registry } : {},
6047
+ ...def.pullPolicy !== void 0 ? { pullPolicy: def.pullPolicy } : {},
6048
+ ...def.envPassthrough !== void 0 ? { envPassthrough: def.envPassthrough } : {},
6049
+ ...def.runtime !== void 0 ? { runtime: def.runtime } : {}
6050
+ });
6051
+ default: {
6052
+ const exhaustive = def.adapter;
6053
+ throw new Error(`createBackend: unknown serverless adapter ${JSON.stringify(exhaustive)}`);
6054
+ }
6055
+ }
6056
+ }
5896
6057
  function createBackend(def, options = {}) {
5897
6058
  switch (def.type) {
5898
6059
  case "mock":
5899
6060
  return new MockBackend();
5900
6061
  case "claude":
5901
- return new ClaudeBackend(def.command ?? "claude", {
5902
- ...options.cacheMetrics ? { cacheMetrics: options.cacheMetrics } : {}
5903
- });
6062
+ return createClaudeBackend(def, options);
5904
6063
  case "anthropic":
5905
- return new AnthropicBackend({
5906
- model: def.model,
5907
- ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
5908
- });
6064
+ return createAnthropicBackend(def);
5909
6065
  case "openai":
5910
- return new OpenAIBackend({
5911
- model: def.model,
5912
- ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
5913
- });
6066
+ return createOpenAIBackend(def);
5914
6067
  case "gemini":
5915
- return new GeminiBackend({
5916
- model: def.model,
5917
- ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
5918
- });
5919
- case "local": {
5920
- const isArray = Array.isArray(def.model);
5921
- return new LocalBackend({
5922
- endpoint: def.endpoint,
5923
- ...typeof def.model === "string" ? { model: def.model } : {},
5924
- ...isArray ? { getModel: makeGetModel(def.model) } : {},
5925
- ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
5926
- ...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
5927
- });
5928
- }
5929
- case "pi": {
5930
- const isArray = Array.isArray(def.model);
5931
- return new PiBackend({
5932
- endpoint: def.endpoint,
5933
- ...typeof def.model === "string" ? { model: def.model } : {},
5934
- ...isArray ? { getModel: makeGetModel(def.model) } : {},
5935
- ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
5936
- ...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
5937
- });
5938
- }
5939
- case "ssh": {
5940
- return new SshBackend({
5941
- host: def.host,
5942
- remoteCommand: def.remoteCommand,
5943
- ...def.user !== void 0 ? { user: def.user } : {},
5944
- ...def.port !== void 0 ? { port: def.port } : {},
5945
- ...def.identityFile !== void 0 ? { identityFile: def.identityFile } : {},
5946
- ...def.sshOptions !== void 0 ? { sshOptions: def.sshOptions } : {},
5947
- ...def.sshBinary !== void 0 ? { sshBinary: def.sshBinary } : {}
5948
- });
5949
- }
5950
- case "serverless": {
5951
- switch (def.adapter) {
5952
- case "oci":
5953
- return new OciServerlessBackend({
5954
- image: def.image,
5955
- ...def.registry !== void 0 ? { registry: def.registry } : {},
5956
- ...def.pullPolicy !== void 0 ? { pullPolicy: def.pullPolicy } : {},
5957
- ...def.envPassthrough !== void 0 ? { envPassthrough: def.envPassthrough } : {},
5958
- ...def.runtime !== void 0 ? { runtime: def.runtime } : {}
5959
- });
5960
- default: {
5961
- const exhaustive = def.adapter;
5962
- throw new Error(
5963
- `createBackend: unknown serverless adapter ${JSON.stringify(exhaustive)}`
5964
- );
5965
- }
5966
- }
5967
- }
6068
+ return createGeminiBackend(def);
6069
+ case "local":
6070
+ return createLocalBackend(def);
6071
+ case "pi":
6072
+ return createPiBackend(def);
6073
+ case "ssh":
6074
+ return createSshBackend(def);
6075
+ case "serverless":
6076
+ return createServerlessBackend(def);
5968
6077
  default: {
5969
6078
  const exhaustive = def;
5970
6079
  throw new Error(`createBackend: unknown backend type ${JSON.stringify(exhaustive)}`);
@@ -5974,7 +6083,7 @@ function createBackend(def, options = {}) {
5974
6083
 
5975
6084
  // src/agent/backends/container.ts
5976
6085
  import {
5977
- Err as Err15
6086
+ Err as Err16
5978
6087
  } from "@harness-engineering/types";
5979
6088
  function toAgentError(message, details) {
5980
6089
  return { category: "response_error", message, details };
@@ -6006,7 +6115,7 @@ var ContainerBackend = class {
6006
6115
  }
6007
6116
  const result = await this.secretBackend.resolveSecrets(this.secretKeys);
6008
6117
  if (!result.ok) {
6009
- return Err15(toAgentError(`Secret resolution failed: ${result.error.message}`, result.error));
6118
+ return Err16(toAgentError(`Secret resolution failed: ${result.error.message}`, result.error));
6010
6119
  }
6011
6120
  return { ok: true, value: result.value };
6012
6121
  }
@@ -6031,7 +6140,7 @@ var ContainerBackend = class {
6031
6140
  const createOpts = this.buildCreateOpts(params, envResult.value);
6032
6141
  const containerResult = await this.runtime.createContainer(createOpts);
6033
6142
  if (!containerResult.ok) {
6034
- return Err15(
6143
+ return Err16(
6035
6144
  toAgentError(
6036
6145
  `Container creation failed: ${containerResult.error.message}`,
6037
6146
  containerResult.error
@@ -6056,7 +6165,7 @@ var ContainerBackend = class {
6056
6165
  this.containerHandles.delete(session.sessionId);
6057
6166
  const removeResult = await this.runtime.removeContainer(handle);
6058
6167
  if (!removeResult.ok) {
6059
- return Err15(
6168
+ return Err16(
6060
6169
  toAgentError(
6061
6170
  `Container removal failed: ${removeResult.error.message}`,
6062
6171
  removeResult.error
@@ -6069,7 +6178,7 @@ var ContainerBackend = class {
6069
6178
  async healthCheck() {
6070
6179
  const runtimeResult = await this.runtime.healthCheck();
6071
6180
  if (!runtimeResult.ok) {
6072
- return Err15({
6181
+ return Err16({
6073
6182
  category: "agent_not_found",
6074
6183
  message: `Container runtime unhealthy: ${runtimeResult.error.message}`,
6075
6184
  details: runtimeResult.error
@@ -6081,7 +6190,7 @@ var ContainerBackend = class {
6081
6190
 
6082
6191
  // src/agent/runtime/docker.ts
6083
6192
  import { execFile as execFile3, spawn as spawn5 } from "child_process";
6084
- import { Ok as Ok18, Err as Err16 } from "@harness-engineering/types";
6193
+ import { Ok as Ok18, Err as Err17 } from "@harness-engineering/types";
6085
6194
  function dockerExec(args) {
6086
6195
  return new Promise((resolve8, reject) => {
6087
6196
  execFile3("docker", args, (error, stdout) => {
@@ -6116,7 +6225,7 @@ var DockerRuntime = class {
6116
6225
  const containerId = await dockerExec(args);
6117
6226
  return Ok18({ containerId, runtime: this.name });
6118
6227
  } catch (error) {
6119
- return Err16({
6228
+ return Err17({
6120
6229
  category: "container_create_failed",
6121
6230
  message: `Failed to create container: ${error instanceof Error ? error.message : String(error)}`,
6122
6231
  details: error
@@ -6162,7 +6271,7 @@ var DockerRuntime = class {
6162
6271
  await dockerExec(["rm", "-f", handle.containerId]);
6163
6272
  return Ok18(void 0);
6164
6273
  } catch (error) {
6165
- return Err16({
6274
+ return Err17({
6166
6275
  category: "container_remove_failed",
6167
6276
  message: `Failed to remove container: ${error instanceof Error ? error.message : String(error)}`,
6168
6277
  details: error
@@ -6174,7 +6283,7 @@ var DockerRuntime = class {
6174
6283
  await dockerExec(["info", "--format", "{{.ServerVersion}}"]);
6175
6284
  return Ok18(void 0);
6176
6285
  } catch (error) {
6177
- return Err16({
6286
+ return Err17({
6178
6287
  category: "runtime_not_found",
6179
6288
  message: `Docker is not available: ${error instanceof Error ? error.message : String(error)}`,
6180
6289
  details: error
@@ -6184,7 +6293,7 @@ var DockerRuntime = class {
6184
6293
  };
6185
6294
 
6186
6295
  // src/agent/secrets/env.ts
6187
- import { Ok as Ok19, Err as Err17 } from "@harness-engineering/types";
6296
+ import { Ok as Ok19, Err as Err18 } from "@harness-engineering/types";
6188
6297
  var EnvSecretBackend = class {
6189
6298
  name = "env";
6190
6299
  async resolveSecrets(keys) {
@@ -6192,7 +6301,7 @@ var EnvSecretBackend = class {
6192
6301
  for (const key of keys) {
6193
6302
  const value = process.env[key];
6194
6303
  if (value === void 0) {
6195
- return Err17({
6304
+ return Err18({
6196
6305
  category: "secret_not_found",
6197
6306
  message: `Environment variable '${key}' is not set`,
6198
6307
  key
@@ -6209,7 +6318,7 @@ var EnvSecretBackend = class {
6209
6318
 
6210
6319
  // src/agent/secrets/onepassword.ts
6211
6320
  import { execFile as execFile4 } from "child_process";
6212
- import { Ok as Ok20, Err as Err18 } from "@harness-engineering/types";
6321
+ import { Ok as Ok20, Err as Err19 } from "@harness-engineering/types";
6213
6322
  function opExec(args) {
6214
6323
  return new Promise((resolve8, reject) => {
6215
6324
  execFile4("op", args, (error, stdout) => {
@@ -6234,7 +6343,7 @@ var OnePasswordSecretBackend = class {
6234
6343
  const value = await opExec(["read", `op://${this.vault}/${key}/password`]);
6235
6344
  secrets[key] = value;
6236
6345
  } catch (error) {
6237
- return Err18({
6346
+ return Err19({
6238
6347
  category: "access_denied",
6239
6348
  message: `Failed to read secret '${key}' from 1Password: ${error instanceof Error ? error.message : String(error)}`,
6240
6349
  key
@@ -6248,7 +6357,7 @@ var OnePasswordSecretBackend = class {
6248
6357
  await opExec(["--version"]);
6249
6358
  return Ok20(void 0);
6250
6359
  } catch (error) {
6251
- return Err18({
6360
+ return Err19({
6252
6361
  category: "provider_unavailable",
6253
6362
  message: `1Password CLI is not available: ${error instanceof Error ? error.message : String(error)}`
6254
6363
  });
@@ -6258,7 +6367,7 @@ var OnePasswordSecretBackend = class {
6258
6367
 
6259
6368
  // src/agent/secrets/vault.ts
6260
6369
  import { execFile as execFile5 } from "child_process";
6261
- import { Ok as Ok21, Err as Err19 } from "@harness-engineering/types";
6370
+ import { Ok as Ok21, Err as Err20 } from "@harness-engineering/types";
6262
6371
  function vaultExec(args, env) {
6263
6372
  return new Promise((resolve8, reject) => {
6264
6373
  execFile5("vault", args, { env: { ...process.env, ...env } }, (error, stdout) => {
@@ -6289,11 +6398,11 @@ var VaultSecretBackend = class {
6289
6398
  } catch (error) {
6290
6399
  const msg = error instanceof Error ? error.message : String(error);
6291
6400
  const category = error instanceof SyntaxError ? "access_denied" : "access_denied";
6292
- return Err19({ category, message: `Failed to read from Vault: ${msg}` });
6401
+ return Err20({ category, message: `Failed to read from Vault: ${msg}` });
6293
6402
  }
6294
6403
  const missing = keys.find((k) => !(k in data));
6295
6404
  if (missing) {
6296
- return Err19({
6405
+ return Err20({
6297
6406
  category: "secret_not_found",
6298
6407
  message: `Secret key '${missing}' not found in Vault path '${this.path}'`,
6299
6408
  key: missing
@@ -6308,7 +6417,7 @@ var VaultSecretBackend = class {
6308
6417
  await vaultExec(["version"]);
6309
6418
  return Ok21(void 0);
6310
6419
  } catch (error) {
6311
- return Err19({
6420
+ return Err20({
6312
6421
  category: "provider_unavailable",
6313
6422
  message: `Vault CLI is not available: ${error instanceof Error ? error.message : String(error)}`
6314
6423
  });
@@ -6437,6 +6546,96 @@ var OrchestratorBackendFactory = class {
6437
6546
  }
6438
6547
  };
6439
6548
 
6549
+ // src/agent/backend-resolver.ts
6550
+ function makeBackendResolver(backends) {
6551
+ return (backendName) => {
6552
+ const def = backends?.[backendName];
6553
+ return def ? createBackend(def) : null;
6554
+ };
6555
+ }
6556
+
6557
+ // src/maintenance/agent-dispatcher.ts
6558
+ function syntheticIssue(branch, skill) {
6559
+ return {
6560
+ id: `maintenance:${branch}`,
6561
+ identifier: branch,
6562
+ title: `Maintenance: ${skill}`,
6563
+ description: null,
6564
+ priority: null,
6565
+ state: "in_progress",
6566
+ branchName: branch,
6567
+ url: null,
6568
+ labels: ["maintenance"],
6569
+ blockedBy: [],
6570
+ spec: null,
6571
+ plans: [],
6572
+ createdAt: null,
6573
+ updatedAt: null,
6574
+ externalId: null
6575
+ };
6576
+ }
6577
+ function buildPrompt(skill, promptContext) {
6578
+ 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.`;
6579
+ return promptContext ? `${promptContext}
6580
+
6581
+ ${instruction}` : instruction;
6582
+ }
6583
+ function headOf(git2, cwd) {
6584
+ try {
6585
+ return git2(["rev-parse", "HEAD"], cwd) || null;
6586
+ } catch {
6587
+ return null;
6588
+ }
6589
+ }
6590
+ function createAgentDispatcher(deps) {
6591
+ const maxTurns = deps.maxTurns ?? 10;
6592
+ const dispatch = async (skill, branch, backendName, cwd, options) => {
6593
+ const backend = deps.resolveBackend(backendName);
6594
+ if (!backend) {
6595
+ deps.logger.warn(`Maintenance agent dispatch skipped: unknown backend "${backendName}"`, {
6596
+ skill,
6597
+ branch
6598
+ });
6599
+ return { producedCommits: false, fixed: 0 };
6600
+ }
6601
+ const before = headOf(deps.git, cwd);
6602
+ const runner = new AgentRunner(backend, { maxTurns });
6603
+ const session = runner.runSession(
6604
+ syntheticIssue(branch, skill),
6605
+ cwd,
6606
+ buildPrompt(skill, options?.promptContext)
6607
+ );
6608
+ let step = await session.next();
6609
+ while (!step.done) step = await session.next();
6610
+ const after = headOf(deps.git, cwd);
6611
+ let fixed = 0;
6612
+ if (after && after !== before) {
6613
+ if (before) {
6614
+ try {
6615
+ fixed = parseInt(deps.git(["rev-list", "--count", `${before}..${after}`], cwd), 10) || 0;
6616
+ } catch {
6617
+ fixed = 1;
6618
+ }
6619
+ } else {
6620
+ fixed = 1;
6621
+ }
6622
+ }
6623
+ const producedCommits = fixed > 0;
6624
+ deps.logger.info("Maintenance agent dispatch complete", {
6625
+ skill,
6626
+ branch,
6627
+ backendName,
6628
+ producedCommits,
6629
+ fixed
6630
+ });
6631
+ return { producedCommits, fixed };
6632
+ };
6633
+ return { dispatch };
6634
+ }
6635
+
6636
+ // src/orchestrator.ts
6637
+ import { execFileSync } from "child_process";
6638
+
6440
6639
  // src/agent/intelligence-factory.ts
6441
6640
  import {
6442
6641
  IntelligencePipeline,
@@ -6947,7 +7146,7 @@ function handleV1InteractionsResolveRoute(req, res, queue) {
6947
7146
 
6948
7147
  // src/server/routes/plans.ts
6949
7148
  import { z as z5 } from "zod";
6950
- import * as fs10 from "fs/promises";
7149
+ import * as fs9 from "fs/promises";
6951
7150
  import * as path11 from "path";
6952
7151
  var PlanWriteSchema = z5.object({
6953
7152
  filename: z5.string().min(1),
@@ -6976,9 +7175,9 @@ function handlePlansRoute(req, res, plansDir) {
6976
7175
  );
6977
7176
  return;
6978
7177
  }
6979
- await fs10.mkdir(plansDir, { recursive: true });
7178
+ await fs9.mkdir(plansDir, { recursive: true });
6980
7179
  const filePath = path11.join(plansDir, basename3);
6981
- await fs10.writeFile(filePath, parsed.content, "utf-8");
7180
+ await fs9.writeFile(filePath, parsed.content, "utf-8");
6982
7181
  res.writeHead(201, { "Content-Type": "application/json" });
6983
7182
  res.end(JSON.stringify({ ok: true, filename: basename3 }));
6984
7183
  } catch {
@@ -7353,11 +7552,10 @@ function handleAnalyzeRoute(req, res, pipeline) {
7353
7552
  }
7354
7553
 
7355
7554
  // src/server/routes/roadmap-actions.ts
7356
- import * as fs11 from "fs/promises";
7357
7555
  import * as path12 from "path";
7358
7556
  import {
7359
- parseRoadmap as parseRoadmap2,
7360
- serializeRoadmap as serializeRoadmap2,
7557
+ resolveRoadmapStoreForFile as resolveRoadmapStoreForFile2,
7558
+ applyRoadmapDiff as applyRoadmapDiff2,
7361
7559
  loadProjectRoadmapMode,
7362
7560
  loadTrackerClientConfigFromProject,
7363
7561
  createTrackerClient,
@@ -7443,13 +7641,14 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
7443
7641
  sendJSON2(res, 400, { error: "Title must not contain newlines or markdown headings" });
7444
7642
  return;
7445
7643
  }
7446
- const content = await fs11.readFile(roadmapPath, "utf-8");
7447
- const roadmapResult = parseRoadmap2(content);
7448
- if (!roadmapResult.ok) {
7644
+ const store = resolveRoadmapStoreForFile2({ roadmapPath });
7645
+ const loaded = await store.load();
7646
+ if (!loaded.ok) {
7449
7647
  sendJSON2(res, 500, { error: "Failed to parse roadmap file" });
7450
7648
  return;
7451
7649
  }
7452
- const roadmap = roadmapResult.value;
7650
+ const roadmap = loaded.value;
7651
+ const before = structuredClone(roadmap);
7453
7652
  let backlog = roadmap.milestones.find((m) => m.isBacklog);
7454
7653
  if (!backlog) {
7455
7654
  backlog = { name: "Backlog", isBacklog: true, features: [] };
@@ -7472,10 +7671,11 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
7472
7671
  updatedAt: null
7473
7672
  });
7474
7673
  roadmap.frontmatter.lastManualEdit = (/* @__PURE__ */ new Date()).toISOString();
7475
- const tmpPath = roadmapPath + ".tmp";
7476
- const serialized = serializeRoadmap2(roadmap);
7477
- await fs11.writeFile(tmpPath, serialized, "utf-8");
7478
- await fs11.rename(tmpPath, roadmapPath);
7674
+ const persisted = await applyRoadmapDiff2(store, before, roadmap);
7675
+ if (!persisted.ok) {
7676
+ sendJSON2(res, 500, { error: persisted.error.message });
7677
+ return;
7678
+ }
7479
7679
  sendJSON2(res, 201, { ok: true, featureName: parsed.title });
7480
7680
  } catch (err) {
7481
7681
  const msg = err instanceof Error ? err.message : "Failed to append to roadmap";
@@ -7741,7 +7941,6 @@ function handleV1JobsMaintenanceRoute(req, res, deps) {
7741
7941
  }
7742
7942
 
7743
7943
  // src/server/routes/v1/events-sse.ts
7744
- import { randomBytes as randomBytes2 } from "crypto";
7745
7944
  var SSE_TOPICS = [
7746
7945
  "state_change",
7747
7946
  "agent_event",
@@ -7757,8 +7956,55 @@ var SSE_TOPICS = [
7757
7956
  "webhook.subscription.deleted"
7758
7957
  ];
7759
7958
  var HEARTBEAT_MS = 15e3;
7760
- function newEventId() {
7761
- return `evt_${randomBytes2(8).toString("hex")}`;
7959
+ var DEFAULT_REPLAY_BUFFER = 1024;
7960
+ var SseEventLog = class {
7961
+ seq = 0;
7962
+ buffer = [];
7963
+ subscribers = /* @__PURE__ */ new Set();
7964
+ constructor(bus, cap = DEFAULT_REPLAY_BUFFER) {
7965
+ this.cap = cap;
7966
+ for (const topic of SSE_TOPICS) {
7967
+ bus.on(topic, (data) => this.record(topic, data));
7968
+ }
7969
+ }
7970
+ cap;
7971
+ record(topic, data) {
7972
+ const event = { id: ++this.seq, topic, data };
7973
+ this.buffer.push(event);
7974
+ if (this.buffer.length > this.cap) this.buffer.shift();
7975
+ for (const fn of this.subscribers) fn(event);
7976
+ }
7977
+ /** Highest assigned id (0 when nothing has been recorded yet). */
7978
+ currentSeq() {
7979
+ return this.seq;
7980
+ }
7981
+ /** Buffered events strictly newer than `lastId`, oldest-first. */
7982
+ replayFrom(lastId) {
7983
+ return this.buffer.filter((e) => e.id > lastId);
7984
+ }
7985
+ /** Register a live listener; returns an unsubscribe function. */
7986
+ subscribe(fn) {
7987
+ this.subscribers.add(fn);
7988
+ return () => {
7989
+ this.subscribers.delete(fn);
7990
+ };
7991
+ }
7992
+ };
7993
+ var logsByBus = /* @__PURE__ */ new WeakMap();
7994
+ function getSseEventLog(bus) {
7995
+ let log = logsByBus.get(bus);
7996
+ if (!log) {
7997
+ log = new SseEventLog(bus);
7998
+ logsByBus.set(bus, log);
7999
+ }
8000
+ return log;
8001
+ }
8002
+ function parseLastEventId(req) {
8003
+ const raw = req.headers["last-event-id"];
8004
+ const value = Array.isArray(raw) ? raw[0] : raw;
8005
+ if (value === void 0) return null;
8006
+ const n = Number(value);
8007
+ return Number.isInteger(n) && n >= 0 ? n : null;
7762
8008
  }
7763
8009
  function handleV1EventsSseRoute(req, res, bus) {
7764
8010
  if (req.method !== "GET" || req.url !== "/api/v1/events") return false;
@@ -7770,22 +8016,28 @@ function handleV1EventsSseRoute(req, res, bus) {
7770
8016
  res.write(`: harness gateway SSE \u2014 connected at ${(/* @__PURE__ */ new Date()).toISOString()}
7771
8017
 
7772
8018
  `);
7773
- const listeners = [];
7774
- for (const topic of SSE_TOPICS) {
7775
- const fn = (data) => {
7776
- try {
7777
- const frame = `event: ${topic}
7778
- data: ${JSON.stringify(data)}
7779
- id: ${newEventId()}
8019
+ const log = getSseEventLog(bus);
8020
+ const send = (e) => {
8021
+ try {
8022
+ res.write(`event: ${e.topic}
8023
+ data: ${JSON.stringify(e.data)}
8024
+ id: ${e.id}
7780
8025
 
7781
- `;
7782
- res.write(frame);
7783
- } catch {
7784
- }
7785
- };
7786
- bus.on(topic, fn);
7787
- listeners.push({ topic, fn });
8026
+ `);
8027
+ } catch {
8028
+ }
8029
+ };
8030
+ const lastId = parseLastEventId(req);
8031
+ let replayedThrough = lastId ?? log.currentSeq();
8032
+ if (lastId !== null) {
8033
+ for (const e of log.replayFrom(lastId)) {
8034
+ send(e);
8035
+ replayedThrough = e.id;
8036
+ }
7788
8037
  }
8038
+ const unsubscribe = log.subscribe((e) => {
8039
+ if (e.id > replayedThrough) send(e);
8040
+ });
7789
8041
  const heartbeat = setInterval(() => {
7790
8042
  try {
7791
8043
  res.write(": heartbeat\n\n");
@@ -7795,7 +8047,7 @@ id: ${newEventId()}
7795
8047
  heartbeat.unref();
7796
8048
  const cleanup = () => {
7797
8049
  clearInterval(heartbeat);
7798
- for (const { topic, fn } of listeners) bus.removeListener(topic, fn);
8050
+ unsubscribe();
7799
8051
  };
7800
8052
  res.on("close", cleanup);
7801
8053
  res.on("finish", cleanup);
@@ -8115,7 +8367,7 @@ async function runGate(projectPath, proposalId) {
8115
8367
  }
8116
8368
 
8117
8369
  // src/proposals/promote.ts
8118
- import * as fs12 from "fs";
8370
+ import * as fs10 from "fs";
8119
8371
  import * as path13 from "path";
8120
8372
  import { parse as parseYaml3, stringify as stringifyYaml } from "yaml";
8121
8373
  import {
@@ -8141,7 +8393,7 @@ function skillDir(projectPath, name) {
8141
8393
  }
8142
8394
  function readIfExists(p) {
8143
8395
  try {
8144
- return fs12.readFileSync(p, "utf-8");
8396
+ return fs10.readFileSync(p, "utf-8");
8145
8397
  } catch {
8146
8398
  return null;
8147
8399
  }
@@ -8187,15 +8439,15 @@ function assertGateReady(proposal) {
8187
8439
  }
8188
8440
  async function promoteNewSkill(projectPath, proposal) {
8189
8441
  const target = skillDir(projectPath, proposal.content.name);
8190
- if (fs12.existsSync(target)) {
8442
+ if (fs10.existsSync(target)) {
8191
8443
  throw new PromotionError(
8192
8444
  `a catalog skill already exists at ${target}; use a refinement proposal to update it`
8193
8445
  );
8194
8446
  }
8195
- fs12.mkdirSync(target, { recursive: true });
8447
+ fs10.mkdirSync(target, { recursive: true });
8196
8448
  const yamlOut = injectProvenanceIntoYaml(proposal.content.skillYaml ?? "", proposal.id);
8197
- fs12.writeFileSync(path13.join(target, "skill.yaml"), yamlOut);
8198
- fs12.writeFileSync(path13.join(target, "SKILL.md"), proposal.content.skillMd ?? "");
8449
+ fs10.writeFileSync(path13.join(target, "skill.yaml"), yamlOut);
8450
+ fs10.writeFileSync(path13.join(target, "SKILL.md"), proposal.content.skillMd ?? "");
8199
8451
  return { skillPath: target };
8200
8452
  }
8201
8453
  async function promoteRefinement(projectPath, proposal) {
@@ -8203,7 +8455,7 @@ async function promoteRefinement(projectPath, proposal) {
8203
8455
  throw new PromotionError("refinement proposal is missing targetSkill");
8204
8456
  }
8205
8457
  const target = skillDir(projectPath, proposal.targetSkill);
8206
- if (!fs12.existsSync(target)) {
8458
+ if (!fs10.existsSync(target)) {
8207
8459
  throw new PromotionError(
8208
8460
  `target skill ${proposal.targetSkill} does not exist at ${target}; cannot refine`
8209
8461
  );
@@ -8216,7 +8468,7 @@ async function promoteRefinement(projectPath, proposal) {
8216
8468
  "no metadata changes detected; check that the reviewer applied the proposed diff before approving"
8217
8469
  );
8218
8470
  }
8219
- fs12.writeFileSync(yamlPath, after);
8471
+ fs10.writeFileSync(yamlPath, after);
8220
8472
  return { skillPath: target };
8221
8473
  }
8222
8474
  async function promote(projectPath, proposalId, decidedBy) {
@@ -8654,7 +8906,7 @@ function handleV1RoutingRoute(req, res, deps) {
8654
8906
  }
8655
8907
 
8656
8908
  // src/server/routes/sessions.ts
8657
- import * as fs13 from "fs/promises";
8909
+ import * as fs11 from "fs/promises";
8658
8910
  import * as path14 from "path";
8659
8911
  import { z as z15 } from "zod";
8660
8912
  var SessionCreateSchema = z15.object({
@@ -8675,12 +8927,12 @@ function extractSessionId(url) {
8675
8927
  }
8676
8928
  async function handleList2(res, sessionsDir) {
8677
8929
  try {
8678
- const entries = await fs13.readdir(sessionsDir, { withFileTypes: true });
8930
+ const entries = await fs11.readdir(sessionsDir, { withFileTypes: true });
8679
8931
  const sessions = [];
8680
8932
  for (const entry of entries) {
8681
8933
  if (!entry.isDirectory()) continue;
8682
8934
  try {
8683
- const content = await fs13.readFile(
8935
+ const content = await fs11.readFile(
8684
8936
  path14.join(sessionsDir, entry.name, "session.json"),
8685
8937
  "utf-8"
8686
8938
  );
@@ -8706,7 +8958,7 @@ async function handleGet2(res, id, sessionsDir) {
8706
8958
  return;
8707
8959
  }
8708
8960
  try {
8709
- const content = await fs13.readFile(path14.join(sessionsDir, id, "session.json"), "utf-8");
8961
+ const content = await fs11.readFile(path14.join(sessionsDir, id, "session.json"), "utf-8");
8710
8962
  jsonResponse(res, 200, JSON.parse(content));
8711
8963
  } catch (err) {
8712
8964
  if (err.code === "ENOENT") {
@@ -8730,8 +8982,8 @@ async function handleCreate(req, res, sessionsDir) {
8730
8982
  return;
8731
8983
  }
8732
8984
  const sessionDir = path14.join(sessionsDir, session.sessionId);
8733
- await fs13.mkdir(sessionDir, { recursive: true });
8734
- await fs13.writeFile(path14.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
8985
+ await fs11.mkdir(sessionDir, { recursive: true });
8986
+ await fs11.writeFile(path14.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
8735
8987
  jsonResponse(res, 200, { ok: true });
8736
8988
  } catch {
8737
8989
  jsonResponse(res, 500, { error: "Failed to save session" });
@@ -8747,8 +8999,8 @@ async function handleUpdate(req, res, url, sessionsDir) {
8747
8999
  const body = await readBody(req);
8748
9000
  const updates = z15.record(z15.unknown()).parse(JSON.parse(body));
8749
9001
  const sessionFilePath = path14.join(sessionsDir, id, "session.json");
8750
- const current = JSON.parse(await fs13.readFile(sessionFilePath, "utf-8"));
8751
- await fs13.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
9002
+ const current = JSON.parse(await fs11.readFile(sessionFilePath, "utf-8"));
9003
+ await fs11.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
8752
9004
  jsonResponse(res, 200, { ok: true });
8753
9005
  } catch {
8754
9006
  jsonResponse(res, 500, { error: "Failed to update session" });
@@ -8761,7 +9013,7 @@ async function handleDelete(res, url, sessionsDir) {
8761
9013
  jsonResponse(res, 400, { error: "Missing or invalid sessionId" });
8762
9014
  return;
8763
9015
  }
8764
- await fs13.rm(path14.join(sessionsDir, id), { recursive: true, force: true });
9016
+ await fs11.rm(path14.join(sessionsDir, id), { recursive: true, force: true });
8765
9017
  jsonResponse(res, 200, { ok: true });
8766
9018
  } catch {
8767
9019
  jsonResponse(res, 500, { error: "Failed to delete session" });
@@ -9009,7 +9261,7 @@ function handleLocalModelsRoute(req, res, getStatuses) {
9009
9261
  }
9010
9262
 
9011
9263
  // src/server/static.ts
9012
- import * as fs14 from "fs";
9264
+ import * as fs12 from "fs";
9013
9265
  import * as path15 from "path";
9014
9266
  var MIME_TYPES = {
9015
9267
  ".html": "text/html; charset=utf-8",
@@ -9039,11 +9291,11 @@ function handleStaticFile(req, res, dashboardDir) {
9039
9291
  if (!resolved.startsWith(path15.resolve(dashboardDir))) {
9040
9292
  return serveFile(path15.join(dashboardDir, "index.html"), res);
9041
9293
  }
9042
- if (fs14.existsSync(resolved) && fs14.statSync(resolved).isFile()) {
9294
+ if (fs12.existsSync(resolved) && fs12.statSync(resolved).isFile()) {
9043
9295
  return serveFile(resolved, res);
9044
9296
  }
9045
9297
  const indexPath = path15.join(dashboardDir, "index.html");
9046
- if (fs14.existsSync(indexPath)) {
9298
+ if (fs12.existsSync(indexPath)) {
9047
9299
  return serveFile(indexPath, res);
9048
9300
  }
9049
9301
  return false;
@@ -9052,7 +9304,7 @@ function serveFile(filePath, res) {
9052
9304
  const ext = path15.extname(filePath).toLowerCase();
9053
9305
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
9054
9306
  try {
9055
- const content = fs14.readFileSync(filePath);
9307
+ const content = fs12.readFileSync(filePath);
9056
9308
  res.writeHead(200, { "Content-Type": contentType });
9057
9309
  res.end(content);
9058
9310
  return true;
@@ -9062,7 +9314,7 @@ function serveFile(filePath, res) {
9062
9314
  }
9063
9315
 
9064
9316
  // src/server/plan-watcher.ts
9065
- import * as fs15 from "fs";
9317
+ import * as fs13 from "fs";
9066
9318
  import * as path16 from "path";
9067
9319
  var PlanWatcher = class {
9068
9320
  plansDir;
@@ -9077,11 +9329,11 @@ var PlanWatcher = class {
9077
9329
  * Creates the directory if it does not exist.
9078
9330
  */
9079
9331
  start() {
9080
- fs15.mkdirSync(this.plansDir, { recursive: true });
9081
- this.watcher = fs15.watch(this.plansDir, (eventType, filename) => {
9332
+ fs13.mkdirSync(this.plansDir, { recursive: true });
9333
+ this.watcher = fs13.watch(this.plansDir, (eventType, filename) => {
9082
9334
  if (eventType === "rename" && filename && filename.endsWith(".md")) {
9083
9335
  const filePath = path16.join(this.plansDir, filename);
9084
- if (fs15.existsSync(filePath)) {
9336
+ if (fs13.existsSync(filePath)) {
9085
9337
  void this.handleNewPlan(filename);
9086
9338
  }
9087
9339
  }
@@ -9112,8 +9364,8 @@ var PlanWatcher = class {
9112
9364
  };
9113
9365
 
9114
9366
  // src/auth/tokens.ts
9115
- import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
9116
- import { readFile as readFile8, writeFile as writeFile8, mkdir as mkdir7, rename as rename2 } from "fs/promises";
9367
+ import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
9368
+ import { readFile as readFile6, writeFile as writeFile6, mkdir as mkdir7, rename } from "fs/promises";
9117
9369
  import { dirname as dirname5 } from "path";
9118
9370
  import bcrypt from "bcryptjs";
9119
9371
  import {
@@ -9123,10 +9375,10 @@ import {
9123
9375
  var BCRYPT_ROUNDS = 12;
9124
9376
  var LEGACY_ENV_ID = "tok_legacy_env";
9125
9377
  function genId() {
9126
- return `tok_${randomBytes3(8).toString("hex")}`;
9378
+ return `tok_${randomBytes2(8).toString("hex")}`;
9127
9379
  }
9128
9380
  function genSecret() {
9129
- return randomBytes3(24).toString("base64url");
9381
+ return randomBytes2(24).toString("base64url");
9130
9382
  }
9131
9383
  function parseToken(raw) {
9132
9384
  const dot = raw.indexOf(".");
@@ -9142,7 +9394,7 @@ var TokenStore = class {
9142
9394
  async load() {
9143
9395
  if (this.cache) return this.cache;
9144
9396
  try {
9145
- const raw = await readFile8(this.path, "utf8");
9397
+ const raw = await readFile6(this.path, "utf8");
9146
9398
  const parsed = JSON.parse(raw);
9147
9399
  const list = Array.isArray(parsed) ? parsed : [];
9148
9400
  this.cache = list.map((entry) => {
@@ -9157,9 +9409,9 @@ var TokenStore = class {
9157
9409
  }
9158
9410
  async persist(records) {
9159
9411
  await mkdir7(dirname5(this.path), { recursive: true });
9160
- const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${randomBytes3(4).toString("hex")}`;
9161
- await writeFile8(tmp, JSON.stringify(records, null, 2), "utf8");
9162
- await rename2(tmp, this.path);
9412
+ const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${randomBytes2(4).toString("hex")}`;
9413
+ await writeFile6(tmp, JSON.stringify(records, null, 2), "utf8");
9414
+ await rename(tmp, this.path);
9163
9415
  this.cache = records;
9164
9416
  }
9165
9417
  async create(input) {
@@ -9409,26 +9661,41 @@ function hasScope(held, required) {
9409
9661
  if (held.includes("admin")) return true;
9410
9662
  return held.includes(required);
9411
9663
  }
9412
- function requiredScopeForRoute(method, path24) {
9413
- const bridgeScope = requiredBridgeScope(method, path24);
9414
- if (bridgeScope) return bridgeScope;
9664
+ function exactScopeForRoute(method, path24) {
9415
9665
  if (path24 === "/api/v1/auth/token" && method === "POST") return "admin";
9416
9666
  if (path24 === "/api/v1/auth/tokens" && method === "GET") return "admin";
9417
9667
  if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path24) && method === "DELETE") return "admin";
9418
9668
  if ((path24 === "/api/state" || path24 === "/api/v1/state") && method === "GET") return "read-status";
9419
- if (path24.startsWith("/api/interactions")) return "resolve-interaction";
9420
- if (path24.startsWith("/api/plans")) return "read-status";
9421
- if (path24.startsWith("/api/analyze") || path24.startsWith("/api/analyses")) return "read-status";
9422
- if (path24.startsWith("/api/roadmap-actions")) return "modify-roadmap";
9423
- if (path24.startsWith("/api/dispatch-actions")) return "trigger-job";
9424
- if (path24.startsWith("/api/local-model") || path24.startsWith("/api/local-models"))
9425
- return "read-status";
9426
- if (path24.startsWith("/api/maintenance")) return "trigger-job";
9427
- if (path24.startsWith("/api/streams")) return "read-status";
9428
- if (path24.startsWith("/api/sessions")) return "read-status";
9429
- if (path24 === "/api/chat" || path24.startsWith("/api/chat-proxy")) return "trigger-job";
9430
9669
  return null;
9431
9670
  }
9671
+ var PREFIX_SCOPES = [
9672
+ ["/api/interactions", "resolve-interaction"],
9673
+ ["/api/plans", "read-status"],
9674
+ ["/api/analyze", "read-status"],
9675
+ ["/api/analyses", "read-status"],
9676
+ ["/api/roadmap-actions", "modify-roadmap"],
9677
+ ["/api/dispatch-actions", "trigger-job"],
9678
+ ["/api/local-model", "read-status"],
9679
+ ["/api/local-models", "read-status"],
9680
+ ["/api/maintenance", "trigger-job"],
9681
+ ["/api/streams", "read-status"],
9682
+ ["/api/sessions", "read-status"],
9683
+ ["/api/chat-proxy", "trigger-job"]
9684
+ ];
9685
+ function prefixScopeForPath(path24) {
9686
+ if (path24 === "/api/chat") return "trigger-job";
9687
+ for (const [prefix, scope] of PREFIX_SCOPES) {
9688
+ if (path24.startsWith(prefix)) return scope;
9689
+ }
9690
+ return null;
9691
+ }
9692
+ function requiredScopeForRoute(method, path24) {
9693
+ const bridgeScope = requiredBridgeScope(method, path24);
9694
+ if (bridgeScope) return bridgeScope;
9695
+ const exactScope = exactScopeForRoute(method, path24);
9696
+ if (exactScope) return exactScope;
9697
+ return prefixScopeForPath(path24);
9698
+ }
9432
9699
 
9433
9700
  // src/server/http.ts
9434
9701
  var RATE_LIMIT = Number(process.env["HARNESS_RATE_LIMIT"]) || 100;
@@ -9843,8 +10110,8 @@ var OrchestratorServer = class {
9843
10110
  };
9844
10111
 
9845
10112
  // src/gateway/webhooks/store.ts
9846
- import { randomBytes as randomBytes4 } from "crypto";
9847
- import { readFile as readFile9, writeFile as writeFile9, mkdir as mkdir9, rename as rename3, chmod } from "fs/promises";
10113
+ import { randomBytes as randomBytes3 } from "crypto";
10114
+ import { readFile as readFile7, writeFile as writeFile7, mkdir as mkdir9, rename as rename2, chmod } from "fs/promises";
9848
10115
  import { dirname as dirname7 } from "path";
9849
10116
  import { WebhookSubscriptionSchema } from "@harness-engineering/types";
9850
10117
 
@@ -9870,10 +10137,10 @@ function eventMatches(pattern, type) {
9870
10137
 
9871
10138
  // src/gateway/webhooks/store.ts
9872
10139
  function genId2() {
9873
- return `whk_${randomBytes4(8).toString("hex")}`;
10140
+ return `whk_${randomBytes3(8).toString("hex")}`;
9874
10141
  }
9875
10142
  function genSecret2() {
9876
- return randomBytes4(32).toString("base64url");
10143
+ return randomBytes3(32).toString("base64url");
9877
10144
  }
9878
10145
  var WebhookStore = class {
9879
10146
  constructor(path24) {
@@ -9884,7 +10151,7 @@ var WebhookStore = class {
9884
10151
  async load() {
9885
10152
  if (this.cache) return this.cache;
9886
10153
  try {
9887
- const raw = await readFile9(this.path, "utf8");
10154
+ const raw = await readFile7(this.path, "utf8");
9888
10155
  const parsed = JSON.parse(raw);
9889
10156
  const list = Array.isArray(parsed) ? parsed : [];
9890
10157
  this.cache = list.map((entry) => {
@@ -9898,11 +10165,11 @@ var WebhookStore = class {
9898
10165
  return this.cache;
9899
10166
  }
9900
10167
  async persist(records) {
9901
- const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${randomBytes4(4).toString("hex")}`;
10168
+ const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${randomBytes3(4).toString("hex")}`;
9902
10169
  try {
9903
10170
  await mkdir9(dirname7(this.path), { recursive: true });
9904
- await writeFile9(tmp, JSON.stringify(records, null, 2), { encoding: "utf8", mode: 384 });
9905
- await rename3(tmp, this.path);
10171
+ await writeFile7(tmp, JSON.stringify(records, null, 2), { encoding: "utf8", mode: 384 });
10172
+ await rename2(tmp, this.path);
9906
10173
  await chmod(this.path, 384);
9907
10174
  } catch (err) {
9908
10175
  if (err.code !== "ENOENT") throw err;
@@ -9942,7 +10209,7 @@ var WebhookStore = class {
9942
10209
  };
9943
10210
 
9944
10211
  // src/gateway/webhooks/delivery.ts
9945
- import { randomBytes as randomBytes5 } from "crypto";
10212
+ import { randomBytes as randomBytes4 } from "crypto";
9946
10213
 
9947
10214
  // src/gateway/webhooks/queue.ts
9948
10215
  import Database from "better-sqlite3";
@@ -10152,7 +10419,7 @@ var WebhookDelivery = class {
10152
10419
  enqueue(sub, event) {
10153
10420
  const payload = JSON.stringify(event);
10154
10421
  this.queue.insert({
10155
- id: `dlv_${randomBytes5(8).toString("hex")}`,
10422
+ id: `dlv_${randomBytes4(8).toString("hex")}`,
10156
10423
  subscriptionId: sub.id,
10157
10424
  eventType: event.type,
10158
10425
  payload
@@ -10260,7 +10527,7 @@ var WebhookDelivery = class {
10260
10527
  };
10261
10528
 
10262
10529
  // src/gateway/webhooks/events.ts
10263
- import { randomBytes as randomBytes6 } from "crypto";
10530
+ import { randomBytes as randomBytes5 } from "crypto";
10264
10531
  var WEBHOOK_TOPICS = [
10265
10532
  "interaction.created",
10266
10533
  "interaction.resolved",
@@ -10275,8 +10542,8 @@ var WEBHOOK_TOPICS = [
10275
10542
  "proposal.approved",
10276
10543
  "proposal.rejected"
10277
10544
  ];
10278
- function newEventId2() {
10279
- return `evt_${randomBytes6(8).toString("hex")}`;
10545
+ function newEventId() {
10546
+ return `evt_${randomBytes5(8).toString("hex")}`;
10280
10547
  }
10281
10548
  function wireWebhookFanout({ bus, store, delivery }) {
10282
10549
  const handlers = [];
@@ -10287,7 +10554,7 @@ function wireWebhookFanout({ bus, store, delivery }) {
10287
10554
  const subs = await store.listForEvent(eventType);
10288
10555
  if (subs.length === 0) return;
10289
10556
  const event = {
10290
- id: newEventId2(),
10557
+ id: newEventId(),
10291
10558
  type: eventType,
10292
10559
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10293
10560
  data
@@ -10306,7 +10573,7 @@ function wireWebhookFanout({ bus, store, delivery }) {
10306
10573
  }
10307
10574
 
10308
10575
  // src/gateway/telemetry/fanout.ts
10309
- import { randomBytes as randomBytes7 } from "crypto";
10576
+ import { randomBytes as randomBytes6 } from "crypto";
10310
10577
  import { SpanKind } from "@harness-engineering/core";
10311
10578
  var TOPICS = {
10312
10579
  MAINTENANCE_STARTED: "maintenance:started",
@@ -10329,14 +10596,14 @@ var SPAN_NAME = {
10329
10596
  [TOPICS.DISPATCH_DECISION]: "dispatch_decision",
10330
10597
  [TOPICS.SKILL_INVOCATION]: "skill_invocation"
10331
10598
  };
10332
- function newEventId3() {
10333
- return `evt_${randomBytes7(8).toString("hex")}`;
10599
+ function newEventId2() {
10600
+ return `evt_${randomBytes6(8).toString("hex")}`;
10334
10601
  }
10335
10602
  function newTraceId() {
10336
- return randomBytes7(16).toString("hex");
10603
+ return randomBytes6(16).toString("hex");
10337
10604
  }
10338
10605
  function newSpanId() {
10339
- return randomBytes7(8).toString("hex");
10606
+ return randomBytes6(8).toString("hex");
10340
10607
  }
10341
10608
  function nowNs() {
10342
10609
  return BigInt(Date.now()) * 1000000n;
@@ -10387,79 +10654,102 @@ function buildAttributes(payload, extras = {}) {
10387
10654
  }
10388
10655
  return attrs;
10389
10656
  }
10657
+ function extractIds(payload) {
10658
+ const correlationId = typeof payload["correlationId"] === "string" ? payload["correlationId"] : void 0;
10659
+ const taskId = typeof payload["taskId"] === "string" ? payload["taskId"] : void 0;
10660
+ return { correlationId, taskId };
10661
+ }
10662
+ function resolveActiveRun(registry, correlationId, taskId) {
10663
+ return registry.resolve({
10664
+ ...correlationId !== void 0 ? { correlationId } : {},
10665
+ ...taskId !== void 0 ? { taskId } : {}
10666
+ });
10667
+ }
10668
+ function openMaintenanceSpan(registry, correlationId, taskId) {
10669
+ const traceId = newTraceId();
10670
+ const spanId = newSpanId();
10671
+ const key = correlationId ?? taskId ?? `run_${spanId}`;
10672
+ registry.open(key, { traceId, spanId });
10673
+ return { traceId, spanId };
10674
+ }
10675
+ function closeMaintenanceSpan(registry, topic, correlationId, taskId) {
10676
+ const existing = resolveActiveRun(registry, correlationId, taskId);
10677
+ const traceId = existing?.traceId ?? newTraceId();
10678
+ const spanId = existing?.spanId ?? newSpanId();
10679
+ const statusCode = topic === TOPICS.MAINTENANCE_ERROR ? 2 : 1;
10680
+ const key = correlationId ?? taskId ?? "";
10681
+ if (key) registry.close(key);
10682
+ return { traceId, spanId, statusCode };
10683
+ }
10684
+ function childSpan(registry, correlationId, taskId) {
10685
+ const parent = resolveActiveRun(registry, correlationId, taskId);
10686
+ const plan = {
10687
+ traceId: parent?.traceId ?? newTraceId(),
10688
+ spanId: newSpanId()
10689
+ };
10690
+ if (parent?.spanId !== void 0) plan.parentSpanId = parent.spanId;
10691
+ return plan;
10692
+ }
10693
+ function planSpan(registry, topic, correlationId, taskId) {
10694
+ if (topic === TOPICS.MAINTENANCE_STARTED) {
10695
+ return openMaintenanceSpan(registry, correlationId, taskId);
10696
+ }
10697
+ if (topic === TOPICS.MAINTENANCE_COMPLETED || topic === TOPICS.MAINTENANCE_ERROR) {
10698
+ return closeMaintenanceSpan(registry, topic, correlationId, taskId);
10699
+ }
10700
+ return childSpan(registry, correlationId, taskId);
10701
+ }
10702
+ function buildSpan(topic, plan, payload) {
10703
+ const startNs = nowNs();
10704
+ return {
10705
+ traceId: plan.traceId,
10706
+ spanId: plan.spanId,
10707
+ ...plan.parentSpanId !== void 0 ? { parentSpanId: plan.parentSpanId } : {},
10708
+ name: SPAN_NAME[topic],
10709
+ kind: SpanKind.INTERNAL,
10710
+ startTimeNs: startNs,
10711
+ endTimeNs: startNs,
10712
+ attributes: buildAttributes(payload, { "harness.topic": topic }),
10713
+ ...plan.statusCode !== void 0 ? { statusCode: plan.statusCode } : {}
10714
+ };
10715
+ }
10716
+ function buildGatewayEvent(topic, payload, correlationId) {
10717
+ return {
10718
+ id: newEventId2(),
10719
+ type: TELEMETRY_TYPE[topic],
10720
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10721
+ data: payload,
10722
+ ...correlationId !== void 0 ? { correlationId } : {}
10723
+ };
10724
+ }
10725
+ async function enqueueToMatchingSubs(store, webhookDelivery, event) {
10726
+ const subs = await store.listForEvent(event.type);
10727
+ if (subs.length === 0) return;
10728
+ const filtered = subs.filter((sub) => sub.events.some((p) => eventMatches(p, event.type)));
10729
+ for (const sub of filtered) {
10730
+ webhookDelivery.enqueue(sub, event);
10731
+ }
10732
+ }
10733
+ function makeTelemetryHandler(ctx, topic) {
10734
+ return (data) => {
10735
+ const payload = data ?? {};
10736
+ const { correlationId, taskId } = extractIds(payload);
10737
+ const plan = planSpan(ctx.registry, topic, correlationId, taskId);
10738
+ ctx.exporter.push(buildSpan(topic, plan, payload));
10739
+ void enqueueToMatchingSubs(
10740
+ ctx.store,
10741
+ ctx.webhookDelivery,
10742
+ buildGatewayEvent(topic, payload, correlationId)
10743
+ );
10744
+ };
10745
+ }
10390
10746
  function wireTelemetryFanout(params) {
10391
10747
  const { bus, exporter, webhookDelivery, store } = params;
10392
10748
  const registry = new ActiveRunRegistry();
10393
10749
  const handlers = [];
10394
- const enqueueToMatchingSubs = async (event) => {
10395
- const subs = await store.listForEvent(event.type);
10396
- if (subs.length === 0) return;
10397
- const filtered = subs.filter((sub) => sub.events.some((p) => eventMatches(p, event.type)));
10398
- for (const sub of filtered) {
10399
- webhookDelivery.enqueue(sub, event);
10400
- }
10401
- };
10402
- const makeHandler = (topic) => (data) => {
10403
- const payload = data ?? {};
10404
- const correlationId = typeof payload["correlationId"] === "string" ? payload["correlationId"] : void 0;
10405
- const taskId = typeof payload["taskId"] === "string" ? payload["taskId"] : void 0;
10406
- let traceId;
10407
- let spanId;
10408
- let parentSpanId;
10409
- let statusCode;
10410
- if (topic === TOPICS.MAINTENANCE_STARTED) {
10411
- traceId = newTraceId();
10412
- spanId = newSpanId();
10413
- const key = correlationId ?? taskId ?? `run_${spanId}`;
10414
- registry.open(key, { traceId, spanId });
10415
- } else if (topic === TOPICS.MAINTENANCE_COMPLETED || topic === TOPICS.MAINTENANCE_ERROR) {
10416
- const existing = registry.resolve({
10417
- ...correlationId !== void 0 ? { correlationId } : {},
10418
- ...taskId !== void 0 ? { taskId } : {}
10419
- });
10420
- if (existing !== void 0) {
10421
- traceId = existing.traceId;
10422
- spanId = existing.spanId;
10423
- } else {
10424
- traceId = newTraceId();
10425
- spanId = newSpanId();
10426
- }
10427
- statusCode = topic === TOPICS.MAINTENANCE_ERROR ? 2 : 1;
10428
- const key = correlationId ?? taskId ?? "";
10429
- if (key) registry.close(key);
10430
- } else {
10431
- const parent = registry.resolve({
10432
- ...correlationId !== void 0 ? { correlationId } : {},
10433
- ...taskId !== void 0 ? { taskId } : {}
10434
- });
10435
- traceId = parent?.traceId ?? newTraceId();
10436
- spanId = newSpanId();
10437
- parentSpanId = parent?.spanId;
10438
- }
10439
- const startNs = nowNs();
10440
- const span = {
10441
- traceId,
10442
- spanId,
10443
- ...parentSpanId !== void 0 ? { parentSpanId } : {},
10444
- name: SPAN_NAME[topic],
10445
- kind: SpanKind.INTERNAL,
10446
- startTimeNs: startNs,
10447
- endTimeNs: startNs,
10448
- attributes: buildAttributes(payload, { "harness.topic": topic }),
10449
- ...statusCode !== void 0 ? { statusCode } : {}
10450
- };
10451
- exporter.push(span);
10452
- const gatewayEvent = {
10453
- id: newEventId3(),
10454
- type: TELEMETRY_TYPE[topic],
10455
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10456
- data: payload,
10457
- ...correlationId !== void 0 ? { correlationId } : {}
10458
- };
10459
- void enqueueToMatchingSubs(gatewayEvent);
10460
- };
10750
+ const ctx = { registry, exporter, webhookDelivery, store };
10461
10751
  for (const topic of Object.values(TOPICS)) {
10462
- const fn = makeHandler(topic);
10752
+ const fn = makeTelemetryHandler(ctx, topic);
10463
10753
  bus.on(topic, fn);
10464
10754
  handlers.push({ topic, fn });
10465
10755
  }
@@ -10644,7 +10934,7 @@ function buildSlackSink(config, options) {
10644
10934
  }
10645
10935
 
10646
10936
  // src/notifications/events.ts
10647
- import { randomBytes as randomBytes8 } from "crypto";
10937
+ import { randomBytes as randomBytes7 } from "crypto";
10648
10938
 
10649
10939
  // src/notifications/envelope.ts
10650
10940
  function asObj(data) {
@@ -10775,8 +11065,8 @@ var NOTIFICATION_TOPICS = [
10775
11065
  "proposal.approved",
10776
11066
  "proposal.rejected"
10777
11067
  ];
10778
- function newEventId4() {
10779
- return `evt_${randomBytes8(8).toString("hex")}`;
11068
+ function newEventId3() {
11069
+ return `evt_${randomBytes7(8).toString("hex")}`;
10780
11070
  }
10781
11071
  function dispatchToEntry(bus, entry, event) {
10782
11072
  const eventType = event.type;
@@ -10811,7 +11101,7 @@ function wireNotificationSinks({ bus, registry }) {
10811
11101
  const entries = registry.list();
10812
11102
  if (entries.length === 0) return;
10813
11103
  const event = {
10814
- id: newEventId4(),
11104
+ id: newEventId3(),
10815
11105
  type: eventType,
10816
11106
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10817
11107
  data
@@ -10922,6 +11212,37 @@ async function scanWorkspaceConfig(workspacePath) {
10922
11212
  return { exitCode: computeScanExitCode(results), results };
10923
11213
  }
10924
11214
 
11215
+ // src/core/lane-persistence.ts
11216
+ import { eventSourcing } from "@harness-engineering/core";
11217
+ import { Err as Err21 } from "@harness-engineering/types";
11218
+ var SIGNAL_TO_LANE = {
11219
+ claim: "claimed",
11220
+ dispatch: "in_progress",
11221
+ success: "in_review",
11222
+ failure: "blocked",
11223
+ abandon: "canceled"
11224
+ };
11225
+ function mapOrchestratorLane(signal) {
11226
+ return SIGNAL_TO_LANE[signal];
11227
+ }
11228
+ async function persistLane(projectPath, issueId, signal) {
11229
+ try {
11230
+ const reg = await eventSourcing.registerTask(projectPath, issueId, []);
11231
+ if (!reg.ok) return reg;
11232
+ return await eventSourcing.transitionLane(projectPath, issueId, mapOrchestratorLane(signal));
11233
+ } catch (err) {
11234
+ return Err21(err instanceof Error ? err : new Error(String(err)));
11235
+ }
11236
+ }
11237
+ async function readPersistedLanes(projectPath) {
11238
+ try {
11239
+ const snap = await eventSourcing.readSnapshot(projectPath);
11240
+ return snap.ok ? snap.value.lanes : { tasks: {} };
11241
+ } catch {
11242
+ return { tasks: {} };
11243
+ }
11244
+ }
11245
+
10925
11246
  // src/maintenance/task-registry.ts
10926
11247
  var BUILT_IN_TASKS = [
10927
11248
  // --- Mechanical-AI ---
@@ -10985,7 +11306,13 @@ var BUILT_IN_TASKS = [
10985
11306
  description: "Detect and fix cross-check violations",
10986
11307
  schedule: "0 6 * * 1",
10987
11308
  branch: "harness-maint/cross-check-fixes",
10988
- checkCommand: ["validate-cross-check"],
11309
+ // `harness cross-check` is a dedicated read-only CLI subcommand that surfaces
11310
+ // JUST cross-artifact consistency (plan→implementation coverage + staleness),
11311
+ // mirroring the `validate_cross_check` MCP tool's core (`runCrossCheck`)
11312
+ // WITHOUT running the full `harness validate` suite. It prints a parseable
11313
+ // `Cross-check: N issues` line and exits 0 (clean) / 1 (N issues), so the
11314
+ // maintenance runner reports real results instead of an honest `failure`.
11315
+ checkCommand: ["cross-check"],
10989
11316
  fixSkill: "harness-cross-check-fix"
10990
11317
  },
10991
11318
  // --- Pure-AI ---
@@ -11044,7 +11371,10 @@ var BUILT_IN_TASKS = [
11044
11371
  description: "Assess overall project health",
11045
11372
  schedule: "0 6 * * *",
11046
11373
  branch: null,
11047
- checkCommand: ["assess_project"]
11374
+ // `assess_project` is an MCP tool name, not a CLI subcommand. The CLI
11375
+ // composite project-health report is `harness insights` (health, entropy,
11376
+ // decay, attention, impact) — a read-only report that records metrics.
11377
+ checkCommand: ["insights"]
11048
11378
  },
11049
11379
  {
11050
11380
  id: "stale-constraints",
@@ -11052,7 +11382,14 @@ var BUILT_IN_TASKS = [
11052
11382
  description: "Detect stale architectural constraints",
11053
11383
  schedule: "0 2 1 * *",
11054
11384
  branch: null,
11055
- checkCommand: ["detect_stale_constraints"]
11385
+ // `harness stale-constraints` is a dedicated read-only CLI subcommand that
11386
+ // surfaces the `detect_stale_constraints` MCP tool's core in-process. It is
11387
+ // precondition-gated on the knowledge graph: with no graph it emits the
11388
+ // "No knowledge graph found. Run `harness scan` first." signature and exits
11389
+ // non-zero, which the runner classifies as `skipped` (not failure). With a
11390
+ // graph it prints a parseable `Stale constraints: N findings` line and exits
11391
+ // 0 (clean) / 1 (N stale), recorded as report-only metrics.
11392
+ checkCommand: ["stale-constraints"]
11056
11393
  },
11057
11394
  {
11058
11395
  id: "graph-refresh",
@@ -11085,7 +11422,8 @@ var BUILT_IN_TASKS = [
11085
11422
  description: "Clean up stale orchestrator sessions",
11086
11423
  schedule: "0 0 * * *",
11087
11424
  branch: null,
11088
- checkCommand: ["cleanup-sessions"]
11425
+ checkCommand: ["cleanup-sessions"],
11426
+ excludeFromHumanSweep: true
11089
11427
  },
11090
11428
  {
11091
11429
  id: "perf-baselines",
@@ -11093,7 +11431,8 @@ var BUILT_IN_TASKS = [
11093
11431
  description: "Update performance baselines",
11094
11432
  schedule: "0 7 * * *",
11095
11433
  branch: null,
11096
- checkCommand: ["perf", "baselines", "update"]
11434
+ checkCommand: ["perf", "baselines", "update"],
11435
+ excludeFromHumanSweep: true
11097
11436
  },
11098
11437
  {
11099
11438
  id: "main-sync",
@@ -11101,7 +11440,8 @@ var BUILT_IN_TASKS = [
11101
11440
  description: "Fast-forward local default branch from origin",
11102
11441
  schedule: "*/15 * * * *",
11103
11442
  branch: null,
11104
- checkCommand: ["harness", "sync-main", "--json"]
11443
+ checkCommand: ["harness", "sync-main", "--json"],
11444
+ excludeFromHumanSweep: true
11105
11445
  },
11106
11446
  // Hermes Phase 4 — one-shot backfill that stamps `provenance: user-authored`
11107
11447
  // on every existing catalog skill. Schedule is Feb 31 (a date that never
@@ -11114,7 +11454,8 @@ var BUILT_IN_TASKS = [
11114
11454
  description: "Backfill provenance: user-authored on every existing skill (one-shot, idempotent)",
11115
11455
  schedule: "0 0 31 2 *",
11116
11456
  branch: null,
11117
- checkCommand: ["backfill-skill-provenance"]
11457
+ checkCommand: ["backfill-skill-provenance"],
11458
+ excludeFromHumanSweep: true
11118
11459
  }
11119
11460
  ];
11120
11461
 
@@ -11181,6 +11522,17 @@ function cronMatchesNow(expression, now) {
11181
11522
  const daysOfWeek = parseField(dowField, 0, 6);
11182
11523
  return minutes.has(minute) && hours.has(hour) && daysOfMonth.has(dayOfMonth) && months.has(month) && daysOfWeek.has(dayOfWeek);
11183
11524
  }
11525
+ function cronMatchesDate(expression, date) {
11526
+ const fields = expression.trim().split(/\s+/);
11527
+ if (fields.length !== 5) {
11528
+ throw new Error(`Invalid cron expression: expected 5 fields, got ${fields.length}`);
11529
+ }
11530
+ const [, , domField, monthField, dowField] = fields;
11531
+ const daysOfMonth = parseField(domField, 1, 31);
11532
+ const months = parseField(monthField, 1, 12);
11533
+ const daysOfWeek = parseField(dowField, 0, 6);
11534
+ return daysOfMonth.has(date.getDate()) && months.has(date.getMonth() + 1) && daysOfWeek.has(date.getDay());
11535
+ }
11184
11536
 
11185
11537
  // src/maintenance/scheduler.ts
11186
11538
  var MaintenanceScheduler = class {
@@ -11430,7 +11782,7 @@ var SingleProcessLeaderElector = class {
11430
11782
  };
11431
11783
 
11432
11784
  // src/maintenance/reporter.ts
11433
- import * as fs16 from "fs";
11785
+ import * as fs14 from "fs";
11434
11786
  import * as path18 from "path";
11435
11787
  import { z as z17 } from "zod";
11436
11788
  var RunResultSchema = z17.object({
@@ -11466,9 +11818,9 @@ var MaintenanceReporter = class {
11466
11818
  */
11467
11819
  async load() {
11468
11820
  try {
11469
- await fs16.promises.mkdir(this.persistDir, { recursive: true });
11821
+ await fs14.promises.mkdir(this.persistDir, { recursive: true });
11470
11822
  const filePath = path18.join(this.persistDir, "history.json");
11471
- const data = await fs16.promises.readFile(filePath, "utf-8");
11823
+ const data = await fs14.promises.readFile(filePath, "utf-8");
11472
11824
  const parsed = z17.array(RunResultSchema).safeParse(JSON.parse(data));
11473
11825
  if (parsed.success) {
11474
11826
  this.history = parsed.data.slice(0, MAX_HISTORY);
@@ -11502,9 +11854,9 @@ var MaintenanceReporter = class {
11502
11854
  */
11503
11855
  async persist() {
11504
11856
  try {
11505
- await fs16.promises.mkdir(this.persistDir, { recursive: true });
11857
+ await fs14.promises.mkdir(this.persistDir, { recursive: true });
11506
11858
  const filePath = path18.join(this.persistDir, "history.json");
11507
- await fs16.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
11859
+ await fs14.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
11508
11860
  } catch (err) {
11509
11861
  this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
11510
11862
  }
@@ -11544,20 +11896,20 @@ var TaskRunner = class {
11544
11896
  * @param origin - Hermes Phase 2 trigger-source tag; defaults to `'cron'`
11545
11897
  * when called from the scheduler path.
11546
11898
  */
11547
- async run(task, origin = "cron") {
11899
+ async run(task, origin = "cron", mode = "fix") {
11548
11900
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
11549
11901
  let result;
11550
11902
  let captured;
11551
11903
  try {
11552
11904
  switch (task.type) {
11553
11905
  case "mechanical-ai": {
11554
- const out = await this.runMechanicalAI(task, startedAt);
11906
+ const out = await this.runMechanicalAI(task, startedAt, mode);
11555
11907
  result = out.result;
11556
11908
  captured = out.captured;
11557
11909
  break;
11558
11910
  }
11559
11911
  case "pure-ai":
11560
- result = await this.runPureAI(task, startedAt);
11912
+ result = await this.runPureAI(task, startedAt, mode);
11561
11913
  break;
11562
11914
  case "report-only": {
11563
11915
  const out = await this.runReportOnly(task, startedAt);
@@ -11566,7 +11918,7 @@ var TaskRunner = class {
11566
11918
  break;
11567
11919
  }
11568
11920
  case "housekeeping": {
11569
- const out = await this.runHousekeeping(task, startedAt);
11921
+ const out = await this.runHousekeeping(task, startedAt, mode);
11570
11922
  result = out.result;
11571
11923
  captured = out.captured;
11572
11924
  break;
@@ -11630,7 +11982,8 @@ var TaskRunner = class {
11630
11982
  findings: r2.findings,
11631
11983
  stdout: r2.output,
11632
11984
  stderr: r2.stderr,
11633
- structured: r2.structured ? r2.structured : null
11985
+ structured: r2.structured ? r2.structured : null,
11986
+ executionFailed: false
11634
11987
  };
11635
11988
  }
11636
11989
  if (!task.checkCommand || task.checkCommand.length === 0) {
@@ -11642,7 +11995,8 @@ var TaskRunner = class {
11642
11995
  findings: r.findings,
11643
11996
  stdout: r.output,
11644
11997
  stderr: "",
11645
- structured: null
11998
+ structured: null,
11999
+ executionFailed: r.executionFailed ?? false
11646
12000
  };
11647
12001
  }
11648
12002
  /**
@@ -11667,7 +12021,7 @@ var TaskRunner = class {
11667
12021
  * only if fixable findings exist; persist captured stdout/stderr/context
11668
12022
  * via the output store on the way out.
11669
12023
  */
11670
- async runMechanicalAI(task, startedAt) {
12024
+ async runMechanicalAI(task, startedAt, mode = "fix") {
11671
12025
  if (!task.fixSkill) {
11672
12026
  return wrap(this.failureResult(task.id, startedAt, "mechanical-ai task missing fixSkill"));
11673
12027
  }
@@ -11689,6 +12043,27 @@ var TaskRunner = class {
11689
12043
  } catch (err) {
11690
12044
  return wrap(this.failureResult(task.id, startedAt, String(err)));
11691
12045
  }
12046
+ if (check.executionFailed) {
12047
+ const cls = classifyCheckExecutionFailure(`${check.stdout}
12048
+ ${check.stderr}`);
12049
+ if (cls.kind === "unrunnable") {
12050
+ return wrap(
12051
+ this.failureResult(task.id, startedAt, checkExecutionError(check)),
12052
+ captureFromCheck(check)
12053
+ );
12054
+ }
12055
+ if (cls.kind === "precondition") {
12056
+ return wrap(
12057
+ this.skippedResult(task.id, startedAt, cls.reason ?? "precondition not met"),
12058
+ captureFromCheck(check)
12059
+ );
12060
+ }
12061
+ check = {
12062
+ ...check,
12063
+ executionFailed: false,
12064
+ findings: check.findings > 0 ? check.findings : recoverFindingsCount(check.stdout)
12065
+ };
12066
+ }
11692
12067
  const promptContext = await this.composePromptContext(task);
11693
12068
  const baseCaptured = {
11694
12069
  stdout: check.stdout,
@@ -11697,13 +12072,14 @@ var TaskRunner = class {
11697
12072
  ...promptContext ? { context: promptContext } : {}
11698
12073
  };
11699
12074
  const wakeAgentExplicitlyFalse = check.structured !== null && typeof check.structured === "object" && check.structured.wakeAgent === false;
11700
- if (check.findings === 0 || wakeAgentExplicitlyFalse) {
12075
+ if (check.findings === 0 || wakeAgentExplicitlyFalse || mode === "report") {
12076
+ const reportedWithFindings = mode === "report" && check.findings > 0;
11701
12077
  return {
11702
12078
  result: {
11703
12079
  taskId: task.id,
11704
12080
  startedAt,
11705
12081
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
11706
- status: "no-issues",
12082
+ status: reportedWithFindings ? "success" : "no-issues",
11707
12083
  findings: check.findings,
11708
12084
  fixed: 0,
11709
12085
  prUrl: null,
@@ -11773,7 +12149,19 @@ var TaskRunner = class {
11773
12149
  /**
11774
12150
  * Pure-AI: always dispatch agent with configured skill.
11775
12151
  */
11776
- async runPureAI(task, startedAt) {
12152
+ async runPureAI(task, startedAt, mode = "fix") {
12153
+ if (mode === "report") {
12154
+ return {
12155
+ taskId: task.id,
12156
+ startedAt,
12157
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
12158
+ status: "no-issues",
12159
+ findings: 0,
12160
+ fixed: 0,
12161
+ prUrl: null,
12162
+ prUpdated: false
12163
+ };
12164
+ }
11777
12165
  if (!task.fixSkill) {
11778
12166
  return this.failureResult(task.id, startedAt, "pure-ai task missing fixSkill");
11779
12167
  }
@@ -11847,6 +12235,27 @@ var TaskRunner = class {
11847
12235
  return wrap(this.failureResult(task.id, startedAt, String(err)));
11848
12236
  }
11849
12237
  const parsed = parseStatusLine(check.stdout);
12238
+ if (parsed === null && check.executionFailed) {
12239
+ const cls = classifyCheckExecutionFailure(`${check.stdout}
12240
+ ${check.stderr}`);
12241
+ if (cls.kind === "unrunnable") {
12242
+ return wrap(
12243
+ this.failureResult(task.id, startedAt, checkExecutionError(check)),
12244
+ captureFromCheck(check)
12245
+ );
12246
+ }
12247
+ if (cls.kind === "precondition") {
12248
+ return wrap(
12249
+ this.skippedResult(task.id, startedAt, cls.reason ?? "precondition not met"),
12250
+ captureFromCheck(check)
12251
+ );
12252
+ }
12253
+ check = {
12254
+ ...check,
12255
+ executionFailed: false,
12256
+ findings: check.findings > 0 ? check.findings : recoverFindingsCount(check.stdout)
12257
+ };
12258
+ }
11850
12259
  const status = parsed?.status ?? "success";
11851
12260
  const findings = parsed === null ? check.findings : typeof parsed.candidatesFound === "number" ? parsed.candidatesFound : 0;
11852
12261
  const result = {
@@ -11880,7 +12289,20 @@ var TaskRunner = class {
11880
12289
  * Hermes Phase 2: a `checkScript` may replace `checkCommand` for housekeeping
11881
12290
  * tasks; the runner falls through to the same JSON-status parsing path.
11882
12291
  */
11883
- async runHousekeeping(task, startedAt) {
12292
+ async runHousekeeping(task, startedAt, mode = "fix") {
12293
+ if (mode === "report") {
12294
+ return wrap({
12295
+ taskId: task.id,
12296
+ startedAt,
12297
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
12298
+ status: "skipped",
12299
+ findings: 0,
12300
+ fixed: 0,
12301
+ prUrl: null,
12302
+ prUpdated: false,
12303
+ error: "skipped in report mode: housekeeping may mutate and report runs are read-only"
12304
+ });
12305
+ }
11884
12306
  if (!task.checkCommand && !task.checkScript) {
11885
12307
  return wrap(
11886
12308
  this.failureResult(
@@ -11947,46 +12369,133 @@ var TaskRunner = class {
11947
12369
  error
11948
12370
  };
11949
12371
  }
12372
+ /**
12373
+ * A precondition-gated check (e.g. `predict` with <3 snapshots, or a
12374
+ * graph-backed check before `harness scan`). The command is correctly
12375
+ * configured; the repo just lacks the state it needs. Reported as `skipped`
12376
+ * — distinct from a hard `failure` — carrying the refusal line as the reason
12377
+ * (surfaced in the run-report summary column). ADR 0050.
12378
+ */
12379
+ skippedResult(taskId, startedAt, reason) {
12380
+ return {
12381
+ taskId,
12382
+ startedAt,
12383
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
12384
+ status: "skipped",
12385
+ findings: 0,
12386
+ fixed: 0,
12387
+ prUrl: null,
12388
+ prUpdated: false,
12389
+ error: reason
12390
+ };
12391
+ }
11950
12392
  };
11951
12393
  function wrap(result, captured) {
11952
12394
  return captured ? { result, captured } : { result };
11953
12395
  }
12396
+ function checkExecutionError(check) {
12397
+ const detail = (check.stderr || check.stdout || "").trim();
12398
+ return detail ? `check command failed to execute: ${detail}` : "check command failed to execute";
12399
+ }
12400
+ function captureFromCheck(check) {
12401
+ return { stdout: check.stdout, stderr: check.stderr, structured: check.structured };
12402
+ }
12403
+ var PRECONDITION_PATTERNS = [
12404
+ /requires at least \d+ snapshots?/i,
12405
+ /Run ["'`]?harness snapshot/i,
12406
+ /no knowledge graph found/i,
12407
+ /Run ["'`]?harness scan/i,
12408
+ /no graph (?:available|found)/i
12409
+ ];
12410
+ var UNRUNNABLE_PATTERNS = [
12411
+ /unknown command/i,
12412
+ /unknown option/i,
12413
+ /\bENOENT\b/,
12414
+ /command not found/i,
12415
+ /cannot find module/i,
12416
+ // A timed-out check did not complete — it is a hard failure, never a
12417
+ // "ran-no-count" success. The runners synthesize this message on SIGTERM.
12418
+ /timed out/i,
12419
+ /\bETIMEDOUT\b/
12420
+ ];
12421
+ var TIMEOUT_SIGNATURE = /check timed out after \d+\s*ms/i;
12422
+ function explicitFindingsCount(output) {
12423
+ const after = output.match(/(?:findings?|issues?|violations?|errors?)\s*[:=]\s*(\d+)/i);
12424
+ if (after) return parseInt(after[1], 10);
12425
+ const before = output.match(/(\d+)\s+(?:findings?|issues?|violations?|errors?)\b/i);
12426
+ if (before) return parseInt(before[1], 10);
12427
+ return null;
12428
+ }
12429
+ function recoverFindingsCount(output) {
12430
+ return explicitFindingsCount(output) ?? 1;
12431
+ }
12432
+ function firstMeaningfulLine(output) {
12433
+ const line = output.split("\n").map((l) => l.trim()).find(Boolean);
12434
+ if (!line) return "precondition not met";
12435
+ return line.replace(/^[x✗✓!-]\s*/u, "").trim();
12436
+ }
12437
+ var CLASSIFY_HEAD_LINES = 3;
12438
+ function classifyCheckExecutionFailure(output) {
12439
+ const text = (output ?? "").trim();
12440
+ if (text.length === 0) return { kind: "unrunnable" };
12441
+ if (TIMEOUT_SIGNATURE.test(text)) return { kind: "unrunnable" };
12442
+ if (explicitFindingsCount(text) !== null) return { kind: "ran-no-count" };
12443
+ const head = text.split("\n").filter((l) => l.trim()).slice(0, CLASSIFY_HEAD_LINES).join("\n");
12444
+ for (const re of PRECONDITION_PATTERNS) {
12445
+ if (re.test(head)) return { kind: "precondition", reason: firstMeaningfulLine(text) };
12446
+ }
12447
+ for (const re of UNRUNNABLE_PATTERNS) {
12448
+ if (re.test(head)) return { kind: "unrunnable" };
12449
+ }
12450
+ return { kind: "ran-no-count" };
12451
+ }
11954
12452
  function parseStatusLine(output) {
11955
12453
  const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
11956
12454
  for (let i = lines.length - 1; i >= 0; i--) {
11957
- const line = lines[i];
11958
- if (!line || !line.startsWith("{") || !line.endsWith("}")) continue;
11959
- try {
11960
- const obj = JSON.parse(line);
11961
- const s = obj.status;
11962
- if (s === "success" || s === "skipped" || s === "failure" || s === "no-issues") {
11963
- const parsed = { status: s, rawStatus: s };
11964
- if (typeof obj.candidatesFound === "number") {
11965
- parsed.candidatesFound = obj.candidatesFound;
11966
- }
11967
- if (typeof obj.error === "string") {
11968
- parsed.error = obj.error;
11969
- }
11970
- if (typeof obj.reason === "string") {
11971
- parsed.reason = obj.reason;
11972
- }
11973
- if (typeof obj.detail === "string" && !parsed.error) {
11974
- parsed.error = `${parsed.reason ?? "skipped"}: ${obj.detail}`;
11975
- }
11976
- return parsed;
11977
- }
11978
- if (s === "updated" || s === "no-op") {
11979
- return { status: "success", rawStatus: s };
11980
- }
11981
- if (s === "error") {
11982
- const message = typeof obj.message === "string" ? obj.message : "unknown error";
11983
- return { status: "failure", error: message, rawStatus: "error" };
11984
- }
11985
- } catch {
11986
- }
12455
+ const parsed = parseStatusObjectLine(lines[i]);
12456
+ if (parsed) return parsed;
12457
+ }
12458
+ return null;
12459
+ }
12460
+ function parseStatusObjectLine(line) {
12461
+ if (!line || !line.startsWith("{") || !line.endsWith("}")) return null;
12462
+ let obj;
12463
+ try {
12464
+ obj = JSON.parse(line);
12465
+ } catch {
12466
+ return null;
12467
+ }
12468
+ return classifyStatusObject(obj);
12469
+ }
12470
+ var PHASE_STATUSES = /* @__PURE__ */ new Set(["success", "skipped", "failure", "no-issues"]);
12471
+ var SYNC_SUCCESS_STATUSES = /* @__PURE__ */ new Set(["updated", "no-op"]);
12472
+ function classifyStatusObject(obj) {
12473
+ const s = obj.status;
12474
+ if (typeof s !== "string") return null;
12475
+ if (PHASE_STATUSES.has(s)) return buildPhaseStatus(s, obj);
12476
+ if (SYNC_SUCCESS_STATUSES.has(s)) return { status: "success", rawStatus: s };
12477
+ if (s === "error") {
12478
+ const message = typeof obj.message === "string" ? obj.message : "unknown error";
12479
+ return { status: "failure", error: message, rawStatus: "error" };
11987
12480
  }
11988
12481
  return null;
11989
12482
  }
12483
+ function buildPhaseStatus(status, obj) {
12484
+ const parsed = { status, rawStatus: status };
12485
+ if (typeof obj.candidatesFound === "number") {
12486
+ parsed.candidatesFound = obj.candidatesFound;
12487
+ }
12488
+ if (typeof obj.error === "string") {
12489
+ parsed.error = obj.error;
12490
+ }
12491
+ if (typeof obj.reason === "string") {
12492
+ parsed.reason = obj.reason;
12493
+ }
12494
+ if (typeof obj.detail === "string" && !parsed.error) {
12495
+ parsed.error = `${parsed.reason ?? "skipped"}: ${obj.detail}`;
12496
+ }
12497
+ return parsed;
12498
+ }
11990
12499
 
11991
12500
  // src/maintenance/check-script-runner.ts
11992
12501
  import { execFile as execFile6 } from "child_process";
@@ -12099,8 +12608,49 @@ function heuristicResult(stdout, stderr, exitedAbnormally) {
12099
12608
  };
12100
12609
  }
12101
12610
 
12611
+ // src/maintenance/check-runner.ts
12612
+ import { execFile as nodeExecFile } from "child_process";
12613
+ import { promisify as promisify4 } from "util";
12614
+ var MAINTENANCE_CHECK_MAX_BUFFER = 64 * 1024 * 1024;
12615
+ var MAINTENANCE_CHECK_TIMEOUT_MS = 3e5;
12616
+ var FINDINGS_RE = /(\d+)\s+(?:finding|issue|violation|error)/i;
12617
+ var nodeExecFileAsync = promisify4(nodeExecFile);
12618
+ function isCheckTimeoutError(e) {
12619
+ return e.killed === true || e.signal === "SIGTERM" || e.code === "ETIMEDOUT";
12620
+ }
12621
+ async function runHarnessCheck(spawn7, cwd, opts = {}) {
12622
+ const execFileAsync2 = opts.execFileAsync ?? nodeExecFileAsync;
12623
+ const timeoutMs = opts.timeoutMs ?? MAINTENANCE_CHECK_TIMEOUT_MS;
12624
+ const maxBuffer = opts.maxBuffer ?? MAINTENANCE_CHECK_MAX_BUFFER;
12625
+ try {
12626
+ const { stdout } = await execFileAsync2(spawn7.file, spawn7.args, {
12627
+ cwd,
12628
+ timeout: timeoutMs,
12629
+ maxBuffer
12630
+ });
12631
+ const text = String(stdout);
12632
+ const m = text.match(FINDINGS_RE);
12633
+ const findings = m ? parseInt(m[1], 10) : 0;
12634
+ return { passed: findings === 0, findings, output: text, executionFailed: false };
12635
+ } catch (err) {
12636
+ const e = err;
12637
+ let output = [e.stdout, e.stderr].map((v) => v == null ? "" : String(v)).filter(Boolean).join("\n");
12638
+ if (isCheckTimeoutError(e)) {
12639
+ const note = `check timed out after ${timeoutMs}ms`;
12640
+ output = output.trim() ? `${output}
12641
+ ${note}` : note;
12642
+ return { passed: false, findings: 0, output, executionFailed: true };
12643
+ }
12644
+ const m = output.match(FINDINGS_RE);
12645
+ if (m) {
12646
+ return { passed: false, findings: parseInt(m[1], 10), output, executionFailed: false };
12647
+ }
12648
+ return { passed: false, findings: 0, output, executionFailed: true };
12649
+ }
12650
+ }
12651
+
12102
12652
  // src/maintenance/output-store.ts
12103
- import * as fs17 from "fs";
12653
+ import * as fs15 from "fs";
12104
12654
  import * as path20 from "path";
12105
12655
  var DEFAULT_RETENTION = {
12106
12656
  runs: 50,
@@ -12141,13 +12691,13 @@ var TaskOutputStore = class {
12141
12691
  async write(taskId, entry, retention) {
12142
12692
  this.ensureSafeTaskId(taskId);
12143
12693
  const dir = this.dirFor(taskId);
12144
- await fs17.promises.mkdir(dir, { recursive: true });
12694
+ await fs15.promises.mkdir(dir, { recursive: true });
12145
12695
  const fileName = `${sanitizeIso(entry.completedAt || (/* @__PURE__ */ new Date()).toISOString())}.json`;
12146
12696
  const filePath = path20.join(dir, fileName);
12147
12697
  const tmpPath = `${filePath}.tmp`;
12148
12698
  const payload = JSON.stringify(entry, null, 2);
12149
- await fs17.promises.writeFile(tmpPath, payload, "utf-8");
12150
- await fs17.promises.rename(tmpPath, filePath);
12699
+ await fs15.promises.writeFile(tmpPath, payload, "utf-8");
12700
+ await fs15.promises.rename(tmpPath, filePath);
12151
12701
  try {
12152
12702
  await this.applyRetention(taskId, retention);
12153
12703
  } catch (err) {
@@ -12198,7 +12748,7 @@ var TaskOutputStore = class {
12198
12748
  }
12199
12749
  async readEntry(filePath) {
12200
12750
  try {
12201
- const buf = await fs17.promises.readFile(filePath, "utf-8");
12751
+ const buf = await fs15.promises.readFile(filePath, "utf-8");
12202
12752
  const parsed = JSON.parse(buf);
12203
12753
  return parsed;
12204
12754
  } catch {
@@ -12220,7 +12770,7 @@ var TaskOutputStore = class {
12220
12770
  const toRemove = /* @__PURE__ */ new Set([...overflow, ...aged]);
12221
12771
  for (const name of toRemove) {
12222
12772
  try {
12223
- await fs17.promises.unlink(path20.join(dir, name));
12773
+ await fs15.promises.unlink(path20.join(dir, name));
12224
12774
  } catch {
12225
12775
  }
12226
12776
  }
@@ -12229,7 +12779,7 @@ var TaskOutputStore = class {
12229
12779
  async function listJsonFilesDescending(dir) {
12230
12780
  let names;
12231
12781
  try {
12232
- names = await fs17.promises.readdir(dir);
12782
+ names = await fs15.promises.readdir(dir);
12233
12783
  } catch {
12234
12784
  return [];
12235
12785
  }
@@ -12338,7 +12888,7 @@ var fallbackLogger3 = {
12338
12888
  };
12339
12889
 
12340
12890
  // src/maintenance/custom-task-validator.ts
12341
- import { Ok as Ok23, Err as Err20 } from "@harness-engineering/types";
12891
+ import { Ok as Ok23, Err as Err22 } from "@harness-engineering/types";
12342
12892
  var ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
12343
12893
  var REQUIRED_FIELDS_BY_TYPE = {
12344
12894
  "mechanical-ai": ["branch", "fixSkill"],
@@ -12358,7 +12908,7 @@ function validateCustomTasks(customTasks, builtIns, deps = {}) {
12358
12908
  validateOne(id, task, builtInIds, allIds, deps, errors);
12359
12909
  }
12360
12910
  detectCycles(customTasks, builtIns, errors);
12361
- return errors.length === 0 ? Ok23(void 0) : Err20(errors);
12911
+ return errors.length === 0 ? Ok23(void 0) : Err22(errors);
12362
12912
  }
12363
12913
  function validateOne(id, task, builtInIds, allIds, deps, errors) {
12364
12914
  const prefix = `customTasks.${id}`;
@@ -12547,6 +13097,11 @@ function deriveSeedPaths(config) {
12547
13097
  const roadmapPath = config.tracker.kind === "roadmap" && config.tracker.filePath ? config.tracker.filePath : "docs/roadmap.md";
12548
13098
  return [".harness/proposals", roadmapPath];
12549
13099
  }
13100
+ function normalizeHarnessCommand(command) {
13101
+ if (command.length === 0) return [];
13102
+ if (command[0] === "harness") return command;
13103
+ return ["harness", ...command];
13104
+ }
12550
13105
  var Orchestrator = class extends EventEmitter {
12551
13106
  state;
12552
13107
  config;
@@ -12673,6 +13228,11 @@ var Orchestrator = class extends EventEmitter {
12673
13228
  abortControllers = /* @__PURE__ */ new Map();
12674
13229
  /** Guards against overlapping ticks when a tick takes longer than the polling interval */
12675
13230
  tickInProgress = false;
13231
+ /** Phase 4 (DLane-5): lanes read back from the durable log on first tick, for
13232
+ * observability only — NOT fed into reconciliation. Empty until the first tick. */
13233
+ persistedLanes = { tasks: {} };
13234
+ /** Ensures the lane read-back diagnostic runs at most once (first tick). */
13235
+ laneReadbackDone = false;
12676
13236
  /** Timestamp of the last stale branch sweep (at most once per hour) */
12677
13237
  lastBranchSweepMs = 0;
12678
13238
  /** Current tick-phase activity visible to the dashboard */
@@ -12830,22 +13390,7 @@ var Orchestrator = class extends EventEmitter {
12830
13390
  });
12831
13391
  webhookDelivery.start();
12832
13392
  this.setupNotifications(config.notifications);
12833
- const otlpCfg = config.telemetry?.export?.otlp;
12834
- if (otlpCfg) {
12835
- this.otlpExporter = new OTLPExporter({
12836
- endpoint: otlpCfg.endpoint,
12837
- ...otlpCfg.enabled !== void 0 ? { enabled: otlpCfg.enabled } : {},
12838
- ...otlpCfg.headers !== void 0 ? { headers: otlpCfg.headers } : {},
12839
- ...otlpCfg.flushIntervalMs !== void 0 ? { flushIntervalMs: otlpCfg.flushIntervalMs } : {},
12840
- ...otlpCfg.batchSize !== void 0 ? { batchSize: otlpCfg.batchSize } : {}
12841
- });
12842
- this.telemetryFanoutOff = wireTelemetryFanout({
12843
- bus: this,
12844
- exporter: this.otlpExporter,
12845
- webhookDelivery,
12846
- store: webhookStore
12847
- });
12848
- }
13393
+ this.setupTelemetryExport(config, webhookStore, webhookDelivery);
12849
13394
  this.server = new OrchestratorServer(this, config.server.port, {
12850
13395
  interactionQueue: this.interactionQueue,
12851
13396
  webhooks: {
@@ -12868,24 +13413,8 @@ var Orchestrator = class extends EventEmitter {
12868
13413
  analysisArchive: this.analysisArchive,
12869
13414
  roadmapPath: config.tracker.filePath ?? null,
12870
13415
  dispatchAdHoc: this.dispatchAdHoc.bind(this),
12871
- getLocalModelStatus: () => {
12872
- const first = this.localResolvers.values().next();
12873
- return first.done ? null : first.value.getStatus();
12874
- },
12875
- getLocalModelStatuses: () => {
12876
- const backends = this.config.agent.backends ?? {};
12877
- const out = [];
12878
- for (const [name, resolver] of this.localResolvers) {
12879
- const def = backends[name];
12880
- if (!def || def.type !== "local" && def.type !== "pi") continue;
12881
- out.push({
12882
- ...resolver.getStatus(),
12883
- backendName: name,
12884
- endpoint: def.endpoint
12885
- });
12886
- }
12887
- return out;
12888
- }
13416
+ getLocalModelStatus: () => this.getFirstLocalModelStatus(),
13417
+ getLocalModelStatuses: () => this.buildLocalModelStatuses()
12889
13418
  });
12890
13419
  this.server.setRecorder(this.recorder);
12891
13420
  this.interactionQueue.onPush((interaction) => {
@@ -12893,6 +13422,56 @@ var Orchestrator = class extends EventEmitter {
12893
13422
  });
12894
13423
  }
12895
13424
  }
13425
+ /**
13426
+ * Phase 5: construct the OTLP/HTTP trace exporter and wire telemetry fanout.
13427
+ * Only fires when the operator configures `telemetry.export.otlp` in
13428
+ * harness.config.json. Extracted from the server-init block in the
13429
+ * constructor to keep that block's cyclomatic complexity under threshold.
13430
+ */
13431
+ setupTelemetryExport(config, webhookStore, webhookDelivery) {
13432
+ const otlpCfg = config.telemetry?.export?.otlp;
13433
+ if (!otlpCfg) return;
13434
+ this.otlpExporter = new OTLPExporter({
13435
+ endpoint: otlpCfg.endpoint,
13436
+ ...otlpCfg.enabled !== void 0 ? { enabled: otlpCfg.enabled } : {},
13437
+ ...otlpCfg.headers !== void 0 ? { headers: otlpCfg.headers } : {},
13438
+ ...otlpCfg.flushIntervalMs !== void 0 ? { flushIntervalMs: otlpCfg.flushIntervalMs } : {},
13439
+ ...otlpCfg.batchSize !== void 0 ? { batchSize: otlpCfg.batchSize } : {}
13440
+ });
13441
+ this.telemetryFanoutOff = wireTelemetryFanout({
13442
+ bus: this,
13443
+ exporter: this.otlpExporter,
13444
+ webhookDelivery,
13445
+ store: webhookStore
13446
+ });
13447
+ }
13448
+ /**
13449
+ * Deprecated alias for /api/v1/local-model/status (Spec 1 endpoint retained
13450
+ * as a compat shim per spec line 35; superseded by getLocalModelStatuses for
13451
+ * the multi-local UI). Returns the first-registered resolver's status.
13452
+ */
13453
+ getFirstLocalModelStatus() {
13454
+ const first = this.localResolvers.values().next();
13455
+ return first.done ? null : first.value.getStatus();
13456
+ }
13457
+ /**
13458
+ * SC38: build NamedLocalModelStatus[] from each registered resolver, tagged
13459
+ * with its backendName + endpoint from the config.
13460
+ */
13461
+ buildLocalModelStatuses() {
13462
+ const backends = this.config.agent.backends ?? {};
13463
+ const out = [];
13464
+ for (const [name, resolver] of this.localResolvers) {
13465
+ const def = backends[name];
13466
+ if (!def || def.type !== "local" && def.type !== "pi") continue;
13467
+ out.push({
13468
+ ...resolver.getStatus(),
13469
+ backendName: name,
13470
+ endpoint: def.endpoint
13471
+ });
13472
+ }
13473
+ return out;
13474
+ }
12896
13475
  createTracker() {
12897
13476
  if (this.config.tracker.kind === "github-issues") {
12898
13477
  const trackerCfg = {
@@ -12919,48 +13498,30 @@ var Orchestrator = class extends EventEmitter {
12919
13498
  const logger = this.logger;
12920
13499
  const checkRunner = {
12921
13500
  run: async (command, cwd) => {
12922
- const { execFile: execFile7 } = await import("child_process");
12923
- const { promisify: promisify5 } = await import("util");
12924
- const execFileAsync2 = promisify5(execFile7);
12925
- const [cmd, ...args] = command;
12926
- if (!cmd) return { passed: true, findings: 0, output: "" };
12927
- try {
12928
- const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
12929
- const findingsMatch = stdout.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
12930
- const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 0;
12931
- return { passed: findings === 0, findings, output: stdout };
12932
- } catch (err) {
12933
- const error = err;
12934
- const output = [error.stdout, error.stderr].filter(Boolean).join("\n");
12935
- const findingsMatch = output.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
12936
- const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 1;
12937
- return { passed: false, findings, output };
12938
- }
12939
- }
12940
- };
12941
- const agentDispatcher = {
12942
- dispatch: async (skill, branch, backendName, cwd) => {
12943
- logger.info(
12944
- "Maintenance agent dispatcher invoked (stub \u2014 skill dispatch integration pending)",
12945
- {
12946
- skill,
12947
- branch,
12948
- backendName,
12949
- cwd
12950
- }
12951
- );
12952
- return { producedCommits: false, fixed: 0 };
13501
+ const [cmd, ...args] = normalizeHarnessCommand(command);
13502
+ if (!cmd) return { passed: true, findings: 0, output: "", executionFailed: false };
13503
+ return runHarnessCheck({ file: cmd, args }, cwd);
12953
13504
  }
12954
13505
  };
13506
+ const resolveBackend2 = makeBackendResolver(this.getBackends());
13507
+ const agentDispatcher = createAgentDispatcher({
13508
+ resolveBackend: resolveBackend2,
13509
+ git: (args, cwd) => execFileSync("git", args, { cwd, encoding: "utf-8" }).toString().trim(),
13510
+ logger
13511
+ });
12955
13512
  const commandExecutor = {
12956
13513
  exec: async (command, cwd) => {
12957
13514
  const { execFile: execFile7 } = await import("child_process");
12958
- const { promisify: promisify5 } = await import("util");
12959
- const execFileAsync2 = promisify5(execFile7);
12960
- const [cmd, ...args] = command;
13515
+ const { promisify: promisify6 } = await import("util");
13516
+ const execFileAsync2 = promisify6(execFile7);
13517
+ const [cmd, ...args] = normalizeHarnessCommand(command);
12961
13518
  if (!cmd) return { stdout: "" };
12962
13519
  try {
12963
- const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
13520
+ const { stdout } = await execFileAsync2(cmd, args, {
13521
+ cwd,
13522
+ timeout: MAINTENANCE_CHECK_TIMEOUT_MS,
13523
+ maxBuffer: MAINTENANCE_CHECK_MAX_BUFFER
13524
+ });
12964
13525
  return { stdout: String(stdout) };
12965
13526
  } catch (err) {
12966
13527
  logger.warn("Maintenance command execution failed", {
@@ -13095,6 +13656,10 @@ ${messages}`);
13095
13656
  async asyncTick() {
13096
13657
  await this.ensureClaimManager();
13097
13658
  await this.intelligenceRunner.loadPersistedData();
13659
+ if (!this.laneReadbackDone) {
13660
+ this.laneReadbackDone = true;
13661
+ await this.readBackPersistedLanes();
13662
+ }
13098
13663
  const nowMs = Date.now();
13099
13664
  this.setTickActivity("fetching", "Polling tracker for candidates");
13100
13665
  const candidatesResult = await this.tracker.fetchCandidateIssues();
@@ -13247,12 +13812,48 @@ ${messages}`);
13247
13812
  break;
13248
13813
  case "escalate":
13249
13814
  await this.handleEscalation(effect);
13815
+ await this.persistLaneSafe(effect.issueId, "abandon");
13250
13816
  break;
13251
13817
  case "claim":
13252
13818
  await this.handleClaimEffect(effect);
13253
13819
  break;
13254
13820
  }
13255
13821
  }
13822
+ /**
13823
+ * Phase 4 (DLane-5): persist an orchestrator lane transition to the durable
13824
+ * core event log at the effect boundary. NEVER throws — `persistLane` returns
13825
+ * an `Err` Result on failure, which is logged and swallowed here so a
13826
+ * lane-persistence failure can never break orchestrator dispatch.
13827
+ */
13828
+ async persistLaneSafe(issueId, signal) {
13829
+ const r = await persistLane(this.projectRoot, issueId, signal);
13830
+ if (!r.ok) {
13831
+ this.logger.warn(`lane persist failed for ${issueId} (${signal}): ${r.error.message}`);
13832
+ }
13833
+ }
13834
+ /**
13835
+ * Phase 4 (DLane-5): read persisted task lanes back from the durable log and
13836
+ * log a one-line summary. Stores the projection on `this.persistedLanes` for
13837
+ * observability. Read-only — never feeds reconciliation, never throws.
13838
+ */
13839
+ async readBackPersistedLanes() {
13840
+ const lanes = await readPersistedLanes(this.projectRoot);
13841
+ this.persistedLanes = lanes;
13842
+ const entries = Object.keys(lanes.tasks).map((id) => `${id}:${lanes.tasks[id]?.lane}`);
13843
+ const nonTerminal = entries.filter((e) => !e.endsWith(":done") && !e.endsWith(":canceled"));
13844
+ this.logger.info(
13845
+ `Lane read-back on startup: ${entries.length} persisted task(s), ${nonTerminal.length} non-terminal`,
13846
+ { nonTerminal }
13847
+ );
13848
+ }
13849
+ /**
13850
+ * Phase 4 (DLane-5): the task lanes most recently read back from the durable
13851
+ * log on startup, exposed for external observability. Read-only — a fresh
13852
+ * `{ tasks: {} }` until the first tick's read-back has run.
13853
+ */
13854
+ getPersistedLanes() {
13855
+ return this.persistedLanes;
13856
+ }
13256
13857
  /**
13257
13858
  * Guards workspace cleanup by checking whether the agent pushed a branch
13258
13859
  * that does not yet have a pull request. If so, the worktree is preserved
@@ -13439,6 +14040,7 @@ ${messages}`);
13439
14040
  }
13440
14041
  return;
13441
14042
  }
14043
+ await this.persistLaneSafe(effect.issue.id, "claim");
13442
14044
  await this.postClaimComment(effect.issue);
13443
14045
  await this.dispatchIssue(effect.issue, effect.attempt, effect.backend);
13444
14046
  }
@@ -13499,6 +14101,7 @@ ${messages}`);
13499
14101
  this.logger.info(`Dispatching issue: ${issue.identifier} (attempt ${attempt})`, {
13500
14102
  issueId: issue.id
13501
14103
  });
14104
+ await this.persistLaneSafe(issue.id, "dispatch");
13502
14105
  try {
13503
14106
  const workspaceResult = await this.workspace.ensureWorkspace(issue.identifier);
13504
14107
  if (!workspaceResult.ok) throw workspaceResult.error;
@@ -13707,6 +14310,7 @@ ${messages}`);
13707
14310
  * Informs the state machine that an agent worker has exited.
13708
14311
  */
13709
14312
  async emitWorkerExit(issueId, reason, attempt, error) {
14313
+ await this.persistLaneSafe(issueId, reason === "normal" ? "success" : "failure");
13710
14314
  await this.completionHandler.handleWorkerExit(
13711
14315
  issueId,
13712
14316
  reason,
@@ -14242,11 +14846,11 @@ function launchTUI(orchestrator) {
14242
14846
  }
14243
14847
 
14244
14848
  // src/maintenance/sync-main.ts
14245
- import { execFile as nodeExecFile } from "child_process";
14246
- import { promisify as promisify4 } from "util";
14849
+ import { execFile as nodeExecFile2 } from "child_process";
14850
+ import { promisify as promisify5 } from "util";
14247
14851
  var DEFAULT_TIMEOUT_MS3 = 6e4;
14248
14852
  async function git(execFileFn, args, cwd, timeoutMs) {
14249
- const exec = promisify4(execFileFn);
14853
+ const exec = promisify5(execFileFn);
14250
14854
  const { stdout, stderr } = await exec("git", args, { cwd, timeout: timeoutMs });
14251
14855
  return { stdout: String(stdout), stderr: String(stderr) };
14252
14856
  }
@@ -14308,7 +14912,7 @@ async function isAncestor(execFileFn, a, b, cwd, timeoutMs) {
14308
14912
  }
14309
14913
  }
14310
14914
  async function syncMain(repoRoot, opts = {}) {
14311
- const execFileFn = opts.execFileFn ?? nodeExecFile;
14915
+ const execFileFn = opts.execFileFn ?? nodeExecFile2;
14312
14916
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
14313
14917
  try {
14314
14918
  const originRef = await resolveOriginDefault(execFileFn, repoRoot, timeoutMs);
@@ -14385,8 +14989,56 @@ async function syncMain(repoRoot, opts = {}) {
14385
14989
  }
14386
14990
  }
14387
14991
 
14992
+ // src/maintenance/overdue.ts
14993
+ var LOOKBACK_DAYS = 366;
14994
+ function previousFireTime(schedule, now) {
14995
+ let day = new Date(now.getFullYear(), now.getMonth(), now.getDate());
14996
+ for (let d = 0; d <= LOOKBACK_DAYS; d++) {
14997
+ if (cronMatchesDate(schedule, day)) {
14998
+ const dayStartMs = day.getTime();
14999
+ const scanStart = d === 0 ? new Date(
15000
+ now.getFullYear(),
15001
+ now.getMonth(),
15002
+ now.getDate(),
15003
+ now.getHours(),
15004
+ now.getMinutes()
15005
+ ) : new Date(day.getFullYear(), day.getMonth(), day.getDate(), 23, 59);
15006
+ for (let ms = scanStart.getTime(); ms >= dayStartMs; ms -= 6e4) {
15007
+ const candidate = new Date(ms);
15008
+ if (cronMatchesNow(schedule, candidate)) return candidate;
15009
+ }
15010
+ }
15011
+ day = new Date(day.getFullYear(), day.getMonth(), day.getDate() - 1);
15012
+ }
15013
+ return null;
15014
+ }
15015
+ function isSatisfyingRun(r) {
15016
+ return r.status === "success" || r.status === "no-issues";
15017
+ }
15018
+ function isOverdue(task, history, now) {
15019
+ const satisfyingRuns = history.filter((r) => r.taskId === task.id && isSatisfyingRun(r));
15020
+ const fire = previousFireTime(task.schedule, now);
15021
+ if (fire === null) return satisfyingRuns.length === 0;
15022
+ const fireMs = fire.getTime();
15023
+ const satisfied = satisfyingRuns.some((r) => new Date(r.completedAt).getTime() >= fireMs);
15024
+ return !satisfied;
15025
+ }
15026
+ function selectTasks(tasks, history, filter) {
15027
+ const eligible = tasks.filter((t) => t.excludeFromHumanSweep !== true);
15028
+ switch (filter.mode) {
15029
+ case "all":
15030
+ return eligible;
15031
+ case "ids": {
15032
+ const wanted = new Set(filter.ids ?? []);
15033
+ return eligible.filter((t) => wanted.has(t.id));
15034
+ }
15035
+ case "overdue":
15036
+ return eligible.filter((t) => isOverdue(t, history, filter.now));
15037
+ }
15038
+ }
15039
+
14388
15040
  // src/sessions/search-index.ts
14389
- import * as fs18 from "fs";
15041
+ import * as fs16 from "fs";
14390
15042
  import * as path22 from "path";
14391
15043
  import Database2 from "better-sqlite3";
14392
15044
  import { INDEXED_FILE_KINDS } from "@harness-engineering/types";
@@ -14447,7 +15099,7 @@ var SqliteSearchIndex = class {
14447
15099
  removeSessionStmt;
14448
15100
  totalStmt;
14449
15101
  constructor(dbPath) {
14450
- fs18.mkdirSync(path22.dirname(dbPath), { recursive: true });
15102
+ fs16.mkdirSync(path22.dirname(dbPath), { recursive: true });
14451
15103
  this.db = new Database2(dbPath);
14452
15104
  this.db.pragma("journal_mode = WAL");
14453
15105
  this.db.pragma("synchronous = NORMAL");
@@ -14553,12 +15205,12 @@ function indexSessionDirectory(idx, args) {
14553
15205
  for (const kind of kinds) {
14554
15206
  const fileName = FILE_KIND_TO_FILENAME[kind];
14555
15207
  const filePath = path22.join(args.sessionDir, fileName);
14556
- if (!fs18.existsSync(filePath)) continue;
14557
- let body = fs18.readFileSync(filePath, "utf8");
15208
+ if (!fs16.existsSync(filePath)) continue;
15209
+ let body = fs16.readFileSync(filePath, "utf8");
14558
15210
  if (Buffer.byteLength(body, "utf8") > cap) {
14559
15211
  body = body.slice(0, cap) + "\n\n[TRUNCATED]";
14560
15212
  }
14561
- const stat = fs18.statSync(filePath);
15213
+ const stat = fs16.statSync(filePath);
14562
15214
  const relPath = path22.relative(args.projectPath, filePath).replaceAll("\\", "/");
14563
15215
  idx.upsertSessionDoc({
14564
15216
  sessionId: args.sessionId,
@@ -14580,8 +15232,8 @@ function reindexFromArchive(projectPath, opts = {}) {
14580
15232
  idx.resetArchived();
14581
15233
  let sessionsIndexed = 0;
14582
15234
  let docsWritten = 0;
14583
- if (fs18.existsSync(archiveBase)) {
14584
- const entries = fs18.readdirSync(archiveBase, { withFileTypes: true });
15235
+ if (fs16.existsSync(archiveBase)) {
15236
+ const entries = fs16.readdirSync(archiveBase, { withFileTypes: true });
14585
15237
  for (const entry of entries) {
14586
15238
  if (!entry.isDirectory()) continue;
14587
15239
  const sessionDir = path22.join(archiveBase, entry.name);
@@ -14604,12 +15256,12 @@ function reindexFromArchive(projectPath, opts = {}) {
14604
15256
  }
14605
15257
 
14606
15258
  // src/sessions/summarize.ts
14607
- import * as fs19 from "fs";
15259
+ import * as fs17 from "fs";
14608
15260
  import * as path23 from "path";
14609
15261
  import {
14610
15262
  SessionSummarySchema
14611
15263
  } from "@harness-engineering/types";
14612
- import { Ok as Ok24, Err as Err21 } from "@harness-engineering/types";
15264
+ import { Ok as Ok24, Err as Err23 } from "@harness-engineering/types";
14613
15265
  var LLM_SUMMARY_FILE = "llm-summary.md";
14614
15266
  var SUMMARY_INPUT_FILES = [
14615
15267
  { filename: "summary.md", kind: "summary" },
@@ -14636,9 +15288,9 @@ function readInputCorpus(archiveDir) {
14636
15288
  const parts = [];
14637
15289
  for (const { filename, kind } of SUMMARY_INPUT_FILES) {
14638
15290
  const p = path23.join(archiveDir, filename);
14639
- if (!fs19.existsSync(p)) continue;
15291
+ if (!fs17.existsSync(p)) continue;
14640
15292
  try {
14641
- const content = fs19.readFileSync(p, "utf8");
15293
+ const content = fs17.readFileSync(p, "utf8");
14642
15294
  if (content.trim().length === 0) continue;
14643
15295
  parts.push(`## FILE: ${kind}
14644
15296
 
@@ -14700,17 +15352,17 @@ status: failed
14700
15352
 
14701
15353
  - reason: ${reason}
14702
15354
  `;
14703
- fs19.writeFileSync(filePath, body, "utf8");
15355
+ fs17.writeFileSync(filePath, body, "utf8");
14704
15356
  return filePath;
14705
15357
  }
14706
15358
  async function summarizeArchivedSession(ctx) {
14707
15359
  const writeStubOnError = ctx.writeStubOnError ?? true;
14708
- if (!fs19.existsSync(ctx.archiveDir)) {
14709
- return Err21(new Error(`archive directory not found: ${ctx.archiveDir}`));
15360
+ if (!fs17.existsSync(ctx.archiveDir)) {
15361
+ return Err23(new Error(`archive directory not found: ${ctx.archiveDir}`));
14710
15362
  }
14711
15363
  const corpus = readInputCorpus(ctx.archiveDir);
14712
15364
  if (corpus.trim().length === 0) {
14713
- return Err21(new Error(`no summary input files found in ${ctx.archiveDir}`));
15365
+ return Err23(new Error(`no summary input files found in ${ctx.archiveDir}`));
14714
15366
  }
14715
15367
  const inputBudgetTokens = ctx.config?.inputBudgetTokens ?? DEFAULT_INPUT_BUDGET_TOKENS;
14716
15368
  const truncated = truncateForBudget(corpus, inputBudgetTokens);
@@ -14743,7 +15395,7 @@ async function summarizeArchivedSession(ctx) {
14743
15395
  } catch {
14744
15396
  }
14745
15397
  }
14746
- return Err21(
15398
+ return Err23(
14747
15399
  new Error(`session summary failed: ${reason}` + (stubPath ? ` (stub: ${stubPath})` : ""))
14748
15400
  );
14749
15401
  }
@@ -14757,7 +15409,7 @@ async function summarizeArchivedSession(ctx) {
14757
15409
  } catch {
14758
15410
  }
14759
15411
  }
14760
- return Err21(new Error(reason));
15412
+ return Err23(new Error(reason));
14761
15413
  }
14762
15414
  const meta = {
14763
15415
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -14768,7 +15420,7 @@ async function summarizeArchivedSession(ctx) {
14768
15420
  };
14769
15421
  const filePath = path23.join(ctx.archiveDir, LLM_SUMMARY_FILE);
14770
15422
  const body = renderLlmSummaryMarkdown(parsed.data, meta);
14771
- fs19.writeFileSync(filePath, body, "utf8");
15423
+ fs17.writeFileSync(filePath, body, "utf8");
14772
15424
  return Ok24({ summary: parsed.data, meta, filePath });
14773
15425
  }
14774
15426
  function isSummaryEnabled(config) {
@@ -14848,13 +15500,18 @@ export {
14848
15500
  BUILT_IN_TASKS,
14849
15501
  BackendDefSchema,
14850
15502
  BackendRouter,
15503
+ CheckScriptRunner,
14851
15504
  ClaimManager,
14852
15505
  GateNotReadyError,
14853
15506
  GateRunError,
14854
15507
  InteractionQueue,
15508
+ LinearGraphQLClient,
14855
15509
  LinearGraphQLStub,
14856
15510
  LocalModelResolver,
15511
+ MAINTENANCE_CHECK_MAX_BUFFER,
15512
+ MAINTENANCE_CHECK_TIMEOUT_MS,
14857
15513
  MAX_ATTEMPTS,
15514
+ MaintenanceReporter,
14858
15515
  MockBackend,
14859
15516
  ORCHESTRATOR_IDENTITY_FILE,
14860
15517
  Orchestrator,
@@ -14872,6 +15529,7 @@ export {
14872
15529
  SqliteSearchIndex,
14873
15530
  StreamRecorder,
14874
15531
  TaskOutputStore,
15532
+ TaskRunner,
14875
15533
  TokenStore,
14876
15534
  WebhookQueue,
14877
15535
  WorkflowLoader,
@@ -14882,7 +15540,9 @@ export {
14882
15540
  buildArchiveHooks,
14883
15541
  calculateRetryDelay,
14884
15542
  canDispatch,
15543
+ classifyCheckExecutionFailure,
14885
15544
  computeRateLimitDelay,
15545
+ createAgentDispatcher,
14886
15546
  createBackend,
14887
15547
  createEmptyState,
14888
15548
  crossFieldRoutingIssues,
@@ -14894,22 +15554,27 @@ export {
14894
15554
  emitProposalApproved,
14895
15555
  emitProposalCreated,
14896
15556
  emitProposalRejected,
15557
+ explicitFindingsCount,
14897
15558
  extractHighlights,
14898
15559
  extractTitlePrefix,
14899
15560
  getAvailableSlots,
14900
15561
  getDefaultConfig,
14901
15562
  getPerStateCount,
14902
15563
  indexSessionDirectory,
15564
+ isCheckTimeoutError,
14903
15565
  isEligible,
14904
15566
  isSummaryEnabled,
14905
15567
  launchTUI,
14906
15568
  loadPublishedIndex,
15569
+ makeBackendResolver,
14907
15570
  migrateAgentConfig,
14908
15571
  normalizeFts5Query,
15572
+ normalizeHarnessCommand,
14909
15573
  normalizeLocalModel,
14910
15574
  openSearchIndex,
14911
15575
  promote,
14912
15576
  reconcile,
15577
+ recoverFindingsCount,
14913
15578
  reindexFromArchive,
14914
15579
  renderAnalysisComment,
14915
15580
  renderLlmSummaryMarkdown,
@@ -14919,9 +15584,11 @@ export {
14919
15584
  routeIssue,
14920
15585
  routingWarnings,
14921
15586
  runGate,
15587
+ runHarnessCheck,
14922
15588
  savePublishedIndex,
14923
15589
  searchIndexPath,
14924
15590
  selectCandidates,
15591
+ selectTasks,
14925
15592
  sortCandidates,
14926
15593
  summarizeArchivedSession,
14927
15594
  syncMain,