@harness-engineering/orchestrator 0.8.3 → 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,
@@ -1341,7 +1356,7 @@ var ClaimManager = class {
1341
1356
  // src/core/pr-detector.ts
1342
1357
  var import_node_child_process = require("child_process");
1343
1358
  var import_node_util = require("util");
1344
- var PRDetector = class {
1359
+ var PRDetector = class _PRDetector {
1345
1360
  logger;
1346
1361
  execFileFn;
1347
1362
  projectRoot;
@@ -1457,35 +1472,124 @@ var PRDetector = class {
1457
1472
  }
1458
1473
  }
1459
1474
  /**
1460
- * Filters out candidates that already have an open GitHub PR, running
1461
- * checks with limited concurrency to avoid overwhelming the GitHub API.
1462
- * For candidates with an externalId, searches for PRs linked to the
1463
- * GitHub issue. Falls back to `feat/<identifier>` branch lookup otherwise.
1464
- * Fail-open on API errors.
1475
+ * GitHub closing keywords that link a PR to an issue it will close.
1476
+ * @see https://docs.github.com/articles/closing-issues-using-keywords
1477
+ */
1478
+ static CLOSING_REF_RE = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)[\s:]+#(\d+)/gi;
1479
+ /**
1480
+ * Extracts the issue numbers a PR body declares it will close
1481
+ * (e.g. `Closes #42`, `fixes #7`, `Resolves: #99`).
1482
+ */
1483
+ parseClosingIssueNumbers(body) {
1484
+ const nums = [];
1485
+ for (const match of body.matchAll(_PRDetector.CLOSING_REF_RE)) {
1486
+ nums.push(parseInt(match[1], 10));
1487
+ }
1488
+ return nums;
1489
+ }
1490
+ /**
1491
+ * Lists every open PR for a repo in a single `gh pr list` call and returns
1492
+ * the set of issue numbers those PRs close (parsed from their bodies).
1493
+ *
1494
+ * This is the batched replacement for per-issue `gh pr list --search
1495
+ * "closes #N"` queries. Every `gh pr list` form (search or plain list) is
1496
+ * served by GitHub's GraphQL API and consumes from the shared ~5000/hr
1497
+ * GraphQL budget, so issuing one query per candidate per tick exhausted the
1498
+ * limit on busy boards. A single `--state open` list returns every open PR
1499
+ * in one request regardless of candidate count, collapsing N calls into 1.
1500
+ *
1501
+ * Returns `null` if the check itself failed (gh missing, rate limited,
1502
+ * network error) so callers can fail open rather than block real work.
1503
+ */
1504
+ async fetchOpenPRClosures(owner, repo) {
1505
+ try {
1506
+ const exec = (0, import_node_util.promisify)(this.execFileFn);
1507
+ const { stdout } = await exec(
1508
+ "gh",
1509
+ [
1510
+ "pr",
1511
+ "list",
1512
+ "--repo",
1513
+ `${owner}/${repo}`,
1514
+ "--state",
1515
+ "open",
1516
+ "--json",
1517
+ "body",
1518
+ "--limit",
1519
+ "200"
1520
+ ],
1521
+ {
1522
+ cwd: this.projectRoot,
1523
+ timeout: 15e3
1524
+ }
1525
+ );
1526
+ const prs = JSON.parse(stdout);
1527
+ const closed = /* @__PURE__ */ new Set();
1528
+ for (const pr of prs) {
1529
+ for (const n of this.parseClosingIssueNumbers(pr.body ?? "")) closed.add(n);
1530
+ }
1531
+ return closed;
1532
+ } catch (err) {
1533
+ this.logger.debug(`Failed to list open PRs for ${owner}/${repo}`, {
1534
+ error: String(err)
1535
+ });
1536
+ return null;
1537
+ }
1538
+ }
1539
+ /**
1540
+ * Filters out candidates that already have an open GitHub PR.
1541
+ *
1542
+ * For GitHub-issue candidates the check is batched: one `gh pr list` per
1543
+ * distinct repo (not one search per issue), parsing closing references
1544
+ * locally. Candidates without a GitHub externalId fall back to a
1545
+ * `feat/<identifier>` branch lookup, throttled to a few concurrent gh calls.
1546
+ * Fail-open on API errors so a flaky/rate-limited GitHub never blocks work.
1465
1547
  */
1466
1548
  async filterCandidatesWithOpenPRs(candidates) {
1549
+ const repos = /* @__PURE__ */ new Map();
1550
+ for (const candidate of candidates) {
1551
+ const parsed = candidate.externalId ? this.parseExternalId(candidate.externalId) : null;
1552
+ if (parsed) repos.set(`${parsed.owner}/${parsed.repo}`, parsed);
1553
+ }
1554
+ const repoClosures = /* @__PURE__ */ new Map();
1555
+ await Promise.all(
1556
+ [...repos.values()].map(async ({ owner, repo }) => {
1557
+ repoClosures.set(`${owner}/${repo}`, await this.fetchOpenPRClosures(owner, repo));
1558
+ })
1559
+ );
1560
+ const identifierCandidates = candidates.filter(
1561
+ (c) => !(c.externalId && this.parseExternalId(c.externalId))
1562
+ );
1563
+ const identifiersWithOpenPR = /* @__PURE__ */ new Set();
1467
1564
  const concurrency = 3;
1468
- const results = [];
1469
- for (let i = 0; i < candidates.length; i += concurrency) {
1470
- const batch = candidates.slice(i, i + concurrency);
1471
- const batchResults = await Promise.allSettled(
1472
- batch.map(async (candidate) => {
1473
- const hasOpenPR = candidate.externalId ? await this.hasOpenPRForExternalId(candidate.externalId) : await this.hasOpenPRForIdentifier(candidate.identifier);
1474
- return { candidate, hasOpenPR };
1475
- })
1565
+ for (let i = 0; i < identifierCandidates.length; i += concurrency) {
1566
+ const batch = identifierCandidates.slice(i, i + concurrency);
1567
+ const settled = await Promise.allSettled(
1568
+ batch.map(async (c) => ({
1569
+ identifier: c.identifier,
1570
+ hasOpenPR: await this.hasOpenPRForIdentifier(c.identifier)
1571
+ }))
1476
1572
  );
1477
- results.push(...batchResults);
1573
+ for (const result of settled) {
1574
+ if (result.status === "fulfilled" && result.value.hasOpenPR) {
1575
+ identifiersWithOpenPR.add(result.value.identifier);
1576
+ }
1577
+ }
1478
1578
  }
1479
1579
  const filtered = [];
1480
- for (let i = 0; i < results.length; i++) {
1481
- const result = results[i];
1482
- if (!result || result.status === "rejected") {
1483
- filtered.push(candidates[i]);
1484
- continue;
1580
+ for (const candidate of candidates) {
1581
+ const parsed = candidate.externalId ? this.parseExternalId(candidate.externalId) : null;
1582
+ let hasOpenPR;
1583
+ let via;
1584
+ if (parsed) {
1585
+ const closures = repoClosures.get(`${parsed.owner}/${parsed.repo}`);
1586
+ hasOpenPR = closures ? closures.has(parsed.number) : false;
1587
+ via = `externalId ${candidate.externalId}`;
1588
+ } else {
1589
+ hasOpenPR = identifiersWithOpenPR.has(candidate.identifier);
1590
+ via = `feat/${candidate.identifier}`;
1485
1591
  }
1486
- const { candidate, hasOpenPR } = result.value;
1487
1592
  if (hasOpenPR) {
1488
- const via = candidate.externalId ? `externalId ${candidate.externalId}` : `feat/${candidate.identifier}`;
1489
1593
  this.logger.info(`Skipping ${candidate.title}: open PR exists (${via})`);
1490
1594
  } else {
1491
1595
  filtered.push(candidate);
@@ -2238,7 +2342,6 @@ var WorkflowLoader = class {
2238
2342
  };
2239
2343
 
2240
2344
  // src/tracker/adapters/roadmap.ts
2241
- var fs8 = __toESM(require("fs/promises"));
2242
2345
  var import_node_crypto2 = require("crypto");
2243
2346
  var import_core = require("@harness-engineering/core");
2244
2347
  var import_types4 = require("@harness-engineering/types");
@@ -2269,8 +2372,7 @@ var RoadmapTrackerAdapter = class {
2269
2372
  async fetchIssuesByStates(stateNames) {
2270
2373
  try {
2271
2374
  if (!this.config.filePath) return (0, import_types4.Err)(new Error("Missing filePath"));
2272
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2273
- const roadmapResult = (0, import_core.parseRoadmap)(content);
2375
+ const roadmapResult = await this.loadRoadmap();
2274
2376
  if (!roadmapResult.ok) return roadmapResult;
2275
2377
  const issues = [];
2276
2378
  for (const milestone of roadmapResult.value.milestones) {
@@ -2301,16 +2403,19 @@ var RoadmapTrackerAdapter = class {
2301
2403
  if (!terminal) {
2302
2404
  return (0, import_types4.Err)(new Error("Tracker config has no terminalStates; cannot mark complete"));
2303
2405
  }
2304
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2305
- const roadmapResult = (0, import_core.parseRoadmap)(content);
2406
+ const store = this.resolveStore();
2407
+ const roadmapResult = await store.load();
2306
2408
  if (!roadmapResult.ok) return roadmapResult;
2307
2409
  const roadmap = roadmapResult.value;
2410
+ const before = structuredClone(roadmap);
2308
2411
  const target = this.findFeatureById(roadmap.milestones, issueId);
2309
2412
  if (!target) return (0, import_types4.Ok)(void 0);
2310
2413
  const normalizedTerminal = this.config.terminalStates.map((s) => s.toLowerCase());
2311
2414
  if (normalizedTerminal.includes(target.status.toLowerCase())) return (0, import_types4.Ok)(void 0);
2312
- target.status = terminal;
2313
- await fs8.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2415
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2416
+ (0, import_core.setStatus)(roadmap, target, terminal, now.slice(0, 10));
2417
+ const persisted = await (0, import_core.applyRoadmapDiff)(store, before, roadmap);
2418
+ if (!persisted.ok) return persisted;
2314
2419
  return (0, import_types4.Ok)(void 0);
2315
2420
  } catch (error) {
2316
2421
  return (0, import_types4.Err)(error instanceof Error ? error : new Error(String(error)));
@@ -2324,22 +2429,24 @@ var RoadmapTrackerAdapter = class {
2324
2429
  async claimIssue(issueId, orchestratorId) {
2325
2430
  try {
2326
2431
  if (!this.config.filePath) return (0, import_types4.Err)(new Error("Missing filePath"));
2327
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2328
- const roadmapResult = (0, import_core.parseRoadmap)(content);
2432
+ const store = this.resolveStore();
2433
+ const roadmapResult = await store.load();
2329
2434
  if (!roadmapResult.ok) return roadmapResult;
2330
2435
  const roadmap = roadmapResult.value;
2436
+ const before = structuredClone(roadmap);
2331
2437
  const target = this.findFeatureById(roadmap.milestones, issueId);
2332
2438
  if (!target) return (0, import_types4.Ok)(void 0);
2333
- if (target.assignee != null && target.assignee !== orchestratorId) {
2439
+ if (!(0, import_core.isClaimableBy)(target, orchestratorId)) {
2334
2440
  return (0, import_types4.Ok)(void 0);
2335
2441
  }
2336
2442
  if (target.status === "in-progress" && target.assignee === orchestratorId) {
2337
2443
  return (0, import_types4.Ok)(void 0);
2338
2444
  }
2339
- target.status = "in-progress";
2340
- target.assignee = orchestratorId;
2341
- target.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2342
- await fs8.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2445
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2446
+ (0, import_core.claim)(roadmap, target, orchestratorId, now.slice(0, 10));
2447
+ target.updatedAt = now;
2448
+ const persisted = await (0, import_core.applyRoadmapDiff)(store, before, roadmap);
2449
+ if (!persisted.ok) return persisted;
2343
2450
  return (0, import_types4.Ok)(void 0);
2344
2451
  } catch (error) {
2345
2452
  return (0, import_types4.Err)(error instanceof Error ? error : new Error(String(error)));
@@ -2356,24 +2463,39 @@ var RoadmapTrackerAdapter = class {
2356
2463
  if (!activeState) {
2357
2464
  return (0, import_types4.Err)(new Error("Tracker config has no activeStates; cannot release"));
2358
2465
  }
2359
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2360
- const roadmapResult = (0, import_core.parseRoadmap)(content);
2466
+ const store = this.resolveStore();
2467
+ const roadmapResult = await store.load();
2361
2468
  if (!roadmapResult.ok) return roadmapResult;
2362
2469
  const roadmap = roadmapResult.value;
2470
+ const before = structuredClone(roadmap);
2363
2471
  const target = this.findFeatureById(roadmap.milestones, issueId);
2364
2472
  if (!target) return (0, import_types4.Ok)(void 0);
2365
2473
  if (this.config.activeStates.includes(target.status) && target.assignee === null) {
2366
2474
  return (0, import_types4.Ok)(void 0);
2367
2475
  }
2368
- target.status = activeState;
2369
- target.assignee = null;
2476
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2477
+ (0, import_core.setStatus)(roadmap, target, activeState, now.slice(0, 10));
2370
2478
  target.updatedAt = null;
2371
- 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;
2372
2481
  return (0, import_types4.Ok)(void 0);
2373
2482
  } catch (error) {
2374
2483
  return (0, import_types4.Err)(error instanceof Error ? error : new Error(String(error)));
2375
2484
  }
2376
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
+ }
2377
2499
  findFeatureById(milestones, issueId) {
2378
2500
  for (const milestone of milestones) {
2379
2501
  for (const feature of milestone.features) {
@@ -2390,8 +2512,7 @@ var RoadmapTrackerAdapter = class {
2390
2512
  async fetchIssueStatesByIds(issueIds) {
2391
2513
  try {
2392
2514
  if (!this.config.filePath) return (0, import_types4.Err)(new Error("Missing filePath"));
2393
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2394
- const roadmapResult = (0, import_core.parseRoadmap)(content);
2515
+ const roadmapResult = await this.loadRoadmap();
2395
2516
  if (!roadmapResult.ok) return roadmapResult;
2396
2517
  const issueMap = /* @__PURE__ */ new Map();
2397
2518
  for (const milestone of roadmapResult.value.milestones) {
@@ -2447,6 +2568,56 @@ var RoadmapTrackerAdapter = class {
2447
2568
 
2448
2569
  // src/tracker/extensions/linear.ts
2449
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
+ };
2450
2621
  var LinearGraphQLStub = class {
2451
2622
  async query(query, _variables) {
2452
2623
  console.log("Linear GraphQL query (stub):", query);
@@ -2455,7 +2626,7 @@ var LinearGraphQLStub = class {
2455
2626
  };
2456
2627
 
2457
2628
  // src/workspace/manager.ts
2458
- var fs9 = __toESM(require("fs/promises"));
2629
+ var fs8 = __toESM(require("fs/promises"));
2459
2630
  var path8 = __toESM(require("path"));
2460
2631
  var import_node_child_process2 = require("child_process");
2461
2632
  var import_node_util2 = require("util");
@@ -2495,7 +2666,7 @@ var WorkspaceManager = class _WorkspaceManager {
2495
2666
  async getRepoRoot() {
2496
2667
  if (this.repoRoot) return this.repoRoot;
2497
2668
  const root = path8.resolve(this.config.root);
2498
- await fs9.mkdir(root, { recursive: true });
2669
+ await fs8.mkdir(root, { recursive: true });
2499
2670
  const stdout = await this.git(["rev-parse", "--show-toplevel"], root);
2500
2671
  this.repoRoot = stdout.trim();
2501
2672
  return this.repoRoot;
@@ -2508,21 +2679,21 @@ var WorkspaceManager = class _WorkspaceManager {
2508
2679
  try {
2509
2680
  const workspacePath = path8.resolve(this.resolvePath(identifier));
2510
2681
  try {
2511
- await fs9.access(path8.join(workspacePath, ".git"));
2682
+ await fs8.access(path8.join(workspacePath, ".git"));
2512
2683
  const repoRoot2 = await this.getRepoRoot();
2513
2684
  try {
2514
2685
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
2515
2686
  } catch {
2516
- await fs9.rm(workspacePath, { recursive: true, force: true });
2687
+ await fs8.rm(workspacePath, { recursive: true, force: true });
2517
2688
  }
2518
2689
  } catch {
2519
2690
  try {
2520
- await fs9.access(workspacePath);
2691
+ await fs8.access(workspacePath);
2521
2692
  const repoRoot2 = await this.getRepoRoot();
2522
2693
  try {
2523
2694
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
2524
2695
  } catch {
2525
- await fs9.rm(workspacePath, { recursive: true, force: true });
2696
+ await fs8.rm(workspacePath, { recursive: true, force: true });
2526
2697
  }
2527
2698
  } catch {
2528
2699
  }
@@ -2575,13 +2746,13 @@ var WorkspaceManager = class _WorkspaceManager {
2575
2746
  }
2576
2747
  const src = path8.join(repoRoot, rel);
2577
2748
  try {
2578
- await fs9.access(src);
2749
+ await fs8.access(src);
2579
2750
  } catch {
2580
2751
  continue;
2581
2752
  }
2582
2753
  const dest = path8.join(workspacePath, rel);
2583
2754
  try {
2584
- await fs9.cp(src, dest, { recursive: true, force: true });
2755
+ await fs8.cp(src, dest, { recursive: true, force: true });
2585
2756
  } catch {
2586
2757
  }
2587
2758
  }
@@ -2657,7 +2828,7 @@ var WorkspaceManager = class _WorkspaceManager {
2657
2828
  async exists(identifier) {
2658
2829
  try {
2659
2830
  const workspacePath = this.resolvePath(identifier);
2660
- await fs9.access(workspacePath);
2831
+ await fs8.access(workspacePath);
2661
2832
  return true;
2662
2833
  } catch {
2663
2834
  return false;
@@ -2672,7 +2843,7 @@ var WorkspaceManager = class _WorkspaceManager {
2672
2843
  try {
2673
2844
  const workspacePath = path8.resolve(this.resolvePath(identifier));
2674
2845
  try {
2675
- await fs9.access(path8.join(workspacePath, ".git"));
2846
+ await fs8.access(path8.join(workspacePath, ".git"));
2676
2847
  } catch {
2677
2848
  return null;
2678
2849
  }
@@ -2783,7 +2954,7 @@ var WorkspaceManager = class _WorkspaceManager {
2783
2954
  const repoRoot = await this.getRepoRoot();
2784
2955
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot);
2785
2956
  } catch {
2786
- await fs9.rm(workspacePath, { recursive: true, force: true });
2957
+ await fs8.rm(workspacePath, { recursive: true, force: true });
2787
2958
  }
2788
2959
  return (0, import_types6.Ok)(void 0);
2789
2960
  } catch (error) {
@@ -2924,8 +3095,8 @@ var PromptRenderer = class {
2924
3095
  // src/orchestrator.ts
2925
3096
  var import_node_events = require("events");
2926
3097
  var path21 = __toESM(require("path"));
2927
- var import_node_crypto16 = require("crypto");
2928
- var import_core14 = require("@harness-engineering/core");
3098
+ var import_node_crypto15 = require("crypto");
3099
+ var import_core15 = require("@harness-engineering/core");
2929
3100
 
2930
3101
  // src/core/stall-detector.ts
2931
3102
  function detectStalledIssues(running, nowMs, stallTimeoutMs) {
@@ -3501,7 +3672,7 @@ var CompletionHandler = class {
3501
3672
  };
3502
3673
 
3503
3674
  // src/orchestrator.ts
3504
- var import_core15 = require("@harness-engineering/core");
3675
+ var import_core16 = require("@harness-engineering/core");
3505
3676
 
3506
3677
  // src/tracker/adapters/github-issues-issue-tracker.ts
3507
3678
  var import_types9 = require("@harness-engineering/types");
@@ -6421,6 +6592,96 @@ var OrchestratorBackendFactory = class {
6421
6592
  }
6422
6593
  };
6423
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
+
6424
6685
  // src/agent/intelligence-factory.ts
6425
6686
  var import_intelligence3 = require("@harness-engineering/intelligence");
6426
6687
  var import_graph = require("@harness-engineering/graph");
@@ -6922,7 +7183,7 @@ function handleV1InteractionsResolveRoute(req, res, queue) {
6922
7183
 
6923
7184
  // src/server/routes/plans.ts
6924
7185
  var import_zod5 = require("zod");
6925
- var fs10 = __toESM(require("fs/promises"));
7186
+ var fs9 = __toESM(require("fs/promises"));
6926
7187
  var path11 = __toESM(require("path"));
6927
7188
  var PlanWriteSchema = import_zod5.z.object({
6928
7189
  filename: import_zod5.z.string().min(1),
@@ -6951,9 +7212,9 @@ function handlePlansRoute(req, res, plansDir) {
6951
7212
  );
6952
7213
  return;
6953
7214
  }
6954
- await fs10.mkdir(plansDir, { recursive: true });
7215
+ await fs9.mkdir(plansDir, { recursive: true });
6955
7216
  const filePath = path11.join(plansDir, basename3);
6956
- await fs10.writeFile(filePath, parsed.content, "utf-8");
7217
+ await fs9.writeFile(filePath, parsed.content, "utf-8");
6957
7218
  res.writeHead(201, { "Content-Type": "application/json" });
6958
7219
  res.end(JSON.stringify({ ok: true, filename: basename3 }));
6959
7220
  } catch {
@@ -7328,7 +7589,6 @@ function handleAnalyzeRoute(req, res, pipeline) {
7328
7589
  }
7329
7590
 
7330
7591
  // src/server/routes/roadmap-actions.ts
7331
- var fs11 = __toESM(require("fs/promises"));
7332
7592
  var path12 = __toESM(require("path"));
7333
7593
  var import_core7 = require("@harness-engineering/core");
7334
7594
  var import_zod8 = require("zod");
@@ -7410,13 +7670,14 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
7410
7670
  sendJSON2(res, 400, { error: "Title must not contain newlines or markdown headings" });
7411
7671
  return;
7412
7672
  }
7413
- const content = await fs11.readFile(roadmapPath, "utf-8");
7414
- const roadmapResult = (0, import_core7.parseRoadmap)(content);
7415
- if (!roadmapResult.ok) {
7673
+ const store = (0, import_core7.resolveRoadmapStoreForFile)({ roadmapPath });
7674
+ const loaded = await store.load();
7675
+ if (!loaded.ok) {
7416
7676
  sendJSON2(res, 500, { error: "Failed to parse roadmap file" });
7417
7677
  return;
7418
7678
  }
7419
- const roadmap = roadmapResult.value;
7679
+ const roadmap = loaded.value;
7680
+ const before = structuredClone(roadmap);
7420
7681
  let backlog = roadmap.milestones.find((m) => m.isBacklog);
7421
7682
  if (!backlog) {
7422
7683
  backlog = { name: "Backlog", isBacklog: true, features: [] };
@@ -7439,10 +7700,11 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
7439
7700
  updatedAt: null
7440
7701
  });
7441
7702
  roadmap.frontmatter.lastManualEdit = (/* @__PURE__ */ new Date()).toISOString();
7442
- const tmpPath = roadmapPath + ".tmp";
7443
- const serialized = (0, import_core7.serializeRoadmap)(roadmap);
7444
- await fs11.writeFile(tmpPath, serialized, "utf-8");
7445
- 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
+ }
7446
7708
  sendJSON2(res, 201, { ok: true, featureName: parsed.title });
7447
7709
  } catch (err) {
7448
7710
  const msg = err instanceof Error ? err.message : "Failed to append to roadmap";
@@ -7708,7 +7970,6 @@ function handleV1JobsMaintenanceRoute(req, res, deps) {
7708
7970
  }
7709
7971
 
7710
7972
  // src/server/routes/v1/events-sse.ts
7711
- var import_node_crypto8 = require("crypto");
7712
7973
  var SSE_TOPICS = [
7713
7974
  "state_change",
7714
7975
  "agent_event",
@@ -7724,8 +7985,55 @@ var SSE_TOPICS = [
7724
7985
  "webhook.subscription.deleted"
7725
7986
  ];
7726
7987
  var HEARTBEAT_MS = 15e3;
7727
- function newEventId() {
7728
- 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;
7729
8037
  }
7730
8038
  function handleV1EventsSseRoute(req, res, bus) {
7731
8039
  if (req.method !== "GET" || req.url !== "/api/v1/events") return false;
@@ -7737,22 +8045,28 @@ function handleV1EventsSseRoute(req, res, bus) {
7737
8045
  res.write(`: harness gateway SSE \u2014 connected at ${(/* @__PURE__ */ new Date()).toISOString()}
7738
8046
 
7739
8047
  `);
7740
- const listeners = [];
7741
- for (const topic of SSE_TOPICS) {
7742
- const fn = (data) => {
7743
- try {
7744
- const frame = `event: ${topic}
7745
- data: ${JSON.stringify(data)}
7746
- 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}
7747
8054
 
7748
- `;
7749
- res.write(frame);
7750
- } catch {
7751
- }
7752
- };
7753
- bus.on(topic, fn);
7754
- 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
+ }
7755
8066
  }
8067
+ const unsubscribe = log.subscribe((e) => {
8068
+ if (e.id > replayedThrough) send(e);
8069
+ });
7756
8070
  const heartbeat = setInterval(() => {
7757
8071
  try {
7758
8072
  res.write(": heartbeat\n\n");
@@ -7762,7 +8076,7 @@ id: ${newEventId()}
7762
8076
  heartbeat.unref();
7763
8077
  const cleanup = () => {
7764
8078
  clearInterval(heartbeat);
7765
- for (const { topic, fn } of listeners) bus.removeListener(topic, fn);
8079
+ unsubscribe();
7766
8080
  };
7767
8081
  res.on("close", cleanup);
7768
8082
  res.on("finish", cleanup);
@@ -8071,7 +8385,7 @@ async function runGate(projectPath, proposalId) {
8071
8385
  }
8072
8386
 
8073
8387
  // src/proposals/promote.ts
8074
- var fs12 = __toESM(require("fs"));
8388
+ var fs10 = __toESM(require("fs"));
8075
8389
  var path13 = __toESM(require("path"));
8076
8390
  var import_yaml4 = require("yaml");
8077
8391
  var import_core9 = require("@harness-engineering/core");
@@ -8093,7 +8407,7 @@ function skillDir(projectPath, name) {
8093
8407
  }
8094
8408
  function readIfExists(p) {
8095
8409
  try {
8096
- return fs12.readFileSync(p, "utf-8");
8410
+ return fs10.readFileSync(p, "utf-8");
8097
8411
  } catch {
8098
8412
  return null;
8099
8413
  }
@@ -8139,15 +8453,15 @@ function assertGateReady(proposal) {
8139
8453
  }
8140
8454
  async function promoteNewSkill(projectPath, proposal) {
8141
8455
  const target = skillDir(projectPath, proposal.content.name);
8142
- if (fs12.existsSync(target)) {
8456
+ if (fs10.existsSync(target)) {
8143
8457
  throw new PromotionError(
8144
8458
  `a catalog skill already exists at ${target}; use a refinement proposal to update it`
8145
8459
  );
8146
8460
  }
8147
- fs12.mkdirSync(target, { recursive: true });
8461
+ fs10.mkdirSync(target, { recursive: true });
8148
8462
  const yamlOut = injectProvenanceIntoYaml(proposal.content.skillYaml ?? "", proposal.id);
8149
- fs12.writeFileSync(path13.join(target, "skill.yaml"), yamlOut);
8150
- 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 ?? "");
8151
8465
  return { skillPath: target };
8152
8466
  }
8153
8467
  async function promoteRefinement(projectPath, proposal) {
@@ -8155,7 +8469,7 @@ async function promoteRefinement(projectPath, proposal) {
8155
8469
  throw new PromotionError("refinement proposal is missing targetSkill");
8156
8470
  }
8157
8471
  const target = skillDir(projectPath, proposal.targetSkill);
8158
- if (!fs12.existsSync(target)) {
8472
+ if (!fs10.existsSync(target)) {
8159
8473
  throw new PromotionError(
8160
8474
  `target skill ${proposal.targetSkill} does not exist at ${target}; cannot refine`
8161
8475
  );
@@ -8168,7 +8482,7 @@ async function promoteRefinement(projectPath, proposal) {
8168
8482
  "no metadata changes detected; check that the reviewer applied the proposed diff before approving"
8169
8483
  );
8170
8484
  }
8171
- fs12.writeFileSync(yamlPath, after);
8485
+ fs10.writeFileSync(yamlPath, after);
8172
8486
  return { skillPath: target };
8173
8487
  }
8174
8488
  async function promote(projectPath, proposalId, decidedBy) {
@@ -8606,7 +8920,7 @@ function handleV1RoutingRoute(req, res, deps) {
8606
8920
  }
8607
8921
 
8608
8922
  // src/server/routes/sessions.ts
8609
- var fs13 = __toESM(require("fs/promises"));
8923
+ var fs11 = __toESM(require("fs/promises"));
8610
8924
  var path14 = __toESM(require("path"));
8611
8925
  var import_zod15 = require("zod");
8612
8926
  var SessionCreateSchema = import_zod15.z.object({
@@ -8627,12 +8941,12 @@ function extractSessionId(url) {
8627
8941
  }
8628
8942
  async function handleList2(res, sessionsDir) {
8629
8943
  try {
8630
- const entries = await fs13.readdir(sessionsDir, { withFileTypes: true });
8944
+ const entries = await fs11.readdir(sessionsDir, { withFileTypes: true });
8631
8945
  const sessions = [];
8632
8946
  for (const entry of entries) {
8633
8947
  if (!entry.isDirectory()) continue;
8634
8948
  try {
8635
- const content = await fs13.readFile(
8949
+ const content = await fs11.readFile(
8636
8950
  path14.join(sessionsDir, entry.name, "session.json"),
8637
8951
  "utf-8"
8638
8952
  );
@@ -8658,7 +8972,7 @@ async function handleGet2(res, id, sessionsDir) {
8658
8972
  return;
8659
8973
  }
8660
8974
  try {
8661
- 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");
8662
8976
  jsonResponse(res, 200, JSON.parse(content));
8663
8977
  } catch (err) {
8664
8978
  if (err.code === "ENOENT") {
@@ -8682,8 +8996,8 @@ async function handleCreate(req, res, sessionsDir) {
8682
8996
  return;
8683
8997
  }
8684
8998
  const sessionDir = path14.join(sessionsDir, session.sessionId);
8685
- await fs13.mkdir(sessionDir, { recursive: true });
8686
- 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));
8687
9001
  jsonResponse(res, 200, { ok: true });
8688
9002
  } catch {
8689
9003
  jsonResponse(res, 500, { error: "Failed to save session" });
@@ -8699,8 +9013,8 @@ async function handleUpdate(req, res, url, sessionsDir) {
8699
9013
  const body = await readBody(req);
8700
9014
  const updates = import_zod15.z.record(import_zod15.z.unknown()).parse(JSON.parse(body));
8701
9015
  const sessionFilePath = path14.join(sessionsDir, id, "session.json");
8702
- const current = JSON.parse(await fs13.readFile(sessionFilePath, "utf-8"));
8703
- 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));
8704
9018
  jsonResponse(res, 200, { ok: true });
8705
9019
  } catch {
8706
9020
  jsonResponse(res, 500, { error: "Failed to update session" });
@@ -8713,7 +9027,7 @@ async function handleDelete(res, url, sessionsDir) {
8713
9027
  jsonResponse(res, 400, { error: "Missing or invalid sessionId" });
8714
9028
  return;
8715
9029
  }
8716
- await fs13.rm(path14.join(sessionsDir, id), { recursive: true, force: true });
9030
+ await fs11.rm(path14.join(sessionsDir, id), { recursive: true, force: true });
8717
9031
  jsonResponse(res, 200, { ok: true });
8718
9032
  } catch {
8719
9033
  jsonResponse(res, 500, { error: "Failed to delete session" });
@@ -8957,7 +9271,7 @@ function handleLocalModelsRoute(req, res, getStatuses) {
8957
9271
  }
8958
9272
 
8959
9273
  // src/server/static.ts
8960
- var fs14 = __toESM(require("fs"));
9274
+ var fs12 = __toESM(require("fs"));
8961
9275
  var path15 = __toESM(require("path"));
8962
9276
  var MIME_TYPES = {
8963
9277
  ".html": "text/html; charset=utf-8",
@@ -8987,11 +9301,11 @@ function handleStaticFile(req, res, dashboardDir) {
8987
9301
  if (!resolved.startsWith(path15.resolve(dashboardDir))) {
8988
9302
  return serveFile(path15.join(dashboardDir, "index.html"), res);
8989
9303
  }
8990
- if (fs14.existsSync(resolved) && fs14.statSync(resolved).isFile()) {
9304
+ if (fs12.existsSync(resolved) && fs12.statSync(resolved).isFile()) {
8991
9305
  return serveFile(resolved, res);
8992
9306
  }
8993
9307
  const indexPath = path15.join(dashboardDir, "index.html");
8994
- if (fs14.existsSync(indexPath)) {
9308
+ if (fs12.existsSync(indexPath)) {
8995
9309
  return serveFile(indexPath, res);
8996
9310
  }
8997
9311
  return false;
@@ -9000,7 +9314,7 @@ function serveFile(filePath, res) {
9000
9314
  const ext = path15.extname(filePath).toLowerCase();
9001
9315
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
9002
9316
  try {
9003
- const content = fs14.readFileSync(filePath);
9317
+ const content = fs12.readFileSync(filePath);
9004
9318
  res.writeHead(200, { "Content-Type": contentType });
9005
9319
  res.end(content);
9006
9320
  return true;
@@ -9010,7 +9324,7 @@ function serveFile(filePath, res) {
9010
9324
  }
9011
9325
 
9012
9326
  // src/server/plan-watcher.ts
9013
- var fs15 = __toESM(require("fs"));
9327
+ var fs13 = __toESM(require("fs"));
9014
9328
  var path16 = __toESM(require("path"));
9015
9329
  var PlanWatcher = class {
9016
9330
  plansDir;
@@ -9025,11 +9339,11 @@ var PlanWatcher = class {
9025
9339
  * Creates the directory if it does not exist.
9026
9340
  */
9027
9341
  start() {
9028
- fs15.mkdirSync(this.plansDir, { recursive: true });
9029
- this.watcher = fs15.watch(this.plansDir, (eventType, filename) => {
9342
+ fs13.mkdirSync(this.plansDir, { recursive: true });
9343
+ this.watcher = fs13.watch(this.plansDir, (eventType, filename) => {
9030
9344
  if (eventType === "rename" && filename && filename.endsWith(".md")) {
9031
9345
  const filePath = path16.join(this.plansDir, filename);
9032
- if (fs15.existsSync(filePath)) {
9346
+ if (fs13.existsSync(filePath)) {
9033
9347
  void this.handleNewPlan(filename);
9034
9348
  }
9035
9349
  }
@@ -9060,7 +9374,7 @@ var PlanWatcher = class {
9060
9374
  };
9061
9375
 
9062
9376
  // src/auth/tokens.ts
9063
- var import_node_crypto9 = require("crypto");
9377
+ var import_node_crypto8 = require("crypto");
9064
9378
  var import_promises = require("fs/promises");
9065
9379
  var import_node_path = require("path");
9066
9380
  var import_bcryptjs = __toESM(require("bcryptjs"));
@@ -9068,10 +9382,10 @@ var import_types26 = require("@harness-engineering/types");
9068
9382
  var BCRYPT_ROUNDS = 12;
9069
9383
  var LEGACY_ENV_ID = "tok_legacy_env";
9070
9384
  function genId() {
9071
- return `tok_${(0, import_node_crypto9.randomBytes)(8).toString("hex")}`;
9385
+ return `tok_${(0, import_node_crypto8.randomBytes)(8).toString("hex")}`;
9072
9386
  }
9073
9387
  function genSecret() {
9074
- return (0, import_node_crypto9.randomBytes)(24).toString("base64url");
9388
+ return (0, import_node_crypto8.randomBytes)(24).toString("base64url");
9075
9389
  }
9076
9390
  function parseToken(raw) {
9077
9391
  const dot = raw.indexOf(".");
@@ -9102,7 +9416,7 @@ var TokenStore = class {
9102
9416
  }
9103
9417
  async persist(records) {
9104
9418
  await (0, import_promises.mkdir)((0, import_node_path.dirname)(this.path), { recursive: true });
9105
- 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")}`;
9106
9420
  await (0, import_promises.writeFile)(tmp, JSON.stringify(records, null, 2), "utf8");
9107
9421
  await (0, import_promises.rename)(tmp, this.path);
9108
9422
  this.cache = records;
@@ -9171,7 +9485,7 @@ var TokenStore = class {
9171
9485
  const a = Buffer.from(presented);
9172
9486
  const b = Buffer.from(envValue);
9173
9487
  if (a.length !== b.length) return null;
9174
- if (!(0, import_node_crypto9.timingSafeEqual)(a, b)) return null;
9488
+ if (!(0, import_node_crypto8.timingSafeEqual)(a, b)) return null;
9175
9489
  return {
9176
9490
  id: LEGACY_ENV_ID,
9177
9491
  name: "legacy-env",
@@ -9788,15 +10102,15 @@ var OrchestratorServer = class {
9788
10102
  };
9789
10103
 
9790
10104
  // src/gateway/webhooks/store.ts
9791
- var import_node_crypto11 = require("crypto");
10105
+ var import_node_crypto10 = require("crypto");
9792
10106
  var import_promises3 = require("fs/promises");
9793
10107
  var import_node_path3 = require("path");
9794
10108
  var import_types28 = require("@harness-engineering/types");
9795
10109
 
9796
10110
  // src/gateway/webhooks/signer.ts
9797
- var import_node_crypto10 = require("crypto");
10111
+ var import_node_crypto9 = require("crypto");
9798
10112
  function sign(secret, body) {
9799
- 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");
9800
10114
  return `sha256=${hex}`;
9801
10115
  }
9802
10116
  function eventMatches(pattern, type) {
@@ -9815,10 +10129,10 @@ function eventMatches(pattern, type) {
9815
10129
 
9816
10130
  // src/gateway/webhooks/store.ts
9817
10131
  function genId2() {
9818
- return `whk_${(0, import_node_crypto11.randomBytes)(8).toString("hex")}`;
10132
+ return `whk_${(0, import_node_crypto10.randomBytes)(8).toString("hex")}`;
9819
10133
  }
9820
10134
  function genSecret2() {
9821
- return (0, import_node_crypto11.randomBytes)(32).toString("base64url");
10135
+ return (0, import_node_crypto10.randomBytes)(32).toString("base64url");
9822
10136
  }
9823
10137
  var WebhookStore = class {
9824
10138
  constructor(path24) {
@@ -9843,7 +10157,7 @@ var WebhookStore = class {
9843
10157
  return this.cache;
9844
10158
  }
9845
10159
  async persist(records) {
9846
- 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")}`;
9847
10161
  try {
9848
10162
  await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(this.path), { recursive: true });
9849
10163
  await (0, import_promises3.writeFile)(tmp, JSON.stringify(records, null, 2), { encoding: "utf8", mode: 384 });
@@ -9887,7 +10201,7 @@ var WebhookStore = class {
9887
10201
  };
9888
10202
 
9889
10203
  // src/gateway/webhooks/delivery.ts
9890
- var import_node_crypto12 = require("crypto");
10204
+ var import_node_crypto11 = require("crypto");
9891
10205
 
9892
10206
  // src/gateway/webhooks/queue.ts
9893
10207
  var import_better_sqlite3 = __toESM(require("better-sqlite3"));
@@ -10097,7 +10411,7 @@ var WebhookDelivery = class {
10097
10411
  enqueue(sub, event) {
10098
10412
  const payload = JSON.stringify(event);
10099
10413
  this.queue.insert({
10100
- id: `dlv_${(0, import_node_crypto12.randomBytes)(8).toString("hex")}`,
10414
+ id: `dlv_${(0, import_node_crypto11.randomBytes)(8).toString("hex")}`,
10101
10415
  subscriptionId: sub.id,
10102
10416
  eventType: event.type,
10103
10417
  payload
@@ -10205,7 +10519,7 @@ var WebhookDelivery = class {
10205
10519
  };
10206
10520
 
10207
10521
  // src/gateway/webhooks/events.ts
10208
- var import_node_crypto13 = require("crypto");
10522
+ var import_node_crypto12 = require("crypto");
10209
10523
  var WEBHOOK_TOPICS = [
10210
10524
  "interaction.created",
10211
10525
  "interaction.resolved",
@@ -10220,8 +10534,8 @@ var WEBHOOK_TOPICS = [
10220
10534
  "proposal.approved",
10221
10535
  "proposal.rejected"
10222
10536
  ];
10223
- function newEventId2() {
10224
- return `evt_${(0, import_node_crypto13.randomBytes)(8).toString("hex")}`;
10537
+ function newEventId() {
10538
+ return `evt_${(0, import_node_crypto12.randomBytes)(8).toString("hex")}`;
10225
10539
  }
10226
10540
  function wireWebhookFanout({ bus, store, delivery }) {
10227
10541
  const handlers = [];
@@ -10232,7 +10546,7 @@ function wireWebhookFanout({ bus, store, delivery }) {
10232
10546
  const subs = await store.listForEvent(eventType);
10233
10547
  if (subs.length === 0) return;
10234
10548
  const event = {
10235
- id: newEventId2(),
10549
+ id: newEventId(),
10236
10550
  type: eventType,
10237
10551
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10238
10552
  data
@@ -10251,7 +10565,7 @@ function wireWebhookFanout({ bus, store, delivery }) {
10251
10565
  }
10252
10566
 
10253
10567
  // src/gateway/telemetry/fanout.ts
10254
- var import_node_crypto14 = require("crypto");
10568
+ var import_node_crypto13 = require("crypto");
10255
10569
  var import_core12 = require("@harness-engineering/core");
10256
10570
  var TOPICS = {
10257
10571
  MAINTENANCE_STARTED: "maintenance:started",
@@ -10274,14 +10588,14 @@ var SPAN_NAME = {
10274
10588
  [TOPICS.DISPATCH_DECISION]: "dispatch_decision",
10275
10589
  [TOPICS.SKILL_INVOCATION]: "skill_invocation"
10276
10590
  };
10277
- function newEventId3() {
10278
- return `evt_${(0, import_node_crypto14.randomBytes)(8).toString("hex")}`;
10591
+ function newEventId2() {
10592
+ return `evt_${(0, import_node_crypto13.randomBytes)(8).toString("hex")}`;
10279
10593
  }
10280
10594
  function newTraceId() {
10281
- return (0, import_node_crypto14.randomBytes)(16).toString("hex");
10595
+ return (0, import_node_crypto13.randomBytes)(16).toString("hex");
10282
10596
  }
10283
10597
  function newSpanId() {
10284
- return (0, import_node_crypto14.randomBytes)(8).toString("hex");
10598
+ return (0, import_node_crypto13.randomBytes)(8).toString("hex");
10285
10599
  }
10286
10600
  function nowNs() {
10287
10601
  return BigInt(Date.now()) * 1000000n;
@@ -10395,7 +10709,7 @@ function wireTelemetryFanout(params) {
10395
10709
  };
10396
10710
  exporter.push(span);
10397
10711
  const gatewayEvent = {
10398
- id: newEventId3(),
10712
+ id: newEventId2(),
10399
10713
  type: TELEMETRY_TYPE[topic],
10400
10714
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10401
10715
  data: payload,
@@ -10589,7 +10903,7 @@ function buildSlackSink(config, options) {
10589
10903
  }
10590
10904
 
10591
10905
  // src/notifications/events.ts
10592
- var import_node_crypto15 = require("crypto");
10906
+ var import_node_crypto14 = require("crypto");
10593
10907
 
10594
10908
  // src/notifications/envelope.ts
10595
10909
  function asObj(data) {
@@ -10720,8 +11034,8 @@ var NOTIFICATION_TOPICS = [
10720
11034
  "proposal.approved",
10721
11035
  "proposal.rejected"
10722
11036
  ];
10723
- function newEventId4() {
10724
- return `evt_${(0, import_node_crypto15.randomBytes)(8).toString("hex")}`;
11037
+ function newEventId3() {
11038
+ return `evt_${(0, import_node_crypto14.randomBytes)(8).toString("hex")}`;
10725
11039
  }
10726
11040
  function dispatchToEntry(bus, entry, event) {
10727
11041
  const eventType = event.type;
@@ -10756,7 +11070,7 @@ function wireNotificationSinks({ bus, registry }) {
10756
11070
  const entries = registry.list();
10757
11071
  if (entries.length === 0) return;
10758
11072
  const event = {
10759
- id: newEventId4(),
11073
+ id: newEventId3(),
10760
11074
  type: eventType,
10761
11075
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10762
11076
  data
@@ -10774,7 +11088,7 @@ function wireNotificationSinks({ bus, registry }) {
10774
11088
  }
10775
11089
 
10776
11090
  // src/orchestrator.ts
10777
- var import_core16 = require("@harness-engineering/core");
11091
+ var import_core17 = require("@harness-engineering/core");
10778
11092
 
10779
11093
  // src/logging/logger.ts
10780
11094
  var StructuredLogger = class {
@@ -10859,6 +11173,37 @@ async function scanWorkspaceConfig(workspacePath) {
10859
11173
  return { exitCode: (0, import_core13.computeScanExitCode)(results), results };
10860
11174
  }
10861
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
+
10862
11207
  // src/maintenance/task-registry.ts
10863
11208
  var BUILT_IN_TASKS = [
10864
11209
  // --- Mechanical-AI ---
@@ -10922,7 +11267,13 @@ var BUILT_IN_TASKS = [
10922
11267
  description: "Detect and fix cross-check violations",
10923
11268
  schedule: "0 6 * * 1",
10924
11269
  branch: "harness-maint/cross-check-fixes",
10925
- 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"],
10926
11277
  fixSkill: "harness-cross-check-fix"
10927
11278
  },
10928
11279
  // --- Pure-AI ---
@@ -10981,7 +11332,10 @@ var BUILT_IN_TASKS = [
10981
11332
  description: "Assess overall project health",
10982
11333
  schedule: "0 6 * * *",
10983
11334
  branch: null,
10984
- 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"]
10985
11339
  },
10986
11340
  {
10987
11341
  id: "stale-constraints",
@@ -10989,7 +11343,14 @@ var BUILT_IN_TASKS = [
10989
11343
  description: "Detect stale architectural constraints",
10990
11344
  schedule: "0 2 1 * *",
10991
11345
  branch: null,
10992
- 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"]
10993
11354
  },
10994
11355
  {
10995
11356
  id: "graph-refresh",
@@ -11022,7 +11383,8 @@ var BUILT_IN_TASKS = [
11022
11383
  description: "Clean up stale orchestrator sessions",
11023
11384
  schedule: "0 0 * * *",
11024
11385
  branch: null,
11025
- checkCommand: ["cleanup-sessions"]
11386
+ checkCommand: ["cleanup-sessions"],
11387
+ excludeFromHumanSweep: true
11026
11388
  },
11027
11389
  {
11028
11390
  id: "perf-baselines",
@@ -11030,7 +11392,8 @@ var BUILT_IN_TASKS = [
11030
11392
  description: "Update performance baselines",
11031
11393
  schedule: "0 7 * * *",
11032
11394
  branch: null,
11033
- checkCommand: ["perf", "baselines", "update"]
11395
+ checkCommand: ["perf", "baselines", "update"],
11396
+ excludeFromHumanSweep: true
11034
11397
  },
11035
11398
  {
11036
11399
  id: "main-sync",
@@ -11038,7 +11401,8 @@ var BUILT_IN_TASKS = [
11038
11401
  description: "Fast-forward local default branch from origin",
11039
11402
  schedule: "*/15 * * * *",
11040
11403
  branch: null,
11041
- checkCommand: ["harness", "sync-main", "--json"]
11404
+ checkCommand: ["harness", "sync-main", "--json"],
11405
+ excludeFromHumanSweep: true
11042
11406
  },
11043
11407
  // Hermes Phase 4 — one-shot backfill that stamps `provenance: user-authored`
11044
11408
  // on every existing catalog skill. Schedule is Feb 31 (a date that never
@@ -11051,7 +11415,8 @@ var BUILT_IN_TASKS = [
11051
11415
  description: "Backfill provenance: user-authored on every existing skill (one-shot, idempotent)",
11052
11416
  schedule: "0 0 31 2 *",
11053
11417
  branch: null,
11054
- checkCommand: ["backfill-skill-provenance"]
11418
+ checkCommand: ["backfill-skill-provenance"],
11419
+ excludeFromHumanSweep: true
11055
11420
  }
11056
11421
  ];
11057
11422
 
@@ -11118,6 +11483,17 @@ function cronMatchesNow(expression, now) {
11118
11483
  const daysOfWeek = parseField(dowField, 0, 6);
11119
11484
  return minutes.has(minute) && hours.has(hour) && daysOfMonth.has(dayOfMonth) && months.has(month) && daysOfWeek.has(dayOfWeek);
11120
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
+ }
11121
11497
 
11122
11498
  // src/maintenance/scheduler.ts
11123
11499
  var MaintenanceScheduler = class {
@@ -11359,15 +11735,15 @@ var MaintenanceScheduler = class {
11359
11735
  };
11360
11736
 
11361
11737
  // src/maintenance/leader-elector.ts
11362
- var import_types29 = require("@harness-engineering/types");
11738
+ var import_types30 = require("@harness-engineering/types");
11363
11739
  var SingleProcessLeaderElector = class {
11364
11740
  async electLeader() {
11365
- return (0, import_types29.Ok)("claimed");
11741
+ return (0, import_types30.Ok)("claimed");
11366
11742
  }
11367
11743
  };
11368
11744
 
11369
11745
  // src/maintenance/reporter.ts
11370
- var fs16 = __toESM(require("fs"));
11746
+ var fs14 = __toESM(require("fs"));
11371
11747
  var path18 = __toESM(require("path"));
11372
11748
  var import_zod17 = require("zod");
11373
11749
  var RunResultSchema = import_zod17.z.object({
@@ -11403,9 +11779,9 @@ var MaintenanceReporter = class {
11403
11779
  */
11404
11780
  async load() {
11405
11781
  try {
11406
- await fs16.promises.mkdir(this.persistDir, { recursive: true });
11782
+ await fs14.promises.mkdir(this.persistDir, { recursive: true });
11407
11783
  const filePath = path18.join(this.persistDir, "history.json");
11408
- const data = await fs16.promises.readFile(filePath, "utf-8");
11784
+ const data = await fs14.promises.readFile(filePath, "utf-8");
11409
11785
  const parsed = import_zod17.z.array(RunResultSchema).safeParse(JSON.parse(data));
11410
11786
  if (parsed.success) {
11411
11787
  this.history = parsed.data.slice(0, MAX_HISTORY);
@@ -11439,9 +11815,9 @@ var MaintenanceReporter = class {
11439
11815
  */
11440
11816
  async persist() {
11441
11817
  try {
11442
- await fs16.promises.mkdir(this.persistDir, { recursive: true });
11818
+ await fs14.promises.mkdir(this.persistDir, { recursive: true });
11443
11819
  const filePath = path18.join(this.persistDir, "history.json");
11444
- 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");
11445
11821
  } catch (err) {
11446
11822
  this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
11447
11823
  }
@@ -11481,20 +11857,20 @@ var TaskRunner = class {
11481
11857
  * @param origin - Hermes Phase 2 trigger-source tag; defaults to `'cron'`
11482
11858
  * when called from the scheduler path.
11483
11859
  */
11484
- async run(task, origin = "cron") {
11860
+ async run(task, origin = "cron", mode = "fix") {
11485
11861
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
11486
11862
  let result;
11487
11863
  let captured;
11488
11864
  try {
11489
11865
  switch (task.type) {
11490
11866
  case "mechanical-ai": {
11491
- const out = await this.runMechanicalAI(task, startedAt);
11867
+ const out = await this.runMechanicalAI(task, startedAt, mode);
11492
11868
  result = out.result;
11493
11869
  captured = out.captured;
11494
11870
  break;
11495
11871
  }
11496
11872
  case "pure-ai":
11497
- result = await this.runPureAI(task, startedAt);
11873
+ result = await this.runPureAI(task, startedAt, mode);
11498
11874
  break;
11499
11875
  case "report-only": {
11500
11876
  const out = await this.runReportOnly(task, startedAt);
@@ -11503,7 +11879,7 @@ var TaskRunner = class {
11503
11879
  break;
11504
11880
  }
11505
11881
  case "housekeeping": {
11506
- const out = await this.runHousekeeping(task, startedAt);
11882
+ const out = await this.runHousekeeping(task, startedAt, mode);
11507
11883
  result = out.result;
11508
11884
  captured = out.captured;
11509
11885
  break;
@@ -11567,7 +11943,8 @@ var TaskRunner = class {
11567
11943
  findings: r2.findings,
11568
11944
  stdout: r2.output,
11569
11945
  stderr: r2.stderr,
11570
- structured: r2.structured ? r2.structured : null
11946
+ structured: r2.structured ? r2.structured : null,
11947
+ executionFailed: false
11571
11948
  };
11572
11949
  }
11573
11950
  if (!task.checkCommand || task.checkCommand.length === 0) {
@@ -11579,7 +11956,8 @@ var TaskRunner = class {
11579
11956
  findings: r.findings,
11580
11957
  stdout: r.output,
11581
11958
  stderr: "",
11582
- structured: null
11959
+ structured: null,
11960
+ executionFailed: r.executionFailed ?? false
11583
11961
  };
11584
11962
  }
11585
11963
  /**
@@ -11604,7 +11982,7 @@ var TaskRunner = class {
11604
11982
  * only if fixable findings exist; persist captured stdout/stderr/context
11605
11983
  * via the output store on the way out.
11606
11984
  */
11607
- async runMechanicalAI(task, startedAt) {
11985
+ async runMechanicalAI(task, startedAt, mode = "fix") {
11608
11986
  if (!task.fixSkill) {
11609
11987
  return wrap(this.failureResult(task.id, startedAt, "mechanical-ai task missing fixSkill"));
11610
11988
  }
@@ -11626,6 +12004,27 @@ var TaskRunner = class {
11626
12004
  } catch (err) {
11627
12005
  return wrap(this.failureResult(task.id, startedAt, String(err)));
11628
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
+ }
11629
12028
  const promptContext = await this.composePromptContext(task);
11630
12029
  const baseCaptured = {
11631
12030
  stdout: check.stdout,
@@ -11634,13 +12033,14 @@ var TaskRunner = class {
11634
12033
  ...promptContext ? { context: promptContext } : {}
11635
12034
  };
11636
12035
  const wakeAgentExplicitlyFalse = check.structured !== null && typeof check.structured === "object" && check.structured.wakeAgent === false;
11637
- if (check.findings === 0 || wakeAgentExplicitlyFalse) {
12036
+ if (check.findings === 0 || wakeAgentExplicitlyFalse || mode === "report") {
12037
+ const reportedWithFindings = mode === "report" && check.findings > 0;
11638
12038
  return {
11639
12039
  result: {
11640
12040
  taskId: task.id,
11641
12041
  startedAt,
11642
12042
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
11643
- status: "no-issues",
12043
+ status: reportedWithFindings ? "success" : "no-issues",
11644
12044
  findings: check.findings,
11645
12045
  fixed: 0,
11646
12046
  prUrl: null,
@@ -11710,7 +12110,19 @@ var TaskRunner = class {
11710
12110
  /**
11711
12111
  * Pure-AI: always dispatch agent with configured skill.
11712
12112
  */
11713
- 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
+ }
11714
12126
  if (!task.fixSkill) {
11715
12127
  return this.failureResult(task.id, startedAt, "pure-ai task missing fixSkill");
11716
12128
  }
@@ -11784,6 +12196,27 @@ var TaskRunner = class {
11784
12196
  return wrap(this.failureResult(task.id, startedAt, String(err)));
11785
12197
  }
11786
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
+ }
11787
12220
  const status = parsed?.status ?? "success";
11788
12221
  const findings = parsed === null ? check.findings : typeof parsed.candidatesFound === "number" ? parsed.candidatesFound : 0;
11789
12222
  const result = {
@@ -11817,7 +12250,20 @@ var TaskRunner = class {
11817
12250
  * Hermes Phase 2: a `checkScript` may replace `checkCommand` for housekeeping
11818
12251
  * tasks; the runner falls through to the same JSON-status parsing path.
11819
12252
  */
11820
- 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
+ }
11821
12267
  if (!task.checkCommand && !task.checkScript) {
11822
12268
  return wrap(
11823
12269
  this.failureResult(
@@ -11884,10 +12330,86 @@ var TaskRunner = class {
11884
12330
  error
11885
12331
  };
11886
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
+ }
11887
12353
  };
11888
12354
  function wrap(result, captured) {
11889
12355
  return captured ? { result, captured } : { result };
11890
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
+ }
11891
12413
  function parseStatusLine(output) {
11892
12414
  const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
11893
12415
  for (let i = lines.length - 1; i >= 0; i--) {
@@ -12036,8 +12558,49 @@ function heuristicResult(stdout, stderr, exitedAbnormally) {
12036
12558
  };
12037
12559
  }
12038
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
+
12039
12602
  // src/maintenance/output-store.ts
12040
- var fs17 = __toESM(require("fs"));
12603
+ var fs15 = __toESM(require("fs"));
12041
12604
  var path20 = __toESM(require("path"));
12042
12605
  var DEFAULT_RETENTION = {
12043
12606
  runs: 50,
@@ -12078,13 +12641,13 @@ var TaskOutputStore = class {
12078
12641
  async write(taskId, entry, retention) {
12079
12642
  this.ensureSafeTaskId(taskId);
12080
12643
  const dir = this.dirFor(taskId);
12081
- await fs17.promises.mkdir(dir, { recursive: true });
12644
+ await fs15.promises.mkdir(dir, { recursive: true });
12082
12645
  const fileName = `${sanitizeIso(entry.completedAt || (/* @__PURE__ */ new Date()).toISOString())}.json`;
12083
12646
  const filePath = path20.join(dir, fileName);
12084
12647
  const tmpPath = `${filePath}.tmp`;
12085
12648
  const payload = JSON.stringify(entry, null, 2);
12086
- await fs17.promises.writeFile(tmpPath, payload, "utf-8");
12087
- await fs17.promises.rename(tmpPath, filePath);
12649
+ await fs15.promises.writeFile(tmpPath, payload, "utf-8");
12650
+ await fs15.promises.rename(tmpPath, filePath);
12088
12651
  try {
12089
12652
  await this.applyRetention(taskId, retention);
12090
12653
  } catch (err) {
@@ -12135,7 +12698,7 @@ var TaskOutputStore = class {
12135
12698
  }
12136
12699
  async readEntry(filePath) {
12137
12700
  try {
12138
- const buf = await fs17.promises.readFile(filePath, "utf-8");
12701
+ const buf = await fs15.promises.readFile(filePath, "utf-8");
12139
12702
  const parsed = JSON.parse(buf);
12140
12703
  return parsed;
12141
12704
  } catch {
@@ -12157,7 +12720,7 @@ var TaskOutputStore = class {
12157
12720
  const toRemove = /* @__PURE__ */ new Set([...overflow, ...aged]);
12158
12721
  for (const name of toRemove) {
12159
12722
  try {
12160
- await fs17.promises.unlink(path20.join(dir, name));
12723
+ await fs15.promises.unlink(path20.join(dir, name));
12161
12724
  } catch {
12162
12725
  }
12163
12726
  }
@@ -12166,7 +12729,7 @@ var TaskOutputStore = class {
12166
12729
  async function listJsonFilesDescending(dir) {
12167
12730
  let names;
12168
12731
  try {
12169
- names = await fs17.promises.readdir(dir);
12732
+ names = await fs15.promises.readdir(dir);
12170
12733
  } catch {
12171
12734
  return [];
12172
12735
  }
@@ -12275,7 +12838,7 @@ var fallbackLogger3 = {
12275
12838
  };
12276
12839
 
12277
12840
  // src/maintenance/custom-task-validator.ts
12278
- var import_types30 = require("@harness-engineering/types");
12841
+ var import_types31 = require("@harness-engineering/types");
12279
12842
  var ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
12280
12843
  var REQUIRED_FIELDS_BY_TYPE = {
12281
12844
  "mechanical-ai": ["branch", "fixSkill"],
@@ -12285,7 +12848,7 @@ var REQUIRED_FIELDS_BY_TYPE = {
12285
12848
  };
12286
12849
  function validateCustomTasks(customTasks, builtIns, deps = {}) {
12287
12850
  const errors = [];
12288
- if (!customTasks) return (0, import_types30.Ok)(void 0);
12851
+ if (!customTasks) return (0, import_types31.Ok)(void 0);
12289
12852
  const builtInIds = new Set(builtIns.map((t) => t.id));
12290
12853
  const customIds = Object.keys(customTasks);
12291
12854
  const allIds = /* @__PURE__ */ new Set([...builtInIds, ...customIds]);
@@ -12295,7 +12858,7 @@ function validateCustomTasks(customTasks, builtIns, deps = {}) {
12295
12858
  validateOne(id, task, builtInIds, allIds, deps, errors);
12296
12859
  }
12297
12860
  detectCycles(customTasks, builtIns, errors);
12298
- 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);
12299
12862
  }
12300
12863
  function validateOne(id, task, builtInIds, allIds, deps, errors) {
12301
12864
  const prefix = `customTasks.${id}`;
@@ -12484,6 +13047,11 @@ function deriveSeedPaths(config) {
12484
13047
  const roadmapPath = config.tracker.kind === "roadmap" && config.tracker.filePath ? config.tracker.filePath : "docs/roadmap.md";
12485
13048
  return [".harness/proposals", roadmapPath];
12486
13049
  }
13050
+ function normalizeHarnessCommand(command) {
13051
+ if (command.length === 0) return [];
13052
+ if (command[0] === "harness") return command;
13053
+ return ["harness", ...command];
13054
+ }
12487
13055
  var Orchestrator = class extends import_node_events.EventEmitter {
12488
13056
  state;
12489
13057
  config;
@@ -12610,6 +13178,11 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12610
13178
  abortControllers = /* @__PURE__ */ new Map();
12611
13179
  /** Guards against overlapping ticks when a tick takes longer than the polling interval */
12612
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;
12613
13186
  /** Timestamp of the last stale branch sweep (at most once per hour) */
12614
13187
  lastBranchSweepMs = 0;
12615
13188
  /** Current tick-phase activity visible to the dashboard */
@@ -12683,7 +13256,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12683
13256
  this.localResolvers.set(name, new LocalModelResolver(resolverOpts));
12684
13257
  }
12685
13258
  }
12686
- this.cacheMetrics = new import_core16.CacheMetricsRecorder();
13259
+ this.cacheMetrics = new import_core17.CacheMetricsRecorder();
12687
13260
  if (this.config.agent.backends !== void 0 && Object.keys(this.config.agent.backends).length > 0) {
12688
13261
  const sandboxPolicy = this.config.agent.sandboxPolicy === "docker" ? "docker" : "none";
12689
13262
  const firstBackendName = Object.keys(this.config.agent.backends)[0];
@@ -12769,7 +13342,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12769
13342
  this.setupNotifications(config.notifications);
12770
13343
  const otlpCfg = config.telemetry?.export?.otlp;
12771
13344
  if (otlpCfg) {
12772
- this.otlpExporter = new import_core16.OTLPExporter({
13345
+ this.otlpExporter = new import_core17.OTLPExporter({
12773
13346
  endpoint: otlpCfg.endpoint,
12774
13347
  ...otlpCfg.enabled !== void 0 ? { enabled: otlpCfg.enabled } : {},
12775
13348
  ...otlpCfg.headers !== void 0 ? { headers: otlpCfg.headers } : {},
@@ -12838,7 +13411,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12838
13411
  ...this.config.tracker.apiKey ? { token: this.config.tracker.apiKey } : {},
12839
13412
  ...this.config.tracker.endpoint ? { apiBase: this.config.tracker.endpoint } : {}
12840
13413
  };
12841
- const clientResult = (0, import_core15.createTrackerClient)(trackerCfg);
13414
+ const clientResult = (0, import_core16.createTrackerClient)(trackerCfg);
12842
13415
  if (!clientResult.ok) throw clientResult.error;
12843
13416
  return new GitHubIssuesIssueTrackerAdapter(clientResult.value, this.config.tracker);
12844
13417
  }
@@ -12856,48 +13429,30 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12856
13429
  const logger = this.logger;
12857
13430
  const checkRunner = {
12858
13431
  run: async (command, cwd) => {
12859
- const { execFile: execFile7 } = await import("child_process");
12860
- const { promisify: promisify5 } = await import("util");
12861
- const execFileAsync2 = promisify5(execFile7);
12862
- const [cmd, ...args] = command;
12863
- if (!cmd) return { passed: true, findings: 0, output: "" };
12864
- try {
12865
- const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
12866
- const findingsMatch = stdout.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
12867
- const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 0;
12868
- return { passed: findings === 0, findings, output: stdout };
12869
- } catch (err) {
12870
- const error = err;
12871
- const output = [error.stdout, error.stderr].filter(Boolean).join("\n");
12872
- const findingsMatch = output.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
12873
- const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 1;
12874
- return { passed: false, findings, output };
12875
- }
12876
- }
12877
- };
12878
- const agentDispatcher = {
12879
- dispatch: async (skill, branch, backendName, cwd) => {
12880
- logger.info(
12881
- "Maintenance agent dispatcher invoked (stub \u2014 skill dispatch integration pending)",
12882
- {
12883
- skill,
12884
- branch,
12885
- backendName,
12886
- cwd
12887
- }
12888
- );
12889
- 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);
12890
13435
  }
12891
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
+ });
12892
13443
  const commandExecutor = {
12893
13444
  exec: async (command, cwd) => {
12894
13445
  const { execFile: execFile7 } = await import("child_process");
12895
- const { promisify: promisify5 } = await import("util");
12896
- const execFileAsync2 = promisify5(execFile7);
12897
- const [cmd, ...args] = command;
13446
+ const { promisify: promisify6 } = await import("util");
13447
+ const execFileAsync2 = promisify6(execFile7);
13448
+ const [cmd, ...args] = normalizeHarnessCommand(command);
12898
13449
  if (!cmd) return { stdout: "" };
12899
13450
  try {
12900
- 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
+ });
12901
13456
  return { stdout: String(stdout) };
12902
13457
  } catch (err) {
12903
13458
  logger.warn("Maintenance command execution failed", {
@@ -13032,6 +13587,10 @@ ${messages}`);
13032
13587
  async asyncTick() {
13033
13588
  await this.ensureClaimManager();
13034
13589
  await this.intelligenceRunner.loadPersistedData();
13590
+ if (!this.laneReadbackDone) {
13591
+ this.laneReadbackDone = true;
13592
+ await this.readBackPersistedLanes();
13593
+ }
13035
13594
  const nowMs = Date.now();
13036
13595
  this.setTickActivity("fetching", "Polling tracker for candidates");
13037
13596
  const candidatesResult = await this.tracker.fetchCandidateIssues();
@@ -13184,12 +13743,48 @@ ${messages}`);
13184
13743
  break;
13185
13744
  case "escalate":
13186
13745
  await this.handleEscalation(effect);
13746
+ await this.persistLaneSafe(effect.issueId, "abandon");
13187
13747
  break;
13188
13748
  case "claim":
13189
13749
  await this.handleClaimEffect(effect);
13190
13750
  break;
13191
13751
  }
13192
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
+ }
13193
13788
  /**
13194
13789
  * Guards workspace cleanup by checking whether the agent pushed a branch
13195
13790
  * that does not yet have a pull request. If so, the worktree is preserved
@@ -13222,7 +13817,7 @@ ${messages}`);
13222
13817
  { issueId }
13223
13818
  );
13224
13819
  await this.interactionQueue.push({
13225
- id: `interaction-${(0, import_node_crypto16.randomUUID)()}`,
13820
+ id: `interaction-${(0, import_node_crypto15.randomUUID)()}`,
13226
13821
  issueId,
13227
13822
  type: "needs-human",
13228
13823
  reasons: [`Agent pushed branch "${branch}" but did not create a PR. Worktree preserved.`],
@@ -13298,7 +13893,7 @@ ${messages}`);
13298
13893
  { issueId: effect.issueId }
13299
13894
  );
13300
13895
  await this.interactionQueue.push({
13301
- id: `interaction-${(0, import_node_crypto16.randomUUID)()}`,
13896
+ id: `interaction-${(0, import_node_crypto15.randomUUID)()}`,
13302
13897
  issueId: effect.issueId,
13303
13898
  type: "needs-human",
13304
13899
  reasons: effect.reasons,
@@ -13376,6 +13971,7 @@ ${messages}`);
13376
13971
  }
13377
13972
  return;
13378
13973
  }
13974
+ await this.persistLaneSafe(effect.issue.id, "claim");
13379
13975
  await this.postClaimComment(effect.issue);
13380
13976
  await this.dispatchIssue(effect.issue, effect.attempt, effect.backend);
13381
13977
  }
@@ -13394,12 +13990,12 @@ ${messages}`);
13394
13990
  async postLifecycleComment(identifier, externalId, event) {
13395
13991
  try {
13396
13992
  if (!externalId) return;
13397
- const trackerConfig = (0, import_core15.loadTrackerSyncConfig)(this.projectRoot);
13993
+ const trackerConfig = (0, import_core16.loadTrackerSyncConfig)(this.projectRoot);
13398
13994
  if (!trackerConfig) return;
13399
13995
  const token = process.env.GITHUB_TOKEN;
13400
13996
  if (!token) return;
13401
13997
  const orchestratorId = await this.orchestratorIdPromise;
13402
- const adapter = new import_core15.GitHubIssuesSyncAdapter({ token, config: trackerConfig });
13998
+ const adapter = new import_core16.GitHubIssuesSyncAdapter({ token, config: trackerConfig });
13403
13999
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
13404
14000
  const actionMap = {
13405
14001
  claimed: "Dispatching agent for autonomous execution",
@@ -13436,6 +14032,7 @@ ${messages}`);
13436
14032
  this.logger.info(`Dispatching issue: ${issue.identifier} (attempt ${attempt})`, {
13437
14033
  issueId: issue.id
13438
14034
  });
14035
+ await this.persistLaneSafe(issue.id, "dispatch");
13439
14036
  try {
13440
14037
  const workspaceResult = await this.workspace.ensureWorkspace(issue.identifier);
13441
14038
  if (!workspaceResult.ok) throw workspaceResult.error;
@@ -13472,7 +14069,7 @@ ${messages}`);
13472
14069
  ...f.line !== void 0 ? { line: f.line } : {}
13473
14070
  }))
13474
14071
  );
13475
- (0, import_core14.writeTaint)(
14072
+ (0, import_core15.writeTaint)(
13476
14073
  workspacePath,
13477
14074
  issue.id,
13478
14075
  "Medium-severity injection patterns found in workspace config files",
@@ -13644,6 +14241,7 @@ ${messages}`);
13644
14241
  * Informs the state machine that an agent worker has exited.
13645
14242
  */
13646
14243
  async emitWorkerExit(issueId, reason, attempt, error) {
14244
+ await this.persistLaneSafe(issueId, reason === "normal" ? "success" : "failure");
13647
14245
  await this.completionHandler.handleWorkerExit(
13648
14246
  issueId,
13649
14247
  reason,
@@ -14179,11 +14777,11 @@ function launchTUI(orchestrator) {
14179
14777
  }
14180
14778
 
14181
14779
  // src/maintenance/sync-main.ts
14182
- var import_node_child_process12 = require("child_process");
14183
- var import_node_util4 = require("util");
14780
+ var import_node_child_process14 = require("child_process");
14781
+ var import_node_util5 = require("util");
14184
14782
  var DEFAULT_TIMEOUT_MS3 = 6e4;
14185
14783
  async function git(execFileFn, args, cwd, timeoutMs) {
14186
- const exec = (0, import_node_util4.promisify)(execFileFn);
14784
+ const exec = (0, import_node_util5.promisify)(execFileFn);
14187
14785
  const { stdout, stderr } = await exec("git", args, { cwd, timeout: timeoutMs });
14188
14786
  return { stdout: String(stdout), stderr: String(stderr) };
14189
14787
  }
@@ -14245,7 +14843,7 @@ async function isAncestor(execFileFn, a, b, cwd, timeoutMs) {
14245
14843
  }
14246
14844
  }
14247
14845
  async function syncMain(repoRoot, opts = {}) {
14248
- const execFileFn = opts.execFileFn ?? import_node_child_process12.execFile;
14846
+ const execFileFn = opts.execFileFn ?? import_node_child_process14.execFile;
14249
14847
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
14250
14848
  try {
14251
14849
  const originRef = await resolveOriginDefault(execFileFn, repoRoot, timeoutMs);
@@ -14322,11 +14920,59 @@ async function syncMain(repoRoot, opts = {}) {
14322
14920
  }
14323
14921
  }
14324
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
+
14325
14971
  // src/sessions/search-index.ts
14326
- var fs18 = __toESM(require("fs"));
14972
+ var fs16 = __toESM(require("fs"));
14327
14973
  var path22 = __toESM(require("path"));
14328
14974
  var import_better_sqlite32 = __toESM(require("better-sqlite3"));
14329
- var import_types31 = require("@harness-engineering/types");
14975
+ var import_types32 = require("@harness-engineering/types");
14330
14976
  var SEARCH_INDEX_FILE = "search-index.sqlite";
14331
14977
  var SCHEMA_SQL2 = `
14332
14978
  CREATE TABLE IF NOT EXISTS session_docs (
@@ -14384,7 +15030,7 @@ var SqliteSearchIndex = class {
14384
15030
  removeSessionStmt;
14385
15031
  totalStmt;
14386
15032
  constructor(dbPath) {
14387
- fs18.mkdirSync(path22.dirname(dbPath), { recursive: true });
15033
+ fs16.mkdirSync(path22.dirname(dbPath), { recursive: true });
14388
15034
  this.db = new import_better_sqlite32.default(dbPath);
14389
15035
  this.db.pragma("journal_mode = WAL");
14390
15036
  this.db.pragma("synchronous = NORMAL");
@@ -14484,18 +15130,18 @@ function openSearchIndex(projectPath) {
14484
15130
  return new SqliteSearchIndex(searchIndexPath(projectPath));
14485
15131
  }
14486
15132
  function indexSessionDirectory(idx, args) {
14487
- const kinds = args.fileKinds ?? [...import_types31.INDEXED_FILE_KINDS];
15133
+ const kinds = args.fileKinds ?? [...import_types32.INDEXED_FILE_KINDS];
14488
15134
  const cap = args.maxBytesPerBody ?? 256 * 1024;
14489
15135
  let docsWritten = 0;
14490
15136
  for (const kind of kinds) {
14491
15137
  const fileName = FILE_KIND_TO_FILENAME[kind];
14492
15138
  const filePath = path22.join(args.sessionDir, fileName);
14493
- if (!fs18.existsSync(filePath)) continue;
14494
- let body = fs18.readFileSync(filePath, "utf8");
15139
+ if (!fs16.existsSync(filePath)) continue;
15140
+ let body = fs16.readFileSync(filePath, "utf8");
14495
15141
  if (Buffer.byteLength(body, "utf8") > cap) {
14496
15142
  body = body.slice(0, cap) + "\n\n[TRUNCATED]";
14497
15143
  }
14498
- const stat = fs18.statSync(filePath);
15144
+ const stat = fs16.statSync(filePath);
14499
15145
  const relPath = path22.relative(args.projectPath, filePath).replaceAll("\\", "/");
14500
15146
  idx.upsertSessionDoc({
14501
15147
  sessionId: args.sessionId,
@@ -14517,8 +15163,8 @@ function reindexFromArchive(projectPath, opts = {}) {
14517
15163
  idx.resetArchived();
14518
15164
  let sessionsIndexed = 0;
14519
15165
  let docsWritten = 0;
14520
- if (fs18.existsSync(archiveBase)) {
14521
- const entries = fs18.readdirSync(archiveBase, { withFileTypes: true });
15166
+ if (fs16.existsSync(archiveBase)) {
15167
+ const entries = fs16.readdirSync(archiveBase, { withFileTypes: true });
14522
15168
  for (const entry of entries) {
14523
15169
  if (!entry.isDirectory()) continue;
14524
15170
  const sessionDir = path22.join(archiveBase, entry.name);
@@ -14541,10 +15187,10 @@ function reindexFromArchive(projectPath, opts = {}) {
14541
15187
  }
14542
15188
 
14543
15189
  // src/sessions/summarize.ts
14544
- var fs19 = __toESM(require("fs"));
15190
+ var fs17 = __toESM(require("fs"));
14545
15191
  var path23 = __toESM(require("path"));
14546
- var import_types32 = require("@harness-engineering/types");
14547
15192
  var import_types33 = require("@harness-engineering/types");
15193
+ var import_types34 = require("@harness-engineering/types");
14548
15194
  var LLM_SUMMARY_FILE = "llm-summary.md";
14549
15195
  var SUMMARY_INPUT_FILES = [
14550
15196
  { filename: "summary.md", kind: "summary" },
@@ -14571,9 +15217,9 @@ function readInputCorpus(archiveDir) {
14571
15217
  const parts = [];
14572
15218
  for (const { filename, kind } of SUMMARY_INPUT_FILES) {
14573
15219
  const p = path23.join(archiveDir, filename);
14574
- if (!fs19.existsSync(p)) continue;
15220
+ if (!fs17.existsSync(p)) continue;
14575
15221
  try {
14576
- const content = fs19.readFileSync(p, "utf8");
15222
+ const content = fs17.readFileSync(p, "utf8");
14577
15223
  if (content.trim().length === 0) continue;
14578
15224
  parts.push(`## FILE: ${kind}
14579
15225
 
@@ -14635,17 +15281,17 @@ status: failed
14635
15281
 
14636
15282
  - reason: ${reason}
14637
15283
  `;
14638
- fs19.writeFileSync(filePath, body, "utf8");
15284
+ fs17.writeFileSync(filePath, body, "utf8");
14639
15285
  return filePath;
14640
15286
  }
14641
15287
  async function summarizeArchivedSession(ctx) {
14642
15288
  const writeStubOnError = ctx.writeStubOnError ?? true;
14643
- if (!fs19.existsSync(ctx.archiveDir)) {
14644
- 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}`));
14645
15291
  }
14646
15292
  const corpus = readInputCorpus(ctx.archiveDir);
14647
15293
  if (corpus.trim().length === 0) {
14648
- 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}`));
14649
15295
  }
14650
15296
  const inputBudgetTokens = ctx.config?.inputBudgetTokens ?? DEFAULT_INPUT_BUDGET_TOKENS;
14651
15297
  const truncated = truncateForBudget(corpus, inputBudgetTokens);
@@ -14654,7 +15300,7 @@ async function summarizeArchivedSession(ctx) {
14654
15300
  const analyzeOpts = {
14655
15301
  prompt,
14656
15302
  systemPrompt: SYSTEM_PROMPT,
14657
- responseSchema: import_types32.SessionSummarySchema,
15303
+ responseSchema: import_types33.SessionSummarySchema,
14658
15304
  ...ctx.config?.model && { model: ctx.config.model }
14659
15305
  };
14660
15306
  let response;
@@ -14678,11 +15324,11 @@ async function summarizeArchivedSession(ctx) {
14678
15324
  } catch {
14679
15325
  }
14680
15326
  }
14681
- return (0, import_types33.Err)(
15327
+ return (0, import_types34.Err)(
14682
15328
  new Error(`session summary failed: ${reason}` + (stubPath ? ` (stub: ${stubPath})` : ""))
14683
15329
  );
14684
15330
  }
14685
- const parsed = import_types32.SessionSummarySchema.safeParse(response.result);
15331
+ const parsed = import_types33.SessionSummarySchema.safeParse(response.result);
14686
15332
  if (!parsed.success) {
14687
15333
  const reason = `schema validation failed: ${parsed.error.message}`;
14688
15334
  ctx.logger?.warn?.("session summary: invalid provider payload", { reason });
@@ -14692,7 +15338,7 @@ async function summarizeArchivedSession(ctx) {
14692
15338
  } catch {
14693
15339
  }
14694
15340
  }
14695
- return (0, import_types33.Err)(new Error(reason));
15341
+ return (0, import_types34.Err)(new Error(reason));
14696
15342
  }
14697
15343
  const meta = {
14698
15344
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -14703,8 +15349,8 @@ async function summarizeArchivedSession(ctx) {
14703
15349
  };
14704
15350
  const filePath = path23.join(ctx.archiveDir, LLM_SUMMARY_FILE);
14705
15351
  const body = renderLlmSummaryMarkdown(parsed.data, meta);
14706
- fs19.writeFileSync(filePath, body, "utf8");
14707
- 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 });
14708
15354
  }
14709
15355
  function isSummaryEnabled(config) {
14710
15356
  if (!config) return false;
@@ -14784,13 +15430,18 @@ function buildArchiveHooks(opts) {
14784
15430
  BUILT_IN_TASKS,
14785
15431
  BackendDefSchema,
14786
15432
  BackendRouter,
15433
+ CheckScriptRunner,
14787
15434
  ClaimManager,
14788
15435
  GateNotReadyError,
14789
15436
  GateRunError,
14790
15437
  InteractionQueue,
15438
+ LinearGraphQLClient,
14791
15439
  LinearGraphQLStub,
14792
15440
  LocalModelResolver,
15441
+ MAINTENANCE_CHECK_MAX_BUFFER,
15442
+ MAINTENANCE_CHECK_TIMEOUT_MS,
14793
15443
  MAX_ATTEMPTS,
15444
+ MaintenanceReporter,
14794
15445
  MockBackend,
14795
15446
  ORCHESTRATOR_IDENTITY_FILE,
14796
15447
  Orchestrator,
@@ -14808,6 +15459,7 @@ function buildArchiveHooks(opts) {
14808
15459
  SqliteSearchIndex,
14809
15460
  StreamRecorder,
14810
15461
  TaskOutputStore,
15462
+ TaskRunner,
14811
15463
  TokenStore,
14812
15464
  WebhookQueue,
14813
15465
  WorkflowLoader,
@@ -14818,7 +15470,9 @@ function buildArchiveHooks(opts) {
14818
15470
  buildArchiveHooks,
14819
15471
  calculateRetryDelay,
14820
15472
  canDispatch,
15473
+ classifyCheckExecutionFailure,
14821
15474
  computeRateLimitDelay,
15475
+ createAgentDispatcher,
14822
15476
  createBackend,
14823
15477
  createEmptyState,
14824
15478
  crossFieldRoutingIssues,
@@ -14830,22 +15484,27 @@ function buildArchiveHooks(opts) {
14830
15484
  emitProposalApproved,
14831
15485
  emitProposalCreated,
14832
15486
  emitProposalRejected,
15487
+ explicitFindingsCount,
14833
15488
  extractHighlights,
14834
15489
  extractTitlePrefix,
14835
15490
  getAvailableSlots,
14836
15491
  getDefaultConfig,
14837
15492
  getPerStateCount,
14838
15493
  indexSessionDirectory,
15494
+ isCheckTimeoutError,
14839
15495
  isEligible,
14840
15496
  isSummaryEnabled,
14841
15497
  launchTUI,
14842
15498
  loadPublishedIndex,
15499
+ makeBackendResolver,
14843
15500
  migrateAgentConfig,
14844
15501
  normalizeFts5Query,
15502
+ normalizeHarnessCommand,
14845
15503
  normalizeLocalModel,
14846
15504
  openSearchIndex,
14847
15505
  promote,
14848
15506
  reconcile,
15507
+ recoverFindingsCount,
14849
15508
  reindexFromArchive,
14850
15509
  renderAnalysisComment,
14851
15510
  renderLlmSummaryMarkdown,
@@ -14855,9 +15514,11 @@ function buildArchiveHooks(opts) {
14855
15514
  routeIssue,
14856
15515
  routingWarnings,
14857
15516
  runGate,
15517
+ runHarnessCheck,
14858
15518
  savePublishedIndex,
14859
15519
  searchIndexPath,
14860
15520
  selectCandidates,
15521
+ selectTasks,
14861
15522
  sortCandidates,
14862
15523
  summarizeArchivedSession,
14863
15524
  syncMain,