@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.mjs CHANGED
@@ -1219,7 +1219,7 @@ var ClaimManager = class {
1219
1219
  // src/core/pr-detector.ts
1220
1220
  import { execFile } from "child_process";
1221
1221
  import { promisify } from "util";
1222
- var PRDetector = class {
1222
+ var PRDetector = class _PRDetector {
1223
1223
  logger;
1224
1224
  execFileFn;
1225
1225
  projectRoot;
@@ -1335,35 +1335,124 @@ var PRDetector = class {
1335
1335
  }
1336
1336
  }
1337
1337
  /**
1338
- * Filters out candidates that already have an open GitHub PR, running
1339
- * checks with limited concurrency to avoid overwhelming the GitHub API.
1340
- * For candidates with an externalId, searches for PRs linked to the
1341
- * GitHub issue. Falls back to `feat/<identifier>` branch lookup otherwise.
1342
- * Fail-open on API errors.
1338
+ * GitHub closing keywords that link a PR to an issue it will close.
1339
+ * @see https://docs.github.com/articles/closing-issues-using-keywords
1340
+ */
1341
+ static CLOSING_REF_RE = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)[\s:]+#(\d+)/gi;
1342
+ /**
1343
+ * Extracts the issue numbers a PR body declares it will close
1344
+ * (e.g. `Closes #42`, `fixes #7`, `Resolves: #99`).
1345
+ */
1346
+ parseClosingIssueNumbers(body) {
1347
+ const nums = [];
1348
+ for (const match of body.matchAll(_PRDetector.CLOSING_REF_RE)) {
1349
+ nums.push(parseInt(match[1], 10));
1350
+ }
1351
+ return nums;
1352
+ }
1353
+ /**
1354
+ * Lists every open PR for a repo in a single `gh pr list` call and returns
1355
+ * the set of issue numbers those PRs close (parsed from their bodies).
1356
+ *
1357
+ * This is the batched replacement for per-issue `gh pr list --search
1358
+ * "closes #N"` queries. Every `gh pr list` form (search or plain list) is
1359
+ * served by GitHub's GraphQL API and consumes from the shared ~5000/hr
1360
+ * GraphQL budget, so issuing one query per candidate per tick exhausted the
1361
+ * limit on busy boards. A single `--state open` list returns every open PR
1362
+ * in one request regardless of candidate count, collapsing N calls into 1.
1363
+ *
1364
+ * Returns `null` if the check itself failed (gh missing, rate limited,
1365
+ * network error) so callers can fail open rather than block real work.
1366
+ */
1367
+ async fetchOpenPRClosures(owner, repo) {
1368
+ try {
1369
+ const exec = promisify(this.execFileFn);
1370
+ const { stdout } = await exec(
1371
+ "gh",
1372
+ [
1373
+ "pr",
1374
+ "list",
1375
+ "--repo",
1376
+ `${owner}/${repo}`,
1377
+ "--state",
1378
+ "open",
1379
+ "--json",
1380
+ "body",
1381
+ "--limit",
1382
+ "200"
1383
+ ],
1384
+ {
1385
+ cwd: this.projectRoot,
1386
+ timeout: 15e3
1387
+ }
1388
+ );
1389
+ const prs = JSON.parse(stdout);
1390
+ const closed = /* @__PURE__ */ new Set();
1391
+ for (const pr of prs) {
1392
+ for (const n of this.parseClosingIssueNumbers(pr.body ?? "")) closed.add(n);
1393
+ }
1394
+ return closed;
1395
+ } catch (err) {
1396
+ this.logger.debug(`Failed to list open PRs for ${owner}/${repo}`, {
1397
+ error: String(err)
1398
+ });
1399
+ return null;
1400
+ }
1401
+ }
1402
+ /**
1403
+ * Filters out candidates that already have an open GitHub PR.
1404
+ *
1405
+ * For GitHub-issue candidates the check is batched: one `gh pr list` per
1406
+ * distinct repo (not one search per issue), parsing closing references
1407
+ * locally. Candidates without a GitHub externalId fall back to a
1408
+ * `feat/<identifier>` branch lookup, throttled to a few concurrent gh calls.
1409
+ * Fail-open on API errors so a flaky/rate-limited GitHub never blocks work.
1343
1410
  */
1344
1411
  async filterCandidatesWithOpenPRs(candidates) {
1412
+ const repos = /* @__PURE__ */ new Map();
1413
+ for (const candidate of candidates) {
1414
+ const parsed = candidate.externalId ? this.parseExternalId(candidate.externalId) : null;
1415
+ if (parsed) repos.set(`${parsed.owner}/${parsed.repo}`, parsed);
1416
+ }
1417
+ const repoClosures = /* @__PURE__ */ new Map();
1418
+ await Promise.all(
1419
+ [...repos.values()].map(async ({ owner, repo }) => {
1420
+ repoClosures.set(`${owner}/${repo}`, await this.fetchOpenPRClosures(owner, repo));
1421
+ })
1422
+ );
1423
+ const identifierCandidates = candidates.filter(
1424
+ (c) => !(c.externalId && this.parseExternalId(c.externalId))
1425
+ );
1426
+ const identifiersWithOpenPR = /* @__PURE__ */ new Set();
1345
1427
  const concurrency = 3;
1346
- const results = [];
1347
- for (let i = 0; i < candidates.length; i += concurrency) {
1348
- const batch = candidates.slice(i, i + concurrency);
1349
- const batchResults = await Promise.allSettled(
1350
- batch.map(async (candidate) => {
1351
- const hasOpenPR = candidate.externalId ? await this.hasOpenPRForExternalId(candidate.externalId) : await this.hasOpenPRForIdentifier(candidate.identifier);
1352
- return { candidate, hasOpenPR };
1353
- })
1428
+ for (let i = 0; i < identifierCandidates.length; i += concurrency) {
1429
+ const batch = identifierCandidates.slice(i, i + concurrency);
1430
+ const settled = await Promise.allSettled(
1431
+ batch.map(async (c) => ({
1432
+ identifier: c.identifier,
1433
+ hasOpenPR: await this.hasOpenPRForIdentifier(c.identifier)
1434
+ }))
1354
1435
  );
1355
- results.push(...batchResults);
1436
+ for (const result of settled) {
1437
+ if (result.status === "fulfilled" && result.value.hasOpenPR) {
1438
+ identifiersWithOpenPR.add(result.value.identifier);
1439
+ }
1440
+ }
1356
1441
  }
1357
1442
  const filtered = [];
1358
- for (let i = 0; i < results.length; i++) {
1359
- const result = results[i];
1360
- if (!result || result.status === "rejected") {
1361
- filtered.push(candidates[i]);
1362
- continue;
1443
+ for (const candidate of candidates) {
1444
+ const parsed = candidate.externalId ? this.parseExternalId(candidate.externalId) : null;
1445
+ let hasOpenPR;
1446
+ let via;
1447
+ if (parsed) {
1448
+ const closures = repoClosures.get(`${parsed.owner}/${parsed.repo}`);
1449
+ hasOpenPR = closures ? closures.has(parsed.number) : false;
1450
+ via = `externalId ${candidate.externalId}`;
1451
+ } else {
1452
+ hasOpenPR = identifiersWithOpenPR.has(candidate.identifier);
1453
+ via = `feat/${candidate.identifier}`;
1363
1454
  }
1364
- const { candidate, hasOpenPR } = result.value;
1365
1455
  if (hasOpenPR) {
1366
- const via = candidate.externalId ? `externalId ${candidate.externalId}` : `feat/${candidate.identifier}`;
1367
1456
  this.logger.info(`Skipping ${candidate.title}: open PR exists (${via})`);
1368
1457
  } else {
1369
1458
  filtered.push(candidate);
@@ -2120,11 +2209,13 @@ var WorkflowLoader = class {
2120
2209
  };
2121
2210
 
2122
2211
  // src/tracker/adapters/roadmap.ts
2123
- import * as fs8 from "fs/promises";
2124
2212
  import { createHash as createHash2 } from "crypto";
2125
2213
  import {
2126
- parseRoadmap,
2127
- serializeRoadmap
2214
+ resolveRoadmapStoreForFile,
2215
+ applyRoadmapDiff,
2216
+ claim as claimFeature,
2217
+ setStatus as setFeatureStatus,
2218
+ isClaimableBy
2128
2219
  } from "@harness-engineering/core";
2129
2220
  import {
2130
2221
  Ok as Ok4,
@@ -2157,8 +2248,7 @@ var RoadmapTrackerAdapter = class {
2157
2248
  async fetchIssuesByStates(stateNames) {
2158
2249
  try {
2159
2250
  if (!this.config.filePath) return Err3(new Error("Missing filePath"));
2160
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2161
- const roadmapResult = parseRoadmap(content);
2251
+ const roadmapResult = await this.loadRoadmap();
2162
2252
  if (!roadmapResult.ok) return roadmapResult;
2163
2253
  const issues = [];
2164
2254
  for (const milestone of roadmapResult.value.milestones) {
@@ -2189,16 +2279,19 @@ var RoadmapTrackerAdapter = class {
2189
2279
  if (!terminal) {
2190
2280
  return Err3(new Error("Tracker config has no terminalStates; cannot mark complete"));
2191
2281
  }
2192
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2193
- const roadmapResult = parseRoadmap(content);
2282
+ const store = this.resolveStore();
2283
+ const roadmapResult = await store.load();
2194
2284
  if (!roadmapResult.ok) return roadmapResult;
2195
2285
  const roadmap = roadmapResult.value;
2286
+ const before = structuredClone(roadmap);
2196
2287
  const target = this.findFeatureById(roadmap.milestones, issueId);
2197
2288
  if (!target) return Ok4(void 0);
2198
2289
  const normalizedTerminal = this.config.terminalStates.map((s) => s.toLowerCase());
2199
2290
  if (normalizedTerminal.includes(target.status.toLowerCase())) return Ok4(void 0);
2200
- target.status = terminal;
2201
- await fs8.writeFile(this.config.filePath, serializeRoadmap(roadmap), "utf-8");
2291
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2292
+ setFeatureStatus(roadmap, target, terminal, now.slice(0, 10));
2293
+ const persisted = await applyRoadmapDiff(store, before, roadmap);
2294
+ if (!persisted.ok) return persisted;
2202
2295
  return Ok4(void 0);
2203
2296
  } catch (error) {
2204
2297
  return Err3(error instanceof Error ? error : new Error(String(error)));
@@ -2212,22 +2305,24 @@ var RoadmapTrackerAdapter = class {
2212
2305
  async claimIssue(issueId, orchestratorId) {
2213
2306
  try {
2214
2307
  if (!this.config.filePath) return Err3(new Error("Missing filePath"));
2215
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2216
- const roadmapResult = parseRoadmap(content);
2308
+ const store = this.resolveStore();
2309
+ const roadmapResult = await store.load();
2217
2310
  if (!roadmapResult.ok) return roadmapResult;
2218
2311
  const roadmap = roadmapResult.value;
2312
+ const before = structuredClone(roadmap);
2219
2313
  const target = this.findFeatureById(roadmap.milestones, issueId);
2220
2314
  if (!target) return Ok4(void 0);
2221
- if (target.assignee != null && target.assignee !== orchestratorId) {
2315
+ if (!isClaimableBy(target, orchestratorId)) {
2222
2316
  return Ok4(void 0);
2223
2317
  }
2224
2318
  if (target.status === "in-progress" && target.assignee === orchestratorId) {
2225
2319
  return Ok4(void 0);
2226
2320
  }
2227
- target.status = "in-progress";
2228
- target.assignee = orchestratorId;
2229
- target.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2230
- await fs8.writeFile(this.config.filePath, serializeRoadmap(roadmap), "utf-8");
2321
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2322
+ claimFeature(roadmap, target, orchestratorId, now.slice(0, 10));
2323
+ target.updatedAt = now;
2324
+ const persisted = await applyRoadmapDiff(store, before, roadmap);
2325
+ if (!persisted.ok) return persisted;
2231
2326
  return Ok4(void 0);
2232
2327
  } catch (error) {
2233
2328
  return Err3(error instanceof Error ? error : new Error(String(error)));
@@ -2244,24 +2339,39 @@ var RoadmapTrackerAdapter = class {
2244
2339
  if (!activeState) {
2245
2340
  return Err3(new Error("Tracker config has no activeStates; cannot release"));
2246
2341
  }
2247
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2248
- const roadmapResult = parseRoadmap(content);
2342
+ const store = this.resolveStore();
2343
+ const roadmapResult = await store.load();
2249
2344
  if (!roadmapResult.ok) return roadmapResult;
2250
2345
  const roadmap = roadmapResult.value;
2346
+ const before = structuredClone(roadmap);
2251
2347
  const target = this.findFeatureById(roadmap.milestones, issueId);
2252
2348
  if (!target) return Ok4(void 0);
2253
2349
  if (this.config.activeStates.includes(target.status) && target.assignee === null) {
2254
2350
  return Ok4(void 0);
2255
2351
  }
2256
- target.status = activeState;
2257
- target.assignee = null;
2352
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2353
+ setFeatureStatus(roadmap, target, activeState, now.slice(0, 10));
2258
2354
  target.updatedAt = null;
2259
- await fs8.writeFile(this.config.filePath, serializeRoadmap(roadmap), "utf-8");
2355
+ const persisted = await applyRoadmapDiff(store, before, roadmap);
2356
+ if (!persisted.ok) return persisted;
2260
2357
  return Ok4(void 0);
2261
2358
  } catch (error) {
2262
2359
  return Err3(error instanceof Error ? error : new Error(String(error)));
2263
2360
  }
2264
2361
  }
2362
+ /**
2363
+ * Resolve the roadmap store anchored on the configured aggregate file path.
2364
+ * The shard backend is chosen when a sibling `roadmap.d/` exists next to the
2365
+ * file; otherwise the monolith file is read/written whole. Callers guard
2366
+ * `this.config.filePath` before invoking.
2367
+ */
2368
+ resolveStore() {
2369
+ return resolveRoadmapStoreForFile({ roadmapPath: this.config.filePath });
2370
+ }
2371
+ /** Load the roadmap via the store (sharded or monolith), returning a Result. */
2372
+ loadRoadmap() {
2373
+ return this.resolveStore().load();
2374
+ }
2265
2375
  findFeatureById(milestones, issueId) {
2266
2376
  for (const milestone of milestones) {
2267
2377
  for (const feature of milestone.features) {
@@ -2278,8 +2388,7 @@ var RoadmapTrackerAdapter = class {
2278
2388
  async fetchIssueStatesByIds(issueIds) {
2279
2389
  try {
2280
2390
  if (!this.config.filePath) return Err3(new Error("Missing filePath"));
2281
- const content = await fs8.readFile(this.config.filePath, "utf-8");
2282
- const roadmapResult = parseRoadmap(content);
2391
+ const roadmapResult = await this.loadRoadmap();
2283
2392
  if (!roadmapResult.ok) return roadmapResult;
2284
2393
  const issueMap = /* @__PURE__ */ new Map();
2285
2394
  for (const milestone of roadmapResult.value.milestones) {
@@ -2334,7 +2443,57 @@ var RoadmapTrackerAdapter = class {
2334
2443
  };
2335
2444
 
2336
2445
  // src/tracker/extensions/linear.ts
2337
- import { Ok as Ok5 } from "@harness-engineering/types";
2446
+ import { Ok as Ok5, Err as Err4 } from "@harness-engineering/types";
2447
+ var LINEAR_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql";
2448
+ var LinearGraphQLClient = class {
2449
+ apiKey;
2450
+ endpoint;
2451
+ fetchFn;
2452
+ constructor(opts) {
2453
+ this.apiKey = opts.apiKey;
2454
+ this.endpoint = opts.endpoint ?? LINEAR_GRAPHQL_ENDPOINT;
2455
+ this.fetchFn = opts.fetchFn ?? globalThis.fetch;
2456
+ }
2457
+ async query(query, variables) {
2458
+ let res;
2459
+ try {
2460
+ res = await this.fetchFn(this.endpoint, {
2461
+ method: "POST",
2462
+ headers: {
2463
+ Authorization: this.apiKey,
2464
+ "Content-Type": "application/json"
2465
+ },
2466
+ body: JSON.stringify({ query, variables: variables ?? {} })
2467
+ });
2468
+ } catch (err) {
2469
+ return Err4(
2470
+ new Error(
2471
+ `Linear GraphQL request failed: ${err instanceof Error ? err.message : String(err)}`
2472
+ )
2473
+ );
2474
+ }
2475
+ if (!res.ok) {
2476
+ const body = await res.text().catch(() => "");
2477
+ const detail = body ? `: ${body.slice(0, 500)}` : "";
2478
+ return Err4(new Error(`Linear GraphQL HTTP ${res.status}${detail}`));
2479
+ }
2480
+ let envelope;
2481
+ try {
2482
+ envelope = await res.json();
2483
+ } catch (err) {
2484
+ return Err4(
2485
+ new Error(
2486
+ `Linear GraphQL response was not valid JSON: ${err instanceof Error ? err.message : String(err)}`
2487
+ )
2488
+ );
2489
+ }
2490
+ if (envelope.errors && envelope.errors.length > 0) {
2491
+ const message = envelope.errors.map((e) => e.message ?? "unknown error").join("; ");
2492
+ return Err4(new Error(`Linear GraphQL error: ${message}`));
2493
+ }
2494
+ return Ok5(envelope.data ?? {});
2495
+ }
2496
+ };
2338
2497
  var LinearGraphQLStub = class {
2339
2498
  async query(query, _variables) {
2340
2499
  console.log("Linear GraphQL query (stub):", query);
@@ -2343,11 +2502,11 @@ var LinearGraphQLStub = class {
2343
2502
  };
2344
2503
 
2345
2504
  // src/workspace/manager.ts
2346
- import * as fs9 from "fs/promises";
2505
+ import * as fs8 from "fs/promises";
2347
2506
  import * as path8 from "path";
2348
2507
  import { execFile as execFile2 } from "child_process";
2349
2508
  import { promisify as promisify2 } from "util";
2350
- import { Ok as Ok6, Err as Err4 } from "@harness-engineering/types";
2509
+ import { Ok as Ok6, Err as Err5 } from "@harness-engineering/types";
2351
2510
  var WorkspaceManager = class _WorkspaceManager {
2352
2511
  config;
2353
2512
  /** Absolute path to the git repository root (resolved lazily). */
@@ -2383,7 +2542,7 @@ var WorkspaceManager = class _WorkspaceManager {
2383
2542
  async getRepoRoot() {
2384
2543
  if (this.repoRoot) return this.repoRoot;
2385
2544
  const root = path8.resolve(this.config.root);
2386
- await fs9.mkdir(root, { recursive: true });
2545
+ await fs8.mkdir(root, { recursive: true });
2387
2546
  const stdout = await this.git(["rev-parse", "--show-toplevel"], root);
2388
2547
  this.repoRoot = stdout.trim();
2389
2548
  return this.repoRoot;
@@ -2396,21 +2555,21 @@ var WorkspaceManager = class _WorkspaceManager {
2396
2555
  try {
2397
2556
  const workspacePath = path8.resolve(this.resolvePath(identifier));
2398
2557
  try {
2399
- await fs9.access(path8.join(workspacePath, ".git"));
2558
+ await fs8.access(path8.join(workspacePath, ".git"));
2400
2559
  const repoRoot2 = await this.getRepoRoot();
2401
2560
  try {
2402
2561
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
2403
2562
  } catch {
2404
- await fs9.rm(workspacePath, { recursive: true, force: true });
2563
+ await fs8.rm(workspacePath, { recursive: true, force: true });
2405
2564
  }
2406
2565
  } catch {
2407
2566
  try {
2408
- await fs9.access(workspacePath);
2567
+ await fs8.access(workspacePath);
2409
2568
  const repoRoot2 = await this.getRepoRoot();
2410
2569
  try {
2411
2570
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
2412
2571
  } catch {
2413
- await fs9.rm(workspacePath, { recursive: true, force: true });
2572
+ await fs8.rm(workspacePath, { recursive: true, force: true });
2414
2573
  }
2415
2574
  } catch {
2416
2575
  }
@@ -2422,7 +2581,7 @@ var WorkspaceManager = class _WorkspaceManager {
2422
2581
  await this.seedWorkspace(workspacePath, repoRoot);
2423
2582
  return Ok6(workspacePath);
2424
2583
  } catch (error) {
2425
- return Err4(error instanceof Error ? error : new Error(String(error)));
2584
+ return Err5(error instanceof Error ? error : new Error(String(error)));
2426
2585
  }
2427
2586
  }
2428
2587
  /**
@@ -2463,13 +2622,13 @@ var WorkspaceManager = class _WorkspaceManager {
2463
2622
  }
2464
2623
  const src = path8.join(repoRoot, rel);
2465
2624
  try {
2466
- await fs9.access(src);
2625
+ await fs8.access(src);
2467
2626
  } catch {
2468
2627
  continue;
2469
2628
  }
2470
2629
  const dest = path8.join(workspacePath, rel);
2471
2630
  try {
2472
- await fs9.cp(src, dest, { recursive: true, force: true });
2631
+ await fs8.cp(src, dest, { recursive: true, force: true });
2473
2632
  } catch {
2474
2633
  }
2475
2634
  }
@@ -2545,7 +2704,7 @@ var WorkspaceManager = class _WorkspaceManager {
2545
2704
  async exists(identifier) {
2546
2705
  try {
2547
2706
  const workspacePath = this.resolvePath(identifier);
2548
- await fs9.access(workspacePath);
2707
+ await fs8.access(workspacePath);
2549
2708
  return true;
2550
2709
  } catch {
2551
2710
  return false;
@@ -2560,7 +2719,7 @@ var WorkspaceManager = class _WorkspaceManager {
2560
2719
  try {
2561
2720
  const workspacePath = path8.resolve(this.resolvePath(identifier));
2562
2721
  try {
2563
- await fs9.access(path8.join(workspacePath, ".git"));
2722
+ await fs8.access(path8.join(workspacePath, ".git"));
2564
2723
  } catch {
2565
2724
  return null;
2566
2725
  }
@@ -2671,18 +2830,18 @@ var WorkspaceManager = class _WorkspaceManager {
2671
2830
  const repoRoot = await this.getRepoRoot();
2672
2831
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot);
2673
2832
  } catch {
2674
- await fs9.rm(workspacePath, { recursive: true, force: true });
2833
+ await fs8.rm(workspacePath, { recursive: true, force: true });
2675
2834
  }
2676
2835
  return Ok6(void 0);
2677
2836
  } catch (error) {
2678
- return Err4(error instanceof Error ? error : new Error(String(error)));
2837
+ return Err5(error instanceof Error ? error : new Error(String(error)));
2679
2838
  }
2680
2839
  }
2681
2840
  };
2682
2841
 
2683
2842
  // src/workspace/hooks.ts
2684
2843
  import { spawn } from "child_process";
2685
- import { Ok as Ok7, Err as Err5 } from "@harness-engineering/types";
2844
+ import { Ok as Ok7, Err as Err6 } from "@harness-engineering/types";
2686
2845
  var WorkspaceHooks = class {
2687
2846
  config;
2688
2847
  constructor(config) {
@@ -2709,19 +2868,19 @@ var WorkspaceHooks = class {
2709
2868
  });
2710
2869
  const timeout = setTimeout(() => {
2711
2870
  child.kill();
2712
- resolve8(Err5(new Error(`Hook ${hookName} timed out after ${this.config.timeoutMs}ms`)));
2871
+ resolve8(Err6(new Error(`Hook ${hookName} timed out after ${this.config.timeoutMs}ms`)));
2713
2872
  }, this.config.timeoutMs);
2714
2873
  child.on("exit", (code) => {
2715
2874
  clearTimeout(timeout);
2716
2875
  if (code === 0) {
2717
2876
  resolve8(Ok7(void 0));
2718
2877
  } else {
2719
- resolve8(Err5(new Error(`Hook ${hookName} failed with exit code ${code}`)));
2878
+ resolve8(Err6(new Error(`Hook ${hookName} failed with exit code ${code}`)));
2720
2879
  }
2721
2880
  });
2722
2881
  child.on("error", (error) => {
2723
2882
  clearTimeout(timeout);
2724
- resolve8(Err5(error));
2883
+ resolve8(Err6(error));
2725
2884
  });
2726
2885
  });
2727
2886
  }
@@ -3401,7 +3560,7 @@ import {
3401
3560
  } from "@harness-engineering/core";
3402
3561
 
3403
3562
  // src/tracker/adapters/github-issues-issue-tracker.ts
3404
- import { Ok as Ok9, Err as Err6 } from "@harness-engineering/types";
3563
+ import { Ok as Ok9, Err as Err7 } from "@harness-engineering/types";
3405
3564
  var GitHubIssuesIssueTrackerAdapter = class {
3406
3565
  client;
3407
3566
  config;
@@ -3416,12 +3575,12 @@ var GitHubIssuesIssueTrackerAdapter = class {
3416
3575
  const r = await this.client.fetchByStatus(
3417
3576
  stateNames
3418
3577
  );
3419
- if (!r.ok) return Err6(r.error);
3578
+ if (!r.ok) return Err7(r.error);
3420
3579
  return Ok9(r.value.map((f) => this.mapTrackedToIssue(f)));
3421
3580
  }
3422
3581
  async fetchIssueStatesByIds(issueIds) {
3423
3582
  const r = await this.client.fetchAll();
3424
- if (!r.ok) return Err6(r.error);
3583
+ if (!r.ok) return Err7(r.error);
3425
3584
  const wanted = new Set(issueIds);
3426
3585
  const out = /* @__PURE__ */ new Map();
3427
3586
  for (const f of r.value.features) {
@@ -3431,17 +3590,17 @@ var GitHubIssuesIssueTrackerAdapter = class {
3431
3590
  }
3432
3591
  async claimIssue(issueId, orchestratorId) {
3433
3592
  const r = await this.client.claim(issueId, orchestratorId);
3434
- if (!r.ok) return Err6(r.error);
3593
+ if (!r.ok) return Err7(r.error);
3435
3594
  return Ok9(void 0);
3436
3595
  }
3437
3596
  async releaseIssue(issueId) {
3438
3597
  const r = await this.client.release(issueId);
3439
- if (!r.ok) return Err6(r.error);
3598
+ if (!r.ok) return Err7(r.error);
3440
3599
  return Ok9(void 0);
3441
3600
  }
3442
3601
  async markIssueComplete(issueId) {
3443
3602
  const r = await this.client.complete(issueId);
3444
- if (!r.ok) return Err6(r.error);
3603
+ if (!r.ok) return Err7(r.error);
3445
3604
  return Ok9(void 0);
3446
3605
  }
3447
3606
  /**
@@ -4092,14 +4251,14 @@ import * as readline from "readline";
4092
4251
  import { randomUUID as randomUUID2 } from "crypto";
4093
4252
  import {
4094
4253
  Ok as Ok10,
4095
- Err as Err7
4254
+ Err as Err8
4096
4255
  } from "@harness-engineering/types";
4097
4256
  function resolveExitCode(code, command, resolve8) {
4098
4257
  if (code === 0) {
4099
4258
  resolve8(Ok10(void 0));
4100
4259
  } else {
4101
4260
  resolve8(
4102
- Err7({
4261
+ Err8({
4103
4262
  category: "agent_not_found",
4104
4263
  message: `Claude command '${command}' not found or failed`
4105
4264
  })
@@ -4107,7 +4266,7 @@ function resolveExitCode(code, command, resolve8) {
4107
4266
  }
4108
4267
  }
4109
4268
  function resolveSpawnError(command, resolve8) {
4110
- resolve8(Err7({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
4269
+ resolve8(Err8({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
4111
4270
  }
4112
4271
  var JUST_PAST_GRACE_MS = 5 * 6e4;
4113
4272
  var PRIMARY_LIMIT_RE = /You[\u2019']ve hit your limit.*resets\s+(\d{1,2}(?::\d{2})?\s*(?:am|pm))\s*\(([^)]+)\)/i;
@@ -4457,7 +4616,7 @@ var ClaudeBackend = class {
4457
4616
  import Anthropic from "@anthropic-ai/sdk";
4458
4617
  import {
4459
4618
  Ok as Ok11,
4460
- Err as Err8
4619
+ Err as Err9
4461
4620
  } from "@harness-engineering/types";
4462
4621
  import { AnthropicCacheAdapter } from "@harness-engineering/core";
4463
4622
  var AnthropicBackend = class {
@@ -4476,7 +4635,7 @@ var AnthropicBackend = class {
4476
4635
  }
4477
4636
  async startSession(params) {
4478
4637
  if (!this.config.apiKey) {
4479
- return Err8({
4638
+ return Err9({
4480
4639
  category: "agent_not_found",
4481
4640
  message: "ANTHROPIC_API_KEY is not set"
4482
4641
  });
@@ -4557,7 +4716,7 @@ var AnthropicBackend = class {
4557
4716
  }
4558
4717
  async healthCheck() {
4559
4718
  if (!this.config.apiKey) {
4560
- return Err8({
4719
+ return Err9({
4561
4720
  category: "response_error",
4562
4721
  message: "ANTHROPIC_API_KEY is not set"
4563
4722
  });
@@ -4570,7 +4729,7 @@ var AnthropicBackend = class {
4570
4729
  import OpenAI from "openai";
4571
4730
  import {
4572
4731
  Ok as Ok12,
4573
- Err as Err9
4732
+ Err as Err10
4574
4733
  } from "@harness-engineering/types";
4575
4734
  import { OpenAICacheAdapter } from "@harness-engineering/core";
4576
4735
  var OpenAIBackend = class {
@@ -4588,7 +4747,7 @@ var OpenAIBackend = class {
4588
4747
  }
4589
4748
  async startSession(params) {
4590
4749
  if (!this.config.apiKey) {
4591
- return Err9({
4750
+ return Err10({
4592
4751
  category: "agent_not_found",
4593
4752
  message: "OPENAI_API_KEY is not set"
4594
4753
  });
@@ -4683,7 +4842,7 @@ var OpenAIBackend = class {
4683
4842
  await this.client.models.list();
4684
4843
  return Ok12(void 0);
4685
4844
  } catch (err) {
4686
- return Err9({
4845
+ return Err10({
4687
4846
  category: "response_error",
4688
4847
  message: err instanceof Error ? err.message : "OpenAI health check failed"
4689
4848
  });
@@ -4695,7 +4854,7 @@ var OpenAIBackend = class {
4695
4854
  import { GoogleGenAI } from "@google/genai";
4696
4855
  import {
4697
4856
  Ok as Ok13,
4698
- Err as Err10
4857
+ Err as Err11
4699
4858
  } from "@harness-engineering/types";
4700
4859
  import { GeminiCacheAdapter } from "@harness-engineering/core";
4701
4860
  var GeminiBackend = class {
@@ -4711,7 +4870,7 @@ var GeminiBackend = class {
4711
4870
  }
4712
4871
  async startSession(params) {
4713
4872
  if (!this.config.apiKey) {
4714
- return Err10({
4873
+ return Err11({
4715
4874
  category: "agent_not_found",
4716
4875
  message: "GEMINI_API_KEY is not set"
4717
4876
  });
@@ -4811,7 +4970,7 @@ var GeminiBackend = class {
4811
4970
  }
4812
4971
  async healthCheck() {
4813
4972
  if (!this.config.apiKey) {
4814
- return Err10({
4973
+ return Err11({
4815
4974
  category: "agent_not_found",
4816
4975
  message: "GEMINI_API_KEY is not set"
4817
4976
  });
@@ -4820,7 +4979,7 @@ var GeminiBackend = class {
4820
4979
  new GoogleGenAI({ apiKey: this.config.apiKey });
4821
4980
  return Ok13(void 0);
4822
4981
  } catch (err) {
4823
- return Err10({
4982
+ return Err11({
4824
4983
  category: "response_error",
4825
4984
  message: err instanceof Error ? err.message : "Gemini health check failed"
4826
4985
  });
@@ -4832,7 +4991,7 @@ var GeminiBackend = class {
4832
4991
  import OpenAI2 from "openai";
4833
4992
  import {
4834
4993
  Ok as Ok14,
4835
- Err as Err11
4994
+ Err as Err12
4836
4995
  } from "@harness-engineering/types";
4837
4996
  var DEFAULT_TIMEOUT_MS = 9e4;
4838
4997
  var LocalBackend = class {
@@ -4859,7 +5018,7 @@ var LocalBackend = class {
4859
5018
  if (this.getModel) {
4860
5019
  const candidate = this.getModel();
4861
5020
  if (candidate === null) {
4862
- return Err11({
5021
+ return Err12({
4863
5022
  category: "agent_not_found",
4864
5023
  message: "No local model available; check dashboard for details."
4865
5024
  });
@@ -4948,7 +5107,7 @@ var LocalBackend = class {
4948
5107
  await this.client.models.list();
4949
5108
  return Ok14(void 0);
4950
5109
  } catch (err) {
4951
- return Err11({
5110
+ return Err12({
4952
5111
  category: "response_error",
4953
5112
  message: err instanceof Error ? err.message : "Local backend health check failed"
4954
5113
  });
@@ -4960,7 +5119,7 @@ var LocalBackend = class {
4960
5119
  import { randomUUID as randomUUID3 } from "crypto";
4961
5120
  import {
4962
5121
  Ok as Ok15,
4963
- Err as Err12
5122
+ Err as Err13
4964
5123
  } from "@harness-engineering/types";
4965
5124
  var SILENT_EVENTS = /* @__PURE__ */ new Set([
4966
5125
  "turn_end",
@@ -5078,7 +5237,7 @@ var PiBackend = class {
5078
5237
  if (this.config.getModel) {
5079
5238
  const candidate = this.config.getModel();
5080
5239
  if (candidate === null) {
5081
- return Err12({
5240
+ return Err13({
5082
5241
  category: "agent_not_found",
5083
5242
  message: "No local model available; check dashboard for details."
5084
5243
  });
@@ -5108,7 +5267,7 @@ var PiBackend = class {
5108
5267
  };
5109
5268
  return Ok15(session);
5110
5269
  } catch (err) {
5111
- return Err12({
5270
+ return Err13({
5112
5271
  category: "response_error",
5113
5272
  message: `Failed to create pi session: ${err instanceof Error ? err.message : String(err)}`
5114
5273
  });
@@ -5245,7 +5404,7 @@ var PiBackend = class {
5245
5404
  await import("@earendil-works/pi-coding-agent");
5246
5405
  return Ok15(void 0);
5247
5406
  } catch (err) {
5248
- return Err12({
5407
+ return Err13({
5249
5408
  category: "agent_not_found",
5250
5409
  message: `Pi SDK not available: ${err instanceof Error ? err.message : String(err)}`
5251
5410
  });
@@ -5257,7 +5416,7 @@ var PiBackend = class {
5257
5416
  import { spawn as spawn3 } from "child_process";
5258
5417
  import {
5259
5418
  Ok as Ok16,
5260
- Err as Err13
5419
+ Err as Err14
5261
5420
  } from "@harness-engineering/types";
5262
5421
  var DEFAULT_TIMEOUT_MS2 = 9e4;
5263
5422
  var FORBIDDEN_HOST_CHARS = /[;&|`$()\n\r<>]/;
@@ -5404,7 +5563,7 @@ var SshBackend = class {
5404
5563
  });
5405
5564
  } catch (err) {
5406
5565
  resolve8(
5407
- Err13({
5566
+ Err14({
5408
5567
  category: "agent_not_found",
5409
5568
  message: err instanceof Error ? err.message : "failed to spawn ssh"
5410
5569
  })
@@ -5427,7 +5586,7 @@ var SshBackend = class {
5427
5586
  resolve8(Ok16(void 0));
5428
5587
  } else {
5429
5588
  resolve8(
5430
- Err13({
5589
+ Err14({
5431
5590
  category: "agent_not_found",
5432
5591
  message: `ssh health check failed (exit=${code ?? "null"}): ${stderr.slice(0, 500)}`
5433
5592
  })
@@ -5436,7 +5595,7 @@ var SshBackend = class {
5436
5595
  });
5437
5596
  child.on("error", (err) => {
5438
5597
  clearTimeout(timer);
5439
- resolve8(Err13({ category: "agent_not_found", message: err.message }));
5598
+ resolve8(Err14({ category: "agent_not_found", message: err.message }));
5440
5599
  });
5441
5600
  });
5442
5601
  }
@@ -5498,7 +5657,7 @@ function waitForExit(child) {
5498
5657
  import { spawn as spawn4 } from "child_process";
5499
5658
  import {
5500
5659
  Ok as Ok17,
5501
- Err as Err14
5660
+ Err as Err15
5502
5661
  } from "@harness-engineering/types";
5503
5662
  var ServerlessBackend = class {
5504
5663
  handles = /* @__PURE__ */ new Map();
@@ -5597,7 +5756,7 @@ var OciServerlessBackend = class extends ServerlessBackend {
5597
5756
  if (!result.ok) return result;
5598
5757
  const id = result.value.trim().split(/\s+/)[0] ?? "";
5599
5758
  if (!id) {
5600
- return Err14({
5759
+ return Err15({
5601
5760
  category: "response_error",
5602
5761
  message: "OciServerlessBackend: empty container id from runtime"
5603
5762
  });
@@ -5657,7 +5816,7 @@ var OciServerlessBackend = class extends ServerlessBackend {
5657
5816
  }
5658
5817
  async teardown(handle) {
5659
5818
  if (handle.adapter !== this.name) {
5660
- return Err14({
5819
+ return Err15({
5661
5820
  category: "response_error",
5662
5821
  message: `handle adapter mismatch: got '${handle.adapter}', expected '${this.name}'`
5663
5822
  });
@@ -5688,7 +5847,7 @@ var OciServerlessBackend = class extends ServerlessBackend {
5688
5847
  });
5689
5848
  } catch (err) {
5690
5849
  resolve8(
5691
- Err14({
5850
+ Err15({
5692
5851
  category: "agent_not_found",
5693
5852
  message: err instanceof Error ? err.message : "failed to spawn runtime"
5694
5853
  })
@@ -5715,7 +5874,7 @@ var OciServerlessBackend = class extends ServerlessBackend {
5715
5874
  resolve8(Ok17(stdout));
5716
5875
  } else {
5717
5876
  resolve8(
5718
- Err14({
5877
+ Err15({
5719
5878
  category: "response_error",
5720
5879
  message: `runtime '${binary} ${args.join(" ")}' exited ${code ?? "null"}: ${stderr.slice(0, 500)}`
5721
5880
  })
@@ -5724,7 +5883,7 @@ var OciServerlessBackend = class extends ServerlessBackend {
5724
5883
  });
5725
5884
  child.on("error", (err) => {
5726
5885
  clearTimeout(timer);
5727
- resolve8(Err14({ category: "agent_not_found", message: err.message }));
5886
+ resolve8(Err15({ category: "agent_not_found", message: err.message }));
5728
5887
  });
5729
5888
  });
5730
5889
  }
@@ -5881,7 +6040,7 @@ function createBackend(def, options = {}) {
5881
6040
 
5882
6041
  // src/agent/backends/container.ts
5883
6042
  import {
5884
- Err as Err15
6043
+ Err as Err16
5885
6044
  } from "@harness-engineering/types";
5886
6045
  function toAgentError(message, details) {
5887
6046
  return { category: "response_error", message, details };
@@ -5913,7 +6072,7 @@ var ContainerBackend = class {
5913
6072
  }
5914
6073
  const result = await this.secretBackend.resolveSecrets(this.secretKeys);
5915
6074
  if (!result.ok) {
5916
- return Err15(toAgentError(`Secret resolution failed: ${result.error.message}`, result.error));
6075
+ return Err16(toAgentError(`Secret resolution failed: ${result.error.message}`, result.error));
5917
6076
  }
5918
6077
  return { ok: true, value: result.value };
5919
6078
  }
@@ -5938,7 +6097,7 @@ var ContainerBackend = class {
5938
6097
  const createOpts = this.buildCreateOpts(params, envResult.value);
5939
6098
  const containerResult = await this.runtime.createContainer(createOpts);
5940
6099
  if (!containerResult.ok) {
5941
- return Err15(
6100
+ return Err16(
5942
6101
  toAgentError(
5943
6102
  `Container creation failed: ${containerResult.error.message}`,
5944
6103
  containerResult.error
@@ -5963,7 +6122,7 @@ var ContainerBackend = class {
5963
6122
  this.containerHandles.delete(session.sessionId);
5964
6123
  const removeResult = await this.runtime.removeContainer(handle);
5965
6124
  if (!removeResult.ok) {
5966
- return Err15(
6125
+ return Err16(
5967
6126
  toAgentError(
5968
6127
  `Container removal failed: ${removeResult.error.message}`,
5969
6128
  removeResult.error
@@ -5976,7 +6135,7 @@ var ContainerBackend = class {
5976
6135
  async healthCheck() {
5977
6136
  const runtimeResult = await this.runtime.healthCheck();
5978
6137
  if (!runtimeResult.ok) {
5979
- return Err15({
6138
+ return Err16({
5980
6139
  category: "agent_not_found",
5981
6140
  message: `Container runtime unhealthy: ${runtimeResult.error.message}`,
5982
6141
  details: runtimeResult.error
@@ -5988,7 +6147,7 @@ var ContainerBackend = class {
5988
6147
 
5989
6148
  // src/agent/runtime/docker.ts
5990
6149
  import { execFile as execFile3, spawn as spawn5 } from "child_process";
5991
- import { Ok as Ok18, Err as Err16 } from "@harness-engineering/types";
6150
+ import { Ok as Ok18, Err as Err17 } from "@harness-engineering/types";
5992
6151
  function dockerExec(args) {
5993
6152
  return new Promise((resolve8, reject) => {
5994
6153
  execFile3("docker", args, (error, stdout) => {
@@ -6023,7 +6182,7 @@ var DockerRuntime = class {
6023
6182
  const containerId = await dockerExec(args);
6024
6183
  return Ok18({ containerId, runtime: this.name });
6025
6184
  } catch (error) {
6026
- return Err16({
6185
+ return Err17({
6027
6186
  category: "container_create_failed",
6028
6187
  message: `Failed to create container: ${error instanceof Error ? error.message : String(error)}`,
6029
6188
  details: error
@@ -6069,7 +6228,7 @@ var DockerRuntime = class {
6069
6228
  await dockerExec(["rm", "-f", handle.containerId]);
6070
6229
  return Ok18(void 0);
6071
6230
  } catch (error) {
6072
- return Err16({
6231
+ return Err17({
6073
6232
  category: "container_remove_failed",
6074
6233
  message: `Failed to remove container: ${error instanceof Error ? error.message : String(error)}`,
6075
6234
  details: error
@@ -6081,7 +6240,7 @@ var DockerRuntime = class {
6081
6240
  await dockerExec(["info", "--format", "{{.ServerVersion}}"]);
6082
6241
  return Ok18(void 0);
6083
6242
  } catch (error) {
6084
- return Err16({
6243
+ return Err17({
6085
6244
  category: "runtime_not_found",
6086
6245
  message: `Docker is not available: ${error instanceof Error ? error.message : String(error)}`,
6087
6246
  details: error
@@ -6091,7 +6250,7 @@ var DockerRuntime = class {
6091
6250
  };
6092
6251
 
6093
6252
  // src/agent/secrets/env.ts
6094
- import { Ok as Ok19, Err as Err17 } from "@harness-engineering/types";
6253
+ import { Ok as Ok19, Err as Err18 } from "@harness-engineering/types";
6095
6254
  var EnvSecretBackend = class {
6096
6255
  name = "env";
6097
6256
  async resolveSecrets(keys) {
@@ -6099,7 +6258,7 @@ var EnvSecretBackend = class {
6099
6258
  for (const key of keys) {
6100
6259
  const value = process.env[key];
6101
6260
  if (value === void 0) {
6102
- return Err17({
6261
+ return Err18({
6103
6262
  category: "secret_not_found",
6104
6263
  message: `Environment variable '${key}' is not set`,
6105
6264
  key
@@ -6116,7 +6275,7 @@ var EnvSecretBackend = class {
6116
6275
 
6117
6276
  // src/agent/secrets/onepassword.ts
6118
6277
  import { execFile as execFile4 } from "child_process";
6119
- import { Ok as Ok20, Err as Err18 } from "@harness-engineering/types";
6278
+ import { Ok as Ok20, Err as Err19 } from "@harness-engineering/types";
6120
6279
  function opExec(args) {
6121
6280
  return new Promise((resolve8, reject) => {
6122
6281
  execFile4("op", args, (error, stdout) => {
@@ -6141,7 +6300,7 @@ var OnePasswordSecretBackend = class {
6141
6300
  const value = await opExec(["read", `op://${this.vault}/${key}/password`]);
6142
6301
  secrets[key] = value;
6143
6302
  } catch (error) {
6144
- return Err18({
6303
+ return Err19({
6145
6304
  category: "access_denied",
6146
6305
  message: `Failed to read secret '${key}' from 1Password: ${error instanceof Error ? error.message : String(error)}`,
6147
6306
  key
@@ -6155,7 +6314,7 @@ var OnePasswordSecretBackend = class {
6155
6314
  await opExec(["--version"]);
6156
6315
  return Ok20(void 0);
6157
6316
  } catch (error) {
6158
- return Err18({
6317
+ return Err19({
6159
6318
  category: "provider_unavailable",
6160
6319
  message: `1Password CLI is not available: ${error instanceof Error ? error.message : String(error)}`
6161
6320
  });
@@ -6165,7 +6324,7 @@ var OnePasswordSecretBackend = class {
6165
6324
 
6166
6325
  // src/agent/secrets/vault.ts
6167
6326
  import { execFile as execFile5 } from "child_process";
6168
- import { Ok as Ok21, Err as Err19 } from "@harness-engineering/types";
6327
+ import { Ok as Ok21, Err as Err20 } from "@harness-engineering/types";
6169
6328
  function vaultExec(args, env) {
6170
6329
  return new Promise((resolve8, reject) => {
6171
6330
  execFile5("vault", args, { env: { ...process.env, ...env } }, (error, stdout) => {
@@ -6196,11 +6355,11 @@ var VaultSecretBackend = class {
6196
6355
  } catch (error) {
6197
6356
  const msg = error instanceof Error ? error.message : String(error);
6198
6357
  const category = error instanceof SyntaxError ? "access_denied" : "access_denied";
6199
- return Err19({ category, message: `Failed to read from Vault: ${msg}` });
6358
+ return Err20({ category, message: `Failed to read from Vault: ${msg}` });
6200
6359
  }
6201
6360
  const missing = keys.find((k) => !(k in data));
6202
6361
  if (missing) {
6203
- return Err19({
6362
+ return Err20({
6204
6363
  category: "secret_not_found",
6205
6364
  message: `Secret key '${missing}' not found in Vault path '${this.path}'`,
6206
6365
  key: missing
@@ -6215,7 +6374,7 @@ var VaultSecretBackend = class {
6215
6374
  await vaultExec(["version"]);
6216
6375
  return Ok21(void 0);
6217
6376
  } catch (error) {
6218
- return Err19({
6377
+ return Err20({
6219
6378
  category: "provider_unavailable",
6220
6379
  message: `Vault CLI is not available: ${error instanceof Error ? error.message : String(error)}`
6221
6380
  });
@@ -6344,6 +6503,96 @@ var OrchestratorBackendFactory = class {
6344
6503
  }
6345
6504
  };
6346
6505
 
6506
+ // src/agent/backend-resolver.ts
6507
+ function makeBackendResolver(backends) {
6508
+ return (backendName) => {
6509
+ const def = backends?.[backendName];
6510
+ return def ? createBackend(def) : null;
6511
+ };
6512
+ }
6513
+
6514
+ // src/maintenance/agent-dispatcher.ts
6515
+ function syntheticIssue(branch, skill) {
6516
+ return {
6517
+ id: `maintenance:${branch}`,
6518
+ identifier: branch,
6519
+ title: `Maintenance: ${skill}`,
6520
+ description: null,
6521
+ priority: null,
6522
+ state: "in_progress",
6523
+ branchName: branch,
6524
+ url: null,
6525
+ labels: ["maintenance"],
6526
+ blockedBy: [],
6527
+ spec: null,
6528
+ plans: [],
6529
+ createdAt: null,
6530
+ updatedAt: null,
6531
+ externalId: null
6532
+ };
6533
+ }
6534
+ function buildPrompt(skill, promptContext) {
6535
+ 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.`;
6536
+ return promptContext ? `${promptContext}
6537
+
6538
+ ${instruction}` : instruction;
6539
+ }
6540
+ function headOf(git2, cwd) {
6541
+ try {
6542
+ return git2(["rev-parse", "HEAD"], cwd) || null;
6543
+ } catch {
6544
+ return null;
6545
+ }
6546
+ }
6547
+ function createAgentDispatcher(deps) {
6548
+ const maxTurns = deps.maxTurns ?? 10;
6549
+ const dispatch = async (skill, branch, backendName, cwd, options) => {
6550
+ const backend = deps.resolveBackend(backendName);
6551
+ if (!backend) {
6552
+ deps.logger.warn(`Maintenance agent dispatch skipped: unknown backend "${backendName}"`, {
6553
+ skill,
6554
+ branch
6555
+ });
6556
+ return { producedCommits: false, fixed: 0 };
6557
+ }
6558
+ const before = headOf(deps.git, cwd);
6559
+ const runner = new AgentRunner(backend, { maxTurns });
6560
+ const session = runner.runSession(
6561
+ syntheticIssue(branch, skill),
6562
+ cwd,
6563
+ buildPrompt(skill, options?.promptContext)
6564
+ );
6565
+ let step = await session.next();
6566
+ while (!step.done) step = await session.next();
6567
+ const after = headOf(deps.git, cwd);
6568
+ let fixed = 0;
6569
+ if (after && after !== before) {
6570
+ if (before) {
6571
+ try {
6572
+ fixed = parseInt(deps.git(["rev-list", "--count", `${before}..${after}`], cwd), 10) || 0;
6573
+ } catch {
6574
+ fixed = 1;
6575
+ }
6576
+ } else {
6577
+ fixed = 1;
6578
+ }
6579
+ }
6580
+ const producedCommits = fixed > 0;
6581
+ deps.logger.info("Maintenance agent dispatch complete", {
6582
+ skill,
6583
+ branch,
6584
+ backendName,
6585
+ producedCommits,
6586
+ fixed
6587
+ });
6588
+ return { producedCommits, fixed };
6589
+ };
6590
+ return { dispatch };
6591
+ }
6592
+
6593
+ // src/orchestrator.ts
6594
+ import { execFileSync } from "child_process";
6595
+
6347
6596
  // src/agent/intelligence-factory.ts
6348
6597
  import {
6349
6598
  IntelligencePipeline,
@@ -6854,7 +7103,7 @@ function handleV1InteractionsResolveRoute(req, res, queue) {
6854
7103
 
6855
7104
  // src/server/routes/plans.ts
6856
7105
  import { z as z5 } from "zod";
6857
- import * as fs10 from "fs/promises";
7106
+ import * as fs9 from "fs/promises";
6858
7107
  import * as path11 from "path";
6859
7108
  var PlanWriteSchema = z5.object({
6860
7109
  filename: z5.string().min(1),
@@ -6883,9 +7132,9 @@ function handlePlansRoute(req, res, plansDir) {
6883
7132
  );
6884
7133
  return;
6885
7134
  }
6886
- await fs10.mkdir(plansDir, { recursive: true });
7135
+ await fs9.mkdir(plansDir, { recursive: true });
6887
7136
  const filePath = path11.join(plansDir, basename3);
6888
- await fs10.writeFile(filePath, parsed.content, "utf-8");
7137
+ await fs9.writeFile(filePath, parsed.content, "utf-8");
6889
7138
  res.writeHead(201, { "Content-Type": "application/json" });
6890
7139
  res.end(JSON.stringify({ ok: true, filename: basename3 }));
6891
7140
  } catch {
@@ -7260,11 +7509,10 @@ function handleAnalyzeRoute(req, res, pipeline) {
7260
7509
  }
7261
7510
 
7262
7511
  // src/server/routes/roadmap-actions.ts
7263
- import * as fs11 from "fs/promises";
7264
7512
  import * as path12 from "path";
7265
7513
  import {
7266
- parseRoadmap as parseRoadmap2,
7267
- serializeRoadmap as serializeRoadmap2,
7514
+ resolveRoadmapStoreForFile as resolveRoadmapStoreForFile2,
7515
+ applyRoadmapDiff as applyRoadmapDiff2,
7268
7516
  loadProjectRoadmapMode,
7269
7517
  loadTrackerClientConfigFromProject,
7270
7518
  createTrackerClient,
@@ -7350,13 +7598,14 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
7350
7598
  sendJSON2(res, 400, { error: "Title must not contain newlines or markdown headings" });
7351
7599
  return;
7352
7600
  }
7353
- const content = await fs11.readFile(roadmapPath, "utf-8");
7354
- const roadmapResult = parseRoadmap2(content);
7355
- if (!roadmapResult.ok) {
7601
+ const store = resolveRoadmapStoreForFile2({ roadmapPath });
7602
+ const loaded = await store.load();
7603
+ if (!loaded.ok) {
7356
7604
  sendJSON2(res, 500, { error: "Failed to parse roadmap file" });
7357
7605
  return;
7358
7606
  }
7359
- const roadmap = roadmapResult.value;
7607
+ const roadmap = loaded.value;
7608
+ const before = structuredClone(roadmap);
7360
7609
  let backlog = roadmap.milestones.find((m) => m.isBacklog);
7361
7610
  if (!backlog) {
7362
7611
  backlog = { name: "Backlog", isBacklog: true, features: [] };
@@ -7379,10 +7628,11 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
7379
7628
  updatedAt: null
7380
7629
  });
7381
7630
  roadmap.frontmatter.lastManualEdit = (/* @__PURE__ */ new Date()).toISOString();
7382
- const tmpPath = roadmapPath + ".tmp";
7383
- const serialized = serializeRoadmap2(roadmap);
7384
- await fs11.writeFile(tmpPath, serialized, "utf-8");
7385
- await fs11.rename(tmpPath, roadmapPath);
7631
+ const persisted = await applyRoadmapDiff2(store, before, roadmap);
7632
+ if (!persisted.ok) {
7633
+ sendJSON2(res, 500, { error: persisted.error.message });
7634
+ return;
7635
+ }
7386
7636
  sendJSON2(res, 201, { ok: true, featureName: parsed.title });
7387
7637
  } catch (err) {
7388
7638
  const msg = err instanceof Error ? err.message : "Failed to append to roadmap";
@@ -7648,7 +7898,6 @@ function handleV1JobsMaintenanceRoute(req, res, deps) {
7648
7898
  }
7649
7899
 
7650
7900
  // src/server/routes/v1/events-sse.ts
7651
- import { randomBytes as randomBytes2 } from "crypto";
7652
7901
  var SSE_TOPICS = [
7653
7902
  "state_change",
7654
7903
  "agent_event",
@@ -7664,8 +7913,55 @@ var SSE_TOPICS = [
7664
7913
  "webhook.subscription.deleted"
7665
7914
  ];
7666
7915
  var HEARTBEAT_MS = 15e3;
7667
- function newEventId() {
7668
- return `evt_${randomBytes2(8).toString("hex")}`;
7916
+ var DEFAULT_REPLAY_BUFFER = 1024;
7917
+ var SseEventLog = class {
7918
+ seq = 0;
7919
+ buffer = [];
7920
+ subscribers = /* @__PURE__ */ new Set();
7921
+ constructor(bus, cap = DEFAULT_REPLAY_BUFFER) {
7922
+ this.cap = cap;
7923
+ for (const topic of SSE_TOPICS) {
7924
+ bus.on(topic, (data) => this.record(topic, data));
7925
+ }
7926
+ }
7927
+ cap;
7928
+ record(topic, data) {
7929
+ const event = { id: ++this.seq, topic, data };
7930
+ this.buffer.push(event);
7931
+ if (this.buffer.length > this.cap) this.buffer.shift();
7932
+ for (const fn of this.subscribers) fn(event);
7933
+ }
7934
+ /** Highest assigned id (0 when nothing has been recorded yet). */
7935
+ currentSeq() {
7936
+ return this.seq;
7937
+ }
7938
+ /** Buffered events strictly newer than `lastId`, oldest-first. */
7939
+ replayFrom(lastId) {
7940
+ return this.buffer.filter((e) => e.id > lastId);
7941
+ }
7942
+ /** Register a live listener; returns an unsubscribe function. */
7943
+ subscribe(fn) {
7944
+ this.subscribers.add(fn);
7945
+ return () => {
7946
+ this.subscribers.delete(fn);
7947
+ };
7948
+ }
7949
+ };
7950
+ var logsByBus = /* @__PURE__ */ new WeakMap();
7951
+ function getSseEventLog(bus) {
7952
+ let log = logsByBus.get(bus);
7953
+ if (!log) {
7954
+ log = new SseEventLog(bus);
7955
+ logsByBus.set(bus, log);
7956
+ }
7957
+ return log;
7958
+ }
7959
+ function parseLastEventId(req) {
7960
+ const raw = req.headers["last-event-id"];
7961
+ const value = Array.isArray(raw) ? raw[0] : raw;
7962
+ if (value === void 0) return null;
7963
+ const n = Number(value);
7964
+ return Number.isInteger(n) && n >= 0 ? n : null;
7669
7965
  }
7670
7966
  function handleV1EventsSseRoute(req, res, bus) {
7671
7967
  if (req.method !== "GET" || req.url !== "/api/v1/events") return false;
@@ -7677,22 +7973,28 @@ function handleV1EventsSseRoute(req, res, bus) {
7677
7973
  res.write(`: harness gateway SSE \u2014 connected at ${(/* @__PURE__ */ new Date()).toISOString()}
7678
7974
 
7679
7975
  `);
7680
- const listeners = [];
7681
- for (const topic of SSE_TOPICS) {
7682
- const fn = (data) => {
7683
- try {
7684
- const frame = `event: ${topic}
7685
- data: ${JSON.stringify(data)}
7686
- id: ${newEventId()}
7976
+ const log = getSseEventLog(bus);
7977
+ const send = (e) => {
7978
+ try {
7979
+ res.write(`event: ${e.topic}
7980
+ data: ${JSON.stringify(e.data)}
7981
+ id: ${e.id}
7687
7982
 
7688
- `;
7689
- res.write(frame);
7690
- } catch {
7691
- }
7692
- };
7693
- bus.on(topic, fn);
7694
- listeners.push({ topic, fn });
7983
+ `);
7984
+ } catch {
7985
+ }
7986
+ };
7987
+ const lastId = parseLastEventId(req);
7988
+ let replayedThrough = lastId ?? log.currentSeq();
7989
+ if (lastId !== null) {
7990
+ for (const e of log.replayFrom(lastId)) {
7991
+ send(e);
7992
+ replayedThrough = e.id;
7993
+ }
7695
7994
  }
7995
+ const unsubscribe = log.subscribe((e) => {
7996
+ if (e.id > replayedThrough) send(e);
7997
+ });
7696
7998
  const heartbeat = setInterval(() => {
7697
7999
  try {
7698
8000
  res.write(": heartbeat\n\n");
@@ -7702,7 +8004,7 @@ id: ${newEventId()}
7702
8004
  heartbeat.unref();
7703
8005
  const cleanup = () => {
7704
8006
  clearInterval(heartbeat);
7705
- for (const { topic, fn } of listeners) bus.removeListener(topic, fn);
8007
+ unsubscribe();
7706
8008
  };
7707
8009
  res.on("close", cleanup);
7708
8010
  res.on("finish", cleanup);
@@ -8022,7 +8324,7 @@ async function runGate(projectPath, proposalId) {
8022
8324
  }
8023
8325
 
8024
8326
  // src/proposals/promote.ts
8025
- import * as fs12 from "fs";
8327
+ import * as fs10 from "fs";
8026
8328
  import * as path13 from "path";
8027
8329
  import { parse as parseYaml3, stringify as stringifyYaml } from "yaml";
8028
8330
  import {
@@ -8048,7 +8350,7 @@ function skillDir(projectPath, name) {
8048
8350
  }
8049
8351
  function readIfExists(p) {
8050
8352
  try {
8051
- return fs12.readFileSync(p, "utf-8");
8353
+ return fs10.readFileSync(p, "utf-8");
8052
8354
  } catch {
8053
8355
  return null;
8054
8356
  }
@@ -8094,15 +8396,15 @@ function assertGateReady(proposal) {
8094
8396
  }
8095
8397
  async function promoteNewSkill(projectPath, proposal) {
8096
8398
  const target = skillDir(projectPath, proposal.content.name);
8097
- if (fs12.existsSync(target)) {
8399
+ if (fs10.existsSync(target)) {
8098
8400
  throw new PromotionError(
8099
8401
  `a catalog skill already exists at ${target}; use a refinement proposal to update it`
8100
8402
  );
8101
8403
  }
8102
- fs12.mkdirSync(target, { recursive: true });
8404
+ fs10.mkdirSync(target, { recursive: true });
8103
8405
  const yamlOut = injectProvenanceIntoYaml(proposal.content.skillYaml ?? "", proposal.id);
8104
- fs12.writeFileSync(path13.join(target, "skill.yaml"), yamlOut);
8105
- fs12.writeFileSync(path13.join(target, "SKILL.md"), proposal.content.skillMd ?? "");
8406
+ fs10.writeFileSync(path13.join(target, "skill.yaml"), yamlOut);
8407
+ fs10.writeFileSync(path13.join(target, "SKILL.md"), proposal.content.skillMd ?? "");
8106
8408
  return { skillPath: target };
8107
8409
  }
8108
8410
  async function promoteRefinement(projectPath, proposal) {
@@ -8110,7 +8412,7 @@ async function promoteRefinement(projectPath, proposal) {
8110
8412
  throw new PromotionError("refinement proposal is missing targetSkill");
8111
8413
  }
8112
8414
  const target = skillDir(projectPath, proposal.targetSkill);
8113
- if (!fs12.existsSync(target)) {
8415
+ if (!fs10.existsSync(target)) {
8114
8416
  throw new PromotionError(
8115
8417
  `target skill ${proposal.targetSkill} does not exist at ${target}; cannot refine`
8116
8418
  );
@@ -8123,7 +8425,7 @@ async function promoteRefinement(projectPath, proposal) {
8123
8425
  "no metadata changes detected; check that the reviewer applied the proposed diff before approving"
8124
8426
  );
8125
8427
  }
8126
- fs12.writeFileSync(yamlPath, after);
8428
+ fs10.writeFileSync(yamlPath, after);
8127
8429
  return { skillPath: target };
8128
8430
  }
8129
8431
  async function promote(projectPath, proposalId, decidedBy) {
@@ -8561,7 +8863,7 @@ function handleV1RoutingRoute(req, res, deps) {
8561
8863
  }
8562
8864
 
8563
8865
  // src/server/routes/sessions.ts
8564
- import * as fs13 from "fs/promises";
8866
+ import * as fs11 from "fs/promises";
8565
8867
  import * as path14 from "path";
8566
8868
  import { z as z15 } from "zod";
8567
8869
  var SessionCreateSchema = z15.object({
@@ -8582,12 +8884,12 @@ function extractSessionId(url) {
8582
8884
  }
8583
8885
  async function handleList2(res, sessionsDir) {
8584
8886
  try {
8585
- const entries = await fs13.readdir(sessionsDir, { withFileTypes: true });
8887
+ const entries = await fs11.readdir(sessionsDir, { withFileTypes: true });
8586
8888
  const sessions = [];
8587
8889
  for (const entry of entries) {
8588
8890
  if (!entry.isDirectory()) continue;
8589
8891
  try {
8590
- const content = await fs13.readFile(
8892
+ const content = await fs11.readFile(
8591
8893
  path14.join(sessionsDir, entry.name, "session.json"),
8592
8894
  "utf-8"
8593
8895
  );
@@ -8613,7 +8915,7 @@ async function handleGet2(res, id, sessionsDir) {
8613
8915
  return;
8614
8916
  }
8615
8917
  try {
8616
- const content = await fs13.readFile(path14.join(sessionsDir, id, "session.json"), "utf-8");
8918
+ const content = await fs11.readFile(path14.join(sessionsDir, id, "session.json"), "utf-8");
8617
8919
  jsonResponse(res, 200, JSON.parse(content));
8618
8920
  } catch (err) {
8619
8921
  if (err.code === "ENOENT") {
@@ -8637,8 +8939,8 @@ async function handleCreate(req, res, sessionsDir) {
8637
8939
  return;
8638
8940
  }
8639
8941
  const sessionDir = path14.join(sessionsDir, session.sessionId);
8640
- await fs13.mkdir(sessionDir, { recursive: true });
8641
- await fs13.writeFile(path14.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
8942
+ await fs11.mkdir(sessionDir, { recursive: true });
8943
+ await fs11.writeFile(path14.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
8642
8944
  jsonResponse(res, 200, { ok: true });
8643
8945
  } catch {
8644
8946
  jsonResponse(res, 500, { error: "Failed to save session" });
@@ -8654,8 +8956,8 @@ async function handleUpdate(req, res, url, sessionsDir) {
8654
8956
  const body = await readBody(req);
8655
8957
  const updates = z15.record(z15.unknown()).parse(JSON.parse(body));
8656
8958
  const sessionFilePath = path14.join(sessionsDir, id, "session.json");
8657
- const current = JSON.parse(await fs13.readFile(sessionFilePath, "utf-8"));
8658
- await fs13.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
8959
+ const current = JSON.parse(await fs11.readFile(sessionFilePath, "utf-8"));
8960
+ await fs11.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
8659
8961
  jsonResponse(res, 200, { ok: true });
8660
8962
  } catch {
8661
8963
  jsonResponse(res, 500, { error: "Failed to update session" });
@@ -8668,7 +8970,7 @@ async function handleDelete(res, url, sessionsDir) {
8668
8970
  jsonResponse(res, 400, { error: "Missing or invalid sessionId" });
8669
8971
  return;
8670
8972
  }
8671
- await fs13.rm(path14.join(sessionsDir, id), { recursive: true, force: true });
8973
+ await fs11.rm(path14.join(sessionsDir, id), { recursive: true, force: true });
8672
8974
  jsonResponse(res, 200, { ok: true });
8673
8975
  } catch {
8674
8976
  jsonResponse(res, 500, { error: "Failed to delete session" });
@@ -8916,7 +9218,7 @@ function handleLocalModelsRoute(req, res, getStatuses) {
8916
9218
  }
8917
9219
 
8918
9220
  // src/server/static.ts
8919
- import * as fs14 from "fs";
9221
+ import * as fs12 from "fs";
8920
9222
  import * as path15 from "path";
8921
9223
  var MIME_TYPES = {
8922
9224
  ".html": "text/html; charset=utf-8",
@@ -8946,11 +9248,11 @@ function handleStaticFile(req, res, dashboardDir) {
8946
9248
  if (!resolved.startsWith(path15.resolve(dashboardDir))) {
8947
9249
  return serveFile(path15.join(dashboardDir, "index.html"), res);
8948
9250
  }
8949
- if (fs14.existsSync(resolved) && fs14.statSync(resolved).isFile()) {
9251
+ if (fs12.existsSync(resolved) && fs12.statSync(resolved).isFile()) {
8950
9252
  return serveFile(resolved, res);
8951
9253
  }
8952
9254
  const indexPath = path15.join(dashboardDir, "index.html");
8953
- if (fs14.existsSync(indexPath)) {
9255
+ if (fs12.existsSync(indexPath)) {
8954
9256
  return serveFile(indexPath, res);
8955
9257
  }
8956
9258
  return false;
@@ -8959,7 +9261,7 @@ function serveFile(filePath, res) {
8959
9261
  const ext = path15.extname(filePath).toLowerCase();
8960
9262
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
8961
9263
  try {
8962
- const content = fs14.readFileSync(filePath);
9264
+ const content = fs12.readFileSync(filePath);
8963
9265
  res.writeHead(200, { "Content-Type": contentType });
8964
9266
  res.end(content);
8965
9267
  return true;
@@ -8969,7 +9271,7 @@ function serveFile(filePath, res) {
8969
9271
  }
8970
9272
 
8971
9273
  // src/server/plan-watcher.ts
8972
- import * as fs15 from "fs";
9274
+ import * as fs13 from "fs";
8973
9275
  import * as path16 from "path";
8974
9276
  var PlanWatcher = class {
8975
9277
  plansDir;
@@ -8984,11 +9286,11 @@ var PlanWatcher = class {
8984
9286
  * Creates the directory if it does not exist.
8985
9287
  */
8986
9288
  start() {
8987
- fs15.mkdirSync(this.plansDir, { recursive: true });
8988
- this.watcher = fs15.watch(this.plansDir, (eventType, filename) => {
9289
+ fs13.mkdirSync(this.plansDir, { recursive: true });
9290
+ this.watcher = fs13.watch(this.plansDir, (eventType, filename) => {
8989
9291
  if (eventType === "rename" && filename && filename.endsWith(".md")) {
8990
9292
  const filePath = path16.join(this.plansDir, filename);
8991
- if (fs15.existsSync(filePath)) {
9293
+ if (fs13.existsSync(filePath)) {
8992
9294
  void this.handleNewPlan(filename);
8993
9295
  }
8994
9296
  }
@@ -9019,8 +9321,8 @@ var PlanWatcher = class {
9019
9321
  };
9020
9322
 
9021
9323
  // src/auth/tokens.ts
9022
- import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
9023
- import { readFile as readFile8, writeFile as writeFile8, mkdir as mkdir7, rename as rename2 } from "fs/promises";
9324
+ import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
9325
+ import { readFile as readFile6, writeFile as writeFile6, mkdir as mkdir7, rename } from "fs/promises";
9024
9326
  import { dirname as dirname5 } from "path";
9025
9327
  import bcrypt from "bcryptjs";
9026
9328
  import {
@@ -9030,10 +9332,10 @@ import {
9030
9332
  var BCRYPT_ROUNDS = 12;
9031
9333
  var LEGACY_ENV_ID = "tok_legacy_env";
9032
9334
  function genId() {
9033
- return `tok_${randomBytes3(8).toString("hex")}`;
9335
+ return `tok_${randomBytes2(8).toString("hex")}`;
9034
9336
  }
9035
9337
  function genSecret() {
9036
- return randomBytes3(24).toString("base64url");
9338
+ return randomBytes2(24).toString("base64url");
9037
9339
  }
9038
9340
  function parseToken(raw) {
9039
9341
  const dot = raw.indexOf(".");
@@ -9049,7 +9351,7 @@ var TokenStore = class {
9049
9351
  async load() {
9050
9352
  if (this.cache) return this.cache;
9051
9353
  try {
9052
- const raw = await readFile8(this.path, "utf8");
9354
+ const raw = await readFile6(this.path, "utf8");
9053
9355
  const parsed = JSON.parse(raw);
9054
9356
  const list = Array.isArray(parsed) ? parsed : [];
9055
9357
  this.cache = list.map((entry) => {
@@ -9064,9 +9366,9 @@ var TokenStore = class {
9064
9366
  }
9065
9367
  async persist(records) {
9066
9368
  await mkdir7(dirname5(this.path), { recursive: true });
9067
- const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${randomBytes3(4).toString("hex")}`;
9068
- await writeFile8(tmp, JSON.stringify(records, null, 2), "utf8");
9069
- await rename2(tmp, this.path);
9369
+ const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${randomBytes2(4).toString("hex")}`;
9370
+ await writeFile6(tmp, JSON.stringify(records, null, 2), "utf8");
9371
+ await rename(tmp, this.path);
9070
9372
  this.cache = records;
9071
9373
  }
9072
9374
  async create(input) {
@@ -9750,8 +10052,8 @@ var OrchestratorServer = class {
9750
10052
  };
9751
10053
 
9752
10054
  // src/gateway/webhooks/store.ts
9753
- import { randomBytes as randomBytes4 } from "crypto";
9754
- import { readFile as readFile9, writeFile as writeFile9, mkdir as mkdir9, rename as rename3, chmod } from "fs/promises";
10055
+ import { randomBytes as randomBytes3 } from "crypto";
10056
+ import { readFile as readFile7, writeFile as writeFile7, mkdir as mkdir9, rename as rename2, chmod } from "fs/promises";
9755
10057
  import { dirname as dirname7 } from "path";
9756
10058
  import { WebhookSubscriptionSchema } from "@harness-engineering/types";
9757
10059
 
@@ -9777,10 +10079,10 @@ function eventMatches(pattern, type) {
9777
10079
 
9778
10080
  // src/gateway/webhooks/store.ts
9779
10081
  function genId2() {
9780
- return `whk_${randomBytes4(8).toString("hex")}`;
10082
+ return `whk_${randomBytes3(8).toString("hex")}`;
9781
10083
  }
9782
10084
  function genSecret2() {
9783
- return randomBytes4(32).toString("base64url");
10085
+ return randomBytes3(32).toString("base64url");
9784
10086
  }
9785
10087
  var WebhookStore = class {
9786
10088
  constructor(path24) {
@@ -9791,7 +10093,7 @@ var WebhookStore = class {
9791
10093
  async load() {
9792
10094
  if (this.cache) return this.cache;
9793
10095
  try {
9794
- const raw = await readFile9(this.path, "utf8");
10096
+ const raw = await readFile7(this.path, "utf8");
9795
10097
  const parsed = JSON.parse(raw);
9796
10098
  const list = Array.isArray(parsed) ? parsed : [];
9797
10099
  this.cache = list.map((entry) => {
@@ -9805,11 +10107,11 @@ var WebhookStore = class {
9805
10107
  return this.cache;
9806
10108
  }
9807
10109
  async persist(records) {
9808
- const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${randomBytes4(4).toString("hex")}`;
10110
+ const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${randomBytes3(4).toString("hex")}`;
9809
10111
  try {
9810
10112
  await mkdir9(dirname7(this.path), { recursive: true });
9811
- await writeFile9(tmp, JSON.stringify(records, null, 2), { encoding: "utf8", mode: 384 });
9812
- await rename3(tmp, this.path);
10113
+ await writeFile7(tmp, JSON.stringify(records, null, 2), { encoding: "utf8", mode: 384 });
10114
+ await rename2(tmp, this.path);
9813
10115
  await chmod(this.path, 384);
9814
10116
  } catch (err) {
9815
10117
  if (err.code !== "ENOENT") throw err;
@@ -9849,7 +10151,7 @@ var WebhookStore = class {
9849
10151
  };
9850
10152
 
9851
10153
  // src/gateway/webhooks/delivery.ts
9852
- import { randomBytes as randomBytes5 } from "crypto";
10154
+ import { randomBytes as randomBytes4 } from "crypto";
9853
10155
 
9854
10156
  // src/gateway/webhooks/queue.ts
9855
10157
  import Database from "better-sqlite3";
@@ -10059,7 +10361,7 @@ var WebhookDelivery = class {
10059
10361
  enqueue(sub, event) {
10060
10362
  const payload = JSON.stringify(event);
10061
10363
  this.queue.insert({
10062
- id: `dlv_${randomBytes5(8).toString("hex")}`,
10364
+ id: `dlv_${randomBytes4(8).toString("hex")}`,
10063
10365
  subscriptionId: sub.id,
10064
10366
  eventType: event.type,
10065
10367
  payload
@@ -10167,7 +10469,7 @@ var WebhookDelivery = class {
10167
10469
  };
10168
10470
 
10169
10471
  // src/gateway/webhooks/events.ts
10170
- import { randomBytes as randomBytes6 } from "crypto";
10472
+ import { randomBytes as randomBytes5 } from "crypto";
10171
10473
  var WEBHOOK_TOPICS = [
10172
10474
  "interaction.created",
10173
10475
  "interaction.resolved",
@@ -10182,8 +10484,8 @@ var WEBHOOK_TOPICS = [
10182
10484
  "proposal.approved",
10183
10485
  "proposal.rejected"
10184
10486
  ];
10185
- function newEventId2() {
10186
- return `evt_${randomBytes6(8).toString("hex")}`;
10487
+ function newEventId() {
10488
+ return `evt_${randomBytes5(8).toString("hex")}`;
10187
10489
  }
10188
10490
  function wireWebhookFanout({ bus, store, delivery }) {
10189
10491
  const handlers = [];
@@ -10194,7 +10496,7 @@ function wireWebhookFanout({ bus, store, delivery }) {
10194
10496
  const subs = await store.listForEvent(eventType);
10195
10497
  if (subs.length === 0) return;
10196
10498
  const event = {
10197
- id: newEventId2(),
10499
+ id: newEventId(),
10198
10500
  type: eventType,
10199
10501
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10200
10502
  data
@@ -10213,7 +10515,7 @@ function wireWebhookFanout({ bus, store, delivery }) {
10213
10515
  }
10214
10516
 
10215
10517
  // src/gateway/telemetry/fanout.ts
10216
- import { randomBytes as randomBytes7 } from "crypto";
10518
+ import { randomBytes as randomBytes6 } from "crypto";
10217
10519
  import { SpanKind } from "@harness-engineering/core";
10218
10520
  var TOPICS = {
10219
10521
  MAINTENANCE_STARTED: "maintenance:started",
@@ -10236,14 +10538,14 @@ var SPAN_NAME = {
10236
10538
  [TOPICS.DISPATCH_DECISION]: "dispatch_decision",
10237
10539
  [TOPICS.SKILL_INVOCATION]: "skill_invocation"
10238
10540
  };
10239
- function newEventId3() {
10240
- return `evt_${randomBytes7(8).toString("hex")}`;
10541
+ function newEventId2() {
10542
+ return `evt_${randomBytes6(8).toString("hex")}`;
10241
10543
  }
10242
10544
  function newTraceId() {
10243
- return randomBytes7(16).toString("hex");
10545
+ return randomBytes6(16).toString("hex");
10244
10546
  }
10245
10547
  function newSpanId() {
10246
- return randomBytes7(8).toString("hex");
10548
+ return randomBytes6(8).toString("hex");
10247
10549
  }
10248
10550
  function nowNs() {
10249
10551
  return BigInt(Date.now()) * 1000000n;
@@ -10357,7 +10659,7 @@ function wireTelemetryFanout(params) {
10357
10659
  };
10358
10660
  exporter.push(span);
10359
10661
  const gatewayEvent = {
10360
- id: newEventId3(),
10662
+ id: newEventId2(),
10361
10663
  type: TELEMETRY_TYPE[topic],
10362
10664
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10363
10665
  data: payload,
@@ -10551,7 +10853,7 @@ function buildSlackSink(config, options) {
10551
10853
  }
10552
10854
 
10553
10855
  // src/notifications/events.ts
10554
- import { randomBytes as randomBytes8 } from "crypto";
10856
+ import { randomBytes as randomBytes7 } from "crypto";
10555
10857
 
10556
10858
  // src/notifications/envelope.ts
10557
10859
  function asObj(data) {
@@ -10682,8 +10984,8 @@ var NOTIFICATION_TOPICS = [
10682
10984
  "proposal.approved",
10683
10985
  "proposal.rejected"
10684
10986
  ];
10685
- function newEventId4() {
10686
- return `evt_${randomBytes8(8).toString("hex")}`;
10987
+ function newEventId3() {
10988
+ return `evt_${randomBytes7(8).toString("hex")}`;
10687
10989
  }
10688
10990
  function dispatchToEntry(bus, entry, event) {
10689
10991
  const eventType = event.type;
@@ -10718,7 +11020,7 @@ function wireNotificationSinks({ bus, registry }) {
10718
11020
  const entries = registry.list();
10719
11021
  if (entries.length === 0) return;
10720
11022
  const event = {
10721
- id: newEventId4(),
11023
+ id: newEventId3(),
10722
11024
  type: eventType,
10723
11025
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10724
11026
  data
@@ -10829,6 +11131,37 @@ async function scanWorkspaceConfig(workspacePath) {
10829
11131
  return { exitCode: computeScanExitCode(results), results };
10830
11132
  }
10831
11133
 
11134
+ // src/core/lane-persistence.ts
11135
+ import { eventSourcing } from "@harness-engineering/core";
11136
+ import { Err as Err21 } from "@harness-engineering/types";
11137
+ var SIGNAL_TO_LANE = {
11138
+ claim: "claimed",
11139
+ dispatch: "in_progress",
11140
+ success: "in_review",
11141
+ failure: "blocked",
11142
+ abandon: "canceled"
11143
+ };
11144
+ function mapOrchestratorLane(signal) {
11145
+ return SIGNAL_TO_LANE[signal];
11146
+ }
11147
+ async function persistLane(projectPath, issueId, signal) {
11148
+ try {
11149
+ const reg = await eventSourcing.registerTask(projectPath, issueId, []);
11150
+ if (!reg.ok) return reg;
11151
+ return await eventSourcing.transitionLane(projectPath, issueId, mapOrchestratorLane(signal));
11152
+ } catch (err) {
11153
+ return Err21(err instanceof Error ? err : new Error(String(err)));
11154
+ }
11155
+ }
11156
+ async function readPersistedLanes(projectPath) {
11157
+ try {
11158
+ const snap = await eventSourcing.readSnapshot(projectPath);
11159
+ return snap.ok ? snap.value.lanes : { tasks: {} };
11160
+ } catch {
11161
+ return { tasks: {} };
11162
+ }
11163
+ }
11164
+
10832
11165
  // src/maintenance/task-registry.ts
10833
11166
  var BUILT_IN_TASKS = [
10834
11167
  // --- Mechanical-AI ---
@@ -10892,7 +11225,13 @@ var BUILT_IN_TASKS = [
10892
11225
  description: "Detect and fix cross-check violations",
10893
11226
  schedule: "0 6 * * 1",
10894
11227
  branch: "harness-maint/cross-check-fixes",
10895
- checkCommand: ["validate-cross-check"],
11228
+ // `harness cross-check` is a dedicated read-only CLI subcommand that surfaces
11229
+ // JUST cross-artifact consistency (plan→implementation coverage + staleness),
11230
+ // mirroring the `validate_cross_check` MCP tool's core (`runCrossCheck`)
11231
+ // WITHOUT running the full `harness validate` suite. It prints a parseable
11232
+ // `Cross-check: N issues` line and exits 0 (clean) / 1 (N issues), so the
11233
+ // maintenance runner reports real results instead of an honest `failure`.
11234
+ checkCommand: ["cross-check"],
10896
11235
  fixSkill: "harness-cross-check-fix"
10897
11236
  },
10898
11237
  // --- Pure-AI ---
@@ -10951,7 +11290,10 @@ var BUILT_IN_TASKS = [
10951
11290
  description: "Assess overall project health",
10952
11291
  schedule: "0 6 * * *",
10953
11292
  branch: null,
10954
- checkCommand: ["assess_project"]
11293
+ // `assess_project` is an MCP tool name, not a CLI subcommand. The CLI
11294
+ // composite project-health report is `harness insights` (health, entropy,
11295
+ // decay, attention, impact) — a read-only report that records metrics.
11296
+ checkCommand: ["insights"]
10955
11297
  },
10956
11298
  {
10957
11299
  id: "stale-constraints",
@@ -10959,7 +11301,14 @@ var BUILT_IN_TASKS = [
10959
11301
  description: "Detect stale architectural constraints",
10960
11302
  schedule: "0 2 1 * *",
10961
11303
  branch: null,
10962
- checkCommand: ["detect_stale_constraints"]
11304
+ // `harness stale-constraints` is a dedicated read-only CLI subcommand that
11305
+ // surfaces the `detect_stale_constraints` MCP tool's core in-process. It is
11306
+ // precondition-gated on the knowledge graph: with no graph it emits the
11307
+ // "No knowledge graph found. Run `harness scan` first." signature and exits
11308
+ // non-zero, which the runner classifies as `skipped` (not failure). With a
11309
+ // graph it prints a parseable `Stale constraints: N findings` line and exits
11310
+ // 0 (clean) / 1 (N stale), recorded as report-only metrics.
11311
+ checkCommand: ["stale-constraints"]
10963
11312
  },
10964
11313
  {
10965
11314
  id: "graph-refresh",
@@ -10992,7 +11341,8 @@ var BUILT_IN_TASKS = [
10992
11341
  description: "Clean up stale orchestrator sessions",
10993
11342
  schedule: "0 0 * * *",
10994
11343
  branch: null,
10995
- checkCommand: ["cleanup-sessions"]
11344
+ checkCommand: ["cleanup-sessions"],
11345
+ excludeFromHumanSweep: true
10996
11346
  },
10997
11347
  {
10998
11348
  id: "perf-baselines",
@@ -11000,7 +11350,8 @@ var BUILT_IN_TASKS = [
11000
11350
  description: "Update performance baselines",
11001
11351
  schedule: "0 7 * * *",
11002
11352
  branch: null,
11003
- checkCommand: ["perf", "baselines", "update"]
11353
+ checkCommand: ["perf", "baselines", "update"],
11354
+ excludeFromHumanSweep: true
11004
11355
  },
11005
11356
  {
11006
11357
  id: "main-sync",
@@ -11008,7 +11359,8 @@ var BUILT_IN_TASKS = [
11008
11359
  description: "Fast-forward local default branch from origin",
11009
11360
  schedule: "*/15 * * * *",
11010
11361
  branch: null,
11011
- checkCommand: ["harness", "sync-main", "--json"]
11362
+ checkCommand: ["harness", "sync-main", "--json"],
11363
+ excludeFromHumanSweep: true
11012
11364
  },
11013
11365
  // Hermes Phase 4 — one-shot backfill that stamps `provenance: user-authored`
11014
11366
  // on every existing catalog skill. Schedule is Feb 31 (a date that never
@@ -11021,7 +11373,8 @@ var BUILT_IN_TASKS = [
11021
11373
  description: "Backfill provenance: user-authored on every existing skill (one-shot, idempotent)",
11022
11374
  schedule: "0 0 31 2 *",
11023
11375
  branch: null,
11024
- checkCommand: ["backfill-skill-provenance"]
11376
+ checkCommand: ["backfill-skill-provenance"],
11377
+ excludeFromHumanSweep: true
11025
11378
  }
11026
11379
  ];
11027
11380
 
@@ -11088,6 +11441,17 @@ function cronMatchesNow(expression, now) {
11088
11441
  const daysOfWeek = parseField(dowField, 0, 6);
11089
11442
  return minutes.has(minute) && hours.has(hour) && daysOfMonth.has(dayOfMonth) && months.has(month) && daysOfWeek.has(dayOfWeek);
11090
11443
  }
11444
+ function cronMatchesDate(expression, date) {
11445
+ const fields = expression.trim().split(/\s+/);
11446
+ if (fields.length !== 5) {
11447
+ throw new Error(`Invalid cron expression: expected 5 fields, got ${fields.length}`);
11448
+ }
11449
+ const [, , domField, monthField, dowField] = fields;
11450
+ const daysOfMonth = parseField(domField, 1, 31);
11451
+ const months = parseField(monthField, 1, 12);
11452
+ const daysOfWeek = parseField(dowField, 0, 6);
11453
+ return daysOfMonth.has(date.getDate()) && months.has(date.getMonth() + 1) && daysOfWeek.has(date.getDay());
11454
+ }
11091
11455
 
11092
11456
  // src/maintenance/scheduler.ts
11093
11457
  var MaintenanceScheduler = class {
@@ -11337,7 +11701,7 @@ var SingleProcessLeaderElector = class {
11337
11701
  };
11338
11702
 
11339
11703
  // src/maintenance/reporter.ts
11340
- import * as fs16 from "fs";
11704
+ import * as fs14 from "fs";
11341
11705
  import * as path18 from "path";
11342
11706
  import { z as z17 } from "zod";
11343
11707
  var RunResultSchema = z17.object({
@@ -11373,9 +11737,9 @@ var MaintenanceReporter = class {
11373
11737
  */
11374
11738
  async load() {
11375
11739
  try {
11376
- await fs16.promises.mkdir(this.persistDir, { recursive: true });
11740
+ await fs14.promises.mkdir(this.persistDir, { recursive: true });
11377
11741
  const filePath = path18.join(this.persistDir, "history.json");
11378
- const data = await fs16.promises.readFile(filePath, "utf-8");
11742
+ const data = await fs14.promises.readFile(filePath, "utf-8");
11379
11743
  const parsed = z17.array(RunResultSchema).safeParse(JSON.parse(data));
11380
11744
  if (parsed.success) {
11381
11745
  this.history = parsed.data.slice(0, MAX_HISTORY);
@@ -11409,9 +11773,9 @@ var MaintenanceReporter = class {
11409
11773
  */
11410
11774
  async persist() {
11411
11775
  try {
11412
- await fs16.promises.mkdir(this.persistDir, { recursive: true });
11776
+ await fs14.promises.mkdir(this.persistDir, { recursive: true });
11413
11777
  const filePath = path18.join(this.persistDir, "history.json");
11414
- await fs16.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
11778
+ await fs14.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
11415
11779
  } catch (err) {
11416
11780
  this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
11417
11781
  }
@@ -11451,20 +11815,20 @@ var TaskRunner = class {
11451
11815
  * @param origin - Hermes Phase 2 trigger-source tag; defaults to `'cron'`
11452
11816
  * when called from the scheduler path.
11453
11817
  */
11454
- async run(task, origin = "cron") {
11818
+ async run(task, origin = "cron", mode = "fix") {
11455
11819
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
11456
11820
  let result;
11457
11821
  let captured;
11458
11822
  try {
11459
11823
  switch (task.type) {
11460
11824
  case "mechanical-ai": {
11461
- const out = await this.runMechanicalAI(task, startedAt);
11825
+ const out = await this.runMechanicalAI(task, startedAt, mode);
11462
11826
  result = out.result;
11463
11827
  captured = out.captured;
11464
11828
  break;
11465
11829
  }
11466
11830
  case "pure-ai":
11467
- result = await this.runPureAI(task, startedAt);
11831
+ result = await this.runPureAI(task, startedAt, mode);
11468
11832
  break;
11469
11833
  case "report-only": {
11470
11834
  const out = await this.runReportOnly(task, startedAt);
@@ -11473,7 +11837,7 @@ var TaskRunner = class {
11473
11837
  break;
11474
11838
  }
11475
11839
  case "housekeeping": {
11476
- const out = await this.runHousekeeping(task, startedAt);
11840
+ const out = await this.runHousekeeping(task, startedAt, mode);
11477
11841
  result = out.result;
11478
11842
  captured = out.captured;
11479
11843
  break;
@@ -11537,7 +11901,8 @@ var TaskRunner = class {
11537
11901
  findings: r2.findings,
11538
11902
  stdout: r2.output,
11539
11903
  stderr: r2.stderr,
11540
- structured: r2.structured ? r2.structured : null
11904
+ structured: r2.structured ? r2.structured : null,
11905
+ executionFailed: false
11541
11906
  };
11542
11907
  }
11543
11908
  if (!task.checkCommand || task.checkCommand.length === 0) {
@@ -11549,7 +11914,8 @@ var TaskRunner = class {
11549
11914
  findings: r.findings,
11550
11915
  stdout: r.output,
11551
11916
  stderr: "",
11552
- structured: null
11917
+ structured: null,
11918
+ executionFailed: r.executionFailed ?? false
11553
11919
  };
11554
11920
  }
11555
11921
  /**
@@ -11574,7 +11940,7 @@ var TaskRunner = class {
11574
11940
  * only if fixable findings exist; persist captured stdout/stderr/context
11575
11941
  * via the output store on the way out.
11576
11942
  */
11577
- async runMechanicalAI(task, startedAt) {
11943
+ async runMechanicalAI(task, startedAt, mode = "fix") {
11578
11944
  if (!task.fixSkill) {
11579
11945
  return wrap(this.failureResult(task.id, startedAt, "mechanical-ai task missing fixSkill"));
11580
11946
  }
@@ -11596,6 +11962,27 @@ var TaskRunner = class {
11596
11962
  } catch (err) {
11597
11963
  return wrap(this.failureResult(task.id, startedAt, String(err)));
11598
11964
  }
11965
+ if (check.executionFailed) {
11966
+ const cls = classifyCheckExecutionFailure(`${check.stdout}
11967
+ ${check.stderr}`);
11968
+ if (cls.kind === "unrunnable") {
11969
+ return wrap(
11970
+ this.failureResult(task.id, startedAt, checkExecutionError(check)),
11971
+ captureFromCheck(check)
11972
+ );
11973
+ }
11974
+ if (cls.kind === "precondition") {
11975
+ return wrap(
11976
+ this.skippedResult(task.id, startedAt, cls.reason ?? "precondition not met"),
11977
+ captureFromCheck(check)
11978
+ );
11979
+ }
11980
+ check = {
11981
+ ...check,
11982
+ executionFailed: false,
11983
+ findings: check.findings > 0 ? check.findings : recoverFindingsCount(check.stdout)
11984
+ };
11985
+ }
11599
11986
  const promptContext = await this.composePromptContext(task);
11600
11987
  const baseCaptured = {
11601
11988
  stdout: check.stdout,
@@ -11604,13 +11991,14 @@ var TaskRunner = class {
11604
11991
  ...promptContext ? { context: promptContext } : {}
11605
11992
  };
11606
11993
  const wakeAgentExplicitlyFalse = check.structured !== null && typeof check.structured === "object" && check.structured.wakeAgent === false;
11607
- if (check.findings === 0 || wakeAgentExplicitlyFalse) {
11994
+ if (check.findings === 0 || wakeAgentExplicitlyFalse || mode === "report") {
11995
+ const reportedWithFindings = mode === "report" && check.findings > 0;
11608
11996
  return {
11609
11997
  result: {
11610
11998
  taskId: task.id,
11611
11999
  startedAt,
11612
12000
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
11613
- status: "no-issues",
12001
+ status: reportedWithFindings ? "success" : "no-issues",
11614
12002
  findings: check.findings,
11615
12003
  fixed: 0,
11616
12004
  prUrl: null,
@@ -11680,7 +12068,19 @@ var TaskRunner = class {
11680
12068
  /**
11681
12069
  * Pure-AI: always dispatch agent with configured skill.
11682
12070
  */
11683
- async runPureAI(task, startedAt) {
12071
+ async runPureAI(task, startedAt, mode = "fix") {
12072
+ if (mode === "report") {
12073
+ return {
12074
+ taskId: task.id,
12075
+ startedAt,
12076
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
12077
+ status: "no-issues",
12078
+ findings: 0,
12079
+ fixed: 0,
12080
+ prUrl: null,
12081
+ prUpdated: false
12082
+ };
12083
+ }
11684
12084
  if (!task.fixSkill) {
11685
12085
  return this.failureResult(task.id, startedAt, "pure-ai task missing fixSkill");
11686
12086
  }
@@ -11754,6 +12154,27 @@ var TaskRunner = class {
11754
12154
  return wrap(this.failureResult(task.id, startedAt, String(err)));
11755
12155
  }
11756
12156
  const parsed = parseStatusLine(check.stdout);
12157
+ if (parsed === null && check.executionFailed) {
12158
+ const cls = classifyCheckExecutionFailure(`${check.stdout}
12159
+ ${check.stderr}`);
12160
+ if (cls.kind === "unrunnable") {
12161
+ return wrap(
12162
+ this.failureResult(task.id, startedAt, checkExecutionError(check)),
12163
+ captureFromCheck(check)
12164
+ );
12165
+ }
12166
+ if (cls.kind === "precondition") {
12167
+ return wrap(
12168
+ this.skippedResult(task.id, startedAt, cls.reason ?? "precondition not met"),
12169
+ captureFromCheck(check)
12170
+ );
12171
+ }
12172
+ check = {
12173
+ ...check,
12174
+ executionFailed: false,
12175
+ findings: check.findings > 0 ? check.findings : recoverFindingsCount(check.stdout)
12176
+ };
12177
+ }
11757
12178
  const status = parsed?.status ?? "success";
11758
12179
  const findings = parsed === null ? check.findings : typeof parsed.candidatesFound === "number" ? parsed.candidatesFound : 0;
11759
12180
  const result = {
@@ -11787,7 +12208,20 @@ var TaskRunner = class {
11787
12208
  * Hermes Phase 2: a `checkScript` may replace `checkCommand` for housekeeping
11788
12209
  * tasks; the runner falls through to the same JSON-status parsing path.
11789
12210
  */
11790
- async runHousekeeping(task, startedAt) {
12211
+ async runHousekeeping(task, startedAt, mode = "fix") {
12212
+ if (mode === "report") {
12213
+ return wrap({
12214
+ taskId: task.id,
12215
+ startedAt,
12216
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
12217
+ status: "skipped",
12218
+ findings: 0,
12219
+ fixed: 0,
12220
+ prUrl: null,
12221
+ prUpdated: false,
12222
+ error: "skipped in report mode: housekeeping may mutate and report runs are read-only"
12223
+ });
12224
+ }
11791
12225
  if (!task.checkCommand && !task.checkScript) {
11792
12226
  return wrap(
11793
12227
  this.failureResult(
@@ -11854,10 +12288,86 @@ var TaskRunner = class {
11854
12288
  error
11855
12289
  };
11856
12290
  }
12291
+ /**
12292
+ * A precondition-gated check (e.g. `predict` with <3 snapshots, or a
12293
+ * graph-backed check before `harness scan`). The command is correctly
12294
+ * configured; the repo just lacks the state it needs. Reported as `skipped`
12295
+ * — distinct from a hard `failure` — carrying the refusal line as the reason
12296
+ * (surfaced in the run-report summary column). ADR 0050.
12297
+ */
12298
+ skippedResult(taskId, startedAt, reason) {
12299
+ return {
12300
+ taskId,
12301
+ startedAt,
12302
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
12303
+ status: "skipped",
12304
+ findings: 0,
12305
+ fixed: 0,
12306
+ prUrl: null,
12307
+ prUpdated: false,
12308
+ error: reason
12309
+ };
12310
+ }
11857
12311
  };
11858
12312
  function wrap(result, captured) {
11859
12313
  return captured ? { result, captured } : { result };
11860
12314
  }
12315
+ function checkExecutionError(check) {
12316
+ const detail = (check.stderr || check.stdout || "").trim();
12317
+ return detail ? `check command failed to execute: ${detail}` : "check command failed to execute";
12318
+ }
12319
+ function captureFromCheck(check) {
12320
+ return { stdout: check.stdout, stderr: check.stderr, structured: check.structured };
12321
+ }
12322
+ var PRECONDITION_PATTERNS = [
12323
+ /requires at least \d+ snapshots?/i,
12324
+ /Run ["'`]?harness snapshot/i,
12325
+ /no knowledge graph found/i,
12326
+ /Run ["'`]?harness scan/i,
12327
+ /no graph (?:available|found)/i
12328
+ ];
12329
+ var UNRUNNABLE_PATTERNS = [
12330
+ /unknown command/i,
12331
+ /unknown option/i,
12332
+ /\bENOENT\b/,
12333
+ /command not found/i,
12334
+ /cannot find module/i,
12335
+ // A timed-out check did not complete — it is a hard failure, never a
12336
+ // "ran-no-count" success. The runners synthesize this message on SIGTERM.
12337
+ /timed out/i,
12338
+ /\bETIMEDOUT\b/
12339
+ ];
12340
+ var TIMEOUT_SIGNATURE = /check timed out after \d+\s*ms/i;
12341
+ function explicitFindingsCount(output) {
12342
+ const after = output.match(/(?:findings?|issues?|violations?|errors?)\s*[:=]\s*(\d+)/i);
12343
+ if (after) return parseInt(after[1], 10);
12344
+ const before = output.match(/(\d+)\s+(?:findings?|issues?|violations?|errors?)\b/i);
12345
+ if (before) return parseInt(before[1], 10);
12346
+ return null;
12347
+ }
12348
+ function recoverFindingsCount(output) {
12349
+ return explicitFindingsCount(output) ?? 1;
12350
+ }
12351
+ function firstMeaningfulLine(output) {
12352
+ const line = output.split("\n").map((l) => l.trim()).find(Boolean);
12353
+ if (!line) return "precondition not met";
12354
+ return line.replace(/^[x✗✓!-]\s*/u, "").trim();
12355
+ }
12356
+ var CLASSIFY_HEAD_LINES = 3;
12357
+ function classifyCheckExecutionFailure(output) {
12358
+ const text = (output ?? "").trim();
12359
+ if (text.length === 0) return { kind: "unrunnable" };
12360
+ if (TIMEOUT_SIGNATURE.test(text)) return { kind: "unrunnable" };
12361
+ if (explicitFindingsCount(text) !== null) return { kind: "ran-no-count" };
12362
+ const head = text.split("\n").filter((l) => l.trim()).slice(0, CLASSIFY_HEAD_LINES).join("\n");
12363
+ for (const re of PRECONDITION_PATTERNS) {
12364
+ if (re.test(head)) return { kind: "precondition", reason: firstMeaningfulLine(text) };
12365
+ }
12366
+ for (const re of UNRUNNABLE_PATTERNS) {
12367
+ if (re.test(head)) return { kind: "unrunnable" };
12368
+ }
12369
+ return { kind: "ran-no-count" };
12370
+ }
11861
12371
  function parseStatusLine(output) {
11862
12372
  const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
11863
12373
  for (let i = lines.length - 1; i >= 0; i--) {
@@ -12006,8 +12516,49 @@ function heuristicResult(stdout, stderr, exitedAbnormally) {
12006
12516
  };
12007
12517
  }
12008
12518
 
12519
+ // src/maintenance/check-runner.ts
12520
+ import { execFile as nodeExecFile } from "child_process";
12521
+ import { promisify as promisify4 } from "util";
12522
+ var MAINTENANCE_CHECK_MAX_BUFFER = 64 * 1024 * 1024;
12523
+ var MAINTENANCE_CHECK_TIMEOUT_MS = 3e5;
12524
+ var FINDINGS_RE = /(\d+)\s+(?:finding|issue|violation|error)/i;
12525
+ var nodeExecFileAsync = promisify4(nodeExecFile);
12526
+ function isCheckTimeoutError(e) {
12527
+ return e.killed === true || e.signal === "SIGTERM" || e.code === "ETIMEDOUT";
12528
+ }
12529
+ async function runHarnessCheck(spawn7, cwd, opts = {}) {
12530
+ const execFileAsync2 = opts.execFileAsync ?? nodeExecFileAsync;
12531
+ const timeoutMs = opts.timeoutMs ?? MAINTENANCE_CHECK_TIMEOUT_MS;
12532
+ const maxBuffer = opts.maxBuffer ?? MAINTENANCE_CHECK_MAX_BUFFER;
12533
+ try {
12534
+ const { stdout } = await execFileAsync2(spawn7.file, spawn7.args, {
12535
+ cwd,
12536
+ timeout: timeoutMs,
12537
+ maxBuffer
12538
+ });
12539
+ const text = String(stdout);
12540
+ const m = text.match(FINDINGS_RE);
12541
+ const findings = m ? parseInt(m[1], 10) : 0;
12542
+ return { passed: findings === 0, findings, output: text, executionFailed: false };
12543
+ } catch (err) {
12544
+ const e = err;
12545
+ let output = [e.stdout, e.stderr].map((v) => v == null ? "" : String(v)).filter(Boolean).join("\n");
12546
+ if (isCheckTimeoutError(e)) {
12547
+ const note = `check timed out after ${timeoutMs}ms`;
12548
+ output = output.trim() ? `${output}
12549
+ ${note}` : note;
12550
+ return { passed: false, findings: 0, output, executionFailed: true };
12551
+ }
12552
+ const m = output.match(FINDINGS_RE);
12553
+ if (m) {
12554
+ return { passed: false, findings: parseInt(m[1], 10), output, executionFailed: false };
12555
+ }
12556
+ return { passed: false, findings: 0, output, executionFailed: true };
12557
+ }
12558
+ }
12559
+
12009
12560
  // src/maintenance/output-store.ts
12010
- import * as fs17 from "fs";
12561
+ import * as fs15 from "fs";
12011
12562
  import * as path20 from "path";
12012
12563
  var DEFAULT_RETENTION = {
12013
12564
  runs: 50,
@@ -12048,13 +12599,13 @@ var TaskOutputStore = class {
12048
12599
  async write(taskId, entry, retention) {
12049
12600
  this.ensureSafeTaskId(taskId);
12050
12601
  const dir = this.dirFor(taskId);
12051
- await fs17.promises.mkdir(dir, { recursive: true });
12602
+ await fs15.promises.mkdir(dir, { recursive: true });
12052
12603
  const fileName = `${sanitizeIso(entry.completedAt || (/* @__PURE__ */ new Date()).toISOString())}.json`;
12053
12604
  const filePath = path20.join(dir, fileName);
12054
12605
  const tmpPath = `${filePath}.tmp`;
12055
12606
  const payload = JSON.stringify(entry, null, 2);
12056
- await fs17.promises.writeFile(tmpPath, payload, "utf-8");
12057
- await fs17.promises.rename(tmpPath, filePath);
12607
+ await fs15.promises.writeFile(tmpPath, payload, "utf-8");
12608
+ await fs15.promises.rename(tmpPath, filePath);
12058
12609
  try {
12059
12610
  await this.applyRetention(taskId, retention);
12060
12611
  } catch (err) {
@@ -12105,7 +12656,7 @@ var TaskOutputStore = class {
12105
12656
  }
12106
12657
  async readEntry(filePath) {
12107
12658
  try {
12108
- const buf = await fs17.promises.readFile(filePath, "utf-8");
12659
+ const buf = await fs15.promises.readFile(filePath, "utf-8");
12109
12660
  const parsed = JSON.parse(buf);
12110
12661
  return parsed;
12111
12662
  } catch {
@@ -12127,7 +12678,7 @@ var TaskOutputStore = class {
12127
12678
  const toRemove = /* @__PURE__ */ new Set([...overflow, ...aged]);
12128
12679
  for (const name of toRemove) {
12129
12680
  try {
12130
- await fs17.promises.unlink(path20.join(dir, name));
12681
+ await fs15.promises.unlink(path20.join(dir, name));
12131
12682
  } catch {
12132
12683
  }
12133
12684
  }
@@ -12136,7 +12687,7 @@ var TaskOutputStore = class {
12136
12687
  async function listJsonFilesDescending(dir) {
12137
12688
  let names;
12138
12689
  try {
12139
- names = await fs17.promises.readdir(dir);
12690
+ names = await fs15.promises.readdir(dir);
12140
12691
  } catch {
12141
12692
  return [];
12142
12693
  }
@@ -12245,7 +12796,7 @@ var fallbackLogger3 = {
12245
12796
  };
12246
12797
 
12247
12798
  // src/maintenance/custom-task-validator.ts
12248
- import { Ok as Ok23, Err as Err20 } from "@harness-engineering/types";
12799
+ import { Ok as Ok23, Err as Err22 } from "@harness-engineering/types";
12249
12800
  var ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
12250
12801
  var REQUIRED_FIELDS_BY_TYPE = {
12251
12802
  "mechanical-ai": ["branch", "fixSkill"],
@@ -12265,7 +12816,7 @@ function validateCustomTasks(customTasks, builtIns, deps = {}) {
12265
12816
  validateOne(id, task, builtInIds, allIds, deps, errors);
12266
12817
  }
12267
12818
  detectCycles(customTasks, builtIns, errors);
12268
- return errors.length === 0 ? Ok23(void 0) : Err20(errors);
12819
+ return errors.length === 0 ? Ok23(void 0) : Err22(errors);
12269
12820
  }
12270
12821
  function validateOne(id, task, builtInIds, allIds, deps, errors) {
12271
12822
  const prefix = `customTasks.${id}`;
@@ -12454,6 +13005,11 @@ function deriveSeedPaths(config) {
12454
13005
  const roadmapPath = config.tracker.kind === "roadmap" && config.tracker.filePath ? config.tracker.filePath : "docs/roadmap.md";
12455
13006
  return [".harness/proposals", roadmapPath];
12456
13007
  }
13008
+ function normalizeHarnessCommand(command) {
13009
+ if (command.length === 0) return [];
13010
+ if (command[0] === "harness") return command;
13011
+ return ["harness", ...command];
13012
+ }
12457
13013
  var Orchestrator = class extends EventEmitter {
12458
13014
  state;
12459
13015
  config;
@@ -12580,6 +13136,11 @@ var Orchestrator = class extends EventEmitter {
12580
13136
  abortControllers = /* @__PURE__ */ new Map();
12581
13137
  /** Guards against overlapping ticks when a tick takes longer than the polling interval */
12582
13138
  tickInProgress = false;
13139
+ /** Phase 4 (DLane-5): lanes read back from the durable log on first tick, for
13140
+ * observability only — NOT fed into reconciliation. Empty until the first tick. */
13141
+ persistedLanes = { tasks: {} };
13142
+ /** Ensures the lane read-back diagnostic runs at most once (first tick). */
13143
+ laneReadbackDone = false;
12583
13144
  /** Timestamp of the last stale branch sweep (at most once per hour) */
12584
13145
  lastBranchSweepMs = 0;
12585
13146
  /** Current tick-phase activity visible to the dashboard */
@@ -12826,48 +13387,30 @@ var Orchestrator = class extends EventEmitter {
12826
13387
  const logger = this.logger;
12827
13388
  const checkRunner = {
12828
13389
  run: async (command, cwd) => {
12829
- const { execFile: execFile7 } = await import("child_process");
12830
- const { promisify: promisify5 } = await import("util");
12831
- const execFileAsync2 = promisify5(execFile7);
12832
- const [cmd, ...args] = command;
12833
- if (!cmd) return { passed: true, findings: 0, output: "" };
12834
- try {
12835
- const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
12836
- const findingsMatch = stdout.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
12837
- const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 0;
12838
- return { passed: findings === 0, findings, output: stdout };
12839
- } catch (err) {
12840
- const error = err;
12841
- const output = [error.stdout, error.stderr].filter(Boolean).join("\n");
12842
- const findingsMatch = output.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
12843
- const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 1;
12844
- return { passed: false, findings, output };
12845
- }
12846
- }
12847
- };
12848
- const agentDispatcher = {
12849
- dispatch: async (skill, branch, backendName, cwd) => {
12850
- logger.info(
12851
- "Maintenance agent dispatcher invoked (stub \u2014 skill dispatch integration pending)",
12852
- {
12853
- skill,
12854
- branch,
12855
- backendName,
12856
- cwd
12857
- }
12858
- );
12859
- return { producedCommits: false, fixed: 0 };
13390
+ const [cmd, ...args] = normalizeHarnessCommand(command);
13391
+ if (!cmd) return { passed: true, findings: 0, output: "", executionFailed: false };
13392
+ return runHarnessCheck({ file: cmd, args }, cwd);
12860
13393
  }
12861
13394
  };
13395
+ const resolveBackend2 = makeBackendResolver(this.getBackends());
13396
+ const agentDispatcher = createAgentDispatcher({
13397
+ resolveBackend: resolveBackend2,
13398
+ git: (args, cwd) => execFileSync("git", args, { cwd, encoding: "utf-8" }).toString().trim(),
13399
+ logger
13400
+ });
12862
13401
  const commandExecutor = {
12863
13402
  exec: async (command, cwd) => {
12864
13403
  const { execFile: execFile7 } = await import("child_process");
12865
- const { promisify: promisify5 } = await import("util");
12866
- const execFileAsync2 = promisify5(execFile7);
12867
- const [cmd, ...args] = command;
13404
+ const { promisify: promisify6 } = await import("util");
13405
+ const execFileAsync2 = promisify6(execFile7);
13406
+ const [cmd, ...args] = normalizeHarnessCommand(command);
12868
13407
  if (!cmd) return { stdout: "" };
12869
13408
  try {
12870
- const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
13409
+ const { stdout } = await execFileAsync2(cmd, args, {
13410
+ cwd,
13411
+ timeout: MAINTENANCE_CHECK_TIMEOUT_MS,
13412
+ maxBuffer: MAINTENANCE_CHECK_MAX_BUFFER
13413
+ });
12871
13414
  return { stdout: String(stdout) };
12872
13415
  } catch (err) {
12873
13416
  logger.warn("Maintenance command execution failed", {
@@ -13002,6 +13545,10 @@ ${messages}`);
13002
13545
  async asyncTick() {
13003
13546
  await this.ensureClaimManager();
13004
13547
  await this.intelligenceRunner.loadPersistedData();
13548
+ if (!this.laneReadbackDone) {
13549
+ this.laneReadbackDone = true;
13550
+ await this.readBackPersistedLanes();
13551
+ }
13005
13552
  const nowMs = Date.now();
13006
13553
  this.setTickActivity("fetching", "Polling tracker for candidates");
13007
13554
  const candidatesResult = await this.tracker.fetchCandidateIssues();
@@ -13154,12 +13701,48 @@ ${messages}`);
13154
13701
  break;
13155
13702
  case "escalate":
13156
13703
  await this.handleEscalation(effect);
13704
+ await this.persistLaneSafe(effect.issueId, "abandon");
13157
13705
  break;
13158
13706
  case "claim":
13159
13707
  await this.handleClaimEffect(effect);
13160
13708
  break;
13161
13709
  }
13162
13710
  }
13711
+ /**
13712
+ * Phase 4 (DLane-5): persist an orchestrator lane transition to the durable
13713
+ * core event log at the effect boundary. NEVER throws — `persistLane` returns
13714
+ * an `Err` Result on failure, which is logged and swallowed here so a
13715
+ * lane-persistence failure can never break orchestrator dispatch.
13716
+ */
13717
+ async persistLaneSafe(issueId, signal) {
13718
+ const r = await persistLane(this.projectRoot, issueId, signal);
13719
+ if (!r.ok) {
13720
+ this.logger.warn(`lane persist failed for ${issueId} (${signal}): ${r.error.message}`);
13721
+ }
13722
+ }
13723
+ /**
13724
+ * Phase 4 (DLane-5): read persisted task lanes back from the durable log and
13725
+ * log a one-line summary. Stores the projection on `this.persistedLanes` for
13726
+ * observability. Read-only — never feeds reconciliation, never throws.
13727
+ */
13728
+ async readBackPersistedLanes() {
13729
+ const lanes = await readPersistedLanes(this.projectRoot);
13730
+ this.persistedLanes = lanes;
13731
+ const entries = Object.keys(lanes.tasks).map((id) => `${id}:${lanes.tasks[id]?.lane}`);
13732
+ const nonTerminal = entries.filter((e) => !e.endsWith(":done") && !e.endsWith(":canceled"));
13733
+ this.logger.info(
13734
+ `Lane read-back on startup: ${entries.length} persisted task(s), ${nonTerminal.length} non-terminal`,
13735
+ { nonTerminal }
13736
+ );
13737
+ }
13738
+ /**
13739
+ * Phase 4 (DLane-5): the task lanes most recently read back from the durable
13740
+ * log on startup, exposed for external observability. Read-only — a fresh
13741
+ * `{ tasks: {} }` until the first tick's read-back has run.
13742
+ */
13743
+ getPersistedLanes() {
13744
+ return this.persistedLanes;
13745
+ }
13163
13746
  /**
13164
13747
  * Guards workspace cleanup by checking whether the agent pushed a branch
13165
13748
  * that does not yet have a pull request. If so, the worktree is preserved
@@ -13346,6 +13929,7 @@ ${messages}`);
13346
13929
  }
13347
13930
  return;
13348
13931
  }
13932
+ await this.persistLaneSafe(effect.issue.id, "claim");
13349
13933
  await this.postClaimComment(effect.issue);
13350
13934
  await this.dispatchIssue(effect.issue, effect.attempt, effect.backend);
13351
13935
  }
@@ -13406,6 +13990,7 @@ ${messages}`);
13406
13990
  this.logger.info(`Dispatching issue: ${issue.identifier} (attempt ${attempt})`, {
13407
13991
  issueId: issue.id
13408
13992
  });
13993
+ await this.persistLaneSafe(issue.id, "dispatch");
13409
13994
  try {
13410
13995
  const workspaceResult = await this.workspace.ensureWorkspace(issue.identifier);
13411
13996
  if (!workspaceResult.ok) throw workspaceResult.error;
@@ -13614,6 +14199,7 @@ ${messages}`);
13614
14199
  * Informs the state machine that an agent worker has exited.
13615
14200
  */
13616
14201
  async emitWorkerExit(issueId, reason, attempt, error) {
14202
+ await this.persistLaneSafe(issueId, reason === "normal" ? "success" : "failure");
13617
14203
  await this.completionHandler.handleWorkerExit(
13618
14204
  issueId,
13619
14205
  reason,
@@ -14149,11 +14735,11 @@ function launchTUI(orchestrator) {
14149
14735
  }
14150
14736
 
14151
14737
  // src/maintenance/sync-main.ts
14152
- import { execFile as nodeExecFile } from "child_process";
14153
- import { promisify as promisify4 } from "util";
14738
+ import { execFile as nodeExecFile2 } from "child_process";
14739
+ import { promisify as promisify5 } from "util";
14154
14740
  var DEFAULT_TIMEOUT_MS3 = 6e4;
14155
14741
  async function git(execFileFn, args, cwd, timeoutMs) {
14156
- const exec = promisify4(execFileFn);
14742
+ const exec = promisify5(execFileFn);
14157
14743
  const { stdout, stderr } = await exec("git", args, { cwd, timeout: timeoutMs });
14158
14744
  return { stdout: String(stdout), stderr: String(stderr) };
14159
14745
  }
@@ -14215,7 +14801,7 @@ async function isAncestor(execFileFn, a, b, cwd, timeoutMs) {
14215
14801
  }
14216
14802
  }
14217
14803
  async function syncMain(repoRoot, opts = {}) {
14218
- const execFileFn = opts.execFileFn ?? nodeExecFile;
14804
+ const execFileFn = opts.execFileFn ?? nodeExecFile2;
14219
14805
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
14220
14806
  try {
14221
14807
  const originRef = await resolveOriginDefault(execFileFn, repoRoot, timeoutMs);
@@ -14292,8 +14878,56 @@ async function syncMain(repoRoot, opts = {}) {
14292
14878
  }
14293
14879
  }
14294
14880
 
14881
+ // src/maintenance/overdue.ts
14882
+ var LOOKBACK_DAYS = 366;
14883
+ function previousFireTime(schedule, now) {
14884
+ let day = new Date(now.getFullYear(), now.getMonth(), now.getDate());
14885
+ for (let d = 0; d <= LOOKBACK_DAYS; d++) {
14886
+ if (cronMatchesDate(schedule, day)) {
14887
+ const dayStartMs = day.getTime();
14888
+ const scanStart = d === 0 ? new Date(
14889
+ now.getFullYear(),
14890
+ now.getMonth(),
14891
+ now.getDate(),
14892
+ now.getHours(),
14893
+ now.getMinutes()
14894
+ ) : new Date(day.getFullYear(), day.getMonth(), day.getDate(), 23, 59);
14895
+ for (let ms = scanStart.getTime(); ms >= dayStartMs; ms -= 6e4) {
14896
+ const candidate = new Date(ms);
14897
+ if (cronMatchesNow(schedule, candidate)) return candidate;
14898
+ }
14899
+ }
14900
+ day = new Date(day.getFullYear(), day.getMonth(), day.getDate() - 1);
14901
+ }
14902
+ return null;
14903
+ }
14904
+ function isSatisfyingRun(r) {
14905
+ return r.status === "success" || r.status === "no-issues";
14906
+ }
14907
+ function isOverdue(task, history, now) {
14908
+ const satisfyingRuns = history.filter((r) => r.taskId === task.id && isSatisfyingRun(r));
14909
+ const fire = previousFireTime(task.schedule, now);
14910
+ if (fire === null) return satisfyingRuns.length === 0;
14911
+ const fireMs = fire.getTime();
14912
+ const satisfied = satisfyingRuns.some((r) => new Date(r.completedAt).getTime() >= fireMs);
14913
+ return !satisfied;
14914
+ }
14915
+ function selectTasks(tasks, history, filter) {
14916
+ const eligible = tasks.filter((t) => t.excludeFromHumanSweep !== true);
14917
+ switch (filter.mode) {
14918
+ case "all":
14919
+ return eligible;
14920
+ case "ids": {
14921
+ const wanted = new Set(filter.ids ?? []);
14922
+ return eligible.filter((t) => wanted.has(t.id));
14923
+ }
14924
+ case "overdue":
14925
+ return eligible.filter((t) => isOverdue(t, history, filter.now));
14926
+ }
14927
+ }
14928
+
14295
14929
  // src/sessions/search-index.ts
14296
- import * as fs18 from "fs";
14930
+ import * as fs16 from "fs";
14297
14931
  import * as path22 from "path";
14298
14932
  import Database2 from "better-sqlite3";
14299
14933
  import { INDEXED_FILE_KINDS } from "@harness-engineering/types";
@@ -14354,7 +14988,7 @@ var SqliteSearchIndex = class {
14354
14988
  removeSessionStmt;
14355
14989
  totalStmt;
14356
14990
  constructor(dbPath) {
14357
- fs18.mkdirSync(path22.dirname(dbPath), { recursive: true });
14991
+ fs16.mkdirSync(path22.dirname(dbPath), { recursive: true });
14358
14992
  this.db = new Database2(dbPath);
14359
14993
  this.db.pragma("journal_mode = WAL");
14360
14994
  this.db.pragma("synchronous = NORMAL");
@@ -14460,12 +15094,12 @@ function indexSessionDirectory(idx, args) {
14460
15094
  for (const kind of kinds) {
14461
15095
  const fileName = FILE_KIND_TO_FILENAME[kind];
14462
15096
  const filePath = path22.join(args.sessionDir, fileName);
14463
- if (!fs18.existsSync(filePath)) continue;
14464
- let body = fs18.readFileSync(filePath, "utf8");
15097
+ if (!fs16.existsSync(filePath)) continue;
15098
+ let body = fs16.readFileSync(filePath, "utf8");
14465
15099
  if (Buffer.byteLength(body, "utf8") > cap) {
14466
15100
  body = body.slice(0, cap) + "\n\n[TRUNCATED]";
14467
15101
  }
14468
- const stat = fs18.statSync(filePath);
15102
+ const stat = fs16.statSync(filePath);
14469
15103
  const relPath = path22.relative(args.projectPath, filePath).replaceAll("\\", "/");
14470
15104
  idx.upsertSessionDoc({
14471
15105
  sessionId: args.sessionId,
@@ -14487,8 +15121,8 @@ function reindexFromArchive(projectPath, opts = {}) {
14487
15121
  idx.resetArchived();
14488
15122
  let sessionsIndexed = 0;
14489
15123
  let docsWritten = 0;
14490
- if (fs18.existsSync(archiveBase)) {
14491
- const entries = fs18.readdirSync(archiveBase, { withFileTypes: true });
15124
+ if (fs16.existsSync(archiveBase)) {
15125
+ const entries = fs16.readdirSync(archiveBase, { withFileTypes: true });
14492
15126
  for (const entry of entries) {
14493
15127
  if (!entry.isDirectory()) continue;
14494
15128
  const sessionDir = path22.join(archiveBase, entry.name);
@@ -14511,12 +15145,12 @@ function reindexFromArchive(projectPath, opts = {}) {
14511
15145
  }
14512
15146
 
14513
15147
  // src/sessions/summarize.ts
14514
- import * as fs19 from "fs";
15148
+ import * as fs17 from "fs";
14515
15149
  import * as path23 from "path";
14516
15150
  import {
14517
15151
  SessionSummarySchema
14518
15152
  } from "@harness-engineering/types";
14519
- import { Ok as Ok24, Err as Err21 } from "@harness-engineering/types";
15153
+ import { Ok as Ok24, Err as Err23 } from "@harness-engineering/types";
14520
15154
  var LLM_SUMMARY_FILE = "llm-summary.md";
14521
15155
  var SUMMARY_INPUT_FILES = [
14522
15156
  { filename: "summary.md", kind: "summary" },
@@ -14543,9 +15177,9 @@ function readInputCorpus(archiveDir) {
14543
15177
  const parts = [];
14544
15178
  for (const { filename, kind } of SUMMARY_INPUT_FILES) {
14545
15179
  const p = path23.join(archiveDir, filename);
14546
- if (!fs19.existsSync(p)) continue;
15180
+ if (!fs17.existsSync(p)) continue;
14547
15181
  try {
14548
- const content = fs19.readFileSync(p, "utf8");
15182
+ const content = fs17.readFileSync(p, "utf8");
14549
15183
  if (content.trim().length === 0) continue;
14550
15184
  parts.push(`## FILE: ${kind}
14551
15185
 
@@ -14607,17 +15241,17 @@ status: failed
14607
15241
 
14608
15242
  - reason: ${reason}
14609
15243
  `;
14610
- fs19.writeFileSync(filePath, body, "utf8");
15244
+ fs17.writeFileSync(filePath, body, "utf8");
14611
15245
  return filePath;
14612
15246
  }
14613
15247
  async function summarizeArchivedSession(ctx) {
14614
15248
  const writeStubOnError = ctx.writeStubOnError ?? true;
14615
- if (!fs19.existsSync(ctx.archiveDir)) {
14616
- return Err21(new Error(`archive directory not found: ${ctx.archiveDir}`));
15249
+ if (!fs17.existsSync(ctx.archiveDir)) {
15250
+ return Err23(new Error(`archive directory not found: ${ctx.archiveDir}`));
14617
15251
  }
14618
15252
  const corpus = readInputCorpus(ctx.archiveDir);
14619
15253
  if (corpus.trim().length === 0) {
14620
- return Err21(new Error(`no summary input files found in ${ctx.archiveDir}`));
15254
+ return Err23(new Error(`no summary input files found in ${ctx.archiveDir}`));
14621
15255
  }
14622
15256
  const inputBudgetTokens = ctx.config?.inputBudgetTokens ?? DEFAULT_INPUT_BUDGET_TOKENS;
14623
15257
  const truncated = truncateForBudget(corpus, inputBudgetTokens);
@@ -14650,7 +15284,7 @@ async function summarizeArchivedSession(ctx) {
14650
15284
  } catch {
14651
15285
  }
14652
15286
  }
14653
- return Err21(
15287
+ return Err23(
14654
15288
  new Error(`session summary failed: ${reason}` + (stubPath ? ` (stub: ${stubPath})` : ""))
14655
15289
  );
14656
15290
  }
@@ -14664,7 +15298,7 @@ async function summarizeArchivedSession(ctx) {
14664
15298
  } catch {
14665
15299
  }
14666
15300
  }
14667
- return Err21(new Error(reason));
15301
+ return Err23(new Error(reason));
14668
15302
  }
14669
15303
  const meta = {
14670
15304
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -14675,7 +15309,7 @@ async function summarizeArchivedSession(ctx) {
14675
15309
  };
14676
15310
  const filePath = path23.join(ctx.archiveDir, LLM_SUMMARY_FILE);
14677
15311
  const body = renderLlmSummaryMarkdown(parsed.data, meta);
14678
- fs19.writeFileSync(filePath, body, "utf8");
15312
+ fs17.writeFileSync(filePath, body, "utf8");
14679
15313
  return Ok24({ summary: parsed.data, meta, filePath });
14680
15314
  }
14681
15315
  function isSummaryEnabled(config) {
@@ -14755,13 +15389,18 @@ export {
14755
15389
  BUILT_IN_TASKS,
14756
15390
  BackendDefSchema,
14757
15391
  BackendRouter,
15392
+ CheckScriptRunner,
14758
15393
  ClaimManager,
14759
15394
  GateNotReadyError,
14760
15395
  GateRunError,
14761
15396
  InteractionQueue,
15397
+ LinearGraphQLClient,
14762
15398
  LinearGraphQLStub,
14763
15399
  LocalModelResolver,
15400
+ MAINTENANCE_CHECK_MAX_BUFFER,
15401
+ MAINTENANCE_CHECK_TIMEOUT_MS,
14764
15402
  MAX_ATTEMPTS,
15403
+ MaintenanceReporter,
14765
15404
  MockBackend,
14766
15405
  ORCHESTRATOR_IDENTITY_FILE,
14767
15406
  Orchestrator,
@@ -14779,6 +15418,7 @@ export {
14779
15418
  SqliteSearchIndex,
14780
15419
  StreamRecorder,
14781
15420
  TaskOutputStore,
15421
+ TaskRunner,
14782
15422
  TokenStore,
14783
15423
  WebhookQueue,
14784
15424
  WorkflowLoader,
@@ -14789,7 +15429,9 @@ export {
14789
15429
  buildArchiveHooks,
14790
15430
  calculateRetryDelay,
14791
15431
  canDispatch,
15432
+ classifyCheckExecutionFailure,
14792
15433
  computeRateLimitDelay,
15434
+ createAgentDispatcher,
14793
15435
  createBackend,
14794
15436
  createEmptyState,
14795
15437
  crossFieldRoutingIssues,
@@ -14801,22 +15443,27 @@ export {
14801
15443
  emitProposalApproved,
14802
15444
  emitProposalCreated,
14803
15445
  emitProposalRejected,
15446
+ explicitFindingsCount,
14804
15447
  extractHighlights,
14805
15448
  extractTitlePrefix,
14806
15449
  getAvailableSlots,
14807
15450
  getDefaultConfig,
14808
15451
  getPerStateCount,
14809
15452
  indexSessionDirectory,
15453
+ isCheckTimeoutError,
14810
15454
  isEligible,
14811
15455
  isSummaryEnabled,
14812
15456
  launchTUI,
14813
15457
  loadPublishedIndex,
15458
+ makeBackendResolver,
14814
15459
  migrateAgentConfig,
14815
15460
  normalizeFts5Query,
15461
+ normalizeHarnessCommand,
14816
15462
  normalizeLocalModel,
14817
15463
  openSearchIndex,
14818
15464
  promote,
14819
15465
  reconcile,
15466
+ recoverFindingsCount,
14820
15467
  reindexFromArchive,
14821
15468
  renderAnalysisComment,
14822
15469
  renderLlmSummaryMarkdown,
@@ -14826,9 +15473,11 @@ export {
14826
15473
  routeIssue,
14827
15474
  routingWarnings,
14828
15475
  runGate,
15476
+ runHarnessCheck,
14829
15477
  savePublishedIndex,
14830
15478
  searchIndexPath,
14831
15479
  selectCandidates,
15480
+ selectTasks,
14832
15481
  sortCandidates,
14833
15482
  summarizeArchivedSession,
14834
15483
  syncMain,