@harness-engineering/orchestrator 0.2.13 → 0.2.15
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 +15 -3
- package/dist/index.d.ts +15 -3
- package/dist/index.js +369 -57
- package/dist/index.mjs +357 -45
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -676,7 +676,26 @@ function handleTick(state, event, config) {
|
|
|
676
676
|
continue;
|
|
677
677
|
}
|
|
678
678
|
const scopeTier = detectScopeTier(issue, artifactPresenceFromIssue(issue));
|
|
679
|
-
const signals = event.concernSignals?.get(issue.id) ?? [];
|
|
679
|
+
const signals = [...event.concernSignals?.get(issue.id) ?? []];
|
|
680
|
+
let suggestedPersona;
|
|
681
|
+
try {
|
|
682
|
+
const personaRecs = event.personaRecommendations?.get(issue.id);
|
|
683
|
+
if (personaRecs && personaRecs.length > 0) {
|
|
684
|
+
suggestedPersona = personaRecs[0].persona;
|
|
685
|
+
if (personaRecs[0].weightedScore < 0.3) {
|
|
686
|
+
signals.push({
|
|
687
|
+
name: "lowExpertise",
|
|
688
|
+
reason: `Top persona "${suggestedPersona}" scored ${personaRecs[0].weightedScore.toFixed(2)} (below 0.3 threshold)`
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
} else if (personaRecs && personaRecs.length === 0) {
|
|
692
|
+
signals.push({
|
|
693
|
+
name: "noPersonaMatch",
|
|
694
|
+
reason: "No persona recommendations available for this issue's systems"
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
} catch {
|
|
698
|
+
}
|
|
680
699
|
const decision = routeIssue(scopeTier, signals, escalationConfig);
|
|
681
700
|
if (decision.action === "needs-human") {
|
|
682
701
|
next.claimed.add(issue.id);
|
|
@@ -692,6 +711,12 @@ function handleTick(state, event, config) {
|
|
|
692
711
|
}
|
|
693
712
|
const backend = resolveBackend(decision.action, !!config.agent.localBackend);
|
|
694
713
|
claimAndDispatch(next, issue, backend, nowMs, effects);
|
|
714
|
+
if (suggestedPersona) {
|
|
715
|
+
const lastEffect = effects[effects.length - 1];
|
|
716
|
+
if (lastEffect && lastEffect.type === "claim") {
|
|
717
|
+
lastEffect.suggestedPersona = suggestedPersona;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
695
720
|
}
|
|
696
721
|
pruneCompleted(next);
|
|
697
722
|
return { nextState: next, effects };
|
|
@@ -818,7 +843,7 @@ function handleAgentUpdate(state, issueId, event) {
|
|
|
818
843
|
}
|
|
819
844
|
return { nextState: next, effects };
|
|
820
845
|
}
|
|
821
|
-
function handleRetryFired(state, issueId, candidates, config, nowMs) {
|
|
846
|
+
function handleRetryFired(state, issueId, candidates, config, nowMs, concernSignals) {
|
|
822
847
|
const next = cloneState(state);
|
|
823
848
|
const effects = [];
|
|
824
849
|
const retryEntry = next.retryAttempts.get(issueId);
|
|
@@ -875,7 +900,8 @@ function handleRetryFired(state, issueId, candidates, config, nowMs) {
|
|
|
875
900
|
}
|
|
876
901
|
const escalationConfig = resolveEscalationConfig(config);
|
|
877
902
|
const scopeTier = detectScopeTier(issue, artifactPresenceFromIssue(issue));
|
|
878
|
-
const
|
|
903
|
+
const signals = [...concernSignals?.get(issue.id) ?? []];
|
|
904
|
+
const decision = routeIssue(scopeTier, signals, escalationConfig);
|
|
879
905
|
if (decision.action === "needs-human") {
|
|
880
906
|
effects.push(
|
|
881
907
|
buildEscalateEffect(issue.id, issue.identifier, decision.reasons, {
|
|
@@ -955,7 +981,14 @@ function applyEvent(state, event, config) {
|
|
|
955
981
|
case "agent_update":
|
|
956
982
|
return handleAgentUpdate(state, event.issueId, event.event);
|
|
957
983
|
case "retry_fired":
|
|
958
|
-
return handleRetryFired(
|
|
984
|
+
return handleRetryFired(
|
|
985
|
+
state,
|
|
986
|
+
event.issueId,
|
|
987
|
+
event.candidates,
|
|
988
|
+
config,
|
|
989
|
+
event.nowMs,
|
|
990
|
+
event.concernSignals
|
|
991
|
+
);
|
|
959
992
|
case "stall_detected":
|
|
960
993
|
return handleStallDetected(state, event.issueId, config);
|
|
961
994
|
case "claim_rejected":
|
|
@@ -2407,10 +2440,15 @@ var WorkspaceHooks = class {
|
|
|
2407
2440
|
return (0, import_types7.Ok)(void 0);
|
|
2408
2441
|
}
|
|
2409
2442
|
return new Promise((resolve6) => {
|
|
2410
|
-
const
|
|
2411
|
-
|
|
2443
|
+
const filteredEnv = {};
|
|
2444
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
2445
|
+
if (v != null && !k.includes("SECRET") && !k.includes("TOKEN") && !k.includes("PASSWORD")) {
|
|
2446
|
+
filteredEnv[k] = v;
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
const child = (0, import_node_child_process3.spawn)("/bin/sh", ["-c", command], {
|
|
2412
2450
|
cwd,
|
|
2413
|
-
env:
|
|
2451
|
+
env: filteredEnv
|
|
2414
2452
|
});
|
|
2415
2453
|
const timeout = setTimeout(() => {
|
|
2416
2454
|
child.kill();
|
|
@@ -2519,11 +2557,12 @@ var import_node_events = require("events");
|
|
|
2519
2557
|
var path15 = __toESM(require("path"));
|
|
2520
2558
|
var import_node_crypto7 = require("crypto");
|
|
2521
2559
|
var import_core9 = require("@harness-engineering/core");
|
|
2522
|
-
var
|
|
2560
|
+
var import_intelligence3 = require("@harness-engineering/intelligence");
|
|
2523
2561
|
var import_graph = require("@harness-engineering/graph");
|
|
2524
2562
|
|
|
2525
2563
|
// src/intelligence/pipeline-runner.ts
|
|
2526
2564
|
var path7 = __toESM(require("path"));
|
|
2565
|
+
var import_intelligence = require("@harness-engineering/intelligence");
|
|
2527
2566
|
var import_core2 = require("@harness-engineering/core");
|
|
2528
2567
|
var CONNECTION_ERROR_PATTERNS = [
|
|
2529
2568
|
"Connection error",
|
|
@@ -2552,6 +2591,7 @@ var IntelligencePipelineRunner = class {
|
|
|
2552
2591
|
this.graphLoaded = true;
|
|
2553
2592
|
await this.loadGraphStore();
|
|
2554
2593
|
await this.hydrateSpecCache();
|
|
2594
|
+
this.refreshSpecializationProfiles();
|
|
2555
2595
|
}
|
|
2556
2596
|
/**
|
|
2557
2597
|
* Runs the full intelligence pipeline for the given candidates:
|
|
@@ -2605,11 +2645,39 @@ var IntelligencePipelineRunner = class {
|
|
|
2605
2645
|
} catch (err) {
|
|
2606
2646
|
this.ctx.logger.warn("Auto-publish analyses failed", { error: String(err) });
|
|
2607
2647
|
}
|
|
2608
|
-
|
|
2648
|
+
this.refreshSpecializationProfiles();
|
|
2649
|
+
setTickActivity("analyzing", "Scoring persona recommendations");
|
|
2650
|
+
const personaRecommendations = this.computePersonaRecommendations(candidates);
|
|
2651
|
+
return {
|
|
2652
|
+
concernSignals,
|
|
2653
|
+
enrichedSpecs,
|
|
2654
|
+
complexityScores,
|
|
2655
|
+
simulationResults,
|
|
2656
|
+
personaRecommendations
|
|
2657
|
+
};
|
|
2609
2658
|
}
|
|
2610
2659
|
// ---------------------------------------------------------------------------
|
|
2611
2660
|
// Private helpers
|
|
2612
2661
|
// ---------------------------------------------------------------------------
|
|
2662
|
+
/**
|
|
2663
|
+
* Refresh specialization profiles from the graph store. This recomputes
|
|
2664
|
+
* expertise scores for all personas with execution_outcome nodes and
|
|
2665
|
+
* persists them to .harness/specialization-profiles.json. Non-fatal.
|
|
2666
|
+
*/
|
|
2667
|
+
refreshSpecializationProfiles() {
|
|
2668
|
+
if (!this.ctx.graphStore) return;
|
|
2669
|
+
try {
|
|
2670
|
+
const store = (0, import_intelligence.refreshProfiles)(this.ctx.projectRoot, this.ctx.graphStore);
|
|
2671
|
+
const personaCount = Object.keys(store.profiles).length;
|
|
2672
|
+
if (personaCount > 0) {
|
|
2673
|
+
this.ctx.logger.info(`Refreshed specialization profiles for ${personaCount} persona(s)`);
|
|
2674
|
+
}
|
|
2675
|
+
} catch (err) {
|
|
2676
|
+
this.ctx.logger.warn("Failed to refresh specialization profiles", {
|
|
2677
|
+
error: String(err)
|
|
2678
|
+
});
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2613
2681
|
async loadGraphStore() {
|
|
2614
2682
|
try {
|
|
2615
2683
|
const graphDir = path7.join(this.ctx.config.workspace.root, "..", "graph");
|
|
@@ -2768,7 +2836,22 @@ var IntelligencePipelineRunner = class {
|
|
|
2768
2836
|
const scopeTier = detectScopeTier(issue, artifactPresenceFromIssue(issue));
|
|
2769
2837
|
try {
|
|
2770
2838
|
const result = await this.ctx.pipeline.preprocessIssue(issue, scopeTier, escalationConfig);
|
|
2771
|
-
|
|
2839
|
+
const signals = [...result.signals];
|
|
2840
|
+
if (result.spec) {
|
|
2841
|
+
if (result.spec.unknowns.length > 3) {
|
|
2842
|
+
signals.push({
|
|
2843
|
+
name: "high-unknowns",
|
|
2844
|
+
reason: `Enriched spec has ${result.spec.unknowns.length} unknowns (threshold: 3)`
|
|
2845
|
+
});
|
|
2846
|
+
}
|
|
2847
|
+
if (result.spec.ambiguities.length > 5) {
|
|
2848
|
+
signals.push({
|
|
2849
|
+
name: "high-ambiguities",
|
|
2850
|
+
reason: `Enriched spec has ${result.spec.ambiguities.length} ambiguities (threshold: 5)`
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
if (signals.length > 0) concernSignals.set(issue.id, signals);
|
|
2772
2855
|
if (result.spec) {
|
|
2773
2856
|
enrichedSpecs.set(issue.id, result.spec);
|
|
2774
2857
|
this.ctx.enrichedSpecsByIssue.set(issue.id, result.spec);
|
|
@@ -2815,6 +2898,30 @@ var IntelligencePipelineRunner = class {
|
|
|
2815
2898
|
return true;
|
|
2816
2899
|
});
|
|
2817
2900
|
}
|
|
2901
|
+
/**
|
|
2902
|
+
* Compute persona recommendations for each candidate using the specialization
|
|
2903
|
+
* scorer. Extracts system node IDs from issue labels prefixed with `system:`
|
|
2904
|
+
* or `module:`. Failures are non-fatal — returns an empty map on error.
|
|
2905
|
+
*/
|
|
2906
|
+
computePersonaRecommendations(candidates) {
|
|
2907
|
+
const results = /* @__PURE__ */ new Map();
|
|
2908
|
+
if (!this.ctx.graphStore) return results;
|
|
2909
|
+
try {
|
|
2910
|
+
for (const issue of candidates) {
|
|
2911
|
+
const systemNodeIds = issue.labels.filter((l) => l.startsWith("system:") || l.startsWith("module:")).map((l) => l.split(":")[1]).filter((id) => id.length > 0);
|
|
2912
|
+
if (systemNodeIds.length === 0) continue;
|
|
2913
|
+
const recs = (0, import_intelligence.weightedRecommendPersona)(this.ctx.graphStore, { systemNodeIds });
|
|
2914
|
+
if (recs.length > 0) {
|
|
2915
|
+
results.set(issue.id, recs);
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
} catch (err) {
|
|
2919
|
+
this.ctx.logger.warn("Persona recommendation scoring failed", {
|
|
2920
|
+
error: String(err)
|
|
2921
|
+
});
|
|
2922
|
+
}
|
|
2923
|
+
return results;
|
|
2924
|
+
}
|
|
2818
2925
|
async analyzeCandidates(eligibleCandidates, escalationConfig, nowForCache, failureTtl, circuitBreakerThreshold, concernSignals, enrichedSpecs, complexityScores, setTickActivity) {
|
|
2819
2926
|
let processed = 0;
|
|
2820
2927
|
let consecutiveConnErrors = 0;
|
|
@@ -2892,10 +2999,27 @@ var CompletionHandler = class {
|
|
|
2892
2999
|
// ---------------------------------------------------------------------------
|
|
2893
3000
|
// Private helpers
|
|
2894
3001
|
// ---------------------------------------------------------------------------
|
|
3002
|
+
/**
|
|
3003
|
+
* Infer a TaskType from issue labels.
|
|
3004
|
+
* Looks for common label patterns: bug/bugfix → 'bugfix', feat/feature → 'feature', etc.
|
|
3005
|
+
*/
|
|
3006
|
+
inferTaskType(labels) {
|
|
3007
|
+
const joined = labels.map((l) => l.toLowerCase()).join(" ");
|
|
3008
|
+
if (/\bbug(fix)?\b/.test(joined)) return "bugfix";
|
|
3009
|
+
if (/\bfeat(ure)?\b/.test(joined)) return "feature";
|
|
3010
|
+
if (/\brefactor\b/.test(joined)) return "refactor";
|
|
3011
|
+
if (/\bdoc(s|umentation)?\b/.test(joined)) return "docs";
|
|
3012
|
+
if (/\btest(s|ing)?\b/.test(joined)) return "test";
|
|
3013
|
+
if (/\bchore\b/.test(joined)) return "chore";
|
|
3014
|
+
return void 0;
|
|
3015
|
+
}
|
|
2895
3016
|
async recordOutcomeIfPipelineEnabled(issueId, reason, attempt, error, entry) {
|
|
2896
3017
|
if (!this.ctx.pipeline) return;
|
|
2897
3018
|
const enrichedSpec = this.ctx.enrichedSpecsByIssue.get(issueId);
|
|
2898
3019
|
const affectedSystemNodeIds = enrichedSpec ? enrichedSpec.affectedSystems.filter((s) => s.graphNodeId !== null).map((s) => s.graphNodeId) : [];
|
|
3020
|
+
const agentPersona = entry?.session?.backendName ?? this.ctx.config.agent.backend ?? "default";
|
|
3021
|
+
const labels = entry?.issue?.labels ?? [];
|
|
3022
|
+
const taskType = this.inferTaskType(labels);
|
|
2899
3023
|
const outcome = {
|
|
2900
3024
|
id: `outcome:${issueId}:${attempt ?? 0}`,
|
|
2901
3025
|
issueId,
|
|
@@ -2906,7 +3030,9 @@ var CompletionHandler = class {
|
|
|
2906
3030
|
durationMs: entry ? Date.now() - new Date(entry.startedAt).getTime() : 0,
|
|
2907
3031
|
linkedSpecId: enrichedSpec?.id ?? null,
|
|
2908
3032
|
affectedSystemNodeIds,
|
|
2909
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3033
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3034
|
+
agentPersona,
|
|
3035
|
+
...taskType ? { taskType } : {}
|
|
2910
3036
|
};
|
|
2911
3037
|
try {
|
|
2912
3038
|
this.ctx.pipeline.recordOutcome(outcome);
|
|
@@ -4948,7 +5074,7 @@ function extractChunks(event) {
|
|
|
4948
5074
|
}
|
|
4949
5075
|
|
|
4950
5076
|
// src/server/routes/analyze.ts
|
|
4951
|
-
var
|
|
5077
|
+
var import_intelligence2 = require("@harness-engineering/intelligence");
|
|
4952
5078
|
var import_zod4 = require("zod");
|
|
4953
5079
|
var AnalyzeRequestSchema = import_zod4.z.object({
|
|
4954
5080
|
title: import_zod4.z.string().min(1),
|
|
@@ -4978,7 +5104,7 @@ async function runPipeline(res, pipeline, parsed) {
|
|
|
4978
5104
|
disconnected = true;
|
|
4979
5105
|
});
|
|
4980
5106
|
emit2(res, { type: "status", text: "Converting to work item..." });
|
|
4981
|
-
const rawItem = (0,
|
|
5107
|
+
const rawItem = (0, import_intelligence2.manualToRawWorkItem)({
|
|
4982
5108
|
title: parsed.title,
|
|
4983
5109
|
description: parsed.description ?? "",
|
|
4984
5110
|
labels: parsed.labels ?? []
|
|
@@ -5021,7 +5147,7 @@ async function runPipeline(res, pipeline, parsed) {
|
|
|
5021
5147
|
}
|
|
5022
5148
|
}
|
|
5023
5149
|
if (disconnected) return;
|
|
5024
|
-
const signals = (0,
|
|
5150
|
+
const signals = (0, import_intelligence2.scoreToConcernSignals)(score);
|
|
5025
5151
|
if (signals.length > 0) {
|
|
5026
5152
|
emit2(res, { type: "signals", data: signals });
|
|
5027
5153
|
}
|
|
@@ -5619,6 +5745,37 @@ var PlanWatcher = class {
|
|
|
5619
5745
|
};
|
|
5620
5746
|
|
|
5621
5747
|
// src/server/http.ts
|
|
5748
|
+
var RATE_LIMIT = Number(process.env["HARNESS_RATE_LIMIT"]) || 100;
|
|
5749
|
+
var WINDOW_MS = 6e4;
|
|
5750
|
+
var rateBuckets = /* @__PURE__ */ new Map();
|
|
5751
|
+
var ratePruneTimer = setInterval(() => {
|
|
5752
|
+
const now = Date.now();
|
|
5753
|
+
for (const [ip, bucket] of rateBuckets) {
|
|
5754
|
+
if (bucket.resetAt <= now) rateBuckets.delete(ip);
|
|
5755
|
+
}
|
|
5756
|
+
}, 6e4);
|
|
5757
|
+
ratePruneTimer.unref();
|
|
5758
|
+
function isLocalhost(ip) {
|
|
5759
|
+
return ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1";
|
|
5760
|
+
}
|
|
5761
|
+
function checkRateLimit(req, res) {
|
|
5762
|
+
const ip = req.socket.remoteAddress ?? "unknown";
|
|
5763
|
+
if (!process.env["HARNESS_RATE_LIMIT_LOCALHOST"] && isLocalhost(ip)) return true;
|
|
5764
|
+
const now = Date.now();
|
|
5765
|
+
let bucket = rateBuckets.get(ip);
|
|
5766
|
+
if (!bucket || bucket.resetAt <= now) {
|
|
5767
|
+
bucket = { count: 0, resetAt: now + WINDOW_MS };
|
|
5768
|
+
rateBuckets.set(ip, bucket);
|
|
5769
|
+
}
|
|
5770
|
+
bucket.count++;
|
|
5771
|
+
if (bucket.count > RATE_LIMIT) {
|
|
5772
|
+
const retryAfter = Math.ceil((bucket.resetAt - now) / 1e3);
|
|
5773
|
+
res.writeHead(429, { "Content-Type": "application/json", "Retry-After": String(retryAfter) });
|
|
5774
|
+
res.end(JSON.stringify({ error: "Too Many Requests" }));
|
|
5775
|
+
return false;
|
|
5776
|
+
}
|
|
5777
|
+
return true;
|
|
5778
|
+
}
|
|
5622
5779
|
function getBindHost() {
|
|
5623
5780
|
return process.env["HOST"] ?? "127.0.0.1";
|
|
5624
5781
|
}
|
|
@@ -5706,6 +5863,9 @@ var OrchestratorServer = class {
|
|
|
5706
5863
|
if (this.handleStateEndpoint(req, res)) {
|
|
5707
5864
|
return;
|
|
5708
5865
|
}
|
|
5866
|
+
if (!checkRateLimit(req, res)) {
|
|
5867
|
+
return;
|
|
5868
|
+
}
|
|
5709
5869
|
if (this.handleApiRoutes(req, res)) {
|
|
5710
5870
|
return;
|
|
5711
5871
|
}
|
|
@@ -6326,6 +6486,18 @@ var MaintenanceScheduler = class {
|
|
|
6326
6486
|
// src/maintenance/reporter.ts
|
|
6327
6487
|
var fs14 = __toESM(require("fs"));
|
|
6328
6488
|
var path14 = __toESM(require("path"));
|
|
6489
|
+
var import_zod9 = require("zod");
|
|
6490
|
+
var RunResultSchema = import_zod9.z.object({
|
|
6491
|
+
taskId: import_zod9.z.string(),
|
|
6492
|
+
startedAt: import_zod9.z.string(),
|
|
6493
|
+
completedAt: import_zod9.z.string(),
|
|
6494
|
+
status: import_zod9.z.enum(["success", "failure", "skipped", "no-issues"]),
|
|
6495
|
+
findings: import_zod9.z.number(),
|
|
6496
|
+
fixed: import_zod9.z.number(),
|
|
6497
|
+
prUrl: import_zod9.z.string().nullable(),
|
|
6498
|
+
prUpdated: import_zod9.z.boolean(),
|
|
6499
|
+
error: import_zod9.z.string().optional()
|
|
6500
|
+
});
|
|
6329
6501
|
var MAX_HISTORY = 500;
|
|
6330
6502
|
var fallbackLogger = {
|
|
6331
6503
|
info: () => {
|
|
@@ -6351,9 +6523,9 @@ var MaintenanceReporter = class {
|
|
|
6351
6523
|
await fs14.promises.mkdir(this.persistDir, { recursive: true });
|
|
6352
6524
|
const filePath = path14.join(this.persistDir, "history.json");
|
|
6353
6525
|
const data = await fs14.promises.readFile(filePath, "utf-8");
|
|
6354
|
-
const parsed = JSON.parse(data);
|
|
6355
|
-
if (
|
|
6356
|
-
this.history = parsed.slice(0, MAX_HISTORY);
|
|
6526
|
+
const parsed = import_zod9.z.array(RunResultSchema).safeParse(JSON.parse(data));
|
|
6527
|
+
if (parsed.success) {
|
|
6528
|
+
this.history = parsed.data.slice(0, MAX_HISTORY);
|
|
6357
6529
|
}
|
|
6358
6530
|
} catch (err) {
|
|
6359
6531
|
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
@@ -6504,7 +6676,8 @@ var TaskRunner = class {
|
|
|
6504
6676
|
const prResult = await this.prManager.ensurePR(task, summary);
|
|
6505
6677
|
prUrl = prResult.prUrl;
|
|
6506
6678
|
prUpdated = prResult.prUpdated;
|
|
6507
|
-
} catch {
|
|
6679
|
+
} catch (err) {
|
|
6680
|
+
console.warn(`[maintenance] PR creation failed for task ${task.id}: ${String(err)}`);
|
|
6508
6681
|
}
|
|
6509
6682
|
}
|
|
6510
6683
|
return {
|
|
@@ -6555,7 +6728,8 @@ var TaskRunner = class {
|
|
|
6555
6728
|
const prResult = await this.prManager.ensurePR(task, summary);
|
|
6556
6729
|
prUrl = prResult.prUrl;
|
|
6557
6730
|
prUpdated = prResult.prUpdated;
|
|
6558
|
-
} catch {
|
|
6731
|
+
} catch (err) {
|
|
6732
|
+
console.warn(`[maintenance] PR creation failed for task ${task.id}: ${String(err)}`);
|
|
6559
6733
|
}
|
|
6560
6734
|
}
|
|
6561
6735
|
return {
|
|
@@ -6665,6 +6839,10 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
6665
6839
|
enrichedSpecsByIssue = /* @__PURE__ */ new Map();
|
|
6666
6840
|
/** Tracks recently-failed intelligence analysis to avoid re-requesting every tick */
|
|
6667
6841
|
analysisFailureCache = /* @__PURE__ */ new Map();
|
|
6842
|
+
/** Abort controllers and PIDs for running agent tasks — used by stopIssue to cancel in-flight work.
|
|
6843
|
+
* The PID is stored here because the running entry may be deleted by the state machine
|
|
6844
|
+
* before the stop effect executes (e.g., stall_detected removes the entry first). */
|
|
6845
|
+
abortControllers = /* @__PURE__ */ new Map();
|
|
6668
6846
|
/** Guards against overlapping ticks when a tick takes longer than the polling interval */
|
|
6669
6847
|
tickInProgress = false;
|
|
6670
6848
|
/** Timestamp of the last stale branch sweep (at most once per hour) */
|
|
@@ -6795,33 +6973,63 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
6795
6973
|
}
|
|
6796
6974
|
/**
|
|
6797
6975
|
* Creates a TaskRunner for the maintenance scheduler.
|
|
6798
|
-
*
|
|
6799
|
-
*
|
|
6976
|
+
* CheckCommandRunner and CommandExecutor use real child_process execution.
|
|
6977
|
+
* AgentDispatcher remains stubbed (requires full skill dispatch integration).
|
|
6800
6978
|
*/
|
|
6801
6979
|
createMaintenanceTaskRunner(maintenanceConfig) {
|
|
6802
|
-
this.logger
|
|
6803
|
-
"Maintenance task runner using stub implementations \u2014 tasks will not execute real checks or dispatch agents. Real implementations will be wired in a follow-up."
|
|
6804
|
-
);
|
|
6980
|
+
const logger = this.logger;
|
|
6805
6981
|
const checkRunner = {
|
|
6806
6982
|
run: async (command, cwd) => {
|
|
6807
|
-
|
|
6808
|
-
|
|
6983
|
+
const { execFile: execFile6 } = await import("child_process");
|
|
6984
|
+
const { promisify: promisify3 } = await import("util");
|
|
6985
|
+
const execFileAsync = promisify3(execFile6);
|
|
6986
|
+
const [cmd, ...args] = command;
|
|
6987
|
+
if (!cmd) return { passed: true, findings: 0, output: "" };
|
|
6988
|
+
try {
|
|
6989
|
+
const { stdout } = await execFileAsync(cmd, args, { cwd, timeout: 12e4 });
|
|
6990
|
+
const findingsMatch = stdout.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
|
|
6991
|
+
const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 0;
|
|
6992
|
+
return { passed: findings === 0, findings, output: stdout };
|
|
6993
|
+
} catch (err) {
|
|
6994
|
+
const error = err;
|
|
6995
|
+
const output = [error.stdout, error.stderr].filter(Boolean).join("\n");
|
|
6996
|
+
const findingsMatch = output.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
|
|
6997
|
+
const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 1;
|
|
6998
|
+
return { passed: false, findings, output };
|
|
6999
|
+
}
|
|
6809
7000
|
}
|
|
6810
7001
|
};
|
|
6811
7002
|
const agentDispatcher = {
|
|
6812
7003
|
dispatch: async (skill, branch, backendName, cwd) => {
|
|
6813
|
-
|
|
6814
|
-
skill,
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
7004
|
+
logger.info(
|
|
7005
|
+
"Maintenance agent dispatcher invoked (stub \u2014 skill dispatch integration pending)",
|
|
7006
|
+
{
|
|
7007
|
+
skill,
|
|
7008
|
+
branch,
|
|
7009
|
+
backendName,
|
|
7010
|
+
cwd
|
|
7011
|
+
}
|
|
7012
|
+
);
|
|
6819
7013
|
return { producedCommits: false, fixed: 0 };
|
|
6820
7014
|
}
|
|
6821
7015
|
};
|
|
6822
7016
|
const commandExecutor = {
|
|
6823
7017
|
exec: async (command, cwd) => {
|
|
6824
|
-
|
|
7018
|
+
const { execFile: execFile6 } = await import("child_process");
|
|
7019
|
+
const { promisify: promisify3 } = await import("util");
|
|
7020
|
+
const execFileAsync = promisify3(execFile6);
|
|
7021
|
+
const [cmd, ...args] = command;
|
|
7022
|
+
if (!cmd) return;
|
|
7023
|
+
try {
|
|
7024
|
+
await execFileAsync(cmd, args, { cwd, timeout: 12e4 });
|
|
7025
|
+
} catch (err) {
|
|
7026
|
+
logger.warn("Maintenance command execution failed", {
|
|
7027
|
+
command,
|
|
7028
|
+
cwd,
|
|
7029
|
+
error: String(err)
|
|
7030
|
+
});
|
|
7031
|
+
throw err;
|
|
7032
|
+
}
|
|
6825
7033
|
}
|
|
6826
7034
|
};
|
|
6827
7035
|
return new TaskRunner({
|
|
@@ -6851,19 +7059,18 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
6851
7059
|
historyProvider: reporter,
|
|
6852
7060
|
onTaskDue: async (task) => {
|
|
6853
7061
|
this.logger.info(`Maintenance task due: ${task.id}`, { taskId: task.id });
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
});
|
|
7062
|
+
const startPayload = { taskId: task.id, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
7063
|
+
this.server?.broadcastMaintenance("maintenance:started", startPayload);
|
|
7064
|
+
this.emit("maintenance:started", startPayload);
|
|
6858
7065
|
const result = await taskRunner.run(task);
|
|
6859
7066
|
await reporter.record(result);
|
|
6860
7067
|
if (result.status === "failure") {
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
});
|
|
7068
|
+
const errorPayload = { taskId: task.id, error: result.error };
|
|
7069
|
+
this.server?.broadcastMaintenance("maintenance:error", errorPayload);
|
|
7070
|
+
this.emit("maintenance:error", errorPayload);
|
|
6865
7071
|
} else {
|
|
6866
7072
|
this.server?.broadcastMaintenance("maintenance:completed", result);
|
|
7073
|
+
this.emit("maintenance:completed", result);
|
|
6867
7074
|
}
|
|
6868
7075
|
this.logger.info(`Maintenance task completed: ${task.id}`, {
|
|
6869
7076
|
taskId: task.id,
|
|
@@ -6916,7 +7123,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
6916
7123
|
const peslModel = intel.models?.pesl ?? this.config.agent.model;
|
|
6917
7124
|
const store = new import_graph.GraphStore();
|
|
6918
7125
|
this.graphStore = store;
|
|
6919
|
-
return new
|
|
7126
|
+
return new import_intelligence3.IntelligencePipeline(provider, store, {
|
|
6920
7127
|
...peslModel !== void 0 && { peslModel }
|
|
6921
7128
|
});
|
|
6922
7129
|
}
|
|
@@ -6939,7 +7146,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
6939
7146
|
const apiKey = this.config.agent.localApiKey ?? "ollama";
|
|
6940
7147
|
const model = selModel ?? this.config.agent.localModel;
|
|
6941
7148
|
this.logger.info(`Intelligence pipeline using local backend at ${endpoint}`);
|
|
6942
|
-
return new
|
|
7149
|
+
return new import_intelligence3.OpenAICompatibleAnalysisProvider({
|
|
6943
7150
|
apiKey,
|
|
6944
7151
|
baseUrl: endpoint,
|
|
6945
7152
|
...model !== void 0 && { defaultModel: model },
|
|
@@ -6952,7 +7159,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
6952
7159
|
if (backend === "anthropic" || backend === "claude") {
|
|
6953
7160
|
const apiKey = this.config.agent.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
6954
7161
|
if (apiKey) {
|
|
6955
|
-
return new
|
|
7162
|
+
return new import_intelligence3.AnthropicAnalysisProvider({
|
|
6956
7163
|
apiKey,
|
|
6957
7164
|
...selModel !== void 0 && { defaultModel: selModel }
|
|
6958
7165
|
});
|
|
@@ -6961,7 +7168,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
6961
7168
|
if (backend === "openai") {
|
|
6962
7169
|
const apiKey = this.config.agent.apiKey ?? process.env.OPENAI_API_KEY;
|
|
6963
7170
|
if (apiKey) {
|
|
6964
|
-
return new
|
|
7171
|
+
return new import_intelligence3.OpenAICompatibleAnalysisProvider({
|
|
6965
7172
|
apiKey,
|
|
6966
7173
|
baseUrl: "https://api.openai.com/v1",
|
|
6967
7174
|
...selModel !== void 0 && { defaultModel: selModel }
|
|
@@ -6970,7 +7177,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
6970
7177
|
}
|
|
6971
7178
|
if (backend === "claude" || backend === "anthropic") {
|
|
6972
7179
|
this.logger.info("Intelligence pipeline using Claude CLI (no API key configured)");
|
|
6973
|
-
return new
|
|
7180
|
+
return new import_intelligence3.ClaudeCliAnalysisProvider({
|
|
6974
7181
|
command: this.config.agent.command,
|
|
6975
7182
|
...selModel !== void 0 && { defaultModel: selModel },
|
|
6976
7183
|
...intel?.requestTimeoutMs !== void 0 && { timeoutMs: intel.requestTimeoutMs }
|
|
@@ -6987,13 +7194,13 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
6987
7194
|
if (!apiKey2) {
|
|
6988
7195
|
throw new Error("Intelligence pipeline: no Anthropic API key found.");
|
|
6989
7196
|
}
|
|
6990
|
-
return new
|
|
7197
|
+
return new import_intelligence3.AnthropicAnalysisProvider({
|
|
6991
7198
|
apiKey: apiKey2,
|
|
6992
7199
|
...selModel !== void 0 && { defaultModel: selModel }
|
|
6993
7200
|
});
|
|
6994
7201
|
}
|
|
6995
7202
|
if (provider.kind === "claude-cli") {
|
|
6996
|
-
return new
|
|
7203
|
+
return new import_intelligence3.ClaudeCliAnalysisProvider({
|
|
6997
7204
|
command: this.config.agent.command,
|
|
6998
7205
|
...selModel !== void 0 && { defaultModel: selModel },
|
|
6999
7206
|
...this.config.intelligence?.requestTimeoutMs !== void 0 && {
|
|
@@ -7004,7 +7211,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7004
7211
|
const apiKey = provider.apiKey ?? this.config.agent.apiKey ?? "ollama";
|
|
7005
7212
|
const baseUrl = provider.baseUrl ?? "http://localhost:11434/v1";
|
|
7006
7213
|
const intel = this.config.intelligence;
|
|
7007
|
-
return new
|
|
7214
|
+
return new import_intelligence3.OpenAICompatibleAnalysisProvider({
|
|
7008
7215
|
apiKey,
|
|
7009
7216
|
baseUrl,
|
|
7010
7217
|
...selModel !== void 0 && { defaultModel: selModel },
|
|
@@ -7051,7 +7258,13 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7051
7258
|
(phase, detail, progress) => this.setTickActivity(phase, detail, progress)
|
|
7052
7259
|
) : void 0;
|
|
7053
7260
|
this.setTickActivity("dispatching", "Applying state machine");
|
|
7054
|
-
const {
|
|
7261
|
+
const {
|
|
7262
|
+
concernSignals,
|
|
7263
|
+
enrichedSpecs,
|
|
7264
|
+
complexityScores,
|
|
7265
|
+
simulationResults,
|
|
7266
|
+
personaRecommendations
|
|
7267
|
+
} = pipelineResult ?? {};
|
|
7055
7268
|
const tickEvent = {
|
|
7056
7269
|
type: "tick",
|
|
7057
7270
|
candidates,
|
|
@@ -7060,7 +7273,8 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7060
7273
|
...concernSignals !== void 0 && { concernSignals },
|
|
7061
7274
|
...enrichedSpecs !== void 0 && { enrichedSpecs },
|
|
7062
7275
|
...complexityScores !== void 0 && { complexityScores },
|
|
7063
|
-
...simulationResults !== void 0 && { simulationResults }
|
|
7276
|
+
...simulationResults !== void 0 && { simulationResults },
|
|
7277
|
+
...personaRecommendations !== void 0 && { personaRecommendations }
|
|
7064
7278
|
};
|
|
7065
7279
|
let { nextState, effects } = applyEvent(this.state, tickEvent, this.config);
|
|
7066
7280
|
this.state = nextState;
|
|
@@ -7070,7 +7284,8 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7070
7284
|
type: "retry_fired",
|
|
7071
7285
|
issueId,
|
|
7072
7286
|
candidates,
|
|
7073
|
-
nowMs
|
|
7287
|
+
nowMs,
|
|
7288
|
+
...concernSignals !== void 0 && { concernSignals }
|
|
7074
7289
|
};
|
|
7075
7290
|
const result = applyEvent(this.state, retryEvent, this.config);
|
|
7076
7291
|
this.state = result.nextState;
|
|
@@ -7079,6 +7294,35 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7079
7294
|
for (const effect of effects) {
|
|
7080
7295
|
await this.handleEffect(effect);
|
|
7081
7296
|
}
|
|
7297
|
+
const stallTimeoutMs = this.config.agent.stallTimeoutMs;
|
|
7298
|
+
if (stallTimeoutMs > 0) {
|
|
7299
|
+
const stalledIds = [];
|
|
7300
|
+
for (const [runId, runEntry] of this.state.running) {
|
|
7301
|
+
const lastTs = runEntry.session?.lastTimestamp;
|
|
7302
|
+
if (!lastTs) continue;
|
|
7303
|
+
const silentMs = nowMs - new Date(lastTs).getTime();
|
|
7304
|
+
if (silentMs >= stallTimeoutMs) {
|
|
7305
|
+
stalledIds.push(runId);
|
|
7306
|
+
}
|
|
7307
|
+
}
|
|
7308
|
+
for (const runId of stalledIds) {
|
|
7309
|
+
const runEntry = this.state.running.get(runId);
|
|
7310
|
+
if (!runEntry) continue;
|
|
7311
|
+
this.logger.warn(
|
|
7312
|
+
`Agent stalled for ${runEntry.identifier}: ${Math.round((nowMs - new Date(runEntry.session?.lastTimestamp ?? 0).getTime()) / 1e3)}s since last event`,
|
|
7313
|
+
{ issueId: runId }
|
|
7314
|
+
);
|
|
7315
|
+
const stallEvent = {
|
|
7316
|
+
type: "stall_detected",
|
|
7317
|
+
issueId: runId
|
|
7318
|
+
};
|
|
7319
|
+
const stallResult = applyEvent(this.state, stallEvent, this.config);
|
|
7320
|
+
this.state = stallResult.nextState;
|
|
7321
|
+
for (const eff of stallResult.effects) {
|
|
7322
|
+
await this.handleEffect(eff);
|
|
7323
|
+
}
|
|
7324
|
+
}
|
|
7325
|
+
}
|
|
7082
7326
|
const openPrNumbers = [];
|
|
7083
7327
|
for (const [, runEntry] of this.state.running) {
|
|
7084
7328
|
const externalId = runEntry.issue.externalId;
|
|
@@ -7125,9 +7369,6 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7125
7369
|
*/
|
|
7126
7370
|
async handleEffect(effect) {
|
|
7127
7371
|
switch (effect.type) {
|
|
7128
|
-
case "dispatch":
|
|
7129
|
-
await this.dispatchIssue(effect.issue, effect.attempt, effect.backend);
|
|
7130
|
-
break;
|
|
7131
7372
|
case "stop":
|
|
7132
7373
|
await this.stopIssue(effect.issueId);
|
|
7133
7374
|
break;
|
|
@@ -7138,6 +7379,11 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7138
7379
|
break;
|
|
7139
7380
|
case "releaseClaim":
|
|
7140
7381
|
break;
|
|
7382
|
+
case "scheduleRetry":
|
|
7383
|
+
this.logger.info(
|
|
7384
|
+
`Retry scheduled for ${effect.issueId} (attempt ${effect.attempt}, delay ${effect.delayMs}ms)`
|
|
7385
|
+
);
|
|
7386
|
+
break;
|
|
7141
7387
|
case "cleanWorkspace":
|
|
7142
7388
|
await this.cleanWorkspaceWithGuard(effect.identifier, effect.issueId);
|
|
7143
7389
|
break;
|
|
@@ -7163,6 +7409,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7163
7409
|
`Branch "${branch}" not found on remote for ${identifier}, cleaning up worktree`,
|
|
7164
7410
|
{ issueId }
|
|
7165
7411
|
);
|
|
7412
|
+
await this.runBeforeRemoveHook(identifier);
|
|
7166
7413
|
await this.workspace.removeWorkspace(identifier);
|
|
7167
7414
|
return;
|
|
7168
7415
|
}
|
|
@@ -7197,8 +7444,17 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7197
7444
|
return;
|
|
7198
7445
|
}
|
|
7199
7446
|
}
|
|
7447
|
+
await this.runBeforeRemoveHook(identifier);
|
|
7200
7448
|
await this.workspace.removeWorkspace(identifier);
|
|
7201
7449
|
}
|
|
7450
|
+
/** Run the beforeRemove hook for a workspace. Failures are logged but non-fatal. */
|
|
7451
|
+
async runBeforeRemoveHook(identifier) {
|
|
7452
|
+
const wsPath = this.workspace.resolvePath(identifier);
|
|
7453
|
+
const result = await this.hooks.beforeRemove(wsPath);
|
|
7454
|
+
if (!result.ok) {
|
|
7455
|
+
this.logger.warn(`beforeRemove hook failed for ${identifier}: ${result.error.message}`);
|
|
7456
|
+
}
|
|
7457
|
+
}
|
|
7202
7458
|
/**
|
|
7203
7459
|
* Delegates to PRDetector.filterCandidatesWithOpenPRs.
|
|
7204
7460
|
* @see PRDetector#filterCandidatesWithOpenPRs
|
|
@@ -7368,7 +7624,11 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7368
7624
|
if (!result.ok) {
|
|
7369
7625
|
this.logger.warn(`Lifecycle comment failed for ${identifier}: ${result.error.message}`);
|
|
7370
7626
|
}
|
|
7371
|
-
} catch {
|
|
7627
|
+
} catch (err) {
|
|
7628
|
+
this.logger.debug("Lifecycle comment failed (best-effort)", {
|
|
7629
|
+
identifier,
|
|
7630
|
+
error: String(err)
|
|
7631
|
+
});
|
|
7372
7632
|
}
|
|
7373
7633
|
}
|
|
7374
7634
|
/**
|
|
@@ -7385,6 +7645,12 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7385
7645
|
const workspaceResult = await this.workspace.ensureWorkspace(issue.identifier);
|
|
7386
7646
|
if (!workspaceResult.ok) throw workspaceResult.error;
|
|
7387
7647
|
const workspacePath = workspaceResult.value;
|
|
7648
|
+
const afterCreateResult = await this.hooks.afterCreate(workspacePath);
|
|
7649
|
+
if (!afterCreateResult.ok) {
|
|
7650
|
+
this.logger.warn(
|
|
7651
|
+
`afterCreate hook failed for ${issue.identifier}: ${afterCreateResult.error.message}`
|
|
7652
|
+
);
|
|
7653
|
+
}
|
|
7388
7654
|
const hookResult = await this.hooks.beforeRun(workspacePath);
|
|
7389
7655
|
if (!hookResult.ok) throw hookResult.error;
|
|
7390
7656
|
const scanResult = await scanWorkspaceConfig(workspacePath);
|
|
@@ -7494,21 +7760,54 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7494
7760
|
runAgentInBackgroundTask(issue, workspacePath, prompt, attempt, runner) {
|
|
7495
7761
|
const activeRunner = runner ?? this.runner;
|
|
7496
7762
|
this.logger.info(`Starting background task for ${issue.identifier}`);
|
|
7763
|
+
const abortController = new AbortController();
|
|
7764
|
+
this.abortControllers.set(issue.id, { controller: abortController, pid: null });
|
|
7497
7765
|
(async () => {
|
|
7498
7766
|
try {
|
|
7499
7767
|
this.logger.info(`Calling runner.runSession for ${issue.identifier}`);
|
|
7500
7768
|
const sessionGen = activeRunner.runSession(issue, workspacePath, prompt);
|
|
7501
7769
|
for await (const event of sessionGen) {
|
|
7770
|
+
if (abortController.signal.aborted) {
|
|
7771
|
+
this.logger.info(`Agent session aborted for ${issue.identifier}`);
|
|
7772
|
+
break;
|
|
7773
|
+
}
|
|
7774
|
+
if (event.type === "session_started" && event.content) {
|
|
7775
|
+
const pid = event.content.pid;
|
|
7776
|
+
if (pid) {
|
|
7777
|
+
const tracked = this.abortControllers.get(issue.id);
|
|
7778
|
+
if (tracked) tracked.pid = pid;
|
|
7779
|
+
}
|
|
7780
|
+
}
|
|
7502
7781
|
await this.processAgentEvent(issue, event);
|
|
7503
7782
|
if (event.type === "turn_start") {
|
|
7504
7783
|
await this.awaitRateLimitClearance(issue.identifier);
|
|
7505
7784
|
}
|
|
7506
7785
|
}
|
|
7507
7786
|
this.logger.info(`Session generator finished for ${issue.identifier}`);
|
|
7508
|
-
await this.
|
|
7787
|
+
const afterRunResult = await this.hooks.afterRun(workspacePath);
|
|
7788
|
+
if (!afterRunResult.ok) {
|
|
7789
|
+
this.logger.warn(
|
|
7790
|
+
`afterRun hook failed for ${issue.identifier}: ${afterRunResult.error.message}`
|
|
7791
|
+
);
|
|
7792
|
+
}
|
|
7793
|
+
if (abortController.signal.aborted) {
|
|
7794
|
+
if (this.state.running.has(issue.id)) {
|
|
7795
|
+
await this.emitWorkerExit(issue.id, "error", attempt, "Stopped by reconciliation");
|
|
7796
|
+
}
|
|
7797
|
+
} else {
|
|
7798
|
+
await this.emitWorkerExit(issue.id, "normal", attempt);
|
|
7799
|
+
}
|
|
7509
7800
|
} catch (error) {
|
|
7510
7801
|
this.logger.error(`Agent runner failed for ${issue.identifier}`, { error: String(error) });
|
|
7802
|
+
const afterRunResult = await this.hooks.afterRun(workspacePath);
|
|
7803
|
+
if (!afterRunResult.ok) {
|
|
7804
|
+
this.logger.warn(
|
|
7805
|
+
`afterRun hook failed for ${issue.identifier}: ${afterRunResult.error.message}`
|
|
7806
|
+
);
|
|
7807
|
+
}
|
|
7511
7808
|
await this.emitWorkerExit(issue.id, "error", attempt, String(error));
|
|
7809
|
+
} finally {
|
|
7810
|
+
this.abortControllers.delete(issue.id);
|
|
7512
7811
|
}
|
|
7513
7812
|
})().catch((err) => {
|
|
7514
7813
|
this.logger.error("Fatal error in background task", { error: String(err) });
|
|
@@ -7534,6 +7833,19 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7534
7833
|
*/
|
|
7535
7834
|
async stopIssue(issueId) {
|
|
7536
7835
|
this.logger.info(`Stopping issue: ${issueId}`);
|
|
7836
|
+
const tracked = this.abortControllers.get(issueId);
|
|
7837
|
+
if (tracked) {
|
|
7838
|
+
tracked.controller.abort();
|
|
7839
|
+
this.logger.info(`Abort signal sent for ${issueId}`);
|
|
7840
|
+
}
|
|
7841
|
+
const pid = tracked?.pid ?? this.state.running.get(issueId)?.session?.agentPid;
|
|
7842
|
+
if (pid) {
|
|
7843
|
+
try {
|
|
7844
|
+
process.kill(pid, "SIGTERM");
|
|
7845
|
+
this.logger.info(`Sent SIGTERM to agent PID ${pid} for ${issueId}`);
|
|
7846
|
+
} catch {
|
|
7847
|
+
}
|
|
7848
|
+
}
|
|
7537
7849
|
}
|
|
7538
7850
|
/**
|
|
7539
7851
|
* Dispatch a work item immediately, bypassing the normal tick → roadmap cycle.
|