@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.js
CHANGED
|
@@ -34,13 +34,18 @@ __export(index_exports, {
|
|
|
34
34
|
BUILT_IN_TASKS: () => BUILT_IN_TASKS,
|
|
35
35
|
BackendDefSchema: () => BackendDefSchema,
|
|
36
36
|
BackendRouter: () => BackendRouter,
|
|
37
|
+
CheckScriptRunner: () => CheckScriptRunner,
|
|
37
38
|
ClaimManager: () => ClaimManager,
|
|
38
39
|
GateNotReadyError: () => GateNotReadyError,
|
|
39
40
|
GateRunError: () => GateRunError,
|
|
40
41
|
InteractionQueue: () => InteractionQueue,
|
|
42
|
+
LinearGraphQLClient: () => LinearGraphQLClient,
|
|
41
43
|
LinearGraphQLStub: () => LinearGraphQLStub,
|
|
42
44
|
LocalModelResolver: () => LocalModelResolver,
|
|
45
|
+
MAINTENANCE_CHECK_MAX_BUFFER: () => MAINTENANCE_CHECK_MAX_BUFFER,
|
|
46
|
+
MAINTENANCE_CHECK_TIMEOUT_MS: () => MAINTENANCE_CHECK_TIMEOUT_MS,
|
|
43
47
|
MAX_ATTEMPTS: () => MAX_ATTEMPTS,
|
|
48
|
+
MaintenanceReporter: () => MaintenanceReporter,
|
|
44
49
|
MockBackend: () => MockBackend,
|
|
45
50
|
ORCHESTRATOR_IDENTITY_FILE: () => ORCHESTRATOR_IDENTITY_FILE,
|
|
46
51
|
Orchestrator: () => Orchestrator,
|
|
@@ -58,6 +63,7 @@ __export(index_exports, {
|
|
|
58
63
|
SqliteSearchIndex: () => SqliteSearchIndex,
|
|
59
64
|
StreamRecorder: () => StreamRecorder,
|
|
60
65
|
TaskOutputStore: () => TaskOutputStore,
|
|
66
|
+
TaskRunner: () => TaskRunner,
|
|
61
67
|
TokenStore: () => TokenStore,
|
|
62
68
|
WebhookQueue: () => WebhookQueue,
|
|
63
69
|
WorkflowLoader: () => WorkflowLoader,
|
|
@@ -68,7 +74,9 @@ __export(index_exports, {
|
|
|
68
74
|
buildArchiveHooks: () => buildArchiveHooks,
|
|
69
75
|
calculateRetryDelay: () => calculateRetryDelay,
|
|
70
76
|
canDispatch: () => canDispatch,
|
|
77
|
+
classifyCheckExecutionFailure: () => classifyCheckExecutionFailure,
|
|
71
78
|
computeRateLimitDelay: () => computeRateLimitDelay,
|
|
79
|
+
createAgentDispatcher: () => createAgentDispatcher,
|
|
72
80
|
createBackend: () => createBackend,
|
|
73
81
|
createEmptyState: () => createEmptyState,
|
|
74
82
|
crossFieldRoutingIssues: () => crossFieldRoutingIssues,
|
|
@@ -80,22 +88,27 @@ __export(index_exports, {
|
|
|
80
88
|
emitProposalApproved: () => emitProposalApproved,
|
|
81
89
|
emitProposalCreated: () => emitProposalCreated,
|
|
82
90
|
emitProposalRejected: () => emitProposalRejected,
|
|
91
|
+
explicitFindingsCount: () => explicitFindingsCount,
|
|
83
92
|
extractHighlights: () => extractHighlights,
|
|
84
93
|
extractTitlePrefix: () => extractTitlePrefix,
|
|
85
94
|
getAvailableSlots: () => getAvailableSlots,
|
|
86
95
|
getDefaultConfig: () => getDefaultConfig,
|
|
87
96
|
getPerStateCount: () => getPerStateCount,
|
|
88
97
|
indexSessionDirectory: () => indexSessionDirectory,
|
|
98
|
+
isCheckTimeoutError: () => isCheckTimeoutError,
|
|
89
99
|
isEligible: () => isEligible,
|
|
90
100
|
isSummaryEnabled: () => isSummaryEnabled,
|
|
91
101
|
launchTUI: () => launchTUI,
|
|
92
102
|
loadPublishedIndex: () => loadPublishedIndex,
|
|
103
|
+
makeBackendResolver: () => makeBackendResolver,
|
|
93
104
|
migrateAgentConfig: () => migrateAgentConfig,
|
|
94
105
|
normalizeFts5Query: () => normalizeFts5Query,
|
|
106
|
+
normalizeHarnessCommand: () => normalizeHarnessCommand,
|
|
95
107
|
normalizeLocalModel: () => normalizeLocalModel,
|
|
96
108
|
openSearchIndex: () => openSearchIndex,
|
|
97
109
|
promote: () => promote,
|
|
98
110
|
reconcile: () => reconcile,
|
|
111
|
+
recoverFindingsCount: () => recoverFindingsCount,
|
|
99
112
|
reindexFromArchive: () => reindexFromArchive,
|
|
100
113
|
renderAnalysisComment: () => renderAnalysisComment,
|
|
101
114
|
renderLlmSummaryMarkdown: () => renderLlmSummaryMarkdown,
|
|
@@ -105,9 +118,11 @@ __export(index_exports, {
|
|
|
105
118
|
routeIssue: () => routeIssue,
|
|
106
119
|
routingWarnings: () => routingWarnings,
|
|
107
120
|
runGate: () => runGate,
|
|
121
|
+
runHarnessCheck: () => runHarnessCheck,
|
|
108
122
|
savePublishedIndex: () => savePublishedIndex,
|
|
109
123
|
searchIndexPath: () => searchIndexPath,
|
|
110
124
|
selectCandidates: () => selectCandidates,
|
|
125
|
+
selectTasks: () => selectTasks,
|
|
111
126
|
sortCandidates: () => sortCandidates,
|
|
112
127
|
summarizeArchivedSession: () => summarizeArchivedSession,
|
|
113
128
|
syncMain: () => syncMain,
|
|
@@ -1341,7 +1356,7 @@ var ClaimManager = class {
|
|
|
1341
1356
|
// src/core/pr-detector.ts
|
|
1342
1357
|
var import_node_child_process = require("child_process");
|
|
1343
1358
|
var import_node_util = require("util");
|
|
1344
|
-
var PRDetector = class {
|
|
1359
|
+
var PRDetector = class _PRDetector {
|
|
1345
1360
|
logger;
|
|
1346
1361
|
execFileFn;
|
|
1347
1362
|
projectRoot;
|
|
@@ -1457,35 +1472,124 @@ var PRDetector = class {
|
|
|
1457
1472
|
}
|
|
1458
1473
|
}
|
|
1459
1474
|
/**
|
|
1460
|
-
*
|
|
1461
|
-
*
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1475
|
+
* GitHub closing keywords that link a PR to an issue it will close.
|
|
1476
|
+
* @see https://docs.github.com/articles/closing-issues-using-keywords
|
|
1477
|
+
*/
|
|
1478
|
+
static CLOSING_REF_RE = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)[\s:]+#(\d+)/gi;
|
|
1479
|
+
/**
|
|
1480
|
+
* Extracts the issue numbers a PR body declares it will close
|
|
1481
|
+
* (e.g. `Closes #42`, `fixes #7`, `Resolves: #99`).
|
|
1482
|
+
*/
|
|
1483
|
+
parseClosingIssueNumbers(body) {
|
|
1484
|
+
const nums = [];
|
|
1485
|
+
for (const match of body.matchAll(_PRDetector.CLOSING_REF_RE)) {
|
|
1486
|
+
nums.push(parseInt(match[1], 10));
|
|
1487
|
+
}
|
|
1488
|
+
return nums;
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Lists every open PR for a repo in a single `gh pr list` call and returns
|
|
1492
|
+
* the set of issue numbers those PRs close (parsed from their bodies).
|
|
1493
|
+
*
|
|
1494
|
+
* This is the batched replacement for per-issue `gh pr list --search
|
|
1495
|
+
* "closes #N"` queries. Every `gh pr list` form (search or plain list) is
|
|
1496
|
+
* served by GitHub's GraphQL API and consumes from the shared ~5000/hr
|
|
1497
|
+
* GraphQL budget, so issuing one query per candidate per tick exhausted the
|
|
1498
|
+
* limit on busy boards. A single `--state open` list returns every open PR
|
|
1499
|
+
* in one request regardless of candidate count, collapsing N calls into 1.
|
|
1500
|
+
*
|
|
1501
|
+
* Returns `null` if the check itself failed (gh missing, rate limited,
|
|
1502
|
+
* network error) so callers can fail open rather than block real work.
|
|
1503
|
+
*/
|
|
1504
|
+
async fetchOpenPRClosures(owner, repo) {
|
|
1505
|
+
try {
|
|
1506
|
+
const exec = (0, import_node_util.promisify)(this.execFileFn);
|
|
1507
|
+
const { stdout } = await exec(
|
|
1508
|
+
"gh",
|
|
1509
|
+
[
|
|
1510
|
+
"pr",
|
|
1511
|
+
"list",
|
|
1512
|
+
"--repo",
|
|
1513
|
+
`${owner}/${repo}`,
|
|
1514
|
+
"--state",
|
|
1515
|
+
"open",
|
|
1516
|
+
"--json",
|
|
1517
|
+
"body",
|
|
1518
|
+
"--limit",
|
|
1519
|
+
"200"
|
|
1520
|
+
],
|
|
1521
|
+
{
|
|
1522
|
+
cwd: this.projectRoot,
|
|
1523
|
+
timeout: 15e3
|
|
1524
|
+
}
|
|
1525
|
+
);
|
|
1526
|
+
const prs = JSON.parse(stdout);
|
|
1527
|
+
const closed = /* @__PURE__ */ new Set();
|
|
1528
|
+
for (const pr of prs) {
|
|
1529
|
+
for (const n of this.parseClosingIssueNumbers(pr.body ?? "")) closed.add(n);
|
|
1530
|
+
}
|
|
1531
|
+
return closed;
|
|
1532
|
+
} catch (err) {
|
|
1533
|
+
this.logger.debug(`Failed to list open PRs for ${owner}/${repo}`, {
|
|
1534
|
+
error: String(err)
|
|
1535
|
+
});
|
|
1536
|
+
return null;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Filters out candidates that already have an open GitHub PR.
|
|
1541
|
+
*
|
|
1542
|
+
* For GitHub-issue candidates the check is batched: one `gh pr list` per
|
|
1543
|
+
* distinct repo (not one search per issue), parsing closing references
|
|
1544
|
+
* locally. Candidates without a GitHub externalId fall back to a
|
|
1545
|
+
* `feat/<identifier>` branch lookup, throttled to a few concurrent gh calls.
|
|
1546
|
+
* Fail-open on API errors so a flaky/rate-limited GitHub never blocks work.
|
|
1465
1547
|
*/
|
|
1466
1548
|
async filterCandidatesWithOpenPRs(candidates) {
|
|
1549
|
+
const repos = /* @__PURE__ */ new Map();
|
|
1550
|
+
for (const candidate of candidates) {
|
|
1551
|
+
const parsed = candidate.externalId ? this.parseExternalId(candidate.externalId) : null;
|
|
1552
|
+
if (parsed) repos.set(`${parsed.owner}/${parsed.repo}`, parsed);
|
|
1553
|
+
}
|
|
1554
|
+
const repoClosures = /* @__PURE__ */ new Map();
|
|
1555
|
+
await Promise.all(
|
|
1556
|
+
[...repos.values()].map(async ({ owner, repo }) => {
|
|
1557
|
+
repoClosures.set(`${owner}/${repo}`, await this.fetchOpenPRClosures(owner, repo));
|
|
1558
|
+
})
|
|
1559
|
+
);
|
|
1560
|
+
const identifierCandidates = candidates.filter(
|
|
1561
|
+
(c) => !(c.externalId && this.parseExternalId(c.externalId))
|
|
1562
|
+
);
|
|
1563
|
+
const identifiersWithOpenPR = /* @__PURE__ */ new Set();
|
|
1467
1564
|
const concurrency = 3;
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
const
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
})
|
|
1565
|
+
for (let i = 0; i < identifierCandidates.length; i += concurrency) {
|
|
1566
|
+
const batch = identifierCandidates.slice(i, i + concurrency);
|
|
1567
|
+
const settled = await Promise.allSettled(
|
|
1568
|
+
batch.map(async (c) => ({
|
|
1569
|
+
identifier: c.identifier,
|
|
1570
|
+
hasOpenPR: await this.hasOpenPRForIdentifier(c.identifier)
|
|
1571
|
+
}))
|
|
1476
1572
|
);
|
|
1477
|
-
|
|
1573
|
+
for (const result of settled) {
|
|
1574
|
+
if (result.status === "fulfilled" && result.value.hasOpenPR) {
|
|
1575
|
+
identifiersWithOpenPR.add(result.value.identifier);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1478
1578
|
}
|
|
1479
1579
|
const filtered = [];
|
|
1480
|
-
for (
|
|
1481
|
-
const
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1580
|
+
for (const candidate of candidates) {
|
|
1581
|
+
const parsed = candidate.externalId ? this.parseExternalId(candidate.externalId) : null;
|
|
1582
|
+
let hasOpenPR;
|
|
1583
|
+
let via;
|
|
1584
|
+
if (parsed) {
|
|
1585
|
+
const closures = repoClosures.get(`${parsed.owner}/${parsed.repo}`);
|
|
1586
|
+
hasOpenPR = closures ? closures.has(parsed.number) : false;
|
|
1587
|
+
via = `externalId ${candidate.externalId}`;
|
|
1588
|
+
} else {
|
|
1589
|
+
hasOpenPR = identifiersWithOpenPR.has(candidate.identifier);
|
|
1590
|
+
via = `feat/${candidate.identifier}`;
|
|
1485
1591
|
}
|
|
1486
|
-
const { candidate, hasOpenPR } = result.value;
|
|
1487
1592
|
if (hasOpenPR) {
|
|
1488
|
-
const via = candidate.externalId ? `externalId ${candidate.externalId}` : `feat/${candidate.identifier}`;
|
|
1489
1593
|
this.logger.info(`Skipping ${candidate.title}: open PR exists (${via})`);
|
|
1490
1594
|
} else {
|
|
1491
1595
|
filtered.push(candidate);
|
|
@@ -2238,7 +2342,6 @@ var WorkflowLoader = class {
|
|
|
2238
2342
|
};
|
|
2239
2343
|
|
|
2240
2344
|
// src/tracker/adapters/roadmap.ts
|
|
2241
|
-
var fs8 = __toESM(require("fs/promises"));
|
|
2242
2345
|
var import_node_crypto2 = require("crypto");
|
|
2243
2346
|
var import_core = require("@harness-engineering/core");
|
|
2244
2347
|
var import_types4 = require("@harness-engineering/types");
|
|
@@ -2269,8 +2372,7 @@ var RoadmapTrackerAdapter = class {
|
|
|
2269
2372
|
async fetchIssuesByStates(stateNames) {
|
|
2270
2373
|
try {
|
|
2271
2374
|
if (!this.config.filePath) return (0, import_types4.Err)(new Error("Missing filePath"));
|
|
2272
|
-
const
|
|
2273
|
-
const roadmapResult = (0, import_core.parseRoadmap)(content);
|
|
2375
|
+
const roadmapResult = await this.loadRoadmap();
|
|
2274
2376
|
if (!roadmapResult.ok) return roadmapResult;
|
|
2275
2377
|
const issues = [];
|
|
2276
2378
|
for (const milestone of roadmapResult.value.milestones) {
|
|
@@ -2301,16 +2403,19 @@ var RoadmapTrackerAdapter = class {
|
|
|
2301
2403
|
if (!terminal) {
|
|
2302
2404
|
return (0, import_types4.Err)(new Error("Tracker config has no terminalStates; cannot mark complete"));
|
|
2303
2405
|
}
|
|
2304
|
-
const
|
|
2305
|
-
const roadmapResult =
|
|
2406
|
+
const store = this.resolveStore();
|
|
2407
|
+
const roadmapResult = await store.load();
|
|
2306
2408
|
if (!roadmapResult.ok) return roadmapResult;
|
|
2307
2409
|
const roadmap = roadmapResult.value;
|
|
2410
|
+
const before = structuredClone(roadmap);
|
|
2308
2411
|
const target = this.findFeatureById(roadmap.milestones, issueId);
|
|
2309
2412
|
if (!target) return (0, import_types4.Ok)(void 0);
|
|
2310
2413
|
const normalizedTerminal = this.config.terminalStates.map((s) => s.toLowerCase());
|
|
2311
2414
|
if (normalizedTerminal.includes(target.status.toLowerCase())) return (0, import_types4.Ok)(void 0);
|
|
2312
|
-
|
|
2313
|
-
|
|
2415
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2416
|
+
(0, import_core.setStatus)(roadmap, target, terminal, now.slice(0, 10));
|
|
2417
|
+
const persisted = await (0, import_core.applyRoadmapDiff)(store, before, roadmap);
|
|
2418
|
+
if (!persisted.ok) return persisted;
|
|
2314
2419
|
return (0, import_types4.Ok)(void 0);
|
|
2315
2420
|
} catch (error) {
|
|
2316
2421
|
return (0, import_types4.Err)(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -2324,22 +2429,24 @@ var RoadmapTrackerAdapter = class {
|
|
|
2324
2429
|
async claimIssue(issueId, orchestratorId) {
|
|
2325
2430
|
try {
|
|
2326
2431
|
if (!this.config.filePath) return (0, import_types4.Err)(new Error("Missing filePath"));
|
|
2327
|
-
const
|
|
2328
|
-
const roadmapResult =
|
|
2432
|
+
const store = this.resolveStore();
|
|
2433
|
+
const roadmapResult = await store.load();
|
|
2329
2434
|
if (!roadmapResult.ok) return roadmapResult;
|
|
2330
2435
|
const roadmap = roadmapResult.value;
|
|
2436
|
+
const before = structuredClone(roadmap);
|
|
2331
2437
|
const target = this.findFeatureById(roadmap.milestones, issueId);
|
|
2332
2438
|
if (!target) return (0, import_types4.Ok)(void 0);
|
|
2333
|
-
if (
|
|
2439
|
+
if (!(0, import_core.isClaimableBy)(target, orchestratorId)) {
|
|
2334
2440
|
return (0, import_types4.Ok)(void 0);
|
|
2335
2441
|
}
|
|
2336
2442
|
if (target.status === "in-progress" && target.assignee === orchestratorId) {
|
|
2337
2443
|
return (0, import_types4.Ok)(void 0);
|
|
2338
2444
|
}
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
target.updatedAt =
|
|
2342
|
-
await
|
|
2445
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2446
|
+
(0, import_core.claim)(roadmap, target, orchestratorId, now.slice(0, 10));
|
|
2447
|
+
target.updatedAt = now;
|
|
2448
|
+
const persisted = await (0, import_core.applyRoadmapDiff)(store, before, roadmap);
|
|
2449
|
+
if (!persisted.ok) return persisted;
|
|
2343
2450
|
return (0, import_types4.Ok)(void 0);
|
|
2344
2451
|
} catch (error) {
|
|
2345
2452
|
return (0, import_types4.Err)(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -2356,24 +2463,39 @@ var RoadmapTrackerAdapter = class {
|
|
|
2356
2463
|
if (!activeState) {
|
|
2357
2464
|
return (0, import_types4.Err)(new Error("Tracker config has no activeStates; cannot release"));
|
|
2358
2465
|
}
|
|
2359
|
-
const
|
|
2360
|
-
const roadmapResult =
|
|
2466
|
+
const store = this.resolveStore();
|
|
2467
|
+
const roadmapResult = await store.load();
|
|
2361
2468
|
if (!roadmapResult.ok) return roadmapResult;
|
|
2362
2469
|
const roadmap = roadmapResult.value;
|
|
2470
|
+
const before = structuredClone(roadmap);
|
|
2363
2471
|
const target = this.findFeatureById(roadmap.milestones, issueId);
|
|
2364
2472
|
if (!target) return (0, import_types4.Ok)(void 0);
|
|
2365
2473
|
if (this.config.activeStates.includes(target.status) && target.assignee === null) {
|
|
2366
2474
|
return (0, import_types4.Ok)(void 0);
|
|
2367
2475
|
}
|
|
2368
|
-
|
|
2369
|
-
target.
|
|
2476
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2477
|
+
(0, import_core.setStatus)(roadmap, target, activeState, now.slice(0, 10));
|
|
2370
2478
|
target.updatedAt = null;
|
|
2371
|
-
await
|
|
2479
|
+
const persisted = await (0, import_core.applyRoadmapDiff)(store, before, roadmap);
|
|
2480
|
+
if (!persisted.ok) return persisted;
|
|
2372
2481
|
return (0, import_types4.Ok)(void 0);
|
|
2373
2482
|
} catch (error) {
|
|
2374
2483
|
return (0, import_types4.Err)(error instanceof Error ? error : new Error(String(error)));
|
|
2375
2484
|
}
|
|
2376
2485
|
}
|
|
2486
|
+
/**
|
|
2487
|
+
* Resolve the roadmap store anchored on the configured aggregate file path.
|
|
2488
|
+
* The shard backend is chosen when a sibling `roadmap.d/` exists next to the
|
|
2489
|
+
* file; otherwise the monolith file is read/written whole. Callers guard
|
|
2490
|
+
* `this.config.filePath` before invoking.
|
|
2491
|
+
*/
|
|
2492
|
+
resolveStore() {
|
|
2493
|
+
return (0, import_core.resolveRoadmapStoreForFile)({ roadmapPath: this.config.filePath });
|
|
2494
|
+
}
|
|
2495
|
+
/** Load the roadmap via the store (sharded or monolith), returning a Result. */
|
|
2496
|
+
loadRoadmap() {
|
|
2497
|
+
return this.resolveStore().load();
|
|
2498
|
+
}
|
|
2377
2499
|
findFeatureById(milestones, issueId) {
|
|
2378
2500
|
for (const milestone of milestones) {
|
|
2379
2501
|
for (const feature of milestone.features) {
|
|
@@ -2390,8 +2512,7 @@ var RoadmapTrackerAdapter = class {
|
|
|
2390
2512
|
async fetchIssueStatesByIds(issueIds) {
|
|
2391
2513
|
try {
|
|
2392
2514
|
if (!this.config.filePath) return (0, import_types4.Err)(new Error("Missing filePath"));
|
|
2393
|
-
const
|
|
2394
|
-
const roadmapResult = (0, import_core.parseRoadmap)(content);
|
|
2515
|
+
const roadmapResult = await this.loadRoadmap();
|
|
2395
2516
|
if (!roadmapResult.ok) return roadmapResult;
|
|
2396
2517
|
const issueMap = /* @__PURE__ */ new Map();
|
|
2397
2518
|
for (const milestone of roadmapResult.value.milestones) {
|
|
@@ -2447,6 +2568,56 @@ var RoadmapTrackerAdapter = class {
|
|
|
2447
2568
|
|
|
2448
2569
|
// src/tracker/extensions/linear.ts
|
|
2449
2570
|
var import_types5 = require("@harness-engineering/types");
|
|
2571
|
+
var LINEAR_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql";
|
|
2572
|
+
var LinearGraphQLClient = class {
|
|
2573
|
+
apiKey;
|
|
2574
|
+
endpoint;
|
|
2575
|
+
fetchFn;
|
|
2576
|
+
constructor(opts) {
|
|
2577
|
+
this.apiKey = opts.apiKey;
|
|
2578
|
+
this.endpoint = opts.endpoint ?? LINEAR_GRAPHQL_ENDPOINT;
|
|
2579
|
+
this.fetchFn = opts.fetchFn ?? globalThis.fetch;
|
|
2580
|
+
}
|
|
2581
|
+
async query(query, variables) {
|
|
2582
|
+
let res;
|
|
2583
|
+
try {
|
|
2584
|
+
res = await this.fetchFn(this.endpoint, {
|
|
2585
|
+
method: "POST",
|
|
2586
|
+
headers: {
|
|
2587
|
+
Authorization: this.apiKey,
|
|
2588
|
+
"Content-Type": "application/json"
|
|
2589
|
+
},
|
|
2590
|
+
body: JSON.stringify({ query, variables: variables ?? {} })
|
|
2591
|
+
});
|
|
2592
|
+
} catch (err) {
|
|
2593
|
+
return (0, import_types5.Err)(
|
|
2594
|
+
new Error(
|
|
2595
|
+
`Linear GraphQL request failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2596
|
+
)
|
|
2597
|
+
);
|
|
2598
|
+
}
|
|
2599
|
+
if (!res.ok) {
|
|
2600
|
+
const body = await res.text().catch(() => "");
|
|
2601
|
+
const detail = body ? `: ${body.slice(0, 500)}` : "";
|
|
2602
|
+
return (0, import_types5.Err)(new Error(`Linear GraphQL HTTP ${res.status}${detail}`));
|
|
2603
|
+
}
|
|
2604
|
+
let envelope;
|
|
2605
|
+
try {
|
|
2606
|
+
envelope = await res.json();
|
|
2607
|
+
} catch (err) {
|
|
2608
|
+
return (0, import_types5.Err)(
|
|
2609
|
+
new Error(
|
|
2610
|
+
`Linear GraphQL response was not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
2611
|
+
)
|
|
2612
|
+
);
|
|
2613
|
+
}
|
|
2614
|
+
if (envelope.errors && envelope.errors.length > 0) {
|
|
2615
|
+
const message = envelope.errors.map((e) => e.message ?? "unknown error").join("; ");
|
|
2616
|
+
return (0, import_types5.Err)(new Error(`Linear GraphQL error: ${message}`));
|
|
2617
|
+
}
|
|
2618
|
+
return (0, import_types5.Ok)(envelope.data ?? {});
|
|
2619
|
+
}
|
|
2620
|
+
};
|
|
2450
2621
|
var LinearGraphQLStub = class {
|
|
2451
2622
|
async query(query, _variables) {
|
|
2452
2623
|
console.log("Linear GraphQL query (stub):", query);
|
|
@@ -2455,7 +2626,7 @@ var LinearGraphQLStub = class {
|
|
|
2455
2626
|
};
|
|
2456
2627
|
|
|
2457
2628
|
// src/workspace/manager.ts
|
|
2458
|
-
var
|
|
2629
|
+
var fs8 = __toESM(require("fs/promises"));
|
|
2459
2630
|
var path8 = __toESM(require("path"));
|
|
2460
2631
|
var import_node_child_process2 = require("child_process");
|
|
2461
2632
|
var import_node_util2 = require("util");
|
|
@@ -2495,7 +2666,7 @@ var WorkspaceManager = class _WorkspaceManager {
|
|
|
2495
2666
|
async getRepoRoot() {
|
|
2496
2667
|
if (this.repoRoot) return this.repoRoot;
|
|
2497
2668
|
const root = path8.resolve(this.config.root);
|
|
2498
|
-
await
|
|
2669
|
+
await fs8.mkdir(root, { recursive: true });
|
|
2499
2670
|
const stdout = await this.git(["rev-parse", "--show-toplevel"], root);
|
|
2500
2671
|
this.repoRoot = stdout.trim();
|
|
2501
2672
|
return this.repoRoot;
|
|
@@ -2508,21 +2679,21 @@ var WorkspaceManager = class _WorkspaceManager {
|
|
|
2508
2679
|
try {
|
|
2509
2680
|
const workspacePath = path8.resolve(this.resolvePath(identifier));
|
|
2510
2681
|
try {
|
|
2511
|
-
await
|
|
2682
|
+
await fs8.access(path8.join(workspacePath, ".git"));
|
|
2512
2683
|
const repoRoot2 = await this.getRepoRoot();
|
|
2513
2684
|
try {
|
|
2514
2685
|
await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
|
|
2515
2686
|
} catch {
|
|
2516
|
-
await
|
|
2687
|
+
await fs8.rm(workspacePath, { recursive: true, force: true });
|
|
2517
2688
|
}
|
|
2518
2689
|
} catch {
|
|
2519
2690
|
try {
|
|
2520
|
-
await
|
|
2691
|
+
await fs8.access(workspacePath);
|
|
2521
2692
|
const repoRoot2 = await this.getRepoRoot();
|
|
2522
2693
|
try {
|
|
2523
2694
|
await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
|
|
2524
2695
|
} catch {
|
|
2525
|
-
await
|
|
2696
|
+
await fs8.rm(workspacePath, { recursive: true, force: true });
|
|
2526
2697
|
}
|
|
2527
2698
|
} catch {
|
|
2528
2699
|
}
|
|
@@ -2575,13 +2746,13 @@ var WorkspaceManager = class _WorkspaceManager {
|
|
|
2575
2746
|
}
|
|
2576
2747
|
const src = path8.join(repoRoot, rel);
|
|
2577
2748
|
try {
|
|
2578
|
-
await
|
|
2749
|
+
await fs8.access(src);
|
|
2579
2750
|
} catch {
|
|
2580
2751
|
continue;
|
|
2581
2752
|
}
|
|
2582
2753
|
const dest = path8.join(workspacePath, rel);
|
|
2583
2754
|
try {
|
|
2584
|
-
await
|
|
2755
|
+
await fs8.cp(src, dest, { recursive: true, force: true });
|
|
2585
2756
|
} catch {
|
|
2586
2757
|
}
|
|
2587
2758
|
}
|
|
@@ -2657,7 +2828,7 @@ var WorkspaceManager = class _WorkspaceManager {
|
|
|
2657
2828
|
async exists(identifier) {
|
|
2658
2829
|
try {
|
|
2659
2830
|
const workspacePath = this.resolvePath(identifier);
|
|
2660
|
-
await
|
|
2831
|
+
await fs8.access(workspacePath);
|
|
2661
2832
|
return true;
|
|
2662
2833
|
} catch {
|
|
2663
2834
|
return false;
|
|
@@ -2672,7 +2843,7 @@ var WorkspaceManager = class _WorkspaceManager {
|
|
|
2672
2843
|
try {
|
|
2673
2844
|
const workspacePath = path8.resolve(this.resolvePath(identifier));
|
|
2674
2845
|
try {
|
|
2675
|
-
await
|
|
2846
|
+
await fs8.access(path8.join(workspacePath, ".git"));
|
|
2676
2847
|
} catch {
|
|
2677
2848
|
return null;
|
|
2678
2849
|
}
|
|
@@ -2783,7 +2954,7 @@ var WorkspaceManager = class _WorkspaceManager {
|
|
|
2783
2954
|
const repoRoot = await this.getRepoRoot();
|
|
2784
2955
|
await this.git(["worktree", "remove", "--force", workspacePath], repoRoot);
|
|
2785
2956
|
} catch {
|
|
2786
|
-
await
|
|
2957
|
+
await fs8.rm(workspacePath, { recursive: true, force: true });
|
|
2787
2958
|
}
|
|
2788
2959
|
return (0, import_types6.Ok)(void 0);
|
|
2789
2960
|
} catch (error) {
|
|
@@ -2924,8 +3095,8 @@ var PromptRenderer = class {
|
|
|
2924
3095
|
// src/orchestrator.ts
|
|
2925
3096
|
var import_node_events = require("events");
|
|
2926
3097
|
var path21 = __toESM(require("path"));
|
|
2927
|
-
var
|
|
2928
|
-
var
|
|
3098
|
+
var import_node_crypto15 = require("crypto");
|
|
3099
|
+
var import_core15 = require("@harness-engineering/core");
|
|
2929
3100
|
|
|
2930
3101
|
// src/core/stall-detector.ts
|
|
2931
3102
|
function detectStalledIssues(running, nowMs, stallTimeoutMs) {
|
|
@@ -3501,7 +3672,7 @@ var CompletionHandler = class {
|
|
|
3501
3672
|
};
|
|
3502
3673
|
|
|
3503
3674
|
// src/orchestrator.ts
|
|
3504
|
-
var
|
|
3675
|
+
var import_core16 = require("@harness-engineering/core");
|
|
3505
3676
|
|
|
3506
3677
|
// src/tracker/adapters/github-issues-issue-tracker.ts
|
|
3507
3678
|
var import_types9 = require("@harness-engineering/types");
|
|
@@ -6421,6 +6592,96 @@ var OrchestratorBackendFactory = class {
|
|
|
6421
6592
|
}
|
|
6422
6593
|
};
|
|
6423
6594
|
|
|
6595
|
+
// src/agent/backend-resolver.ts
|
|
6596
|
+
function makeBackendResolver(backends) {
|
|
6597
|
+
return (backendName) => {
|
|
6598
|
+
const def = backends?.[backendName];
|
|
6599
|
+
return def ? createBackend(def) : null;
|
|
6600
|
+
};
|
|
6601
|
+
}
|
|
6602
|
+
|
|
6603
|
+
// src/maintenance/agent-dispatcher.ts
|
|
6604
|
+
function syntheticIssue(branch, skill) {
|
|
6605
|
+
return {
|
|
6606
|
+
id: `maintenance:${branch}`,
|
|
6607
|
+
identifier: branch,
|
|
6608
|
+
title: `Maintenance: ${skill}`,
|
|
6609
|
+
description: null,
|
|
6610
|
+
priority: null,
|
|
6611
|
+
state: "in_progress",
|
|
6612
|
+
branchName: branch,
|
|
6613
|
+
url: null,
|
|
6614
|
+
labels: ["maintenance"],
|
|
6615
|
+
blockedBy: [],
|
|
6616
|
+
spec: null,
|
|
6617
|
+
plans: [],
|
|
6618
|
+
createdAt: null,
|
|
6619
|
+
updatedAt: null,
|
|
6620
|
+
externalId: null
|
|
6621
|
+
};
|
|
6622
|
+
}
|
|
6623
|
+
function buildPrompt(skill, promptContext) {
|
|
6624
|
+
const instruction = `Run the \`${skill}\` skill to detect and fix issues in this repository. Apply the fixes and commit each logical change. If there is nothing to fix, make no commits.`;
|
|
6625
|
+
return promptContext ? `${promptContext}
|
|
6626
|
+
|
|
6627
|
+
${instruction}` : instruction;
|
|
6628
|
+
}
|
|
6629
|
+
function headOf(git2, cwd) {
|
|
6630
|
+
try {
|
|
6631
|
+
return git2(["rev-parse", "HEAD"], cwd) || null;
|
|
6632
|
+
} catch {
|
|
6633
|
+
return null;
|
|
6634
|
+
}
|
|
6635
|
+
}
|
|
6636
|
+
function createAgentDispatcher(deps) {
|
|
6637
|
+
const maxTurns = deps.maxTurns ?? 10;
|
|
6638
|
+
const dispatch = async (skill, branch, backendName, cwd, options) => {
|
|
6639
|
+
const backend = deps.resolveBackend(backendName);
|
|
6640
|
+
if (!backend) {
|
|
6641
|
+
deps.logger.warn(`Maintenance agent dispatch skipped: unknown backend "${backendName}"`, {
|
|
6642
|
+
skill,
|
|
6643
|
+
branch
|
|
6644
|
+
});
|
|
6645
|
+
return { producedCommits: false, fixed: 0 };
|
|
6646
|
+
}
|
|
6647
|
+
const before = headOf(deps.git, cwd);
|
|
6648
|
+
const runner = new AgentRunner(backend, { maxTurns });
|
|
6649
|
+
const session = runner.runSession(
|
|
6650
|
+
syntheticIssue(branch, skill),
|
|
6651
|
+
cwd,
|
|
6652
|
+
buildPrompt(skill, options?.promptContext)
|
|
6653
|
+
);
|
|
6654
|
+
let step = await session.next();
|
|
6655
|
+
while (!step.done) step = await session.next();
|
|
6656
|
+
const after = headOf(deps.git, cwd);
|
|
6657
|
+
let fixed = 0;
|
|
6658
|
+
if (after && after !== before) {
|
|
6659
|
+
if (before) {
|
|
6660
|
+
try {
|
|
6661
|
+
fixed = parseInt(deps.git(["rev-list", "--count", `${before}..${after}`], cwd), 10) || 0;
|
|
6662
|
+
} catch {
|
|
6663
|
+
fixed = 1;
|
|
6664
|
+
}
|
|
6665
|
+
} else {
|
|
6666
|
+
fixed = 1;
|
|
6667
|
+
}
|
|
6668
|
+
}
|
|
6669
|
+
const producedCommits = fixed > 0;
|
|
6670
|
+
deps.logger.info("Maintenance agent dispatch complete", {
|
|
6671
|
+
skill,
|
|
6672
|
+
branch,
|
|
6673
|
+
backendName,
|
|
6674
|
+
producedCommits,
|
|
6675
|
+
fixed
|
|
6676
|
+
});
|
|
6677
|
+
return { producedCommits, fixed };
|
|
6678
|
+
};
|
|
6679
|
+
return { dispatch };
|
|
6680
|
+
}
|
|
6681
|
+
|
|
6682
|
+
// src/orchestrator.ts
|
|
6683
|
+
var import_node_child_process13 = require("child_process");
|
|
6684
|
+
|
|
6424
6685
|
// src/agent/intelligence-factory.ts
|
|
6425
6686
|
var import_intelligence3 = require("@harness-engineering/intelligence");
|
|
6426
6687
|
var import_graph = require("@harness-engineering/graph");
|
|
@@ -6922,7 +7183,7 @@ function handleV1InteractionsResolveRoute(req, res, queue) {
|
|
|
6922
7183
|
|
|
6923
7184
|
// src/server/routes/plans.ts
|
|
6924
7185
|
var import_zod5 = require("zod");
|
|
6925
|
-
var
|
|
7186
|
+
var fs9 = __toESM(require("fs/promises"));
|
|
6926
7187
|
var path11 = __toESM(require("path"));
|
|
6927
7188
|
var PlanWriteSchema = import_zod5.z.object({
|
|
6928
7189
|
filename: import_zod5.z.string().min(1),
|
|
@@ -6951,9 +7212,9 @@ function handlePlansRoute(req, res, plansDir) {
|
|
|
6951
7212
|
);
|
|
6952
7213
|
return;
|
|
6953
7214
|
}
|
|
6954
|
-
await
|
|
7215
|
+
await fs9.mkdir(plansDir, { recursive: true });
|
|
6955
7216
|
const filePath = path11.join(plansDir, basename3);
|
|
6956
|
-
await
|
|
7217
|
+
await fs9.writeFile(filePath, parsed.content, "utf-8");
|
|
6957
7218
|
res.writeHead(201, { "Content-Type": "application/json" });
|
|
6958
7219
|
res.end(JSON.stringify({ ok: true, filename: basename3 }));
|
|
6959
7220
|
} catch {
|
|
@@ -7328,7 +7589,6 @@ function handleAnalyzeRoute(req, res, pipeline) {
|
|
|
7328
7589
|
}
|
|
7329
7590
|
|
|
7330
7591
|
// src/server/routes/roadmap-actions.ts
|
|
7331
|
-
var fs11 = __toESM(require("fs/promises"));
|
|
7332
7592
|
var path12 = __toESM(require("path"));
|
|
7333
7593
|
var import_core7 = require("@harness-engineering/core");
|
|
7334
7594
|
var import_zod8 = require("zod");
|
|
@@ -7410,13 +7670,14 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
7410
7670
|
sendJSON2(res, 400, { error: "Title must not contain newlines or markdown headings" });
|
|
7411
7671
|
return;
|
|
7412
7672
|
}
|
|
7413
|
-
const
|
|
7414
|
-
const
|
|
7415
|
-
if (!
|
|
7673
|
+
const store = (0, import_core7.resolveRoadmapStoreForFile)({ roadmapPath });
|
|
7674
|
+
const loaded = await store.load();
|
|
7675
|
+
if (!loaded.ok) {
|
|
7416
7676
|
sendJSON2(res, 500, { error: "Failed to parse roadmap file" });
|
|
7417
7677
|
return;
|
|
7418
7678
|
}
|
|
7419
|
-
const roadmap =
|
|
7679
|
+
const roadmap = loaded.value;
|
|
7680
|
+
const before = structuredClone(roadmap);
|
|
7420
7681
|
let backlog = roadmap.milestones.find((m) => m.isBacklog);
|
|
7421
7682
|
if (!backlog) {
|
|
7422
7683
|
backlog = { name: "Backlog", isBacklog: true, features: [] };
|
|
@@ -7439,10 +7700,11 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
7439
7700
|
updatedAt: null
|
|
7440
7701
|
});
|
|
7441
7702
|
roadmap.frontmatter.lastManualEdit = (/* @__PURE__ */ new Date()).toISOString();
|
|
7442
|
-
const
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7703
|
+
const persisted = await (0, import_core7.applyRoadmapDiff)(store, before, roadmap);
|
|
7704
|
+
if (!persisted.ok) {
|
|
7705
|
+
sendJSON2(res, 500, { error: persisted.error.message });
|
|
7706
|
+
return;
|
|
7707
|
+
}
|
|
7446
7708
|
sendJSON2(res, 201, { ok: true, featureName: parsed.title });
|
|
7447
7709
|
} catch (err) {
|
|
7448
7710
|
const msg = err instanceof Error ? err.message : "Failed to append to roadmap";
|
|
@@ -7708,7 +7970,6 @@ function handleV1JobsMaintenanceRoute(req, res, deps) {
|
|
|
7708
7970
|
}
|
|
7709
7971
|
|
|
7710
7972
|
// src/server/routes/v1/events-sse.ts
|
|
7711
|
-
var import_node_crypto8 = require("crypto");
|
|
7712
7973
|
var SSE_TOPICS = [
|
|
7713
7974
|
"state_change",
|
|
7714
7975
|
"agent_event",
|
|
@@ -7724,8 +7985,55 @@ var SSE_TOPICS = [
|
|
|
7724
7985
|
"webhook.subscription.deleted"
|
|
7725
7986
|
];
|
|
7726
7987
|
var HEARTBEAT_MS = 15e3;
|
|
7727
|
-
|
|
7728
|
-
|
|
7988
|
+
var DEFAULT_REPLAY_BUFFER = 1024;
|
|
7989
|
+
var SseEventLog = class {
|
|
7990
|
+
seq = 0;
|
|
7991
|
+
buffer = [];
|
|
7992
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
7993
|
+
constructor(bus, cap = DEFAULT_REPLAY_BUFFER) {
|
|
7994
|
+
this.cap = cap;
|
|
7995
|
+
for (const topic of SSE_TOPICS) {
|
|
7996
|
+
bus.on(topic, (data) => this.record(topic, data));
|
|
7997
|
+
}
|
|
7998
|
+
}
|
|
7999
|
+
cap;
|
|
8000
|
+
record(topic, data) {
|
|
8001
|
+
const event = { id: ++this.seq, topic, data };
|
|
8002
|
+
this.buffer.push(event);
|
|
8003
|
+
if (this.buffer.length > this.cap) this.buffer.shift();
|
|
8004
|
+
for (const fn of this.subscribers) fn(event);
|
|
8005
|
+
}
|
|
8006
|
+
/** Highest assigned id (0 when nothing has been recorded yet). */
|
|
8007
|
+
currentSeq() {
|
|
8008
|
+
return this.seq;
|
|
8009
|
+
}
|
|
8010
|
+
/** Buffered events strictly newer than `lastId`, oldest-first. */
|
|
8011
|
+
replayFrom(lastId) {
|
|
8012
|
+
return this.buffer.filter((e) => e.id > lastId);
|
|
8013
|
+
}
|
|
8014
|
+
/** Register a live listener; returns an unsubscribe function. */
|
|
8015
|
+
subscribe(fn) {
|
|
8016
|
+
this.subscribers.add(fn);
|
|
8017
|
+
return () => {
|
|
8018
|
+
this.subscribers.delete(fn);
|
|
8019
|
+
};
|
|
8020
|
+
}
|
|
8021
|
+
};
|
|
8022
|
+
var logsByBus = /* @__PURE__ */ new WeakMap();
|
|
8023
|
+
function getSseEventLog(bus) {
|
|
8024
|
+
let log = logsByBus.get(bus);
|
|
8025
|
+
if (!log) {
|
|
8026
|
+
log = new SseEventLog(bus);
|
|
8027
|
+
logsByBus.set(bus, log);
|
|
8028
|
+
}
|
|
8029
|
+
return log;
|
|
8030
|
+
}
|
|
8031
|
+
function parseLastEventId(req) {
|
|
8032
|
+
const raw = req.headers["last-event-id"];
|
|
8033
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
8034
|
+
if (value === void 0) return null;
|
|
8035
|
+
const n = Number(value);
|
|
8036
|
+
return Number.isInteger(n) && n >= 0 ? n : null;
|
|
7729
8037
|
}
|
|
7730
8038
|
function handleV1EventsSseRoute(req, res, bus) {
|
|
7731
8039
|
if (req.method !== "GET" || req.url !== "/api/v1/events") return false;
|
|
@@ -7737,22 +8045,28 @@ function handleV1EventsSseRoute(req, res, bus) {
|
|
|
7737
8045
|
res.write(`: harness gateway SSE \u2014 connected at ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
7738
8046
|
|
|
7739
8047
|
`);
|
|
7740
|
-
const
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
id: ${newEventId()}
|
|
8048
|
+
const log = getSseEventLog(bus);
|
|
8049
|
+
const send = (e) => {
|
|
8050
|
+
try {
|
|
8051
|
+
res.write(`event: ${e.topic}
|
|
8052
|
+
data: ${JSON.stringify(e.data)}
|
|
8053
|
+
id: ${e.id}
|
|
7747
8054
|
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
8055
|
+
`);
|
|
8056
|
+
} catch {
|
|
8057
|
+
}
|
|
8058
|
+
};
|
|
8059
|
+
const lastId = parseLastEventId(req);
|
|
8060
|
+
let replayedThrough = lastId ?? log.currentSeq();
|
|
8061
|
+
if (lastId !== null) {
|
|
8062
|
+
for (const e of log.replayFrom(lastId)) {
|
|
8063
|
+
send(e);
|
|
8064
|
+
replayedThrough = e.id;
|
|
8065
|
+
}
|
|
7755
8066
|
}
|
|
8067
|
+
const unsubscribe = log.subscribe((e) => {
|
|
8068
|
+
if (e.id > replayedThrough) send(e);
|
|
8069
|
+
});
|
|
7756
8070
|
const heartbeat = setInterval(() => {
|
|
7757
8071
|
try {
|
|
7758
8072
|
res.write(": heartbeat\n\n");
|
|
@@ -7762,7 +8076,7 @@ id: ${newEventId()}
|
|
|
7762
8076
|
heartbeat.unref();
|
|
7763
8077
|
const cleanup = () => {
|
|
7764
8078
|
clearInterval(heartbeat);
|
|
7765
|
-
|
|
8079
|
+
unsubscribe();
|
|
7766
8080
|
};
|
|
7767
8081
|
res.on("close", cleanup);
|
|
7768
8082
|
res.on("finish", cleanup);
|
|
@@ -8071,7 +8385,7 @@ async function runGate(projectPath, proposalId) {
|
|
|
8071
8385
|
}
|
|
8072
8386
|
|
|
8073
8387
|
// src/proposals/promote.ts
|
|
8074
|
-
var
|
|
8388
|
+
var fs10 = __toESM(require("fs"));
|
|
8075
8389
|
var path13 = __toESM(require("path"));
|
|
8076
8390
|
var import_yaml4 = require("yaml");
|
|
8077
8391
|
var import_core9 = require("@harness-engineering/core");
|
|
@@ -8093,7 +8407,7 @@ function skillDir(projectPath, name) {
|
|
|
8093
8407
|
}
|
|
8094
8408
|
function readIfExists(p) {
|
|
8095
8409
|
try {
|
|
8096
|
-
return
|
|
8410
|
+
return fs10.readFileSync(p, "utf-8");
|
|
8097
8411
|
} catch {
|
|
8098
8412
|
return null;
|
|
8099
8413
|
}
|
|
@@ -8139,15 +8453,15 @@ function assertGateReady(proposal) {
|
|
|
8139
8453
|
}
|
|
8140
8454
|
async function promoteNewSkill(projectPath, proposal) {
|
|
8141
8455
|
const target = skillDir(projectPath, proposal.content.name);
|
|
8142
|
-
if (
|
|
8456
|
+
if (fs10.existsSync(target)) {
|
|
8143
8457
|
throw new PromotionError(
|
|
8144
8458
|
`a catalog skill already exists at ${target}; use a refinement proposal to update it`
|
|
8145
8459
|
);
|
|
8146
8460
|
}
|
|
8147
|
-
|
|
8461
|
+
fs10.mkdirSync(target, { recursive: true });
|
|
8148
8462
|
const yamlOut = injectProvenanceIntoYaml(proposal.content.skillYaml ?? "", proposal.id);
|
|
8149
|
-
|
|
8150
|
-
|
|
8463
|
+
fs10.writeFileSync(path13.join(target, "skill.yaml"), yamlOut);
|
|
8464
|
+
fs10.writeFileSync(path13.join(target, "SKILL.md"), proposal.content.skillMd ?? "");
|
|
8151
8465
|
return { skillPath: target };
|
|
8152
8466
|
}
|
|
8153
8467
|
async function promoteRefinement(projectPath, proposal) {
|
|
@@ -8155,7 +8469,7 @@ async function promoteRefinement(projectPath, proposal) {
|
|
|
8155
8469
|
throw new PromotionError("refinement proposal is missing targetSkill");
|
|
8156
8470
|
}
|
|
8157
8471
|
const target = skillDir(projectPath, proposal.targetSkill);
|
|
8158
|
-
if (!
|
|
8472
|
+
if (!fs10.existsSync(target)) {
|
|
8159
8473
|
throw new PromotionError(
|
|
8160
8474
|
`target skill ${proposal.targetSkill} does not exist at ${target}; cannot refine`
|
|
8161
8475
|
);
|
|
@@ -8168,7 +8482,7 @@ async function promoteRefinement(projectPath, proposal) {
|
|
|
8168
8482
|
"no metadata changes detected; check that the reviewer applied the proposed diff before approving"
|
|
8169
8483
|
);
|
|
8170
8484
|
}
|
|
8171
|
-
|
|
8485
|
+
fs10.writeFileSync(yamlPath, after);
|
|
8172
8486
|
return { skillPath: target };
|
|
8173
8487
|
}
|
|
8174
8488
|
async function promote(projectPath, proposalId, decidedBy) {
|
|
@@ -8606,7 +8920,7 @@ function handleV1RoutingRoute(req, res, deps) {
|
|
|
8606
8920
|
}
|
|
8607
8921
|
|
|
8608
8922
|
// src/server/routes/sessions.ts
|
|
8609
|
-
var
|
|
8923
|
+
var fs11 = __toESM(require("fs/promises"));
|
|
8610
8924
|
var path14 = __toESM(require("path"));
|
|
8611
8925
|
var import_zod15 = require("zod");
|
|
8612
8926
|
var SessionCreateSchema = import_zod15.z.object({
|
|
@@ -8627,12 +8941,12 @@ function extractSessionId(url) {
|
|
|
8627
8941
|
}
|
|
8628
8942
|
async function handleList2(res, sessionsDir) {
|
|
8629
8943
|
try {
|
|
8630
|
-
const entries = await
|
|
8944
|
+
const entries = await fs11.readdir(sessionsDir, { withFileTypes: true });
|
|
8631
8945
|
const sessions = [];
|
|
8632
8946
|
for (const entry of entries) {
|
|
8633
8947
|
if (!entry.isDirectory()) continue;
|
|
8634
8948
|
try {
|
|
8635
|
-
const content = await
|
|
8949
|
+
const content = await fs11.readFile(
|
|
8636
8950
|
path14.join(sessionsDir, entry.name, "session.json"),
|
|
8637
8951
|
"utf-8"
|
|
8638
8952
|
);
|
|
@@ -8658,7 +8972,7 @@ async function handleGet2(res, id, sessionsDir) {
|
|
|
8658
8972
|
return;
|
|
8659
8973
|
}
|
|
8660
8974
|
try {
|
|
8661
|
-
const content = await
|
|
8975
|
+
const content = await fs11.readFile(path14.join(sessionsDir, id, "session.json"), "utf-8");
|
|
8662
8976
|
jsonResponse(res, 200, JSON.parse(content));
|
|
8663
8977
|
} catch (err) {
|
|
8664
8978
|
if (err.code === "ENOENT") {
|
|
@@ -8682,8 +8996,8 @@ async function handleCreate(req, res, sessionsDir) {
|
|
|
8682
8996
|
return;
|
|
8683
8997
|
}
|
|
8684
8998
|
const sessionDir = path14.join(sessionsDir, session.sessionId);
|
|
8685
|
-
await
|
|
8686
|
-
await
|
|
8999
|
+
await fs11.mkdir(sessionDir, { recursive: true });
|
|
9000
|
+
await fs11.writeFile(path14.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
|
|
8687
9001
|
jsonResponse(res, 200, { ok: true });
|
|
8688
9002
|
} catch {
|
|
8689
9003
|
jsonResponse(res, 500, { error: "Failed to save session" });
|
|
@@ -8699,8 +9013,8 @@ async function handleUpdate(req, res, url, sessionsDir) {
|
|
|
8699
9013
|
const body = await readBody(req);
|
|
8700
9014
|
const updates = import_zod15.z.record(import_zod15.z.unknown()).parse(JSON.parse(body));
|
|
8701
9015
|
const sessionFilePath = path14.join(sessionsDir, id, "session.json");
|
|
8702
|
-
const current = JSON.parse(await
|
|
8703
|
-
await
|
|
9016
|
+
const current = JSON.parse(await fs11.readFile(sessionFilePath, "utf-8"));
|
|
9017
|
+
await fs11.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
|
|
8704
9018
|
jsonResponse(res, 200, { ok: true });
|
|
8705
9019
|
} catch {
|
|
8706
9020
|
jsonResponse(res, 500, { error: "Failed to update session" });
|
|
@@ -8713,7 +9027,7 @@ async function handleDelete(res, url, sessionsDir) {
|
|
|
8713
9027
|
jsonResponse(res, 400, { error: "Missing or invalid sessionId" });
|
|
8714
9028
|
return;
|
|
8715
9029
|
}
|
|
8716
|
-
await
|
|
9030
|
+
await fs11.rm(path14.join(sessionsDir, id), { recursive: true, force: true });
|
|
8717
9031
|
jsonResponse(res, 200, { ok: true });
|
|
8718
9032
|
} catch {
|
|
8719
9033
|
jsonResponse(res, 500, { error: "Failed to delete session" });
|
|
@@ -8957,7 +9271,7 @@ function handleLocalModelsRoute(req, res, getStatuses) {
|
|
|
8957
9271
|
}
|
|
8958
9272
|
|
|
8959
9273
|
// src/server/static.ts
|
|
8960
|
-
var
|
|
9274
|
+
var fs12 = __toESM(require("fs"));
|
|
8961
9275
|
var path15 = __toESM(require("path"));
|
|
8962
9276
|
var MIME_TYPES = {
|
|
8963
9277
|
".html": "text/html; charset=utf-8",
|
|
@@ -8987,11 +9301,11 @@ function handleStaticFile(req, res, dashboardDir) {
|
|
|
8987
9301
|
if (!resolved.startsWith(path15.resolve(dashboardDir))) {
|
|
8988
9302
|
return serveFile(path15.join(dashboardDir, "index.html"), res);
|
|
8989
9303
|
}
|
|
8990
|
-
if (
|
|
9304
|
+
if (fs12.existsSync(resolved) && fs12.statSync(resolved).isFile()) {
|
|
8991
9305
|
return serveFile(resolved, res);
|
|
8992
9306
|
}
|
|
8993
9307
|
const indexPath = path15.join(dashboardDir, "index.html");
|
|
8994
|
-
if (
|
|
9308
|
+
if (fs12.existsSync(indexPath)) {
|
|
8995
9309
|
return serveFile(indexPath, res);
|
|
8996
9310
|
}
|
|
8997
9311
|
return false;
|
|
@@ -9000,7 +9314,7 @@ function serveFile(filePath, res) {
|
|
|
9000
9314
|
const ext = path15.extname(filePath).toLowerCase();
|
|
9001
9315
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
9002
9316
|
try {
|
|
9003
|
-
const content =
|
|
9317
|
+
const content = fs12.readFileSync(filePath);
|
|
9004
9318
|
res.writeHead(200, { "Content-Type": contentType });
|
|
9005
9319
|
res.end(content);
|
|
9006
9320
|
return true;
|
|
@@ -9010,7 +9324,7 @@ function serveFile(filePath, res) {
|
|
|
9010
9324
|
}
|
|
9011
9325
|
|
|
9012
9326
|
// src/server/plan-watcher.ts
|
|
9013
|
-
var
|
|
9327
|
+
var fs13 = __toESM(require("fs"));
|
|
9014
9328
|
var path16 = __toESM(require("path"));
|
|
9015
9329
|
var PlanWatcher = class {
|
|
9016
9330
|
plansDir;
|
|
@@ -9025,11 +9339,11 @@ var PlanWatcher = class {
|
|
|
9025
9339
|
* Creates the directory if it does not exist.
|
|
9026
9340
|
*/
|
|
9027
9341
|
start() {
|
|
9028
|
-
|
|
9029
|
-
this.watcher =
|
|
9342
|
+
fs13.mkdirSync(this.plansDir, { recursive: true });
|
|
9343
|
+
this.watcher = fs13.watch(this.plansDir, (eventType, filename) => {
|
|
9030
9344
|
if (eventType === "rename" && filename && filename.endsWith(".md")) {
|
|
9031
9345
|
const filePath = path16.join(this.plansDir, filename);
|
|
9032
|
-
if (
|
|
9346
|
+
if (fs13.existsSync(filePath)) {
|
|
9033
9347
|
void this.handleNewPlan(filename);
|
|
9034
9348
|
}
|
|
9035
9349
|
}
|
|
@@ -9060,7 +9374,7 @@ var PlanWatcher = class {
|
|
|
9060
9374
|
};
|
|
9061
9375
|
|
|
9062
9376
|
// src/auth/tokens.ts
|
|
9063
|
-
var
|
|
9377
|
+
var import_node_crypto8 = require("crypto");
|
|
9064
9378
|
var import_promises = require("fs/promises");
|
|
9065
9379
|
var import_node_path = require("path");
|
|
9066
9380
|
var import_bcryptjs = __toESM(require("bcryptjs"));
|
|
@@ -9068,10 +9382,10 @@ var import_types26 = require("@harness-engineering/types");
|
|
|
9068
9382
|
var BCRYPT_ROUNDS = 12;
|
|
9069
9383
|
var LEGACY_ENV_ID = "tok_legacy_env";
|
|
9070
9384
|
function genId() {
|
|
9071
|
-
return `tok_${(0,
|
|
9385
|
+
return `tok_${(0, import_node_crypto8.randomBytes)(8).toString("hex")}`;
|
|
9072
9386
|
}
|
|
9073
9387
|
function genSecret() {
|
|
9074
|
-
return (0,
|
|
9388
|
+
return (0, import_node_crypto8.randomBytes)(24).toString("base64url");
|
|
9075
9389
|
}
|
|
9076
9390
|
function parseToken(raw) {
|
|
9077
9391
|
const dot = raw.indexOf(".");
|
|
@@ -9102,7 +9416,7 @@ var TokenStore = class {
|
|
|
9102
9416
|
}
|
|
9103
9417
|
async persist(records) {
|
|
9104
9418
|
await (0, import_promises.mkdir)((0, import_node_path.dirname)(this.path), { recursive: true });
|
|
9105
|
-
const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${(0,
|
|
9419
|
+
const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${(0, import_node_crypto8.randomBytes)(4).toString("hex")}`;
|
|
9106
9420
|
await (0, import_promises.writeFile)(tmp, JSON.stringify(records, null, 2), "utf8");
|
|
9107
9421
|
await (0, import_promises.rename)(tmp, this.path);
|
|
9108
9422
|
this.cache = records;
|
|
@@ -9171,7 +9485,7 @@ var TokenStore = class {
|
|
|
9171
9485
|
const a = Buffer.from(presented);
|
|
9172
9486
|
const b = Buffer.from(envValue);
|
|
9173
9487
|
if (a.length !== b.length) return null;
|
|
9174
|
-
if (!(0,
|
|
9488
|
+
if (!(0, import_node_crypto8.timingSafeEqual)(a, b)) return null;
|
|
9175
9489
|
return {
|
|
9176
9490
|
id: LEGACY_ENV_ID,
|
|
9177
9491
|
name: "legacy-env",
|
|
@@ -9788,15 +10102,15 @@ var OrchestratorServer = class {
|
|
|
9788
10102
|
};
|
|
9789
10103
|
|
|
9790
10104
|
// src/gateway/webhooks/store.ts
|
|
9791
|
-
var
|
|
10105
|
+
var import_node_crypto10 = require("crypto");
|
|
9792
10106
|
var import_promises3 = require("fs/promises");
|
|
9793
10107
|
var import_node_path3 = require("path");
|
|
9794
10108
|
var import_types28 = require("@harness-engineering/types");
|
|
9795
10109
|
|
|
9796
10110
|
// src/gateway/webhooks/signer.ts
|
|
9797
|
-
var
|
|
10111
|
+
var import_node_crypto9 = require("crypto");
|
|
9798
10112
|
function sign(secret, body) {
|
|
9799
|
-
const hex = (0,
|
|
10113
|
+
const hex = (0, import_node_crypto9.createHmac)("sha256", secret).update(body).digest("hex");
|
|
9800
10114
|
return `sha256=${hex}`;
|
|
9801
10115
|
}
|
|
9802
10116
|
function eventMatches(pattern, type) {
|
|
@@ -9815,10 +10129,10 @@ function eventMatches(pattern, type) {
|
|
|
9815
10129
|
|
|
9816
10130
|
// src/gateway/webhooks/store.ts
|
|
9817
10131
|
function genId2() {
|
|
9818
|
-
return `whk_${(0,
|
|
10132
|
+
return `whk_${(0, import_node_crypto10.randomBytes)(8).toString("hex")}`;
|
|
9819
10133
|
}
|
|
9820
10134
|
function genSecret2() {
|
|
9821
|
-
return (0,
|
|
10135
|
+
return (0, import_node_crypto10.randomBytes)(32).toString("base64url");
|
|
9822
10136
|
}
|
|
9823
10137
|
var WebhookStore = class {
|
|
9824
10138
|
constructor(path24) {
|
|
@@ -9843,7 +10157,7 @@ var WebhookStore = class {
|
|
|
9843
10157
|
return this.cache;
|
|
9844
10158
|
}
|
|
9845
10159
|
async persist(records) {
|
|
9846
|
-
const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${(0,
|
|
10160
|
+
const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${(0, import_node_crypto10.randomBytes)(4).toString("hex")}`;
|
|
9847
10161
|
try {
|
|
9848
10162
|
await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(this.path), { recursive: true });
|
|
9849
10163
|
await (0, import_promises3.writeFile)(tmp, JSON.stringify(records, null, 2), { encoding: "utf8", mode: 384 });
|
|
@@ -9887,7 +10201,7 @@ var WebhookStore = class {
|
|
|
9887
10201
|
};
|
|
9888
10202
|
|
|
9889
10203
|
// src/gateway/webhooks/delivery.ts
|
|
9890
|
-
var
|
|
10204
|
+
var import_node_crypto11 = require("crypto");
|
|
9891
10205
|
|
|
9892
10206
|
// src/gateway/webhooks/queue.ts
|
|
9893
10207
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"));
|
|
@@ -10097,7 +10411,7 @@ var WebhookDelivery = class {
|
|
|
10097
10411
|
enqueue(sub, event) {
|
|
10098
10412
|
const payload = JSON.stringify(event);
|
|
10099
10413
|
this.queue.insert({
|
|
10100
|
-
id: `dlv_${(0,
|
|
10414
|
+
id: `dlv_${(0, import_node_crypto11.randomBytes)(8).toString("hex")}`,
|
|
10101
10415
|
subscriptionId: sub.id,
|
|
10102
10416
|
eventType: event.type,
|
|
10103
10417
|
payload
|
|
@@ -10205,7 +10519,7 @@ var WebhookDelivery = class {
|
|
|
10205
10519
|
};
|
|
10206
10520
|
|
|
10207
10521
|
// src/gateway/webhooks/events.ts
|
|
10208
|
-
var
|
|
10522
|
+
var import_node_crypto12 = require("crypto");
|
|
10209
10523
|
var WEBHOOK_TOPICS = [
|
|
10210
10524
|
"interaction.created",
|
|
10211
10525
|
"interaction.resolved",
|
|
@@ -10220,8 +10534,8 @@ var WEBHOOK_TOPICS = [
|
|
|
10220
10534
|
"proposal.approved",
|
|
10221
10535
|
"proposal.rejected"
|
|
10222
10536
|
];
|
|
10223
|
-
function
|
|
10224
|
-
return `evt_${(0,
|
|
10537
|
+
function newEventId() {
|
|
10538
|
+
return `evt_${(0, import_node_crypto12.randomBytes)(8).toString("hex")}`;
|
|
10225
10539
|
}
|
|
10226
10540
|
function wireWebhookFanout({ bus, store, delivery }) {
|
|
10227
10541
|
const handlers = [];
|
|
@@ -10232,7 +10546,7 @@ function wireWebhookFanout({ bus, store, delivery }) {
|
|
|
10232
10546
|
const subs = await store.listForEvent(eventType);
|
|
10233
10547
|
if (subs.length === 0) return;
|
|
10234
10548
|
const event = {
|
|
10235
|
-
id:
|
|
10549
|
+
id: newEventId(),
|
|
10236
10550
|
type: eventType,
|
|
10237
10551
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10238
10552
|
data
|
|
@@ -10251,7 +10565,7 @@ function wireWebhookFanout({ bus, store, delivery }) {
|
|
|
10251
10565
|
}
|
|
10252
10566
|
|
|
10253
10567
|
// src/gateway/telemetry/fanout.ts
|
|
10254
|
-
var
|
|
10568
|
+
var import_node_crypto13 = require("crypto");
|
|
10255
10569
|
var import_core12 = require("@harness-engineering/core");
|
|
10256
10570
|
var TOPICS = {
|
|
10257
10571
|
MAINTENANCE_STARTED: "maintenance:started",
|
|
@@ -10274,14 +10588,14 @@ var SPAN_NAME = {
|
|
|
10274
10588
|
[TOPICS.DISPATCH_DECISION]: "dispatch_decision",
|
|
10275
10589
|
[TOPICS.SKILL_INVOCATION]: "skill_invocation"
|
|
10276
10590
|
};
|
|
10277
|
-
function
|
|
10278
|
-
return `evt_${(0,
|
|
10591
|
+
function newEventId2() {
|
|
10592
|
+
return `evt_${(0, import_node_crypto13.randomBytes)(8).toString("hex")}`;
|
|
10279
10593
|
}
|
|
10280
10594
|
function newTraceId() {
|
|
10281
|
-
return (0,
|
|
10595
|
+
return (0, import_node_crypto13.randomBytes)(16).toString("hex");
|
|
10282
10596
|
}
|
|
10283
10597
|
function newSpanId() {
|
|
10284
|
-
return (0,
|
|
10598
|
+
return (0, import_node_crypto13.randomBytes)(8).toString("hex");
|
|
10285
10599
|
}
|
|
10286
10600
|
function nowNs() {
|
|
10287
10601
|
return BigInt(Date.now()) * 1000000n;
|
|
@@ -10395,7 +10709,7 @@ function wireTelemetryFanout(params) {
|
|
|
10395
10709
|
};
|
|
10396
10710
|
exporter.push(span);
|
|
10397
10711
|
const gatewayEvent = {
|
|
10398
|
-
id:
|
|
10712
|
+
id: newEventId2(),
|
|
10399
10713
|
type: TELEMETRY_TYPE[topic],
|
|
10400
10714
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10401
10715
|
data: payload,
|
|
@@ -10589,7 +10903,7 @@ function buildSlackSink(config, options) {
|
|
|
10589
10903
|
}
|
|
10590
10904
|
|
|
10591
10905
|
// src/notifications/events.ts
|
|
10592
|
-
var
|
|
10906
|
+
var import_node_crypto14 = require("crypto");
|
|
10593
10907
|
|
|
10594
10908
|
// src/notifications/envelope.ts
|
|
10595
10909
|
function asObj(data) {
|
|
@@ -10720,8 +11034,8 @@ var NOTIFICATION_TOPICS = [
|
|
|
10720
11034
|
"proposal.approved",
|
|
10721
11035
|
"proposal.rejected"
|
|
10722
11036
|
];
|
|
10723
|
-
function
|
|
10724
|
-
return `evt_${(0,
|
|
11037
|
+
function newEventId3() {
|
|
11038
|
+
return `evt_${(0, import_node_crypto14.randomBytes)(8).toString("hex")}`;
|
|
10725
11039
|
}
|
|
10726
11040
|
function dispatchToEntry(bus, entry, event) {
|
|
10727
11041
|
const eventType = event.type;
|
|
@@ -10756,7 +11070,7 @@ function wireNotificationSinks({ bus, registry }) {
|
|
|
10756
11070
|
const entries = registry.list();
|
|
10757
11071
|
if (entries.length === 0) return;
|
|
10758
11072
|
const event = {
|
|
10759
|
-
id:
|
|
11073
|
+
id: newEventId3(),
|
|
10760
11074
|
type: eventType,
|
|
10761
11075
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10762
11076
|
data
|
|
@@ -10774,7 +11088,7 @@ function wireNotificationSinks({ bus, registry }) {
|
|
|
10774
11088
|
}
|
|
10775
11089
|
|
|
10776
11090
|
// src/orchestrator.ts
|
|
10777
|
-
var
|
|
11091
|
+
var import_core17 = require("@harness-engineering/core");
|
|
10778
11092
|
|
|
10779
11093
|
// src/logging/logger.ts
|
|
10780
11094
|
var StructuredLogger = class {
|
|
@@ -10859,6 +11173,37 @@ async function scanWorkspaceConfig(workspacePath) {
|
|
|
10859
11173
|
return { exitCode: (0, import_core13.computeScanExitCode)(results), results };
|
|
10860
11174
|
}
|
|
10861
11175
|
|
|
11176
|
+
// src/core/lane-persistence.ts
|
|
11177
|
+
var import_core14 = require("@harness-engineering/core");
|
|
11178
|
+
var import_types29 = require("@harness-engineering/types");
|
|
11179
|
+
var SIGNAL_TO_LANE = {
|
|
11180
|
+
claim: "claimed",
|
|
11181
|
+
dispatch: "in_progress",
|
|
11182
|
+
success: "in_review",
|
|
11183
|
+
failure: "blocked",
|
|
11184
|
+
abandon: "canceled"
|
|
11185
|
+
};
|
|
11186
|
+
function mapOrchestratorLane(signal) {
|
|
11187
|
+
return SIGNAL_TO_LANE[signal];
|
|
11188
|
+
}
|
|
11189
|
+
async function persistLane(projectPath, issueId, signal) {
|
|
11190
|
+
try {
|
|
11191
|
+
const reg = await import_core14.eventSourcing.registerTask(projectPath, issueId, []);
|
|
11192
|
+
if (!reg.ok) return reg;
|
|
11193
|
+
return await import_core14.eventSourcing.transitionLane(projectPath, issueId, mapOrchestratorLane(signal));
|
|
11194
|
+
} catch (err) {
|
|
11195
|
+
return (0, import_types29.Err)(err instanceof Error ? err : new Error(String(err)));
|
|
11196
|
+
}
|
|
11197
|
+
}
|
|
11198
|
+
async function readPersistedLanes(projectPath) {
|
|
11199
|
+
try {
|
|
11200
|
+
const snap = await import_core14.eventSourcing.readSnapshot(projectPath);
|
|
11201
|
+
return snap.ok ? snap.value.lanes : { tasks: {} };
|
|
11202
|
+
} catch {
|
|
11203
|
+
return { tasks: {} };
|
|
11204
|
+
}
|
|
11205
|
+
}
|
|
11206
|
+
|
|
10862
11207
|
// src/maintenance/task-registry.ts
|
|
10863
11208
|
var BUILT_IN_TASKS = [
|
|
10864
11209
|
// --- Mechanical-AI ---
|
|
@@ -10922,7 +11267,13 @@ var BUILT_IN_TASKS = [
|
|
|
10922
11267
|
description: "Detect and fix cross-check violations",
|
|
10923
11268
|
schedule: "0 6 * * 1",
|
|
10924
11269
|
branch: "harness-maint/cross-check-fixes",
|
|
10925
|
-
|
|
11270
|
+
// `harness cross-check` is a dedicated read-only CLI subcommand that surfaces
|
|
11271
|
+
// JUST cross-artifact consistency (plan→implementation coverage + staleness),
|
|
11272
|
+
// mirroring the `validate_cross_check` MCP tool's core (`runCrossCheck`)
|
|
11273
|
+
// WITHOUT running the full `harness validate` suite. It prints a parseable
|
|
11274
|
+
// `Cross-check: N issues` line and exits 0 (clean) / 1 (N issues), so the
|
|
11275
|
+
// maintenance runner reports real results instead of an honest `failure`.
|
|
11276
|
+
checkCommand: ["cross-check"],
|
|
10926
11277
|
fixSkill: "harness-cross-check-fix"
|
|
10927
11278
|
},
|
|
10928
11279
|
// --- Pure-AI ---
|
|
@@ -10981,7 +11332,10 @@ var BUILT_IN_TASKS = [
|
|
|
10981
11332
|
description: "Assess overall project health",
|
|
10982
11333
|
schedule: "0 6 * * *",
|
|
10983
11334
|
branch: null,
|
|
10984
|
-
|
|
11335
|
+
// `assess_project` is an MCP tool name, not a CLI subcommand. The CLI
|
|
11336
|
+
// composite project-health report is `harness insights` (health, entropy,
|
|
11337
|
+
// decay, attention, impact) — a read-only report that records metrics.
|
|
11338
|
+
checkCommand: ["insights"]
|
|
10985
11339
|
},
|
|
10986
11340
|
{
|
|
10987
11341
|
id: "stale-constraints",
|
|
@@ -10989,7 +11343,14 @@ var BUILT_IN_TASKS = [
|
|
|
10989
11343
|
description: "Detect stale architectural constraints",
|
|
10990
11344
|
schedule: "0 2 1 * *",
|
|
10991
11345
|
branch: null,
|
|
10992
|
-
|
|
11346
|
+
// `harness stale-constraints` is a dedicated read-only CLI subcommand that
|
|
11347
|
+
// surfaces the `detect_stale_constraints` MCP tool's core in-process. It is
|
|
11348
|
+
// precondition-gated on the knowledge graph: with no graph it emits the
|
|
11349
|
+
// "No knowledge graph found. Run `harness scan` first." signature and exits
|
|
11350
|
+
// non-zero, which the runner classifies as `skipped` (not failure). With a
|
|
11351
|
+
// graph it prints a parseable `Stale constraints: N findings` line and exits
|
|
11352
|
+
// 0 (clean) / 1 (N stale), recorded as report-only metrics.
|
|
11353
|
+
checkCommand: ["stale-constraints"]
|
|
10993
11354
|
},
|
|
10994
11355
|
{
|
|
10995
11356
|
id: "graph-refresh",
|
|
@@ -11022,7 +11383,8 @@ var BUILT_IN_TASKS = [
|
|
|
11022
11383
|
description: "Clean up stale orchestrator sessions",
|
|
11023
11384
|
schedule: "0 0 * * *",
|
|
11024
11385
|
branch: null,
|
|
11025
|
-
checkCommand: ["cleanup-sessions"]
|
|
11386
|
+
checkCommand: ["cleanup-sessions"],
|
|
11387
|
+
excludeFromHumanSweep: true
|
|
11026
11388
|
},
|
|
11027
11389
|
{
|
|
11028
11390
|
id: "perf-baselines",
|
|
@@ -11030,7 +11392,8 @@ var BUILT_IN_TASKS = [
|
|
|
11030
11392
|
description: "Update performance baselines",
|
|
11031
11393
|
schedule: "0 7 * * *",
|
|
11032
11394
|
branch: null,
|
|
11033
|
-
checkCommand: ["perf", "baselines", "update"]
|
|
11395
|
+
checkCommand: ["perf", "baselines", "update"],
|
|
11396
|
+
excludeFromHumanSweep: true
|
|
11034
11397
|
},
|
|
11035
11398
|
{
|
|
11036
11399
|
id: "main-sync",
|
|
@@ -11038,7 +11401,8 @@ var BUILT_IN_TASKS = [
|
|
|
11038
11401
|
description: "Fast-forward local default branch from origin",
|
|
11039
11402
|
schedule: "*/15 * * * *",
|
|
11040
11403
|
branch: null,
|
|
11041
|
-
checkCommand: ["harness", "sync-main", "--json"]
|
|
11404
|
+
checkCommand: ["harness", "sync-main", "--json"],
|
|
11405
|
+
excludeFromHumanSweep: true
|
|
11042
11406
|
},
|
|
11043
11407
|
// Hermes Phase 4 — one-shot backfill that stamps `provenance: user-authored`
|
|
11044
11408
|
// on every existing catalog skill. Schedule is Feb 31 (a date that never
|
|
@@ -11051,7 +11415,8 @@ var BUILT_IN_TASKS = [
|
|
|
11051
11415
|
description: "Backfill provenance: user-authored on every existing skill (one-shot, idempotent)",
|
|
11052
11416
|
schedule: "0 0 31 2 *",
|
|
11053
11417
|
branch: null,
|
|
11054
|
-
checkCommand: ["backfill-skill-provenance"]
|
|
11418
|
+
checkCommand: ["backfill-skill-provenance"],
|
|
11419
|
+
excludeFromHumanSweep: true
|
|
11055
11420
|
}
|
|
11056
11421
|
];
|
|
11057
11422
|
|
|
@@ -11118,6 +11483,17 @@ function cronMatchesNow(expression, now) {
|
|
|
11118
11483
|
const daysOfWeek = parseField(dowField, 0, 6);
|
|
11119
11484
|
return minutes.has(minute) && hours.has(hour) && daysOfMonth.has(dayOfMonth) && months.has(month) && daysOfWeek.has(dayOfWeek);
|
|
11120
11485
|
}
|
|
11486
|
+
function cronMatchesDate(expression, date) {
|
|
11487
|
+
const fields = expression.trim().split(/\s+/);
|
|
11488
|
+
if (fields.length !== 5) {
|
|
11489
|
+
throw new Error(`Invalid cron expression: expected 5 fields, got ${fields.length}`);
|
|
11490
|
+
}
|
|
11491
|
+
const [, , domField, monthField, dowField] = fields;
|
|
11492
|
+
const daysOfMonth = parseField(domField, 1, 31);
|
|
11493
|
+
const months = parseField(monthField, 1, 12);
|
|
11494
|
+
const daysOfWeek = parseField(dowField, 0, 6);
|
|
11495
|
+
return daysOfMonth.has(date.getDate()) && months.has(date.getMonth() + 1) && daysOfWeek.has(date.getDay());
|
|
11496
|
+
}
|
|
11121
11497
|
|
|
11122
11498
|
// src/maintenance/scheduler.ts
|
|
11123
11499
|
var MaintenanceScheduler = class {
|
|
@@ -11359,15 +11735,15 @@ var MaintenanceScheduler = class {
|
|
|
11359
11735
|
};
|
|
11360
11736
|
|
|
11361
11737
|
// src/maintenance/leader-elector.ts
|
|
11362
|
-
var
|
|
11738
|
+
var import_types30 = require("@harness-engineering/types");
|
|
11363
11739
|
var SingleProcessLeaderElector = class {
|
|
11364
11740
|
async electLeader() {
|
|
11365
|
-
return (0,
|
|
11741
|
+
return (0, import_types30.Ok)("claimed");
|
|
11366
11742
|
}
|
|
11367
11743
|
};
|
|
11368
11744
|
|
|
11369
11745
|
// src/maintenance/reporter.ts
|
|
11370
|
-
var
|
|
11746
|
+
var fs14 = __toESM(require("fs"));
|
|
11371
11747
|
var path18 = __toESM(require("path"));
|
|
11372
11748
|
var import_zod17 = require("zod");
|
|
11373
11749
|
var RunResultSchema = import_zod17.z.object({
|
|
@@ -11403,9 +11779,9 @@ var MaintenanceReporter = class {
|
|
|
11403
11779
|
*/
|
|
11404
11780
|
async load() {
|
|
11405
11781
|
try {
|
|
11406
|
-
await
|
|
11782
|
+
await fs14.promises.mkdir(this.persistDir, { recursive: true });
|
|
11407
11783
|
const filePath = path18.join(this.persistDir, "history.json");
|
|
11408
|
-
const data = await
|
|
11784
|
+
const data = await fs14.promises.readFile(filePath, "utf-8");
|
|
11409
11785
|
const parsed = import_zod17.z.array(RunResultSchema).safeParse(JSON.parse(data));
|
|
11410
11786
|
if (parsed.success) {
|
|
11411
11787
|
this.history = parsed.data.slice(0, MAX_HISTORY);
|
|
@@ -11439,9 +11815,9 @@ var MaintenanceReporter = class {
|
|
|
11439
11815
|
*/
|
|
11440
11816
|
async persist() {
|
|
11441
11817
|
try {
|
|
11442
|
-
await
|
|
11818
|
+
await fs14.promises.mkdir(this.persistDir, { recursive: true });
|
|
11443
11819
|
const filePath = path18.join(this.persistDir, "history.json");
|
|
11444
|
-
await
|
|
11820
|
+
await fs14.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
|
|
11445
11821
|
} catch (err) {
|
|
11446
11822
|
this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
|
|
11447
11823
|
}
|
|
@@ -11481,20 +11857,20 @@ var TaskRunner = class {
|
|
|
11481
11857
|
* @param origin - Hermes Phase 2 trigger-source tag; defaults to `'cron'`
|
|
11482
11858
|
* when called from the scheduler path.
|
|
11483
11859
|
*/
|
|
11484
|
-
async run(task, origin = "cron") {
|
|
11860
|
+
async run(task, origin = "cron", mode = "fix") {
|
|
11485
11861
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11486
11862
|
let result;
|
|
11487
11863
|
let captured;
|
|
11488
11864
|
try {
|
|
11489
11865
|
switch (task.type) {
|
|
11490
11866
|
case "mechanical-ai": {
|
|
11491
|
-
const out = await this.runMechanicalAI(task, startedAt);
|
|
11867
|
+
const out = await this.runMechanicalAI(task, startedAt, mode);
|
|
11492
11868
|
result = out.result;
|
|
11493
11869
|
captured = out.captured;
|
|
11494
11870
|
break;
|
|
11495
11871
|
}
|
|
11496
11872
|
case "pure-ai":
|
|
11497
|
-
result = await this.runPureAI(task, startedAt);
|
|
11873
|
+
result = await this.runPureAI(task, startedAt, mode);
|
|
11498
11874
|
break;
|
|
11499
11875
|
case "report-only": {
|
|
11500
11876
|
const out = await this.runReportOnly(task, startedAt);
|
|
@@ -11503,7 +11879,7 @@ var TaskRunner = class {
|
|
|
11503
11879
|
break;
|
|
11504
11880
|
}
|
|
11505
11881
|
case "housekeeping": {
|
|
11506
|
-
const out = await this.runHousekeeping(task, startedAt);
|
|
11882
|
+
const out = await this.runHousekeeping(task, startedAt, mode);
|
|
11507
11883
|
result = out.result;
|
|
11508
11884
|
captured = out.captured;
|
|
11509
11885
|
break;
|
|
@@ -11567,7 +11943,8 @@ var TaskRunner = class {
|
|
|
11567
11943
|
findings: r2.findings,
|
|
11568
11944
|
stdout: r2.output,
|
|
11569
11945
|
stderr: r2.stderr,
|
|
11570
|
-
structured: r2.structured ? r2.structured : null
|
|
11946
|
+
structured: r2.structured ? r2.structured : null,
|
|
11947
|
+
executionFailed: false
|
|
11571
11948
|
};
|
|
11572
11949
|
}
|
|
11573
11950
|
if (!task.checkCommand || task.checkCommand.length === 0) {
|
|
@@ -11579,7 +11956,8 @@ var TaskRunner = class {
|
|
|
11579
11956
|
findings: r.findings,
|
|
11580
11957
|
stdout: r.output,
|
|
11581
11958
|
stderr: "",
|
|
11582
|
-
structured: null
|
|
11959
|
+
structured: null,
|
|
11960
|
+
executionFailed: r.executionFailed ?? false
|
|
11583
11961
|
};
|
|
11584
11962
|
}
|
|
11585
11963
|
/**
|
|
@@ -11604,7 +11982,7 @@ var TaskRunner = class {
|
|
|
11604
11982
|
* only if fixable findings exist; persist captured stdout/stderr/context
|
|
11605
11983
|
* via the output store on the way out.
|
|
11606
11984
|
*/
|
|
11607
|
-
async runMechanicalAI(task, startedAt) {
|
|
11985
|
+
async runMechanicalAI(task, startedAt, mode = "fix") {
|
|
11608
11986
|
if (!task.fixSkill) {
|
|
11609
11987
|
return wrap(this.failureResult(task.id, startedAt, "mechanical-ai task missing fixSkill"));
|
|
11610
11988
|
}
|
|
@@ -11626,6 +12004,27 @@ var TaskRunner = class {
|
|
|
11626
12004
|
} catch (err) {
|
|
11627
12005
|
return wrap(this.failureResult(task.id, startedAt, String(err)));
|
|
11628
12006
|
}
|
|
12007
|
+
if (check.executionFailed) {
|
|
12008
|
+
const cls = classifyCheckExecutionFailure(`${check.stdout}
|
|
12009
|
+
${check.stderr}`);
|
|
12010
|
+
if (cls.kind === "unrunnable") {
|
|
12011
|
+
return wrap(
|
|
12012
|
+
this.failureResult(task.id, startedAt, checkExecutionError(check)),
|
|
12013
|
+
captureFromCheck(check)
|
|
12014
|
+
);
|
|
12015
|
+
}
|
|
12016
|
+
if (cls.kind === "precondition") {
|
|
12017
|
+
return wrap(
|
|
12018
|
+
this.skippedResult(task.id, startedAt, cls.reason ?? "precondition not met"),
|
|
12019
|
+
captureFromCheck(check)
|
|
12020
|
+
);
|
|
12021
|
+
}
|
|
12022
|
+
check = {
|
|
12023
|
+
...check,
|
|
12024
|
+
executionFailed: false,
|
|
12025
|
+
findings: check.findings > 0 ? check.findings : recoverFindingsCount(check.stdout)
|
|
12026
|
+
};
|
|
12027
|
+
}
|
|
11629
12028
|
const promptContext = await this.composePromptContext(task);
|
|
11630
12029
|
const baseCaptured = {
|
|
11631
12030
|
stdout: check.stdout,
|
|
@@ -11634,13 +12033,14 @@ var TaskRunner = class {
|
|
|
11634
12033
|
...promptContext ? { context: promptContext } : {}
|
|
11635
12034
|
};
|
|
11636
12035
|
const wakeAgentExplicitlyFalse = check.structured !== null && typeof check.structured === "object" && check.structured.wakeAgent === false;
|
|
11637
|
-
if (check.findings === 0 || wakeAgentExplicitlyFalse) {
|
|
12036
|
+
if (check.findings === 0 || wakeAgentExplicitlyFalse || mode === "report") {
|
|
12037
|
+
const reportedWithFindings = mode === "report" && check.findings > 0;
|
|
11638
12038
|
return {
|
|
11639
12039
|
result: {
|
|
11640
12040
|
taskId: task.id,
|
|
11641
12041
|
startedAt,
|
|
11642
12042
|
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11643
|
-
status: "no-issues",
|
|
12043
|
+
status: reportedWithFindings ? "success" : "no-issues",
|
|
11644
12044
|
findings: check.findings,
|
|
11645
12045
|
fixed: 0,
|
|
11646
12046
|
prUrl: null,
|
|
@@ -11710,7 +12110,19 @@ var TaskRunner = class {
|
|
|
11710
12110
|
/**
|
|
11711
12111
|
* Pure-AI: always dispatch agent with configured skill.
|
|
11712
12112
|
*/
|
|
11713
|
-
async runPureAI(task, startedAt) {
|
|
12113
|
+
async runPureAI(task, startedAt, mode = "fix") {
|
|
12114
|
+
if (mode === "report") {
|
|
12115
|
+
return {
|
|
12116
|
+
taskId: task.id,
|
|
12117
|
+
startedAt,
|
|
12118
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12119
|
+
status: "no-issues",
|
|
12120
|
+
findings: 0,
|
|
12121
|
+
fixed: 0,
|
|
12122
|
+
prUrl: null,
|
|
12123
|
+
prUpdated: false
|
|
12124
|
+
};
|
|
12125
|
+
}
|
|
11714
12126
|
if (!task.fixSkill) {
|
|
11715
12127
|
return this.failureResult(task.id, startedAt, "pure-ai task missing fixSkill");
|
|
11716
12128
|
}
|
|
@@ -11784,6 +12196,27 @@ var TaskRunner = class {
|
|
|
11784
12196
|
return wrap(this.failureResult(task.id, startedAt, String(err)));
|
|
11785
12197
|
}
|
|
11786
12198
|
const parsed = parseStatusLine(check.stdout);
|
|
12199
|
+
if (parsed === null && check.executionFailed) {
|
|
12200
|
+
const cls = classifyCheckExecutionFailure(`${check.stdout}
|
|
12201
|
+
${check.stderr}`);
|
|
12202
|
+
if (cls.kind === "unrunnable") {
|
|
12203
|
+
return wrap(
|
|
12204
|
+
this.failureResult(task.id, startedAt, checkExecutionError(check)),
|
|
12205
|
+
captureFromCheck(check)
|
|
12206
|
+
);
|
|
12207
|
+
}
|
|
12208
|
+
if (cls.kind === "precondition") {
|
|
12209
|
+
return wrap(
|
|
12210
|
+
this.skippedResult(task.id, startedAt, cls.reason ?? "precondition not met"),
|
|
12211
|
+
captureFromCheck(check)
|
|
12212
|
+
);
|
|
12213
|
+
}
|
|
12214
|
+
check = {
|
|
12215
|
+
...check,
|
|
12216
|
+
executionFailed: false,
|
|
12217
|
+
findings: check.findings > 0 ? check.findings : recoverFindingsCount(check.stdout)
|
|
12218
|
+
};
|
|
12219
|
+
}
|
|
11787
12220
|
const status = parsed?.status ?? "success";
|
|
11788
12221
|
const findings = parsed === null ? check.findings : typeof parsed.candidatesFound === "number" ? parsed.candidatesFound : 0;
|
|
11789
12222
|
const result = {
|
|
@@ -11817,7 +12250,20 @@ var TaskRunner = class {
|
|
|
11817
12250
|
* Hermes Phase 2: a `checkScript` may replace `checkCommand` for housekeeping
|
|
11818
12251
|
* tasks; the runner falls through to the same JSON-status parsing path.
|
|
11819
12252
|
*/
|
|
11820
|
-
async runHousekeeping(task, startedAt) {
|
|
12253
|
+
async runHousekeeping(task, startedAt, mode = "fix") {
|
|
12254
|
+
if (mode === "report") {
|
|
12255
|
+
return wrap({
|
|
12256
|
+
taskId: task.id,
|
|
12257
|
+
startedAt,
|
|
12258
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12259
|
+
status: "skipped",
|
|
12260
|
+
findings: 0,
|
|
12261
|
+
fixed: 0,
|
|
12262
|
+
prUrl: null,
|
|
12263
|
+
prUpdated: false,
|
|
12264
|
+
error: "skipped in report mode: housekeeping may mutate and report runs are read-only"
|
|
12265
|
+
});
|
|
12266
|
+
}
|
|
11821
12267
|
if (!task.checkCommand && !task.checkScript) {
|
|
11822
12268
|
return wrap(
|
|
11823
12269
|
this.failureResult(
|
|
@@ -11884,10 +12330,86 @@ var TaskRunner = class {
|
|
|
11884
12330
|
error
|
|
11885
12331
|
};
|
|
11886
12332
|
}
|
|
12333
|
+
/**
|
|
12334
|
+
* A precondition-gated check (e.g. `predict` with <3 snapshots, or a
|
|
12335
|
+
* graph-backed check before `harness scan`). The command is correctly
|
|
12336
|
+
* configured; the repo just lacks the state it needs. Reported as `skipped`
|
|
12337
|
+
* — distinct from a hard `failure` — carrying the refusal line as the reason
|
|
12338
|
+
* (surfaced in the run-report summary column). ADR 0050.
|
|
12339
|
+
*/
|
|
12340
|
+
skippedResult(taskId, startedAt, reason) {
|
|
12341
|
+
return {
|
|
12342
|
+
taskId,
|
|
12343
|
+
startedAt,
|
|
12344
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12345
|
+
status: "skipped",
|
|
12346
|
+
findings: 0,
|
|
12347
|
+
fixed: 0,
|
|
12348
|
+
prUrl: null,
|
|
12349
|
+
prUpdated: false,
|
|
12350
|
+
error: reason
|
|
12351
|
+
};
|
|
12352
|
+
}
|
|
11887
12353
|
};
|
|
11888
12354
|
function wrap(result, captured) {
|
|
11889
12355
|
return captured ? { result, captured } : { result };
|
|
11890
12356
|
}
|
|
12357
|
+
function checkExecutionError(check) {
|
|
12358
|
+
const detail = (check.stderr || check.stdout || "").trim();
|
|
12359
|
+
return detail ? `check command failed to execute: ${detail}` : "check command failed to execute";
|
|
12360
|
+
}
|
|
12361
|
+
function captureFromCheck(check) {
|
|
12362
|
+
return { stdout: check.stdout, stderr: check.stderr, structured: check.structured };
|
|
12363
|
+
}
|
|
12364
|
+
var PRECONDITION_PATTERNS = [
|
|
12365
|
+
/requires at least \d+ snapshots?/i,
|
|
12366
|
+
/Run ["'`]?harness snapshot/i,
|
|
12367
|
+
/no knowledge graph found/i,
|
|
12368
|
+
/Run ["'`]?harness scan/i,
|
|
12369
|
+
/no graph (?:available|found)/i
|
|
12370
|
+
];
|
|
12371
|
+
var UNRUNNABLE_PATTERNS = [
|
|
12372
|
+
/unknown command/i,
|
|
12373
|
+
/unknown option/i,
|
|
12374
|
+
/\bENOENT\b/,
|
|
12375
|
+
/command not found/i,
|
|
12376
|
+
/cannot find module/i,
|
|
12377
|
+
// A timed-out check did not complete — it is a hard failure, never a
|
|
12378
|
+
// "ran-no-count" success. The runners synthesize this message on SIGTERM.
|
|
12379
|
+
/timed out/i,
|
|
12380
|
+
/\bETIMEDOUT\b/
|
|
12381
|
+
];
|
|
12382
|
+
var TIMEOUT_SIGNATURE = /check timed out after \d+\s*ms/i;
|
|
12383
|
+
function explicitFindingsCount(output) {
|
|
12384
|
+
const after = output.match(/(?:findings?|issues?|violations?|errors?)\s*[:=]\s*(\d+)/i);
|
|
12385
|
+
if (after) return parseInt(after[1], 10);
|
|
12386
|
+
const before = output.match(/(\d+)\s+(?:findings?|issues?|violations?|errors?)\b/i);
|
|
12387
|
+
if (before) return parseInt(before[1], 10);
|
|
12388
|
+
return null;
|
|
12389
|
+
}
|
|
12390
|
+
function recoverFindingsCount(output) {
|
|
12391
|
+
return explicitFindingsCount(output) ?? 1;
|
|
12392
|
+
}
|
|
12393
|
+
function firstMeaningfulLine(output) {
|
|
12394
|
+
const line = output.split("\n").map((l) => l.trim()).find(Boolean);
|
|
12395
|
+
if (!line) return "precondition not met";
|
|
12396
|
+
return line.replace(/^[x✗✓!-]\s*/u, "").trim();
|
|
12397
|
+
}
|
|
12398
|
+
var CLASSIFY_HEAD_LINES = 3;
|
|
12399
|
+
function classifyCheckExecutionFailure(output) {
|
|
12400
|
+
const text = (output ?? "").trim();
|
|
12401
|
+
if (text.length === 0) return { kind: "unrunnable" };
|
|
12402
|
+
if (TIMEOUT_SIGNATURE.test(text)) return { kind: "unrunnable" };
|
|
12403
|
+
if (explicitFindingsCount(text) !== null) return { kind: "ran-no-count" };
|
|
12404
|
+
const head = text.split("\n").filter((l) => l.trim()).slice(0, CLASSIFY_HEAD_LINES).join("\n");
|
|
12405
|
+
for (const re of PRECONDITION_PATTERNS) {
|
|
12406
|
+
if (re.test(head)) return { kind: "precondition", reason: firstMeaningfulLine(text) };
|
|
12407
|
+
}
|
|
12408
|
+
for (const re of UNRUNNABLE_PATTERNS) {
|
|
12409
|
+
if (re.test(head)) return { kind: "unrunnable" };
|
|
12410
|
+
}
|
|
12411
|
+
return { kind: "ran-no-count" };
|
|
12412
|
+
}
|
|
11891
12413
|
function parseStatusLine(output) {
|
|
11892
12414
|
const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
11893
12415
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -12036,8 +12558,49 @@ function heuristicResult(stdout, stderr, exitedAbnormally) {
|
|
|
12036
12558
|
};
|
|
12037
12559
|
}
|
|
12038
12560
|
|
|
12561
|
+
// src/maintenance/check-runner.ts
|
|
12562
|
+
var import_node_child_process12 = require("child_process");
|
|
12563
|
+
var import_node_util4 = require("util");
|
|
12564
|
+
var MAINTENANCE_CHECK_MAX_BUFFER = 64 * 1024 * 1024;
|
|
12565
|
+
var MAINTENANCE_CHECK_TIMEOUT_MS = 3e5;
|
|
12566
|
+
var FINDINGS_RE = /(\d+)\s+(?:finding|issue|violation|error)/i;
|
|
12567
|
+
var nodeExecFileAsync = (0, import_node_util4.promisify)(import_node_child_process12.execFile);
|
|
12568
|
+
function isCheckTimeoutError(e) {
|
|
12569
|
+
return e.killed === true || e.signal === "SIGTERM" || e.code === "ETIMEDOUT";
|
|
12570
|
+
}
|
|
12571
|
+
async function runHarnessCheck(spawn7, cwd, opts = {}) {
|
|
12572
|
+
const execFileAsync2 = opts.execFileAsync ?? nodeExecFileAsync;
|
|
12573
|
+
const timeoutMs = opts.timeoutMs ?? MAINTENANCE_CHECK_TIMEOUT_MS;
|
|
12574
|
+
const maxBuffer = opts.maxBuffer ?? MAINTENANCE_CHECK_MAX_BUFFER;
|
|
12575
|
+
try {
|
|
12576
|
+
const { stdout } = await execFileAsync2(spawn7.file, spawn7.args, {
|
|
12577
|
+
cwd,
|
|
12578
|
+
timeout: timeoutMs,
|
|
12579
|
+
maxBuffer
|
|
12580
|
+
});
|
|
12581
|
+
const text = String(stdout);
|
|
12582
|
+
const m = text.match(FINDINGS_RE);
|
|
12583
|
+
const findings = m ? parseInt(m[1], 10) : 0;
|
|
12584
|
+
return { passed: findings === 0, findings, output: text, executionFailed: false };
|
|
12585
|
+
} catch (err) {
|
|
12586
|
+
const e = err;
|
|
12587
|
+
let output = [e.stdout, e.stderr].map((v) => v == null ? "" : String(v)).filter(Boolean).join("\n");
|
|
12588
|
+
if (isCheckTimeoutError(e)) {
|
|
12589
|
+
const note = `check timed out after ${timeoutMs}ms`;
|
|
12590
|
+
output = output.trim() ? `${output}
|
|
12591
|
+
${note}` : note;
|
|
12592
|
+
return { passed: false, findings: 0, output, executionFailed: true };
|
|
12593
|
+
}
|
|
12594
|
+
const m = output.match(FINDINGS_RE);
|
|
12595
|
+
if (m) {
|
|
12596
|
+
return { passed: false, findings: parseInt(m[1], 10), output, executionFailed: false };
|
|
12597
|
+
}
|
|
12598
|
+
return { passed: false, findings: 0, output, executionFailed: true };
|
|
12599
|
+
}
|
|
12600
|
+
}
|
|
12601
|
+
|
|
12039
12602
|
// src/maintenance/output-store.ts
|
|
12040
|
-
var
|
|
12603
|
+
var fs15 = __toESM(require("fs"));
|
|
12041
12604
|
var path20 = __toESM(require("path"));
|
|
12042
12605
|
var DEFAULT_RETENTION = {
|
|
12043
12606
|
runs: 50,
|
|
@@ -12078,13 +12641,13 @@ var TaskOutputStore = class {
|
|
|
12078
12641
|
async write(taskId, entry, retention) {
|
|
12079
12642
|
this.ensureSafeTaskId(taskId);
|
|
12080
12643
|
const dir = this.dirFor(taskId);
|
|
12081
|
-
await
|
|
12644
|
+
await fs15.promises.mkdir(dir, { recursive: true });
|
|
12082
12645
|
const fileName = `${sanitizeIso(entry.completedAt || (/* @__PURE__ */ new Date()).toISOString())}.json`;
|
|
12083
12646
|
const filePath = path20.join(dir, fileName);
|
|
12084
12647
|
const tmpPath = `${filePath}.tmp`;
|
|
12085
12648
|
const payload = JSON.stringify(entry, null, 2);
|
|
12086
|
-
await
|
|
12087
|
-
await
|
|
12649
|
+
await fs15.promises.writeFile(tmpPath, payload, "utf-8");
|
|
12650
|
+
await fs15.promises.rename(tmpPath, filePath);
|
|
12088
12651
|
try {
|
|
12089
12652
|
await this.applyRetention(taskId, retention);
|
|
12090
12653
|
} catch (err) {
|
|
@@ -12135,7 +12698,7 @@ var TaskOutputStore = class {
|
|
|
12135
12698
|
}
|
|
12136
12699
|
async readEntry(filePath) {
|
|
12137
12700
|
try {
|
|
12138
|
-
const buf = await
|
|
12701
|
+
const buf = await fs15.promises.readFile(filePath, "utf-8");
|
|
12139
12702
|
const parsed = JSON.parse(buf);
|
|
12140
12703
|
return parsed;
|
|
12141
12704
|
} catch {
|
|
@@ -12157,7 +12720,7 @@ var TaskOutputStore = class {
|
|
|
12157
12720
|
const toRemove = /* @__PURE__ */ new Set([...overflow, ...aged]);
|
|
12158
12721
|
for (const name of toRemove) {
|
|
12159
12722
|
try {
|
|
12160
|
-
await
|
|
12723
|
+
await fs15.promises.unlink(path20.join(dir, name));
|
|
12161
12724
|
} catch {
|
|
12162
12725
|
}
|
|
12163
12726
|
}
|
|
@@ -12166,7 +12729,7 @@ var TaskOutputStore = class {
|
|
|
12166
12729
|
async function listJsonFilesDescending(dir) {
|
|
12167
12730
|
let names;
|
|
12168
12731
|
try {
|
|
12169
|
-
names = await
|
|
12732
|
+
names = await fs15.promises.readdir(dir);
|
|
12170
12733
|
} catch {
|
|
12171
12734
|
return [];
|
|
12172
12735
|
}
|
|
@@ -12275,7 +12838,7 @@ var fallbackLogger3 = {
|
|
|
12275
12838
|
};
|
|
12276
12839
|
|
|
12277
12840
|
// src/maintenance/custom-task-validator.ts
|
|
12278
|
-
var
|
|
12841
|
+
var import_types31 = require("@harness-engineering/types");
|
|
12279
12842
|
var ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
12280
12843
|
var REQUIRED_FIELDS_BY_TYPE = {
|
|
12281
12844
|
"mechanical-ai": ["branch", "fixSkill"],
|
|
@@ -12285,7 +12848,7 @@ var REQUIRED_FIELDS_BY_TYPE = {
|
|
|
12285
12848
|
};
|
|
12286
12849
|
function validateCustomTasks(customTasks, builtIns, deps = {}) {
|
|
12287
12850
|
const errors = [];
|
|
12288
|
-
if (!customTasks) return (0,
|
|
12851
|
+
if (!customTasks) return (0, import_types31.Ok)(void 0);
|
|
12289
12852
|
const builtInIds = new Set(builtIns.map((t) => t.id));
|
|
12290
12853
|
const customIds = Object.keys(customTasks);
|
|
12291
12854
|
const allIds = /* @__PURE__ */ new Set([...builtInIds, ...customIds]);
|
|
@@ -12295,7 +12858,7 @@ function validateCustomTasks(customTasks, builtIns, deps = {}) {
|
|
|
12295
12858
|
validateOne(id, task, builtInIds, allIds, deps, errors);
|
|
12296
12859
|
}
|
|
12297
12860
|
detectCycles(customTasks, builtIns, errors);
|
|
12298
|
-
return errors.length === 0 ? (0,
|
|
12861
|
+
return errors.length === 0 ? (0, import_types31.Ok)(void 0) : (0, import_types31.Err)(errors);
|
|
12299
12862
|
}
|
|
12300
12863
|
function validateOne(id, task, builtInIds, allIds, deps, errors) {
|
|
12301
12864
|
const prefix = `customTasks.${id}`;
|
|
@@ -12484,6 +13047,11 @@ function deriveSeedPaths(config) {
|
|
|
12484
13047
|
const roadmapPath = config.tracker.kind === "roadmap" && config.tracker.filePath ? config.tracker.filePath : "docs/roadmap.md";
|
|
12485
13048
|
return [".harness/proposals", roadmapPath];
|
|
12486
13049
|
}
|
|
13050
|
+
function normalizeHarnessCommand(command) {
|
|
13051
|
+
if (command.length === 0) return [];
|
|
13052
|
+
if (command[0] === "harness") return command;
|
|
13053
|
+
return ["harness", ...command];
|
|
13054
|
+
}
|
|
12487
13055
|
var Orchestrator = class extends import_node_events.EventEmitter {
|
|
12488
13056
|
state;
|
|
12489
13057
|
config;
|
|
@@ -12610,6 +13178,11 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
12610
13178
|
abortControllers = /* @__PURE__ */ new Map();
|
|
12611
13179
|
/** Guards against overlapping ticks when a tick takes longer than the polling interval */
|
|
12612
13180
|
tickInProgress = false;
|
|
13181
|
+
/** Phase 4 (DLane-5): lanes read back from the durable log on first tick, for
|
|
13182
|
+
* observability only — NOT fed into reconciliation. Empty until the first tick. */
|
|
13183
|
+
persistedLanes = { tasks: {} };
|
|
13184
|
+
/** Ensures the lane read-back diagnostic runs at most once (first tick). */
|
|
13185
|
+
laneReadbackDone = false;
|
|
12613
13186
|
/** Timestamp of the last stale branch sweep (at most once per hour) */
|
|
12614
13187
|
lastBranchSweepMs = 0;
|
|
12615
13188
|
/** Current tick-phase activity visible to the dashboard */
|
|
@@ -12683,7 +13256,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
12683
13256
|
this.localResolvers.set(name, new LocalModelResolver(resolverOpts));
|
|
12684
13257
|
}
|
|
12685
13258
|
}
|
|
12686
|
-
this.cacheMetrics = new
|
|
13259
|
+
this.cacheMetrics = new import_core17.CacheMetricsRecorder();
|
|
12687
13260
|
if (this.config.agent.backends !== void 0 && Object.keys(this.config.agent.backends).length > 0) {
|
|
12688
13261
|
const sandboxPolicy = this.config.agent.sandboxPolicy === "docker" ? "docker" : "none";
|
|
12689
13262
|
const firstBackendName = Object.keys(this.config.agent.backends)[0];
|
|
@@ -12769,7 +13342,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
12769
13342
|
this.setupNotifications(config.notifications);
|
|
12770
13343
|
const otlpCfg = config.telemetry?.export?.otlp;
|
|
12771
13344
|
if (otlpCfg) {
|
|
12772
|
-
this.otlpExporter = new
|
|
13345
|
+
this.otlpExporter = new import_core17.OTLPExporter({
|
|
12773
13346
|
endpoint: otlpCfg.endpoint,
|
|
12774
13347
|
...otlpCfg.enabled !== void 0 ? { enabled: otlpCfg.enabled } : {},
|
|
12775
13348
|
...otlpCfg.headers !== void 0 ? { headers: otlpCfg.headers } : {},
|
|
@@ -12838,7 +13411,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
12838
13411
|
...this.config.tracker.apiKey ? { token: this.config.tracker.apiKey } : {},
|
|
12839
13412
|
...this.config.tracker.endpoint ? { apiBase: this.config.tracker.endpoint } : {}
|
|
12840
13413
|
};
|
|
12841
|
-
const clientResult = (0,
|
|
13414
|
+
const clientResult = (0, import_core16.createTrackerClient)(trackerCfg);
|
|
12842
13415
|
if (!clientResult.ok) throw clientResult.error;
|
|
12843
13416
|
return new GitHubIssuesIssueTrackerAdapter(clientResult.value, this.config.tracker);
|
|
12844
13417
|
}
|
|
@@ -12856,48 +13429,30 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
12856
13429
|
const logger = this.logger;
|
|
12857
13430
|
const checkRunner = {
|
|
12858
13431
|
run: async (command, cwd) => {
|
|
12859
|
-
const
|
|
12860
|
-
|
|
12861
|
-
|
|
12862
|
-
const [cmd, ...args] = command;
|
|
12863
|
-
if (!cmd) return { passed: true, findings: 0, output: "" };
|
|
12864
|
-
try {
|
|
12865
|
-
const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
|
|
12866
|
-
const findingsMatch = stdout.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
|
|
12867
|
-
const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 0;
|
|
12868
|
-
return { passed: findings === 0, findings, output: stdout };
|
|
12869
|
-
} catch (err) {
|
|
12870
|
-
const error = err;
|
|
12871
|
-
const output = [error.stdout, error.stderr].filter(Boolean).join("\n");
|
|
12872
|
-
const findingsMatch = output.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
|
|
12873
|
-
const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 1;
|
|
12874
|
-
return { passed: false, findings, output };
|
|
12875
|
-
}
|
|
12876
|
-
}
|
|
12877
|
-
};
|
|
12878
|
-
const agentDispatcher = {
|
|
12879
|
-
dispatch: async (skill, branch, backendName, cwd) => {
|
|
12880
|
-
logger.info(
|
|
12881
|
-
"Maintenance agent dispatcher invoked (stub \u2014 skill dispatch integration pending)",
|
|
12882
|
-
{
|
|
12883
|
-
skill,
|
|
12884
|
-
branch,
|
|
12885
|
-
backendName,
|
|
12886
|
-
cwd
|
|
12887
|
-
}
|
|
12888
|
-
);
|
|
12889
|
-
return { producedCommits: false, fixed: 0 };
|
|
13432
|
+
const [cmd, ...args] = normalizeHarnessCommand(command);
|
|
13433
|
+
if (!cmd) return { passed: true, findings: 0, output: "", executionFailed: false };
|
|
13434
|
+
return runHarnessCheck({ file: cmd, args }, cwd);
|
|
12890
13435
|
}
|
|
12891
13436
|
};
|
|
13437
|
+
const resolveBackend2 = makeBackendResolver(this.getBackends());
|
|
13438
|
+
const agentDispatcher = createAgentDispatcher({
|
|
13439
|
+
resolveBackend: resolveBackend2,
|
|
13440
|
+
git: (args, cwd) => (0, import_node_child_process13.execFileSync)("git", args, { cwd, encoding: "utf-8" }).toString().trim(),
|
|
13441
|
+
logger
|
|
13442
|
+
});
|
|
12892
13443
|
const commandExecutor = {
|
|
12893
13444
|
exec: async (command, cwd) => {
|
|
12894
13445
|
const { execFile: execFile7 } = await import("child_process");
|
|
12895
|
-
const { promisify:
|
|
12896
|
-
const execFileAsync2 =
|
|
12897
|
-
const [cmd, ...args] = command;
|
|
13446
|
+
const { promisify: promisify6 } = await import("util");
|
|
13447
|
+
const execFileAsync2 = promisify6(execFile7);
|
|
13448
|
+
const [cmd, ...args] = normalizeHarnessCommand(command);
|
|
12898
13449
|
if (!cmd) return { stdout: "" };
|
|
12899
13450
|
try {
|
|
12900
|
-
const { stdout } = await execFileAsync2(cmd, args, {
|
|
13451
|
+
const { stdout } = await execFileAsync2(cmd, args, {
|
|
13452
|
+
cwd,
|
|
13453
|
+
timeout: MAINTENANCE_CHECK_TIMEOUT_MS,
|
|
13454
|
+
maxBuffer: MAINTENANCE_CHECK_MAX_BUFFER
|
|
13455
|
+
});
|
|
12901
13456
|
return { stdout: String(stdout) };
|
|
12902
13457
|
} catch (err) {
|
|
12903
13458
|
logger.warn("Maintenance command execution failed", {
|
|
@@ -13032,6 +13587,10 @@ ${messages}`);
|
|
|
13032
13587
|
async asyncTick() {
|
|
13033
13588
|
await this.ensureClaimManager();
|
|
13034
13589
|
await this.intelligenceRunner.loadPersistedData();
|
|
13590
|
+
if (!this.laneReadbackDone) {
|
|
13591
|
+
this.laneReadbackDone = true;
|
|
13592
|
+
await this.readBackPersistedLanes();
|
|
13593
|
+
}
|
|
13035
13594
|
const nowMs = Date.now();
|
|
13036
13595
|
this.setTickActivity("fetching", "Polling tracker for candidates");
|
|
13037
13596
|
const candidatesResult = await this.tracker.fetchCandidateIssues();
|
|
@@ -13184,12 +13743,48 @@ ${messages}`);
|
|
|
13184
13743
|
break;
|
|
13185
13744
|
case "escalate":
|
|
13186
13745
|
await this.handleEscalation(effect);
|
|
13746
|
+
await this.persistLaneSafe(effect.issueId, "abandon");
|
|
13187
13747
|
break;
|
|
13188
13748
|
case "claim":
|
|
13189
13749
|
await this.handleClaimEffect(effect);
|
|
13190
13750
|
break;
|
|
13191
13751
|
}
|
|
13192
13752
|
}
|
|
13753
|
+
/**
|
|
13754
|
+
* Phase 4 (DLane-5): persist an orchestrator lane transition to the durable
|
|
13755
|
+
* core event log at the effect boundary. NEVER throws — `persistLane` returns
|
|
13756
|
+
* an `Err` Result on failure, which is logged and swallowed here so a
|
|
13757
|
+
* lane-persistence failure can never break orchestrator dispatch.
|
|
13758
|
+
*/
|
|
13759
|
+
async persistLaneSafe(issueId, signal) {
|
|
13760
|
+
const r = await persistLane(this.projectRoot, issueId, signal);
|
|
13761
|
+
if (!r.ok) {
|
|
13762
|
+
this.logger.warn(`lane persist failed for ${issueId} (${signal}): ${r.error.message}`);
|
|
13763
|
+
}
|
|
13764
|
+
}
|
|
13765
|
+
/**
|
|
13766
|
+
* Phase 4 (DLane-5): read persisted task lanes back from the durable log and
|
|
13767
|
+
* log a one-line summary. Stores the projection on `this.persistedLanes` for
|
|
13768
|
+
* observability. Read-only — never feeds reconciliation, never throws.
|
|
13769
|
+
*/
|
|
13770
|
+
async readBackPersistedLanes() {
|
|
13771
|
+
const lanes = await readPersistedLanes(this.projectRoot);
|
|
13772
|
+
this.persistedLanes = lanes;
|
|
13773
|
+
const entries = Object.keys(lanes.tasks).map((id) => `${id}:${lanes.tasks[id]?.lane}`);
|
|
13774
|
+
const nonTerminal = entries.filter((e) => !e.endsWith(":done") && !e.endsWith(":canceled"));
|
|
13775
|
+
this.logger.info(
|
|
13776
|
+
`Lane read-back on startup: ${entries.length} persisted task(s), ${nonTerminal.length} non-terminal`,
|
|
13777
|
+
{ nonTerminal }
|
|
13778
|
+
);
|
|
13779
|
+
}
|
|
13780
|
+
/**
|
|
13781
|
+
* Phase 4 (DLane-5): the task lanes most recently read back from the durable
|
|
13782
|
+
* log on startup, exposed for external observability. Read-only — a fresh
|
|
13783
|
+
* `{ tasks: {} }` until the first tick's read-back has run.
|
|
13784
|
+
*/
|
|
13785
|
+
getPersistedLanes() {
|
|
13786
|
+
return this.persistedLanes;
|
|
13787
|
+
}
|
|
13193
13788
|
/**
|
|
13194
13789
|
* Guards workspace cleanup by checking whether the agent pushed a branch
|
|
13195
13790
|
* that does not yet have a pull request. If so, the worktree is preserved
|
|
@@ -13222,7 +13817,7 @@ ${messages}`);
|
|
|
13222
13817
|
{ issueId }
|
|
13223
13818
|
);
|
|
13224
13819
|
await this.interactionQueue.push({
|
|
13225
|
-
id: `interaction-${(0,
|
|
13820
|
+
id: `interaction-${(0, import_node_crypto15.randomUUID)()}`,
|
|
13226
13821
|
issueId,
|
|
13227
13822
|
type: "needs-human",
|
|
13228
13823
|
reasons: [`Agent pushed branch "${branch}" but did not create a PR. Worktree preserved.`],
|
|
@@ -13298,7 +13893,7 @@ ${messages}`);
|
|
|
13298
13893
|
{ issueId: effect.issueId }
|
|
13299
13894
|
);
|
|
13300
13895
|
await this.interactionQueue.push({
|
|
13301
|
-
id: `interaction-${(0,
|
|
13896
|
+
id: `interaction-${(0, import_node_crypto15.randomUUID)()}`,
|
|
13302
13897
|
issueId: effect.issueId,
|
|
13303
13898
|
type: "needs-human",
|
|
13304
13899
|
reasons: effect.reasons,
|
|
@@ -13376,6 +13971,7 @@ ${messages}`);
|
|
|
13376
13971
|
}
|
|
13377
13972
|
return;
|
|
13378
13973
|
}
|
|
13974
|
+
await this.persistLaneSafe(effect.issue.id, "claim");
|
|
13379
13975
|
await this.postClaimComment(effect.issue);
|
|
13380
13976
|
await this.dispatchIssue(effect.issue, effect.attempt, effect.backend);
|
|
13381
13977
|
}
|
|
@@ -13394,12 +13990,12 @@ ${messages}`);
|
|
|
13394
13990
|
async postLifecycleComment(identifier, externalId, event) {
|
|
13395
13991
|
try {
|
|
13396
13992
|
if (!externalId) return;
|
|
13397
|
-
const trackerConfig = (0,
|
|
13993
|
+
const trackerConfig = (0, import_core16.loadTrackerSyncConfig)(this.projectRoot);
|
|
13398
13994
|
if (!trackerConfig) return;
|
|
13399
13995
|
const token = process.env.GITHUB_TOKEN;
|
|
13400
13996
|
if (!token) return;
|
|
13401
13997
|
const orchestratorId = await this.orchestratorIdPromise;
|
|
13402
|
-
const adapter = new
|
|
13998
|
+
const adapter = new import_core16.GitHubIssuesSyncAdapter({ token, config: trackerConfig });
|
|
13403
13999
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
|
|
13404
14000
|
const actionMap = {
|
|
13405
14001
|
claimed: "Dispatching agent for autonomous execution",
|
|
@@ -13436,6 +14032,7 @@ ${messages}`);
|
|
|
13436
14032
|
this.logger.info(`Dispatching issue: ${issue.identifier} (attempt ${attempt})`, {
|
|
13437
14033
|
issueId: issue.id
|
|
13438
14034
|
});
|
|
14035
|
+
await this.persistLaneSafe(issue.id, "dispatch");
|
|
13439
14036
|
try {
|
|
13440
14037
|
const workspaceResult = await this.workspace.ensureWorkspace(issue.identifier);
|
|
13441
14038
|
if (!workspaceResult.ok) throw workspaceResult.error;
|
|
@@ -13472,7 +14069,7 @@ ${messages}`);
|
|
|
13472
14069
|
...f.line !== void 0 ? { line: f.line } : {}
|
|
13473
14070
|
}))
|
|
13474
14071
|
);
|
|
13475
|
-
(0,
|
|
14072
|
+
(0, import_core15.writeTaint)(
|
|
13476
14073
|
workspacePath,
|
|
13477
14074
|
issue.id,
|
|
13478
14075
|
"Medium-severity injection patterns found in workspace config files",
|
|
@@ -13644,6 +14241,7 @@ ${messages}`);
|
|
|
13644
14241
|
* Informs the state machine that an agent worker has exited.
|
|
13645
14242
|
*/
|
|
13646
14243
|
async emitWorkerExit(issueId, reason, attempt, error) {
|
|
14244
|
+
await this.persistLaneSafe(issueId, reason === "normal" ? "success" : "failure");
|
|
13647
14245
|
await this.completionHandler.handleWorkerExit(
|
|
13648
14246
|
issueId,
|
|
13649
14247
|
reason,
|
|
@@ -14179,11 +14777,11 @@ function launchTUI(orchestrator) {
|
|
|
14179
14777
|
}
|
|
14180
14778
|
|
|
14181
14779
|
// src/maintenance/sync-main.ts
|
|
14182
|
-
var
|
|
14183
|
-
var
|
|
14780
|
+
var import_node_child_process14 = require("child_process");
|
|
14781
|
+
var import_node_util5 = require("util");
|
|
14184
14782
|
var DEFAULT_TIMEOUT_MS3 = 6e4;
|
|
14185
14783
|
async function git(execFileFn, args, cwd, timeoutMs) {
|
|
14186
|
-
const exec = (0,
|
|
14784
|
+
const exec = (0, import_node_util5.promisify)(execFileFn);
|
|
14187
14785
|
const { stdout, stderr } = await exec("git", args, { cwd, timeout: timeoutMs });
|
|
14188
14786
|
return { stdout: String(stdout), stderr: String(stderr) };
|
|
14189
14787
|
}
|
|
@@ -14245,7 +14843,7 @@ async function isAncestor(execFileFn, a, b, cwd, timeoutMs) {
|
|
|
14245
14843
|
}
|
|
14246
14844
|
}
|
|
14247
14845
|
async function syncMain(repoRoot, opts = {}) {
|
|
14248
|
-
const execFileFn = opts.execFileFn ??
|
|
14846
|
+
const execFileFn = opts.execFileFn ?? import_node_child_process14.execFile;
|
|
14249
14847
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
|
|
14250
14848
|
try {
|
|
14251
14849
|
const originRef = await resolveOriginDefault(execFileFn, repoRoot, timeoutMs);
|
|
@@ -14322,11 +14920,59 @@ async function syncMain(repoRoot, opts = {}) {
|
|
|
14322
14920
|
}
|
|
14323
14921
|
}
|
|
14324
14922
|
|
|
14923
|
+
// src/maintenance/overdue.ts
|
|
14924
|
+
var LOOKBACK_DAYS = 366;
|
|
14925
|
+
function previousFireTime(schedule, now) {
|
|
14926
|
+
let day = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
14927
|
+
for (let d = 0; d <= LOOKBACK_DAYS; d++) {
|
|
14928
|
+
if (cronMatchesDate(schedule, day)) {
|
|
14929
|
+
const dayStartMs = day.getTime();
|
|
14930
|
+
const scanStart = d === 0 ? new Date(
|
|
14931
|
+
now.getFullYear(),
|
|
14932
|
+
now.getMonth(),
|
|
14933
|
+
now.getDate(),
|
|
14934
|
+
now.getHours(),
|
|
14935
|
+
now.getMinutes()
|
|
14936
|
+
) : new Date(day.getFullYear(), day.getMonth(), day.getDate(), 23, 59);
|
|
14937
|
+
for (let ms = scanStart.getTime(); ms >= dayStartMs; ms -= 6e4) {
|
|
14938
|
+
const candidate = new Date(ms);
|
|
14939
|
+
if (cronMatchesNow(schedule, candidate)) return candidate;
|
|
14940
|
+
}
|
|
14941
|
+
}
|
|
14942
|
+
day = new Date(day.getFullYear(), day.getMonth(), day.getDate() - 1);
|
|
14943
|
+
}
|
|
14944
|
+
return null;
|
|
14945
|
+
}
|
|
14946
|
+
function isSatisfyingRun(r) {
|
|
14947
|
+
return r.status === "success" || r.status === "no-issues";
|
|
14948
|
+
}
|
|
14949
|
+
function isOverdue(task, history, now) {
|
|
14950
|
+
const satisfyingRuns = history.filter((r) => r.taskId === task.id && isSatisfyingRun(r));
|
|
14951
|
+
const fire = previousFireTime(task.schedule, now);
|
|
14952
|
+
if (fire === null) return satisfyingRuns.length === 0;
|
|
14953
|
+
const fireMs = fire.getTime();
|
|
14954
|
+
const satisfied = satisfyingRuns.some((r) => new Date(r.completedAt).getTime() >= fireMs);
|
|
14955
|
+
return !satisfied;
|
|
14956
|
+
}
|
|
14957
|
+
function selectTasks(tasks, history, filter) {
|
|
14958
|
+
const eligible = tasks.filter((t) => t.excludeFromHumanSweep !== true);
|
|
14959
|
+
switch (filter.mode) {
|
|
14960
|
+
case "all":
|
|
14961
|
+
return eligible;
|
|
14962
|
+
case "ids": {
|
|
14963
|
+
const wanted = new Set(filter.ids ?? []);
|
|
14964
|
+
return eligible.filter((t) => wanted.has(t.id));
|
|
14965
|
+
}
|
|
14966
|
+
case "overdue":
|
|
14967
|
+
return eligible.filter((t) => isOverdue(t, history, filter.now));
|
|
14968
|
+
}
|
|
14969
|
+
}
|
|
14970
|
+
|
|
14325
14971
|
// src/sessions/search-index.ts
|
|
14326
|
-
var
|
|
14972
|
+
var fs16 = __toESM(require("fs"));
|
|
14327
14973
|
var path22 = __toESM(require("path"));
|
|
14328
14974
|
var import_better_sqlite32 = __toESM(require("better-sqlite3"));
|
|
14329
|
-
var
|
|
14975
|
+
var import_types32 = require("@harness-engineering/types");
|
|
14330
14976
|
var SEARCH_INDEX_FILE = "search-index.sqlite";
|
|
14331
14977
|
var SCHEMA_SQL2 = `
|
|
14332
14978
|
CREATE TABLE IF NOT EXISTS session_docs (
|
|
@@ -14384,7 +15030,7 @@ var SqliteSearchIndex = class {
|
|
|
14384
15030
|
removeSessionStmt;
|
|
14385
15031
|
totalStmt;
|
|
14386
15032
|
constructor(dbPath) {
|
|
14387
|
-
|
|
15033
|
+
fs16.mkdirSync(path22.dirname(dbPath), { recursive: true });
|
|
14388
15034
|
this.db = new import_better_sqlite32.default(dbPath);
|
|
14389
15035
|
this.db.pragma("journal_mode = WAL");
|
|
14390
15036
|
this.db.pragma("synchronous = NORMAL");
|
|
@@ -14484,18 +15130,18 @@ function openSearchIndex(projectPath) {
|
|
|
14484
15130
|
return new SqliteSearchIndex(searchIndexPath(projectPath));
|
|
14485
15131
|
}
|
|
14486
15132
|
function indexSessionDirectory(idx, args) {
|
|
14487
|
-
const kinds = args.fileKinds ?? [...
|
|
15133
|
+
const kinds = args.fileKinds ?? [...import_types32.INDEXED_FILE_KINDS];
|
|
14488
15134
|
const cap = args.maxBytesPerBody ?? 256 * 1024;
|
|
14489
15135
|
let docsWritten = 0;
|
|
14490
15136
|
for (const kind of kinds) {
|
|
14491
15137
|
const fileName = FILE_KIND_TO_FILENAME[kind];
|
|
14492
15138
|
const filePath = path22.join(args.sessionDir, fileName);
|
|
14493
|
-
if (!
|
|
14494
|
-
let body =
|
|
15139
|
+
if (!fs16.existsSync(filePath)) continue;
|
|
15140
|
+
let body = fs16.readFileSync(filePath, "utf8");
|
|
14495
15141
|
if (Buffer.byteLength(body, "utf8") > cap) {
|
|
14496
15142
|
body = body.slice(0, cap) + "\n\n[TRUNCATED]";
|
|
14497
15143
|
}
|
|
14498
|
-
const stat =
|
|
15144
|
+
const stat = fs16.statSync(filePath);
|
|
14499
15145
|
const relPath = path22.relative(args.projectPath, filePath).replaceAll("\\", "/");
|
|
14500
15146
|
idx.upsertSessionDoc({
|
|
14501
15147
|
sessionId: args.sessionId,
|
|
@@ -14517,8 +15163,8 @@ function reindexFromArchive(projectPath, opts = {}) {
|
|
|
14517
15163
|
idx.resetArchived();
|
|
14518
15164
|
let sessionsIndexed = 0;
|
|
14519
15165
|
let docsWritten = 0;
|
|
14520
|
-
if (
|
|
14521
|
-
const entries =
|
|
15166
|
+
if (fs16.existsSync(archiveBase)) {
|
|
15167
|
+
const entries = fs16.readdirSync(archiveBase, { withFileTypes: true });
|
|
14522
15168
|
for (const entry of entries) {
|
|
14523
15169
|
if (!entry.isDirectory()) continue;
|
|
14524
15170
|
const sessionDir = path22.join(archiveBase, entry.name);
|
|
@@ -14541,10 +15187,10 @@ function reindexFromArchive(projectPath, opts = {}) {
|
|
|
14541
15187
|
}
|
|
14542
15188
|
|
|
14543
15189
|
// src/sessions/summarize.ts
|
|
14544
|
-
var
|
|
15190
|
+
var fs17 = __toESM(require("fs"));
|
|
14545
15191
|
var path23 = __toESM(require("path"));
|
|
14546
|
-
var import_types32 = require("@harness-engineering/types");
|
|
14547
15192
|
var import_types33 = require("@harness-engineering/types");
|
|
15193
|
+
var import_types34 = require("@harness-engineering/types");
|
|
14548
15194
|
var LLM_SUMMARY_FILE = "llm-summary.md";
|
|
14549
15195
|
var SUMMARY_INPUT_FILES = [
|
|
14550
15196
|
{ filename: "summary.md", kind: "summary" },
|
|
@@ -14571,9 +15217,9 @@ function readInputCorpus(archiveDir) {
|
|
|
14571
15217
|
const parts = [];
|
|
14572
15218
|
for (const { filename, kind } of SUMMARY_INPUT_FILES) {
|
|
14573
15219
|
const p = path23.join(archiveDir, filename);
|
|
14574
|
-
if (!
|
|
15220
|
+
if (!fs17.existsSync(p)) continue;
|
|
14575
15221
|
try {
|
|
14576
|
-
const content =
|
|
15222
|
+
const content = fs17.readFileSync(p, "utf8");
|
|
14577
15223
|
if (content.trim().length === 0) continue;
|
|
14578
15224
|
parts.push(`## FILE: ${kind}
|
|
14579
15225
|
|
|
@@ -14635,17 +15281,17 @@ status: failed
|
|
|
14635
15281
|
|
|
14636
15282
|
- reason: ${reason}
|
|
14637
15283
|
`;
|
|
14638
|
-
|
|
15284
|
+
fs17.writeFileSync(filePath, body, "utf8");
|
|
14639
15285
|
return filePath;
|
|
14640
15286
|
}
|
|
14641
15287
|
async function summarizeArchivedSession(ctx) {
|
|
14642
15288
|
const writeStubOnError = ctx.writeStubOnError ?? true;
|
|
14643
|
-
if (!
|
|
14644
|
-
return (0,
|
|
15289
|
+
if (!fs17.existsSync(ctx.archiveDir)) {
|
|
15290
|
+
return (0, import_types34.Err)(new Error(`archive directory not found: ${ctx.archiveDir}`));
|
|
14645
15291
|
}
|
|
14646
15292
|
const corpus = readInputCorpus(ctx.archiveDir);
|
|
14647
15293
|
if (corpus.trim().length === 0) {
|
|
14648
|
-
return (0,
|
|
15294
|
+
return (0, import_types34.Err)(new Error(`no summary input files found in ${ctx.archiveDir}`));
|
|
14649
15295
|
}
|
|
14650
15296
|
const inputBudgetTokens = ctx.config?.inputBudgetTokens ?? DEFAULT_INPUT_BUDGET_TOKENS;
|
|
14651
15297
|
const truncated = truncateForBudget(corpus, inputBudgetTokens);
|
|
@@ -14654,7 +15300,7 @@ async function summarizeArchivedSession(ctx) {
|
|
|
14654
15300
|
const analyzeOpts = {
|
|
14655
15301
|
prompt,
|
|
14656
15302
|
systemPrompt: SYSTEM_PROMPT,
|
|
14657
|
-
responseSchema:
|
|
15303
|
+
responseSchema: import_types33.SessionSummarySchema,
|
|
14658
15304
|
...ctx.config?.model && { model: ctx.config.model }
|
|
14659
15305
|
};
|
|
14660
15306
|
let response;
|
|
@@ -14678,11 +15324,11 @@ async function summarizeArchivedSession(ctx) {
|
|
|
14678
15324
|
} catch {
|
|
14679
15325
|
}
|
|
14680
15326
|
}
|
|
14681
|
-
return (0,
|
|
15327
|
+
return (0, import_types34.Err)(
|
|
14682
15328
|
new Error(`session summary failed: ${reason}` + (stubPath ? ` (stub: ${stubPath})` : ""))
|
|
14683
15329
|
);
|
|
14684
15330
|
}
|
|
14685
|
-
const parsed =
|
|
15331
|
+
const parsed = import_types33.SessionSummarySchema.safeParse(response.result);
|
|
14686
15332
|
if (!parsed.success) {
|
|
14687
15333
|
const reason = `schema validation failed: ${parsed.error.message}`;
|
|
14688
15334
|
ctx.logger?.warn?.("session summary: invalid provider payload", { reason });
|
|
@@ -14692,7 +15338,7 @@ async function summarizeArchivedSession(ctx) {
|
|
|
14692
15338
|
} catch {
|
|
14693
15339
|
}
|
|
14694
15340
|
}
|
|
14695
|
-
return (0,
|
|
15341
|
+
return (0, import_types34.Err)(new Error(reason));
|
|
14696
15342
|
}
|
|
14697
15343
|
const meta = {
|
|
14698
15344
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -14703,8 +15349,8 @@ async function summarizeArchivedSession(ctx) {
|
|
|
14703
15349
|
};
|
|
14704
15350
|
const filePath = path23.join(ctx.archiveDir, LLM_SUMMARY_FILE);
|
|
14705
15351
|
const body = renderLlmSummaryMarkdown(parsed.data, meta);
|
|
14706
|
-
|
|
14707
|
-
return (0,
|
|
15352
|
+
fs17.writeFileSync(filePath, body, "utf8");
|
|
15353
|
+
return (0, import_types34.Ok)({ summary: parsed.data, meta, filePath });
|
|
14708
15354
|
}
|
|
14709
15355
|
function isSummaryEnabled(config) {
|
|
14710
15356
|
if (!config) return false;
|
|
@@ -14784,13 +15430,18 @@ function buildArchiveHooks(opts) {
|
|
|
14784
15430
|
BUILT_IN_TASKS,
|
|
14785
15431
|
BackendDefSchema,
|
|
14786
15432
|
BackendRouter,
|
|
15433
|
+
CheckScriptRunner,
|
|
14787
15434
|
ClaimManager,
|
|
14788
15435
|
GateNotReadyError,
|
|
14789
15436
|
GateRunError,
|
|
14790
15437
|
InteractionQueue,
|
|
15438
|
+
LinearGraphQLClient,
|
|
14791
15439
|
LinearGraphQLStub,
|
|
14792
15440
|
LocalModelResolver,
|
|
15441
|
+
MAINTENANCE_CHECK_MAX_BUFFER,
|
|
15442
|
+
MAINTENANCE_CHECK_TIMEOUT_MS,
|
|
14793
15443
|
MAX_ATTEMPTS,
|
|
15444
|
+
MaintenanceReporter,
|
|
14794
15445
|
MockBackend,
|
|
14795
15446
|
ORCHESTRATOR_IDENTITY_FILE,
|
|
14796
15447
|
Orchestrator,
|
|
@@ -14808,6 +15459,7 @@ function buildArchiveHooks(opts) {
|
|
|
14808
15459
|
SqliteSearchIndex,
|
|
14809
15460
|
StreamRecorder,
|
|
14810
15461
|
TaskOutputStore,
|
|
15462
|
+
TaskRunner,
|
|
14811
15463
|
TokenStore,
|
|
14812
15464
|
WebhookQueue,
|
|
14813
15465
|
WorkflowLoader,
|
|
@@ -14818,7 +15470,9 @@ function buildArchiveHooks(opts) {
|
|
|
14818
15470
|
buildArchiveHooks,
|
|
14819
15471
|
calculateRetryDelay,
|
|
14820
15472
|
canDispatch,
|
|
15473
|
+
classifyCheckExecutionFailure,
|
|
14821
15474
|
computeRateLimitDelay,
|
|
15475
|
+
createAgentDispatcher,
|
|
14822
15476
|
createBackend,
|
|
14823
15477
|
createEmptyState,
|
|
14824
15478
|
crossFieldRoutingIssues,
|
|
@@ -14830,22 +15484,27 @@ function buildArchiveHooks(opts) {
|
|
|
14830
15484
|
emitProposalApproved,
|
|
14831
15485
|
emitProposalCreated,
|
|
14832
15486
|
emitProposalRejected,
|
|
15487
|
+
explicitFindingsCount,
|
|
14833
15488
|
extractHighlights,
|
|
14834
15489
|
extractTitlePrefix,
|
|
14835
15490
|
getAvailableSlots,
|
|
14836
15491
|
getDefaultConfig,
|
|
14837
15492
|
getPerStateCount,
|
|
14838
15493
|
indexSessionDirectory,
|
|
15494
|
+
isCheckTimeoutError,
|
|
14839
15495
|
isEligible,
|
|
14840
15496
|
isSummaryEnabled,
|
|
14841
15497
|
launchTUI,
|
|
14842
15498
|
loadPublishedIndex,
|
|
15499
|
+
makeBackendResolver,
|
|
14843
15500
|
migrateAgentConfig,
|
|
14844
15501
|
normalizeFts5Query,
|
|
15502
|
+
normalizeHarnessCommand,
|
|
14845
15503
|
normalizeLocalModel,
|
|
14846
15504
|
openSearchIndex,
|
|
14847
15505
|
promote,
|
|
14848
15506
|
reconcile,
|
|
15507
|
+
recoverFindingsCount,
|
|
14849
15508
|
reindexFromArchive,
|
|
14850
15509
|
renderAnalysisComment,
|
|
14851
15510
|
renderLlmSummaryMarkdown,
|
|
@@ -14855,9 +15514,11 @@ function buildArchiveHooks(opts) {
|
|
|
14855
15514
|
routeIssue,
|
|
14856
15515
|
routingWarnings,
|
|
14857
15516
|
runGate,
|
|
15517
|
+
runHarnessCheck,
|
|
14858
15518
|
savePublishedIndex,
|
|
14859
15519
|
searchIndexPath,
|
|
14860
15520
|
selectCandidates,
|
|
15521
|
+
selectTasks,
|
|
14861
15522
|
sortCandidates,
|
|
14862
15523
|
summarizeArchivedSession,
|
|
14863
15524
|
syncMain,
|