@harness-engineering/orchestrator 0.3.2 → 0.4.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 +114 -66
- package/dist/index.d.ts +114 -66
- package/dist/index.js +754 -455
- package/dist/index.mjs +779 -466
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -570,6 +570,56 @@ function reconcileCompletedAndClaimed(next, candidates, nowMs, effects) {
|
|
|
570
570
|
}
|
|
571
571
|
}
|
|
572
572
|
}
|
|
573
|
+
function gatherSignalsAndPersona(issue, event) {
|
|
574
|
+
const signals = [...event.concernSignals?.get(issue.id) ?? []];
|
|
575
|
+
let suggestedPersona;
|
|
576
|
+
try {
|
|
577
|
+
const personaRecs = event.personaRecommendations?.get(issue.id);
|
|
578
|
+
if (personaRecs && personaRecs.length > 0) {
|
|
579
|
+
suggestedPersona = personaRecs[0].persona;
|
|
580
|
+
if (personaRecs[0].weightedScore < 0.3) {
|
|
581
|
+
signals.push({
|
|
582
|
+
name: "lowExpertise",
|
|
583
|
+
reason: `Top persona "${suggestedPersona}" scored ${personaRecs[0].weightedScore.toFixed(2)} (below 0.3 threshold)`
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
} else if (personaRecs && personaRecs.length === 0) {
|
|
587
|
+
signals.push({
|
|
588
|
+
name: "noPersonaMatch",
|
|
589
|
+
reason: "No persona recommendations available for this issue's systems"
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
} catch {
|
|
593
|
+
}
|
|
594
|
+
return { signals, suggestedPersona };
|
|
595
|
+
}
|
|
596
|
+
function attachPersonaToLastClaim(effects, suggestedPersona) {
|
|
597
|
+
if (!suggestedPersona) return;
|
|
598
|
+
const lastEffect = effects[effects.length - 1];
|
|
599
|
+
if (lastEffect && lastEffect.type === "claim") {
|
|
600
|
+
lastEffect.suggestedPersona = suggestedPersona;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
function dispatchEligibleIssue(next, issue, event, escalationConfig, config, effects) {
|
|
604
|
+
const scopeTier = detectScopeTier(issue, artifactPresenceFromIssue(issue));
|
|
605
|
+
const { signals, suggestedPersona } = gatherSignalsAndPersona(issue, event);
|
|
606
|
+
const decision = routeIssue(scopeTier, signals, escalationConfig);
|
|
607
|
+
if (decision.action === "needs-human") {
|
|
608
|
+
next.claimed.add(issue.id);
|
|
609
|
+
effects.push(
|
|
610
|
+
buildEscalateEffect(issue.id, issue.identifier, decision.reasons, {
|
|
611
|
+
issueTitle: issue.title,
|
|
612
|
+
issueDescription: issue.description,
|
|
613
|
+
enrichedSpec: event.enrichedSpecs?.get(issue.id),
|
|
614
|
+
complexityScore: event.complexityScores?.get(issue.id)
|
|
615
|
+
})
|
|
616
|
+
);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const backend = resolveBackend(decision.action, !!config.agent.localBackend);
|
|
620
|
+
claimAndDispatch(next, issue, backend, event.nowMs, effects);
|
|
621
|
+
attachPersonaToLastClaim(effects, suggestedPersona);
|
|
622
|
+
}
|
|
573
623
|
function handleTick(state, event, config) {
|
|
574
624
|
const { candidates, runningStates, nowMs } = event;
|
|
575
625
|
const next = cloneState(state);
|
|
@@ -600,48 +650,7 @@ function handleTick(state, event, config) {
|
|
|
600
650
|
effects.push(peslAbort);
|
|
601
651
|
continue;
|
|
602
652
|
}
|
|
603
|
-
|
|
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
|
-
}
|
|
624
|
-
const decision = routeIssue(scopeTier, signals, escalationConfig);
|
|
625
|
-
if (decision.action === "needs-human") {
|
|
626
|
-
next.claimed.add(issue.id);
|
|
627
|
-
effects.push(
|
|
628
|
-
buildEscalateEffect(issue.id, issue.identifier, decision.reasons, {
|
|
629
|
-
issueTitle: issue.title,
|
|
630
|
-
issueDescription: issue.description,
|
|
631
|
-
enrichedSpec: event.enrichedSpecs?.get(issue.id),
|
|
632
|
-
complexityScore: event.complexityScores?.get(issue.id)
|
|
633
|
-
})
|
|
634
|
-
);
|
|
635
|
-
continue;
|
|
636
|
-
}
|
|
637
|
-
const backend = resolveBackend(decision.action, !!config.agent.localBackend);
|
|
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
|
-
}
|
|
653
|
+
dispatchEligibleIssue(next, issue, event, escalationConfig, config, effects);
|
|
645
654
|
}
|
|
646
655
|
pruneCompleted(next);
|
|
647
656
|
return { nextState: next, effects };
|
|
@@ -1840,11 +1849,11 @@ var BackendsMapSchema = z2.record(z2.string(), BackendDefSchema);
|
|
|
1840
1849
|
function crossFieldRoutingIssues(backends, routing) {
|
|
1841
1850
|
const issues = [];
|
|
1842
1851
|
const names = new Set(Object.keys(backends));
|
|
1843
|
-
const checkRef = (
|
|
1852
|
+
const checkRef = (path17, name) => {
|
|
1844
1853
|
if (name !== void 0 && !names.has(name)) {
|
|
1845
1854
|
issues.push({
|
|
1846
|
-
path:
|
|
1847
|
-
message: `routing.${
|
|
1855
|
+
path: path17,
|
|
1856
|
+
message: `routing.${path17.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
|
|
1848
1857
|
});
|
|
1849
1858
|
}
|
|
1850
1859
|
};
|
|
@@ -1982,7 +1991,10 @@ var WorkflowLoader = class {
|
|
|
1982
1991
|
// src/tracker/adapters/roadmap.ts
|
|
1983
1992
|
import * as fs7 from "fs/promises";
|
|
1984
1993
|
import { createHash as createHash2 } from "crypto";
|
|
1985
|
-
import {
|
|
1994
|
+
import {
|
|
1995
|
+
parseRoadmap,
|
|
1996
|
+
serializeRoadmap
|
|
1997
|
+
} from "@harness-engineering/core";
|
|
1986
1998
|
import {
|
|
1987
1999
|
Ok as Ok4,
|
|
1988
2000
|
Err as Err3
|
|
@@ -2206,8 +2218,11 @@ var WorkspaceManager = class {
|
|
|
2206
2218
|
config;
|
|
2207
2219
|
/** Absolute path to the git repository root (resolved lazily). */
|
|
2208
2220
|
repoRoot = null;
|
|
2209
|
-
|
|
2221
|
+
/** Phase 3 (D6): emit baseref_fallback when fallback chain selects a local-only ref. */
|
|
2222
|
+
emitEvent;
|
|
2223
|
+
constructor(config, options = {}) {
|
|
2210
2224
|
this.config = config;
|
|
2225
|
+
this.emitEvent = options.emitEvent ?? null;
|
|
2211
2226
|
}
|
|
2212
2227
|
/** Runs a git command and returns stdout. Extracted for testability. */
|
|
2213
2228
|
async git(args, cwd) {
|
|
@@ -2292,9 +2307,14 @@ var WorkspaceManager = class {
|
|
|
2292
2307
|
* Priority order:
|
|
2293
2308
|
* 1. `config.baseRef` (explicit override). Throws if it doesn't resolve.
|
|
2294
2309
|
* 2. Default branch via `git symbolic-ref --short refs/remotes/origin/HEAD`.
|
|
2295
|
-
* 3.
|
|
2296
|
-
* 4.
|
|
2297
|
-
*
|
|
2310
|
+
* 3. Remote fallbacks: `origin/main`, `origin/master`. (No event.)
|
|
2311
|
+
* 4. Local-only fallbacks: `main`, `master`. (Emits `baseref_fallback`.)
|
|
2312
|
+
* 5. `HEAD` as ultimate fallback. (Emits `baseref_fallback`.)
|
|
2313
|
+
*
|
|
2314
|
+
* Phase 3 / spec D6 / R4: when the priority chain falls past `origin/*`
|
|
2315
|
+
* to a local-only ref, the optional `emitEvent` callback (if injected)
|
|
2316
|
+
* is invoked exactly once with `{ kind: 'baseref_fallback', ref, repoRoot }`
|
|
2317
|
+
* so operators are warned when the remote is misconfigured or unreachable.
|
|
2298
2318
|
*/
|
|
2299
2319
|
async resolveBaseRef(repoRoot) {
|
|
2300
2320
|
const configured = this.config.baseRef;
|
|
@@ -2313,11 +2333,30 @@ var WorkspaceManager = class {
|
|
|
2313
2333
|
if (detected) return detected;
|
|
2314
2334
|
} catch {
|
|
2315
2335
|
}
|
|
2316
|
-
for (const candidate of ["origin/main", "origin/master"
|
|
2336
|
+
for (const candidate of ["origin/main", "origin/master"]) {
|
|
2317
2337
|
if (await this.refExists(candidate, repoRoot)) return candidate;
|
|
2318
2338
|
}
|
|
2339
|
+
for (const candidate of ["main", "master"]) {
|
|
2340
|
+
if (await this.refExists(candidate, repoRoot)) {
|
|
2341
|
+
this.emitFallback(candidate, repoRoot);
|
|
2342
|
+
return candidate;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
this.emitFallback("HEAD", repoRoot);
|
|
2319
2346
|
return "HEAD";
|
|
2320
2347
|
}
|
|
2348
|
+
/**
|
|
2349
|
+
* Phase 3 (D6): emit a `baseref_fallback` event via the injected
|
|
2350
|
+
* callback (if any). Errors from the callback are swallowed so a
|
|
2351
|
+
* broken emitter does not block worktree dispatch.
|
|
2352
|
+
*/
|
|
2353
|
+
emitFallback(ref, repoRoot) {
|
|
2354
|
+
if (!this.emitEvent) return;
|
|
2355
|
+
try {
|
|
2356
|
+
this.emitEvent({ kind: "baseref_fallback", ref, repoRoot });
|
|
2357
|
+
} catch {
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2321
2360
|
/** Returns true iff `git rev-parse --verify` accepts the ref. */
|
|
2322
2361
|
async refExists(ref, repoRoot) {
|
|
2323
2362
|
try {
|
|
@@ -2601,16 +2640,9 @@ var PromptRenderer = class {
|
|
|
2601
2640
|
|
|
2602
2641
|
// src/orchestrator.ts
|
|
2603
2642
|
import { EventEmitter } from "events";
|
|
2604
|
-
import * as
|
|
2643
|
+
import * as path16 from "path";
|
|
2605
2644
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
2606
2645
|
import { writeTaint } from "@harness-engineering/core";
|
|
2607
|
-
import {
|
|
2608
|
-
IntelligencePipeline,
|
|
2609
|
-
AnthropicAnalysisProvider as AnthropicAnalysisProvider2,
|
|
2610
|
-
OpenAICompatibleAnalysisProvider as OpenAICompatibleAnalysisProvider2,
|
|
2611
|
-
ClaudeCliAnalysisProvider as ClaudeCliAnalysisProvider2
|
|
2612
|
-
} from "@harness-engineering/intelligence";
|
|
2613
|
-
import { GraphStore } from "@harness-engineering/graph";
|
|
2614
2646
|
|
|
2615
2647
|
// src/intelligence/pipeline-runner.ts
|
|
2616
2648
|
import * as path7 from "path";
|
|
@@ -3174,7 +3206,87 @@ var CompletionHandler = class {
|
|
|
3174
3206
|
};
|
|
3175
3207
|
|
|
3176
3208
|
// src/orchestrator.ts
|
|
3177
|
-
import {
|
|
3209
|
+
import {
|
|
3210
|
+
GitHubIssuesSyncAdapter as GitHubIssuesSyncAdapter3,
|
|
3211
|
+
loadTrackerSyncConfig as loadTrackerSyncConfig3,
|
|
3212
|
+
createTrackerClient as createTrackerClient2
|
|
3213
|
+
} from "@harness-engineering/core";
|
|
3214
|
+
|
|
3215
|
+
// src/tracker/adapters/github-issues-issue-tracker.ts
|
|
3216
|
+
import { Ok as Ok9, Err as Err6 } from "@harness-engineering/types";
|
|
3217
|
+
var GitHubIssuesIssueTrackerAdapter = class {
|
|
3218
|
+
client;
|
|
3219
|
+
config;
|
|
3220
|
+
constructor(client, config) {
|
|
3221
|
+
this.client = client;
|
|
3222
|
+
this.config = config;
|
|
3223
|
+
}
|
|
3224
|
+
async fetchCandidateIssues() {
|
|
3225
|
+
return this.fetchIssuesByStates(this.config.activeStates);
|
|
3226
|
+
}
|
|
3227
|
+
async fetchIssuesByStates(stateNames) {
|
|
3228
|
+
const r = await this.client.fetchByStatus(
|
|
3229
|
+
stateNames
|
|
3230
|
+
);
|
|
3231
|
+
if (!r.ok) return Err6(r.error);
|
|
3232
|
+
return Ok9(r.value.map((f) => this.mapTrackedToIssue(f)));
|
|
3233
|
+
}
|
|
3234
|
+
async fetchIssueStatesByIds(issueIds) {
|
|
3235
|
+
const r = await this.client.fetchAll();
|
|
3236
|
+
if (!r.ok) return Err6(r.error);
|
|
3237
|
+
const wanted = new Set(issueIds);
|
|
3238
|
+
const out = /* @__PURE__ */ new Map();
|
|
3239
|
+
for (const f of r.value.features) {
|
|
3240
|
+
if (wanted.has(f.externalId)) out.set(f.externalId, this.mapTrackedToIssue(f));
|
|
3241
|
+
}
|
|
3242
|
+
return Ok9(out);
|
|
3243
|
+
}
|
|
3244
|
+
async claimIssue(issueId, orchestratorId) {
|
|
3245
|
+
const r = await this.client.claim(issueId, orchestratorId);
|
|
3246
|
+
if (!r.ok) return Err6(r.error);
|
|
3247
|
+
return Ok9(void 0);
|
|
3248
|
+
}
|
|
3249
|
+
async releaseIssue(issueId) {
|
|
3250
|
+
const r = await this.client.release(issueId);
|
|
3251
|
+
if (!r.ok) return Err6(r.error);
|
|
3252
|
+
return Ok9(void 0);
|
|
3253
|
+
}
|
|
3254
|
+
async markIssueComplete(issueId) {
|
|
3255
|
+
const r = await this.client.complete(issueId);
|
|
3256
|
+
if (!r.ok) return Err6(r.error);
|
|
3257
|
+
return Ok9(void 0);
|
|
3258
|
+
}
|
|
3259
|
+
/**
|
|
3260
|
+
* Project a wide-interface `TrackedFeature` onto the small-interface
|
|
3261
|
+
* `Issue` shape consumed by the orchestrator's tick loop.
|
|
3262
|
+
*/
|
|
3263
|
+
mapTrackedToIssue(f) {
|
|
3264
|
+
return {
|
|
3265
|
+
id: f.externalId,
|
|
3266
|
+
identifier: f.externalId,
|
|
3267
|
+
title: f.name,
|
|
3268
|
+
description: f.summary,
|
|
3269
|
+
priority: null,
|
|
3270
|
+
state: f.status,
|
|
3271
|
+
branchName: null,
|
|
3272
|
+
url: null,
|
|
3273
|
+
labels: [],
|
|
3274
|
+
spec: f.spec,
|
|
3275
|
+
plans: f.plans,
|
|
3276
|
+
blockedBy: f.blockedBy.map(
|
|
3277
|
+
(b) => ({
|
|
3278
|
+
id: null,
|
|
3279
|
+
identifier: b,
|
|
3280
|
+
state: null
|
|
3281
|
+
})
|
|
3282
|
+
),
|
|
3283
|
+
createdAt: f.createdAt,
|
|
3284
|
+
updatedAt: f.updatedAt,
|
|
3285
|
+
externalId: f.externalId,
|
|
3286
|
+
assignee: f.assignee
|
|
3287
|
+
};
|
|
3288
|
+
}
|
|
3289
|
+
};
|
|
3178
3290
|
|
|
3179
3291
|
// src/agent/runner.ts
|
|
3180
3292
|
var MAX_SLEEP_MS = 12 * 60 * 6e4;
|
|
@@ -3476,9 +3588,17 @@ var LocalModelResolver = class {
|
|
|
3476
3588
|
|
|
3477
3589
|
// src/agent/config-migration.ts
|
|
3478
3590
|
var MIGRATION_GUIDE = "docs/guides/multi-backend-routing.md";
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3591
|
+
var CASE1_ALWAYS_SUPPRESS = /* @__PURE__ */ new Set(["agent.backend"]);
|
|
3592
|
+
var CASE1_LOCAL_GROUP = /* @__PURE__ */ new Set([
|
|
3593
|
+
"agent.localBackend",
|
|
3594
|
+
"agent.localEndpoint",
|
|
3595
|
+
"agent.localModel",
|
|
3596
|
+
"agent.localApiKey",
|
|
3597
|
+
"agent.localTimeoutMs",
|
|
3598
|
+
"agent.localProbeIntervalMs"
|
|
3599
|
+
]);
|
|
3600
|
+
function detectLegacyFields(agent) {
|
|
3601
|
+
const fields = [
|
|
3482
3602
|
{ path: "agent.backend", present: agent.backend !== void 0 && agent.backend !== "" },
|
|
3483
3603
|
{ path: "agent.command", present: agent.command !== void 0 },
|
|
3484
3604
|
{ path: "agent.model", present: agent.model !== void 0 },
|
|
@@ -3490,56 +3610,73 @@ function migrateAgentConfig(agent) {
|
|
|
3490
3610
|
{ path: "agent.localTimeoutMs", present: agent.localTimeoutMs !== void 0 },
|
|
3491
3611
|
{ path: "agent.localProbeIntervalMs", present: agent.localProbeIntervalMs !== void 0 }
|
|
3492
3612
|
];
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
const suppressLocalGroup = agent.localBackend !== void 0;
|
|
3504
|
-
if (agent.backends !== void 0) {
|
|
3505
|
-
for (const path16 of presentLegacy) {
|
|
3506
|
-
if (CASE1_ALWAYS_SUPPRESS.has(path16)) continue;
|
|
3507
|
-
if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path16)) continue;
|
|
3508
|
-
warnings.push(
|
|
3509
|
-
`Ignoring legacy field '${path16}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
|
|
3510
|
-
);
|
|
3511
|
-
}
|
|
3512
|
-
return { config: agent, warnings };
|
|
3513
|
-
}
|
|
3514
|
-
if (presentLegacy.length === 0) {
|
|
3515
|
-
return { config: agent, warnings };
|
|
3613
|
+
return fields.filter((f) => f.present).map((f) => f.path);
|
|
3614
|
+
}
|
|
3615
|
+
function buildCase1Warnings(presentLegacy, suppressLocalGroup) {
|
|
3616
|
+
const warnings = [];
|
|
3617
|
+
for (const path17 of presentLegacy) {
|
|
3618
|
+
if (CASE1_ALWAYS_SUPPRESS.has(path17)) continue;
|
|
3619
|
+
if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path17)) continue;
|
|
3620
|
+
warnings.push(
|
|
3621
|
+
`Ignoring legacy field '${path17}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
|
|
3622
|
+
);
|
|
3516
3623
|
}
|
|
3517
|
-
|
|
3624
|
+
return warnings;
|
|
3625
|
+
}
|
|
3626
|
+
function synthesizeBackendsAndRouting(agent) {
|
|
3627
|
+
const backends = { primary: synthesizePrimary(agent) };
|
|
3518
3628
|
const routing = { default: "primary" };
|
|
3519
|
-
backends.primary = synthesizePrimary(agent);
|
|
3520
3629
|
if (agent.localBackend !== void 0) {
|
|
3521
3630
|
backends.local = synthesizeLocal(agent);
|
|
3631
|
+
const autoExec = agent.escalation?.autoExecute ?? [];
|
|
3632
|
+
for (const tier of autoExec) routing[tier] = "local";
|
|
3522
3633
|
}
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3634
|
+
return { backends, routing };
|
|
3635
|
+
}
|
|
3636
|
+
function migrateAgentConfig(agent) {
|
|
3637
|
+
const presentLegacy = detectLegacyFields(agent);
|
|
3638
|
+
if (agent.backends !== void 0) {
|
|
3639
|
+
return {
|
|
3640
|
+
config: agent,
|
|
3641
|
+
warnings: buildCase1Warnings(presentLegacy, agent.localBackend !== void 0)
|
|
3642
|
+
};
|
|
3528
3643
|
}
|
|
3529
|
-
|
|
3530
|
-
warnings
|
|
3531
|
-
`Deprecated config field '${path16}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
|
|
3532
|
-
);
|
|
3644
|
+
if (presentLegacy.length === 0) {
|
|
3645
|
+
return { config: agent, warnings: [] };
|
|
3533
3646
|
}
|
|
3647
|
+
const { backends, routing } = synthesizeBackendsAndRouting(agent);
|
|
3648
|
+
const warnings = presentLegacy.map(
|
|
3649
|
+
(path17) => `Deprecated config field '${path17}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
|
|
3650
|
+
);
|
|
3534
3651
|
return {
|
|
3535
|
-
config: {
|
|
3536
|
-
...agent,
|
|
3537
|
-
backends,
|
|
3538
|
-
routing
|
|
3539
|
-
},
|
|
3652
|
+
config: { ...agent, backends, routing },
|
|
3540
3653
|
warnings
|
|
3541
3654
|
};
|
|
3542
3655
|
}
|
|
3656
|
+
function buildRemoteBackend(type, agent, context) {
|
|
3657
|
+
if (agent.model === void 0) {
|
|
3658
|
+
throw new Error(`migrateAgentConfig: ${context} requires agent.model`);
|
|
3659
|
+
}
|
|
3660
|
+
const def = { type, model: agent.model };
|
|
3661
|
+
if (agent.apiKey !== void 0) def.apiKey = agent.apiKey;
|
|
3662
|
+
return def;
|
|
3663
|
+
}
|
|
3664
|
+
function buildLocalConnectionBackend(type, agent, context) {
|
|
3665
|
+
if (agent.localEndpoint === void 0 || agent.localModel === void 0) {
|
|
3666
|
+
throw new Error(
|
|
3667
|
+
`migrateAgentConfig: ${context} requires agent.localEndpoint and agent.localModel`
|
|
3668
|
+
);
|
|
3669
|
+
}
|
|
3670
|
+
const def = {
|
|
3671
|
+
type,
|
|
3672
|
+
endpoint: agent.localEndpoint,
|
|
3673
|
+
model: agent.localModel
|
|
3674
|
+
};
|
|
3675
|
+
if (agent.localApiKey !== void 0) def.apiKey = agent.localApiKey;
|
|
3676
|
+
if (agent.localTimeoutMs !== void 0) def.timeoutMs = agent.localTimeoutMs;
|
|
3677
|
+
if (agent.localProbeIntervalMs !== void 0) def.probeIntervalMs = agent.localProbeIntervalMs;
|
|
3678
|
+
return def;
|
|
3679
|
+
}
|
|
3543
3680
|
function synthesizePrimary(agent) {
|
|
3544
3681
|
const backend = agent.backend;
|
|
3545
3682
|
switch (backend) {
|
|
@@ -3550,64 +3687,13 @@ function synthesizePrimary(agent) {
|
|
|
3550
3687
|
if (agent.command !== void 0) def.command = agent.command;
|
|
3551
3688
|
return def;
|
|
3552
3689
|
}
|
|
3553
|
-
case "anthropic":
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
}
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
return
|
|
3560
|
-
}
|
|
3561
|
-
case "openai": {
|
|
3562
|
-
if (agent.model === void 0) {
|
|
3563
|
-
throw new Error("migrateAgentConfig: agent.backend='openai' requires agent.model");
|
|
3564
|
-
}
|
|
3565
|
-
const def = { type: "openai", model: agent.model };
|
|
3566
|
-
if (agent.apiKey !== void 0) def.apiKey = agent.apiKey;
|
|
3567
|
-
return def;
|
|
3568
|
-
}
|
|
3569
|
-
case "gemini": {
|
|
3570
|
-
if (agent.model === void 0) {
|
|
3571
|
-
throw new Error("migrateAgentConfig: agent.backend='gemini' requires agent.model");
|
|
3572
|
-
}
|
|
3573
|
-
const def = { type: "gemini", model: agent.model };
|
|
3574
|
-
if (agent.apiKey !== void 0) def.apiKey = agent.apiKey;
|
|
3575
|
-
return def;
|
|
3576
|
-
}
|
|
3577
|
-
case "local": {
|
|
3578
|
-
if (agent.localEndpoint === void 0 || agent.localModel === void 0) {
|
|
3579
|
-
throw new Error(
|
|
3580
|
-
"migrateAgentConfig: agent.backend='local' requires agent.localEndpoint and agent.localModel"
|
|
3581
|
-
);
|
|
3582
|
-
}
|
|
3583
|
-
const def = {
|
|
3584
|
-
type: "local",
|
|
3585
|
-
endpoint: agent.localEndpoint,
|
|
3586
|
-
model: agent.localModel
|
|
3587
|
-
};
|
|
3588
|
-
if (agent.localApiKey !== void 0) def.apiKey = agent.localApiKey;
|
|
3589
|
-
if (agent.localTimeoutMs !== void 0) def.timeoutMs = agent.localTimeoutMs;
|
|
3590
|
-
if (agent.localProbeIntervalMs !== void 0)
|
|
3591
|
-
def.probeIntervalMs = agent.localProbeIntervalMs;
|
|
3592
|
-
return def;
|
|
3593
|
-
}
|
|
3594
|
-
case "pi": {
|
|
3595
|
-
if (agent.localEndpoint === void 0 || agent.localModel === void 0) {
|
|
3596
|
-
throw new Error(
|
|
3597
|
-
"migrateAgentConfig: agent.backend='pi' requires agent.localEndpoint and agent.localModel"
|
|
3598
|
-
);
|
|
3599
|
-
}
|
|
3600
|
-
const def = {
|
|
3601
|
-
type: "pi",
|
|
3602
|
-
endpoint: agent.localEndpoint,
|
|
3603
|
-
model: agent.localModel
|
|
3604
|
-
};
|
|
3605
|
-
if (agent.localApiKey !== void 0) def.apiKey = agent.localApiKey;
|
|
3606
|
-
if (agent.localTimeoutMs !== void 0) def.timeoutMs = agent.localTimeoutMs;
|
|
3607
|
-
if (agent.localProbeIntervalMs !== void 0)
|
|
3608
|
-
def.probeIntervalMs = agent.localProbeIntervalMs;
|
|
3609
|
-
return def;
|
|
3610
|
-
}
|
|
3690
|
+
case "anthropic":
|
|
3691
|
+
case "openai":
|
|
3692
|
+
case "gemini":
|
|
3693
|
+
return buildRemoteBackend(backend, agent, `agent.backend='${backend}'`);
|
|
3694
|
+
case "local":
|
|
3695
|
+
case "pi":
|
|
3696
|
+
return buildLocalConnectionBackend(backend, agent, `agent.backend='${backend}'`);
|
|
3611
3697
|
default:
|
|
3612
3698
|
throw new Error(
|
|
3613
3699
|
`migrateAgentConfig: unknown legacy backend '${String(backend)}'. Expected one of: mock, claude, anthropic, openai, gemini, local, pi.`
|
|
@@ -3618,31 +3704,8 @@ function synthesizeLocal(agent) {
|
|
|
3618
3704
|
if (agent.localBackend === void 0) {
|
|
3619
3705
|
throw new Error("synthesizeLocal called without agent.localBackend");
|
|
3620
3706
|
}
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
"migrateAgentConfig: agent.localBackend requires agent.localEndpoint and agent.localModel"
|
|
3624
|
-
);
|
|
3625
|
-
}
|
|
3626
|
-
if (agent.localBackend === "pi") {
|
|
3627
|
-
const def2 = {
|
|
3628
|
-
type: "pi",
|
|
3629
|
-
endpoint: agent.localEndpoint,
|
|
3630
|
-
model: agent.localModel
|
|
3631
|
-
};
|
|
3632
|
-
if (agent.localApiKey !== void 0) def2.apiKey = agent.localApiKey;
|
|
3633
|
-
if (agent.localTimeoutMs !== void 0) def2.timeoutMs = agent.localTimeoutMs;
|
|
3634
|
-
if (agent.localProbeIntervalMs !== void 0) def2.probeIntervalMs = agent.localProbeIntervalMs;
|
|
3635
|
-
return def2;
|
|
3636
|
-
}
|
|
3637
|
-
const def = {
|
|
3638
|
-
type: "local",
|
|
3639
|
-
endpoint: agent.localEndpoint,
|
|
3640
|
-
model: agent.localModel
|
|
3641
|
-
};
|
|
3642
|
-
if (agent.localApiKey !== void 0) def.apiKey = agent.localApiKey;
|
|
3643
|
-
if (agent.localTimeoutMs !== void 0) def.timeoutMs = agent.localTimeoutMs;
|
|
3644
|
-
if (agent.localProbeIntervalMs !== void 0) def.probeIntervalMs = agent.localProbeIntervalMs;
|
|
3645
|
-
return def;
|
|
3707
|
+
const type = agent.localBackend === "pi" ? "pi" : "local";
|
|
3708
|
+
return buildLocalConnectionBackend(type, agent, "agent.localBackend");
|
|
3646
3709
|
}
|
|
3647
3710
|
|
|
3648
3711
|
// src/agent/backend-router.ts
|
|
@@ -3695,8 +3758,8 @@ var BackendRouter = class {
|
|
|
3695
3758
|
validateReferences() {
|
|
3696
3759
|
const known = new Set(Object.keys(this.backends));
|
|
3697
3760
|
const missing = [];
|
|
3698
|
-
const check = (
|
|
3699
|
-
if (name !== void 0 && !known.has(name)) missing.push({ path:
|
|
3761
|
+
const check = (path17, name) => {
|
|
3762
|
+
if (name !== void 0 && !known.has(name)) missing.push({ path: path17, name });
|
|
3700
3763
|
};
|
|
3701
3764
|
check("default", this.routing.default);
|
|
3702
3765
|
check("quick-fix", this.routing["quick-fix"]);
|
|
@@ -3706,7 +3769,7 @@ var BackendRouter = class {
|
|
|
3706
3769
|
check("intelligence.sel", this.routing.intelligence?.sel);
|
|
3707
3770
|
check("intelligence.pesl", this.routing.intelligence?.pesl);
|
|
3708
3771
|
if (missing.length > 0) {
|
|
3709
|
-
const detail = missing.map(({ path:
|
|
3772
|
+
const detail = missing.map(({ path: path17, name }) => `routing.${path17} -> '${name}'`).join("; ");
|
|
3710
3773
|
const known_ = [...known].join(", ") || "(none)";
|
|
3711
3774
|
throw new Error(
|
|
3712
3775
|
`BackendRouter: routing references unknown backend(s): ${detail}. Defined backends: [${known_}].`
|
|
@@ -3720,15 +3783,15 @@ import { spawn as spawn2 } from "child_process";
|
|
|
3720
3783
|
import * as readline from "readline";
|
|
3721
3784
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3722
3785
|
import {
|
|
3723
|
-
Ok as
|
|
3724
|
-
Err as
|
|
3786
|
+
Ok as Ok10,
|
|
3787
|
+
Err as Err7
|
|
3725
3788
|
} from "@harness-engineering/types";
|
|
3726
3789
|
function resolveExitCode(code, command, resolve6) {
|
|
3727
3790
|
if (code === 0) {
|
|
3728
|
-
resolve6(
|
|
3791
|
+
resolve6(Ok10(void 0));
|
|
3729
3792
|
} else {
|
|
3730
3793
|
resolve6(
|
|
3731
|
-
|
|
3794
|
+
Err7({
|
|
3732
3795
|
category: "agent_not_found",
|
|
3733
3796
|
message: `Claude command '${command}' not found or failed`
|
|
3734
3797
|
})
|
|
@@ -3736,7 +3799,7 @@ function resolveExitCode(code, command, resolve6) {
|
|
|
3736
3799
|
}
|
|
3737
3800
|
}
|
|
3738
3801
|
function resolveSpawnError(command, resolve6) {
|
|
3739
|
-
resolve6(
|
|
3802
|
+
resolve6(Err7({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
|
|
3740
3803
|
}
|
|
3741
3804
|
var JUST_PAST_GRACE_MS = 5 * 6e4;
|
|
3742
3805
|
var PRIMARY_LIMIT_RE = /You[\u2019']ve hit your limit.*resets\s+(\d{1,2}(?::\d{2})?\s*(?:am|pm))\s*\(([^)]+)\)/i;
|
|
@@ -3935,7 +3998,7 @@ var ClaudeBackend = class {
|
|
|
3935
3998
|
backendName: this.name,
|
|
3936
3999
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3937
4000
|
};
|
|
3938
|
-
return
|
|
4001
|
+
return Ok10(session);
|
|
3939
4002
|
}
|
|
3940
4003
|
async *runTurn(session, params) {
|
|
3941
4004
|
const args = [
|
|
@@ -4059,7 +4122,7 @@ var ClaudeBackend = class {
|
|
|
4059
4122
|
};
|
|
4060
4123
|
}
|
|
4061
4124
|
async stopSession(_session) {
|
|
4062
|
-
return
|
|
4125
|
+
return Ok10(void 0);
|
|
4063
4126
|
}
|
|
4064
4127
|
async healthCheck() {
|
|
4065
4128
|
return new Promise((resolve6) => {
|
|
@@ -4073,8 +4136,8 @@ var ClaudeBackend = class {
|
|
|
4073
4136
|
// src/agent/backends/anthropic.ts
|
|
4074
4137
|
import Anthropic from "@anthropic-ai/sdk";
|
|
4075
4138
|
import {
|
|
4076
|
-
Ok as
|
|
4077
|
-
Err as
|
|
4139
|
+
Ok as Ok11,
|
|
4140
|
+
Err as Err8
|
|
4078
4141
|
} from "@harness-engineering/types";
|
|
4079
4142
|
import { AnthropicCacheAdapter } from "@harness-engineering/core";
|
|
4080
4143
|
var AnthropicBackend = class {
|
|
@@ -4093,7 +4156,7 @@ var AnthropicBackend = class {
|
|
|
4093
4156
|
}
|
|
4094
4157
|
async startSession(params) {
|
|
4095
4158
|
if (!this.config.apiKey) {
|
|
4096
|
-
return
|
|
4159
|
+
return Err8({
|
|
4097
4160
|
category: "agent_not_found",
|
|
4098
4161
|
message: "ANTHROPIC_API_KEY is not set"
|
|
4099
4162
|
});
|
|
@@ -4105,7 +4168,7 @@ var AnthropicBackend = class {
|
|
|
4105
4168
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4106
4169
|
...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
|
|
4107
4170
|
};
|
|
4108
|
-
return
|
|
4171
|
+
return Ok11(session);
|
|
4109
4172
|
}
|
|
4110
4173
|
async *runTurn(session, params) {
|
|
4111
4174
|
const anthropicSession = session;
|
|
@@ -4170,24 +4233,24 @@ var AnthropicBackend = class {
|
|
|
4170
4233
|
}
|
|
4171
4234
|
}
|
|
4172
4235
|
async stopSession(_session) {
|
|
4173
|
-
return
|
|
4236
|
+
return Ok11(void 0);
|
|
4174
4237
|
}
|
|
4175
4238
|
async healthCheck() {
|
|
4176
4239
|
if (!this.config.apiKey) {
|
|
4177
|
-
return
|
|
4240
|
+
return Err8({
|
|
4178
4241
|
category: "response_error",
|
|
4179
4242
|
message: "ANTHROPIC_API_KEY is not set"
|
|
4180
4243
|
});
|
|
4181
4244
|
}
|
|
4182
|
-
return
|
|
4245
|
+
return Ok11(void 0);
|
|
4183
4246
|
}
|
|
4184
4247
|
};
|
|
4185
4248
|
|
|
4186
4249
|
// src/agent/backends/openai.ts
|
|
4187
4250
|
import OpenAI from "openai";
|
|
4188
4251
|
import {
|
|
4189
|
-
Ok as
|
|
4190
|
-
Err as
|
|
4252
|
+
Ok as Ok12,
|
|
4253
|
+
Err as Err9
|
|
4191
4254
|
} from "@harness-engineering/types";
|
|
4192
4255
|
import { OpenAICacheAdapter } from "@harness-engineering/core";
|
|
4193
4256
|
var OpenAIBackend = class {
|
|
@@ -4205,7 +4268,7 @@ var OpenAIBackend = class {
|
|
|
4205
4268
|
}
|
|
4206
4269
|
async startSession(params) {
|
|
4207
4270
|
if (!this.config.apiKey) {
|
|
4208
|
-
return
|
|
4271
|
+
return Err9({
|
|
4209
4272
|
category: "agent_not_found",
|
|
4210
4273
|
message: "OPENAI_API_KEY is not set"
|
|
4211
4274
|
});
|
|
@@ -4217,7 +4280,7 @@ var OpenAIBackend = class {
|
|
|
4217
4280
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4218
4281
|
...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
|
|
4219
4282
|
};
|
|
4220
|
-
return
|
|
4283
|
+
return Ok12(session);
|
|
4221
4284
|
}
|
|
4222
4285
|
async *runTurn(session, params) {
|
|
4223
4286
|
const openAISession = session;
|
|
@@ -4293,14 +4356,14 @@ var OpenAIBackend = class {
|
|
|
4293
4356
|
};
|
|
4294
4357
|
}
|
|
4295
4358
|
async stopSession(_session) {
|
|
4296
|
-
return
|
|
4359
|
+
return Ok12(void 0);
|
|
4297
4360
|
}
|
|
4298
4361
|
async healthCheck() {
|
|
4299
4362
|
try {
|
|
4300
4363
|
await this.client.models.list();
|
|
4301
|
-
return
|
|
4364
|
+
return Ok12(void 0);
|
|
4302
4365
|
} catch (err) {
|
|
4303
|
-
return
|
|
4366
|
+
return Err9({
|
|
4304
4367
|
category: "response_error",
|
|
4305
4368
|
message: err instanceof Error ? err.message : "OpenAI health check failed"
|
|
4306
4369
|
});
|
|
@@ -4311,8 +4374,8 @@ var OpenAIBackend = class {
|
|
|
4311
4374
|
// src/agent/backends/gemini.ts
|
|
4312
4375
|
import { GoogleGenerativeAI } from "@google/generative-ai";
|
|
4313
4376
|
import {
|
|
4314
|
-
Ok as
|
|
4315
|
-
Err as
|
|
4377
|
+
Ok as Ok13,
|
|
4378
|
+
Err as Err10
|
|
4316
4379
|
} from "@harness-engineering/types";
|
|
4317
4380
|
import { GeminiCacheAdapter } from "@harness-engineering/core";
|
|
4318
4381
|
var GeminiBackend = class {
|
|
@@ -4328,7 +4391,7 @@ var GeminiBackend = class {
|
|
|
4328
4391
|
}
|
|
4329
4392
|
async startSession(params) {
|
|
4330
4393
|
if (!this.config.apiKey) {
|
|
4331
|
-
return
|
|
4394
|
+
return Err10({
|
|
4332
4395
|
category: "agent_not_found",
|
|
4333
4396
|
message: "GEMINI_API_KEY is not set"
|
|
4334
4397
|
});
|
|
@@ -4340,7 +4403,7 @@ var GeminiBackend = class {
|
|
|
4340
4403
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4341
4404
|
...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
|
|
4342
4405
|
};
|
|
4343
|
-
return
|
|
4406
|
+
return Ok13(session);
|
|
4344
4407
|
}
|
|
4345
4408
|
async *runTurn(session, params) {
|
|
4346
4409
|
const geminiSession = session;
|
|
@@ -4413,15 +4476,15 @@ var GeminiBackend = class {
|
|
|
4413
4476
|
};
|
|
4414
4477
|
}
|
|
4415
4478
|
async stopSession(_session) {
|
|
4416
|
-
return
|
|
4479
|
+
return Ok13(void 0);
|
|
4417
4480
|
}
|
|
4418
4481
|
async healthCheck() {
|
|
4419
4482
|
try {
|
|
4420
4483
|
const genAI = new GoogleGenerativeAI(this.config.apiKey);
|
|
4421
4484
|
genAI.getGenerativeModel({ model: this.config.model });
|
|
4422
|
-
return
|
|
4485
|
+
return Ok13(void 0);
|
|
4423
4486
|
} catch (err) {
|
|
4424
|
-
return
|
|
4487
|
+
return Err10({
|
|
4425
4488
|
category: "response_error",
|
|
4426
4489
|
message: err instanceof Error ? err.message : "Gemini health check failed"
|
|
4427
4490
|
});
|
|
@@ -4432,8 +4495,8 @@ var GeminiBackend = class {
|
|
|
4432
4495
|
// src/agent/backends/local.ts
|
|
4433
4496
|
import OpenAI2 from "openai";
|
|
4434
4497
|
import {
|
|
4435
|
-
Ok as
|
|
4436
|
-
Err as
|
|
4498
|
+
Ok as Ok14,
|
|
4499
|
+
Err as Err11
|
|
4437
4500
|
} from "@harness-engineering/types";
|
|
4438
4501
|
var DEFAULT_TIMEOUT_MS = 9e4;
|
|
4439
4502
|
var LocalBackend = class {
|
|
@@ -4460,7 +4523,7 @@ var LocalBackend = class {
|
|
|
4460
4523
|
if (this.getModel) {
|
|
4461
4524
|
const candidate = this.getModel();
|
|
4462
4525
|
if (candidate === null) {
|
|
4463
|
-
return
|
|
4526
|
+
return Err11({
|
|
4464
4527
|
category: "agent_not_found",
|
|
4465
4528
|
message: "No local model available; check dashboard for details."
|
|
4466
4529
|
});
|
|
@@ -4477,7 +4540,7 @@ var LocalBackend = class {
|
|
|
4477
4540
|
resolvedModel,
|
|
4478
4541
|
...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
|
|
4479
4542
|
};
|
|
4480
|
-
return
|
|
4543
|
+
return Ok14(session);
|
|
4481
4544
|
}
|
|
4482
4545
|
async *runTurn(session, params) {
|
|
4483
4546
|
const localSession = session;
|
|
@@ -4542,14 +4605,14 @@ var LocalBackend = class {
|
|
|
4542
4605
|
};
|
|
4543
4606
|
}
|
|
4544
4607
|
async stopSession(_session) {
|
|
4545
|
-
return
|
|
4608
|
+
return Ok14(void 0);
|
|
4546
4609
|
}
|
|
4547
4610
|
async healthCheck() {
|
|
4548
4611
|
try {
|
|
4549
4612
|
await this.client.models.list();
|
|
4550
|
-
return
|
|
4613
|
+
return Ok14(void 0);
|
|
4551
4614
|
} catch (err) {
|
|
4552
|
-
return
|
|
4615
|
+
return Err11({
|
|
4553
4616
|
category: "response_error",
|
|
4554
4617
|
message: err instanceof Error ? err.message : "Local backend health check failed"
|
|
4555
4618
|
});
|
|
@@ -4560,8 +4623,8 @@ var LocalBackend = class {
|
|
|
4560
4623
|
// src/agent/backends/pi.ts
|
|
4561
4624
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
4562
4625
|
import {
|
|
4563
|
-
Ok as
|
|
4564
|
-
Err as
|
|
4626
|
+
Ok as Ok15,
|
|
4627
|
+
Err as Err12
|
|
4565
4628
|
} from "@harness-engineering/types";
|
|
4566
4629
|
var SILENT_EVENTS = /* @__PURE__ */ new Set([
|
|
4567
4630
|
"turn_end",
|
|
@@ -4679,7 +4742,7 @@ var PiBackend = class {
|
|
|
4679
4742
|
if (this.config.getModel) {
|
|
4680
4743
|
const candidate = this.config.getModel();
|
|
4681
4744
|
if (candidate === null) {
|
|
4682
|
-
return
|
|
4745
|
+
return Err12({
|
|
4683
4746
|
category: "agent_not_found",
|
|
4684
4747
|
message: "No local model available; check dashboard for details."
|
|
4685
4748
|
});
|
|
@@ -4708,9 +4771,9 @@ var PiBackend = class {
|
|
|
4708
4771
|
piSession,
|
|
4709
4772
|
unsubscribe: null
|
|
4710
4773
|
};
|
|
4711
|
-
return
|
|
4774
|
+
return Ok15(session);
|
|
4712
4775
|
} catch (err) {
|
|
4713
|
-
return
|
|
4776
|
+
return Err12({
|
|
4714
4777
|
category: "response_error",
|
|
4715
4778
|
message: `Failed to create pi session: ${err instanceof Error ? err.message : String(err)}`
|
|
4716
4779
|
});
|
|
@@ -4840,14 +4903,14 @@ var PiBackend = class {
|
|
|
4840
4903
|
await piSession.abort();
|
|
4841
4904
|
} catch {
|
|
4842
4905
|
}
|
|
4843
|
-
return
|
|
4906
|
+
return Ok15(void 0);
|
|
4844
4907
|
}
|
|
4845
4908
|
async healthCheck() {
|
|
4846
4909
|
try {
|
|
4847
4910
|
await import("@mariozechner/pi-coding-agent");
|
|
4848
|
-
return
|
|
4911
|
+
return Ok15(void 0);
|
|
4849
4912
|
} catch (err) {
|
|
4850
|
-
return
|
|
4913
|
+
return Err12({
|
|
4851
4914
|
category: "agent_not_found",
|
|
4852
4915
|
message: `Pi SDK not available: ${err instanceof Error ? err.message : String(err)}`
|
|
4853
4916
|
});
|
|
@@ -4911,7 +4974,7 @@ function createBackend(def) {
|
|
|
4911
4974
|
|
|
4912
4975
|
// src/agent/backends/container.ts
|
|
4913
4976
|
import {
|
|
4914
|
-
Err as
|
|
4977
|
+
Err as Err13
|
|
4915
4978
|
} from "@harness-engineering/types";
|
|
4916
4979
|
function toAgentError(message, details) {
|
|
4917
4980
|
return { category: "response_error", message, details };
|
|
@@ -4943,7 +5006,7 @@ var ContainerBackend = class {
|
|
|
4943
5006
|
}
|
|
4944
5007
|
const result = await this.secretBackend.resolveSecrets(this.secretKeys);
|
|
4945
5008
|
if (!result.ok) {
|
|
4946
|
-
return
|
|
5009
|
+
return Err13(toAgentError(`Secret resolution failed: ${result.error.message}`, result.error));
|
|
4947
5010
|
}
|
|
4948
5011
|
return { ok: true, value: result.value };
|
|
4949
5012
|
}
|
|
@@ -4968,7 +5031,7 @@ var ContainerBackend = class {
|
|
|
4968
5031
|
const createOpts = this.buildCreateOpts(params, envResult.value);
|
|
4969
5032
|
const containerResult = await this.runtime.createContainer(createOpts);
|
|
4970
5033
|
if (!containerResult.ok) {
|
|
4971
|
-
return
|
|
5034
|
+
return Err13(
|
|
4972
5035
|
toAgentError(
|
|
4973
5036
|
`Container creation failed: ${containerResult.error.message}`,
|
|
4974
5037
|
containerResult.error
|
|
@@ -4993,7 +5056,7 @@ var ContainerBackend = class {
|
|
|
4993
5056
|
this.containerHandles.delete(session.sessionId);
|
|
4994
5057
|
const removeResult = await this.runtime.removeContainer(handle);
|
|
4995
5058
|
if (!removeResult.ok) {
|
|
4996
|
-
return
|
|
5059
|
+
return Err13(
|
|
4997
5060
|
toAgentError(
|
|
4998
5061
|
`Container removal failed: ${removeResult.error.message}`,
|
|
4999
5062
|
removeResult.error
|
|
@@ -5006,7 +5069,7 @@ var ContainerBackend = class {
|
|
|
5006
5069
|
async healthCheck() {
|
|
5007
5070
|
const runtimeResult = await this.runtime.healthCheck();
|
|
5008
5071
|
if (!runtimeResult.ok) {
|
|
5009
|
-
return
|
|
5072
|
+
return Err13({
|
|
5010
5073
|
category: "agent_not_found",
|
|
5011
5074
|
message: `Container runtime unhealthy: ${runtimeResult.error.message}`,
|
|
5012
5075
|
details: runtimeResult.error
|
|
@@ -5018,7 +5081,7 @@ var ContainerBackend = class {
|
|
|
5018
5081
|
|
|
5019
5082
|
// src/agent/runtime/docker.ts
|
|
5020
5083
|
import { execFile as execFile3, spawn as spawn3 } from "child_process";
|
|
5021
|
-
import { Ok as
|
|
5084
|
+
import { Ok as Ok16, Err as Err14 } from "@harness-engineering/types";
|
|
5022
5085
|
function dockerExec(args) {
|
|
5023
5086
|
return new Promise((resolve6, reject) => {
|
|
5024
5087
|
execFile3("docker", args, (error, stdout) => {
|
|
@@ -5051,9 +5114,9 @@ var DockerRuntime = class {
|
|
|
5051
5114
|
args.push(opts.image);
|
|
5052
5115
|
args.push("sleep", "infinity");
|
|
5053
5116
|
const containerId = await dockerExec(args);
|
|
5054
|
-
return
|
|
5117
|
+
return Ok16({ containerId, runtime: this.name });
|
|
5055
5118
|
} catch (error) {
|
|
5056
|
-
return
|
|
5119
|
+
return Err14({
|
|
5057
5120
|
category: "container_create_failed",
|
|
5058
5121
|
message: `Failed to create container: ${error instanceof Error ? error.message : String(error)}`,
|
|
5059
5122
|
details: error
|
|
@@ -5097,9 +5160,9 @@ var DockerRuntime = class {
|
|
|
5097
5160
|
async removeContainer(handle) {
|
|
5098
5161
|
try {
|
|
5099
5162
|
await dockerExec(["rm", "-f", handle.containerId]);
|
|
5100
|
-
return
|
|
5163
|
+
return Ok16(void 0);
|
|
5101
5164
|
} catch (error) {
|
|
5102
|
-
return
|
|
5165
|
+
return Err14({
|
|
5103
5166
|
category: "container_remove_failed",
|
|
5104
5167
|
message: `Failed to remove container: ${error instanceof Error ? error.message : String(error)}`,
|
|
5105
5168
|
details: error
|
|
@@ -5109,9 +5172,9 @@ var DockerRuntime = class {
|
|
|
5109
5172
|
async healthCheck() {
|
|
5110
5173
|
try {
|
|
5111
5174
|
await dockerExec(["info", "--format", "{{.ServerVersion}}"]);
|
|
5112
|
-
return
|
|
5175
|
+
return Ok16(void 0);
|
|
5113
5176
|
} catch (error) {
|
|
5114
|
-
return
|
|
5177
|
+
return Err14({
|
|
5115
5178
|
category: "runtime_not_found",
|
|
5116
5179
|
message: `Docker is not available: ${error instanceof Error ? error.message : String(error)}`,
|
|
5117
5180
|
details: error
|
|
@@ -5121,7 +5184,7 @@ var DockerRuntime = class {
|
|
|
5121
5184
|
};
|
|
5122
5185
|
|
|
5123
5186
|
// src/agent/secrets/env.ts
|
|
5124
|
-
import { Ok as
|
|
5187
|
+
import { Ok as Ok17, Err as Err15 } from "@harness-engineering/types";
|
|
5125
5188
|
var EnvSecretBackend = class {
|
|
5126
5189
|
name = "env";
|
|
5127
5190
|
async resolveSecrets(keys) {
|
|
@@ -5129,7 +5192,7 @@ var EnvSecretBackend = class {
|
|
|
5129
5192
|
for (const key of keys) {
|
|
5130
5193
|
const value = process.env[key];
|
|
5131
5194
|
if (value === void 0) {
|
|
5132
|
-
return
|
|
5195
|
+
return Err15({
|
|
5133
5196
|
category: "secret_not_found",
|
|
5134
5197
|
message: `Environment variable '${key}' is not set`,
|
|
5135
5198
|
key
|
|
@@ -5137,16 +5200,16 @@ var EnvSecretBackend = class {
|
|
|
5137
5200
|
}
|
|
5138
5201
|
secrets[key] = value;
|
|
5139
5202
|
}
|
|
5140
|
-
return
|
|
5203
|
+
return Ok17(secrets);
|
|
5141
5204
|
}
|
|
5142
5205
|
async healthCheck() {
|
|
5143
|
-
return
|
|
5206
|
+
return Ok17(void 0);
|
|
5144
5207
|
}
|
|
5145
5208
|
};
|
|
5146
5209
|
|
|
5147
5210
|
// src/agent/secrets/onepassword.ts
|
|
5148
5211
|
import { execFile as execFile4 } from "child_process";
|
|
5149
|
-
import { Ok as
|
|
5212
|
+
import { Ok as Ok18, Err as Err16 } from "@harness-engineering/types";
|
|
5150
5213
|
function opExec(args) {
|
|
5151
5214
|
return new Promise((resolve6, reject) => {
|
|
5152
5215
|
execFile4("op", args, (error, stdout) => {
|
|
@@ -5171,21 +5234,21 @@ var OnePasswordSecretBackend = class {
|
|
|
5171
5234
|
const value = await opExec(["read", `op://${this.vault}/${key}/password`]);
|
|
5172
5235
|
secrets[key] = value;
|
|
5173
5236
|
} catch (error) {
|
|
5174
|
-
return
|
|
5237
|
+
return Err16({
|
|
5175
5238
|
category: "access_denied",
|
|
5176
5239
|
message: `Failed to read secret '${key}' from 1Password: ${error instanceof Error ? error.message : String(error)}`,
|
|
5177
5240
|
key
|
|
5178
5241
|
});
|
|
5179
5242
|
}
|
|
5180
5243
|
}
|
|
5181
|
-
return
|
|
5244
|
+
return Ok18(secrets);
|
|
5182
5245
|
}
|
|
5183
5246
|
async healthCheck() {
|
|
5184
5247
|
try {
|
|
5185
5248
|
await opExec(["--version"]);
|
|
5186
|
-
return
|
|
5249
|
+
return Ok18(void 0);
|
|
5187
5250
|
} catch (error) {
|
|
5188
|
-
return
|
|
5251
|
+
return Err16({
|
|
5189
5252
|
category: "provider_unavailable",
|
|
5190
5253
|
message: `1Password CLI is not available: ${error instanceof Error ? error.message : String(error)}`
|
|
5191
5254
|
});
|
|
@@ -5195,7 +5258,7 @@ var OnePasswordSecretBackend = class {
|
|
|
5195
5258
|
|
|
5196
5259
|
// src/agent/secrets/vault.ts
|
|
5197
5260
|
import { execFile as execFile5 } from "child_process";
|
|
5198
|
-
import { Ok as
|
|
5261
|
+
import { Ok as Ok19, Err as Err17 } from "@harness-engineering/types";
|
|
5199
5262
|
function vaultExec(args, env) {
|
|
5200
5263
|
return new Promise((resolve6, reject) => {
|
|
5201
5264
|
execFile5("vault", args, { env: { ...process.env, ...env } }, (error, stdout) => {
|
|
@@ -5226,11 +5289,11 @@ var VaultSecretBackend = class {
|
|
|
5226
5289
|
} catch (error) {
|
|
5227
5290
|
const msg = error instanceof Error ? error.message : String(error);
|
|
5228
5291
|
const category = error instanceof SyntaxError ? "access_denied" : "access_denied";
|
|
5229
|
-
return
|
|
5292
|
+
return Err17({ category, message: `Failed to read from Vault: ${msg}` });
|
|
5230
5293
|
}
|
|
5231
5294
|
const missing = keys.find((k) => !(k in data));
|
|
5232
5295
|
if (missing) {
|
|
5233
|
-
return
|
|
5296
|
+
return Err17({
|
|
5234
5297
|
category: "secret_not_found",
|
|
5235
5298
|
message: `Secret key '${missing}' not found in Vault path '${this.path}'`,
|
|
5236
5299
|
key: missing
|
|
@@ -5238,14 +5301,14 @@ var VaultSecretBackend = class {
|
|
|
5238
5301
|
}
|
|
5239
5302
|
const secrets = {};
|
|
5240
5303
|
for (const key of keys) secrets[key] = data[key];
|
|
5241
|
-
return
|
|
5304
|
+
return Ok19(secrets);
|
|
5242
5305
|
}
|
|
5243
5306
|
async healthCheck() {
|
|
5244
5307
|
try {
|
|
5245
5308
|
await vaultExec(["version"]);
|
|
5246
|
-
return
|
|
5309
|
+
return Ok19(void 0);
|
|
5247
5310
|
} catch (error) {
|
|
5248
|
-
return
|
|
5311
|
+
return Err17({
|
|
5249
5312
|
category: "provider_unavailable",
|
|
5250
5313
|
message: `Vault CLI is not available: ${error instanceof Error ? error.message : String(error)}`
|
|
5251
5314
|
});
|
|
@@ -5360,6 +5423,15 @@ var OrchestratorBackendFactory = class {
|
|
|
5360
5423
|
}
|
|
5361
5424
|
};
|
|
5362
5425
|
|
|
5426
|
+
// src/agent/intelligence-factory.ts
|
|
5427
|
+
import {
|
|
5428
|
+
IntelligencePipeline,
|
|
5429
|
+
AnthropicAnalysisProvider as AnthropicAnalysisProvider2,
|
|
5430
|
+
OpenAICompatibleAnalysisProvider as OpenAICompatibleAnalysisProvider2,
|
|
5431
|
+
ClaudeCliAnalysisProvider as ClaudeCliAnalysisProvider2
|
|
5432
|
+
} from "@harness-engineering/intelligence";
|
|
5433
|
+
import { GraphStore } from "@harness-engineering/graph";
|
|
5434
|
+
|
|
5363
5435
|
// src/agent/analysis-provider-factory.ts
|
|
5364
5436
|
import {
|
|
5365
5437
|
AnthropicAnalysisProvider,
|
|
@@ -5461,9 +5533,109 @@ function buildClaudeCliProvider(def, args, layerModel) {
|
|
|
5461
5533
|
});
|
|
5462
5534
|
}
|
|
5463
5535
|
|
|
5536
|
+
// src/agent/intelligence-factory.ts
|
|
5537
|
+
function buildIntelligencePipeline(deps) {
|
|
5538
|
+
const { config } = deps;
|
|
5539
|
+
const intel = config.intelligence;
|
|
5540
|
+
if (!intel?.enabled) return null;
|
|
5541
|
+
const selProvider = buildAnalysisProviderForLayer("sel", deps);
|
|
5542
|
+
if (!selProvider) return null;
|
|
5543
|
+
const routing = config.agent.routing;
|
|
5544
|
+
const peslName = routing?.intelligence?.pesl;
|
|
5545
|
+
const selName = routing?.intelligence?.sel ?? routing?.default;
|
|
5546
|
+
const peslProvider = peslName !== void 0 && peslName !== selName ? buildAnalysisProviderForLayer("pesl", deps) : null;
|
|
5547
|
+
const peslModel = intel.models?.pesl ?? config.agent.model;
|
|
5548
|
+
const graphStore = new GraphStore();
|
|
5549
|
+
const pipeline = new IntelligencePipeline(selProvider, graphStore, {
|
|
5550
|
+
...peslModel !== void 0 && { peslModel },
|
|
5551
|
+
...peslProvider !== null && peslProvider !== void 0 && { peslProvider }
|
|
5552
|
+
});
|
|
5553
|
+
return { pipeline, graphStore };
|
|
5554
|
+
}
|
|
5555
|
+
function buildAnalysisProviderForLayer(layer, deps) {
|
|
5556
|
+
const { config, localResolvers, logger } = deps;
|
|
5557
|
+
const intel = config.intelligence;
|
|
5558
|
+
if (!intel?.enabled) return null;
|
|
5559
|
+
if (intel.provider) {
|
|
5560
|
+
const layerModel = layer === "sel" ? intel.models?.sel : intel.models?.pesl;
|
|
5561
|
+
return buildExplicitProvider(intel.provider, layerModel ?? config.agent.model, config);
|
|
5562
|
+
}
|
|
5563
|
+
const routed = resolveRoutedBackend(layer, config, logger);
|
|
5564
|
+
if (!routed) return null;
|
|
5565
|
+
const { name, def } = routed;
|
|
5566
|
+
const resolver = localResolvers.get(name);
|
|
5567
|
+
return buildAnalysisProvider({
|
|
5568
|
+
def,
|
|
5569
|
+
backendName: name,
|
|
5570
|
+
layer,
|
|
5571
|
+
// Spec 2 P3-IMP-1: a single snapshot read feeds the factory's
|
|
5572
|
+
// unavailable-warn diagnostic (Configured/Detected lists) and
|
|
5573
|
+
// collapses the two `getStatus()` calls flagged by P3-SUG-2.
|
|
5574
|
+
getResolverStatusSnapshot: () => {
|
|
5575
|
+
if (!resolver) return null;
|
|
5576
|
+
const status = resolver.getStatus();
|
|
5577
|
+
return {
|
|
5578
|
+
available: status.available,
|
|
5579
|
+
resolved: status.resolved,
|
|
5580
|
+
configured: status.configured,
|
|
5581
|
+
detected: status.detected
|
|
5582
|
+
};
|
|
5583
|
+
},
|
|
5584
|
+
intelligence: intel,
|
|
5585
|
+
logger
|
|
5586
|
+
});
|
|
5587
|
+
}
|
|
5588
|
+
function resolveRoutedBackend(layer, config, logger) {
|
|
5589
|
+
const routing = config.agent.routing;
|
|
5590
|
+
const backends = config.agent.backends;
|
|
5591
|
+
if (!routing || !backends) return null;
|
|
5592
|
+
const layerName = routing.intelligence?.[layer];
|
|
5593
|
+
const name = layerName ?? routing.default;
|
|
5594
|
+
const def = backends[name];
|
|
5595
|
+
if (!def) {
|
|
5596
|
+
logger.warn(
|
|
5597
|
+
`Intelligence pipeline: routed backend '${name}' for layer '${layer}' is not in agent.backends.`
|
|
5598
|
+
);
|
|
5599
|
+
return null;
|
|
5600
|
+
}
|
|
5601
|
+
return { name, def };
|
|
5602
|
+
}
|
|
5603
|
+
function buildExplicitProvider(provider, selModel, config) {
|
|
5604
|
+
if (provider.kind === "anthropic") {
|
|
5605
|
+
const apiKey2 = provider.apiKey ?? config.agent.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
5606
|
+
if (!apiKey2) {
|
|
5607
|
+
throw new Error("Intelligence pipeline: no Anthropic API key found.");
|
|
5608
|
+
}
|
|
5609
|
+
return new AnthropicAnalysisProvider2({
|
|
5610
|
+
apiKey: apiKey2,
|
|
5611
|
+
...selModel !== void 0 && { defaultModel: selModel }
|
|
5612
|
+
});
|
|
5613
|
+
}
|
|
5614
|
+
if (provider.kind === "claude-cli") {
|
|
5615
|
+
return new ClaudeCliAnalysisProvider2({
|
|
5616
|
+
command: config.agent.command,
|
|
5617
|
+
...selModel !== void 0 && { defaultModel: selModel },
|
|
5618
|
+
...config.intelligence?.requestTimeoutMs !== void 0 && {
|
|
5619
|
+
timeoutMs: config.intelligence.requestTimeoutMs
|
|
5620
|
+
}
|
|
5621
|
+
});
|
|
5622
|
+
}
|
|
5623
|
+
const apiKey = provider.apiKey ?? config.agent.apiKey ?? "ollama";
|
|
5624
|
+
const baseUrl = provider.baseUrl ?? "http://localhost:11434/v1";
|
|
5625
|
+
const intel = config.intelligence;
|
|
5626
|
+
return new OpenAICompatibleAnalysisProvider2({
|
|
5627
|
+
apiKey,
|
|
5628
|
+
baseUrl,
|
|
5629
|
+
...selModel !== void 0 && { defaultModel: selModel },
|
|
5630
|
+
...intel?.requestTimeoutMs !== void 0 && { timeoutMs: intel.requestTimeoutMs },
|
|
5631
|
+
...intel?.promptSuffix !== void 0 && { promptSuffix: intel.promptSuffix },
|
|
5632
|
+
...intel?.jsonMode !== void 0 && { jsonMode: intel.jsonMode }
|
|
5633
|
+
});
|
|
5634
|
+
}
|
|
5635
|
+
|
|
5464
5636
|
// src/server/http.ts
|
|
5465
5637
|
import * as http from "http";
|
|
5466
|
-
import * as
|
|
5638
|
+
import * as path14 from "path";
|
|
5467
5639
|
|
|
5468
5640
|
// src/server/websocket.ts
|
|
5469
5641
|
import { WebSocketServer, WebSocket } from "ws";
|
|
@@ -6003,7 +6175,16 @@ function handleAnalyzeRoute(req, res, pipeline) {
|
|
|
6003
6175
|
|
|
6004
6176
|
// src/server/routes/roadmap-actions.ts
|
|
6005
6177
|
import * as fs10 from "fs/promises";
|
|
6006
|
-
import
|
|
6178
|
+
import * as path10 from "path";
|
|
6179
|
+
import {
|
|
6180
|
+
parseRoadmap as parseRoadmap2,
|
|
6181
|
+
serializeRoadmap as serializeRoadmap2,
|
|
6182
|
+
loadProjectRoadmapMode,
|
|
6183
|
+
loadTrackerClientConfigFromProject,
|
|
6184
|
+
createTrackerClient,
|
|
6185
|
+
ConflictError,
|
|
6186
|
+
makeTrackerConflictBody
|
|
6187
|
+
} from "@harness-engineering/core";
|
|
6007
6188
|
import { z as z7 } from "zod";
|
|
6008
6189
|
var AppendRoadmapRequestSchema = z7.object({
|
|
6009
6190
|
title: z7.string().min(1),
|
|
@@ -6030,6 +6211,48 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
6030
6211
|
sendJSON(res, 503, { error: "Roadmap path not configured" });
|
|
6031
6212
|
return;
|
|
6032
6213
|
}
|
|
6214
|
+
const projectRoot = path10.dirname(path10.dirname(roadmapPath));
|
|
6215
|
+
const mode = loadProjectRoadmapMode(projectRoot);
|
|
6216
|
+
if (mode === "file-less") {
|
|
6217
|
+
const trackerCfg = loadTrackerClientConfigFromProject(projectRoot);
|
|
6218
|
+
if (!trackerCfg.ok) {
|
|
6219
|
+
sendJSON(res, 500, { error: trackerCfg.error.message });
|
|
6220
|
+
return;
|
|
6221
|
+
}
|
|
6222
|
+
const clientR = createTrackerClient(trackerCfg.value);
|
|
6223
|
+
if (!clientR.ok) {
|
|
6224
|
+
sendJSON(res, 500, { error: clientR.error.message });
|
|
6225
|
+
return;
|
|
6226
|
+
}
|
|
6227
|
+
const body2 = await readBody(req);
|
|
6228
|
+
const parseResult = AppendRoadmapRequestSchema.safeParse(JSON.parse(body2));
|
|
6229
|
+
if (!parseResult.success) {
|
|
6230
|
+
sendJSON(res, 400, {
|
|
6231
|
+
error: parseResult.error.issues[0]?.message ?? "Invalid request body"
|
|
6232
|
+
});
|
|
6233
|
+
return;
|
|
6234
|
+
}
|
|
6235
|
+
const newFeature = {
|
|
6236
|
+
name: parseResult.data.title,
|
|
6237
|
+
summary: parseResult.data.enrichedSpec?.intent ?? parseResult.data.summary ?? parseResult.data.title,
|
|
6238
|
+
status: "planned"
|
|
6239
|
+
};
|
|
6240
|
+
const r = await clientR.value.create(newFeature);
|
|
6241
|
+
if (!r.ok) {
|
|
6242
|
+
if (r.error instanceof ConflictError) {
|
|
6243
|
+
sendJSON(res, 409, makeTrackerConflictBody(r.error));
|
|
6244
|
+
return;
|
|
6245
|
+
}
|
|
6246
|
+
sendJSON(res, 502, { error: r.error.message });
|
|
6247
|
+
return;
|
|
6248
|
+
}
|
|
6249
|
+
sendJSON(res, 201, {
|
|
6250
|
+
ok: true,
|
|
6251
|
+
featureName: r.value.name,
|
|
6252
|
+
externalId: r.value.externalId
|
|
6253
|
+
});
|
|
6254
|
+
return;
|
|
6255
|
+
}
|
|
6033
6256
|
const body = await readBody(req);
|
|
6034
6257
|
const result = AppendRoadmapRequestSchema.safeParse(JSON.parse(body));
|
|
6035
6258
|
if (!result.success) {
|
|
@@ -6282,21 +6505,21 @@ function handleMaintenanceRoute(req, res, deps) {
|
|
|
6282
6505
|
|
|
6283
6506
|
// src/server/routes/sessions.ts
|
|
6284
6507
|
import * as fs11 from "fs/promises";
|
|
6285
|
-
import * as
|
|
6508
|
+
import * as path11 from "path";
|
|
6286
6509
|
import { z as z10 } from "zod";
|
|
6287
6510
|
var SessionCreateSchema = z10.object({
|
|
6288
6511
|
sessionId: z10.string().min(1)
|
|
6289
6512
|
}).passthrough();
|
|
6290
6513
|
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6291
6514
|
function isSafeId(id) {
|
|
6292
|
-
return UUID_RE2.test(id) ||
|
|
6515
|
+
return UUID_RE2.test(id) || path11.basename(id) === id && !id.includes("..");
|
|
6293
6516
|
}
|
|
6294
6517
|
function jsonResponse(res, status, data) {
|
|
6295
6518
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
6296
6519
|
res.end(JSON.stringify(data));
|
|
6297
6520
|
}
|
|
6298
6521
|
function extractSessionId(url) {
|
|
6299
|
-
const segments = new URL(url, "http://localhost").pathname.split(
|
|
6522
|
+
const segments = new URL(url, "http://localhost").pathname.split(path11.posix.sep);
|
|
6300
6523
|
const id = segments.pop();
|
|
6301
6524
|
return id && id !== "sessions" ? id : null;
|
|
6302
6525
|
}
|
|
@@ -6308,7 +6531,7 @@ async function handleList(res, sessionsDir) {
|
|
|
6308
6531
|
if (!entry.isDirectory()) continue;
|
|
6309
6532
|
try {
|
|
6310
6533
|
const content = await fs11.readFile(
|
|
6311
|
-
|
|
6534
|
+
path11.join(sessionsDir, entry.name, "session.json"),
|
|
6312
6535
|
"utf-8"
|
|
6313
6536
|
);
|
|
6314
6537
|
sessions.push(JSON.parse(content));
|
|
@@ -6333,7 +6556,7 @@ async function handleGet(res, id, sessionsDir) {
|
|
|
6333
6556
|
return;
|
|
6334
6557
|
}
|
|
6335
6558
|
try {
|
|
6336
|
-
const content = await fs11.readFile(
|
|
6559
|
+
const content = await fs11.readFile(path11.join(sessionsDir, id, "session.json"), "utf-8");
|
|
6337
6560
|
jsonResponse(res, 200, JSON.parse(content));
|
|
6338
6561
|
} catch (err) {
|
|
6339
6562
|
if (err.code === "ENOENT") {
|
|
@@ -6356,9 +6579,9 @@ async function handleCreate(req, res, sessionsDir) {
|
|
|
6356
6579
|
jsonResponse(res, 400, { error: "Invalid sessionId" });
|
|
6357
6580
|
return;
|
|
6358
6581
|
}
|
|
6359
|
-
const sessionDir =
|
|
6582
|
+
const sessionDir = path11.join(sessionsDir, session.sessionId);
|
|
6360
6583
|
await fs11.mkdir(sessionDir, { recursive: true });
|
|
6361
|
-
await fs11.writeFile(
|
|
6584
|
+
await fs11.writeFile(path11.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
|
|
6362
6585
|
jsonResponse(res, 200, { ok: true });
|
|
6363
6586
|
} catch {
|
|
6364
6587
|
jsonResponse(res, 500, { error: "Failed to save session" });
|
|
@@ -6373,7 +6596,7 @@ async function handleUpdate(req, res, url, sessionsDir) {
|
|
|
6373
6596
|
}
|
|
6374
6597
|
const body = await readBody(req);
|
|
6375
6598
|
const updates = z10.record(z10.unknown()).parse(JSON.parse(body));
|
|
6376
|
-
const sessionFilePath =
|
|
6599
|
+
const sessionFilePath = path11.join(sessionsDir, id, "session.json");
|
|
6377
6600
|
const current = JSON.parse(await fs11.readFile(sessionFilePath, "utf-8"));
|
|
6378
6601
|
await fs11.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
|
|
6379
6602
|
jsonResponse(res, 200, { ok: true });
|
|
@@ -6388,7 +6611,7 @@ async function handleDelete(res, url, sessionsDir) {
|
|
|
6388
6611
|
jsonResponse(res, 400, { error: "Missing or invalid sessionId" });
|
|
6389
6612
|
return;
|
|
6390
6613
|
}
|
|
6391
|
-
await fs11.rm(
|
|
6614
|
+
await fs11.rm(path11.join(sessionsDir, id), { recursive: true, force: true });
|
|
6392
6615
|
jsonResponse(res, 200, { ok: true });
|
|
6393
6616
|
} catch {
|
|
6394
6617
|
jsonResponse(res, 500, { error: "Failed to delete session" });
|
|
@@ -6529,7 +6752,7 @@ function handleLocalModelsRoute(req, res, getStatuses) {
|
|
|
6529
6752
|
|
|
6530
6753
|
// src/server/static.ts
|
|
6531
6754
|
import * as fs12 from "fs";
|
|
6532
|
-
import * as
|
|
6755
|
+
import * as path12 from "path";
|
|
6533
6756
|
var MIME_TYPES = {
|
|
6534
6757
|
".html": "text/html; charset=utf-8",
|
|
6535
6758
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -6549,26 +6772,26 @@ var MIME_TYPES = {
|
|
|
6549
6772
|
function handleStaticFile(req, res, dashboardDir) {
|
|
6550
6773
|
const { method, url } = req;
|
|
6551
6774
|
if (method !== "GET") return false;
|
|
6552
|
-
const apiPrefix =
|
|
6553
|
-
const wsPath =
|
|
6775
|
+
const apiPrefix = path12.posix.join(path12.posix.sep, "api", path12.posix.sep);
|
|
6776
|
+
const wsPath = path12.posix.join(path12.posix.sep, "ws");
|
|
6554
6777
|
if (url?.startsWith(apiPrefix) || url === wsPath) return false;
|
|
6555
6778
|
const urlPath = new URL(url ?? "/", "http://localhost").pathname;
|
|
6556
|
-
const requestedPath =
|
|
6557
|
-
const resolved =
|
|
6558
|
-
if (!resolved.startsWith(
|
|
6559
|
-
return serveFile(
|
|
6779
|
+
const requestedPath = path12.join(dashboardDir, urlPath === "/" ? "index.html" : urlPath);
|
|
6780
|
+
const resolved = path12.resolve(requestedPath);
|
|
6781
|
+
if (!resolved.startsWith(path12.resolve(dashboardDir))) {
|
|
6782
|
+
return serveFile(path12.join(dashboardDir, "index.html"), res);
|
|
6560
6783
|
}
|
|
6561
6784
|
if (fs12.existsSync(resolved) && fs12.statSync(resolved).isFile()) {
|
|
6562
6785
|
return serveFile(resolved, res);
|
|
6563
6786
|
}
|
|
6564
|
-
const indexPath =
|
|
6787
|
+
const indexPath = path12.join(dashboardDir, "index.html");
|
|
6565
6788
|
if (fs12.existsSync(indexPath)) {
|
|
6566
6789
|
return serveFile(indexPath, res);
|
|
6567
6790
|
}
|
|
6568
6791
|
return false;
|
|
6569
6792
|
}
|
|
6570
6793
|
function serveFile(filePath, res) {
|
|
6571
|
-
const ext =
|
|
6794
|
+
const ext = path12.extname(filePath).toLowerCase();
|
|
6572
6795
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
6573
6796
|
try {
|
|
6574
6797
|
const content = fs12.readFileSync(filePath);
|
|
@@ -6582,7 +6805,7 @@ function serveFile(filePath, res) {
|
|
|
6582
6805
|
|
|
6583
6806
|
// src/server/plan-watcher.ts
|
|
6584
6807
|
import * as fs13 from "fs";
|
|
6585
|
-
import * as
|
|
6808
|
+
import * as path13 from "path";
|
|
6586
6809
|
var PlanWatcher = class {
|
|
6587
6810
|
plansDir;
|
|
6588
6811
|
queue;
|
|
@@ -6599,7 +6822,7 @@ var PlanWatcher = class {
|
|
|
6599
6822
|
fs13.mkdirSync(this.plansDir, { recursive: true });
|
|
6600
6823
|
this.watcher = fs13.watch(this.plansDir, (eventType, filename) => {
|
|
6601
6824
|
if (eventType === "rename" && filename && filename.endsWith(".md")) {
|
|
6602
|
-
const filePath =
|
|
6825
|
+
const filePath = path13.join(this.plansDir, filename);
|
|
6603
6826
|
if (fs13.existsSync(filePath)) {
|
|
6604
6827
|
void this.handleNewPlan(filename);
|
|
6605
6828
|
}
|
|
@@ -6686,6 +6909,7 @@ var OrchestratorServer = class {
|
|
|
6686
6909
|
planWatcher = null;
|
|
6687
6910
|
stateChangeListener;
|
|
6688
6911
|
agentEventListener;
|
|
6912
|
+
apiRoutes;
|
|
6689
6913
|
constructor(orchestrator, port, deps) {
|
|
6690
6914
|
this.orchestrator = orchestrator;
|
|
6691
6915
|
this.port = port;
|
|
@@ -6695,18 +6919,19 @@ var OrchestratorServer = class {
|
|
|
6695
6919
|
this.httpServer,
|
|
6696
6920
|
() => this.orchestrator.getSnapshot()
|
|
6697
6921
|
);
|
|
6922
|
+
this.apiRoutes = this.buildApiRoutes();
|
|
6698
6923
|
this.wireEvents();
|
|
6699
6924
|
}
|
|
6700
6925
|
initDependencies(deps) {
|
|
6701
6926
|
this.interactionQueue = deps?.interactionQueue;
|
|
6702
|
-
this.plansDir = deps?.plansDir ??
|
|
6703
|
-
this.dashboardDir = deps?.dashboardDir ??
|
|
6927
|
+
this.plansDir = deps?.plansDir ?? path14.resolve("docs", "plans");
|
|
6928
|
+
this.dashboardDir = deps?.dashboardDir ?? path14.resolve("packages", "dashboard", "dist", "client");
|
|
6704
6929
|
this.claudeCommand = deps?.claudeCommand ?? "claude";
|
|
6705
6930
|
this.pipeline = deps?.pipeline ?? null;
|
|
6706
6931
|
this.analysisArchive = deps?.analysisArchive;
|
|
6707
6932
|
this.roadmapPath = deps?.roadmapPath ?? null;
|
|
6708
6933
|
this.dispatchAdHoc = deps?.dispatchAdHoc ?? null;
|
|
6709
|
-
this.sessionsDir = deps?.sessionsDir ??
|
|
6934
|
+
this.sessionsDir = deps?.sessionsDir ?? path14.resolve(".harness", "sessions");
|
|
6710
6935
|
this.maintenanceDeps = deps?.maintenanceDeps ?? null;
|
|
6711
6936
|
this.getLocalModelStatus = deps?.getLocalModelStatus ?? null;
|
|
6712
6937
|
this.getLocalModelStatuses = deps?.getLocalModelStatuses ?? null;
|
|
@@ -6730,8 +6955,8 @@ var OrchestratorServer = class {
|
|
|
6730
6955
|
}
|
|
6731
6956
|
/**
|
|
6732
6957
|
* Broadcast a maintenance event to all WebSocket clients.
|
|
6733
|
-
* @param type - One of 'maintenance:started', 'maintenance:completed', 'maintenance:error'
|
|
6734
|
-
* @param data - Event payload (task info, run result, or
|
|
6958
|
+
* @param type - One of 'maintenance:started', 'maintenance:completed', 'maintenance:error', 'maintenance:baseref_fallback'
|
|
6959
|
+
* @param data - Event payload (task info, run result, error details, or baseref-fallback diagnostic)
|
|
6735
6960
|
*/
|
|
6736
6961
|
broadcastMaintenance(type, data) {
|
|
6737
6962
|
this.broadcaster.broadcast(type, data);
|
|
@@ -6816,44 +7041,37 @@ var OrchestratorServer = class {
|
|
|
6816
7041
|
);
|
|
6817
7042
|
return false;
|
|
6818
7043
|
}
|
|
7044
|
+
/**
|
|
7045
|
+
* Build the ordered API route table. Each entry is invoked in order and
|
|
7046
|
+
* returns true when it has handled the request. Closures capture `this`,
|
|
7047
|
+
* so handlers re-read mutable deps (pipeline, recorder, maintenanceDeps)
|
|
7048
|
+
* on every request — setters like setPipeline() take effect immediately.
|
|
7049
|
+
*
|
|
7050
|
+
* Adding a new route is a one-place change: append an entry here.
|
|
7051
|
+
*/
|
|
7052
|
+
buildApiRoutes() {
|
|
7053
|
+
return [
|
|
7054
|
+
(req, res) => !!this.interactionQueue && handleInteractionsRoute(req, res, this.interactionQueue),
|
|
7055
|
+
(req, res) => handlePlansRoute(req, res, this.plansDir),
|
|
7056
|
+
(req, res) => handleAnalyzeRoute(req, res, this.pipeline),
|
|
7057
|
+
(req, res) => handleAnalysesRoute(req, res, this.analysisArchive),
|
|
7058
|
+
(req, res) => handleRoadmapActionsRoute(req, res, this.roadmapPath),
|
|
7059
|
+
(req, res) => handleDispatchActionsRoute(req, res, this.dispatchAdHoc),
|
|
7060
|
+
(req, res) => handleLocalModelRoute(req, res, this.getLocalModelStatus),
|
|
7061
|
+
// Local-models multi-status route (Spec 2 SC38)
|
|
7062
|
+
(req, res) => handleLocalModelsRoute(req, res, this.getLocalModelStatuses),
|
|
7063
|
+
(req, res) => handleMaintenanceRoute(req, res, this.maintenanceDeps),
|
|
7064
|
+
(req, res) => !!this.recorder && handleStreamsRoute(req, res, this.recorder),
|
|
7065
|
+
(req, res) => handleSessionsRoute(req, res, this.sessionsDir),
|
|
7066
|
+
// Chat proxy route (spawns Claude Code CLI — no API key required)
|
|
7067
|
+
(req, res) => handleChatProxyRoute(req, res, this.claudeCommand)
|
|
7068
|
+
];
|
|
7069
|
+
}
|
|
6819
7070
|
/** Dispatch to API route handlers. Returns true if a route matched. */
|
|
6820
7071
|
handleApiRoutes(req, res) {
|
|
6821
7072
|
if (!this.checkAuth(req, res)) return true;
|
|
6822
|
-
|
|
6823
|
-
return true;
|
|
6824
|
-
}
|
|
6825
|
-
if (handlePlansRoute(req, res, this.plansDir)) {
|
|
6826
|
-
return true;
|
|
6827
|
-
}
|
|
6828
|
-
if (handleAnalyzeRoute(req, res, this.pipeline)) {
|
|
6829
|
-
return true;
|
|
6830
|
-
}
|
|
6831
|
-
if (handleAnalysesRoute(req, res, this.analysisArchive)) {
|
|
6832
|
-
return true;
|
|
6833
|
-
}
|
|
6834
|
-
if (handleRoadmapActionsRoute(req, res, this.roadmapPath)) {
|
|
6835
|
-
return true;
|
|
6836
|
-
}
|
|
6837
|
-
if (handleDispatchActionsRoute(req, res, this.dispatchAdHoc)) {
|
|
6838
|
-
return true;
|
|
6839
|
-
}
|
|
6840
|
-
if (handleLocalModelRoute(req, res, this.getLocalModelStatus)) {
|
|
6841
|
-
return true;
|
|
6842
|
-
}
|
|
6843
|
-
if (handleLocalModelsRoute(req, res, this.getLocalModelStatuses)) {
|
|
6844
|
-
return true;
|
|
6845
|
-
}
|
|
6846
|
-
if (handleMaintenanceRoute(req, res, this.maintenanceDeps)) {
|
|
6847
|
-
return true;
|
|
6848
|
-
}
|
|
6849
|
-
if (this.recorder && handleStreamsRoute(req, res, this.recorder)) {
|
|
6850
|
-
return true;
|
|
6851
|
-
}
|
|
6852
|
-
if (handleSessionsRoute(req, res, this.sessionsDir)) {
|
|
6853
|
-
return true;
|
|
6854
|
-
}
|
|
6855
|
-
if (handleChatProxyRoute(req, res, this.claudeCommand)) {
|
|
6856
|
-
return true;
|
|
7073
|
+
for (const route of this.apiRoutes) {
|
|
7074
|
+
if (route(req, res)) return true;
|
|
6857
7075
|
}
|
|
6858
7076
|
return false;
|
|
6859
7077
|
}
|
|
@@ -7148,6 +7366,14 @@ var BUILT_IN_TASKS = [
|
|
|
7148
7366
|
schedule: "0 7 * * *",
|
|
7149
7367
|
branch: null,
|
|
7150
7368
|
checkCommand: ["perf", "baselines", "update"]
|
|
7369
|
+
},
|
|
7370
|
+
{
|
|
7371
|
+
id: "main-sync",
|
|
7372
|
+
type: "housekeeping",
|
|
7373
|
+
description: "Fast-forward local default branch from origin",
|
|
7374
|
+
schedule: "*/15 * * * *",
|
|
7375
|
+
branch: null,
|
|
7376
|
+
checkCommand: ["harness", "sync-main", "--json"]
|
|
7151
7377
|
}
|
|
7152
7378
|
];
|
|
7153
7379
|
|
|
@@ -7218,7 +7444,7 @@ function cronMatchesNow(expression, now) {
|
|
|
7218
7444
|
// src/maintenance/scheduler.ts
|
|
7219
7445
|
var MaintenanceScheduler = class {
|
|
7220
7446
|
config;
|
|
7221
|
-
|
|
7447
|
+
leaderElector;
|
|
7222
7448
|
logger;
|
|
7223
7449
|
onTaskDue;
|
|
7224
7450
|
historyProvider;
|
|
@@ -7233,7 +7459,7 @@ var MaintenanceScheduler = class {
|
|
|
7233
7459
|
activeRun = null;
|
|
7234
7460
|
constructor(options) {
|
|
7235
7461
|
this.config = options.config;
|
|
7236
|
-
this.
|
|
7462
|
+
this.leaderElector = options.leaderElector;
|
|
7237
7463
|
this.logger = options.logger;
|
|
7238
7464
|
this.historyProvider = options.historyProvider ?? null;
|
|
7239
7465
|
this.onTaskDue = options.onTaskDue;
|
|
@@ -7308,12 +7534,12 @@ var MaintenanceScheduler = class {
|
|
|
7308
7534
|
await this.processQueue(queue, epochMinute);
|
|
7309
7535
|
}
|
|
7310
7536
|
/**
|
|
7311
|
-
* Attempt to claim leadership via
|
|
7537
|
+
* Attempt to claim leadership via the configured LeaderElector.
|
|
7312
7538
|
* Returns true if this instance is the leader.
|
|
7313
7539
|
*/
|
|
7314
7540
|
async attemptLeaderClaim(evalTime) {
|
|
7315
7541
|
try {
|
|
7316
|
-
const result = await this.
|
|
7542
|
+
const result = await this.leaderElector.electLeader();
|
|
7317
7543
|
if (!result.ok) {
|
|
7318
7544
|
this.isLeader = false;
|
|
7319
7545
|
this.logger.warn("Maintenance leader claim failed", { error: result.error?.message });
|
|
@@ -7392,6 +7618,7 @@ var MaintenanceScheduler = class {
|
|
|
7392
7618
|
const history = this.historyProvider ? this.historyProvider.getHistory(200, 0) : this.internalHistory;
|
|
7393
7619
|
const schedule = this.resolvedTasks.map((task) => ({
|
|
7394
7620
|
taskId: task.id,
|
|
7621
|
+
type: task.type,
|
|
7395
7622
|
nextRun: this.computeNextRun(task.schedule),
|
|
7396
7623
|
lastRun: history.find((r) => r.taskId === task.id) ?? null
|
|
7397
7624
|
}));
|
|
@@ -7428,9 +7655,17 @@ var MaintenanceScheduler = class {
|
|
|
7428
7655
|
}
|
|
7429
7656
|
};
|
|
7430
7657
|
|
|
7658
|
+
// src/maintenance/leader-elector.ts
|
|
7659
|
+
import { Ok as Ok20 } from "@harness-engineering/types";
|
|
7660
|
+
var SingleProcessLeaderElector = class {
|
|
7661
|
+
async electLeader() {
|
|
7662
|
+
return Ok20("claimed");
|
|
7663
|
+
}
|
|
7664
|
+
};
|
|
7665
|
+
|
|
7431
7666
|
// src/maintenance/reporter.ts
|
|
7432
7667
|
import * as fs14 from "fs";
|
|
7433
|
-
import * as
|
|
7668
|
+
import * as path15 from "path";
|
|
7434
7669
|
import { z as z11 } from "zod";
|
|
7435
7670
|
var RunResultSchema = z11.object({
|
|
7436
7671
|
taskId: z11.string(),
|
|
@@ -7466,7 +7701,7 @@ var MaintenanceReporter = class {
|
|
|
7466
7701
|
async load() {
|
|
7467
7702
|
try {
|
|
7468
7703
|
await fs14.promises.mkdir(this.persistDir, { recursive: true });
|
|
7469
|
-
const filePath =
|
|
7704
|
+
const filePath = path15.join(this.persistDir, "history.json");
|
|
7470
7705
|
const data = await fs14.promises.readFile(filePath, "utf-8");
|
|
7471
7706
|
const parsed = z11.array(RunResultSchema).safeParse(JSON.parse(data));
|
|
7472
7707
|
if (parsed.success) {
|
|
@@ -7502,7 +7737,7 @@ var MaintenanceReporter = class {
|
|
|
7502
7737
|
async persist() {
|
|
7503
7738
|
try {
|
|
7504
7739
|
await fs14.promises.mkdir(this.persistDir, { recursive: true });
|
|
7505
|
-
const filePath =
|
|
7740
|
+
const filePath = path15.join(this.persistDir, "history.json");
|
|
7506
7741
|
await fs14.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
|
|
7507
7742
|
} catch (err) {
|
|
7508
7743
|
this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
|
|
@@ -7723,22 +7958,39 @@ var TaskRunner = class {
|
|
|
7723
7958
|
}
|
|
7724
7959
|
/**
|
|
7725
7960
|
* Housekeeping: run command directly, no AI, no PR.
|
|
7961
|
+
*
|
|
7962
|
+
* Captures stdout and parses a trailing JSON status line if present.
|
|
7963
|
+
* Recognized contracts:
|
|
7964
|
+
* - Phase 4/5 status contract (e.g., harness pulse run): success/skipped/failure/no-issues
|
|
7965
|
+
* - sync-main contract: updated/no-op/skipped/error → mapped onto the run-result status
|
|
7966
|
+
* Legacy housekeeping commands that emit no JSON keep the prior behavior:
|
|
7967
|
+
* status: 'success', findings: 0.
|
|
7726
7968
|
*/
|
|
7727
7969
|
async runHousekeeping(task, startedAt) {
|
|
7728
7970
|
if (!task.checkCommand || task.checkCommand.length === 0) {
|
|
7729
7971
|
return this.failureResult(task.id, startedAt, "housekeeping task missing checkCommand");
|
|
7730
7972
|
}
|
|
7731
|
-
|
|
7732
|
-
|
|
7973
|
+
let stdout;
|
|
7974
|
+
try {
|
|
7975
|
+
const out = await this.commandExecutor.exec(task.checkCommand, this.cwd);
|
|
7976
|
+
stdout = out.stdout ?? "";
|
|
7977
|
+
} catch (err) {
|
|
7978
|
+
return this.failureResult(task.id, startedAt, String(err));
|
|
7979
|
+
}
|
|
7980
|
+
const parsed = parseStatusLine(stdout);
|
|
7981
|
+
const status = parsed?.status ?? "success";
|
|
7982
|
+
const result = {
|
|
7733
7983
|
taskId: task.id,
|
|
7734
7984
|
startedAt,
|
|
7735
7985
|
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7736
|
-
status
|
|
7986
|
+
status,
|
|
7737
7987
|
findings: 0,
|
|
7738
7988
|
fixed: 0,
|
|
7739
7989
|
prUrl: null,
|
|
7740
7990
|
prUpdated: false
|
|
7741
7991
|
};
|
|
7992
|
+
if (parsed?.error) result.error = parsed.error;
|
|
7993
|
+
return result;
|
|
7742
7994
|
}
|
|
7743
7995
|
/**
|
|
7744
7996
|
* Resolve which AI backend name to use for a given task.
|
|
@@ -7772,7 +8024,7 @@ function parseStatusLine(output) {
|
|
|
7772
8024
|
const obj = JSON.parse(line);
|
|
7773
8025
|
const s = obj.status;
|
|
7774
8026
|
if (s === "success" || s === "skipped" || s === "failure" || s === "no-issues") {
|
|
7775
|
-
const parsed = { status: s };
|
|
8027
|
+
const parsed = { status: s, rawStatus: s };
|
|
7776
8028
|
if (typeof obj.candidatesFound === "number") {
|
|
7777
8029
|
parsed.candidatesFound = obj.candidatesFound;
|
|
7778
8030
|
}
|
|
@@ -7782,8 +8034,18 @@ function parseStatusLine(output) {
|
|
|
7782
8034
|
if (typeof obj.reason === "string") {
|
|
7783
8035
|
parsed.reason = obj.reason;
|
|
7784
8036
|
}
|
|
8037
|
+
if (typeof obj.detail === "string" && !parsed.error) {
|
|
8038
|
+
parsed.error = `${parsed.reason ?? "skipped"}: ${obj.detail}`;
|
|
8039
|
+
}
|
|
7785
8040
|
return parsed;
|
|
7786
8041
|
}
|
|
8042
|
+
if (s === "updated" || s === "no-op") {
|
|
8043
|
+
return { status: "success", rawStatus: s };
|
|
8044
|
+
}
|
|
8045
|
+
if (s === "error") {
|
|
8046
|
+
const message = typeof obj.message === "string" ? obj.message : "unknown error";
|
|
8047
|
+
return { status: "failure", error: message, rawStatus: "error" };
|
|
8048
|
+
}
|
|
7787
8049
|
} catch {
|
|
7788
8050
|
}
|
|
7789
8051
|
}
|
|
@@ -7864,11 +8126,15 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7864
8126
|
completionHandler;
|
|
7865
8127
|
/** Project root directory, derived from workspace root. */
|
|
7866
8128
|
get projectRoot() {
|
|
7867
|
-
return
|
|
8129
|
+
return path16.resolve(this.config.workspace.root, "..", "..");
|
|
7868
8130
|
}
|
|
7869
8131
|
enrichedSpecsByIssue = /* @__PURE__ */ new Map();
|
|
7870
8132
|
/** Tracks recently-failed intelligence analysis to avoid re-requesting every tick */
|
|
7871
8133
|
analysisFailureCache = /* @__PURE__ */ new Map();
|
|
8134
|
+
// Phase 3 added a private `roadmapMode` field used by `createTracker` to
|
|
8135
|
+
// guard the file-less stub. Phase 4 / S2 / D-P4-E shifted dispatch onto
|
|
8136
|
+
// `tracker.kind`, removing the need for the field — it is now dropped to
|
|
8137
|
+
// satisfy `noUnusedLocals`. See decision D-P3-orchestrator-mode-via-fs-read.
|
|
7872
8138
|
/** Abort controllers and PIDs for running agent tasks — used by stopIssue to cancel in-flight work.
|
|
7873
8139
|
* The PID is stored here because the running entry may be deleted by the state machine
|
|
7874
8140
|
* before the stop effect executes (e.g., stall_detected removes the entry first). */
|
|
@@ -7904,14 +8170,19 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7904
8170
|
);
|
|
7905
8171
|
}
|
|
7906
8172
|
this.tracker = overrides?.tracker || this.createTracker();
|
|
7907
|
-
this.workspace = new WorkspaceManager(config.workspace
|
|
8173
|
+
this.workspace = new WorkspaceManager(config.workspace, {
|
|
8174
|
+
emitEvent: (event) => {
|
|
8175
|
+
this.server?.broadcastMaintenance("maintenance:baseref_fallback", event);
|
|
8176
|
+
this.emit("maintenance:baseref_fallback", event);
|
|
8177
|
+
}
|
|
8178
|
+
});
|
|
7908
8179
|
this.hooks = new WorkspaceHooks(config.hooks);
|
|
7909
8180
|
this.renderer = new PromptRenderer();
|
|
7910
8181
|
this.overrideBackend = overrides?.backend ?? null;
|
|
7911
8182
|
this.interactionQueue = new InteractionQueue(
|
|
7912
|
-
|
|
8183
|
+
path16.join(config.workspace.root, "..", "interactions")
|
|
7913
8184
|
);
|
|
7914
|
-
this.analysisArchive = new AnalysisArchive(
|
|
8185
|
+
this.analysisArchive = new AnalysisArchive(path16.join(config.workspace.root, "..", "analyses"));
|
|
7915
8186
|
const backendsMap = this.config.agent.backends ?? {};
|
|
7916
8187
|
for (const [name, def] of Object.entries(backendsMap)) {
|
|
7917
8188
|
if (def.type === "local" || def.type === "pi") {
|
|
@@ -7953,7 +8224,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7953
8224
|
...overrides?.execFileFn ? { execFileFn: overrides.execFileFn } : {}
|
|
7954
8225
|
});
|
|
7955
8226
|
this.recorder = new StreamRecorder(
|
|
7956
|
-
|
|
8227
|
+
path16.resolve(config.workspace.root, "..", "streams"),
|
|
7957
8228
|
this.logger
|
|
7958
8229
|
);
|
|
7959
8230
|
const self = this;
|
|
@@ -7985,7 +8256,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
7985
8256
|
if (config.server?.port) {
|
|
7986
8257
|
this.server = new OrchestratorServer(this, config.server.port, {
|
|
7987
8258
|
interactionQueue: this.interactionQueue,
|
|
7988
|
-
plansDir:
|
|
8259
|
+
plansDir: path16.resolve(config.workspace.root, "..", "docs", "plans"),
|
|
7989
8260
|
pipeline: this.pipeline,
|
|
7990
8261
|
analysisArchive: this.analysisArchive,
|
|
7991
8262
|
roadmapPath: config.tracker.filePath ?? null,
|
|
@@ -8016,6 +8287,17 @@ var Orchestrator = class extends EventEmitter {
|
|
|
8016
8287
|
}
|
|
8017
8288
|
}
|
|
8018
8289
|
createTracker() {
|
|
8290
|
+
if (this.config.tracker.kind === "github-issues") {
|
|
8291
|
+
const trackerCfg = {
|
|
8292
|
+
kind: "github-issues",
|
|
8293
|
+
repo: this.config.tracker.projectSlug ?? "",
|
|
8294
|
+
...this.config.tracker.apiKey ? { token: this.config.tracker.apiKey } : {},
|
|
8295
|
+
...this.config.tracker.endpoint ? { apiBase: this.config.tracker.endpoint } : {}
|
|
8296
|
+
};
|
|
8297
|
+
const clientResult = createTrackerClient2(trackerCfg);
|
|
8298
|
+
if (!clientResult.ok) throw clientResult.error;
|
|
8299
|
+
return new GitHubIssuesIssueTrackerAdapter(clientResult.value, this.config.tracker);
|
|
8300
|
+
}
|
|
8019
8301
|
if (this.config.tracker.kind === "roadmap") {
|
|
8020
8302
|
return new RoadmapTrackerAdapter(this.config.tracker);
|
|
8021
8303
|
}
|
|
@@ -8031,8 +8313,8 @@ var Orchestrator = class extends EventEmitter {
|
|
|
8031
8313
|
const checkRunner = {
|
|
8032
8314
|
run: async (command, cwd) => {
|
|
8033
8315
|
const { execFile: execFile6 } = await import("child_process");
|
|
8034
|
-
const { promisify:
|
|
8035
|
-
const execFileAsync =
|
|
8316
|
+
const { promisify: promisify4 } = await import("util");
|
|
8317
|
+
const execFileAsync = promisify4(execFile6);
|
|
8036
8318
|
const [cmd, ...args] = command;
|
|
8037
8319
|
if (!cmd) return { passed: true, findings: 0, output: "" };
|
|
8038
8320
|
try {
|
|
@@ -8066,12 +8348,13 @@ var Orchestrator = class extends EventEmitter {
|
|
|
8066
8348
|
const commandExecutor = {
|
|
8067
8349
|
exec: async (command, cwd) => {
|
|
8068
8350
|
const { execFile: execFile6 } = await import("child_process");
|
|
8069
|
-
const { promisify:
|
|
8070
|
-
const execFileAsync =
|
|
8351
|
+
const { promisify: promisify4 } = await import("util");
|
|
8352
|
+
const execFileAsync = promisify4(execFile6);
|
|
8071
8353
|
const [cmd, ...args] = command;
|
|
8072
|
-
if (!cmd) return;
|
|
8354
|
+
if (!cmd) return { stdout: "" };
|
|
8073
8355
|
try {
|
|
8074
|
-
await execFileAsync(cmd, args, { cwd, timeout: 12e4 });
|
|
8356
|
+
const { stdout } = await execFileAsync(cmd, args, { cwd, timeout: 12e4 });
|
|
8357
|
+
return { stdout: String(stdout) };
|
|
8075
8358
|
} catch (err) {
|
|
8076
8359
|
logger.warn("Maintenance command execution failed", {
|
|
8077
8360
|
command,
|
|
@@ -8096,7 +8379,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
8096
8379
|
*/
|
|
8097
8380
|
async initMaintenance(maintenanceConfig) {
|
|
8098
8381
|
this.maintenanceReporter = new MaintenanceReporter({
|
|
8099
|
-
persistDir:
|
|
8382
|
+
persistDir: path16.join(this.projectRoot, ".harness", "maintenance"),
|
|
8100
8383
|
logger: this.logger
|
|
8101
8384
|
});
|
|
8102
8385
|
await this.maintenanceReporter.load();
|
|
@@ -8104,7 +8387,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
8104
8387
|
const reporter = this.maintenanceReporter;
|
|
8105
8388
|
this.maintenanceScheduler = new MaintenanceScheduler({
|
|
8106
8389
|
config: maintenanceConfig,
|
|
8107
|
-
|
|
8390
|
+
leaderElector: new SingleProcessLeaderElector(),
|
|
8108
8391
|
logger: this.logger,
|
|
8109
8392
|
historyProvider: reporter,
|
|
8110
8393
|
onTaskDue: async (task) => {
|
|
@@ -8147,129 +8430,14 @@ var Orchestrator = class extends EventEmitter {
|
|
|
8147
8430
|
}
|
|
8148
8431
|
}
|
|
8149
8432
|
createIntelligencePipeline() {
|
|
8150
|
-
const
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
if (!selProvider) return null;
|
|
8154
|
-
const routing = this.config.agent.routing;
|
|
8155
|
-
const peslName = routing?.intelligence?.pesl;
|
|
8156
|
-
const selName = routing?.intelligence?.sel ?? routing?.default;
|
|
8157
|
-
const peslProvider = peslName !== void 0 && peslName !== selName ? this.createAnalysisProvider("pesl") : null;
|
|
8158
|
-
const peslModel = intel.models?.pesl ?? this.config.agent.model;
|
|
8159
|
-
const store = new GraphStore();
|
|
8160
|
-
this.graphStore = store;
|
|
8161
|
-
return new IntelligencePipeline(selProvider, store, {
|
|
8162
|
-
...peslModel !== void 0 && { peslModel },
|
|
8163
|
-
...peslProvider !== null && peslProvider !== void 0 && { peslProvider }
|
|
8164
|
-
});
|
|
8165
|
-
}
|
|
8166
|
-
/**
|
|
8167
|
-
* Create the AnalysisProvider for an intelligence-pipeline layer
|
|
8168
|
-
* (`sel` by default; `pesl` when constructing a distinct PESL
|
|
8169
|
-
* provider per Spec 2 SC35).
|
|
8170
|
-
*
|
|
8171
|
-
* Spec 2 Phase 4 (SC31–SC36) — resolution order:
|
|
8172
|
-
* 1. Explicit `intelligence.provider` config wins (preserves Phase 0–3 behavior; SC33).
|
|
8173
|
-
* 2. Otherwise, consult `agent.routing.intelligence.<layer>` (or
|
|
8174
|
-
* `routing.default`) to pick a `BackendDef` from `agent.backends`,
|
|
8175
|
-
* then translate via `buildAnalysisProvider` (the per-type factory).
|
|
8176
|
-
*
|
|
8177
|
-
* Closes the Phase 2 deferral (P2-DEF-638): the legacy
|
|
8178
|
-
* `this.config.agent.backend` read at the bottom of this method is
|
|
8179
|
-
* removed; routing is the sole source for non-explicit configs.
|
|
8180
|
-
*
|
|
8181
|
-
* Cyclomatic complexity: pre-Phase-4 was 33 (factory dispatch was
|
|
8182
|
-
* inlined here). Phase 4 extracts the per-type tree into
|
|
8183
|
-
* `buildAnalysisProvider`, dropping this method to ≤ 5 branches
|
|
8184
|
-
* (under the 15 threshold).
|
|
8185
|
-
*/
|
|
8186
|
-
createAnalysisProvider(layer = "sel") {
|
|
8187
|
-
const intel = this.config.intelligence;
|
|
8188
|
-
if (!intel?.enabled) return null;
|
|
8189
|
-
if (intel.provider) {
|
|
8190
|
-
const layerModel = layer === "sel" ? intel.models?.sel : intel.models?.pesl;
|
|
8191
|
-
return this.createProviderFromExplicitConfig(
|
|
8192
|
-
intel.provider,
|
|
8193
|
-
layerModel ?? this.config.agent.model
|
|
8194
|
-
);
|
|
8195
|
-
}
|
|
8196
|
-
const routed = this.resolveRoutedBackendForIntelligence(layer);
|
|
8197
|
-
if (!routed) return null;
|
|
8198
|
-
const { name, def } = routed;
|
|
8199
|
-
const resolver = this.localResolvers.get(name);
|
|
8200
|
-
return buildAnalysisProvider({
|
|
8201
|
-
def,
|
|
8202
|
-
backendName: name,
|
|
8203
|
-
layer,
|
|
8204
|
-
// Spec 2 P3-IMP-1: a single snapshot read feeds the factory's
|
|
8205
|
-
// unavailable-warn diagnostic (Configured/Detected lists) and
|
|
8206
|
-
// collapses the two `getStatus()` calls flagged by P3-SUG-2.
|
|
8207
|
-
getResolverStatusSnapshot: () => {
|
|
8208
|
-
if (!resolver) return null;
|
|
8209
|
-
const status = resolver.getStatus();
|
|
8210
|
-
return {
|
|
8211
|
-
available: status.available,
|
|
8212
|
-
resolved: status.resolved,
|
|
8213
|
-
configured: status.configured,
|
|
8214
|
-
detected: status.detected
|
|
8215
|
-
};
|
|
8216
|
-
},
|
|
8217
|
-
intelligence: intel,
|
|
8433
|
+
const bundle = buildIntelligencePipeline({
|
|
8434
|
+
config: this.config,
|
|
8435
|
+
localResolvers: this.localResolvers,
|
|
8218
8436
|
logger: this.logger
|
|
8219
8437
|
});
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
* back through `routing.intelligence.<layer>` → `routing.default`
|
|
8224
|
-
* → null. Returns the resolved name alongside the def so callers can
|
|
8225
|
-
* key into the per-name resolver map.
|
|
8226
|
-
*/
|
|
8227
|
-
resolveRoutedBackendForIntelligence(layer) {
|
|
8228
|
-
const routing = this.config.agent.routing;
|
|
8229
|
-
const backends = this.config.agent.backends;
|
|
8230
|
-
if (!routing || !backends) return null;
|
|
8231
|
-
const layerName = routing.intelligence?.[layer];
|
|
8232
|
-
const name = layerName ?? routing.default;
|
|
8233
|
-
const def = backends[name];
|
|
8234
|
-
if (!def) {
|
|
8235
|
-
this.logger.warn(
|
|
8236
|
-
`Intelligence pipeline: routed backend '${name}' for layer '${layer}' is not in agent.backends.`
|
|
8237
|
-
);
|
|
8238
|
-
return null;
|
|
8239
|
-
}
|
|
8240
|
-
return { name, def };
|
|
8241
|
-
}
|
|
8242
|
-
createProviderFromExplicitConfig(provider, selModel) {
|
|
8243
|
-
if (provider.kind === "anthropic") {
|
|
8244
|
-
const apiKey2 = provider.apiKey ?? this.config.agent.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
8245
|
-
if (!apiKey2) {
|
|
8246
|
-
throw new Error("Intelligence pipeline: no Anthropic API key found.");
|
|
8247
|
-
}
|
|
8248
|
-
return new AnthropicAnalysisProvider2({
|
|
8249
|
-
apiKey: apiKey2,
|
|
8250
|
-
...selModel !== void 0 && { defaultModel: selModel }
|
|
8251
|
-
});
|
|
8252
|
-
}
|
|
8253
|
-
if (provider.kind === "claude-cli") {
|
|
8254
|
-
return new ClaudeCliAnalysisProvider2({
|
|
8255
|
-
command: this.config.agent.command,
|
|
8256
|
-
...selModel !== void 0 && { defaultModel: selModel },
|
|
8257
|
-
...this.config.intelligence?.requestTimeoutMs !== void 0 && {
|
|
8258
|
-
timeoutMs: this.config.intelligence.requestTimeoutMs
|
|
8259
|
-
}
|
|
8260
|
-
});
|
|
8261
|
-
}
|
|
8262
|
-
const apiKey = provider.apiKey ?? this.config.agent.apiKey ?? "ollama";
|
|
8263
|
-
const baseUrl = provider.baseUrl ?? "http://localhost:11434/v1";
|
|
8264
|
-
const intel = this.config.intelligence;
|
|
8265
|
-
return new OpenAICompatibleAnalysisProvider2({
|
|
8266
|
-
apiKey,
|
|
8267
|
-
baseUrl,
|
|
8268
|
-
...selModel !== void 0 && { defaultModel: selModel },
|
|
8269
|
-
...intel?.requestTimeoutMs !== void 0 && { timeoutMs: intel.requestTimeoutMs },
|
|
8270
|
-
...intel?.promptSuffix !== void 0 && { promptSuffix: intel.promptSuffix },
|
|
8271
|
-
...intel?.jsonMode !== void 0 && { jsonMode: intel.jsonMode }
|
|
8272
|
-
});
|
|
8438
|
+
if (!bundle) return null;
|
|
8439
|
+
this.graphStore = bundle.graphStore;
|
|
8440
|
+
return bundle.pipeline;
|
|
8273
8441
|
}
|
|
8274
8442
|
/**
|
|
8275
8443
|
* Lazily initializes the ClaimManager if it hasn't been created yet.
|
|
@@ -9329,6 +9497,150 @@ function launchTUI(orchestrator) {
|
|
|
9329
9497
|
const { waitUntilExit } = render(/* @__PURE__ */ jsx5(Dashboard, { orchestrator }));
|
|
9330
9498
|
return { waitUntilExit };
|
|
9331
9499
|
}
|
|
9500
|
+
|
|
9501
|
+
// src/maintenance/sync-main.ts
|
|
9502
|
+
import { execFile as nodeExecFile } from "child_process";
|
|
9503
|
+
import { promisify as promisify3 } from "util";
|
|
9504
|
+
var DEFAULT_TIMEOUT_MS2 = 6e4;
|
|
9505
|
+
async function git(execFileFn, args, cwd, timeoutMs) {
|
|
9506
|
+
const exec = promisify3(execFileFn);
|
|
9507
|
+
const { stdout, stderr } = await exec("git", args, { cwd, timeout: timeoutMs });
|
|
9508
|
+
return { stdout: String(stdout), stderr: String(stderr) };
|
|
9509
|
+
}
|
|
9510
|
+
function isSpawnEnoent(err) {
|
|
9511
|
+
if (!err || typeof err !== "object") return false;
|
|
9512
|
+
const code = err.code;
|
|
9513
|
+
return code === "ENOENT";
|
|
9514
|
+
}
|
|
9515
|
+
async function refExists(execFileFn, ref, cwd, timeoutMs) {
|
|
9516
|
+
try {
|
|
9517
|
+
await git(execFileFn, ["rev-parse", "--verify", "--quiet", ref], cwd, timeoutMs);
|
|
9518
|
+
return true;
|
|
9519
|
+
} catch (err) {
|
|
9520
|
+
if (isSpawnEnoent(err)) throw err;
|
|
9521
|
+
return false;
|
|
9522
|
+
}
|
|
9523
|
+
}
|
|
9524
|
+
async function resolveOriginDefault(execFileFn, cwd, timeoutMs) {
|
|
9525
|
+
try {
|
|
9526
|
+
const { stdout } = await git(
|
|
9527
|
+
execFileFn,
|
|
9528
|
+
["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
|
|
9529
|
+
cwd,
|
|
9530
|
+
timeoutMs
|
|
9531
|
+
);
|
|
9532
|
+
const v = stdout.trim();
|
|
9533
|
+
if (v) return v;
|
|
9534
|
+
} catch (err) {
|
|
9535
|
+
if (isSpawnEnoent(err)) throw err;
|
|
9536
|
+
}
|
|
9537
|
+
for (const candidate of ["origin/main", "origin/master"]) {
|
|
9538
|
+
if (await refExists(execFileFn, candidate, cwd, timeoutMs)) return candidate;
|
|
9539
|
+
}
|
|
9540
|
+
return null;
|
|
9541
|
+
}
|
|
9542
|
+
function shortName(originRef) {
|
|
9543
|
+
return originRef.startsWith("origin/") ? originRef.slice("origin/".length) : originRef;
|
|
9544
|
+
}
|
|
9545
|
+
function isDirtyConflictStderr(s) {
|
|
9546
|
+
return /would be overwritten|local changes|Aborting/i.test(s);
|
|
9547
|
+
}
|
|
9548
|
+
function extractStderr(err) {
|
|
9549
|
+
if (err && typeof err === "object" && "stderr" in err) {
|
|
9550
|
+
const raw = err.stderr;
|
|
9551
|
+
if (typeof raw === "string") return raw;
|
|
9552
|
+
if (raw instanceof Buffer) return raw.toString("utf8");
|
|
9553
|
+
}
|
|
9554
|
+
if (err instanceof Error) return err.message;
|
|
9555
|
+
if (typeof err === "string") return err;
|
|
9556
|
+
return "";
|
|
9557
|
+
}
|
|
9558
|
+
async function isAncestor(execFileFn, a, b, cwd, timeoutMs) {
|
|
9559
|
+
try {
|
|
9560
|
+
await git(execFileFn, ["merge-base", "--is-ancestor", a, b], cwd, timeoutMs);
|
|
9561
|
+
return true;
|
|
9562
|
+
} catch (err) {
|
|
9563
|
+
if (isSpawnEnoent(err)) throw err;
|
|
9564
|
+
return false;
|
|
9565
|
+
}
|
|
9566
|
+
}
|
|
9567
|
+
async function syncMain(repoRoot, opts = {}) {
|
|
9568
|
+
const execFileFn = opts.execFileFn ?? nodeExecFile;
|
|
9569
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
9570
|
+
try {
|
|
9571
|
+
const originRef = await resolveOriginDefault(execFileFn, repoRoot, timeoutMs);
|
|
9572
|
+
if (!originRef) {
|
|
9573
|
+
return {
|
|
9574
|
+
status: "skipped",
|
|
9575
|
+
reason: "no-remote",
|
|
9576
|
+
detail: "origin/HEAD unset and neither origin/main nor origin/master resolves",
|
|
9577
|
+
defaultBranch: ""
|
|
9578
|
+
};
|
|
9579
|
+
}
|
|
9580
|
+
const defaultBranch = shortName(originRef);
|
|
9581
|
+
const { stdout: currentRaw } = await git(
|
|
9582
|
+
execFileFn,
|
|
9583
|
+
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
9584
|
+
repoRoot,
|
|
9585
|
+
timeoutMs
|
|
9586
|
+
);
|
|
9587
|
+
const current = currentRaw.trim();
|
|
9588
|
+
if (current !== defaultBranch) {
|
|
9589
|
+
return {
|
|
9590
|
+
status: "skipped",
|
|
9591
|
+
reason: "wrong-branch",
|
|
9592
|
+
detail: `current branch '${current}' is not the default '${defaultBranch}'`,
|
|
9593
|
+
defaultBranch
|
|
9594
|
+
};
|
|
9595
|
+
}
|
|
9596
|
+
try {
|
|
9597
|
+
await git(execFileFn, ["fetch", "origin", defaultBranch, "--quiet"], repoRoot, timeoutMs);
|
|
9598
|
+
} catch (err) {
|
|
9599
|
+
if (isSpawnEnoent(err)) throw err;
|
|
9600
|
+
return {
|
|
9601
|
+
status: "skipped",
|
|
9602
|
+
reason: "fetch-failed",
|
|
9603
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
9604
|
+
defaultBranch
|
|
9605
|
+
};
|
|
9606
|
+
}
|
|
9607
|
+
const headIsAncestor = await isAncestor(execFileFn, "HEAD", originRef, repoRoot, timeoutMs);
|
|
9608
|
+
const originIsAncestor = await isAncestor(execFileFn, originRef, "HEAD", repoRoot, timeoutMs);
|
|
9609
|
+
if (headIsAncestor && originIsAncestor) {
|
|
9610
|
+
return { status: "no-op", defaultBranch };
|
|
9611
|
+
}
|
|
9612
|
+
if (!headIsAncestor) {
|
|
9613
|
+
return {
|
|
9614
|
+
status: "skipped",
|
|
9615
|
+
reason: "diverged",
|
|
9616
|
+
detail: `local '${defaultBranch}' has commits not on '${originRef}'`,
|
|
9617
|
+
defaultBranch
|
|
9618
|
+
};
|
|
9619
|
+
}
|
|
9620
|
+
const before = (await git(execFileFn, ["rev-parse", "HEAD"], repoRoot, timeoutMs)).stdout.trim();
|
|
9621
|
+
try {
|
|
9622
|
+
await git(execFileFn, ["merge", "--ff-only", originRef], repoRoot, timeoutMs);
|
|
9623
|
+
} catch (err) {
|
|
9624
|
+
const stderr = extractStderr(err);
|
|
9625
|
+
if (isDirtyConflictStderr(stderr)) {
|
|
9626
|
+
return {
|
|
9627
|
+
status: "skipped",
|
|
9628
|
+
reason: "dirty-conflict",
|
|
9629
|
+
detail: stderr.split("\n")[0] ?? "merge --ff-only failed due to working-tree changes",
|
|
9630
|
+
defaultBranch
|
|
9631
|
+
};
|
|
9632
|
+
}
|
|
9633
|
+
throw err;
|
|
9634
|
+
}
|
|
9635
|
+
const after = (await git(execFileFn, ["rev-parse", "HEAD"], repoRoot, timeoutMs)).stdout.trim();
|
|
9636
|
+
return { status: "updated", from: before, to: after, defaultBranch };
|
|
9637
|
+
} catch (err) {
|
|
9638
|
+
return {
|
|
9639
|
+
status: "error",
|
|
9640
|
+
message: err instanceof Error ? err.message : String(err)
|
|
9641
|
+
};
|
|
9642
|
+
}
|
|
9643
|
+
}
|
|
9332
9644
|
export {
|
|
9333
9645
|
AnalysisArchive,
|
|
9334
9646
|
BackendRouter,
|
|
@@ -9372,6 +9684,7 @@ export {
|
|
|
9372
9684
|
savePublishedIndex,
|
|
9373
9685
|
selectCandidates,
|
|
9374
9686
|
sortCandidates,
|
|
9687
|
+
syncMain,
|
|
9375
9688
|
triageIssue,
|
|
9376
9689
|
validateWorkflowConfig
|
|
9377
9690
|
};
|