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