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