@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.mjs
CHANGED
|
@@ -601,7 +601,26 @@ function handleTick(state, event, config) {
|
|
|
601
601
|
continue;
|
|
602
602
|
}
|
|
603
603
|
const scopeTier = detectScopeTier(issue, artifactPresenceFromIssue(issue));
|
|
604
|
-
const signals = event.concernSignals?.get(issue.id) ?? [];
|
|
604
|
+
const signals = [...event.concernSignals?.get(issue.id) ?? []];
|
|
605
|
+
let suggestedPersona;
|
|
606
|
+
try {
|
|
607
|
+
const personaRecs = event.personaRecommendations?.get(issue.id);
|
|
608
|
+
if (personaRecs && personaRecs.length > 0) {
|
|
609
|
+
suggestedPersona = personaRecs[0].persona;
|
|
610
|
+
if (personaRecs[0].weightedScore < 0.3) {
|
|
611
|
+
signals.push({
|
|
612
|
+
name: "lowExpertise",
|
|
613
|
+
reason: `Top persona "${suggestedPersona}" scored ${personaRecs[0].weightedScore.toFixed(2)} (below 0.3 threshold)`
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
} else if (personaRecs && personaRecs.length === 0) {
|
|
617
|
+
signals.push({
|
|
618
|
+
name: "noPersonaMatch",
|
|
619
|
+
reason: "No persona recommendations available for this issue's systems"
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
} catch {
|
|
623
|
+
}
|
|
605
624
|
const decision = routeIssue(scopeTier, signals, escalationConfig);
|
|
606
625
|
if (decision.action === "needs-human") {
|
|
607
626
|
next.claimed.add(issue.id);
|
|
@@ -617,6 +636,12 @@ function handleTick(state, event, config) {
|
|
|
617
636
|
}
|
|
618
637
|
const backend = resolveBackend(decision.action, !!config.agent.localBackend);
|
|
619
638
|
claimAndDispatch(next, issue, backend, nowMs, effects);
|
|
639
|
+
if (suggestedPersona) {
|
|
640
|
+
const lastEffect = effects[effects.length - 1];
|
|
641
|
+
if (lastEffect && lastEffect.type === "claim") {
|
|
642
|
+
lastEffect.suggestedPersona = suggestedPersona;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
620
645
|
}
|
|
621
646
|
pruneCompleted(next);
|
|
622
647
|
return { nextState: next, effects };
|
|
@@ -743,7 +768,7 @@ function handleAgentUpdate(state, issueId, event) {
|
|
|
743
768
|
}
|
|
744
769
|
return { nextState: next, effects };
|
|
745
770
|
}
|
|
746
|
-
function handleRetryFired(state, issueId, candidates, config, nowMs) {
|
|
771
|
+
function handleRetryFired(state, issueId, candidates, config, nowMs, concernSignals) {
|
|
747
772
|
const next = cloneState(state);
|
|
748
773
|
const effects = [];
|
|
749
774
|
const retryEntry = next.retryAttempts.get(issueId);
|
|
@@ -800,7 +825,8 @@ function handleRetryFired(state, issueId, candidates, config, nowMs) {
|
|
|
800
825
|
}
|
|
801
826
|
const escalationConfig = resolveEscalationConfig(config);
|
|
802
827
|
const scopeTier = detectScopeTier(issue, artifactPresenceFromIssue(issue));
|
|
803
|
-
const
|
|
828
|
+
const signals = [...concernSignals?.get(issue.id) ?? []];
|
|
829
|
+
const decision = routeIssue(scopeTier, signals, escalationConfig);
|
|
804
830
|
if (decision.action === "needs-human") {
|
|
805
831
|
effects.push(
|
|
806
832
|
buildEscalateEffect(issue.id, issue.identifier, decision.reasons, {
|
|
@@ -880,7 +906,14 @@ function applyEvent(state, event, config) {
|
|
|
880
906
|
case "agent_update":
|
|
881
907
|
return handleAgentUpdate(state, event.issueId, event.event);
|
|
882
908
|
case "retry_fired":
|
|
883
|
-
return handleRetryFired(
|
|
909
|
+
return handleRetryFired(
|
|
910
|
+
state,
|
|
911
|
+
event.issueId,
|
|
912
|
+
event.candidates,
|
|
913
|
+
config,
|
|
914
|
+
event.nowMs,
|
|
915
|
+
event.concernSignals
|
|
916
|
+
);
|
|
884
917
|
case "stall_detected":
|
|
885
918
|
return handleStallDetected(state, event.issueId, config);
|
|
886
919
|
case "claim_rejected":
|
|
@@ -2335,10 +2368,15 @@ var WorkspaceHooks = class {
|
|
|
2335
2368
|
return Ok7(void 0);
|
|
2336
2369
|
}
|
|
2337
2370
|
return new Promise((resolve6) => {
|
|
2338
|
-
const
|
|
2339
|
-
|
|
2371
|
+
const filteredEnv = {};
|
|
2372
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
2373
|
+
if (v != null && !k.includes("SECRET") && !k.includes("TOKEN") && !k.includes("PASSWORD")) {
|
|
2374
|
+
filteredEnv[k] = v;
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
const child = spawn("/bin/sh", ["-c", command], {
|
|
2340
2378
|
cwd,
|
|
2341
|
-
env:
|
|
2379
|
+
env: filteredEnv
|
|
2342
2380
|
});
|
|
2343
2381
|
const timeout = setTimeout(() => {
|
|
2344
2382
|
child.kill();
|
|
@@ -2459,6 +2497,7 @@ import { GraphStore } from "@harness-engineering/graph";
|
|
|
2459
2497
|
|
|
2460
2498
|
// src/intelligence/pipeline-runner.ts
|
|
2461
2499
|
import * as path7 from "path";
|
|
2500
|
+
import { weightedRecommendPersona, refreshProfiles } from "@harness-engineering/intelligence";
|
|
2462
2501
|
import {
|
|
2463
2502
|
GitHubIssuesSyncAdapter,
|
|
2464
2503
|
loadTrackerSyncConfig
|
|
@@ -2490,6 +2529,7 @@ var IntelligencePipelineRunner = class {
|
|
|
2490
2529
|
this.graphLoaded = true;
|
|
2491
2530
|
await this.loadGraphStore();
|
|
2492
2531
|
await this.hydrateSpecCache();
|
|
2532
|
+
this.refreshSpecializationProfiles();
|
|
2493
2533
|
}
|
|
2494
2534
|
/**
|
|
2495
2535
|
* Runs the full intelligence pipeline for the given candidates:
|
|
@@ -2543,11 +2583,39 @@ var IntelligencePipelineRunner = class {
|
|
|
2543
2583
|
} catch (err) {
|
|
2544
2584
|
this.ctx.logger.warn("Auto-publish analyses failed", { error: String(err) });
|
|
2545
2585
|
}
|
|
2546
|
-
|
|
2586
|
+
this.refreshSpecializationProfiles();
|
|
2587
|
+
setTickActivity("analyzing", "Scoring persona recommendations");
|
|
2588
|
+
const personaRecommendations = this.computePersonaRecommendations(candidates);
|
|
2589
|
+
return {
|
|
2590
|
+
concernSignals,
|
|
2591
|
+
enrichedSpecs,
|
|
2592
|
+
complexityScores,
|
|
2593
|
+
simulationResults,
|
|
2594
|
+
personaRecommendations
|
|
2595
|
+
};
|
|
2547
2596
|
}
|
|
2548
2597
|
// ---------------------------------------------------------------------------
|
|
2549
2598
|
// Private helpers
|
|
2550
2599
|
// ---------------------------------------------------------------------------
|
|
2600
|
+
/**
|
|
2601
|
+
* Refresh specialization profiles from the graph store. This recomputes
|
|
2602
|
+
* expertise scores for all personas with execution_outcome nodes and
|
|
2603
|
+
* persists them to .harness/specialization-profiles.json. Non-fatal.
|
|
2604
|
+
*/
|
|
2605
|
+
refreshSpecializationProfiles() {
|
|
2606
|
+
if (!this.ctx.graphStore) return;
|
|
2607
|
+
try {
|
|
2608
|
+
const store = refreshProfiles(this.ctx.projectRoot, this.ctx.graphStore);
|
|
2609
|
+
const personaCount = Object.keys(store.profiles).length;
|
|
2610
|
+
if (personaCount > 0) {
|
|
2611
|
+
this.ctx.logger.info(`Refreshed specialization profiles for ${personaCount} persona(s)`);
|
|
2612
|
+
}
|
|
2613
|
+
} catch (err) {
|
|
2614
|
+
this.ctx.logger.warn("Failed to refresh specialization profiles", {
|
|
2615
|
+
error: String(err)
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2551
2619
|
async loadGraphStore() {
|
|
2552
2620
|
try {
|
|
2553
2621
|
const graphDir = path7.join(this.ctx.config.workspace.root, "..", "graph");
|
|
@@ -2706,7 +2774,22 @@ var IntelligencePipelineRunner = class {
|
|
|
2706
2774
|
const scopeTier = detectScopeTier(issue, artifactPresenceFromIssue(issue));
|
|
2707
2775
|
try {
|
|
2708
2776
|
const result = await this.ctx.pipeline.preprocessIssue(issue, scopeTier, escalationConfig);
|
|
2709
|
-
|
|
2777
|
+
const signals = [...result.signals];
|
|
2778
|
+
if (result.spec) {
|
|
2779
|
+
if (result.spec.unknowns.length > 3) {
|
|
2780
|
+
signals.push({
|
|
2781
|
+
name: "high-unknowns",
|
|
2782
|
+
reason: `Enriched spec has ${result.spec.unknowns.length} unknowns (threshold: 3)`
|
|
2783
|
+
});
|
|
2784
|
+
}
|
|
2785
|
+
if (result.spec.ambiguities.length > 5) {
|
|
2786
|
+
signals.push({
|
|
2787
|
+
name: "high-ambiguities",
|
|
2788
|
+
reason: `Enriched spec has ${result.spec.ambiguities.length} ambiguities (threshold: 5)`
|
|
2789
|
+
});
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
if (signals.length > 0) concernSignals.set(issue.id, signals);
|
|
2710
2793
|
if (result.spec) {
|
|
2711
2794
|
enrichedSpecs.set(issue.id, result.spec);
|
|
2712
2795
|
this.ctx.enrichedSpecsByIssue.set(issue.id, result.spec);
|
|
@@ -2753,6 +2836,30 @@ var IntelligencePipelineRunner = class {
|
|
|
2753
2836
|
return true;
|
|
2754
2837
|
});
|
|
2755
2838
|
}
|
|
2839
|
+
/**
|
|
2840
|
+
* Compute persona recommendations for each candidate using the specialization
|
|
2841
|
+
* scorer. Extracts system node IDs from issue labels prefixed with `system:`
|
|
2842
|
+
* or `module:`. Failures are non-fatal — returns an empty map on error.
|
|
2843
|
+
*/
|
|
2844
|
+
computePersonaRecommendations(candidates) {
|
|
2845
|
+
const results = /* @__PURE__ */ new Map();
|
|
2846
|
+
if (!this.ctx.graphStore) return results;
|
|
2847
|
+
try {
|
|
2848
|
+
for (const issue of candidates) {
|
|
2849
|
+
const systemNodeIds = issue.labels.filter((l) => l.startsWith("system:") || l.startsWith("module:")).map((l) => l.split(":")[1]).filter((id) => id.length > 0);
|
|
2850
|
+
if (systemNodeIds.length === 0) continue;
|
|
2851
|
+
const recs = weightedRecommendPersona(this.ctx.graphStore, { systemNodeIds });
|
|
2852
|
+
if (recs.length > 0) {
|
|
2853
|
+
results.set(issue.id, recs);
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
} catch (err) {
|
|
2857
|
+
this.ctx.logger.warn("Persona recommendation scoring failed", {
|
|
2858
|
+
error: String(err)
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2861
|
+
return results;
|
|
2862
|
+
}
|
|
2756
2863
|
async analyzeCandidates(eligibleCandidates, escalationConfig, nowForCache, failureTtl, circuitBreakerThreshold, concernSignals, enrichedSpecs, complexityScores, setTickActivity) {
|
|
2757
2864
|
let processed = 0;
|
|
2758
2865
|
let consecutiveConnErrors = 0;
|
|
@@ -2830,10 +2937,27 @@ var CompletionHandler = class {
|
|
|
2830
2937
|
// ---------------------------------------------------------------------------
|
|
2831
2938
|
// Private helpers
|
|
2832
2939
|
// ---------------------------------------------------------------------------
|
|
2940
|
+
/**
|
|
2941
|
+
* Infer a TaskType from issue labels.
|
|
2942
|
+
* Looks for common label patterns: bug/bugfix → 'bugfix', feat/feature → 'feature', etc.
|
|
2943
|
+
*/
|
|
2944
|
+
inferTaskType(labels) {
|
|
2945
|
+
const joined = labels.map((l) => l.toLowerCase()).join(" ");
|
|
2946
|
+
if (/\bbug(fix)?\b/.test(joined)) return "bugfix";
|
|
2947
|
+
if (/\bfeat(ure)?\b/.test(joined)) return "feature";
|
|
2948
|
+
if (/\brefactor\b/.test(joined)) return "refactor";
|
|
2949
|
+
if (/\bdoc(s|umentation)?\b/.test(joined)) return "docs";
|
|
2950
|
+
if (/\btest(s|ing)?\b/.test(joined)) return "test";
|
|
2951
|
+
if (/\bchore\b/.test(joined)) return "chore";
|
|
2952
|
+
return void 0;
|
|
2953
|
+
}
|
|
2833
2954
|
async recordOutcomeIfPipelineEnabled(issueId, reason, attempt, error, entry) {
|
|
2834
2955
|
if (!this.ctx.pipeline) return;
|
|
2835
2956
|
const enrichedSpec = this.ctx.enrichedSpecsByIssue.get(issueId);
|
|
2836
2957
|
const affectedSystemNodeIds = enrichedSpec ? enrichedSpec.affectedSystems.filter((s) => s.graphNodeId !== null).map((s) => s.graphNodeId) : [];
|
|
2958
|
+
const agentPersona = entry?.session?.backendName ?? this.ctx.config.agent.backend ?? "default";
|
|
2959
|
+
const labels = entry?.issue?.labels ?? [];
|
|
2960
|
+
const taskType = this.inferTaskType(labels);
|
|
2837
2961
|
const outcome = {
|
|
2838
2962
|
id: `outcome:${issueId}:${attempt ?? 0}`,
|
|
2839
2963
|
issueId,
|
|
@@ -2844,7 +2968,9 @@ var CompletionHandler = class {
|
|
|
2844
2968
|
durationMs: entry ? Date.now() - new Date(entry.startedAt).getTime() : 0,
|
|
2845
2969
|
linkedSpecId: enrichedSpec?.id ?? null,
|
|
2846
2970
|
affectedSystemNodeIds,
|
|
2847
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2971
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2972
|
+
agentPersona,
|
|
2973
|
+
...taskType ? { taskType } : {}
|
|
2848
2974
|
};
|
|
2849
2975
|
try {
|
|
2850
2976
|
this.ctx.pipeline.recordOutcome(outcome);
|
|
@@ -5577,6 +5703,37 @@ var PlanWatcher = class {
|
|
|
5577
5703
|
};
|
|
5578
5704
|
|
|
5579
5705
|
// src/server/http.ts
|
|
5706
|
+
var RATE_LIMIT = Number(process.env["HARNESS_RATE_LIMIT"]) || 100;
|
|
5707
|
+
var WINDOW_MS = 6e4;
|
|
5708
|
+
var rateBuckets = /* @__PURE__ */ new Map();
|
|
5709
|
+
var ratePruneTimer = setInterval(() => {
|
|
5710
|
+
const now = Date.now();
|
|
5711
|
+
for (const [ip, bucket] of rateBuckets) {
|
|
5712
|
+
if (bucket.resetAt <= now) rateBuckets.delete(ip);
|
|
5713
|
+
}
|
|
5714
|
+
}, 6e4);
|
|
5715
|
+
ratePruneTimer.unref();
|
|
5716
|
+
function isLocalhost(ip) {
|
|
5717
|
+
return ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1";
|
|
5718
|
+
}
|
|
5719
|
+
function checkRateLimit(req, res) {
|
|
5720
|
+
const ip = req.socket.remoteAddress ?? "unknown";
|
|
5721
|
+
if (!process.env["HARNESS_RATE_LIMIT_LOCALHOST"] && isLocalhost(ip)) return true;
|
|
5722
|
+
const now = Date.now();
|
|
5723
|
+
let bucket = rateBuckets.get(ip);
|
|
5724
|
+
if (!bucket || bucket.resetAt <= now) {
|
|
5725
|
+
bucket = { count: 0, resetAt: now + WINDOW_MS };
|
|
5726
|
+
rateBuckets.set(ip, bucket);
|
|
5727
|
+
}
|
|
5728
|
+
bucket.count++;
|
|
5729
|
+
if (bucket.count > RATE_LIMIT) {
|
|
5730
|
+
const retryAfter = Math.ceil((bucket.resetAt - now) / 1e3);
|
|
5731
|
+
res.writeHead(429, { "Content-Type": "application/json", "Retry-After": String(retryAfter) });
|
|
5732
|
+
res.end(JSON.stringify({ error: "Too Many Requests" }));
|
|
5733
|
+
return false;
|
|
5734
|
+
}
|
|
5735
|
+
return true;
|
|
5736
|
+
}
|
|
5580
5737
|
function getBindHost() {
|
|
5581
5738
|
return process.env["HOST"] ?? "127.0.0.1";
|
|
5582
5739
|
}
|
|
@@ -5664,6 +5821,9 @@ var OrchestratorServer = class {
|
|
|
5664
5821
|
if (this.handleStateEndpoint(req, res)) {
|
|
5665
5822
|
return;
|
|
5666
5823
|
}
|
|
5824
|
+
if (!checkRateLimit(req, res)) {
|
|
5825
|
+
return;
|
|
5826
|
+
}
|
|
5667
5827
|
if (this.handleApiRoutes(req, res)) {
|
|
5668
5828
|
return;
|
|
5669
5829
|
}
|
|
@@ -6292,6 +6452,18 @@ var MaintenanceScheduler = class {
|
|
|
6292
6452
|
// src/maintenance/reporter.ts
|
|
6293
6453
|
import * as fs14 from "fs";
|
|
6294
6454
|
import * as path14 from "path";
|
|
6455
|
+
import { z as z9 } from "zod";
|
|
6456
|
+
var RunResultSchema = z9.object({
|
|
6457
|
+
taskId: z9.string(),
|
|
6458
|
+
startedAt: z9.string(),
|
|
6459
|
+
completedAt: z9.string(),
|
|
6460
|
+
status: z9.enum(["success", "failure", "skipped", "no-issues"]),
|
|
6461
|
+
findings: z9.number(),
|
|
6462
|
+
fixed: z9.number(),
|
|
6463
|
+
prUrl: z9.string().nullable(),
|
|
6464
|
+
prUpdated: z9.boolean(),
|
|
6465
|
+
error: z9.string().optional()
|
|
6466
|
+
});
|
|
6295
6467
|
var MAX_HISTORY = 500;
|
|
6296
6468
|
var fallbackLogger = {
|
|
6297
6469
|
info: () => {
|
|
@@ -6317,9 +6489,9 @@ var MaintenanceReporter = class {
|
|
|
6317
6489
|
await fs14.promises.mkdir(this.persistDir, { recursive: true });
|
|
6318
6490
|
const filePath = path14.join(this.persistDir, "history.json");
|
|
6319
6491
|
const data = await fs14.promises.readFile(filePath, "utf-8");
|
|
6320
|
-
const parsed = JSON.parse(data);
|
|
6321
|
-
if (
|
|
6322
|
-
this.history = parsed.slice(0, MAX_HISTORY);
|
|
6492
|
+
const parsed = z9.array(RunResultSchema).safeParse(JSON.parse(data));
|
|
6493
|
+
if (parsed.success) {
|
|
6494
|
+
this.history = parsed.data.slice(0, MAX_HISTORY);
|
|
6323
6495
|
}
|
|
6324
6496
|
} catch (err) {
|
|
6325
6497
|
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
@@ -6470,7 +6642,8 @@ var TaskRunner = class {
|
|
|
6470
6642
|
const prResult = await this.prManager.ensurePR(task, summary);
|
|
6471
6643
|
prUrl = prResult.prUrl;
|
|
6472
6644
|
prUpdated = prResult.prUpdated;
|
|
6473
|
-
} catch {
|
|
6645
|
+
} catch (err) {
|
|
6646
|
+
console.warn(`[maintenance] PR creation failed for task ${task.id}: ${String(err)}`);
|
|
6474
6647
|
}
|
|
6475
6648
|
}
|
|
6476
6649
|
return {
|
|
@@ -6521,7 +6694,8 @@ var TaskRunner = class {
|
|
|
6521
6694
|
const prResult = await this.prManager.ensurePR(task, summary);
|
|
6522
6695
|
prUrl = prResult.prUrl;
|
|
6523
6696
|
prUpdated = prResult.prUpdated;
|
|
6524
|
-
} catch {
|
|
6697
|
+
} catch (err) {
|
|
6698
|
+
console.warn(`[maintenance] PR creation failed for task ${task.id}: ${String(err)}`);
|
|
6525
6699
|
}
|
|
6526
6700
|
}
|
|
6527
6701
|
return {
|
|
@@ -6631,6 +6805,10 @@ var Orchestrator = class extends EventEmitter {
|
|
|
6631
6805
|
enrichedSpecsByIssue = /* @__PURE__ */ new Map();
|
|
6632
6806
|
/** Tracks recently-failed intelligence analysis to avoid re-requesting every tick */
|
|
6633
6807
|
analysisFailureCache = /* @__PURE__ */ new Map();
|
|
6808
|
+
/** Abort controllers and PIDs for running agent tasks — used by stopIssue to cancel in-flight work.
|
|
6809
|
+
* The PID is stored here because the running entry may be deleted by the state machine
|
|
6810
|
+
* before the stop effect executes (e.g., stall_detected removes the entry first). */
|
|
6811
|
+
abortControllers = /* @__PURE__ */ new Map();
|
|
6634
6812
|
/** Guards against overlapping ticks when a tick takes longer than the polling interval */
|
|
6635
6813
|
tickInProgress = false;
|
|
6636
6814
|
/** Timestamp of the last stale branch sweep (at most once per hour) */
|
|
@@ -6761,33 +6939,63 @@ var Orchestrator = class extends EventEmitter {
|
|
|
6761
6939
|
}
|
|
6762
6940
|
/**
|
|
6763
6941
|
* Creates a TaskRunner for the maintenance scheduler.
|
|
6764
|
-
*
|
|
6765
|
-
*
|
|
6942
|
+
* CheckCommandRunner and CommandExecutor use real child_process execution.
|
|
6943
|
+
* AgentDispatcher remains stubbed (requires full skill dispatch integration).
|
|
6766
6944
|
*/
|
|
6767
6945
|
createMaintenanceTaskRunner(maintenanceConfig) {
|
|
6768
|
-
this.logger
|
|
6769
|
-
"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."
|
|
6770
|
-
);
|
|
6946
|
+
const logger = this.logger;
|
|
6771
6947
|
const checkRunner = {
|
|
6772
6948
|
run: async (command, cwd) => {
|
|
6773
|
-
|
|
6774
|
-
|
|
6949
|
+
const { execFile: execFile6 } = await import("child_process");
|
|
6950
|
+
const { promisify: promisify3 } = await import("util");
|
|
6951
|
+
const execFileAsync = promisify3(execFile6);
|
|
6952
|
+
const [cmd, ...args] = command;
|
|
6953
|
+
if (!cmd) return { passed: true, findings: 0, output: "" };
|
|
6954
|
+
try {
|
|
6955
|
+
const { stdout } = await execFileAsync(cmd, args, { cwd, timeout: 12e4 });
|
|
6956
|
+
const findingsMatch = stdout.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
|
|
6957
|
+
const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 0;
|
|
6958
|
+
return { passed: findings === 0, findings, output: stdout };
|
|
6959
|
+
} catch (err) {
|
|
6960
|
+
const error = err;
|
|
6961
|
+
const output = [error.stdout, error.stderr].filter(Boolean).join("\n");
|
|
6962
|
+
const findingsMatch = output.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
|
|
6963
|
+
const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 1;
|
|
6964
|
+
return { passed: false, findings, output };
|
|
6965
|
+
}
|
|
6775
6966
|
}
|
|
6776
6967
|
};
|
|
6777
6968
|
const agentDispatcher = {
|
|
6778
6969
|
dispatch: async (skill, branch, backendName, cwd) => {
|
|
6779
|
-
|
|
6780
|
-
skill,
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6970
|
+
logger.info(
|
|
6971
|
+
"Maintenance agent dispatcher invoked (stub \u2014 skill dispatch integration pending)",
|
|
6972
|
+
{
|
|
6973
|
+
skill,
|
|
6974
|
+
branch,
|
|
6975
|
+
backendName,
|
|
6976
|
+
cwd
|
|
6977
|
+
}
|
|
6978
|
+
);
|
|
6785
6979
|
return { producedCommits: false, fixed: 0 };
|
|
6786
6980
|
}
|
|
6787
6981
|
};
|
|
6788
6982
|
const commandExecutor = {
|
|
6789
6983
|
exec: async (command, cwd) => {
|
|
6790
|
-
|
|
6984
|
+
const { execFile: execFile6 } = await import("child_process");
|
|
6985
|
+
const { promisify: promisify3 } = await import("util");
|
|
6986
|
+
const execFileAsync = promisify3(execFile6);
|
|
6987
|
+
const [cmd, ...args] = command;
|
|
6988
|
+
if (!cmd) return;
|
|
6989
|
+
try {
|
|
6990
|
+
await execFileAsync(cmd, args, { cwd, timeout: 12e4 });
|
|
6991
|
+
} catch (err) {
|
|
6992
|
+
logger.warn("Maintenance command execution failed", {
|
|
6993
|
+
command,
|
|
6994
|
+
cwd,
|
|
6995
|
+
error: String(err)
|
|
6996
|
+
});
|
|
6997
|
+
throw err;
|
|
6998
|
+
}
|
|
6791
6999
|
}
|
|
6792
7000
|
};
|
|
6793
7001
|
return new TaskRunner({
|
|
@@ -6817,19 +7025,18 @@ var Orchestrator = class extends EventEmitter {
|
|
|
6817
7025
|
historyProvider: reporter,
|
|
6818
7026
|
onTaskDue: async (task) => {
|
|
6819
7027
|
this.logger.info(`Maintenance task due: ${task.id}`, { taskId: task.id });
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
});
|
|
7028
|
+
const startPayload = { taskId: task.id, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
7029
|
+
this.server?.broadcastMaintenance("maintenance:started", startPayload);
|
|
7030
|
+
this.emit("maintenance:started", startPayload);
|
|
6824
7031
|
const result = await taskRunner.run(task);
|
|
6825
7032
|
await reporter.record(result);
|
|
6826
7033
|
if (result.status === "failure") {
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
});
|
|
7034
|
+
const errorPayload = { taskId: task.id, error: result.error };
|
|
7035
|
+
this.server?.broadcastMaintenance("maintenance:error", errorPayload);
|
|
7036
|
+
this.emit("maintenance:error", errorPayload);
|
|
6831
7037
|
} else {
|
|
6832
7038
|
this.server?.broadcastMaintenance("maintenance:completed", result);
|
|
7039
|
+
this.emit("maintenance:completed", result);
|
|
6833
7040
|
}
|
|
6834
7041
|
this.logger.info(`Maintenance task completed: ${task.id}`, {
|
|
6835
7042
|
taskId: task.id,
|
|
@@ -7017,7 +7224,13 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7017
7224
|
(phase, detail, progress) => this.setTickActivity(phase, detail, progress)
|
|
7018
7225
|
) : void 0;
|
|
7019
7226
|
this.setTickActivity("dispatching", "Applying state machine");
|
|
7020
|
-
const {
|
|
7227
|
+
const {
|
|
7228
|
+
concernSignals,
|
|
7229
|
+
enrichedSpecs,
|
|
7230
|
+
complexityScores,
|
|
7231
|
+
simulationResults,
|
|
7232
|
+
personaRecommendations
|
|
7233
|
+
} = pipelineResult ?? {};
|
|
7021
7234
|
const tickEvent = {
|
|
7022
7235
|
type: "tick",
|
|
7023
7236
|
candidates,
|
|
@@ -7026,7 +7239,8 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7026
7239
|
...concernSignals !== void 0 && { concernSignals },
|
|
7027
7240
|
...enrichedSpecs !== void 0 && { enrichedSpecs },
|
|
7028
7241
|
...complexityScores !== void 0 && { complexityScores },
|
|
7029
|
-
...simulationResults !== void 0 && { simulationResults }
|
|
7242
|
+
...simulationResults !== void 0 && { simulationResults },
|
|
7243
|
+
...personaRecommendations !== void 0 && { personaRecommendations }
|
|
7030
7244
|
};
|
|
7031
7245
|
let { nextState, effects } = applyEvent(this.state, tickEvent, this.config);
|
|
7032
7246
|
this.state = nextState;
|
|
@@ -7036,7 +7250,8 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7036
7250
|
type: "retry_fired",
|
|
7037
7251
|
issueId,
|
|
7038
7252
|
candidates,
|
|
7039
|
-
nowMs
|
|
7253
|
+
nowMs,
|
|
7254
|
+
...concernSignals !== void 0 && { concernSignals }
|
|
7040
7255
|
};
|
|
7041
7256
|
const result = applyEvent(this.state, retryEvent, this.config);
|
|
7042
7257
|
this.state = result.nextState;
|
|
@@ -7045,6 +7260,35 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7045
7260
|
for (const effect of effects) {
|
|
7046
7261
|
await this.handleEffect(effect);
|
|
7047
7262
|
}
|
|
7263
|
+
const stallTimeoutMs = this.config.agent.stallTimeoutMs;
|
|
7264
|
+
if (stallTimeoutMs > 0) {
|
|
7265
|
+
const stalledIds = [];
|
|
7266
|
+
for (const [runId, runEntry] of this.state.running) {
|
|
7267
|
+
const lastTs = runEntry.session?.lastTimestamp;
|
|
7268
|
+
if (!lastTs) continue;
|
|
7269
|
+
const silentMs = nowMs - new Date(lastTs).getTime();
|
|
7270
|
+
if (silentMs >= stallTimeoutMs) {
|
|
7271
|
+
stalledIds.push(runId);
|
|
7272
|
+
}
|
|
7273
|
+
}
|
|
7274
|
+
for (const runId of stalledIds) {
|
|
7275
|
+
const runEntry = this.state.running.get(runId);
|
|
7276
|
+
if (!runEntry) continue;
|
|
7277
|
+
this.logger.warn(
|
|
7278
|
+
`Agent stalled for ${runEntry.identifier}: ${Math.round((nowMs - new Date(runEntry.session?.lastTimestamp ?? 0).getTime()) / 1e3)}s since last event`,
|
|
7279
|
+
{ issueId: runId }
|
|
7280
|
+
);
|
|
7281
|
+
const stallEvent = {
|
|
7282
|
+
type: "stall_detected",
|
|
7283
|
+
issueId: runId
|
|
7284
|
+
};
|
|
7285
|
+
const stallResult = applyEvent(this.state, stallEvent, this.config);
|
|
7286
|
+
this.state = stallResult.nextState;
|
|
7287
|
+
for (const eff of stallResult.effects) {
|
|
7288
|
+
await this.handleEffect(eff);
|
|
7289
|
+
}
|
|
7290
|
+
}
|
|
7291
|
+
}
|
|
7048
7292
|
const openPrNumbers = [];
|
|
7049
7293
|
for (const [, runEntry] of this.state.running) {
|
|
7050
7294
|
const externalId = runEntry.issue.externalId;
|
|
@@ -7091,9 +7335,6 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7091
7335
|
*/
|
|
7092
7336
|
async handleEffect(effect) {
|
|
7093
7337
|
switch (effect.type) {
|
|
7094
|
-
case "dispatch":
|
|
7095
|
-
await this.dispatchIssue(effect.issue, effect.attempt, effect.backend);
|
|
7096
|
-
break;
|
|
7097
7338
|
case "stop":
|
|
7098
7339
|
await this.stopIssue(effect.issueId);
|
|
7099
7340
|
break;
|
|
@@ -7104,6 +7345,11 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7104
7345
|
break;
|
|
7105
7346
|
case "releaseClaim":
|
|
7106
7347
|
break;
|
|
7348
|
+
case "scheduleRetry":
|
|
7349
|
+
this.logger.info(
|
|
7350
|
+
`Retry scheduled for ${effect.issueId} (attempt ${effect.attempt}, delay ${effect.delayMs}ms)`
|
|
7351
|
+
);
|
|
7352
|
+
break;
|
|
7107
7353
|
case "cleanWorkspace":
|
|
7108
7354
|
await this.cleanWorkspaceWithGuard(effect.identifier, effect.issueId);
|
|
7109
7355
|
break;
|
|
@@ -7129,6 +7375,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7129
7375
|
`Branch "${branch}" not found on remote for ${identifier}, cleaning up worktree`,
|
|
7130
7376
|
{ issueId }
|
|
7131
7377
|
);
|
|
7378
|
+
await this.runBeforeRemoveHook(identifier);
|
|
7132
7379
|
await this.workspace.removeWorkspace(identifier);
|
|
7133
7380
|
return;
|
|
7134
7381
|
}
|
|
@@ -7163,8 +7410,17 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7163
7410
|
return;
|
|
7164
7411
|
}
|
|
7165
7412
|
}
|
|
7413
|
+
await this.runBeforeRemoveHook(identifier);
|
|
7166
7414
|
await this.workspace.removeWorkspace(identifier);
|
|
7167
7415
|
}
|
|
7416
|
+
/** Run the beforeRemove hook for a workspace. Failures are logged but non-fatal. */
|
|
7417
|
+
async runBeforeRemoveHook(identifier) {
|
|
7418
|
+
const wsPath = this.workspace.resolvePath(identifier);
|
|
7419
|
+
const result = await this.hooks.beforeRemove(wsPath);
|
|
7420
|
+
if (!result.ok) {
|
|
7421
|
+
this.logger.warn(`beforeRemove hook failed for ${identifier}: ${result.error.message}`);
|
|
7422
|
+
}
|
|
7423
|
+
}
|
|
7168
7424
|
/**
|
|
7169
7425
|
* Delegates to PRDetector.filterCandidatesWithOpenPRs.
|
|
7170
7426
|
* @see PRDetector#filterCandidatesWithOpenPRs
|
|
@@ -7334,7 +7590,11 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7334
7590
|
if (!result.ok) {
|
|
7335
7591
|
this.logger.warn(`Lifecycle comment failed for ${identifier}: ${result.error.message}`);
|
|
7336
7592
|
}
|
|
7337
|
-
} catch {
|
|
7593
|
+
} catch (err) {
|
|
7594
|
+
this.logger.debug("Lifecycle comment failed (best-effort)", {
|
|
7595
|
+
identifier,
|
|
7596
|
+
error: String(err)
|
|
7597
|
+
});
|
|
7338
7598
|
}
|
|
7339
7599
|
}
|
|
7340
7600
|
/**
|
|
@@ -7351,6 +7611,12 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7351
7611
|
const workspaceResult = await this.workspace.ensureWorkspace(issue.identifier);
|
|
7352
7612
|
if (!workspaceResult.ok) throw workspaceResult.error;
|
|
7353
7613
|
const workspacePath = workspaceResult.value;
|
|
7614
|
+
const afterCreateResult = await this.hooks.afterCreate(workspacePath);
|
|
7615
|
+
if (!afterCreateResult.ok) {
|
|
7616
|
+
this.logger.warn(
|
|
7617
|
+
`afterCreate hook failed for ${issue.identifier}: ${afterCreateResult.error.message}`
|
|
7618
|
+
);
|
|
7619
|
+
}
|
|
7354
7620
|
const hookResult = await this.hooks.beforeRun(workspacePath);
|
|
7355
7621
|
if (!hookResult.ok) throw hookResult.error;
|
|
7356
7622
|
const scanResult = await scanWorkspaceConfig(workspacePath);
|
|
@@ -7460,21 +7726,54 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7460
7726
|
runAgentInBackgroundTask(issue, workspacePath, prompt, attempt, runner) {
|
|
7461
7727
|
const activeRunner = runner ?? this.runner;
|
|
7462
7728
|
this.logger.info(`Starting background task for ${issue.identifier}`);
|
|
7729
|
+
const abortController = new AbortController();
|
|
7730
|
+
this.abortControllers.set(issue.id, { controller: abortController, pid: null });
|
|
7463
7731
|
(async () => {
|
|
7464
7732
|
try {
|
|
7465
7733
|
this.logger.info(`Calling runner.runSession for ${issue.identifier}`);
|
|
7466
7734
|
const sessionGen = activeRunner.runSession(issue, workspacePath, prompt);
|
|
7467
7735
|
for await (const event of sessionGen) {
|
|
7736
|
+
if (abortController.signal.aborted) {
|
|
7737
|
+
this.logger.info(`Agent session aborted for ${issue.identifier}`);
|
|
7738
|
+
break;
|
|
7739
|
+
}
|
|
7740
|
+
if (event.type === "session_started" && event.content) {
|
|
7741
|
+
const pid = event.content.pid;
|
|
7742
|
+
if (pid) {
|
|
7743
|
+
const tracked = this.abortControllers.get(issue.id);
|
|
7744
|
+
if (tracked) tracked.pid = pid;
|
|
7745
|
+
}
|
|
7746
|
+
}
|
|
7468
7747
|
await this.processAgentEvent(issue, event);
|
|
7469
7748
|
if (event.type === "turn_start") {
|
|
7470
7749
|
await this.awaitRateLimitClearance(issue.identifier);
|
|
7471
7750
|
}
|
|
7472
7751
|
}
|
|
7473
7752
|
this.logger.info(`Session generator finished for ${issue.identifier}`);
|
|
7474
|
-
await this.
|
|
7753
|
+
const afterRunResult = await this.hooks.afterRun(workspacePath);
|
|
7754
|
+
if (!afterRunResult.ok) {
|
|
7755
|
+
this.logger.warn(
|
|
7756
|
+
`afterRun hook failed for ${issue.identifier}: ${afterRunResult.error.message}`
|
|
7757
|
+
);
|
|
7758
|
+
}
|
|
7759
|
+
if (abortController.signal.aborted) {
|
|
7760
|
+
if (this.state.running.has(issue.id)) {
|
|
7761
|
+
await this.emitWorkerExit(issue.id, "error", attempt, "Stopped by reconciliation");
|
|
7762
|
+
}
|
|
7763
|
+
} else {
|
|
7764
|
+
await this.emitWorkerExit(issue.id, "normal", attempt);
|
|
7765
|
+
}
|
|
7475
7766
|
} catch (error) {
|
|
7476
7767
|
this.logger.error(`Agent runner failed for ${issue.identifier}`, { error: String(error) });
|
|
7768
|
+
const afterRunResult = await this.hooks.afterRun(workspacePath);
|
|
7769
|
+
if (!afterRunResult.ok) {
|
|
7770
|
+
this.logger.warn(
|
|
7771
|
+
`afterRun hook failed for ${issue.identifier}: ${afterRunResult.error.message}`
|
|
7772
|
+
);
|
|
7773
|
+
}
|
|
7477
7774
|
await this.emitWorkerExit(issue.id, "error", attempt, String(error));
|
|
7775
|
+
} finally {
|
|
7776
|
+
this.abortControllers.delete(issue.id);
|
|
7478
7777
|
}
|
|
7479
7778
|
})().catch((err) => {
|
|
7480
7779
|
this.logger.error("Fatal error in background task", { error: String(err) });
|
|
@@ -7500,6 +7799,19 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7500
7799
|
*/
|
|
7501
7800
|
async stopIssue(issueId) {
|
|
7502
7801
|
this.logger.info(`Stopping issue: ${issueId}`);
|
|
7802
|
+
const tracked = this.abortControllers.get(issueId);
|
|
7803
|
+
if (tracked) {
|
|
7804
|
+
tracked.controller.abort();
|
|
7805
|
+
this.logger.info(`Abort signal sent for ${issueId}`);
|
|
7806
|
+
}
|
|
7807
|
+
const pid = tracked?.pid ?? this.state.running.get(issueId)?.session?.agentPid;
|
|
7808
|
+
if (pid) {
|
|
7809
|
+
try {
|
|
7810
|
+
process.kill(pid, "SIGTERM");
|
|
7811
|
+
this.logger.info(`Sent SIGTERM to agent PID ${pid} for ${issueId}`);
|
|
7812
|
+
} catch {
|
|
7813
|
+
}
|
|
7814
|
+
}
|
|
7503
7815
|
}
|
|
7504
7816
|
/**
|
|
7505
7817
|
* Dispatch a work item immediately, bypassing the normal tick → roadmap cycle.
|