@harness-engineering/orchestrator 0.8.4 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -34,13 +34,18 @@ __export(index_exports, {
34
34
  BUILT_IN_TASKS: () => BUILT_IN_TASKS,
35
35
  BackendDefSchema: () => BackendDefSchema,
36
36
  BackendRouter: () => BackendRouter,
37
+ CheckScriptRunner: () => CheckScriptRunner,
37
38
  ClaimManager: () => ClaimManager,
38
39
  GateNotReadyError: () => GateNotReadyError,
39
40
  GateRunError: () => GateRunError,
40
41
  InteractionQueue: () => InteractionQueue,
42
+ LinearGraphQLClient: () => LinearGraphQLClient,
41
43
  LinearGraphQLStub: () => LinearGraphQLStub,
42
44
  LocalModelResolver: () => LocalModelResolver,
45
+ MAINTENANCE_CHECK_MAX_BUFFER: () => MAINTENANCE_CHECK_MAX_BUFFER,
46
+ MAINTENANCE_CHECK_TIMEOUT_MS: () => MAINTENANCE_CHECK_TIMEOUT_MS,
43
47
  MAX_ATTEMPTS: () => MAX_ATTEMPTS,
48
+ MaintenanceReporter: () => MaintenanceReporter,
44
49
  MockBackend: () => MockBackend,
45
50
  ORCHESTRATOR_IDENTITY_FILE: () => ORCHESTRATOR_IDENTITY_FILE,
46
51
  Orchestrator: () => Orchestrator,
@@ -58,6 +63,7 @@ __export(index_exports, {
58
63
  SqliteSearchIndex: () => SqliteSearchIndex,
59
64
  StreamRecorder: () => StreamRecorder,
60
65
  TaskOutputStore: () => TaskOutputStore,
66
+ TaskRunner: () => TaskRunner,
61
67
  TokenStore: () => TokenStore,
62
68
  WebhookQueue: () => WebhookQueue,
63
69
  WorkflowLoader: () => WorkflowLoader,
@@ -68,7 +74,9 @@ __export(index_exports, {
68
74
  buildArchiveHooks: () => buildArchiveHooks,
69
75
  calculateRetryDelay: () => calculateRetryDelay,
70
76
  canDispatch: () => canDispatch,
77
+ classifyCheckExecutionFailure: () => classifyCheckExecutionFailure,
71
78
  computeRateLimitDelay: () => computeRateLimitDelay,
79
+ createAgentDispatcher: () => createAgentDispatcher,
72
80
  createBackend: () => createBackend,
73
81
  createEmptyState: () => createEmptyState,
74
82
  crossFieldRoutingIssues: () => crossFieldRoutingIssues,
@@ -80,22 +88,27 @@ __export(index_exports, {
80
88
  emitProposalApproved: () => emitProposalApproved,
81
89
  emitProposalCreated: () => emitProposalCreated,
82
90
  emitProposalRejected: () => emitProposalRejected,
91
+ explicitFindingsCount: () => explicitFindingsCount,
83
92
  extractHighlights: () => extractHighlights,
84
93
  extractTitlePrefix: () => extractTitlePrefix,
85
94
  getAvailableSlots: () => getAvailableSlots,
86
95
  getDefaultConfig: () => getDefaultConfig,
87
96
  getPerStateCount: () => getPerStateCount,
88
97
  indexSessionDirectory: () => indexSessionDirectory,
98
+ isCheckTimeoutError: () => isCheckTimeoutError,
89
99
  isEligible: () => isEligible,
90
100
  isSummaryEnabled: () => isSummaryEnabled,
91
101
  launchTUI: () => launchTUI,
92
102
  loadPublishedIndex: () => loadPublishedIndex,
103
+ makeBackendResolver: () => makeBackendResolver,
93
104
  migrateAgentConfig: () => migrateAgentConfig,
94
105
  normalizeFts5Query: () => normalizeFts5Query,
106
+ normalizeHarnessCommand: () => normalizeHarnessCommand,
95
107
  normalizeLocalModel: () => normalizeLocalModel,
96
108
  openSearchIndex: () => openSearchIndex,
97
109
  promote: () => promote,
98
110
  reconcile: () => reconcile,
111
+ recoverFindingsCount: () => recoverFindingsCount,
99
112
  reindexFromArchive: () => reindexFromArchive,
100
113
  renderAnalysisComment: () => renderAnalysisComment,
101
114
  renderLlmSummaryMarkdown: () => renderLlmSummaryMarkdown,
@@ -105,9 +118,11 @@ __export(index_exports, {
105
118
  routeIssue: () => routeIssue,
106
119
  routingWarnings: () => routingWarnings,
107
120
  runGate: () => runGate,
121
+ runHarnessCheck: () => runHarnessCheck,
108
122
  savePublishedIndex: () => savePublishedIndex,
109
123
  searchIndexPath: () => searchIndexPath,
110
124
  selectCandidates: () => selectCandidates,
125
+ selectTasks: () => selectTasks,
111
126
  sortCandidates: () => sortCandidates,
112
127
  summarizeArchivedSession: () => summarizeArchivedSession,
113
128
  syncMain: () => syncMain,
@@ -2327,7 +2342,6 @@ var WorkflowLoader = class {
2327
2342
  };
2328
2343
 
2329
2344
  // src/tracker/adapters/roadmap.ts
2330
- var fs8 = __toESM(require("fs/promises"));
2331
2345
  var import_node_crypto2 = require("crypto");
2332
2346
  var import_core = require("@harness-engineering/core");
2333
2347
  var import_types4 = require("@harness-engineering/types");
@@ -2358,8 +2372,7 @@ var RoadmapTrackerAdapter = class {
2358
2372
  async fetchIssuesByStates(stateNames) {
2359
2373
  try {
2360
2374
  if (!this.config.filePath) return (0, import_types4.Err)(new Error("Missing filePath"));
2361
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2362
- const roadmapResult = (0, import_core.parseRoadmap)(content);
2375
+ const roadmapResult = await this.loadRoadmap();
2363
2376
  if (!roadmapResult.ok) return roadmapResult;
2364
2377
  const issues = [];
2365
2378
  for (const milestone of roadmapResult.value.milestones) {
@@ -2390,17 +2403,19 @@ var RoadmapTrackerAdapter = class {
2390
2403
  if (!terminal) {
2391
2404
  return (0, import_types4.Err)(new Error("Tracker config has no terminalStates; cannot mark complete"));
2392
2405
  }
2393
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2394
- const roadmapResult = (0, import_core.parseRoadmap)(content);
2406
+ const store = this.resolveStore();
2407
+ const roadmapResult = await store.load();
2395
2408
  if (!roadmapResult.ok) return roadmapResult;
2396
2409
  const roadmap = roadmapResult.value;
2410
+ const before = structuredClone(roadmap);
2397
2411
  const target = this.findFeatureById(roadmap.milestones, issueId);
2398
2412
  if (!target) return (0, import_types4.Ok)(void 0);
2399
2413
  const normalizedTerminal = this.config.terminalStates.map((s) => s.toLowerCase());
2400
2414
  if (normalizedTerminal.includes(target.status.toLowerCase())) return (0, import_types4.Ok)(void 0);
2401
2415
  const now = (/* @__PURE__ */ new Date()).toISOString();
2402
2416
  (0, import_core.setStatus)(roadmap, target, terminal, now.slice(0, 10));
2403
- await fs8.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2417
+ const persisted = await (0, import_core.applyRoadmapDiff)(store, before, roadmap);
2418
+ if (!persisted.ok) return persisted;
2404
2419
  return (0, import_types4.Ok)(void 0);
2405
2420
  } catch (error) {
2406
2421
  return (0, import_types4.Err)(error instanceof Error ? error : new Error(String(error)));
@@ -2414,10 +2429,11 @@ var RoadmapTrackerAdapter = class {
2414
2429
  async claimIssue(issueId, orchestratorId) {
2415
2430
  try {
2416
2431
  if (!this.config.filePath) return (0, import_types4.Err)(new Error("Missing filePath"));
2417
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2418
- const roadmapResult = (0, import_core.parseRoadmap)(content);
2432
+ const store = this.resolveStore();
2433
+ const roadmapResult = await store.load();
2419
2434
  if (!roadmapResult.ok) return roadmapResult;
2420
2435
  const roadmap = roadmapResult.value;
2436
+ const before = structuredClone(roadmap);
2421
2437
  const target = this.findFeatureById(roadmap.milestones, issueId);
2422
2438
  if (!target) return (0, import_types4.Ok)(void 0);
2423
2439
  if (!(0, import_core.isClaimableBy)(target, orchestratorId)) {
@@ -2429,7 +2445,8 @@ var RoadmapTrackerAdapter = class {
2429
2445
  const now = (/* @__PURE__ */ new Date()).toISOString();
2430
2446
  (0, import_core.claim)(roadmap, target, orchestratorId, now.slice(0, 10));
2431
2447
  target.updatedAt = now;
2432
- await fs8.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2448
+ const persisted = await (0, import_core.applyRoadmapDiff)(store, before, roadmap);
2449
+ if (!persisted.ok) return persisted;
2433
2450
  return (0, import_types4.Ok)(void 0);
2434
2451
  } catch (error) {
2435
2452
  return (0, import_types4.Err)(error instanceof Error ? error : new Error(String(error)));
@@ -2446,10 +2463,11 @@ var RoadmapTrackerAdapter = class {
2446
2463
  if (!activeState) {
2447
2464
  return (0, import_types4.Err)(new Error("Tracker config has no activeStates; cannot release"));
2448
2465
  }
2449
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2450
- const roadmapResult = (0, import_core.parseRoadmap)(content);
2466
+ const store = this.resolveStore();
2467
+ const roadmapResult = await store.load();
2451
2468
  if (!roadmapResult.ok) return roadmapResult;
2452
2469
  const roadmap = roadmapResult.value;
2470
+ const before = structuredClone(roadmap);
2453
2471
  const target = this.findFeatureById(roadmap.milestones, issueId);
2454
2472
  if (!target) return (0, import_types4.Ok)(void 0);
2455
2473
  if (this.config.activeStates.includes(target.status) && target.assignee === null) {
@@ -2458,12 +2476,26 @@ var RoadmapTrackerAdapter = class {
2458
2476
  const now = (/* @__PURE__ */ new Date()).toISOString();
2459
2477
  (0, import_core.setStatus)(roadmap, target, activeState, now.slice(0, 10));
2460
2478
  target.updatedAt = null;
2461
- await fs8.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2479
+ const persisted = await (0, import_core.applyRoadmapDiff)(store, before, roadmap);
2480
+ if (!persisted.ok) return persisted;
2462
2481
  return (0, import_types4.Ok)(void 0);
2463
2482
  } catch (error) {
2464
2483
  return (0, import_types4.Err)(error instanceof Error ? error : new Error(String(error)));
2465
2484
  }
2466
2485
  }
2486
+ /**
2487
+ * Resolve the roadmap store anchored on the configured aggregate file path.
2488
+ * The shard backend is chosen when a sibling `roadmap.d/` exists next to the
2489
+ * file; otherwise the monolith file is read/written whole. Callers guard
2490
+ * `this.config.filePath` before invoking.
2491
+ */
2492
+ resolveStore() {
2493
+ return (0, import_core.resolveRoadmapStoreForFile)({ roadmapPath: this.config.filePath });
2494
+ }
2495
+ /** Load the roadmap via the store (sharded or monolith), returning a Result. */
2496
+ loadRoadmap() {
2497
+ return this.resolveStore().load();
2498
+ }
2467
2499
  findFeatureById(milestones, issueId) {
2468
2500
  for (const milestone of milestones) {
2469
2501
  for (const feature of milestone.features) {
@@ -2480,8 +2512,7 @@ var RoadmapTrackerAdapter = class {
2480
2512
  async fetchIssueStatesByIds(issueIds) {
2481
2513
  try {
2482
2514
  if (!this.config.filePath) return (0, import_types4.Err)(new Error("Missing filePath"));
2483
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2484
- const roadmapResult = (0, import_core.parseRoadmap)(content);
2515
+ const roadmapResult = await this.loadRoadmap();
2485
2516
  if (!roadmapResult.ok) return roadmapResult;
2486
2517
  const issueMap = /* @__PURE__ */ new Map();
2487
2518
  for (const milestone of roadmapResult.value.milestones) {
@@ -2537,6 +2568,56 @@ var RoadmapTrackerAdapter = class {
2537
2568
 
2538
2569
  // src/tracker/extensions/linear.ts
2539
2570
  var import_types5 = require("@harness-engineering/types");
2571
+ var LINEAR_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql";
2572
+ var LinearGraphQLClient = class {
2573
+ apiKey;
2574
+ endpoint;
2575
+ fetchFn;
2576
+ constructor(opts) {
2577
+ this.apiKey = opts.apiKey;
2578
+ this.endpoint = opts.endpoint ?? LINEAR_GRAPHQL_ENDPOINT;
2579
+ this.fetchFn = opts.fetchFn ?? globalThis.fetch;
2580
+ }
2581
+ async query(query, variables) {
2582
+ let res;
2583
+ try {
2584
+ res = await this.fetchFn(this.endpoint, {
2585
+ method: "POST",
2586
+ headers: {
2587
+ Authorization: this.apiKey,
2588
+ "Content-Type": "application/json"
2589
+ },
2590
+ body: JSON.stringify({ query, variables: variables ?? {} })
2591
+ });
2592
+ } catch (err) {
2593
+ return (0, import_types5.Err)(
2594
+ new Error(
2595
+ `Linear GraphQL request failed: ${err instanceof Error ? err.message : String(err)}`
2596
+ )
2597
+ );
2598
+ }
2599
+ if (!res.ok) {
2600
+ const body = await res.text().catch(() => "");
2601
+ const detail = body ? `: ${body.slice(0, 500)}` : "";
2602
+ return (0, import_types5.Err)(new Error(`Linear GraphQL HTTP ${res.status}${detail}`));
2603
+ }
2604
+ let envelope;
2605
+ try {
2606
+ envelope = await res.json();
2607
+ } catch (err) {
2608
+ return (0, import_types5.Err)(
2609
+ new Error(
2610
+ `Linear GraphQL response was not valid JSON: ${err instanceof Error ? err.message : String(err)}`
2611
+ )
2612
+ );
2613
+ }
2614
+ if (envelope.errors && envelope.errors.length > 0) {
2615
+ const message = envelope.errors.map((e) => e.message ?? "unknown error").join("; ");
2616
+ return (0, import_types5.Err)(new Error(`Linear GraphQL error: ${message}`));
2617
+ }
2618
+ return (0, import_types5.Ok)(envelope.data ?? {});
2619
+ }
2620
+ };
2540
2621
  var LinearGraphQLStub = class {
2541
2622
  async query(query, _variables) {
2542
2623
  console.log("Linear GraphQL query (stub):", query);
@@ -2545,7 +2626,7 @@ var LinearGraphQLStub = class {
2545
2626
  };
2546
2627
 
2547
2628
  // src/workspace/manager.ts
2548
- var fs9 = __toESM(require("fs/promises"));
2629
+ var fs8 = __toESM(require("fs/promises"));
2549
2630
  var path8 = __toESM(require("path"));
2550
2631
  var import_node_child_process2 = require("child_process");
2551
2632
  var import_node_util2 = require("util");
@@ -2585,7 +2666,7 @@ var WorkspaceManager = class _WorkspaceManager {
2585
2666
  async getRepoRoot() {
2586
2667
  if (this.repoRoot) return this.repoRoot;
2587
2668
  const root = path8.resolve(this.config.root);
2588
- await fs9.mkdir(root, { recursive: true });
2669
+ await fs8.mkdir(root, { recursive: true });
2589
2670
  const stdout = await this.git(["rev-parse", "--show-toplevel"], root);
2590
2671
  this.repoRoot = stdout.trim();
2591
2672
  return this.repoRoot;
@@ -2598,21 +2679,21 @@ var WorkspaceManager = class _WorkspaceManager {
2598
2679
  try {
2599
2680
  const workspacePath = path8.resolve(this.resolvePath(identifier));
2600
2681
  try {
2601
- await fs9.access(path8.join(workspacePath, ".git"));
2682
+ await fs8.access(path8.join(workspacePath, ".git"));
2602
2683
  const repoRoot2 = await this.getRepoRoot();
2603
2684
  try {
2604
2685
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
2605
2686
  } catch {
2606
- await fs9.rm(workspacePath, { recursive: true, force: true });
2687
+ await fs8.rm(workspacePath, { recursive: true, force: true });
2607
2688
  }
2608
2689
  } catch {
2609
2690
  try {
2610
- await fs9.access(workspacePath);
2691
+ await fs8.access(workspacePath);
2611
2692
  const repoRoot2 = await this.getRepoRoot();
2612
2693
  try {
2613
2694
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
2614
2695
  } catch {
2615
- await fs9.rm(workspacePath, { recursive: true, force: true });
2696
+ await fs8.rm(workspacePath, { recursive: true, force: true });
2616
2697
  }
2617
2698
  } catch {
2618
2699
  }
@@ -2665,13 +2746,13 @@ var WorkspaceManager = class _WorkspaceManager {
2665
2746
  }
2666
2747
  const src = path8.join(repoRoot, rel);
2667
2748
  try {
2668
- await fs9.access(src);
2749
+ await fs8.access(src);
2669
2750
  } catch {
2670
2751
  continue;
2671
2752
  }
2672
2753
  const dest = path8.join(workspacePath, rel);
2673
2754
  try {
2674
- await fs9.cp(src, dest, { recursive: true, force: true });
2755
+ await fs8.cp(src, dest, { recursive: true, force: true });
2675
2756
  } catch {
2676
2757
  }
2677
2758
  }
@@ -2747,7 +2828,7 @@ var WorkspaceManager = class _WorkspaceManager {
2747
2828
  async exists(identifier) {
2748
2829
  try {
2749
2830
  const workspacePath = this.resolvePath(identifier);
2750
- await fs9.access(workspacePath);
2831
+ await fs8.access(workspacePath);
2751
2832
  return true;
2752
2833
  } catch {
2753
2834
  return false;
@@ -2762,7 +2843,7 @@ var WorkspaceManager = class _WorkspaceManager {
2762
2843
  try {
2763
2844
  const workspacePath = path8.resolve(this.resolvePath(identifier));
2764
2845
  try {
2765
- await fs9.access(path8.join(workspacePath, ".git"));
2846
+ await fs8.access(path8.join(workspacePath, ".git"));
2766
2847
  } catch {
2767
2848
  return null;
2768
2849
  }
@@ -2873,7 +2954,7 @@ var WorkspaceManager = class _WorkspaceManager {
2873
2954
  const repoRoot = await this.getRepoRoot();
2874
2955
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot);
2875
2956
  } catch {
2876
- await fs9.rm(workspacePath, { recursive: true, force: true });
2957
+ await fs8.rm(workspacePath, { recursive: true, force: true });
2877
2958
  }
2878
2959
  return (0, import_types6.Ok)(void 0);
2879
2960
  } catch (error) {
@@ -3014,8 +3095,8 @@ var PromptRenderer = class {
3014
3095
  // src/orchestrator.ts
3015
3096
  var import_node_events = require("events");
3016
3097
  var path21 = __toESM(require("path"));
3017
- var import_node_crypto16 = require("crypto");
3018
- var import_core14 = require("@harness-engineering/core");
3098
+ var import_node_crypto15 = require("crypto");
3099
+ var import_core15 = require("@harness-engineering/core");
3019
3100
 
3020
3101
  // src/core/stall-detector.ts
3021
3102
  function detectStalledIssues(running, nowMs, stallTimeoutMs) {
@@ -3591,7 +3672,7 @@ var CompletionHandler = class {
3591
3672
  };
3592
3673
 
3593
3674
  // src/orchestrator.ts
3594
- var import_core15 = require("@harness-engineering/core");
3675
+ var import_core16 = require("@harness-engineering/core");
3595
3676
 
3596
3677
  // src/tracker/adapters/github-issues-issue-tracker.ts
3597
3678
  var import_types9 = require("@harness-engineering/types");
@@ -6511,6 +6592,96 @@ var OrchestratorBackendFactory = class {
6511
6592
  }
6512
6593
  };
6513
6594
 
6595
+ // src/agent/backend-resolver.ts
6596
+ function makeBackendResolver(backends) {
6597
+ return (backendName) => {
6598
+ const def = backends?.[backendName];
6599
+ return def ? createBackend(def) : null;
6600
+ };
6601
+ }
6602
+
6603
+ // src/maintenance/agent-dispatcher.ts
6604
+ function syntheticIssue(branch, skill) {
6605
+ return {
6606
+ id: `maintenance:${branch}`,
6607
+ identifier: branch,
6608
+ title: `Maintenance: ${skill}`,
6609
+ description: null,
6610
+ priority: null,
6611
+ state: "in_progress",
6612
+ branchName: branch,
6613
+ url: null,
6614
+ labels: ["maintenance"],
6615
+ blockedBy: [],
6616
+ spec: null,
6617
+ plans: [],
6618
+ createdAt: null,
6619
+ updatedAt: null,
6620
+ externalId: null
6621
+ };
6622
+ }
6623
+ function buildPrompt(skill, promptContext) {
6624
+ 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.`;
6625
+ return promptContext ? `${promptContext}
6626
+
6627
+ ${instruction}` : instruction;
6628
+ }
6629
+ function headOf(git2, cwd) {
6630
+ try {
6631
+ return git2(["rev-parse", "HEAD"], cwd) || null;
6632
+ } catch {
6633
+ return null;
6634
+ }
6635
+ }
6636
+ function createAgentDispatcher(deps) {
6637
+ const maxTurns = deps.maxTurns ?? 10;
6638
+ const dispatch = async (skill, branch, backendName, cwd, options) => {
6639
+ const backend = deps.resolveBackend(backendName);
6640
+ if (!backend) {
6641
+ deps.logger.warn(`Maintenance agent dispatch skipped: unknown backend "${backendName}"`, {
6642
+ skill,
6643
+ branch
6644
+ });
6645
+ return { producedCommits: false, fixed: 0 };
6646
+ }
6647
+ const before = headOf(deps.git, cwd);
6648
+ const runner = new AgentRunner(backend, { maxTurns });
6649
+ const session = runner.runSession(
6650
+ syntheticIssue(branch, skill),
6651
+ cwd,
6652
+ buildPrompt(skill, options?.promptContext)
6653
+ );
6654
+ let step = await session.next();
6655
+ while (!step.done) step = await session.next();
6656
+ const after = headOf(deps.git, cwd);
6657
+ let fixed = 0;
6658
+ if (after && after !== before) {
6659
+ if (before) {
6660
+ try {
6661
+ fixed = parseInt(deps.git(["rev-list", "--count", `${before}..${after}`], cwd), 10) || 0;
6662
+ } catch {
6663
+ fixed = 1;
6664
+ }
6665
+ } else {
6666
+ fixed = 1;
6667
+ }
6668
+ }
6669
+ const producedCommits = fixed > 0;
6670
+ deps.logger.info("Maintenance agent dispatch complete", {
6671
+ skill,
6672
+ branch,
6673
+ backendName,
6674
+ producedCommits,
6675
+ fixed
6676
+ });
6677
+ return { producedCommits, fixed };
6678
+ };
6679
+ return { dispatch };
6680
+ }
6681
+
6682
+ // src/orchestrator.ts
6683
+ var import_node_child_process13 = require("child_process");
6684
+
6514
6685
  // src/agent/intelligence-factory.ts
6515
6686
  var import_intelligence3 = require("@harness-engineering/intelligence");
6516
6687
  var import_graph = require("@harness-engineering/graph");
@@ -7012,7 +7183,7 @@ function handleV1InteractionsResolveRoute(req, res, queue) {
7012
7183
 
7013
7184
  // src/server/routes/plans.ts
7014
7185
  var import_zod5 = require("zod");
7015
- var fs10 = __toESM(require("fs/promises"));
7186
+ var fs9 = __toESM(require("fs/promises"));
7016
7187
  var path11 = __toESM(require("path"));
7017
7188
  var PlanWriteSchema = import_zod5.z.object({
7018
7189
  filename: import_zod5.z.string().min(1),
@@ -7041,9 +7212,9 @@ function handlePlansRoute(req, res, plansDir) {
7041
7212
  );
7042
7213
  return;
7043
7214
  }
7044
- await fs10.mkdir(plansDir, { recursive: true });
7215
+ await fs9.mkdir(plansDir, { recursive: true });
7045
7216
  const filePath = path11.join(plansDir, basename3);
7046
- await fs10.writeFile(filePath, parsed.content, "utf-8");
7217
+ await fs9.writeFile(filePath, parsed.content, "utf-8");
7047
7218
  res.writeHead(201, { "Content-Type": "application/json" });
7048
7219
  res.end(JSON.stringify({ ok: true, filename: basename3 }));
7049
7220
  } catch {
@@ -7418,7 +7589,6 @@ function handleAnalyzeRoute(req, res, pipeline) {
7418
7589
  }
7419
7590
 
7420
7591
  // src/server/routes/roadmap-actions.ts
7421
- var fs11 = __toESM(require("fs/promises"));
7422
7592
  var path12 = __toESM(require("path"));
7423
7593
  var import_core7 = require("@harness-engineering/core");
7424
7594
  var import_zod8 = require("zod");
@@ -7500,13 +7670,14 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
7500
7670
  sendJSON2(res, 400, { error: "Title must not contain newlines or markdown headings" });
7501
7671
  return;
7502
7672
  }
7503
- const content = await fs11.readFile(roadmapPath, "utf-8");
7504
- const roadmapResult = (0, import_core7.parseRoadmap)(content);
7505
- if (!roadmapResult.ok) {
7673
+ const store = (0, import_core7.resolveRoadmapStoreForFile)({ roadmapPath });
7674
+ const loaded = await store.load();
7675
+ if (!loaded.ok) {
7506
7676
  sendJSON2(res, 500, { error: "Failed to parse roadmap file" });
7507
7677
  return;
7508
7678
  }
7509
- const roadmap = roadmapResult.value;
7679
+ const roadmap = loaded.value;
7680
+ const before = structuredClone(roadmap);
7510
7681
  let backlog = roadmap.milestones.find((m) => m.isBacklog);
7511
7682
  if (!backlog) {
7512
7683
  backlog = { name: "Backlog", isBacklog: true, features: [] };
@@ -7529,10 +7700,11 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
7529
7700
  updatedAt: null
7530
7701
  });
7531
7702
  roadmap.frontmatter.lastManualEdit = (/* @__PURE__ */ new Date()).toISOString();
7532
- const tmpPath = roadmapPath + ".tmp";
7533
- const serialized = (0, import_core7.serializeRoadmap)(roadmap);
7534
- await fs11.writeFile(tmpPath, serialized, "utf-8");
7535
- await fs11.rename(tmpPath, roadmapPath);
7703
+ const persisted = await (0, import_core7.applyRoadmapDiff)(store, before, roadmap);
7704
+ if (!persisted.ok) {
7705
+ sendJSON2(res, 500, { error: persisted.error.message });
7706
+ return;
7707
+ }
7536
7708
  sendJSON2(res, 201, { ok: true, featureName: parsed.title });
7537
7709
  } catch (err) {
7538
7710
  const msg = err instanceof Error ? err.message : "Failed to append to roadmap";
@@ -7798,7 +7970,6 @@ function handleV1JobsMaintenanceRoute(req, res, deps) {
7798
7970
  }
7799
7971
 
7800
7972
  // src/server/routes/v1/events-sse.ts
7801
- var import_node_crypto8 = require("crypto");
7802
7973
  var SSE_TOPICS = [
7803
7974
  "state_change",
7804
7975
  "agent_event",
@@ -7814,8 +7985,55 @@ var SSE_TOPICS = [
7814
7985
  "webhook.subscription.deleted"
7815
7986
  ];
7816
7987
  var HEARTBEAT_MS = 15e3;
7817
- function newEventId() {
7818
- return `evt_${(0, import_node_crypto8.randomBytes)(8).toString("hex")}`;
7988
+ var DEFAULT_REPLAY_BUFFER = 1024;
7989
+ var SseEventLog = class {
7990
+ seq = 0;
7991
+ buffer = [];
7992
+ subscribers = /* @__PURE__ */ new Set();
7993
+ constructor(bus, cap = DEFAULT_REPLAY_BUFFER) {
7994
+ this.cap = cap;
7995
+ for (const topic of SSE_TOPICS) {
7996
+ bus.on(topic, (data) => this.record(topic, data));
7997
+ }
7998
+ }
7999
+ cap;
8000
+ record(topic, data) {
8001
+ const event = { id: ++this.seq, topic, data };
8002
+ this.buffer.push(event);
8003
+ if (this.buffer.length > this.cap) this.buffer.shift();
8004
+ for (const fn of this.subscribers) fn(event);
8005
+ }
8006
+ /** Highest assigned id (0 when nothing has been recorded yet). */
8007
+ currentSeq() {
8008
+ return this.seq;
8009
+ }
8010
+ /** Buffered events strictly newer than `lastId`, oldest-first. */
8011
+ replayFrom(lastId) {
8012
+ return this.buffer.filter((e) => e.id > lastId);
8013
+ }
8014
+ /** Register a live listener; returns an unsubscribe function. */
8015
+ subscribe(fn) {
8016
+ this.subscribers.add(fn);
8017
+ return () => {
8018
+ this.subscribers.delete(fn);
8019
+ };
8020
+ }
8021
+ };
8022
+ var logsByBus = /* @__PURE__ */ new WeakMap();
8023
+ function getSseEventLog(bus) {
8024
+ let log = logsByBus.get(bus);
8025
+ if (!log) {
8026
+ log = new SseEventLog(bus);
8027
+ logsByBus.set(bus, log);
8028
+ }
8029
+ return log;
8030
+ }
8031
+ function parseLastEventId(req) {
8032
+ const raw = req.headers["last-event-id"];
8033
+ const value = Array.isArray(raw) ? raw[0] : raw;
8034
+ if (value === void 0) return null;
8035
+ const n = Number(value);
8036
+ return Number.isInteger(n) && n >= 0 ? n : null;
7819
8037
  }
7820
8038
  function handleV1EventsSseRoute(req, res, bus) {
7821
8039
  if (req.method !== "GET" || req.url !== "/api/v1/events") return false;
@@ -7827,22 +8045,28 @@ function handleV1EventsSseRoute(req, res, bus) {
7827
8045
  res.write(`: harness gateway SSE \u2014 connected at ${(/* @__PURE__ */ new Date()).toISOString()}
7828
8046
 
7829
8047
  `);
7830
- const listeners = [];
7831
- for (const topic of SSE_TOPICS) {
7832
- const fn = (data) => {
7833
- try {
7834
- const frame = `event: ${topic}
7835
- data: ${JSON.stringify(data)}
7836
- id: ${newEventId()}
8048
+ const log = getSseEventLog(bus);
8049
+ const send = (e) => {
8050
+ try {
8051
+ res.write(`event: ${e.topic}
8052
+ data: ${JSON.stringify(e.data)}
8053
+ id: ${e.id}
7837
8054
 
7838
- `;
7839
- res.write(frame);
7840
- } catch {
7841
- }
7842
- };
7843
- bus.on(topic, fn);
7844
- listeners.push({ topic, fn });
8055
+ `);
8056
+ } catch {
8057
+ }
8058
+ };
8059
+ const lastId = parseLastEventId(req);
8060
+ let replayedThrough = lastId ?? log.currentSeq();
8061
+ if (lastId !== null) {
8062
+ for (const e of log.replayFrom(lastId)) {
8063
+ send(e);
8064
+ replayedThrough = e.id;
8065
+ }
7845
8066
  }
8067
+ const unsubscribe = log.subscribe((e) => {
8068
+ if (e.id > replayedThrough) send(e);
8069
+ });
7846
8070
  const heartbeat = setInterval(() => {
7847
8071
  try {
7848
8072
  res.write(": heartbeat\n\n");
@@ -7852,7 +8076,7 @@ id: ${newEventId()}
7852
8076
  heartbeat.unref();
7853
8077
  const cleanup = () => {
7854
8078
  clearInterval(heartbeat);
7855
- for (const { topic, fn } of listeners) bus.removeListener(topic, fn);
8079
+ unsubscribe();
7856
8080
  };
7857
8081
  res.on("close", cleanup);
7858
8082
  res.on("finish", cleanup);
@@ -8161,7 +8385,7 @@ async function runGate(projectPath, proposalId) {
8161
8385
  }
8162
8386
 
8163
8387
  // src/proposals/promote.ts
8164
- var fs12 = __toESM(require("fs"));
8388
+ var fs10 = __toESM(require("fs"));
8165
8389
  var path13 = __toESM(require("path"));
8166
8390
  var import_yaml4 = require("yaml");
8167
8391
  var import_core9 = require("@harness-engineering/core");
@@ -8183,7 +8407,7 @@ function skillDir(projectPath, name) {
8183
8407
  }
8184
8408
  function readIfExists(p) {
8185
8409
  try {
8186
- return fs12.readFileSync(p, "utf-8");
8410
+ return fs10.readFileSync(p, "utf-8");
8187
8411
  } catch {
8188
8412
  return null;
8189
8413
  }
@@ -8229,15 +8453,15 @@ function assertGateReady(proposal) {
8229
8453
  }
8230
8454
  async function promoteNewSkill(projectPath, proposal) {
8231
8455
  const target = skillDir(projectPath, proposal.content.name);
8232
- if (fs12.existsSync(target)) {
8456
+ if (fs10.existsSync(target)) {
8233
8457
  throw new PromotionError(
8234
8458
  `a catalog skill already exists at ${target}; use a refinement proposal to update it`
8235
8459
  );
8236
8460
  }
8237
- fs12.mkdirSync(target, { recursive: true });
8461
+ fs10.mkdirSync(target, { recursive: true });
8238
8462
  const yamlOut = injectProvenanceIntoYaml(proposal.content.skillYaml ?? "", proposal.id);
8239
- fs12.writeFileSync(path13.join(target, "skill.yaml"), yamlOut);
8240
- fs12.writeFileSync(path13.join(target, "SKILL.md"), proposal.content.skillMd ?? "");
8463
+ fs10.writeFileSync(path13.join(target, "skill.yaml"), yamlOut);
8464
+ fs10.writeFileSync(path13.join(target, "SKILL.md"), proposal.content.skillMd ?? "");
8241
8465
  return { skillPath: target };
8242
8466
  }
8243
8467
  async function promoteRefinement(projectPath, proposal) {
@@ -8245,7 +8469,7 @@ async function promoteRefinement(projectPath, proposal) {
8245
8469
  throw new PromotionError("refinement proposal is missing targetSkill");
8246
8470
  }
8247
8471
  const target = skillDir(projectPath, proposal.targetSkill);
8248
- if (!fs12.existsSync(target)) {
8472
+ if (!fs10.existsSync(target)) {
8249
8473
  throw new PromotionError(
8250
8474
  `target skill ${proposal.targetSkill} does not exist at ${target}; cannot refine`
8251
8475
  );
@@ -8258,7 +8482,7 @@ async function promoteRefinement(projectPath, proposal) {
8258
8482
  "no metadata changes detected; check that the reviewer applied the proposed diff before approving"
8259
8483
  );
8260
8484
  }
8261
- fs12.writeFileSync(yamlPath, after);
8485
+ fs10.writeFileSync(yamlPath, after);
8262
8486
  return { skillPath: target };
8263
8487
  }
8264
8488
  async function promote(projectPath, proposalId, decidedBy) {
@@ -8696,7 +8920,7 @@ function handleV1RoutingRoute(req, res, deps) {
8696
8920
  }
8697
8921
 
8698
8922
  // src/server/routes/sessions.ts
8699
- var fs13 = __toESM(require("fs/promises"));
8923
+ var fs11 = __toESM(require("fs/promises"));
8700
8924
  var path14 = __toESM(require("path"));
8701
8925
  var import_zod15 = require("zod");
8702
8926
  var SessionCreateSchema = import_zod15.z.object({
@@ -8717,12 +8941,12 @@ function extractSessionId(url) {
8717
8941
  }
8718
8942
  async function handleList2(res, sessionsDir) {
8719
8943
  try {
8720
- const entries = await fs13.readdir(sessionsDir, { withFileTypes: true });
8944
+ const entries = await fs11.readdir(sessionsDir, { withFileTypes: true });
8721
8945
  const sessions = [];
8722
8946
  for (const entry of entries) {
8723
8947
  if (!entry.isDirectory()) continue;
8724
8948
  try {
8725
- const content = await fs13.readFile(
8949
+ const content = await fs11.readFile(
8726
8950
  path14.join(sessionsDir, entry.name, "session.json"),
8727
8951
  "utf-8"
8728
8952
  );
@@ -8748,7 +8972,7 @@ async function handleGet2(res, id, sessionsDir) {
8748
8972
  return;
8749
8973
  }
8750
8974
  try {
8751
- const content = await fs13.readFile(path14.join(sessionsDir, id, "session.json"), "utf-8");
8975
+ const content = await fs11.readFile(path14.join(sessionsDir, id, "session.json"), "utf-8");
8752
8976
  jsonResponse(res, 200, JSON.parse(content));
8753
8977
  } catch (err) {
8754
8978
  if (err.code === "ENOENT") {
@@ -8772,8 +8996,8 @@ async function handleCreate(req, res, sessionsDir) {
8772
8996
  return;
8773
8997
  }
8774
8998
  const sessionDir = path14.join(sessionsDir, session.sessionId);
8775
- await fs13.mkdir(sessionDir, { recursive: true });
8776
- await fs13.writeFile(path14.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
8999
+ await fs11.mkdir(sessionDir, { recursive: true });
9000
+ await fs11.writeFile(path14.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
8777
9001
  jsonResponse(res, 200, { ok: true });
8778
9002
  } catch {
8779
9003
  jsonResponse(res, 500, { error: "Failed to save session" });
@@ -8789,8 +9013,8 @@ async function handleUpdate(req, res, url, sessionsDir) {
8789
9013
  const body = await readBody(req);
8790
9014
  const updates = import_zod15.z.record(import_zod15.z.unknown()).parse(JSON.parse(body));
8791
9015
  const sessionFilePath = path14.join(sessionsDir, id, "session.json");
8792
- const current = JSON.parse(await fs13.readFile(sessionFilePath, "utf-8"));
8793
- await fs13.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
9016
+ const current = JSON.parse(await fs11.readFile(sessionFilePath, "utf-8"));
9017
+ await fs11.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
8794
9018
  jsonResponse(res, 200, { ok: true });
8795
9019
  } catch {
8796
9020
  jsonResponse(res, 500, { error: "Failed to update session" });
@@ -8803,7 +9027,7 @@ async function handleDelete(res, url, sessionsDir) {
8803
9027
  jsonResponse(res, 400, { error: "Missing or invalid sessionId" });
8804
9028
  return;
8805
9029
  }
8806
- await fs13.rm(path14.join(sessionsDir, id), { recursive: true, force: true });
9030
+ await fs11.rm(path14.join(sessionsDir, id), { recursive: true, force: true });
8807
9031
  jsonResponse(res, 200, { ok: true });
8808
9032
  } catch {
8809
9033
  jsonResponse(res, 500, { error: "Failed to delete session" });
@@ -9047,7 +9271,7 @@ function handleLocalModelsRoute(req, res, getStatuses) {
9047
9271
  }
9048
9272
 
9049
9273
  // src/server/static.ts
9050
- var fs14 = __toESM(require("fs"));
9274
+ var fs12 = __toESM(require("fs"));
9051
9275
  var path15 = __toESM(require("path"));
9052
9276
  var MIME_TYPES = {
9053
9277
  ".html": "text/html; charset=utf-8",
@@ -9077,11 +9301,11 @@ function handleStaticFile(req, res, dashboardDir) {
9077
9301
  if (!resolved.startsWith(path15.resolve(dashboardDir))) {
9078
9302
  return serveFile(path15.join(dashboardDir, "index.html"), res);
9079
9303
  }
9080
- if (fs14.existsSync(resolved) && fs14.statSync(resolved).isFile()) {
9304
+ if (fs12.existsSync(resolved) && fs12.statSync(resolved).isFile()) {
9081
9305
  return serveFile(resolved, res);
9082
9306
  }
9083
9307
  const indexPath = path15.join(dashboardDir, "index.html");
9084
- if (fs14.existsSync(indexPath)) {
9308
+ if (fs12.existsSync(indexPath)) {
9085
9309
  return serveFile(indexPath, res);
9086
9310
  }
9087
9311
  return false;
@@ -9090,7 +9314,7 @@ function serveFile(filePath, res) {
9090
9314
  const ext = path15.extname(filePath).toLowerCase();
9091
9315
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
9092
9316
  try {
9093
- const content = fs14.readFileSync(filePath);
9317
+ const content = fs12.readFileSync(filePath);
9094
9318
  res.writeHead(200, { "Content-Type": contentType });
9095
9319
  res.end(content);
9096
9320
  return true;
@@ -9100,7 +9324,7 @@ function serveFile(filePath, res) {
9100
9324
  }
9101
9325
 
9102
9326
  // src/server/plan-watcher.ts
9103
- var fs15 = __toESM(require("fs"));
9327
+ var fs13 = __toESM(require("fs"));
9104
9328
  var path16 = __toESM(require("path"));
9105
9329
  var PlanWatcher = class {
9106
9330
  plansDir;
@@ -9115,11 +9339,11 @@ var PlanWatcher = class {
9115
9339
  * Creates the directory if it does not exist.
9116
9340
  */
9117
9341
  start() {
9118
- fs15.mkdirSync(this.plansDir, { recursive: true });
9119
- this.watcher = fs15.watch(this.plansDir, (eventType, filename) => {
9342
+ fs13.mkdirSync(this.plansDir, { recursive: true });
9343
+ this.watcher = fs13.watch(this.plansDir, (eventType, filename) => {
9120
9344
  if (eventType === "rename" && filename && filename.endsWith(".md")) {
9121
9345
  const filePath = path16.join(this.plansDir, filename);
9122
- if (fs15.existsSync(filePath)) {
9346
+ if (fs13.existsSync(filePath)) {
9123
9347
  void this.handleNewPlan(filename);
9124
9348
  }
9125
9349
  }
@@ -9150,7 +9374,7 @@ var PlanWatcher = class {
9150
9374
  };
9151
9375
 
9152
9376
  // src/auth/tokens.ts
9153
- var import_node_crypto9 = require("crypto");
9377
+ var import_node_crypto8 = require("crypto");
9154
9378
  var import_promises = require("fs/promises");
9155
9379
  var import_node_path = require("path");
9156
9380
  var import_bcryptjs = __toESM(require("bcryptjs"));
@@ -9158,10 +9382,10 @@ var import_types26 = require("@harness-engineering/types");
9158
9382
  var BCRYPT_ROUNDS = 12;
9159
9383
  var LEGACY_ENV_ID = "tok_legacy_env";
9160
9384
  function genId() {
9161
- return `tok_${(0, import_node_crypto9.randomBytes)(8).toString("hex")}`;
9385
+ return `tok_${(0, import_node_crypto8.randomBytes)(8).toString("hex")}`;
9162
9386
  }
9163
9387
  function genSecret() {
9164
- return (0, import_node_crypto9.randomBytes)(24).toString("base64url");
9388
+ return (0, import_node_crypto8.randomBytes)(24).toString("base64url");
9165
9389
  }
9166
9390
  function parseToken(raw) {
9167
9391
  const dot = raw.indexOf(".");
@@ -9192,7 +9416,7 @@ var TokenStore = class {
9192
9416
  }
9193
9417
  async persist(records) {
9194
9418
  await (0, import_promises.mkdir)((0, import_node_path.dirname)(this.path), { recursive: true });
9195
- const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${(0, import_node_crypto9.randomBytes)(4).toString("hex")}`;
9419
+ const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${(0, import_node_crypto8.randomBytes)(4).toString("hex")}`;
9196
9420
  await (0, import_promises.writeFile)(tmp, JSON.stringify(records, null, 2), "utf8");
9197
9421
  await (0, import_promises.rename)(tmp, this.path);
9198
9422
  this.cache = records;
@@ -9261,7 +9485,7 @@ var TokenStore = class {
9261
9485
  const a = Buffer.from(presented);
9262
9486
  const b = Buffer.from(envValue);
9263
9487
  if (a.length !== b.length) return null;
9264
- if (!(0, import_node_crypto9.timingSafeEqual)(a, b)) return null;
9488
+ if (!(0, import_node_crypto8.timingSafeEqual)(a, b)) return null;
9265
9489
  return {
9266
9490
  id: LEGACY_ENV_ID,
9267
9491
  name: "legacy-env",
@@ -9878,15 +10102,15 @@ var OrchestratorServer = class {
9878
10102
  };
9879
10103
 
9880
10104
  // src/gateway/webhooks/store.ts
9881
- var import_node_crypto11 = require("crypto");
10105
+ var import_node_crypto10 = require("crypto");
9882
10106
  var import_promises3 = require("fs/promises");
9883
10107
  var import_node_path3 = require("path");
9884
10108
  var import_types28 = require("@harness-engineering/types");
9885
10109
 
9886
10110
  // src/gateway/webhooks/signer.ts
9887
- var import_node_crypto10 = require("crypto");
10111
+ var import_node_crypto9 = require("crypto");
9888
10112
  function sign(secret, body) {
9889
- const hex = (0, import_node_crypto10.createHmac)("sha256", secret).update(body).digest("hex");
10113
+ const hex = (0, import_node_crypto9.createHmac)("sha256", secret).update(body).digest("hex");
9890
10114
  return `sha256=${hex}`;
9891
10115
  }
9892
10116
  function eventMatches(pattern, type) {
@@ -9905,10 +10129,10 @@ function eventMatches(pattern, type) {
9905
10129
 
9906
10130
  // src/gateway/webhooks/store.ts
9907
10131
  function genId2() {
9908
- return `whk_${(0, import_node_crypto11.randomBytes)(8).toString("hex")}`;
10132
+ return `whk_${(0, import_node_crypto10.randomBytes)(8).toString("hex")}`;
9909
10133
  }
9910
10134
  function genSecret2() {
9911
- return (0, import_node_crypto11.randomBytes)(32).toString("base64url");
10135
+ return (0, import_node_crypto10.randomBytes)(32).toString("base64url");
9912
10136
  }
9913
10137
  var WebhookStore = class {
9914
10138
  constructor(path24) {
@@ -9933,7 +10157,7 @@ var WebhookStore = class {
9933
10157
  return this.cache;
9934
10158
  }
9935
10159
  async persist(records) {
9936
- const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${(0, import_node_crypto11.randomBytes)(4).toString("hex")}`;
10160
+ const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${(0, import_node_crypto10.randomBytes)(4).toString("hex")}`;
9937
10161
  try {
9938
10162
  await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(this.path), { recursive: true });
9939
10163
  await (0, import_promises3.writeFile)(tmp, JSON.stringify(records, null, 2), { encoding: "utf8", mode: 384 });
@@ -9977,7 +10201,7 @@ var WebhookStore = class {
9977
10201
  };
9978
10202
 
9979
10203
  // src/gateway/webhooks/delivery.ts
9980
- var import_node_crypto12 = require("crypto");
10204
+ var import_node_crypto11 = require("crypto");
9981
10205
 
9982
10206
  // src/gateway/webhooks/queue.ts
9983
10207
  var import_better_sqlite3 = __toESM(require("better-sqlite3"));
@@ -10187,7 +10411,7 @@ var WebhookDelivery = class {
10187
10411
  enqueue(sub, event) {
10188
10412
  const payload = JSON.stringify(event);
10189
10413
  this.queue.insert({
10190
- id: `dlv_${(0, import_node_crypto12.randomBytes)(8).toString("hex")}`,
10414
+ id: `dlv_${(0, import_node_crypto11.randomBytes)(8).toString("hex")}`,
10191
10415
  subscriptionId: sub.id,
10192
10416
  eventType: event.type,
10193
10417
  payload
@@ -10295,7 +10519,7 @@ var WebhookDelivery = class {
10295
10519
  };
10296
10520
 
10297
10521
  // src/gateway/webhooks/events.ts
10298
- var import_node_crypto13 = require("crypto");
10522
+ var import_node_crypto12 = require("crypto");
10299
10523
  var WEBHOOK_TOPICS = [
10300
10524
  "interaction.created",
10301
10525
  "interaction.resolved",
@@ -10310,8 +10534,8 @@ var WEBHOOK_TOPICS = [
10310
10534
  "proposal.approved",
10311
10535
  "proposal.rejected"
10312
10536
  ];
10313
- function newEventId2() {
10314
- return `evt_${(0, import_node_crypto13.randomBytes)(8).toString("hex")}`;
10537
+ function newEventId() {
10538
+ return `evt_${(0, import_node_crypto12.randomBytes)(8).toString("hex")}`;
10315
10539
  }
10316
10540
  function wireWebhookFanout({ bus, store, delivery }) {
10317
10541
  const handlers = [];
@@ -10322,7 +10546,7 @@ function wireWebhookFanout({ bus, store, delivery }) {
10322
10546
  const subs = await store.listForEvent(eventType);
10323
10547
  if (subs.length === 0) return;
10324
10548
  const event = {
10325
- id: newEventId2(),
10549
+ id: newEventId(),
10326
10550
  type: eventType,
10327
10551
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10328
10552
  data
@@ -10341,7 +10565,7 @@ function wireWebhookFanout({ bus, store, delivery }) {
10341
10565
  }
10342
10566
 
10343
10567
  // src/gateway/telemetry/fanout.ts
10344
- var import_node_crypto14 = require("crypto");
10568
+ var import_node_crypto13 = require("crypto");
10345
10569
  var import_core12 = require("@harness-engineering/core");
10346
10570
  var TOPICS = {
10347
10571
  MAINTENANCE_STARTED: "maintenance:started",
@@ -10364,14 +10588,14 @@ var SPAN_NAME = {
10364
10588
  [TOPICS.DISPATCH_DECISION]: "dispatch_decision",
10365
10589
  [TOPICS.SKILL_INVOCATION]: "skill_invocation"
10366
10590
  };
10367
- function newEventId3() {
10368
- return `evt_${(0, import_node_crypto14.randomBytes)(8).toString("hex")}`;
10591
+ function newEventId2() {
10592
+ return `evt_${(0, import_node_crypto13.randomBytes)(8).toString("hex")}`;
10369
10593
  }
10370
10594
  function newTraceId() {
10371
- return (0, import_node_crypto14.randomBytes)(16).toString("hex");
10595
+ return (0, import_node_crypto13.randomBytes)(16).toString("hex");
10372
10596
  }
10373
10597
  function newSpanId() {
10374
- return (0, import_node_crypto14.randomBytes)(8).toString("hex");
10598
+ return (0, import_node_crypto13.randomBytes)(8).toString("hex");
10375
10599
  }
10376
10600
  function nowNs() {
10377
10601
  return BigInt(Date.now()) * 1000000n;
@@ -10485,7 +10709,7 @@ function wireTelemetryFanout(params) {
10485
10709
  };
10486
10710
  exporter.push(span);
10487
10711
  const gatewayEvent = {
10488
- id: newEventId3(),
10712
+ id: newEventId2(),
10489
10713
  type: TELEMETRY_TYPE[topic],
10490
10714
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10491
10715
  data: payload,
@@ -10679,7 +10903,7 @@ function buildSlackSink(config, options) {
10679
10903
  }
10680
10904
 
10681
10905
  // src/notifications/events.ts
10682
- var import_node_crypto15 = require("crypto");
10906
+ var import_node_crypto14 = require("crypto");
10683
10907
 
10684
10908
  // src/notifications/envelope.ts
10685
10909
  function asObj(data) {
@@ -10810,8 +11034,8 @@ var NOTIFICATION_TOPICS = [
10810
11034
  "proposal.approved",
10811
11035
  "proposal.rejected"
10812
11036
  ];
10813
- function newEventId4() {
10814
- return `evt_${(0, import_node_crypto15.randomBytes)(8).toString("hex")}`;
11037
+ function newEventId3() {
11038
+ return `evt_${(0, import_node_crypto14.randomBytes)(8).toString("hex")}`;
10815
11039
  }
10816
11040
  function dispatchToEntry(bus, entry, event) {
10817
11041
  const eventType = event.type;
@@ -10846,7 +11070,7 @@ function wireNotificationSinks({ bus, registry }) {
10846
11070
  const entries = registry.list();
10847
11071
  if (entries.length === 0) return;
10848
11072
  const event = {
10849
- id: newEventId4(),
11073
+ id: newEventId3(),
10850
11074
  type: eventType,
10851
11075
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10852
11076
  data
@@ -10864,7 +11088,7 @@ function wireNotificationSinks({ bus, registry }) {
10864
11088
  }
10865
11089
 
10866
11090
  // src/orchestrator.ts
10867
- var import_core16 = require("@harness-engineering/core");
11091
+ var import_core17 = require("@harness-engineering/core");
10868
11092
 
10869
11093
  // src/logging/logger.ts
10870
11094
  var StructuredLogger = class {
@@ -10949,6 +11173,37 @@ async function scanWorkspaceConfig(workspacePath) {
10949
11173
  return { exitCode: (0, import_core13.computeScanExitCode)(results), results };
10950
11174
  }
10951
11175
 
11176
+ // src/core/lane-persistence.ts
11177
+ var import_core14 = require("@harness-engineering/core");
11178
+ var import_types29 = require("@harness-engineering/types");
11179
+ var SIGNAL_TO_LANE = {
11180
+ claim: "claimed",
11181
+ dispatch: "in_progress",
11182
+ success: "in_review",
11183
+ failure: "blocked",
11184
+ abandon: "canceled"
11185
+ };
11186
+ function mapOrchestratorLane(signal) {
11187
+ return SIGNAL_TO_LANE[signal];
11188
+ }
11189
+ async function persistLane(projectPath, issueId, signal) {
11190
+ try {
11191
+ const reg = await import_core14.eventSourcing.registerTask(projectPath, issueId, []);
11192
+ if (!reg.ok) return reg;
11193
+ return await import_core14.eventSourcing.transitionLane(projectPath, issueId, mapOrchestratorLane(signal));
11194
+ } catch (err) {
11195
+ return (0, import_types29.Err)(err instanceof Error ? err : new Error(String(err)));
11196
+ }
11197
+ }
11198
+ async function readPersistedLanes(projectPath) {
11199
+ try {
11200
+ const snap = await import_core14.eventSourcing.readSnapshot(projectPath);
11201
+ return snap.ok ? snap.value.lanes : { tasks: {} };
11202
+ } catch {
11203
+ return { tasks: {} };
11204
+ }
11205
+ }
11206
+
10952
11207
  // src/maintenance/task-registry.ts
10953
11208
  var BUILT_IN_TASKS = [
10954
11209
  // --- Mechanical-AI ---
@@ -11012,7 +11267,13 @@ var BUILT_IN_TASKS = [
11012
11267
  description: "Detect and fix cross-check violations",
11013
11268
  schedule: "0 6 * * 1",
11014
11269
  branch: "harness-maint/cross-check-fixes",
11015
- checkCommand: ["validate-cross-check"],
11270
+ // `harness cross-check` is a dedicated read-only CLI subcommand that surfaces
11271
+ // JUST cross-artifact consistency (plan→implementation coverage + staleness),
11272
+ // mirroring the `validate_cross_check` MCP tool's core (`runCrossCheck`)
11273
+ // WITHOUT running the full `harness validate` suite. It prints a parseable
11274
+ // `Cross-check: N issues` line and exits 0 (clean) / 1 (N issues), so the
11275
+ // maintenance runner reports real results instead of an honest `failure`.
11276
+ checkCommand: ["cross-check"],
11016
11277
  fixSkill: "harness-cross-check-fix"
11017
11278
  },
11018
11279
  // --- Pure-AI ---
@@ -11071,7 +11332,10 @@ var BUILT_IN_TASKS = [
11071
11332
  description: "Assess overall project health",
11072
11333
  schedule: "0 6 * * *",
11073
11334
  branch: null,
11074
- checkCommand: ["assess_project"]
11335
+ // `assess_project` is an MCP tool name, not a CLI subcommand. The CLI
11336
+ // composite project-health report is `harness insights` (health, entropy,
11337
+ // decay, attention, impact) — a read-only report that records metrics.
11338
+ checkCommand: ["insights"]
11075
11339
  },
11076
11340
  {
11077
11341
  id: "stale-constraints",
@@ -11079,7 +11343,14 @@ var BUILT_IN_TASKS = [
11079
11343
  description: "Detect stale architectural constraints",
11080
11344
  schedule: "0 2 1 * *",
11081
11345
  branch: null,
11082
- checkCommand: ["detect_stale_constraints"]
11346
+ // `harness stale-constraints` is a dedicated read-only CLI subcommand that
11347
+ // surfaces the `detect_stale_constraints` MCP tool's core in-process. It is
11348
+ // precondition-gated on the knowledge graph: with no graph it emits the
11349
+ // "No knowledge graph found. Run `harness scan` first." signature and exits
11350
+ // non-zero, which the runner classifies as `skipped` (not failure). With a
11351
+ // graph it prints a parseable `Stale constraints: N findings` line and exits
11352
+ // 0 (clean) / 1 (N stale), recorded as report-only metrics.
11353
+ checkCommand: ["stale-constraints"]
11083
11354
  },
11084
11355
  {
11085
11356
  id: "graph-refresh",
@@ -11112,7 +11383,8 @@ var BUILT_IN_TASKS = [
11112
11383
  description: "Clean up stale orchestrator sessions",
11113
11384
  schedule: "0 0 * * *",
11114
11385
  branch: null,
11115
- checkCommand: ["cleanup-sessions"]
11386
+ checkCommand: ["cleanup-sessions"],
11387
+ excludeFromHumanSweep: true
11116
11388
  },
11117
11389
  {
11118
11390
  id: "perf-baselines",
@@ -11120,7 +11392,8 @@ var BUILT_IN_TASKS = [
11120
11392
  description: "Update performance baselines",
11121
11393
  schedule: "0 7 * * *",
11122
11394
  branch: null,
11123
- checkCommand: ["perf", "baselines", "update"]
11395
+ checkCommand: ["perf", "baselines", "update"],
11396
+ excludeFromHumanSweep: true
11124
11397
  },
11125
11398
  {
11126
11399
  id: "main-sync",
@@ -11128,7 +11401,8 @@ var BUILT_IN_TASKS = [
11128
11401
  description: "Fast-forward local default branch from origin",
11129
11402
  schedule: "*/15 * * * *",
11130
11403
  branch: null,
11131
- checkCommand: ["harness", "sync-main", "--json"]
11404
+ checkCommand: ["harness", "sync-main", "--json"],
11405
+ excludeFromHumanSweep: true
11132
11406
  },
11133
11407
  // Hermes Phase 4 — one-shot backfill that stamps `provenance: user-authored`
11134
11408
  // on every existing catalog skill. Schedule is Feb 31 (a date that never
@@ -11141,7 +11415,8 @@ var BUILT_IN_TASKS = [
11141
11415
  description: "Backfill provenance: user-authored on every existing skill (one-shot, idempotent)",
11142
11416
  schedule: "0 0 31 2 *",
11143
11417
  branch: null,
11144
- checkCommand: ["backfill-skill-provenance"]
11418
+ checkCommand: ["backfill-skill-provenance"],
11419
+ excludeFromHumanSweep: true
11145
11420
  }
11146
11421
  ];
11147
11422
 
@@ -11208,6 +11483,17 @@ function cronMatchesNow(expression, now) {
11208
11483
  const daysOfWeek = parseField(dowField, 0, 6);
11209
11484
  return minutes.has(minute) && hours.has(hour) && daysOfMonth.has(dayOfMonth) && months.has(month) && daysOfWeek.has(dayOfWeek);
11210
11485
  }
11486
+ function cronMatchesDate(expression, date) {
11487
+ const fields = expression.trim().split(/\s+/);
11488
+ if (fields.length !== 5) {
11489
+ throw new Error(`Invalid cron expression: expected 5 fields, got ${fields.length}`);
11490
+ }
11491
+ const [, , domField, monthField, dowField] = fields;
11492
+ const daysOfMonth = parseField(domField, 1, 31);
11493
+ const months = parseField(monthField, 1, 12);
11494
+ const daysOfWeek = parseField(dowField, 0, 6);
11495
+ return daysOfMonth.has(date.getDate()) && months.has(date.getMonth() + 1) && daysOfWeek.has(date.getDay());
11496
+ }
11211
11497
 
11212
11498
  // src/maintenance/scheduler.ts
11213
11499
  var MaintenanceScheduler = class {
@@ -11449,15 +11735,15 @@ var MaintenanceScheduler = class {
11449
11735
  };
11450
11736
 
11451
11737
  // src/maintenance/leader-elector.ts
11452
- var import_types29 = require("@harness-engineering/types");
11738
+ var import_types30 = require("@harness-engineering/types");
11453
11739
  var SingleProcessLeaderElector = class {
11454
11740
  async electLeader() {
11455
- return (0, import_types29.Ok)("claimed");
11741
+ return (0, import_types30.Ok)("claimed");
11456
11742
  }
11457
11743
  };
11458
11744
 
11459
11745
  // src/maintenance/reporter.ts
11460
- var fs16 = __toESM(require("fs"));
11746
+ var fs14 = __toESM(require("fs"));
11461
11747
  var path18 = __toESM(require("path"));
11462
11748
  var import_zod17 = require("zod");
11463
11749
  var RunResultSchema = import_zod17.z.object({
@@ -11493,9 +11779,9 @@ var MaintenanceReporter = class {
11493
11779
  */
11494
11780
  async load() {
11495
11781
  try {
11496
- await fs16.promises.mkdir(this.persistDir, { recursive: true });
11782
+ await fs14.promises.mkdir(this.persistDir, { recursive: true });
11497
11783
  const filePath = path18.join(this.persistDir, "history.json");
11498
- const data = await fs16.promises.readFile(filePath, "utf-8");
11784
+ const data = await fs14.promises.readFile(filePath, "utf-8");
11499
11785
  const parsed = import_zod17.z.array(RunResultSchema).safeParse(JSON.parse(data));
11500
11786
  if (parsed.success) {
11501
11787
  this.history = parsed.data.slice(0, MAX_HISTORY);
@@ -11529,9 +11815,9 @@ var MaintenanceReporter = class {
11529
11815
  */
11530
11816
  async persist() {
11531
11817
  try {
11532
- await fs16.promises.mkdir(this.persistDir, { recursive: true });
11818
+ await fs14.promises.mkdir(this.persistDir, { recursive: true });
11533
11819
  const filePath = path18.join(this.persistDir, "history.json");
11534
- await fs16.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
11820
+ await fs14.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
11535
11821
  } catch (err) {
11536
11822
  this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
11537
11823
  }
@@ -11571,20 +11857,20 @@ var TaskRunner = class {
11571
11857
  * @param origin - Hermes Phase 2 trigger-source tag; defaults to `'cron'`
11572
11858
  * when called from the scheduler path.
11573
11859
  */
11574
- async run(task, origin = "cron") {
11860
+ async run(task, origin = "cron", mode = "fix") {
11575
11861
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
11576
11862
  let result;
11577
11863
  let captured;
11578
11864
  try {
11579
11865
  switch (task.type) {
11580
11866
  case "mechanical-ai": {
11581
- const out = await this.runMechanicalAI(task, startedAt);
11867
+ const out = await this.runMechanicalAI(task, startedAt, mode);
11582
11868
  result = out.result;
11583
11869
  captured = out.captured;
11584
11870
  break;
11585
11871
  }
11586
11872
  case "pure-ai":
11587
- result = await this.runPureAI(task, startedAt);
11873
+ result = await this.runPureAI(task, startedAt, mode);
11588
11874
  break;
11589
11875
  case "report-only": {
11590
11876
  const out = await this.runReportOnly(task, startedAt);
@@ -11593,7 +11879,7 @@ var TaskRunner = class {
11593
11879
  break;
11594
11880
  }
11595
11881
  case "housekeeping": {
11596
- const out = await this.runHousekeeping(task, startedAt);
11882
+ const out = await this.runHousekeeping(task, startedAt, mode);
11597
11883
  result = out.result;
11598
11884
  captured = out.captured;
11599
11885
  break;
@@ -11657,7 +11943,8 @@ var TaskRunner = class {
11657
11943
  findings: r2.findings,
11658
11944
  stdout: r2.output,
11659
11945
  stderr: r2.stderr,
11660
- structured: r2.structured ? r2.structured : null
11946
+ structured: r2.structured ? r2.structured : null,
11947
+ executionFailed: false
11661
11948
  };
11662
11949
  }
11663
11950
  if (!task.checkCommand || task.checkCommand.length === 0) {
@@ -11669,7 +11956,8 @@ var TaskRunner = class {
11669
11956
  findings: r.findings,
11670
11957
  stdout: r.output,
11671
11958
  stderr: "",
11672
- structured: null
11959
+ structured: null,
11960
+ executionFailed: r.executionFailed ?? false
11673
11961
  };
11674
11962
  }
11675
11963
  /**
@@ -11694,7 +11982,7 @@ var TaskRunner = class {
11694
11982
  * only if fixable findings exist; persist captured stdout/stderr/context
11695
11983
  * via the output store on the way out.
11696
11984
  */
11697
- async runMechanicalAI(task, startedAt) {
11985
+ async runMechanicalAI(task, startedAt, mode = "fix") {
11698
11986
  if (!task.fixSkill) {
11699
11987
  return wrap(this.failureResult(task.id, startedAt, "mechanical-ai task missing fixSkill"));
11700
11988
  }
@@ -11716,6 +12004,27 @@ var TaskRunner = class {
11716
12004
  } catch (err) {
11717
12005
  return wrap(this.failureResult(task.id, startedAt, String(err)));
11718
12006
  }
12007
+ if (check.executionFailed) {
12008
+ const cls = classifyCheckExecutionFailure(`${check.stdout}
12009
+ ${check.stderr}`);
12010
+ if (cls.kind === "unrunnable") {
12011
+ return wrap(
12012
+ this.failureResult(task.id, startedAt, checkExecutionError(check)),
12013
+ captureFromCheck(check)
12014
+ );
12015
+ }
12016
+ if (cls.kind === "precondition") {
12017
+ return wrap(
12018
+ this.skippedResult(task.id, startedAt, cls.reason ?? "precondition not met"),
12019
+ captureFromCheck(check)
12020
+ );
12021
+ }
12022
+ check = {
12023
+ ...check,
12024
+ executionFailed: false,
12025
+ findings: check.findings > 0 ? check.findings : recoverFindingsCount(check.stdout)
12026
+ };
12027
+ }
11719
12028
  const promptContext = await this.composePromptContext(task);
11720
12029
  const baseCaptured = {
11721
12030
  stdout: check.stdout,
@@ -11724,13 +12033,14 @@ var TaskRunner = class {
11724
12033
  ...promptContext ? { context: promptContext } : {}
11725
12034
  };
11726
12035
  const wakeAgentExplicitlyFalse = check.structured !== null && typeof check.structured === "object" && check.structured.wakeAgent === false;
11727
- if (check.findings === 0 || wakeAgentExplicitlyFalse) {
12036
+ if (check.findings === 0 || wakeAgentExplicitlyFalse || mode === "report") {
12037
+ const reportedWithFindings = mode === "report" && check.findings > 0;
11728
12038
  return {
11729
12039
  result: {
11730
12040
  taskId: task.id,
11731
12041
  startedAt,
11732
12042
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
11733
- status: "no-issues",
12043
+ status: reportedWithFindings ? "success" : "no-issues",
11734
12044
  findings: check.findings,
11735
12045
  fixed: 0,
11736
12046
  prUrl: null,
@@ -11800,7 +12110,19 @@ var TaskRunner = class {
11800
12110
  /**
11801
12111
  * Pure-AI: always dispatch agent with configured skill.
11802
12112
  */
11803
- async runPureAI(task, startedAt) {
12113
+ async runPureAI(task, startedAt, mode = "fix") {
12114
+ if (mode === "report") {
12115
+ return {
12116
+ taskId: task.id,
12117
+ startedAt,
12118
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
12119
+ status: "no-issues",
12120
+ findings: 0,
12121
+ fixed: 0,
12122
+ prUrl: null,
12123
+ prUpdated: false
12124
+ };
12125
+ }
11804
12126
  if (!task.fixSkill) {
11805
12127
  return this.failureResult(task.id, startedAt, "pure-ai task missing fixSkill");
11806
12128
  }
@@ -11874,6 +12196,27 @@ var TaskRunner = class {
11874
12196
  return wrap(this.failureResult(task.id, startedAt, String(err)));
11875
12197
  }
11876
12198
  const parsed = parseStatusLine(check.stdout);
12199
+ if (parsed === null && check.executionFailed) {
12200
+ const cls = classifyCheckExecutionFailure(`${check.stdout}
12201
+ ${check.stderr}`);
12202
+ if (cls.kind === "unrunnable") {
12203
+ return wrap(
12204
+ this.failureResult(task.id, startedAt, checkExecutionError(check)),
12205
+ captureFromCheck(check)
12206
+ );
12207
+ }
12208
+ if (cls.kind === "precondition") {
12209
+ return wrap(
12210
+ this.skippedResult(task.id, startedAt, cls.reason ?? "precondition not met"),
12211
+ captureFromCheck(check)
12212
+ );
12213
+ }
12214
+ check = {
12215
+ ...check,
12216
+ executionFailed: false,
12217
+ findings: check.findings > 0 ? check.findings : recoverFindingsCount(check.stdout)
12218
+ };
12219
+ }
11877
12220
  const status = parsed?.status ?? "success";
11878
12221
  const findings = parsed === null ? check.findings : typeof parsed.candidatesFound === "number" ? parsed.candidatesFound : 0;
11879
12222
  const result = {
@@ -11907,7 +12250,20 @@ var TaskRunner = class {
11907
12250
  * Hermes Phase 2: a `checkScript` may replace `checkCommand` for housekeeping
11908
12251
  * tasks; the runner falls through to the same JSON-status parsing path.
11909
12252
  */
11910
- async runHousekeeping(task, startedAt) {
12253
+ async runHousekeeping(task, startedAt, mode = "fix") {
12254
+ if (mode === "report") {
12255
+ return wrap({
12256
+ taskId: task.id,
12257
+ startedAt,
12258
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
12259
+ status: "skipped",
12260
+ findings: 0,
12261
+ fixed: 0,
12262
+ prUrl: null,
12263
+ prUpdated: false,
12264
+ error: "skipped in report mode: housekeeping may mutate and report runs are read-only"
12265
+ });
12266
+ }
11911
12267
  if (!task.checkCommand && !task.checkScript) {
11912
12268
  return wrap(
11913
12269
  this.failureResult(
@@ -11974,10 +12330,86 @@ var TaskRunner = class {
11974
12330
  error
11975
12331
  };
11976
12332
  }
12333
+ /**
12334
+ * A precondition-gated check (e.g. `predict` with <3 snapshots, or a
12335
+ * graph-backed check before `harness scan`). The command is correctly
12336
+ * configured; the repo just lacks the state it needs. Reported as `skipped`
12337
+ * — distinct from a hard `failure` — carrying the refusal line as the reason
12338
+ * (surfaced in the run-report summary column). ADR 0050.
12339
+ */
12340
+ skippedResult(taskId, startedAt, reason) {
12341
+ return {
12342
+ taskId,
12343
+ startedAt,
12344
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
12345
+ status: "skipped",
12346
+ findings: 0,
12347
+ fixed: 0,
12348
+ prUrl: null,
12349
+ prUpdated: false,
12350
+ error: reason
12351
+ };
12352
+ }
11977
12353
  };
11978
12354
  function wrap(result, captured) {
11979
12355
  return captured ? { result, captured } : { result };
11980
12356
  }
12357
+ function checkExecutionError(check) {
12358
+ const detail = (check.stderr || check.stdout || "").trim();
12359
+ return detail ? `check command failed to execute: ${detail}` : "check command failed to execute";
12360
+ }
12361
+ function captureFromCheck(check) {
12362
+ return { stdout: check.stdout, stderr: check.stderr, structured: check.structured };
12363
+ }
12364
+ var PRECONDITION_PATTERNS = [
12365
+ /requires at least \d+ snapshots?/i,
12366
+ /Run ["'`]?harness snapshot/i,
12367
+ /no knowledge graph found/i,
12368
+ /Run ["'`]?harness scan/i,
12369
+ /no graph (?:available|found)/i
12370
+ ];
12371
+ var UNRUNNABLE_PATTERNS = [
12372
+ /unknown command/i,
12373
+ /unknown option/i,
12374
+ /\bENOENT\b/,
12375
+ /command not found/i,
12376
+ /cannot find module/i,
12377
+ // A timed-out check did not complete — it is a hard failure, never a
12378
+ // "ran-no-count" success. The runners synthesize this message on SIGTERM.
12379
+ /timed out/i,
12380
+ /\bETIMEDOUT\b/
12381
+ ];
12382
+ var TIMEOUT_SIGNATURE = /check timed out after \d+\s*ms/i;
12383
+ function explicitFindingsCount(output) {
12384
+ const after = output.match(/(?:findings?|issues?|violations?|errors?)\s*[:=]\s*(\d+)/i);
12385
+ if (after) return parseInt(after[1], 10);
12386
+ const before = output.match(/(\d+)\s+(?:findings?|issues?|violations?|errors?)\b/i);
12387
+ if (before) return parseInt(before[1], 10);
12388
+ return null;
12389
+ }
12390
+ function recoverFindingsCount(output) {
12391
+ return explicitFindingsCount(output) ?? 1;
12392
+ }
12393
+ function firstMeaningfulLine(output) {
12394
+ const line = output.split("\n").map((l) => l.trim()).find(Boolean);
12395
+ if (!line) return "precondition not met";
12396
+ return line.replace(/^[x✗✓!-]\s*/u, "").trim();
12397
+ }
12398
+ var CLASSIFY_HEAD_LINES = 3;
12399
+ function classifyCheckExecutionFailure(output) {
12400
+ const text = (output ?? "").trim();
12401
+ if (text.length === 0) return { kind: "unrunnable" };
12402
+ if (TIMEOUT_SIGNATURE.test(text)) return { kind: "unrunnable" };
12403
+ if (explicitFindingsCount(text) !== null) return { kind: "ran-no-count" };
12404
+ const head = text.split("\n").filter((l) => l.trim()).slice(0, CLASSIFY_HEAD_LINES).join("\n");
12405
+ for (const re of PRECONDITION_PATTERNS) {
12406
+ if (re.test(head)) return { kind: "precondition", reason: firstMeaningfulLine(text) };
12407
+ }
12408
+ for (const re of UNRUNNABLE_PATTERNS) {
12409
+ if (re.test(head)) return { kind: "unrunnable" };
12410
+ }
12411
+ return { kind: "ran-no-count" };
12412
+ }
11981
12413
  function parseStatusLine(output) {
11982
12414
  const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
11983
12415
  for (let i = lines.length - 1; i >= 0; i--) {
@@ -12126,8 +12558,49 @@ function heuristicResult(stdout, stderr, exitedAbnormally) {
12126
12558
  };
12127
12559
  }
12128
12560
 
12561
+ // src/maintenance/check-runner.ts
12562
+ var import_node_child_process12 = require("child_process");
12563
+ var import_node_util4 = require("util");
12564
+ var MAINTENANCE_CHECK_MAX_BUFFER = 64 * 1024 * 1024;
12565
+ var MAINTENANCE_CHECK_TIMEOUT_MS = 3e5;
12566
+ var FINDINGS_RE = /(\d+)\s+(?:finding|issue|violation|error)/i;
12567
+ var nodeExecFileAsync = (0, import_node_util4.promisify)(import_node_child_process12.execFile);
12568
+ function isCheckTimeoutError(e) {
12569
+ return e.killed === true || e.signal === "SIGTERM" || e.code === "ETIMEDOUT";
12570
+ }
12571
+ async function runHarnessCheck(spawn7, cwd, opts = {}) {
12572
+ const execFileAsync2 = opts.execFileAsync ?? nodeExecFileAsync;
12573
+ const timeoutMs = opts.timeoutMs ?? MAINTENANCE_CHECK_TIMEOUT_MS;
12574
+ const maxBuffer = opts.maxBuffer ?? MAINTENANCE_CHECK_MAX_BUFFER;
12575
+ try {
12576
+ const { stdout } = await execFileAsync2(spawn7.file, spawn7.args, {
12577
+ cwd,
12578
+ timeout: timeoutMs,
12579
+ maxBuffer
12580
+ });
12581
+ const text = String(stdout);
12582
+ const m = text.match(FINDINGS_RE);
12583
+ const findings = m ? parseInt(m[1], 10) : 0;
12584
+ return { passed: findings === 0, findings, output: text, executionFailed: false };
12585
+ } catch (err) {
12586
+ const e = err;
12587
+ let output = [e.stdout, e.stderr].map((v) => v == null ? "" : String(v)).filter(Boolean).join("\n");
12588
+ if (isCheckTimeoutError(e)) {
12589
+ const note = `check timed out after ${timeoutMs}ms`;
12590
+ output = output.trim() ? `${output}
12591
+ ${note}` : note;
12592
+ return { passed: false, findings: 0, output, executionFailed: true };
12593
+ }
12594
+ const m = output.match(FINDINGS_RE);
12595
+ if (m) {
12596
+ return { passed: false, findings: parseInt(m[1], 10), output, executionFailed: false };
12597
+ }
12598
+ return { passed: false, findings: 0, output, executionFailed: true };
12599
+ }
12600
+ }
12601
+
12129
12602
  // src/maintenance/output-store.ts
12130
- var fs17 = __toESM(require("fs"));
12603
+ var fs15 = __toESM(require("fs"));
12131
12604
  var path20 = __toESM(require("path"));
12132
12605
  var DEFAULT_RETENTION = {
12133
12606
  runs: 50,
@@ -12168,13 +12641,13 @@ var TaskOutputStore = class {
12168
12641
  async write(taskId, entry, retention) {
12169
12642
  this.ensureSafeTaskId(taskId);
12170
12643
  const dir = this.dirFor(taskId);
12171
- await fs17.promises.mkdir(dir, { recursive: true });
12644
+ await fs15.promises.mkdir(dir, { recursive: true });
12172
12645
  const fileName = `${sanitizeIso(entry.completedAt || (/* @__PURE__ */ new Date()).toISOString())}.json`;
12173
12646
  const filePath = path20.join(dir, fileName);
12174
12647
  const tmpPath = `${filePath}.tmp`;
12175
12648
  const payload = JSON.stringify(entry, null, 2);
12176
- await fs17.promises.writeFile(tmpPath, payload, "utf-8");
12177
- await fs17.promises.rename(tmpPath, filePath);
12649
+ await fs15.promises.writeFile(tmpPath, payload, "utf-8");
12650
+ await fs15.promises.rename(tmpPath, filePath);
12178
12651
  try {
12179
12652
  await this.applyRetention(taskId, retention);
12180
12653
  } catch (err) {
@@ -12225,7 +12698,7 @@ var TaskOutputStore = class {
12225
12698
  }
12226
12699
  async readEntry(filePath) {
12227
12700
  try {
12228
- const buf = await fs17.promises.readFile(filePath, "utf-8");
12701
+ const buf = await fs15.promises.readFile(filePath, "utf-8");
12229
12702
  const parsed = JSON.parse(buf);
12230
12703
  return parsed;
12231
12704
  } catch {
@@ -12247,7 +12720,7 @@ var TaskOutputStore = class {
12247
12720
  const toRemove = /* @__PURE__ */ new Set([...overflow, ...aged]);
12248
12721
  for (const name of toRemove) {
12249
12722
  try {
12250
- await fs17.promises.unlink(path20.join(dir, name));
12723
+ await fs15.promises.unlink(path20.join(dir, name));
12251
12724
  } catch {
12252
12725
  }
12253
12726
  }
@@ -12256,7 +12729,7 @@ var TaskOutputStore = class {
12256
12729
  async function listJsonFilesDescending(dir) {
12257
12730
  let names;
12258
12731
  try {
12259
- names = await fs17.promises.readdir(dir);
12732
+ names = await fs15.promises.readdir(dir);
12260
12733
  } catch {
12261
12734
  return [];
12262
12735
  }
@@ -12365,7 +12838,7 @@ var fallbackLogger3 = {
12365
12838
  };
12366
12839
 
12367
12840
  // src/maintenance/custom-task-validator.ts
12368
- var import_types30 = require("@harness-engineering/types");
12841
+ var import_types31 = require("@harness-engineering/types");
12369
12842
  var ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
12370
12843
  var REQUIRED_FIELDS_BY_TYPE = {
12371
12844
  "mechanical-ai": ["branch", "fixSkill"],
@@ -12375,7 +12848,7 @@ var REQUIRED_FIELDS_BY_TYPE = {
12375
12848
  };
12376
12849
  function validateCustomTasks(customTasks, builtIns, deps = {}) {
12377
12850
  const errors = [];
12378
- if (!customTasks) return (0, import_types30.Ok)(void 0);
12851
+ if (!customTasks) return (0, import_types31.Ok)(void 0);
12379
12852
  const builtInIds = new Set(builtIns.map((t) => t.id));
12380
12853
  const customIds = Object.keys(customTasks);
12381
12854
  const allIds = /* @__PURE__ */ new Set([...builtInIds, ...customIds]);
@@ -12385,7 +12858,7 @@ function validateCustomTasks(customTasks, builtIns, deps = {}) {
12385
12858
  validateOne(id, task, builtInIds, allIds, deps, errors);
12386
12859
  }
12387
12860
  detectCycles(customTasks, builtIns, errors);
12388
- return errors.length === 0 ? (0, import_types30.Ok)(void 0) : (0, import_types30.Err)(errors);
12861
+ return errors.length === 0 ? (0, import_types31.Ok)(void 0) : (0, import_types31.Err)(errors);
12389
12862
  }
12390
12863
  function validateOne(id, task, builtInIds, allIds, deps, errors) {
12391
12864
  const prefix = `customTasks.${id}`;
@@ -12574,6 +13047,11 @@ function deriveSeedPaths(config) {
12574
13047
  const roadmapPath = config.tracker.kind === "roadmap" && config.tracker.filePath ? config.tracker.filePath : "docs/roadmap.md";
12575
13048
  return [".harness/proposals", roadmapPath];
12576
13049
  }
13050
+ function normalizeHarnessCommand(command) {
13051
+ if (command.length === 0) return [];
13052
+ if (command[0] === "harness") return command;
13053
+ return ["harness", ...command];
13054
+ }
12577
13055
  var Orchestrator = class extends import_node_events.EventEmitter {
12578
13056
  state;
12579
13057
  config;
@@ -12700,6 +13178,11 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12700
13178
  abortControllers = /* @__PURE__ */ new Map();
12701
13179
  /** Guards against overlapping ticks when a tick takes longer than the polling interval */
12702
13180
  tickInProgress = false;
13181
+ /** Phase 4 (DLane-5): lanes read back from the durable log on first tick, for
13182
+ * observability only — NOT fed into reconciliation. Empty until the first tick. */
13183
+ persistedLanes = { tasks: {} };
13184
+ /** Ensures the lane read-back diagnostic runs at most once (first tick). */
13185
+ laneReadbackDone = false;
12703
13186
  /** Timestamp of the last stale branch sweep (at most once per hour) */
12704
13187
  lastBranchSweepMs = 0;
12705
13188
  /** Current tick-phase activity visible to the dashboard */
@@ -12773,7 +13256,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12773
13256
  this.localResolvers.set(name, new LocalModelResolver(resolverOpts));
12774
13257
  }
12775
13258
  }
12776
- this.cacheMetrics = new import_core16.CacheMetricsRecorder();
13259
+ this.cacheMetrics = new import_core17.CacheMetricsRecorder();
12777
13260
  if (this.config.agent.backends !== void 0 && Object.keys(this.config.agent.backends).length > 0) {
12778
13261
  const sandboxPolicy = this.config.agent.sandboxPolicy === "docker" ? "docker" : "none";
12779
13262
  const firstBackendName = Object.keys(this.config.agent.backends)[0];
@@ -12859,7 +13342,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12859
13342
  this.setupNotifications(config.notifications);
12860
13343
  const otlpCfg = config.telemetry?.export?.otlp;
12861
13344
  if (otlpCfg) {
12862
- this.otlpExporter = new import_core16.OTLPExporter({
13345
+ this.otlpExporter = new import_core17.OTLPExporter({
12863
13346
  endpoint: otlpCfg.endpoint,
12864
13347
  ...otlpCfg.enabled !== void 0 ? { enabled: otlpCfg.enabled } : {},
12865
13348
  ...otlpCfg.headers !== void 0 ? { headers: otlpCfg.headers } : {},
@@ -12928,7 +13411,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12928
13411
  ...this.config.tracker.apiKey ? { token: this.config.tracker.apiKey } : {},
12929
13412
  ...this.config.tracker.endpoint ? { apiBase: this.config.tracker.endpoint } : {}
12930
13413
  };
12931
- const clientResult = (0, import_core15.createTrackerClient)(trackerCfg);
13414
+ const clientResult = (0, import_core16.createTrackerClient)(trackerCfg);
12932
13415
  if (!clientResult.ok) throw clientResult.error;
12933
13416
  return new GitHubIssuesIssueTrackerAdapter(clientResult.value, this.config.tracker);
12934
13417
  }
@@ -12946,48 +13429,30 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12946
13429
  const logger = this.logger;
12947
13430
  const checkRunner = {
12948
13431
  run: async (command, cwd) => {
12949
- const { execFile: execFile7 } = await import("child_process");
12950
- const { promisify: promisify5 } = await import("util");
12951
- const execFileAsync2 = promisify5(execFile7);
12952
- const [cmd, ...args] = command;
12953
- if (!cmd) return { passed: true, findings: 0, output: "" };
12954
- try {
12955
- const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
12956
- const findingsMatch = stdout.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
12957
- const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 0;
12958
- return { passed: findings === 0, findings, output: stdout };
12959
- } catch (err) {
12960
- const error = err;
12961
- const output = [error.stdout, error.stderr].filter(Boolean).join("\n");
12962
- const findingsMatch = output.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
12963
- const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 1;
12964
- return { passed: false, findings, output };
12965
- }
12966
- }
12967
- };
12968
- const agentDispatcher = {
12969
- dispatch: async (skill, branch, backendName, cwd) => {
12970
- logger.info(
12971
- "Maintenance agent dispatcher invoked (stub \u2014 skill dispatch integration pending)",
12972
- {
12973
- skill,
12974
- branch,
12975
- backendName,
12976
- cwd
12977
- }
12978
- );
12979
- return { producedCommits: false, fixed: 0 };
13432
+ const [cmd, ...args] = normalizeHarnessCommand(command);
13433
+ if (!cmd) return { passed: true, findings: 0, output: "", executionFailed: false };
13434
+ return runHarnessCheck({ file: cmd, args }, cwd);
12980
13435
  }
12981
13436
  };
13437
+ const resolveBackend2 = makeBackendResolver(this.getBackends());
13438
+ const agentDispatcher = createAgentDispatcher({
13439
+ resolveBackend: resolveBackend2,
13440
+ git: (args, cwd) => (0, import_node_child_process13.execFileSync)("git", args, { cwd, encoding: "utf-8" }).toString().trim(),
13441
+ logger
13442
+ });
12982
13443
  const commandExecutor = {
12983
13444
  exec: async (command, cwd) => {
12984
13445
  const { execFile: execFile7 } = await import("child_process");
12985
- const { promisify: promisify5 } = await import("util");
12986
- const execFileAsync2 = promisify5(execFile7);
12987
- const [cmd, ...args] = command;
13446
+ const { promisify: promisify6 } = await import("util");
13447
+ const execFileAsync2 = promisify6(execFile7);
13448
+ const [cmd, ...args] = normalizeHarnessCommand(command);
12988
13449
  if (!cmd) return { stdout: "" };
12989
13450
  try {
12990
- const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
13451
+ const { stdout } = await execFileAsync2(cmd, args, {
13452
+ cwd,
13453
+ timeout: MAINTENANCE_CHECK_TIMEOUT_MS,
13454
+ maxBuffer: MAINTENANCE_CHECK_MAX_BUFFER
13455
+ });
12991
13456
  return { stdout: String(stdout) };
12992
13457
  } catch (err) {
12993
13458
  logger.warn("Maintenance command execution failed", {
@@ -13122,6 +13587,10 @@ ${messages}`);
13122
13587
  async asyncTick() {
13123
13588
  await this.ensureClaimManager();
13124
13589
  await this.intelligenceRunner.loadPersistedData();
13590
+ if (!this.laneReadbackDone) {
13591
+ this.laneReadbackDone = true;
13592
+ await this.readBackPersistedLanes();
13593
+ }
13125
13594
  const nowMs = Date.now();
13126
13595
  this.setTickActivity("fetching", "Polling tracker for candidates");
13127
13596
  const candidatesResult = await this.tracker.fetchCandidateIssues();
@@ -13274,12 +13743,48 @@ ${messages}`);
13274
13743
  break;
13275
13744
  case "escalate":
13276
13745
  await this.handleEscalation(effect);
13746
+ await this.persistLaneSafe(effect.issueId, "abandon");
13277
13747
  break;
13278
13748
  case "claim":
13279
13749
  await this.handleClaimEffect(effect);
13280
13750
  break;
13281
13751
  }
13282
13752
  }
13753
+ /**
13754
+ * Phase 4 (DLane-5): persist an orchestrator lane transition to the durable
13755
+ * core event log at the effect boundary. NEVER throws — `persistLane` returns
13756
+ * an `Err` Result on failure, which is logged and swallowed here so a
13757
+ * lane-persistence failure can never break orchestrator dispatch.
13758
+ */
13759
+ async persistLaneSafe(issueId, signal) {
13760
+ const r = await persistLane(this.projectRoot, issueId, signal);
13761
+ if (!r.ok) {
13762
+ this.logger.warn(`lane persist failed for ${issueId} (${signal}): ${r.error.message}`);
13763
+ }
13764
+ }
13765
+ /**
13766
+ * Phase 4 (DLane-5): read persisted task lanes back from the durable log and
13767
+ * log a one-line summary. Stores the projection on `this.persistedLanes` for
13768
+ * observability. Read-only — never feeds reconciliation, never throws.
13769
+ */
13770
+ async readBackPersistedLanes() {
13771
+ const lanes = await readPersistedLanes(this.projectRoot);
13772
+ this.persistedLanes = lanes;
13773
+ const entries = Object.keys(lanes.tasks).map((id) => `${id}:${lanes.tasks[id]?.lane}`);
13774
+ const nonTerminal = entries.filter((e) => !e.endsWith(":done") && !e.endsWith(":canceled"));
13775
+ this.logger.info(
13776
+ `Lane read-back on startup: ${entries.length} persisted task(s), ${nonTerminal.length} non-terminal`,
13777
+ { nonTerminal }
13778
+ );
13779
+ }
13780
+ /**
13781
+ * Phase 4 (DLane-5): the task lanes most recently read back from the durable
13782
+ * log on startup, exposed for external observability. Read-only — a fresh
13783
+ * `{ tasks: {} }` until the first tick's read-back has run.
13784
+ */
13785
+ getPersistedLanes() {
13786
+ return this.persistedLanes;
13787
+ }
13283
13788
  /**
13284
13789
  * Guards workspace cleanup by checking whether the agent pushed a branch
13285
13790
  * that does not yet have a pull request. If so, the worktree is preserved
@@ -13312,7 +13817,7 @@ ${messages}`);
13312
13817
  { issueId }
13313
13818
  );
13314
13819
  await this.interactionQueue.push({
13315
- id: `interaction-${(0, import_node_crypto16.randomUUID)()}`,
13820
+ id: `interaction-${(0, import_node_crypto15.randomUUID)()}`,
13316
13821
  issueId,
13317
13822
  type: "needs-human",
13318
13823
  reasons: [`Agent pushed branch "${branch}" but did not create a PR. Worktree preserved.`],
@@ -13388,7 +13893,7 @@ ${messages}`);
13388
13893
  { issueId: effect.issueId }
13389
13894
  );
13390
13895
  await this.interactionQueue.push({
13391
- id: `interaction-${(0, import_node_crypto16.randomUUID)()}`,
13896
+ id: `interaction-${(0, import_node_crypto15.randomUUID)()}`,
13392
13897
  issueId: effect.issueId,
13393
13898
  type: "needs-human",
13394
13899
  reasons: effect.reasons,
@@ -13466,6 +13971,7 @@ ${messages}`);
13466
13971
  }
13467
13972
  return;
13468
13973
  }
13974
+ await this.persistLaneSafe(effect.issue.id, "claim");
13469
13975
  await this.postClaimComment(effect.issue);
13470
13976
  await this.dispatchIssue(effect.issue, effect.attempt, effect.backend);
13471
13977
  }
@@ -13484,12 +13990,12 @@ ${messages}`);
13484
13990
  async postLifecycleComment(identifier, externalId, event) {
13485
13991
  try {
13486
13992
  if (!externalId) return;
13487
- const trackerConfig = (0, import_core15.loadTrackerSyncConfig)(this.projectRoot);
13993
+ const trackerConfig = (0, import_core16.loadTrackerSyncConfig)(this.projectRoot);
13488
13994
  if (!trackerConfig) return;
13489
13995
  const token = process.env.GITHUB_TOKEN;
13490
13996
  if (!token) return;
13491
13997
  const orchestratorId = await this.orchestratorIdPromise;
13492
- const adapter = new import_core15.GitHubIssuesSyncAdapter({ token, config: trackerConfig });
13998
+ const adapter = new import_core16.GitHubIssuesSyncAdapter({ token, config: trackerConfig });
13493
13999
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
13494
14000
  const actionMap = {
13495
14001
  claimed: "Dispatching agent for autonomous execution",
@@ -13526,6 +14032,7 @@ ${messages}`);
13526
14032
  this.logger.info(`Dispatching issue: ${issue.identifier} (attempt ${attempt})`, {
13527
14033
  issueId: issue.id
13528
14034
  });
14035
+ await this.persistLaneSafe(issue.id, "dispatch");
13529
14036
  try {
13530
14037
  const workspaceResult = await this.workspace.ensureWorkspace(issue.identifier);
13531
14038
  if (!workspaceResult.ok) throw workspaceResult.error;
@@ -13562,7 +14069,7 @@ ${messages}`);
13562
14069
  ...f.line !== void 0 ? { line: f.line } : {}
13563
14070
  }))
13564
14071
  );
13565
- (0, import_core14.writeTaint)(
14072
+ (0, import_core15.writeTaint)(
13566
14073
  workspacePath,
13567
14074
  issue.id,
13568
14075
  "Medium-severity injection patterns found in workspace config files",
@@ -13734,6 +14241,7 @@ ${messages}`);
13734
14241
  * Informs the state machine that an agent worker has exited.
13735
14242
  */
13736
14243
  async emitWorkerExit(issueId, reason, attempt, error) {
14244
+ await this.persistLaneSafe(issueId, reason === "normal" ? "success" : "failure");
13737
14245
  await this.completionHandler.handleWorkerExit(
13738
14246
  issueId,
13739
14247
  reason,
@@ -14269,11 +14777,11 @@ function launchTUI(orchestrator) {
14269
14777
  }
14270
14778
 
14271
14779
  // src/maintenance/sync-main.ts
14272
- var import_node_child_process12 = require("child_process");
14273
- var import_node_util4 = require("util");
14780
+ var import_node_child_process14 = require("child_process");
14781
+ var import_node_util5 = require("util");
14274
14782
  var DEFAULT_TIMEOUT_MS3 = 6e4;
14275
14783
  async function git(execFileFn, args, cwd, timeoutMs) {
14276
- const exec = (0, import_node_util4.promisify)(execFileFn);
14784
+ const exec = (0, import_node_util5.promisify)(execFileFn);
14277
14785
  const { stdout, stderr } = await exec("git", args, { cwd, timeout: timeoutMs });
14278
14786
  return { stdout: String(stdout), stderr: String(stderr) };
14279
14787
  }
@@ -14335,7 +14843,7 @@ async function isAncestor(execFileFn, a, b, cwd, timeoutMs) {
14335
14843
  }
14336
14844
  }
14337
14845
  async function syncMain(repoRoot, opts = {}) {
14338
- const execFileFn = opts.execFileFn ?? import_node_child_process12.execFile;
14846
+ const execFileFn = opts.execFileFn ?? import_node_child_process14.execFile;
14339
14847
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
14340
14848
  try {
14341
14849
  const originRef = await resolveOriginDefault(execFileFn, repoRoot, timeoutMs);
@@ -14412,11 +14920,59 @@ async function syncMain(repoRoot, opts = {}) {
14412
14920
  }
14413
14921
  }
14414
14922
 
14923
+ // src/maintenance/overdue.ts
14924
+ var LOOKBACK_DAYS = 366;
14925
+ function previousFireTime(schedule, now) {
14926
+ let day = new Date(now.getFullYear(), now.getMonth(), now.getDate());
14927
+ for (let d = 0; d <= LOOKBACK_DAYS; d++) {
14928
+ if (cronMatchesDate(schedule, day)) {
14929
+ const dayStartMs = day.getTime();
14930
+ const scanStart = d === 0 ? new Date(
14931
+ now.getFullYear(),
14932
+ now.getMonth(),
14933
+ now.getDate(),
14934
+ now.getHours(),
14935
+ now.getMinutes()
14936
+ ) : new Date(day.getFullYear(), day.getMonth(), day.getDate(), 23, 59);
14937
+ for (let ms = scanStart.getTime(); ms >= dayStartMs; ms -= 6e4) {
14938
+ const candidate = new Date(ms);
14939
+ if (cronMatchesNow(schedule, candidate)) return candidate;
14940
+ }
14941
+ }
14942
+ day = new Date(day.getFullYear(), day.getMonth(), day.getDate() - 1);
14943
+ }
14944
+ return null;
14945
+ }
14946
+ function isSatisfyingRun(r) {
14947
+ return r.status === "success" || r.status === "no-issues";
14948
+ }
14949
+ function isOverdue(task, history, now) {
14950
+ const satisfyingRuns = history.filter((r) => r.taskId === task.id && isSatisfyingRun(r));
14951
+ const fire = previousFireTime(task.schedule, now);
14952
+ if (fire === null) return satisfyingRuns.length === 0;
14953
+ const fireMs = fire.getTime();
14954
+ const satisfied = satisfyingRuns.some((r) => new Date(r.completedAt).getTime() >= fireMs);
14955
+ return !satisfied;
14956
+ }
14957
+ function selectTasks(tasks, history, filter) {
14958
+ const eligible = tasks.filter((t) => t.excludeFromHumanSweep !== true);
14959
+ switch (filter.mode) {
14960
+ case "all":
14961
+ return eligible;
14962
+ case "ids": {
14963
+ const wanted = new Set(filter.ids ?? []);
14964
+ return eligible.filter((t) => wanted.has(t.id));
14965
+ }
14966
+ case "overdue":
14967
+ return eligible.filter((t) => isOverdue(t, history, filter.now));
14968
+ }
14969
+ }
14970
+
14415
14971
  // src/sessions/search-index.ts
14416
- var fs18 = __toESM(require("fs"));
14972
+ var fs16 = __toESM(require("fs"));
14417
14973
  var path22 = __toESM(require("path"));
14418
14974
  var import_better_sqlite32 = __toESM(require("better-sqlite3"));
14419
- var import_types31 = require("@harness-engineering/types");
14975
+ var import_types32 = require("@harness-engineering/types");
14420
14976
  var SEARCH_INDEX_FILE = "search-index.sqlite";
14421
14977
  var SCHEMA_SQL2 = `
14422
14978
  CREATE TABLE IF NOT EXISTS session_docs (
@@ -14474,7 +15030,7 @@ var SqliteSearchIndex = class {
14474
15030
  removeSessionStmt;
14475
15031
  totalStmt;
14476
15032
  constructor(dbPath) {
14477
- fs18.mkdirSync(path22.dirname(dbPath), { recursive: true });
15033
+ fs16.mkdirSync(path22.dirname(dbPath), { recursive: true });
14478
15034
  this.db = new import_better_sqlite32.default(dbPath);
14479
15035
  this.db.pragma("journal_mode = WAL");
14480
15036
  this.db.pragma("synchronous = NORMAL");
@@ -14574,18 +15130,18 @@ function openSearchIndex(projectPath) {
14574
15130
  return new SqliteSearchIndex(searchIndexPath(projectPath));
14575
15131
  }
14576
15132
  function indexSessionDirectory(idx, args) {
14577
- const kinds = args.fileKinds ?? [...import_types31.INDEXED_FILE_KINDS];
15133
+ const kinds = args.fileKinds ?? [...import_types32.INDEXED_FILE_KINDS];
14578
15134
  const cap = args.maxBytesPerBody ?? 256 * 1024;
14579
15135
  let docsWritten = 0;
14580
15136
  for (const kind of kinds) {
14581
15137
  const fileName = FILE_KIND_TO_FILENAME[kind];
14582
15138
  const filePath = path22.join(args.sessionDir, fileName);
14583
- if (!fs18.existsSync(filePath)) continue;
14584
- let body = fs18.readFileSync(filePath, "utf8");
15139
+ if (!fs16.existsSync(filePath)) continue;
15140
+ let body = fs16.readFileSync(filePath, "utf8");
14585
15141
  if (Buffer.byteLength(body, "utf8") > cap) {
14586
15142
  body = body.slice(0, cap) + "\n\n[TRUNCATED]";
14587
15143
  }
14588
- const stat = fs18.statSync(filePath);
15144
+ const stat = fs16.statSync(filePath);
14589
15145
  const relPath = path22.relative(args.projectPath, filePath).replaceAll("\\", "/");
14590
15146
  idx.upsertSessionDoc({
14591
15147
  sessionId: args.sessionId,
@@ -14607,8 +15163,8 @@ function reindexFromArchive(projectPath, opts = {}) {
14607
15163
  idx.resetArchived();
14608
15164
  let sessionsIndexed = 0;
14609
15165
  let docsWritten = 0;
14610
- if (fs18.existsSync(archiveBase)) {
14611
- const entries = fs18.readdirSync(archiveBase, { withFileTypes: true });
15166
+ if (fs16.existsSync(archiveBase)) {
15167
+ const entries = fs16.readdirSync(archiveBase, { withFileTypes: true });
14612
15168
  for (const entry of entries) {
14613
15169
  if (!entry.isDirectory()) continue;
14614
15170
  const sessionDir = path22.join(archiveBase, entry.name);
@@ -14631,10 +15187,10 @@ function reindexFromArchive(projectPath, opts = {}) {
14631
15187
  }
14632
15188
 
14633
15189
  // src/sessions/summarize.ts
14634
- var fs19 = __toESM(require("fs"));
15190
+ var fs17 = __toESM(require("fs"));
14635
15191
  var path23 = __toESM(require("path"));
14636
- var import_types32 = require("@harness-engineering/types");
14637
15192
  var import_types33 = require("@harness-engineering/types");
15193
+ var import_types34 = require("@harness-engineering/types");
14638
15194
  var LLM_SUMMARY_FILE = "llm-summary.md";
14639
15195
  var SUMMARY_INPUT_FILES = [
14640
15196
  { filename: "summary.md", kind: "summary" },
@@ -14661,9 +15217,9 @@ function readInputCorpus(archiveDir) {
14661
15217
  const parts = [];
14662
15218
  for (const { filename, kind } of SUMMARY_INPUT_FILES) {
14663
15219
  const p = path23.join(archiveDir, filename);
14664
- if (!fs19.existsSync(p)) continue;
15220
+ if (!fs17.existsSync(p)) continue;
14665
15221
  try {
14666
- const content = fs19.readFileSync(p, "utf8");
15222
+ const content = fs17.readFileSync(p, "utf8");
14667
15223
  if (content.trim().length === 0) continue;
14668
15224
  parts.push(`## FILE: ${kind}
14669
15225
 
@@ -14725,17 +15281,17 @@ status: failed
14725
15281
 
14726
15282
  - reason: ${reason}
14727
15283
  `;
14728
- fs19.writeFileSync(filePath, body, "utf8");
15284
+ fs17.writeFileSync(filePath, body, "utf8");
14729
15285
  return filePath;
14730
15286
  }
14731
15287
  async function summarizeArchivedSession(ctx) {
14732
15288
  const writeStubOnError = ctx.writeStubOnError ?? true;
14733
- if (!fs19.existsSync(ctx.archiveDir)) {
14734
- return (0, import_types33.Err)(new Error(`archive directory not found: ${ctx.archiveDir}`));
15289
+ if (!fs17.existsSync(ctx.archiveDir)) {
15290
+ return (0, import_types34.Err)(new Error(`archive directory not found: ${ctx.archiveDir}`));
14735
15291
  }
14736
15292
  const corpus = readInputCorpus(ctx.archiveDir);
14737
15293
  if (corpus.trim().length === 0) {
14738
- return (0, import_types33.Err)(new Error(`no summary input files found in ${ctx.archiveDir}`));
15294
+ return (0, import_types34.Err)(new Error(`no summary input files found in ${ctx.archiveDir}`));
14739
15295
  }
14740
15296
  const inputBudgetTokens = ctx.config?.inputBudgetTokens ?? DEFAULT_INPUT_BUDGET_TOKENS;
14741
15297
  const truncated = truncateForBudget(corpus, inputBudgetTokens);
@@ -14744,7 +15300,7 @@ async function summarizeArchivedSession(ctx) {
14744
15300
  const analyzeOpts = {
14745
15301
  prompt,
14746
15302
  systemPrompt: SYSTEM_PROMPT,
14747
- responseSchema: import_types32.SessionSummarySchema,
15303
+ responseSchema: import_types33.SessionSummarySchema,
14748
15304
  ...ctx.config?.model && { model: ctx.config.model }
14749
15305
  };
14750
15306
  let response;
@@ -14768,11 +15324,11 @@ async function summarizeArchivedSession(ctx) {
14768
15324
  } catch {
14769
15325
  }
14770
15326
  }
14771
- return (0, import_types33.Err)(
15327
+ return (0, import_types34.Err)(
14772
15328
  new Error(`session summary failed: ${reason}` + (stubPath ? ` (stub: ${stubPath})` : ""))
14773
15329
  );
14774
15330
  }
14775
- const parsed = import_types32.SessionSummarySchema.safeParse(response.result);
15331
+ const parsed = import_types33.SessionSummarySchema.safeParse(response.result);
14776
15332
  if (!parsed.success) {
14777
15333
  const reason = `schema validation failed: ${parsed.error.message}`;
14778
15334
  ctx.logger?.warn?.("session summary: invalid provider payload", { reason });
@@ -14782,7 +15338,7 @@ async function summarizeArchivedSession(ctx) {
14782
15338
  } catch {
14783
15339
  }
14784
15340
  }
14785
- return (0, import_types33.Err)(new Error(reason));
15341
+ return (0, import_types34.Err)(new Error(reason));
14786
15342
  }
14787
15343
  const meta = {
14788
15344
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -14793,8 +15349,8 @@ async function summarizeArchivedSession(ctx) {
14793
15349
  };
14794
15350
  const filePath = path23.join(ctx.archiveDir, LLM_SUMMARY_FILE);
14795
15351
  const body = renderLlmSummaryMarkdown(parsed.data, meta);
14796
- fs19.writeFileSync(filePath, body, "utf8");
14797
- return (0, import_types33.Ok)({ summary: parsed.data, meta, filePath });
15352
+ fs17.writeFileSync(filePath, body, "utf8");
15353
+ return (0, import_types34.Ok)({ summary: parsed.data, meta, filePath });
14798
15354
  }
14799
15355
  function isSummaryEnabled(config) {
14800
15356
  if (!config) return false;
@@ -14874,13 +15430,18 @@ function buildArchiveHooks(opts) {
14874
15430
  BUILT_IN_TASKS,
14875
15431
  BackendDefSchema,
14876
15432
  BackendRouter,
15433
+ CheckScriptRunner,
14877
15434
  ClaimManager,
14878
15435
  GateNotReadyError,
14879
15436
  GateRunError,
14880
15437
  InteractionQueue,
15438
+ LinearGraphQLClient,
14881
15439
  LinearGraphQLStub,
14882
15440
  LocalModelResolver,
15441
+ MAINTENANCE_CHECK_MAX_BUFFER,
15442
+ MAINTENANCE_CHECK_TIMEOUT_MS,
14883
15443
  MAX_ATTEMPTS,
15444
+ MaintenanceReporter,
14884
15445
  MockBackend,
14885
15446
  ORCHESTRATOR_IDENTITY_FILE,
14886
15447
  Orchestrator,
@@ -14898,6 +15459,7 @@ function buildArchiveHooks(opts) {
14898
15459
  SqliteSearchIndex,
14899
15460
  StreamRecorder,
14900
15461
  TaskOutputStore,
15462
+ TaskRunner,
14901
15463
  TokenStore,
14902
15464
  WebhookQueue,
14903
15465
  WorkflowLoader,
@@ -14908,7 +15470,9 @@ function buildArchiveHooks(opts) {
14908
15470
  buildArchiveHooks,
14909
15471
  calculateRetryDelay,
14910
15472
  canDispatch,
15473
+ classifyCheckExecutionFailure,
14911
15474
  computeRateLimitDelay,
15475
+ createAgentDispatcher,
14912
15476
  createBackend,
14913
15477
  createEmptyState,
14914
15478
  crossFieldRoutingIssues,
@@ -14920,22 +15484,27 @@ function buildArchiveHooks(opts) {
14920
15484
  emitProposalApproved,
14921
15485
  emitProposalCreated,
14922
15486
  emitProposalRejected,
15487
+ explicitFindingsCount,
14923
15488
  extractHighlights,
14924
15489
  extractTitlePrefix,
14925
15490
  getAvailableSlots,
14926
15491
  getDefaultConfig,
14927
15492
  getPerStateCount,
14928
15493
  indexSessionDirectory,
15494
+ isCheckTimeoutError,
14929
15495
  isEligible,
14930
15496
  isSummaryEnabled,
14931
15497
  launchTUI,
14932
15498
  loadPublishedIndex,
15499
+ makeBackendResolver,
14933
15500
  migrateAgentConfig,
14934
15501
  normalizeFts5Query,
15502
+ normalizeHarnessCommand,
14935
15503
  normalizeLocalModel,
14936
15504
  openSearchIndex,
14937
15505
  promote,
14938
15506
  reconcile,
15507
+ recoverFindingsCount,
14939
15508
  reindexFromArchive,
14940
15509
  renderAnalysisComment,
14941
15510
  renderLlmSummaryMarkdown,
@@ -14945,9 +15514,11 @@ function buildArchiveHooks(opts) {
14945
15514
  routeIssue,
14946
15515
  routingWarnings,
14947
15516
  runGate,
15517
+ runHarnessCheck,
14948
15518
  savePublishedIndex,
14949
15519
  searchIndexPath,
14950
15520
  selectCandidates,
15521
+ selectTasks,
14951
15522
  sortCandidates,
14952
15523
  summarizeArchivedSession,
14953
15524
  syncMain,