@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.js
CHANGED
|
@@ -72,6 +72,7 @@ __export(index_exports, {
|
|
|
72
72
|
savePublishedIndex: () => savePublishedIndex,
|
|
73
73
|
selectCandidates: () => selectCandidates,
|
|
74
74
|
sortCandidates: () => sortCandidates,
|
|
75
|
+
syncMain: () => syncMain,
|
|
75
76
|
triageIssue: () => triageIssue,
|
|
76
77
|
validateWorkflowConfig: () => validateWorkflowConfig
|
|
77
78
|
});
|
|
@@ -649,6 +650,56 @@ function reconcileCompletedAndClaimed(next, candidates, nowMs, effects) {
|
|
|
649
650
|
}
|
|
650
651
|
}
|
|
651
652
|
}
|
|
653
|
+
function gatherSignalsAndPersona(issue, event) {
|
|
654
|
+
const signals = [...event.concernSignals?.get(issue.id) ?? []];
|
|
655
|
+
let suggestedPersona;
|
|
656
|
+
try {
|
|
657
|
+
const personaRecs = event.personaRecommendations?.get(issue.id);
|
|
658
|
+
if (personaRecs && personaRecs.length > 0) {
|
|
659
|
+
suggestedPersona = personaRecs[0].persona;
|
|
660
|
+
if (personaRecs[0].weightedScore < 0.3) {
|
|
661
|
+
signals.push({
|
|
662
|
+
name: "lowExpertise",
|
|
663
|
+
reason: `Top persona "${suggestedPersona}" scored ${personaRecs[0].weightedScore.toFixed(2)} (below 0.3 threshold)`
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
} else if (personaRecs && personaRecs.length === 0) {
|
|
667
|
+
signals.push({
|
|
668
|
+
name: "noPersonaMatch",
|
|
669
|
+
reason: "No persona recommendations available for this issue's systems"
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
} catch {
|
|
673
|
+
}
|
|
674
|
+
return { signals, suggestedPersona };
|
|
675
|
+
}
|
|
676
|
+
function attachPersonaToLastClaim(effects, suggestedPersona) {
|
|
677
|
+
if (!suggestedPersona) return;
|
|
678
|
+
const lastEffect = effects[effects.length - 1];
|
|
679
|
+
if (lastEffect && lastEffect.type === "claim") {
|
|
680
|
+
lastEffect.suggestedPersona = suggestedPersona;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
function dispatchEligibleIssue(next, issue, event, escalationConfig, config, effects) {
|
|
684
|
+
const scopeTier = detectScopeTier(issue, artifactPresenceFromIssue(issue));
|
|
685
|
+
const { signals, suggestedPersona } = gatherSignalsAndPersona(issue, event);
|
|
686
|
+
const decision = routeIssue(scopeTier, signals, escalationConfig);
|
|
687
|
+
if (decision.action === "needs-human") {
|
|
688
|
+
next.claimed.add(issue.id);
|
|
689
|
+
effects.push(
|
|
690
|
+
buildEscalateEffect(issue.id, issue.identifier, decision.reasons, {
|
|
691
|
+
issueTitle: issue.title,
|
|
692
|
+
issueDescription: issue.description,
|
|
693
|
+
enrichedSpec: event.enrichedSpecs?.get(issue.id),
|
|
694
|
+
complexityScore: event.complexityScores?.get(issue.id)
|
|
695
|
+
})
|
|
696
|
+
);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const backend = resolveBackend(decision.action, !!config.agent.localBackend);
|
|
700
|
+
claimAndDispatch(next, issue, backend, event.nowMs, effects);
|
|
701
|
+
attachPersonaToLastClaim(effects, suggestedPersona);
|
|
702
|
+
}
|
|
652
703
|
function handleTick(state, event, config) {
|
|
653
704
|
const { candidates, runningStates, nowMs } = event;
|
|
654
705
|
const next = cloneState(state);
|
|
@@ -679,48 +730,7 @@ function handleTick(state, event, config) {
|
|
|
679
730
|
effects.push(peslAbort);
|
|
680
731
|
continue;
|
|
681
732
|
}
|
|
682
|
-
|
|
683
|
-
const signals = [...event.concernSignals?.get(issue.id) ?? []];
|
|
684
|
-
let suggestedPersona;
|
|
685
|
-
try {
|
|
686
|
-
const personaRecs = event.personaRecommendations?.get(issue.id);
|
|
687
|
-
if (personaRecs && personaRecs.length > 0) {
|
|
688
|
-
suggestedPersona = personaRecs[0].persona;
|
|
689
|
-
if (personaRecs[0].weightedScore < 0.3) {
|
|
690
|
-
signals.push({
|
|
691
|
-
name: "lowExpertise",
|
|
692
|
-
reason: `Top persona "${suggestedPersona}" scored ${personaRecs[0].weightedScore.toFixed(2)} (below 0.3 threshold)`
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
} else if (personaRecs && personaRecs.length === 0) {
|
|
696
|
-
signals.push({
|
|
697
|
-
name: "noPersonaMatch",
|
|
698
|
-
reason: "No persona recommendations available for this issue's systems"
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
} catch {
|
|
702
|
-
}
|
|
703
|
-
const decision = routeIssue(scopeTier, signals, escalationConfig);
|
|
704
|
-
if (decision.action === "needs-human") {
|
|
705
|
-
next.claimed.add(issue.id);
|
|
706
|
-
effects.push(
|
|
707
|
-
buildEscalateEffect(issue.id, issue.identifier, decision.reasons, {
|
|
708
|
-
issueTitle: issue.title,
|
|
709
|
-
issueDescription: issue.description,
|
|
710
|
-
enrichedSpec: event.enrichedSpecs?.get(issue.id),
|
|
711
|
-
complexityScore: event.complexityScores?.get(issue.id)
|
|
712
|
-
})
|
|
713
|
-
);
|
|
714
|
-
continue;
|
|
715
|
-
}
|
|
716
|
-
const backend = resolveBackend(decision.action, !!config.agent.localBackend);
|
|
717
|
-
claimAndDispatch(next, issue, backend, nowMs, effects);
|
|
718
|
-
if (suggestedPersona) {
|
|
719
|
-
const lastEffect = effects[effects.length - 1];
|
|
720
|
-
if (lastEffect && lastEffect.type === "claim") {
|
|
721
|
-
lastEffect.suggestedPersona = suggestedPersona;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
733
|
+
dispatchEligibleIssue(next, issue, event, escalationConfig, config, effects);
|
|
724
734
|
}
|
|
725
735
|
pruneCompleted(next);
|
|
726
736
|
return { nextState: next, effects };
|
|
@@ -1916,11 +1926,11 @@ var BackendsMapSchema = import_zod2.z.record(import_zod2.z.string(), BackendDefS
|
|
|
1916
1926
|
function crossFieldRoutingIssues(backends, routing) {
|
|
1917
1927
|
const issues = [];
|
|
1918
1928
|
const names = new Set(Object.keys(backends));
|
|
1919
|
-
const checkRef = (
|
|
1929
|
+
const checkRef = (path17, name) => {
|
|
1920
1930
|
if (name !== void 0 && !names.has(name)) {
|
|
1921
1931
|
issues.push({
|
|
1922
|
-
path:
|
|
1923
|
-
message: `routing.${
|
|
1932
|
+
path: path17,
|
|
1933
|
+
message: `routing.${path17.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
|
|
1924
1934
|
});
|
|
1925
1935
|
}
|
|
1926
1936
|
};
|
|
@@ -2279,8 +2289,11 @@ var WorkspaceManager = class {
|
|
|
2279
2289
|
config;
|
|
2280
2290
|
/** Absolute path to the git repository root (resolved lazily). */
|
|
2281
2291
|
repoRoot = null;
|
|
2282
|
-
|
|
2292
|
+
/** Phase 3 (D6): emit baseref_fallback when fallback chain selects a local-only ref. */
|
|
2293
|
+
emitEvent;
|
|
2294
|
+
constructor(config, options = {}) {
|
|
2283
2295
|
this.config = config;
|
|
2296
|
+
this.emitEvent = options.emitEvent ?? null;
|
|
2284
2297
|
}
|
|
2285
2298
|
/** Runs a git command and returns stdout. Extracted for testability. */
|
|
2286
2299
|
async git(args, cwd) {
|
|
@@ -2365,9 +2378,14 @@ var WorkspaceManager = class {
|
|
|
2365
2378
|
* Priority order:
|
|
2366
2379
|
* 1. `config.baseRef` (explicit override). Throws if it doesn't resolve.
|
|
2367
2380
|
* 2. Default branch via `git symbolic-ref --short refs/remotes/origin/HEAD`.
|
|
2368
|
-
* 3.
|
|
2369
|
-
* 4.
|
|
2370
|
-
*
|
|
2381
|
+
* 3. Remote fallbacks: `origin/main`, `origin/master`. (No event.)
|
|
2382
|
+
* 4. Local-only fallbacks: `main`, `master`. (Emits `baseref_fallback`.)
|
|
2383
|
+
* 5. `HEAD` as ultimate fallback. (Emits `baseref_fallback`.)
|
|
2384
|
+
*
|
|
2385
|
+
* Phase 3 / spec D6 / R4: when the priority chain falls past `origin/*`
|
|
2386
|
+
* to a local-only ref, the optional `emitEvent` callback (if injected)
|
|
2387
|
+
* is invoked exactly once with `{ kind: 'baseref_fallback', ref, repoRoot }`
|
|
2388
|
+
* so operators are warned when the remote is misconfigured or unreachable.
|
|
2371
2389
|
*/
|
|
2372
2390
|
async resolveBaseRef(repoRoot) {
|
|
2373
2391
|
const configured = this.config.baseRef;
|
|
@@ -2386,11 +2404,30 @@ var WorkspaceManager = class {
|
|
|
2386
2404
|
if (detected) return detected;
|
|
2387
2405
|
} catch {
|
|
2388
2406
|
}
|
|
2389
|
-
for (const candidate of ["origin/main", "origin/master"
|
|
2407
|
+
for (const candidate of ["origin/main", "origin/master"]) {
|
|
2390
2408
|
if (await this.refExists(candidate, repoRoot)) return candidate;
|
|
2391
2409
|
}
|
|
2410
|
+
for (const candidate of ["main", "master"]) {
|
|
2411
|
+
if (await this.refExists(candidate, repoRoot)) {
|
|
2412
|
+
this.emitFallback(candidate, repoRoot);
|
|
2413
|
+
return candidate;
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
this.emitFallback("HEAD", repoRoot);
|
|
2392
2417
|
return "HEAD";
|
|
2393
2418
|
}
|
|
2419
|
+
/**
|
|
2420
|
+
* Phase 3 (D6): emit a `baseref_fallback` event via the injected
|
|
2421
|
+
* callback (if any). Errors from the callback are swallowed so a
|
|
2422
|
+
* broken emitter does not block worktree dispatch.
|
|
2423
|
+
*/
|
|
2424
|
+
emitFallback(ref, repoRoot) {
|
|
2425
|
+
if (!this.emitEvent) return;
|
|
2426
|
+
try {
|
|
2427
|
+
this.emitEvent({ kind: "baseref_fallback", ref, repoRoot });
|
|
2428
|
+
} catch {
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2394
2431
|
/** Returns true iff `git rev-parse --verify` accepts the ref. */
|
|
2395
2432
|
async refExists(ref, repoRoot) {
|
|
2396
2433
|
try {
|
|
@@ -2672,11 +2709,9 @@ var PromptRenderer = class {
|
|
|
2672
2709
|
|
|
2673
2710
|
// src/orchestrator.ts
|
|
2674
2711
|
var import_node_events = require("events");
|
|
2675
|
-
var
|
|
2712
|
+
var path16 = __toESM(require("path"));
|
|
2676
2713
|
var import_node_crypto7 = require("crypto");
|
|
2677
2714
|
var import_core9 = require("@harness-engineering/core");
|
|
2678
|
-
var import_intelligence4 = require("@harness-engineering/intelligence");
|
|
2679
|
-
var import_graph = require("@harness-engineering/graph");
|
|
2680
2715
|
|
|
2681
2716
|
// src/intelligence/pipeline-runner.ts
|
|
2682
2717
|
var path7 = __toESM(require("path"));
|
|
@@ -3239,6 +3274,82 @@ var CompletionHandler = class {
|
|
|
3239
3274
|
// src/orchestrator.ts
|
|
3240
3275
|
var import_core10 = require("@harness-engineering/core");
|
|
3241
3276
|
|
|
3277
|
+
// src/tracker/adapters/github-issues-issue-tracker.ts
|
|
3278
|
+
var import_types9 = require("@harness-engineering/types");
|
|
3279
|
+
var GitHubIssuesIssueTrackerAdapter = class {
|
|
3280
|
+
client;
|
|
3281
|
+
config;
|
|
3282
|
+
constructor(client, config) {
|
|
3283
|
+
this.client = client;
|
|
3284
|
+
this.config = config;
|
|
3285
|
+
}
|
|
3286
|
+
async fetchCandidateIssues() {
|
|
3287
|
+
return this.fetchIssuesByStates(this.config.activeStates);
|
|
3288
|
+
}
|
|
3289
|
+
async fetchIssuesByStates(stateNames) {
|
|
3290
|
+
const r = await this.client.fetchByStatus(
|
|
3291
|
+
stateNames
|
|
3292
|
+
);
|
|
3293
|
+
if (!r.ok) return (0, import_types9.Err)(r.error);
|
|
3294
|
+
return (0, import_types9.Ok)(r.value.map((f) => this.mapTrackedToIssue(f)));
|
|
3295
|
+
}
|
|
3296
|
+
async fetchIssueStatesByIds(issueIds) {
|
|
3297
|
+
const r = await this.client.fetchAll();
|
|
3298
|
+
if (!r.ok) return (0, import_types9.Err)(r.error);
|
|
3299
|
+
const wanted = new Set(issueIds);
|
|
3300
|
+
const out = /* @__PURE__ */ new Map();
|
|
3301
|
+
for (const f of r.value.features) {
|
|
3302
|
+
if (wanted.has(f.externalId)) out.set(f.externalId, this.mapTrackedToIssue(f));
|
|
3303
|
+
}
|
|
3304
|
+
return (0, import_types9.Ok)(out);
|
|
3305
|
+
}
|
|
3306
|
+
async claimIssue(issueId, orchestratorId) {
|
|
3307
|
+
const r = await this.client.claim(issueId, orchestratorId);
|
|
3308
|
+
if (!r.ok) return (0, import_types9.Err)(r.error);
|
|
3309
|
+
return (0, import_types9.Ok)(void 0);
|
|
3310
|
+
}
|
|
3311
|
+
async releaseIssue(issueId) {
|
|
3312
|
+
const r = await this.client.release(issueId);
|
|
3313
|
+
if (!r.ok) return (0, import_types9.Err)(r.error);
|
|
3314
|
+
return (0, import_types9.Ok)(void 0);
|
|
3315
|
+
}
|
|
3316
|
+
async markIssueComplete(issueId) {
|
|
3317
|
+
const r = await this.client.complete(issueId);
|
|
3318
|
+
if (!r.ok) return (0, import_types9.Err)(r.error);
|
|
3319
|
+
return (0, import_types9.Ok)(void 0);
|
|
3320
|
+
}
|
|
3321
|
+
/**
|
|
3322
|
+
* Project a wide-interface `TrackedFeature` onto the small-interface
|
|
3323
|
+
* `Issue` shape consumed by the orchestrator's tick loop.
|
|
3324
|
+
*/
|
|
3325
|
+
mapTrackedToIssue(f) {
|
|
3326
|
+
return {
|
|
3327
|
+
id: f.externalId,
|
|
3328
|
+
identifier: f.externalId,
|
|
3329
|
+
title: f.name,
|
|
3330
|
+
description: f.summary,
|
|
3331
|
+
priority: null,
|
|
3332
|
+
state: f.status,
|
|
3333
|
+
branchName: null,
|
|
3334
|
+
url: null,
|
|
3335
|
+
labels: [],
|
|
3336
|
+
spec: f.spec,
|
|
3337
|
+
plans: f.plans,
|
|
3338
|
+
blockedBy: f.blockedBy.map(
|
|
3339
|
+
(b) => ({
|
|
3340
|
+
id: null,
|
|
3341
|
+
identifier: b,
|
|
3342
|
+
state: null
|
|
3343
|
+
})
|
|
3344
|
+
),
|
|
3345
|
+
createdAt: f.createdAt,
|
|
3346
|
+
updatedAt: f.updatedAt,
|
|
3347
|
+
externalId: f.externalId,
|
|
3348
|
+
assignee: f.assignee
|
|
3349
|
+
};
|
|
3350
|
+
}
|
|
3351
|
+
};
|
|
3352
|
+
|
|
3242
3353
|
// src/agent/runner.ts
|
|
3243
3354
|
var MAX_SLEEP_MS = 12 * 60 * 6e4;
|
|
3244
3355
|
function buildSleepMessage(resetsAtMs, sleepMs, requestedSleepMs, truncated) {
|
|
@@ -3539,9 +3650,17 @@ var LocalModelResolver = class {
|
|
|
3539
3650
|
|
|
3540
3651
|
// src/agent/config-migration.ts
|
|
3541
3652
|
var MIGRATION_GUIDE = "docs/guides/multi-backend-routing.md";
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3653
|
+
var CASE1_ALWAYS_SUPPRESS = /* @__PURE__ */ new Set(["agent.backend"]);
|
|
3654
|
+
var CASE1_LOCAL_GROUP = /* @__PURE__ */ new Set([
|
|
3655
|
+
"agent.localBackend",
|
|
3656
|
+
"agent.localEndpoint",
|
|
3657
|
+
"agent.localModel",
|
|
3658
|
+
"agent.localApiKey",
|
|
3659
|
+
"agent.localTimeoutMs",
|
|
3660
|
+
"agent.localProbeIntervalMs"
|
|
3661
|
+
]);
|
|
3662
|
+
function detectLegacyFields(agent) {
|
|
3663
|
+
const fields = [
|
|
3545
3664
|
{ path: "agent.backend", present: agent.backend !== void 0 && agent.backend !== "" },
|
|
3546
3665
|
{ path: "agent.command", present: agent.command !== void 0 },
|
|
3547
3666
|
{ path: "agent.model", present: agent.model !== void 0 },
|
|
@@ -3553,56 +3672,73 @@ function migrateAgentConfig(agent) {
|
|
|
3553
3672
|
{ path: "agent.localTimeoutMs", present: agent.localTimeoutMs !== void 0 },
|
|
3554
3673
|
{ path: "agent.localProbeIntervalMs", present: agent.localProbeIntervalMs !== void 0 }
|
|
3555
3674
|
];
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
const suppressLocalGroup = agent.localBackend !== void 0;
|
|
3567
|
-
if (agent.backends !== void 0) {
|
|
3568
|
-
for (const path16 of presentLegacy) {
|
|
3569
|
-
if (CASE1_ALWAYS_SUPPRESS.has(path16)) continue;
|
|
3570
|
-
if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path16)) continue;
|
|
3571
|
-
warnings.push(
|
|
3572
|
-
`Ignoring legacy field '${path16}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
|
|
3573
|
-
);
|
|
3574
|
-
}
|
|
3575
|
-
return { config: agent, warnings };
|
|
3576
|
-
}
|
|
3577
|
-
if (presentLegacy.length === 0) {
|
|
3578
|
-
return { config: agent, warnings };
|
|
3675
|
+
return fields.filter((f) => f.present).map((f) => f.path);
|
|
3676
|
+
}
|
|
3677
|
+
function buildCase1Warnings(presentLegacy, suppressLocalGroup) {
|
|
3678
|
+
const warnings = [];
|
|
3679
|
+
for (const path17 of presentLegacy) {
|
|
3680
|
+
if (CASE1_ALWAYS_SUPPRESS.has(path17)) continue;
|
|
3681
|
+
if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path17)) continue;
|
|
3682
|
+
warnings.push(
|
|
3683
|
+
`Ignoring legacy field '${path17}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
|
|
3684
|
+
);
|
|
3579
3685
|
}
|
|
3580
|
-
|
|
3686
|
+
return warnings;
|
|
3687
|
+
}
|
|
3688
|
+
function synthesizeBackendsAndRouting(agent) {
|
|
3689
|
+
const backends = { primary: synthesizePrimary(agent) };
|
|
3581
3690
|
const routing = { default: "primary" };
|
|
3582
|
-
backends.primary = synthesizePrimary(agent);
|
|
3583
3691
|
if (agent.localBackend !== void 0) {
|
|
3584
3692
|
backends.local = synthesizeLocal(agent);
|
|
3693
|
+
const autoExec = agent.escalation?.autoExecute ?? [];
|
|
3694
|
+
for (const tier of autoExec) routing[tier] = "local";
|
|
3585
3695
|
}
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3696
|
+
return { backends, routing };
|
|
3697
|
+
}
|
|
3698
|
+
function migrateAgentConfig(agent) {
|
|
3699
|
+
const presentLegacy = detectLegacyFields(agent);
|
|
3700
|
+
if (agent.backends !== void 0) {
|
|
3701
|
+
return {
|
|
3702
|
+
config: agent,
|
|
3703
|
+
warnings: buildCase1Warnings(presentLegacy, agent.localBackend !== void 0)
|
|
3704
|
+
};
|
|
3591
3705
|
}
|
|
3592
|
-
|
|
3593
|
-
warnings
|
|
3594
|
-
`Deprecated config field '${path16}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
|
|
3595
|
-
);
|
|
3706
|
+
if (presentLegacy.length === 0) {
|
|
3707
|
+
return { config: agent, warnings: [] };
|
|
3596
3708
|
}
|
|
3709
|
+
const { backends, routing } = synthesizeBackendsAndRouting(agent);
|
|
3710
|
+
const warnings = presentLegacy.map(
|
|
3711
|
+
(path17) => `Deprecated config field '${path17}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
|
|
3712
|
+
);
|
|
3597
3713
|
return {
|
|
3598
|
-
config: {
|
|
3599
|
-
...agent,
|
|
3600
|
-
backends,
|
|
3601
|
-
routing
|
|
3602
|
-
},
|
|
3714
|
+
config: { ...agent, backends, routing },
|
|
3603
3715
|
warnings
|
|
3604
3716
|
};
|
|
3605
3717
|
}
|
|
3718
|
+
function buildRemoteBackend(type, agent, context) {
|
|
3719
|
+
if (agent.model === void 0) {
|
|
3720
|
+
throw new Error(`migrateAgentConfig: ${context} requires agent.model`);
|
|
3721
|
+
}
|
|
3722
|
+
const def = { type, model: agent.model };
|
|
3723
|
+
if (agent.apiKey !== void 0) def.apiKey = agent.apiKey;
|
|
3724
|
+
return def;
|
|
3725
|
+
}
|
|
3726
|
+
function buildLocalConnectionBackend(type, agent, context) {
|
|
3727
|
+
if (agent.localEndpoint === void 0 || agent.localModel === void 0) {
|
|
3728
|
+
throw new Error(
|
|
3729
|
+
`migrateAgentConfig: ${context} requires agent.localEndpoint and agent.localModel`
|
|
3730
|
+
);
|
|
3731
|
+
}
|
|
3732
|
+
const def = {
|
|
3733
|
+
type,
|
|
3734
|
+
endpoint: agent.localEndpoint,
|
|
3735
|
+
model: agent.localModel
|
|
3736
|
+
};
|
|
3737
|
+
if (agent.localApiKey !== void 0) def.apiKey = agent.localApiKey;
|
|
3738
|
+
if (agent.localTimeoutMs !== void 0) def.timeoutMs = agent.localTimeoutMs;
|
|
3739
|
+
if (agent.localProbeIntervalMs !== void 0) def.probeIntervalMs = agent.localProbeIntervalMs;
|
|
3740
|
+
return def;
|
|
3741
|
+
}
|
|
3606
3742
|
function synthesizePrimary(agent) {
|
|
3607
3743
|
const backend = agent.backend;
|
|
3608
3744
|
switch (backend) {
|
|
@@ -3613,64 +3749,13 @@ function synthesizePrimary(agent) {
|
|
|
3613
3749
|
if (agent.command !== void 0) def.command = agent.command;
|
|
3614
3750
|
return def;
|
|
3615
3751
|
}
|
|
3616
|
-
case "anthropic":
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
}
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
return
|
|
3623
|
-
}
|
|
3624
|
-
case "openai": {
|
|
3625
|
-
if (agent.model === void 0) {
|
|
3626
|
-
throw new Error("migrateAgentConfig: agent.backend='openai' requires agent.model");
|
|
3627
|
-
}
|
|
3628
|
-
const def = { type: "openai", model: agent.model };
|
|
3629
|
-
if (agent.apiKey !== void 0) def.apiKey = agent.apiKey;
|
|
3630
|
-
return def;
|
|
3631
|
-
}
|
|
3632
|
-
case "gemini": {
|
|
3633
|
-
if (agent.model === void 0) {
|
|
3634
|
-
throw new Error("migrateAgentConfig: agent.backend='gemini' requires agent.model");
|
|
3635
|
-
}
|
|
3636
|
-
const def = { type: "gemini", model: agent.model };
|
|
3637
|
-
if (agent.apiKey !== void 0) def.apiKey = agent.apiKey;
|
|
3638
|
-
return def;
|
|
3639
|
-
}
|
|
3640
|
-
case "local": {
|
|
3641
|
-
if (agent.localEndpoint === void 0 || agent.localModel === void 0) {
|
|
3642
|
-
throw new Error(
|
|
3643
|
-
"migrateAgentConfig: agent.backend='local' requires agent.localEndpoint and agent.localModel"
|
|
3644
|
-
);
|
|
3645
|
-
}
|
|
3646
|
-
const def = {
|
|
3647
|
-
type: "local",
|
|
3648
|
-
endpoint: agent.localEndpoint,
|
|
3649
|
-
model: agent.localModel
|
|
3650
|
-
};
|
|
3651
|
-
if (agent.localApiKey !== void 0) def.apiKey = agent.localApiKey;
|
|
3652
|
-
if (agent.localTimeoutMs !== void 0) def.timeoutMs = agent.localTimeoutMs;
|
|
3653
|
-
if (agent.localProbeIntervalMs !== void 0)
|
|
3654
|
-
def.probeIntervalMs = agent.localProbeIntervalMs;
|
|
3655
|
-
return def;
|
|
3656
|
-
}
|
|
3657
|
-
case "pi": {
|
|
3658
|
-
if (agent.localEndpoint === void 0 || agent.localModel === void 0) {
|
|
3659
|
-
throw new Error(
|
|
3660
|
-
"migrateAgentConfig: agent.backend='pi' requires agent.localEndpoint and agent.localModel"
|
|
3661
|
-
);
|
|
3662
|
-
}
|
|
3663
|
-
const def = {
|
|
3664
|
-
type: "pi",
|
|
3665
|
-
endpoint: agent.localEndpoint,
|
|
3666
|
-
model: agent.localModel
|
|
3667
|
-
};
|
|
3668
|
-
if (agent.localApiKey !== void 0) def.apiKey = agent.localApiKey;
|
|
3669
|
-
if (agent.localTimeoutMs !== void 0) def.timeoutMs = agent.localTimeoutMs;
|
|
3670
|
-
if (agent.localProbeIntervalMs !== void 0)
|
|
3671
|
-
def.probeIntervalMs = agent.localProbeIntervalMs;
|
|
3672
|
-
return def;
|
|
3673
|
-
}
|
|
3752
|
+
case "anthropic":
|
|
3753
|
+
case "openai":
|
|
3754
|
+
case "gemini":
|
|
3755
|
+
return buildRemoteBackend(backend, agent, `agent.backend='${backend}'`);
|
|
3756
|
+
case "local":
|
|
3757
|
+
case "pi":
|
|
3758
|
+
return buildLocalConnectionBackend(backend, agent, `agent.backend='${backend}'`);
|
|
3674
3759
|
default:
|
|
3675
3760
|
throw new Error(
|
|
3676
3761
|
`migrateAgentConfig: unknown legacy backend '${String(backend)}'. Expected one of: mock, claude, anthropic, openai, gemini, local, pi.`
|
|
@@ -3681,31 +3766,8 @@ function synthesizeLocal(agent) {
|
|
|
3681
3766
|
if (agent.localBackend === void 0) {
|
|
3682
3767
|
throw new Error("synthesizeLocal called without agent.localBackend");
|
|
3683
3768
|
}
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
"migrateAgentConfig: agent.localBackend requires agent.localEndpoint and agent.localModel"
|
|
3687
|
-
);
|
|
3688
|
-
}
|
|
3689
|
-
if (agent.localBackend === "pi") {
|
|
3690
|
-
const def2 = {
|
|
3691
|
-
type: "pi",
|
|
3692
|
-
endpoint: agent.localEndpoint,
|
|
3693
|
-
model: agent.localModel
|
|
3694
|
-
};
|
|
3695
|
-
if (agent.localApiKey !== void 0) def2.apiKey = agent.localApiKey;
|
|
3696
|
-
if (agent.localTimeoutMs !== void 0) def2.timeoutMs = agent.localTimeoutMs;
|
|
3697
|
-
if (agent.localProbeIntervalMs !== void 0) def2.probeIntervalMs = agent.localProbeIntervalMs;
|
|
3698
|
-
return def2;
|
|
3699
|
-
}
|
|
3700
|
-
const def = {
|
|
3701
|
-
type: "local",
|
|
3702
|
-
endpoint: agent.localEndpoint,
|
|
3703
|
-
model: agent.localModel
|
|
3704
|
-
};
|
|
3705
|
-
if (agent.localApiKey !== void 0) def.apiKey = agent.localApiKey;
|
|
3706
|
-
if (agent.localTimeoutMs !== void 0) def.timeoutMs = agent.localTimeoutMs;
|
|
3707
|
-
if (agent.localProbeIntervalMs !== void 0) def.probeIntervalMs = agent.localProbeIntervalMs;
|
|
3708
|
-
return def;
|
|
3769
|
+
const type = agent.localBackend === "pi" ? "pi" : "local";
|
|
3770
|
+
return buildLocalConnectionBackend(type, agent, "agent.localBackend");
|
|
3709
3771
|
}
|
|
3710
3772
|
|
|
3711
3773
|
// src/agent/backend-router.ts
|
|
@@ -3758,8 +3820,8 @@ var BackendRouter = class {
|
|
|
3758
3820
|
validateReferences() {
|
|
3759
3821
|
const known = new Set(Object.keys(this.backends));
|
|
3760
3822
|
const missing = [];
|
|
3761
|
-
const check = (
|
|
3762
|
-
if (name !== void 0 && !known.has(name)) missing.push({ path:
|
|
3823
|
+
const check = (path17, name) => {
|
|
3824
|
+
if (name !== void 0 && !known.has(name)) missing.push({ path: path17, name });
|
|
3763
3825
|
};
|
|
3764
3826
|
check("default", this.routing.default);
|
|
3765
3827
|
check("quick-fix", this.routing["quick-fix"]);
|
|
@@ -3769,7 +3831,7 @@ var BackendRouter = class {
|
|
|
3769
3831
|
check("intelligence.sel", this.routing.intelligence?.sel);
|
|
3770
3832
|
check("intelligence.pesl", this.routing.intelligence?.pesl);
|
|
3771
3833
|
if (missing.length > 0) {
|
|
3772
|
-
const detail = missing.map(({ path:
|
|
3834
|
+
const detail = missing.map(({ path: path17, name }) => `routing.${path17} -> '${name}'`).join("; ");
|
|
3773
3835
|
const known_ = [...known].join(", ") || "(none)";
|
|
3774
3836
|
throw new Error(
|
|
3775
3837
|
`BackendRouter: routing references unknown backend(s): ${detail}. Defined backends: [${known_}].`
|
|
@@ -3782,13 +3844,13 @@ var BackendRouter = class {
|
|
|
3782
3844
|
var import_node_child_process4 = require("child_process");
|
|
3783
3845
|
var readline = __toESM(require("readline"));
|
|
3784
3846
|
var import_node_crypto3 = require("crypto");
|
|
3785
|
-
var
|
|
3847
|
+
var import_types10 = require("@harness-engineering/types");
|
|
3786
3848
|
function resolveExitCode(code, command, resolve6) {
|
|
3787
3849
|
if (code === 0) {
|
|
3788
|
-
resolve6((0,
|
|
3850
|
+
resolve6((0, import_types10.Ok)(void 0));
|
|
3789
3851
|
} else {
|
|
3790
3852
|
resolve6(
|
|
3791
|
-
(0,
|
|
3853
|
+
(0, import_types10.Err)({
|
|
3792
3854
|
category: "agent_not_found",
|
|
3793
3855
|
message: `Claude command '${command}' not found or failed`
|
|
3794
3856
|
})
|
|
@@ -3796,7 +3858,7 @@ function resolveExitCode(code, command, resolve6) {
|
|
|
3796
3858
|
}
|
|
3797
3859
|
}
|
|
3798
3860
|
function resolveSpawnError(command, resolve6) {
|
|
3799
|
-
resolve6((0,
|
|
3861
|
+
resolve6((0, import_types10.Err)({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
|
|
3800
3862
|
}
|
|
3801
3863
|
var JUST_PAST_GRACE_MS = 5 * 6e4;
|
|
3802
3864
|
var PRIMARY_LIMIT_RE = /You[\u2019']ve hit your limit.*resets\s+(\d{1,2}(?::\d{2})?\s*(?:am|pm))\s*\(([^)]+)\)/i;
|
|
@@ -3995,7 +4057,7 @@ var ClaudeBackend = class {
|
|
|
3995
4057
|
backendName: this.name,
|
|
3996
4058
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3997
4059
|
};
|
|
3998
|
-
return (0,
|
|
4060
|
+
return (0, import_types10.Ok)(session);
|
|
3999
4061
|
}
|
|
4000
4062
|
async *runTurn(session, params) {
|
|
4001
4063
|
const args = [
|
|
@@ -4119,7 +4181,7 @@ var ClaudeBackend = class {
|
|
|
4119
4181
|
};
|
|
4120
4182
|
}
|
|
4121
4183
|
async stopSession(_session) {
|
|
4122
|
-
return (0,
|
|
4184
|
+
return (0, import_types10.Ok)(void 0);
|
|
4123
4185
|
}
|
|
4124
4186
|
async healthCheck() {
|
|
4125
4187
|
return new Promise((resolve6) => {
|
|
@@ -4132,7 +4194,7 @@ var ClaudeBackend = class {
|
|
|
4132
4194
|
|
|
4133
4195
|
// src/agent/backends/anthropic.ts
|
|
4134
4196
|
var import_sdk = __toESM(require("@anthropic-ai/sdk"));
|
|
4135
|
-
var
|
|
4197
|
+
var import_types11 = require("@harness-engineering/types");
|
|
4136
4198
|
var import_core4 = require("@harness-engineering/core");
|
|
4137
4199
|
var AnthropicBackend = class {
|
|
4138
4200
|
name = "anthropic";
|
|
@@ -4150,7 +4212,7 @@ var AnthropicBackend = class {
|
|
|
4150
4212
|
}
|
|
4151
4213
|
async startSession(params) {
|
|
4152
4214
|
if (!this.config.apiKey) {
|
|
4153
|
-
return (0,
|
|
4215
|
+
return (0, import_types11.Err)({
|
|
4154
4216
|
category: "agent_not_found",
|
|
4155
4217
|
message: "ANTHROPIC_API_KEY is not set"
|
|
4156
4218
|
});
|
|
@@ -4162,7 +4224,7 @@ var AnthropicBackend = class {
|
|
|
4162
4224
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4163
4225
|
...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
|
|
4164
4226
|
};
|
|
4165
|
-
return (0,
|
|
4227
|
+
return (0, import_types11.Ok)(session);
|
|
4166
4228
|
}
|
|
4167
4229
|
async *runTurn(session, params) {
|
|
4168
4230
|
const anthropicSession = session;
|
|
@@ -4227,22 +4289,22 @@ var AnthropicBackend = class {
|
|
|
4227
4289
|
}
|
|
4228
4290
|
}
|
|
4229
4291
|
async stopSession(_session) {
|
|
4230
|
-
return (0,
|
|
4292
|
+
return (0, import_types11.Ok)(void 0);
|
|
4231
4293
|
}
|
|
4232
4294
|
async healthCheck() {
|
|
4233
4295
|
if (!this.config.apiKey) {
|
|
4234
|
-
return (0,
|
|
4296
|
+
return (0, import_types11.Err)({
|
|
4235
4297
|
category: "response_error",
|
|
4236
4298
|
message: "ANTHROPIC_API_KEY is not set"
|
|
4237
4299
|
});
|
|
4238
4300
|
}
|
|
4239
|
-
return (0,
|
|
4301
|
+
return (0, import_types11.Ok)(void 0);
|
|
4240
4302
|
}
|
|
4241
4303
|
};
|
|
4242
4304
|
|
|
4243
4305
|
// src/agent/backends/openai.ts
|
|
4244
4306
|
var import_openai = __toESM(require("openai"));
|
|
4245
|
-
var
|
|
4307
|
+
var import_types12 = require("@harness-engineering/types");
|
|
4246
4308
|
var import_core5 = require("@harness-engineering/core");
|
|
4247
4309
|
var OpenAIBackend = class {
|
|
4248
4310
|
name = "openai";
|
|
@@ -4259,7 +4321,7 @@ var OpenAIBackend = class {
|
|
|
4259
4321
|
}
|
|
4260
4322
|
async startSession(params) {
|
|
4261
4323
|
if (!this.config.apiKey) {
|
|
4262
|
-
return (0,
|
|
4324
|
+
return (0, import_types12.Err)({
|
|
4263
4325
|
category: "agent_not_found",
|
|
4264
4326
|
message: "OPENAI_API_KEY is not set"
|
|
4265
4327
|
});
|
|
@@ -4271,7 +4333,7 @@ var OpenAIBackend = class {
|
|
|
4271
4333
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4272
4334
|
...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
|
|
4273
4335
|
};
|
|
4274
|
-
return (0,
|
|
4336
|
+
return (0, import_types12.Ok)(session);
|
|
4275
4337
|
}
|
|
4276
4338
|
async *runTurn(session, params) {
|
|
4277
4339
|
const openAISession = session;
|
|
@@ -4347,14 +4409,14 @@ var OpenAIBackend = class {
|
|
|
4347
4409
|
};
|
|
4348
4410
|
}
|
|
4349
4411
|
async stopSession(_session) {
|
|
4350
|
-
return (0,
|
|
4412
|
+
return (0, import_types12.Ok)(void 0);
|
|
4351
4413
|
}
|
|
4352
4414
|
async healthCheck() {
|
|
4353
4415
|
try {
|
|
4354
4416
|
await this.client.models.list();
|
|
4355
|
-
return (0,
|
|
4417
|
+
return (0, import_types12.Ok)(void 0);
|
|
4356
4418
|
} catch (err) {
|
|
4357
|
-
return (0,
|
|
4419
|
+
return (0, import_types12.Err)({
|
|
4358
4420
|
category: "response_error",
|
|
4359
4421
|
message: err instanceof Error ? err.message : "OpenAI health check failed"
|
|
4360
4422
|
});
|
|
@@ -4364,7 +4426,7 @@ var OpenAIBackend = class {
|
|
|
4364
4426
|
|
|
4365
4427
|
// src/agent/backends/gemini.ts
|
|
4366
4428
|
var import_generative_ai = require("@google/generative-ai");
|
|
4367
|
-
var
|
|
4429
|
+
var import_types13 = require("@harness-engineering/types");
|
|
4368
4430
|
var import_core6 = require("@harness-engineering/core");
|
|
4369
4431
|
var GeminiBackend = class {
|
|
4370
4432
|
name = "gemini";
|
|
@@ -4379,7 +4441,7 @@ var GeminiBackend = class {
|
|
|
4379
4441
|
}
|
|
4380
4442
|
async startSession(params) {
|
|
4381
4443
|
if (!this.config.apiKey) {
|
|
4382
|
-
return (0,
|
|
4444
|
+
return (0, import_types13.Err)({
|
|
4383
4445
|
category: "agent_not_found",
|
|
4384
4446
|
message: "GEMINI_API_KEY is not set"
|
|
4385
4447
|
});
|
|
@@ -4391,7 +4453,7 @@ var GeminiBackend = class {
|
|
|
4391
4453
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4392
4454
|
...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
|
|
4393
4455
|
};
|
|
4394
|
-
return (0,
|
|
4456
|
+
return (0, import_types13.Ok)(session);
|
|
4395
4457
|
}
|
|
4396
4458
|
async *runTurn(session, params) {
|
|
4397
4459
|
const geminiSession = session;
|
|
@@ -4464,15 +4526,15 @@ var GeminiBackend = class {
|
|
|
4464
4526
|
};
|
|
4465
4527
|
}
|
|
4466
4528
|
async stopSession(_session) {
|
|
4467
|
-
return (0,
|
|
4529
|
+
return (0, import_types13.Ok)(void 0);
|
|
4468
4530
|
}
|
|
4469
4531
|
async healthCheck() {
|
|
4470
4532
|
try {
|
|
4471
4533
|
const genAI = new import_generative_ai.GoogleGenerativeAI(this.config.apiKey);
|
|
4472
4534
|
genAI.getGenerativeModel({ model: this.config.model });
|
|
4473
|
-
return (0,
|
|
4535
|
+
return (0, import_types13.Ok)(void 0);
|
|
4474
4536
|
} catch (err) {
|
|
4475
|
-
return (0,
|
|
4537
|
+
return (0, import_types13.Err)({
|
|
4476
4538
|
category: "response_error",
|
|
4477
4539
|
message: err instanceof Error ? err.message : "Gemini health check failed"
|
|
4478
4540
|
});
|
|
@@ -4482,7 +4544,7 @@ var GeminiBackend = class {
|
|
|
4482
4544
|
|
|
4483
4545
|
// src/agent/backends/local.ts
|
|
4484
4546
|
var import_openai2 = __toESM(require("openai"));
|
|
4485
|
-
var
|
|
4547
|
+
var import_types14 = require("@harness-engineering/types");
|
|
4486
4548
|
var DEFAULT_TIMEOUT_MS = 9e4;
|
|
4487
4549
|
var LocalBackend = class {
|
|
4488
4550
|
name = "local";
|
|
@@ -4508,7 +4570,7 @@ var LocalBackend = class {
|
|
|
4508
4570
|
if (this.getModel) {
|
|
4509
4571
|
const candidate = this.getModel();
|
|
4510
4572
|
if (candidate === null) {
|
|
4511
|
-
return (0,
|
|
4573
|
+
return (0, import_types14.Err)({
|
|
4512
4574
|
category: "agent_not_found",
|
|
4513
4575
|
message: "No local model available; check dashboard for details."
|
|
4514
4576
|
});
|
|
@@ -4525,7 +4587,7 @@ var LocalBackend = class {
|
|
|
4525
4587
|
resolvedModel,
|
|
4526
4588
|
...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
|
|
4527
4589
|
};
|
|
4528
|
-
return (0,
|
|
4590
|
+
return (0, import_types14.Ok)(session);
|
|
4529
4591
|
}
|
|
4530
4592
|
async *runTurn(session, params) {
|
|
4531
4593
|
const localSession = session;
|
|
@@ -4590,14 +4652,14 @@ var LocalBackend = class {
|
|
|
4590
4652
|
};
|
|
4591
4653
|
}
|
|
4592
4654
|
async stopSession(_session) {
|
|
4593
|
-
return (0,
|
|
4655
|
+
return (0, import_types14.Ok)(void 0);
|
|
4594
4656
|
}
|
|
4595
4657
|
async healthCheck() {
|
|
4596
4658
|
try {
|
|
4597
4659
|
await this.client.models.list();
|
|
4598
|
-
return (0,
|
|
4660
|
+
return (0, import_types14.Ok)(void 0);
|
|
4599
4661
|
} catch (err) {
|
|
4600
|
-
return (0,
|
|
4662
|
+
return (0, import_types14.Err)({
|
|
4601
4663
|
category: "response_error",
|
|
4602
4664
|
message: err instanceof Error ? err.message : "Local backend health check failed"
|
|
4603
4665
|
});
|
|
@@ -4607,7 +4669,7 @@ var LocalBackend = class {
|
|
|
4607
4669
|
|
|
4608
4670
|
// src/agent/backends/pi.ts
|
|
4609
4671
|
var import_node_crypto4 = require("crypto");
|
|
4610
|
-
var
|
|
4672
|
+
var import_types15 = require("@harness-engineering/types");
|
|
4611
4673
|
var SILENT_EVENTS = /* @__PURE__ */ new Set([
|
|
4612
4674
|
"turn_end",
|
|
4613
4675
|
"message_start",
|
|
@@ -4724,7 +4786,7 @@ var PiBackend = class {
|
|
|
4724
4786
|
if (this.config.getModel) {
|
|
4725
4787
|
const candidate = this.config.getModel();
|
|
4726
4788
|
if (candidate === null) {
|
|
4727
|
-
return (0,
|
|
4789
|
+
return (0, import_types15.Err)({
|
|
4728
4790
|
category: "agent_not_found",
|
|
4729
4791
|
message: "No local model available; check dashboard for details."
|
|
4730
4792
|
});
|
|
@@ -4753,9 +4815,9 @@ var PiBackend = class {
|
|
|
4753
4815
|
piSession,
|
|
4754
4816
|
unsubscribe: null
|
|
4755
4817
|
};
|
|
4756
|
-
return (0,
|
|
4818
|
+
return (0, import_types15.Ok)(session);
|
|
4757
4819
|
} catch (err) {
|
|
4758
|
-
return (0,
|
|
4820
|
+
return (0, import_types15.Err)({
|
|
4759
4821
|
category: "response_error",
|
|
4760
4822
|
message: `Failed to create pi session: ${err instanceof Error ? err.message : String(err)}`
|
|
4761
4823
|
});
|
|
@@ -4885,14 +4947,14 @@ var PiBackend = class {
|
|
|
4885
4947
|
await piSession.abort();
|
|
4886
4948
|
} catch {
|
|
4887
4949
|
}
|
|
4888
|
-
return (0,
|
|
4950
|
+
return (0, import_types15.Ok)(void 0);
|
|
4889
4951
|
}
|
|
4890
4952
|
async healthCheck() {
|
|
4891
4953
|
try {
|
|
4892
4954
|
await import("@mariozechner/pi-coding-agent");
|
|
4893
|
-
return (0,
|
|
4955
|
+
return (0, import_types15.Ok)(void 0);
|
|
4894
4956
|
} catch (err) {
|
|
4895
|
-
return (0,
|
|
4957
|
+
return (0, import_types15.Err)({
|
|
4896
4958
|
category: "agent_not_found",
|
|
4897
4959
|
message: `Pi SDK not available: ${err instanceof Error ? err.message : String(err)}`
|
|
4898
4960
|
});
|
|
@@ -4955,7 +5017,7 @@ function createBackend(def) {
|
|
|
4955
5017
|
}
|
|
4956
5018
|
|
|
4957
5019
|
// src/agent/backends/container.ts
|
|
4958
|
-
var
|
|
5020
|
+
var import_types16 = require("@harness-engineering/types");
|
|
4959
5021
|
function toAgentError(message, details) {
|
|
4960
5022
|
return { category: "response_error", message, details };
|
|
4961
5023
|
}
|
|
@@ -4986,7 +5048,7 @@ var ContainerBackend = class {
|
|
|
4986
5048
|
}
|
|
4987
5049
|
const result = await this.secretBackend.resolveSecrets(this.secretKeys);
|
|
4988
5050
|
if (!result.ok) {
|
|
4989
|
-
return (0,
|
|
5051
|
+
return (0, import_types16.Err)(toAgentError(`Secret resolution failed: ${result.error.message}`, result.error));
|
|
4990
5052
|
}
|
|
4991
5053
|
return { ok: true, value: result.value };
|
|
4992
5054
|
}
|
|
@@ -5011,7 +5073,7 @@ var ContainerBackend = class {
|
|
|
5011
5073
|
const createOpts = this.buildCreateOpts(params, envResult.value);
|
|
5012
5074
|
const containerResult = await this.runtime.createContainer(createOpts);
|
|
5013
5075
|
if (!containerResult.ok) {
|
|
5014
|
-
return (0,
|
|
5076
|
+
return (0, import_types16.Err)(
|
|
5015
5077
|
toAgentError(
|
|
5016
5078
|
`Container creation failed: ${containerResult.error.message}`,
|
|
5017
5079
|
containerResult.error
|
|
@@ -5036,7 +5098,7 @@ var ContainerBackend = class {
|
|
|
5036
5098
|
this.containerHandles.delete(session.sessionId);
|
|
5037
5099
|
const removeResult = await this.runtime.removeContainer(handle);
|
|
5038
5100
|
if (!removeResult.ok) {
|
|
5039
|
-
return (0,
|
|
5101
|
+
return (0, import_types16.Err)(
|
|
5040
5102
|
toAgentError(
|
|
5041
5103
|
`Container removal failed: ${removeResult.error.message}`,
|
|
5042
5104
|
removeResult.error
|
|
@@ -5049,7 +5111,7 @@ var ContainerBackend = class {
|
|
|
5049
5111
|
async healthCheck() {
|
|
5050
5112
|
const runtimeResult = await this.runtime.healthCheck();
|
|
5051
5113
|
if (!runtimeResult.ok) {
|
|
5052
|
-
return (0,
|
|
5114
|
+
return (0, import_types16.Err)({
|
|
5053
5115
|
category: "agent_not_found",
|
|
5054
5116
|
message: `Container runtime unhealthy: ${runtimeResult.error.message}`,
|
|
5055
5117
|
details: runtimeResult.error
|
|
@@ -5061,7 +5123,7 @@ var ContainerBackend = class {
|
|
|
5061
5123
|
|
|
5062
5124
|
// src/agent/runtime/docker.ts
|
|
5063
5125
|
var import_node_child_process5 = require("child_process");
|
|
5064
|
-
var
|
|
5126
|
+
var import_types17 = require("@harness-engineering/types");
|
|
5065
5127
|
function dockerExec(args) {
|
|
5066
5128
|
return new Promise((resolve6, reject) => {
|
|
5067
5129
|
(0, import_node_child_process5.execFile)("docker", args, (error, stdout) => {
|
|
@@ -5094,9 +5156,9 @@ var DockerRuntime = class {
|
|
|
5094
5156
|
args.push(opts.image);
|
|
5095
5157
|
args.push("sleep", "infinity");
|
|
5096
5158
|
const containerId = await dockerExec(args);
|
|
5097
|
-
return (0,
|
|
5159
|
+
return (0, import_types17.Ok)({ containerId, runtime: this.name });
|
|
5098
5160
|
} catch (error) {
|
|
5099
|
-
return (0,
|
|
5161
|
+
return (0, import_types17.Err)({
|
|
5100
5162
|
category: "container_create_failed",
|
|
5101
5163
|
message: `Failed to create container: ${error instanceof Error ? error.message : String(error)}`,
|
|
5102
5164
|
details: error
|
|
@@ -5140,9 +5202,9 @@ var DockerRuntime = class {
|
|
|
5140
5202
|
async removeContainer(handle) {
|
|
5141
5203
|
try {
|
|
5142
5204
|
await dockerExec(["rm", "-f", handle.containerId]);
|
|
5143
|
-
return (0,
|
|
5205
|
+
return (0, import_types17.Ok)(void 0);
|
|
5144
5206
|
} catch (error) {
|
|
5145
|
-
return (0,
|
|
5207
|
+
return (0, import_types17.Err)({
|
|
5146
5208
|
category: "container_remove_failed",
|
|
5147
5209
|
message: `Failed to remove container: ${error instanceof Error ? error.message : String(error)}`,
|
|
5148
5210
|
details: error
|
|
@@ -5152,9 +5214,9 @@ var DockerRuntime = class {
|
|
|
5152
5214
|
async healthCheck() {
|
|
5153
5215
|
try {
|
|
5154
5216
|
await dockerExec(["info", "--format", "{{.ServerVersion}}"]);
|
|
5155
|
-
return (0,
|
|
5217
|
+
return (0, import_types17.Ok)(void 0);
|
|
5156
5218
|
} catch (error) {
|
|
5157
|
-
return (0,
|
|
5219
|
+
return (0, import_types17.Err)({
|
|
5158
5220
|
category: "runtime_not_found",
|
|
5159
5221
|
message: `Docker is not available: ${error instanceof Error ? error.message : String(error)}`,
|
|
5160
5222
|
details: error
|
|
@@ -5164,7 +5226,7 @@ var DockerRuntime = class {
|
|
|
5164
5226
|
};
|
|
5165
5227
|
|
|
5166
5228
|
// src/agent/secrets/env.ts
|
|
5167
|
-
var
|
|
5229
|
+
var import_types18 = require("@harness-engineering/types");
|
|
5168
5230
|
var EnvSecretBackend = class {
|
|
5169
5231
|
name = "env";
|
|
5170
5232
|
async resolveSecrets(keys) {
|
|
@@ -5172,7 +5234,7 @@ var EnvSecretBackend = class {
|
|
|
5172
5234
|
for (const key of keys) {
|
|
5173
5235
|
const value = process.env[key];
|
|
5174
5236
|
if (value === void 0) {
|
|
5175
|
-
return (0,
|
|
5237
|
+
return (0, import_types18.Err)({
|
|
5176
5238
|
category: "secret_not_found",
|
|
5177
5239
|
message: `Environment variable '${key}' is not set`,
|
|
5178
5240
|
key
|
|
@@ -5180,16 +5242,16 @@ var EnvSecretBackend = class {
|
|
|
5180
5242
|
}
|
|
5181
5243
|
secrets[key] = value;
|
|
5182
5244
|
}
|
|
5183
|
-
return (0,
|
|
5245
|
+
return (0, import_types18.Ok)(secrets);
|
|
5184
5246
|
}
|
|
5185
5247
|
async healthCheck() {
|
|
5186
|
-
return (0,
|
|
5248
|
+
return (0, import_types18.Ok)(void 0);
|
|
5187
5249
|
}
|
|
5188
5250
|
};
|
|
5189
5251
|
|
|
5190
5252
|
// src/agent/secrets/onepassword.ts
|
|
5191
5253
|
var import_node_child_process6 = require("child_process");
|
|
5192
|
-
var
|
|
5254
|
+
var import_types19 = require("@harness-engineering/types");
|
|
5193
5255
|
function opExec(args) {
|
|
5194
5256
|
return new Promise((resolve6, reject) => {
|
|
5195
5257
|
(0, import_node_child_process6.execFile)("op", args, (error, stdout) => {
|
|
@@ -5214,21 +5276,21 @@ var OnePasswordSecretBackend = class {
|
|
|
5214
5276
|
const value = await opExec(["read", `op://${this.vault}/${key}/password`]);
|
|
5215
5277
|
secrets[key] = value;
|
|
5216
5278
|
} catch (error) {
|
|
5217
|
-
return (0,
|
|
5279
|
+
return (0, import_types19.Err)({
|
|
5218
5280
|
category: "access_denied",
|
|
5219
5281
|
message: `Failed to read secret '${key}' from 1Password: ${error instanceof Error ? error.message : String(error)}`,
|
|
5220
5282
|
key
|
|
5221
5283
|
});
|
|
5222
5284
|
}
|
|
5223
5285
|
}
|
|
5224
|
-
return (0,
|
|
5286
|
+
return (0, import_types19.Ok)(secrets);
|
|
5225
5287
|
}
|
|
5226
5288
|
async healthCheck() {
|
|
5227
5289
|
try {
|
|
5228
5290
|
await opExec(["--version"]);
|
|
5229
|
-
return (0,
|
|
5291
|
+
return (0, import_types19.Ok)(void 0);
|
|
5230
5292
|
} catch (error) {
|
|
5231
|
-
return (0,
|
|
5293
|
+
return (0, import_types19.Err)({
|
|
5232
5294
|
category: "provider_unavailable",
|
|
5233
5295
|
message: `1Password CLI is not available: ${error instanceof Error ? error.message : String(error)}`
|
|
5234
5296
|
});
|
|
@@ -5238,7 +5300,7 @@ var OnePasswordSecretBackend = class {
|
|
|
5238
5300
|
|
|
5239
5301
|
// src/agent/secrets/vault.ts
|
|
5240
5302
|
var import_node_child_process7 = require("child_process");
|
|
5241
|
-
var
|
|
5303
|
+
var import_types20 = require("@harness-engineering/types");
|
|
5242
5304
|
function vaultExec(args, env) {
|
|
5243
5305
|
return new Promise((resolve6, reject) => {
|
|
5244
5306
|
(0, import_node_child_process7.execFile)("vault", args, { env: { ...process.env, ...env } }, (error, stdout) => {
|
|
@@ -5269,11 +5331,11 @@ var VaultSecretBackend = class {
|
|
|
5269
5331
|
} catch (error) {
|
|
5270
5332
|
const msg = error instanceof Error ? error.message : String(error);
|
|
5271
5333
|
const category = error instanceof SyntaxError ? "access_denied" : "access_denied";
|
|
5272
|
-
return (0,
|
|
5334
|
+
return (0, import_types20.Err)({ category, message: `Failed to read from Vault: ${msg}` });
|
|
5273
5335
|
}
|
|
5274
5336
|
const missing = keys.find((k) => !(k in data));
|
|
5275
5337
|
if (missing) {
|
|
5276
|
-
return (0,
|
|
5338
|
+
return (0, import_types20.Err)({
|
|
5277
5339
|
category: "secret_not_found",
|
|
5278
5340
|
message: `Secret key '${missing}' not found in Vault path '${this.path}'`,
|
|
5279
5341
|
key: missing
|
|
@@ -5281,14 +5343,14 @@ var VaultSecretBackend = class {
|
|
|
5281
5343
|
}
|
|
5282
5344
|
const secrets = {};
|
|
5283
5345
|
for (const key of keys) secrets[key] = data[key];
|
|
5284
|
-
return (0,
|
|
5346
|
+
return (0, import_types20.Ok)(secrets);
|
|
5285
5347
|
}
|
|
5286
5348
|
async healthCheck() {
|
|
5287
5349
|
try {
|
|
5288
5350
|
await vaultExec(["version"]);
|
|
5289
|
-
return (0,
|
|
5351
|
+
return (0, import_types20.Ok)(void 0);
|
|
5290
5352
|
} catch (error) {
|
|
5291
|
-
return (0,
|
|
5353
|
+
return (0, import_types20.Err)({
|
|
5292
5354
|
category: "provider_unavailable",
|
|
5293
5355
|
message: `Vault CLI is not available: ${error instanceof Error ? error.message : String(error)}`
|
|
5294
5356
|
});
|
|
@@ -5403,6 +5465,10 @@ var OrchestratorBackendFactory = class {
|
|
|
5403
5465
|
}
|
|
5404
5466
|
};
|
|
5405
5467
|
|
|
5468
|
+
// src/agent/intelligence-factory.ts
|
|
5469
|
+
var import_intelligence3 = require("@harness-engineering/intelligence");
|
|
5470
|
+
var import_graph = require("@harness-engineering/graph");
|
|
5471
|
+
|
|
5406
5472
|
// src/agent/analysis-provider-factory.ts
|
|
5407
5473
|
var import_intelligence2 = require("@harness-engineering/intelligence");
|
|
5408
5474
|
function buildAnalysisProvider(args) {
|
|
@@ -5500,9 +5566,109 @@ function buildClaudeCliProvider(def, args, layerModel) {
|
|
|
5500
5566
|
});
|
|
5501
5567
|
}
|
|
5502
5568
|
|
|
5569
|
+
// src/agent/intelligence-factory.ts
|
|
5570
|
+
function buildIntelligencePipeline(deps) {
|
|
5571
|
+
const { config } = deps;
|
|
5572
|
+
const intel = config.intelligence;
|
|
5573
|
+
if (!intel?.enabled) return null;
|
|
5574
|
+
const selProvider = buildAnalysisProviderForLayer("sel", deps);
|
|
5575
|
+
if (!selProvider) return null;
|
|
5576
|
+
const routing = config.agent.routing;
|
|
5577
|
+
const peslName = routing?.intelligence?.pesl;
|
|
5578
|
+
const selName = routing?.intelligence?.sel ?? routing?.default;
|
|
5579
|
+
const peslProvider = peslName !== void 0 && peslName !== selName ? buildAnalysisProviderForLayer("pesl", deps) : null;
|
|
5580
|
+
const peslModel = intel.models?.pesl ?? config.agent.model;
|
|
5581
|
+
const graphStore = new import_graph.GraphStore();
|
|
5582
|
+
const pipeline = new import_intelligence3.IntelligencePipeline(selProvider, graphStore, {
|
|
5583
|
+
...peslModel !== void 0 && { peslModel },
|
|
5584
|
+
...peslProvider !== null && peslProvider !== void 0 && { peslProvider }
|
|
5585
|
+
});
|
|
5586
|
+
return { pipeline, graphStore };
|
|
5587
|
+
}
|
|
5588
|
+
function buildAnalysisProviderForLayer(layer, deps) {
|
|
5589
|
+
const { config, localResolvers, logger } = deps;
|
|
5590
|
+
const intel = config.intelligence;
|
|
5591
|
+
if (!intel?.enabled) return null;
|
|
5592
|
+
if (intel.provider) {
|
|
5593
|
+
const layerModel = layer === "sel" ? intel.models?.sel : intel.models?.pesl;
|
|
5594
|
+
return buildExplicitProvider(intel.provider, layerModel ?? config.agent.model, config);
|
|
5595
|
+
}
|
|
5596
|
+
const routed = resolveRoutedBackend(layer, config, logger);
|
|
5597
|
+
if (!routed) return null;
|
|
5598
|
+
const { name, def } = routed;
|
|
5599
|
+
const resolver = localResolvers.get(name);
|
|
5600
|
+
return buildAnalysisProvider({
|
|
5601
|
+
def,
|
|
5602
|
+
backendName: name,
|
|
5603
|
+
layer,
|
|
5604
|
+
// Spec 2 P3-IMP-1: a single snapshot read feeds the factory's
|
|
5605
|
+
// unavailable-warn diagnostic (Configured/Detected lists) and
|
|
5606
|
+
// collapses the two `getStatus()` calls flagged by P3-SUG-2.
|
|
5607
|
+
getResolverStatusSnapshot: () => {
|
|
5608
|
+
if (!resolver) return null;
|
|
5609
|
+
const status = resolver.getStatus();
|
|
5610
|
+
return {
|
|
5611
|
+
available: status.available,
|
|
5612
|
+
resolved: status.resolved,
|
|
5613
|
+
configured: status.configured,
|
|
5614
|
+
detected: status.detected
|
|
5615
|
+
};
|
|
5616
|
+
},
|
|
5617
|
+
intelligence: intel,
|
|
5618
|
+
logger
|
|
5619
|
+
});
|
|
5620
|
+
}
|
|
5621
|
+
function resolveRoutedBackend(layer, config, logger) {
|
|
5622
|
+
const routing = config.agent.routing;
|
|
5623
|
+
const backends = config.agent.backends;
|
|
5624
|
+
if (!routing || !backends) return null;
|
|
5625
|
+
const layerName = routing.intelligence?.[layer];
|
|
5626
|
+
const name = layerName ?? routing.default;
|
|
5627
|
+
const def = backends[name];
|
|
5628
|
+
if (!def) {
|
|
5629
|
+
logger.warn(
|
|
5630
|
+
`Intelligence pipeline: routed backend '${name}' for layer '${layer}' is not in agent.backends.`
|
|
5631
|
+
);
|
|
5632
|
+
return null;
|
|
5633
|
+
}
|
|
5634
|
+
return { name, def };
|
|
5635
|
+
}
|
|
5636
|
+
function buildExplicitProvider(provider, selModel, config) {
|
|
5637
|
+
if (provider.kind === "anthropic") {
|
|
5638
|
+
const apiKey2 = provider.apiKey ?? config.agent.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
5639
|
+
if (!apiKey2) {
|
|
5640
|
+
throw new Error("Intelligence pipeline: no Anthropic API key found.");
|
|
5641
|
+
}
|
|
5642
|
+
return new import_intelligence3.AnthropicAnalysisProvider({
|
|
5643
|
+
apiKey: apiKey2,
|
|
5644
|
+
...selModel !== void 0 && { defaultModel: selModel }
|
|
5645
|
+
});
|
|
5646
|
+
}
|
|
5647
|
+
if (provider.kind === "claude-cli") {
|
|
5648
|
+
return new import_intelligence3.ClaudeCliAnalysisProvider({
|
|
5649
|
+
command: config.agent.command,
|
|
5650
|
+
...selModel !== void 0 && { defaultModel: selModel },
|
|
5651
|
+
...config.intelligence?.requestTimeoutMs !== void 0 && {
|
|
5652
|
+
timeoutMs: config.intelligence.requestTimeoutMs
|
|
5653
|
+
}
|
|
5654
|
+
});
|
|
5655
|
+
}
|
|
5656
|
+
const apiKey = provider.apiKey ?? config.agent.apiKey ?? "ollama";
|
|
5657
|
+
const baseUrl = provider.baseUrl ?? "http://localhost:11434/v1";
|
|
5658
|
+
const intel = config.intelligence;
|
|
5659
|
+
return new import_intelligence3.OpenAICompatibleAnalysisProvider({
|
|
5660
|
+
apiKey,
|
|
5661
|
+
baseUrl,
|
|
5662
|
+
...selModel !== void 0 && { defaultModel: selModel },
|
|
5663
|
+
...intel?.requestTimeoutMs !== void 0 && { timeoutMs: intel.requestTimeoutMs },
|
|
5664
|
+
...intel?.promptSuffix !== void 0 && { promptSuffix: intel.promptSuffix },
|
|
5665
|
+
...intel?.jsonMode !== void 0 && { jsonMode: intel.jsonMode }
|
|
5666
|
+
});
|
|
5667
|
+
}
|
|
5668
|
+
|
|
5503
5669
|
// src/server/http.ts
|
|
5504
5670
|
var http = __toESM(require("http"));
|
|
5505
|
-
var
|
|
5671
|
+
var path14 = __toESM(require("path"));
|
|
5506
5672
|
|
|
5507
5673
|
// src/server/websocket.ts
|
|
5508
5674
|
var import_ws = require("ws");
|
|
@@ -5930,7 +6096,7 @@ function extractChunks(event) {
|
|
|
5930
6096
|
}
|
|
5931
6097
|
|
|
5932
6098
|
// src/server/routes/analyze.ts
|
|
5933
|
-
var
|
|
6099
|
+
var import_intelligence4 = require("@harness-engineering/intelligence");
|
|
5934
6100
|
var import_zod6 = require("zod");
|
|
5935
6101
|
var AnalyzeRequestSchema = import_zod6.z.object({
|
|
5936
6102
|
title: import_zod6.z.string().min(1),
|
|
@@ -5960,7 +6126,7 @@ async function runPipeline(res, pipeline, parsed) {
|
|
|
5960
6126
|
disconnected = true;
|
|
5961
6127
|
});
|
|
5962
6128
|
emit2(res, { type: "status", text: "Converting to work item..." });
|
|
5963
|
-
const rawItem = (0,
|
|
6129
|
+
const rawItem = (0, import_intelligence4.manualToRawWorkItem)({
|
|
5964
6130
|
title: parsed.title,
|
|
5965
6131
|
description: parsed.description ?? "",
|
|
5966
6132
|
labels: parsed.labels ?? []
|
|
@@ -6003,7 +6169,7 @@ async function runPipeline(res, pipeline, parsed) {
|
|
|
6003
6169
|
}
|
|
6004
6170
|
}
|
|
6005
6171
|
if (disconnected) return;
|
|
6006
|
-
const signals = (0,
|
|
6172
|
+
const signals = (0, import_intelligence4.scoreToConcernSignals)(score);
|
|
6007
6173
|
if (signals.length > 0) {
|
|
6008
6174
|
emit2(res, { type: "signals", data: signals });
|
|
6009
6175
|
}
|
|
@@ -6042,6 +6208,7 @@ function handleAnalyzeRoute(req, res, pipeline) {
|
|
|
6042
6208
|
|
|
6043
6209
|
// src/server/routes/roadmap-actions.ts
|
|
6044
6210
|
var fs10 = __toESM(require("fs/promises"));
|
|
6211
|
+
var path10 = __toESM(require("path"));
|
|
6045
6212
|
var import_core7 = require("@harness-engineering/core");
|
|
6046
6213
|
var import_zod7 = require("zod");
|
|
6047
6214
|
var AppendRoadmapRequestSchema = import_zod7.z.object({
|
|
@@ -6069,6 +6236,48 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
6069
6236
|
sendJSON(res, 503, { error: "Roadmap path not configured" });
|
|
6070
6237
|
return;
|
|
6071
6238
|
}
|
|
6239
|
+
const projectRoot = path10.dirname(path10.dirname(roadmapPath));
|
|
6240
|
+
const mode = (0, import_core7.loadProjectRoadmapMode)(projectRoot);
|
|
6241
|
+
if (mode === "file-less") {
|
|
6242
|
+
const trackerCfg = (0, import_core7.loadTrackerClientConfigFromProject)(projectRoot);
|
|
6243
|
+
if (!trackerCfg.ok) {
|
|
6244
|
+
sendJSON(res, 500, { error: trackerCfg.error.message });
|
|
6245
|
+
return;
|
|
6246
|
+
}
|
|
6247
|
+
const clientR = (0, import_core7.createTrackerClient)(trackerCfg.value);
|
|
6248
|
+
if (!clientR.ok) {
|
|
6249
|
+
sendJSON(res, 500, { error: clientR.error.message });
|
|
6250
|
+
return;
|
|
6251
|
+
}
|
|
6252
|
+
const body2 = await readBody(req);
|
|
6253
|
+
const parseResult = AppendRoadmapRequestSchema.safeParse(JSON.parse(body2));
|
|
6254
|
+
if (!parseResult.success) {
|
|
6255
|
+
sendJSON(res, 400, {
|
|
6256
|
+
error: parseResult.error.issues[0]?.message ?? "Invalid request body"
|
|
6257
|
+
});
|
|
6258
|
+
return;
|
|
6259
|
+
}
|
|
6260
|
+
const newFeature = {
|
|
6261
|
+
name: parseResult.data.title,
|
|
6262
|
+
summary: parseResult.data.enrichedSpec?.intent ?? parseResult.data.summary ?? parseResult.data.title,
|
|
6263
|
+
status: "planned"
|
|
6264
|
+
};
|
|
6265
|
+
const r = await clientR.value.create(newFeature);
|
|
6266
|
+
if (!r.ok) {
|
|
6267
|
+
if (r.error instanceof import_core7.ConflictError) {
|
|
6268
|
+
sendJSON(res, 409, (0, import_core7.makeTrackerConflictBody)(r.error));
|
|
6269
|
+
return;
|
|
6270
|
+
}
|
|
6271
|
+
sendJSON(res, 502, { error: r.error.message });
|
|
6272
|
+
return;
|
|
6273
|
+
}
|
|
6274
|
+
sendJSON(res, 201, {
|
|
6275
|
+
ok: true,
|
|
6276
|
+
featureName: r.value.name,
|
|
6277
|
+
externalId: r.value.externalId
|
|
6278
|
+
});
|
|
6279
|
+
return;
|
|
6280
|
+
}
|
|
6072
6281
|
const body = await readBody(req);
|
|
6073
6282
|
const result = AppendRoadmapRequestSchema.safeParse(JSON.parse(body));
|
|
6074
6283
|
if (!result.success) {
|
|
@@ -6321,21 +6530,21 @@ function handleMaintenanceRoute(req, res, deps) {
|
|
|
6321
6530
|
|
|
6322
6531
|
// src/server/routes/sessions.ts
|
|
6323
6532
|
var fs11 = __toESM(require("fs/promises"));
|
|
6324
|
-
var
|
|
6533
|
+
var path11 = __toESM(require("path"));
|
|
6325
6534
|
var import_zod10 = require("zod");
|
|
6326
6535
|
var SessionCreateSchema = import_zod10.z.object({
|
|
6327
6536
|
sessionId: import_zod10.z.string().min(1)
|
|
6328
6537
|
}).passthrough();
|
|
6329
6538
|
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6330
6539
|
function isSafeId(id) {
|
|
6331
|
-
return UUID_RE2.test(id) ||
|
|
6540
|
+
return UUID_RE2.test(id) || path11.basename(id) === id && !id.includes("..");
|
|
6332
6541
|
}
|
|
6333
6542
|
function jsonResponse(res, status, data) {
|
|
6334
6543
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
6335
6544
|
res.end(JSON.stringify(data));
|
|
6336
6545
|
}
|
|
6337
6546
|
function extractSessionId(url) {
|
|
6338
|
-
const segments = new URL(url, "http://localhost").pathname.split(
|
|
6547
|
+
const segments = new URL(url, "http://localhost").pathname.split(path11.posix.sep);
|
|
6339
6548
|
const id = segments.pop();
|
|
6340
6549
|
return id && id !== "sessions" ? id : null;
|
|
6341
6550
|
}
|
|
@@ -6347,7 +6556,7 @@ async function handleList(res, sessionsDir) {
|
|
|
6347
6556
|
if (!entry.isDirectory()) continue;
|
|
6348
6557
|
try {
|
|
6349
6558
|
const content = await fs11.readFile(
|
|
6350
|
-
|
|
6559
|
+
path11.join(sessionsDir, entry.name, "session.json"),
|
|
6351
6560
|
"utf-8"
|
|
6352
6561
|
);
|
|
6353
6562
|
sessions.push(JSON.parse(content));
|
|
@@ -6372,7 +6581,7 @@ async function handleGet(res, id, sessionsDir) {
|
|
|
6372
6581
|
return;
|
|
6373
6582
|
}
|
|
6374
6583
|
try {
|
|
6375
|
-
const content = await fs11.readFile(
|
|
6584
|
+
const content = await fs11.readFile(path11.join(sessionsDir, id, "session.json"), "utf-8");
|
|
6376
6585
|
jsonResponse(res, 200, JSON.parse(content));
|
|
6377
6586
|
} catch (err) {
|
|
6378
6587
|
if (err.code === "ENOENT") {
|
|
@@ -6395,9 +6604,9 @@ async function handleCreate(req, res, sessionsDir) {
|
|
|
6395
6604
|
jsonResponse(res, 400, { error: "Invalid sessionId" });
|
|
6396
6605
|
return;
|
|
6397
6606
|
}
|
|
6398
|
-
const sessionDir =
|
|
6607
|
+
const sessionDir = path11.join(sessionsDir, session.sessionId);
|
|
6399
6608
|
await fs11.mkdir(sessionDir, { recursive: true });
|
|
6400
|
-
await fs11.writeFile(
|
|
6609
|
+
await fs11.writeFile(path11.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
|
|
6401
6610
|
jsonResponse(res, 200, { ok: true });
|
|
6402
6611
|
} catch {
|
|
6403
6612
|
jsonResponse(res, 500, { error: "Failed to save session" });
|
|
@@ -6412,7 +6621,7 @@ async function handleUpdate(req, res, url, sessionsDir) {
|
|
|
6412
6621
|
}
|
|
6413
6622
|
const body = await readBody(req);
|
|
6414
6623
|
const updates = import_zod10.z.record(import_zod10.z.unknown()).parse(JSON.parse(body));
|
|
6415
|
-
const sessionFilePath =
|
|
6624
|
+
const sessionFilePath = path11.join(sessionsDir, id, "session.json");
|
|
6416
6625
|
const current = JSON.parse(await fs11.readFile(sessionFilePath, "utf-8"));
|
|
6417
6626
|
await fs11.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
|
|
6418
6627
|
jsonResponse(res, 200, { ok: true });
|
|
@@ -6427,7 +6636,7 @@ async function handleDelete(res, url, sessionsDir) {
|
|
|
6427
6636
|
jsonResponse(res, 400, { error: "Missing or invalid sessionId" });
|
|
6428
6637
|
return;
|
|
6429
6638
|
}
|
|
6430
|
-
await fs11.rm(
|
|
6639
|
+
await fs11.rm(path11.join(sessionsDir, id), { recursive: true, force: true });
|
|
6431
6640
|
jsonResponse(res, 200, { ok: true });
|
|
6432
6641
|
} catch {
|
|
6433
6642
|
jsonResponse(res, 500, { error: "Failed to delete session" });
|
|
@@ -6568,7 +6777,7 @@ function handleLocalModelsRoute(req, res, getStatuses) {
|
|
|
6568
6777
|
|
|
6569
6778
|
// src/server/static.ts
|
|
6570
6779
|
var fs12 = __toESM(require("fs"));
|
|
6571
|
-
var
|
|
6780
|
+
var path12 = __toESM(require("path"));
|
|
6572
6781
|
var MIME_TYPES = {
|
|
6573
6782
|
".html": "text/html; charset=utf-8",
|
|
6574
6783
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -6588,26 +6797,26 @@ var MIME_TYPES = {
|
|
|
6588
6797
|
function handleStaticFile(req, res, dashboardDir) {
|
|
6589
6798
|
const { method, url } = req;
|
|
6590
6799
|
if (method !== "GET") return false;
|
|
6591
|
-
const apiPrefix =
|
|
6592
|
-
const wsPath =
|
|
6800
|
+
const apiPrefix = path12.posix.join(path12.posix.sep, "api", path12.posix.sep);
|
|
6801
|
+
const wsPath = path12.posix.join(path12.posix.sep, "ws");
|
|
6593
6802
|
if (url?.startsWith(apiPrefix) || url === wsPath) return false;
|
|
6594
6803
|
const urlPath = new URL(url ?? "/", "http://localhost").pathname;
|
|
6595
|
-
const requestedPath =
|
|
6596
|
-
const resolved =
|
|
6597
|
-
if (!resolved.startsWith(
|
|
6598
|
-
return serveFile(
|
|
6804
|
+
const requestedPath = path12.join(dashboardDir, urlPath === "/" ? "index.html" : urlPath);
|
|
6805
|
+
const resolved = path12.resolve(requestedPath);
|
|
6806
|
+
if (!resolved.startsWith(path12.resolve(dashboardDir))) {
|
|
6807
|
+
return serveFile(path12.join(dashboardDir, "index.html"), res);
|
|
6599
6808
|
}
|
|
6600
6809
|
if (fs12.existsSync(resolved) && fs12.statSync(resolved).isFile()) {
|
|
6601
6810
|
return serveFile(resolved, res);
|
|
6602
6811
|
}
|
|
6603
|
-
const indexPath =
|
|
6812
|
+
const indexPath = path12.join(dashboardDir, "index.html");
|
|
6604
6813
|
if (fs12.existsSync(indexPath)) {
|
|
6605
6814
|
return serveFile(indexPath, res);
|
|
6606
6815
|
}
|
|
6607
6816
|
return false;
|
|
6608
6817
|
}
|
|
6609
6818
|
function serveFile(filePath, res) {
|
|
6610
|
-
const ext =
|
|
6819
|
+
const ext = path12.extname(filePath).toLowerCase();
|
|
6611
6820
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
6612
6821
|
try {
|
|
6613
6822
|
const content = fs12.readFileSync(filePath);
|
|
@@ -6621,7 +6830,7 @@ function serveFile(filePath, res) {
|
|
|
6621
6830
|
|
|
6622
6831
|
// src/server/plan-watcher.ts
|
|
6623
6832
|
var fs13 = __toESM(require("fs"));
|
|
6624
|
-
var
|
|
6833
|
+
var path13 = __toESM(require("path"));
|
|
6625
6834
|
var PlanWatcher = class {
|
|
6626
6835
|
plansDir;
|
|
6627
6836
|
queue;
|
|
@@ -6638,7 +6847,7 @@ var PlanWatcher = class {
|
|
|
6638
6847
|
fs13.mkdirSync(this.plansDir, { recursive: true });
|
|
6639
6848
|
this.watcher = fs13.watch(this.plansDir, (eventType, filename) => {
|
|
6640
6849
|
if (eventType === "rename" && filename && filename.endsWith(".md")) {
|
|
6641
|
-
const filePath =
|
|
6850
|
+
const filePath = path13.join(this.plansDir, filename);
|
|
6642
6851
|
if (fs13.existsSync(filePath)) {
|
|
6643
6852
|
void this.handleNewPlan(filename);
|
|
6644
6853
|
}
|
|
@@ -6725,6 +6934,7 @@ var OrchestratorServer = class {
|
|
|
6725
6934
|
planWatcher = null;
|
|
6726
6935
|
stateChangeListener;
|
|
6727
6936
|
agentEventListener;
|
|
6937
|
+
apiRoutes;
|
|
6728
6938
|
constructor(orchestrator, port, deps) {
|
|
6729
6939
|
this.orchestrator = orchestrator;
|
|
6730
6940
|
this.port = port;
|
|
@@ -6734,18 +6944,19 @@ var OrchestratorServer = class {
|
|
|
6734
6944
|
this.httpServer,
|
|
6735
6945
|
() => this.orchestrator.getSnapshot()
|
|
6736
6946
|
);
|
|
6947
|
+
this.apiRoutes = this.buildApiRoutes();
|
|
6737
6948
|
this.wireEvents();
|
|
6738
6949
|
}
|
|
6739
6950
|
initDependencies(deps) {
|
|
6740
6951
|
this.interactionQueue = deps?.interactionQueue;
|
|
6741
|
-
this.plansDir = deps?.plansDir ??
|
|
6742
|
-
this.dashboardDir = deps?.dashboardDir ??
|
|
6952
|
+
this.plansDir = deps?.plansDir ?? path14.resolve("docs", "plans");
|
|
6953
|
+
this.dashboardDir = deps?.dashboardDir ?? path14.resolve("packages", "dashboard", "dist", "client");
|
|
6743
6954
|
this.claudeCommand = deps?.claudeCommand ?? "claude";
|
|
6744
6955
|
this.pipeline = deps?.pipeline ?? null;
|
|
6745
6956
|
this.analysisArchive = deps?.analysisArchive;
|
|
6746
6957
|
this.roadmapPath = deps?.roadmapPath ?? null;
|
|
6747
6958
|
this.dispatchAdHoc = deps?.dispatchAdHoc ?? null;
|
|
6748
|
-
this.sessionsDir = deps?.sessionsDir ??
|
|
6959
|
+
this.sessionsDir = deps?.sessionsDir ?? path14.resolve(".harness", "sessions");
|
|
6749
6960
|
this.maintenanceDeps = deps?.maintenanceDeps ?? null;
|
|
6750
6961
|
this.getLocalModelStatus = deps?.getLocalModelStatus ?? null;
|
|
6751
6962
|
this.getLocalModelStatuses = deps?.getLocalModelStatuses ?? null;
|
|
@@ -6769,8 +6980,8 @@ var OrchestratorServer = class {
|
|
|
6769
6980
|
}
|
|
6770
6981
|
/**
|
|
6771
6982
|
* Broadcast a maintenance event to all WebSocket clients.
|
|
6772
|
-
* @param type - One of 'maintenance:started', 'maintenance:completed', 'maintenance:error'
|
|
6773
|
-
* @param data - Event payload (task info, run result, or
|
|
6983
|
+
* @param type - One of 'maintenance:started', 'maintenance:completed', 'maintenance:error', 'maintenance:baseref_fallback'
|
|
6984
|
+
* @param data - Event payload (task info, run result, error details, or baseref-fallback diagnostic)
|
|
6774
6985
|
*/
|
|
6775
6986
|
broadcastMaintenance(type, data) {
|
|
6776
6987
|
this.broadcaster.broadcast(type, data);
|
|
@@ -6855,44 +7066,37 @@ var OrchestratorServer = class {
|
|
|
6855
7066
|
);
|
|
6856
7067
|
return false;
|
|
6857
7068
|
}
|
|
7069
|
+
/**
|
|
7070
|
+
* Build the ordered API route table. Each entry is invoked in order and
|
|
7071
|
+
* returns true when it has handled the request. Closures capture `this`,
|
|
7072
|
+
* so handlers re-read mutable deps (pipeline, recorder, maintenanceDeps)
|
|
7073
|
+
* on every request — setters like setPipeline() take effect immediately.
|
|
7074
|
+
*
|
|
7075
|
+
* Adding a new route is a one-place change: append an entry here.
|
|
7076
|
+
*/
|
|
7077
|
+
buildApiRoutes() {
|
|
7078
|
+
return [
|
|
7079
|
+
(req, res) => !!this.interactionQueue && handleInteractionsRoute(req, res, this.interactionQueue),
|
|
7080
|
+
(req, res) => handlePlansRoute(req, res, this.plansDir),
|
|
7081
|
+
(req, res) => handleAnalyzeRoute(req, res, this.pipeline),
|
|
7082
|
+
(req, res) => handleAnalysesRoute(req, res, this.analysisArchive),
|
|
7083
|
+
(req, res) => handleRoadmapActionsRoute(req, res, this.roadmapPath),
|
|
7084
|
+
(req, res) => handleDispatchActionsRoute(req, res, this.dispatchAdHoc),
|
|
7085
|
+
(req, res) => handleLocalModelRoute(req, res, this.getLocalModelStatus),
|
|
7086
|
+
// Local-models multi-status route (Spec 2 SC38)
|
|
7087
|
+
(req, res) => handleLocalModelsRoute(req, res, this.getLocalModelStatuses),
|
|
7088
|
+
(req, res) => handleMaintenanceRoute(req, res, this.maintenanceDeps),
|
|
7089
|
+
(req, res) => !!this.recorder && handleStreamsRoute(req, res, this.recorder),
|
|
7090
|
+
(req, res) => handleSessionsRoute(req, res, this.sessionsDir),
|
|
7091
|
+
// Chat proxy route (spawns Claude Code CLI — no API key required)
|
|
7092
|
+
(req, res) => handleChatProxyRoute(req, res, this.claudeCommand)
|
|
7093
|
+
];
|
|
7094
|
+
}
|
|
6858
7095
|
/** Dispatch to API route handlers. Returns true if a route matched. */
|
|
6859
7096
|
handleApiRoutes(req, res) {
|
|
6860
7097
|
if (!this.checkAuth(req, res)) return true;
|
|
6861
|
-
|
|
6862
|
-
return true;
|
|
6863
|
-
}
|
|
6864
|
-
if (handlePlansRoute(req, res, this.plansDir)) {
|
|
6865
|
-
return true;
|
|
6866
|
-
}
|
|
6867
|
-
if (handleAnalyzeRoute(req, res, this.pipeline)) {
|
|
6868
|
-
return true;
|
|
6869
|
-
}
|
|
6870
|
-
if (handleAnalysesRoute(req, res, this.analysisArchive)) {
|
|
6871
|
-
return true;
|
|
6872
|
-
}
|
|
6873
|
-
if (handleRoadmapActionsRoute(req, res, this.roadmapPath)) {
|
|
6874
|
-
return true;
|
|
6875
|
-
}
|
|
6876
|
-
if (handleDispatchActionsRoute(req, res, this.dispatchAdHoc)) {
|
|
6877
|
-
return true;
|
|
6878
|
-
}
|
|
6879
|
-
if (handleLocalModelRoute(req, res, this.getLocalModelStatus)) {
|
|
6880
|
-
return true;
|
|
6881
|
-
}
|
|
6882
|
-
if (handleLocalModelsRoute(req, res, this.getLocalModelStatuses)) {
|
|
6883
|
-
return true;
|
|
6884
|
-
}
|
|
6885
|
-
if (handleMaintenanceRoute(req, res, this.maintenanceDeps)) {
|
|
6886
|
-
return true;
|
|
6887
|
-
}
|
|
6888
|
-
if (this.recorder && handleStreamsRoute(req, res, this.recorder)) {
|
|
6889
|
-
return true;
|
|
6890
|
-
}
|
|
6891
|
-
if (handleSessionsRoute(req, res, this.sessionsDir)) {
|
|
6892
|
-
return true;
|
|
6893
|
-
}
|
|
6894
|
-
if (handleChatProxyRoute(req, res, this.claudeCommand)) {
|
|
6895
|
-
return true;
|
|
7098
|
+
for (const route of this.apiRoutes) {
|
|
7099
|
+
if (route(req, res)) return true;
|
|
6896
7100
|
}
|
|
6897
7101
|
return false;
|
|
6898
7102
|
}
|
|
@@ -7179,6 +7383,14 @@ var BUILT_IN_TASKS = [
|
|
|
7179
7383
|
schedule: "0 7 * * *",
|
|
7180
7384
|
branch: null,
|
|
7181
7385
|
checkCommand: ["perf", "baselines", "update"]
|
|
7386
|
+
},
|
|
7387
|
+
{
|
|
7388
|
+
id: "main-sync",
|
|
7389
|
+
type: "housekeeping",
|
|
7390
|
+
description: "Fast-forward local default branch from origin",
|
|
7391
|
+
schedule: "*/15 * * * *",
|
|
7392
|
+
branch: null,
|
|
7393
|
+
checkCommand: ["harness", "sync-main", "--json"]
|
|
7182
7394
|
}
|
|
7183
7395
|
];
|
|
7184
7396
|
|
|
@@ -7249,7 +7461,7 @@ function cronMatchesNow(expression, now) {
|
|
|
7249
7461
|
// src/maintenance/scheduler.ts
|
|
7250
7462
|
var MaintenanceScheduler = class {
|
|
7251
7463
|
config;
|
|
7252
|
-
|
|
7464
|
+
leaderElector;
|
|
7253
7465
|
logger;
|
|
7254
7466
|
onTaskDue;
|
|
7255
7467
|
historyProvider;
|
|
@@ -7264,7 +7476,7 @@ var MaintenanceScheduler = class {
|
|
|
7264
7476
|
activeRun = null;
|
|
7265
7477
|
constructor(options) {
|
|
7266
7478
|
this.config = options.config;
|
|
7267
|
-
this.
|
|
7479
|
+
this.leaderElector = options.leaderElector;
|
|
7268
7480
|
this.logger = options.logger;
|
|
7269
7481
|
this.historyProvider = options.historyProvider ?? null;
|
|
7270
7482
|
this.onTaskDue = options.onTaskDue;
|
|
@@ -7339,12 +7551,12 @@ var MaintenanceScheduler = class {
|
|
|
7339
7551
|
await this.processQueue(queue, epochMinute);
|
|
7340
7552
|
}
|
|
7341
7553
|
/**
|
|
7342
|
-
* Attempt to claim leadership via
|
|
7554
|
+
* Attempt to claim leadership via the configured LeaderElector.
|
|
7343
7555
|
* Returns true if this instance is the leader.
|
|
7344
7556
|
*/
|
|
7345
7557
|
async attemptLeaderClaim(evalTime) {
|
|
7346
7558
|
try {
|
|
7347
|
-
const result = await this.
|
|
7559
|
+
const result = await this.leaderElector.electLeader();
|
|
7348
7560
|
if (!result.ok) {
|
|
7349
7561
|
this.isLeader = false;
|
|
7350
7562
|
this.logger.warn("Maintenance leader claim failed", { error: result.error?.message });
|
|
@@ -7423,6 +7635,7 @@ var MaintenanceScheduler = class {
|
|
|
7423
7635
|
const history = this.historyProvider ? this.historyProvider.getHistory(200, 0) : this.internalHistory;
|
|
7424
7636
|
const schedule = this.resolvedTasks.map((task) => ({
|
|
7425
7637
|
taskId: task.id,
|
|
7638
|
+
type: task.type,
|
|
7426
7639
|
nextRun: this.computeNextRun(task.schedule),
|
|
7427
7640
|
lastRun: history.find((r) => r.taskId === task.id) ?? null
|
|
7428
7641
|
}));
|
|
@@ -7459,9 +7672,17 @@ var MaintenanceScheduler = class {
|
|
|
7459
7672
|
}
|
|
7460
7673
|
};
|
|
7461
7674
|
|
|
7675
|
+
// src/maintenance/leader-elector.ts
|
|
7676
|
+
var import_types21 = require("@harness-engineering/types");
|
|
7677
|
+
var SingleProcessLeaderElector = class {
|
|
7678
|
+
async electLeader() {
|
|
7679
|
+
return (0, import_types21.Ok)("claimed");
|
|
7680
|
+
}
|
|
7681
|
+
};
|
|
7682
|
+
|
|
7462
7683
|
// src/maintenance/reporter.ts
|
|
7463
7684
|
var fs14 = __toESM(require("fs"));
|
|
7464
|
-
var
|
|
7685
|
+
var path15 = __toESM(require("path"));
|
|
7465
7686
|
var import_zod11 = require("zod");
|
|
7466
7687
|
var RunResultSchema = import_zod11.z.object({
|
|
7467
7688
|
taskId: import_zod11.z.string(),
|
|
@@ -7497,7 +7718,7 @@ var MaintenanceReporter = class {
|
|
|
7497
7718
|
async load() {
|
|
7498
7719
|
try {
|
|
7499
7720
|
await fs14.promises.mkdir(this.persistDir, { recursive: true });
|
|
7500
|
-
const filePath =
|
|
7721
|
+
const filePath = path15.join(this.persistDir, "history.json");
|
|
7501
7722
|
const data = await fs14.promises.readFile(filePath, "utf-8");
|
|
7502
7723
|
const parsed = import_zod11.z.array(RunResultSchema).safeParse(JSON.parse(data));
|
|
7503
7724
|
if (parsed.success) {
|
|
@@ -7533,7 +7754,7 @@ var MaintenanceReporter = class {
|
|
|
7533
7754
|
async persist() {
|
|
7534
7755
|
try {
|
|
7535
7756
|
await fs14.promises.mkdir(this.persistDir, { recursive: true });
|
|
7536
|
-
const filePath =
|
|
7757
|
+
const filePath = path15.join(this.persistDir, "history.json");
|
|
7537
7758
|
await fs14.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
|
|
7538
7759
|
} catch (err) {
|
|
7539
7760
|
this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
|
|
@@ -7754,22 +7975,39 @@ var TaskRunner = class {
|
|
|
7754
7975
|
}
|
|
7755
7976
|
/**
|
|
7756
7977
|
* Housekeeping: run command directly, no AI, no PR.
|
|
7978
|
+
*
|
|
7979
|
+
* Captures stdout and parses a trailing JSON status line if present.
|
|
7980
|
+
* Recognized contracts:
|
|
7981
|
+
* - Phase 4/5 status contract (e.g., harness pulse run): success/skipped/failure/no-issues
|
|
7982
|
+
* - sync-main contract: updated/no-op/skipped/error → mapped onto the run-result status
|
|
7983
|
+
* Legacy housekeeping commands that emit no JSON keep the prior behavior:
|
|
7984
|
+
* status: 'success', findings: 0.
|
|
7757
7985
|
*/
|
|
7758
7986
|
async runHousekeeping(task, startedAt) {
|
|
7759
7987
|
if (!task.checkCommand || task.checkCommand.length === 0) {
|
|
7760
7988
|
return this.failureResult(task.id, startedAt, "housekeeping task missing checkCommand");
|
|
7761
7989
|
}
|
|
7762
|
-
|
|
7763
|
-
|
|
7990
|
+
let stdout;
|
|
7991
|
+
try {
|
|
7992
|
+
const out = await this.commandExecutor.exec(task.checkCommand, this.cwd);
|
|
7993
|
+
stdout = out.stdout ?? "";
|
|
7994
|
+
} catch (err) {
|
|
7995
|
+
return this.failureResult(task.id, startedAt, String(err));
|
|
7996
|
+
}
|
|
7997
|
+
const parsed = parseStatusLine(stdout);
|
|
7998
|
+
const status = parsed?.status ?? "success";
|
|
7999
|
+
const result = {
|
|
7764
8000
|
taskId: task.id,
|
|
7765
8001
|
startedAt,
|
|
7766
8002
|
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7767
|
-
status
|
|
8003
|
+
status,
|
|
7768
8004
|
findings: 0,
|
|
7769
8005
|
fixed: 0,
|
|
7770
8006
|
prUrl: null,
|
|
7771
8007
|
prUpdated: false
|
|
7772
8008
|
};
|
|
8009
|
+
if (parsed?.error) result.error = parsed.error;
|
|
8010
|
+
return result;
|
|
7773
8011
|
}
|
|
7774
8012
|
/**
|
|
7775
8013
|
* Resolve which AI backend name to use for a given task.
|
|
@@ -7803,7 +8041,7 @@ function parseStatusLine(output) {
|
|
|
7803
8041
|
const obj = JSON.parse(line);
|
|
7804
8042
|
const s = obj.status;
|
|
7805
8043
|
if (s === "success" || s === "skipped" || s === "failure" || s === "no-issues") {
|
|
7806
|
-
const parsed = { status: s };
|
|
8044
|
+
const parsed = { status: s, rawStatus: s };
|
|
7807
8045
|
if (typeof obj.candidatesFound === "number") {
|
|
7808
8046
|
parsed.candidatesFound = obj.candidatesFound;
|
|
7809
8047
|
}
|
|
@@ -7813,8 +8051,18 @@ function parseStatusLine(output) {
|
|
|
7813
8051
|
if (typeof obj.reason === "string") {
|
|
7814
8052
|
parsed.reason = obj.reason;
|
|
7815
8053
|
}
|
|
8054
|
+
if (typeof obj.detail === "string" && !parsed.error) {
|
|
8055
|
+
parsed.error = `${parsed.reason ?? "skipped"}: ${obj.detail}`;
|
|
8056
|
+
}
|
|
7816
8057
|
return parsed;
|
|
7817
8058
|
}
|
|
8059
|
+
if (s === "updated" || s === "no-op") {
|
|
8060
|
+
return { status: "success", rawStatus: s };
|
|
8061
|
+
}
|
|
8062
|
+
if (s === "error") {
|
|
8063
|
+
const message = typeof obj.message === "string" ? obj.message : "unknown error";
|
|
8064
|
+
return { status: "failure", error: message, rawStatus: "error" };
|
|
8065
|
+
}
|
|
7818
8066
|
} catch {
|
|
7819
8067
|
}
|
|
7820
8068
|
}
|
|
@@ -7895,11 +8143,15 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7895
8143
|
completionHandler;
|
|
7896
8144
|
/** Project root directory, derived from workspace root. */
|
|
7897
8145
|
get projectRoot() {
|
|
7898
|
-
return
|
|
8146
|
+
return path16.resolve(this.config.workspace.root, "..", "..");
|
|
7899
8147
|
}
|
|
7900
8148
|
enrichedSpecsByIssue = /* @__PURE__ */ new Map();
|
|
7901
8149
|
/** Tracks recently-failed intelligence analysis to avoid re-requesting every tick */
|
|
7902
8150
|
analysisFailureCache = /* @__PURE__ */ new Map();
|
|
8151
|
+
// Phase 3 added a private `roadmapMode` field used by `createTracker` to
|
|
8152
|
+
// guard the file-less stub. Phase 4 / S2 / D-P4-E shifted dispatch onto
|
|
8153
|
+
// `tracker.kind`, removing the need for the field — it is now dropped to
|
|
8154
|
+
// satisfy `noUnusedLocals`. See decision D-P3-orchestrator-mode-via-fs-read.
|
|
7903
8155
|
/** Abort controllers and PIDs for running agent tasks — used by stopIssue to cancel in-flight work.
|
|
7904
8156
|
* The PID is stored here because the running entry may be deleted by the state machine
|
|
7905
8157
|
* before the stop effect executes (e.g., stall_detected removes the entry first). */
|
|
@@ -7935,14 +8187,19 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7935
8187
|
);
|
|
7936
8188
|
}
|
|
7937
8189
|
this.tracker = overrides?.tracker || this.createTracker();
|
|
7938
|
-
this.workspace = new WorkspaceManager(config.workspace
|
|
8190
|
+
this.workspace = new WorkspaceManager(config.workspace, {
|
|
8191
|
+
emitEvent: (event) => {
|
|
8192
|
+
this.server?.broadcastMaintenance("maintenance:baseref_fallback", event);
|
|
8193
|
+
this.emit("maintenance:baseref_fallback", event);
|
|
8194
|
+
}
|
|
8195
|
+
});
|
|
7939
8196
|
this.hooks = new WorkspaceHooks(config.hooks);
|
|
7940
8197
|
this.renderer = new PromptRenderer();
|
|
7941
8198
|
this.overrideBackend = overrides?.backend ?? null;
|
|
7942
8199
|
this.interactionQueue = new InteractionQueue(
|
|
7943
|
-
|
|
8200
|
+
path16.join(config.workspace.root, "..", "interactions")
|
|
7944
8201
|
);
|
|
7945
|
-
this.analysisArchive = new AnalysisArchive(
|
|
8202
|
+
this.analysisArchive = new AnalysisArchive(path16.join(config.workspace.root, "..", "analyses"));
|
|
7946
8203
|
const backendsMap = this.config.agent.backends ?? {};
|
|
7947
8204
|
for (const [name, def] of Object.entries(backendsMap)) {
|
|
7948
8205
|
if (def.type === "local" || def.type === "pi") {
|
|
@@ -7984,7 +8241,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
7984
8241
|
...overrides?.execFileFn ? { execFileFn: overrides.execFileFn } : {}
|
|
7985
8242
|
});
|
|
7986
8243
|
this.recorder = new StreamRecorder(
|
|
7987
|
-
|
|
8244
|
+
path16.resolve(config.workspace.root, "..", "streams"),
|
|
7988
8245
|
this.logger
|
|
7989
8246
|
);
|
|
7990
8247
|
const self = this;
|
|
@@ -8016,7 +8273,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
8016
8273
|
if (config.server?.port) {
|
|
8017
8274
|
this.server = new OrchestratorServer(this, config.server.port, {
|
|
8018
8275
|
interactionQueue: this.interactionQueue,
|
|
8019
|
-
plansDir:
|
|
8276
|
+
plansDir: path16.resolve(config.workspace.root, "..", "docs", "plans"),
|
|
8020
8277
|
pipeline: this.pipeline,
|
|
8021
8278
|
analysisArchive: this.analysisArchive,
|
|
8022
8279
|
roadmapPath: config.tracker.filePath ?? null,
|
|
@@ -8047,6 +8304,17 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
8047
8304
|
}
|
|
8048
8305
|
}
|
|
8049
8306
|
createTracker() {
|
|
8307
|
+
if (this.config.tracker.kind === "github-issues") {
|
|
8308
|
+
const trackerCfg = {
|
|
8309
|
+
kind: "github-issues",
|
|
8310
|
+
repo: this.config.tracker.projectSlug ?? "",
|
|
8311
|
+
...this.config.tracker.apiKey ? { token: this.config.tracker.apiKey } : {},
|
|
8312
|
+
...this.config.tracker.endpoint ? { apiBase: this.config.tracker.endpoint } : {}
|
|
8313
|
+
};
|
|
8314
|
+
const clientResult = (0, import_core10.createTrackerClient)(trackerCfg);
|
|
8315
|
+
if (!clientResult.ok) throw clientResult.error;
|
|
8316
|
+
return new GitHubIssuesIssueTrackerAdapter(clientResult.value, this.config.tracker);
|
|
8317
|
+
}
|
|
8050
8318
|
if (this.config.tracker.kind === "roadmap") {
|
|
8051
8319
|
return new RoadmapTrackerAdapter(this.config.tracker);
|
|
8052
8320
|
}
|
|
@@ -8062,8 +8330,8 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
8062
8330
|
const checkRunner = {
|
|
8063
8331
|
run: async (command, cwd) => {
|
|
8064
8332
|
const { execFile: execFile6 } = await import("child_process");
|
|
8065
|
-
const { promisify:
|
|
8066
|
-
const execFileAsync =
|
|
8333
|
+
const { promisify: promisify4 } = await import("util");
|
|
8334
|
+
const execFileAsync = promisify4(execFile6);
|
|
8067
8335
|
const [cmd, ...args] = command;
|
|
8068
8336
|
if (!cmd) return { passed: true, findings: 0, output: "" };
|
|
8069
8337
|
try {
|
|
@@ -8097,12 +8365,13 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
8097
8365
|
const commandExecutor = {
|
|
8098
8366
|
exec: async (command, cwd) => {
|
|
8099
8367
|
const { execFile: execFile6 } = await import("child_process");
|
|
8100
|
-
const { promisify:
|
|
8101
|
-
const execFileAsync =
|
|
8368
|
+
const { promisify: promisify4 } = await import("util");
|
|
8369
|
+
const execFileAsync = promisify4(execFile6);
|
|
8102
8370
|
const [cmd, ...args] = command;
|
|
8103
|
-
if (!cmd) return;
|
|
8371
|
+
if (!cmd) return { stdout: "" };
|
|
8104
8372
|
try {
|
|
8105
|
-
await execFileAsync(cmd, args, { cwd, timeout: 12e4 });
|
|
8373
|
+
const { stdout } = await execFileAsync(cmd, args, { cwd, timeout: 12e4 });
|
|
8374
|
+
return { stdout: String(stdout) };
|
|
8106
8375
|
} catch (err) {
|
|
8107
8376
|
logger.warn("Maintenance command execution failed", {
|
|
8108
8377
|
command,
|
|
@@ -8127,7 +8396,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
8127
8396
|
*/
|
|
8128
8397
|
async initMaintenance(maintenanceConfig) {
|
|
8129
8398
|
this.maintenanceReporter = new MaintenanceReporter({
|
|
8130
|
-
persistDir:
|
|
8399
|
+
persistDir: path16.join(this.projectRoot, ".harness", "maintenance"),
|
|
8131
8400
|
logger: this.logger
|
|
8132
8401
|
});
|
|
8133
8402
|
await this.maintenanceReporter.load();
|
|
@@ -8135,7 +8404,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
8135
8404
|
const reporter = this.maintenanceReporter;
|
|
8136
8405
|
this.maintenanceScheduler = new MaintenanceScheduler({
|
|
8137
8406
|
config: maintenanceConfig,
|
|
8138
|
-
|
|
8407
|
+
leaderElector: new SingleProcessLeaderElector(),
|
|
8139
8408
|
logger: this.logger,
|
|
8140
8409
|
historyProvider: reporter,
|
|
8141
8410
|
onTaskDue: async (task) => {
|
|
@@ -8178,129 +8447,14 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
8178
8447
|
}
|
|
8179
8448
|
}
|
|
8180
8449
|
createIntelligencePipeline() {
|
|
8181
|
-
const
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
if (!selProvider) return null;
|
|
8185
|
-
const routing = this.config.agent.routing;
|
|
8186
|
-
const peslName = routing?.intelligence?.pesl;
|
|
8187
|
-
const selName = routing?.intelligence?.sel ?? routing?.default;
|
|
8188
|
-
const peslProvider = peslName !== void 0 && peslName !== selName ? this.createAnalysisProvider("pesl") : null;
|
|
8189
|
-
const peslModel = intel.models?.pesl ?? this.config.agent.model;
|
|
8190
|
-
const store = new import_graph.GraphStore();
|
|
8191
|
-
this.graphStore = store;
|
|
8192
|
-
return new import_intelligence4.IntelligencePipeline(selProvider, store, {
|
|
8193
|
-
...peslModel !== void 0 && { peslModel },
|
|
8194
|
-
...peslProvider !== null && peslProvider !== void 0 && { peslProvider }
|
|
8195
|
-
});
|
|
8196
|
-
}
|
|
8197
|
-
/**
|
|
8198
|
-
* Create the AnalysisProvider for an intelligence-pipeline layer
|
|
8199
|
-
* (`sel` by default; `pesl` when constructing a distinct PESL
|
|
8200
|
-
* provider per Spec 2 SC35).
|
|
8201
|
-
*
|
|
8202
|
-
* Spec 2 Phase 4 (SC31–SC36) — resolution order:
|
|
8203
|
-
* 1. Explicit `intelligence.provider` config wins (preserves Phase 0–3 behavior; SC33).
|
|
8204
|
-
* 2. Otherwise, consult `agent.routing.intelligence.<layer>` (or
|
|
8205
|
-
* `routing.default`) to pick a `BackendDef` from `agent.backends`,
|
|
8206
|
-
* then translate via `buildAnalysisProvider` (the per-type factory).
|
|
8207
|
-
*
|
|
8208
|
-
* Closes the Phase 2 deferral (P2-DEF-638): the legacy
|
|
8209
|
-
* `this.config.agent.backend` read at the bottom of this method is
|
|
8210
|
-
* removed; routing is the sole source for non-explicit configs.
|
|
8211
|
-
*
|
|
8212
|
-
* Cyclomatic complexity: pre-Phase-4 was 33 (factory dispatch was
|
|
8213
|
-
* inlined here). Phase 4 extracts the per-type tree into
|
|
8214
|
-
* `buildAnalysisProvider`, dropping this method to ≤ 5 branches
|
|
8215
|
-
* (under the 15 threshold).
|
|
8216
|
-
*/
|
|
8217
|
-
createAnalysisProvider(layer = "sel") {
|
|
8218
|
-
const intel = this.config.intelligence;
|
|
8219
|
-
if (!intel?.enabled) return null;
|
|
8220
|
-
if (intel.provider) {
|
|
8221
|
-
const layerModel = layer === "sel" ? intel.models?.sel : intel.models?.pesl;
|
|
8222
|
-
return this.createProviderFromExplicitConfig(
|
|
8223
|
-
intel.provider,
|
|
8224
|
-
layerModel ?? this.config.agent.model
|
|
8225
|
-
);
|
|
8226
|
-
}
|
|
8227
|
-
const routed = this.resolveRoutedBackendForIntelligence(layer);
|
|
8228
|
-
if (!routed) return null;
|
|
8229
|
-
const { name, def } = routed;
|
|
8230
|
-
const resolver = this.localResolvers.get(name);
|
|
8231
|
-
return buildAnalysisProvider({
|
|
8232
|
-
def,
|
|
8233
|
-
backendName: name,
|
|
8234
|
-
layer,
|
|
8235
|
-
// Spec 2 P3-IMP-1: a single snapshot read feeds the factory's
|
|
8236
|
-
// unavailable-warn diagnostic (Configured/Detected lists) and
|
|
8237
|
-
// collapses the two `getStatus()` calls flagged by P3-SUG-2.
|
|
8238
|
-
getResolverStatusSnapshot: () => {
|
|
8239
|
-
if (!resolver) return null;
|
|
8240
|
-
const status = resolver.getStatus();
|
|
8241
|
-
return {
|
|
8242
|
-
available: status.available,
|
|
8243
|
-
resolved: status.resolved,
|
|
8244
|
-
configured: status.configured,
|
|
8245
|
-
detected: status.detected
|
|
8246
|
-
};
|
|
8247
|
-
},
|
|
8248
|
-
intelligence: intel,
|
|
8450
|
+
const bundle = buildIntelligencePipeline({
|
|
8451
|
+
config: this.config,
|
|
8452
|
+
localResolvers: this.localResolvers,
|
|
8249
8453
|
logger: this.logger
|
|
8250
8454
|
});
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
* back through `routing.intelligence.<layer>` → `routing.default`
|
|
8255
|
-
* → null. Returns the resolved name alongside the def so callers can
|
|
8256
|
-
* key into the per-name resolver map.
|
|
8257
|
-
*/
|
|
8258
|
-
resolveRoutedBackendForIntelligence(layer) {
|
|
8259
|
-
const routing = this.config.agent.routing;
|
|
8260
|
-
const backends = this.config.agent.backends;
|
|
8261
|
-
if (!routing || !backends) return null;
|
|
8262
|
-
const layerName = routing.intelligence?.[layer];
|
|
8263
|
-
const name = layerName ?? routing.default;
|
|
8264
|
-
const def = backends[name];
|
|
8265
|
-
if (!def) {
|
|
8266
|
-
this.logger.warn(
|
|
8267
|
-
`Intelligence pipeline: routed backend '${name}' for layer '${layer}' is not in agent.backends.`
|
|
8268
|
-
);
|
|
8269
|
-
return null;
|
|
8270
|
-
}
|
|
8271
|
-
return { name, def };
|
|
8272
|
-
}
|
|
8273
|
-
createProviderFromExplicitConfig(provider, selModel) {
|
|
8274
|
-
if (provider.kind === "anthropic") {
|
|
8275
|
-
const apiKey2 = provider.apiKey ?? this.config.agent.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
8276
|
-
if (!apiKey2) {
|
|
8277
|
-
throw new Error("Intelligence pipeline: no Anthropic API key found.");
|
|
8278
|
-
}
|
|
8279
|
-
return new import_intelligence4.AnthropicAnalysisProvider({
|
|
8280
|
-
apiKey: apiKey2,
|
|
8281
|
-
...selModel !== void 0 && { defaultModel: selModel }
|
|
8282
|
-
});
|
|
8283
|
-
}
|
|
8284
|
-
if (provider.kind === "claude-cli") {
|
|
8285
|
-
return new import_intelligence4.ClaudeCliAnalysisProvider({
|
|
8286
|
-
command: this.config.agent.command,
|
|
8287
|
-
...selModel !== void 0 && { defaultModel: selModel },
|
|
8288
|
-
...this.config.intelligence?.requestTimeoutMs !== void 0 && {
|
|
8289
|
-
timeoutMs: this.config.intelligence.requestTimeoutMs
|
|
8290
|
-
}
|
|
8291
|
-
});
|
|
8292
|
-
}
|
|
8293
|
-
const apiKey = provider.apiKey ?? this.config.agent.apiKey ?? "ollama";
|
|
8294
|
-
const baseUrl = provider.baseUrl ?? "http://localhost:11434/v1";
|
|
8295
|
-
const intel = this.config.intelligence;
|
|
8296
|
-
return new import_intelligence4.OpenAICompatibleAnalysisProvider({
|
|
8297
|
-
apiKey,
|
|
8298
|
-
baseUrl,
|
|
8299
|
-
...selModel !== void 0 && { defaultModel: selModel },
|
|
8300
|
-
...intel?.requestTimeoutMs !== void 0 && { timeoutMs: intel.requestTimeoutMs },
|
|
8301
|
-
...intel?.promptSuffix !== void 0 && { promptSuffix: intel.promptSuffix },
|
|
8302
|
-
...intel?.jsonMode !== void 0 && { jsonMode: intel.jsonMode }
|
|
8303
|
-
});
|
|
8455
|
+
if (!bundle) return null;
|
|
8456
|
+
this.graphStore = bundle.graphStore;
|
|
8457
|
+
return bundle.pipeline;
|
|
8304
8458
|
}
|
|
8305
8459
|
/**
|
|
8306
8460
|
* Lazily initializes the ClaimManager if it hasn't been created yet.
|
|
@@ -9360,6 +9514,150 @@ function launchTUI(orchestrator) {
|
|
|
9360
9514
|
const { waitUntilExit } = (0, import_ink5.render)(/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Dashboard, { orchestrator }));
|
|
9361
9515
|
return { waitUntilExit };
|
|
9362
9516
|
}
|
|
9517
|
+
|
|
9518
|
+
// src/maintenance/sync-main.ts
|
|
9519
|
+
var import_node_child_process9 = require("child_process");
|
|
9520
|
+
var import_node_util3 = require("util");
|
|
9521
|
+
var DEFAULT_TIMEOUT_MS2 = 6e4;
|
|
9522
|
+
async function git(execFileFn, args, cwd, timeoutMs) {
|
|
9523
|
+
const exec = (0, import_node_util3.promisify)(execFileFn);
|
|
9524
|
+
const { stdout, stderr } = await exec("git", args, { cwd, timeout: timeoutMs });
|
|
9525
|
+
return { stdout: String(stdout), stderr: String(stderr) };
|
|
9526
|
+
}
|
|
9527
|
+
function isSpawnEnoent(err) {
|
|
9528
|
+
if (!err || typeof err !== "object") return false;
|
|
9529
|
+
const code = err.code;
|
|
9530
|
+
return code === "ENOENT";
|
|
9531
|
+
}
|
|
9532
|
+
async function refExists(execFileFn, ref, cwd, timeoutMs) {
|
|
9533
|
+
try {
|
|
9534
|
+
await git(execFileFn, ["rev-parse", "--verify", "--quiet", ref], cwd, timeoutMs);
|
|
9535
|
+
return true;
|
|
9536
|
+
} catch (err) {
|
|
9537
|
+
if (isSpawnEnoent(err)) throw err;
|
|
9538
|
+
return false;
|
|
9539
|
+
}
|
|
9540
|
+
}
|
|
9541
|
+
async function resolveOriginDefault(execFileFn, cwd, timeoutMs) {
|
|
9542
|
+
try {
|
|
9543
|
+
const { stdout } = await git(
|
|
9544
|
+
execFileFn,
|
|
9545
|
+
["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
|
|
9546
|
+
cwd,
|
|
9547
|
+
timeoutMs
|
|
9548
|
+
);
|
|
9549
|
+
const v = stdout.trim();
|
|
9550
|
+
if (v) return v;
|
|
9551
|
+
} catch (err) {
|
|
9552
|
+
if (isSpawnEnoent(err)) throw err;
|
|
9553
|
+
}
|
|
9554
|
+
for (const candidate of ["origin/main", "origin/master"]) {
|
|
9555
|
+
if (await refExists(execFileFn, candidate, cwd, timeoutMs)) return candidate;
|
|
9556
|
+
}
|
|
9557
|
+
return null;
|
|
9558
|
+
}
|
|
9559
|
+
function shortName(originRef) {
|
|
9560
|
+
return originRef.startsWith("origin/") ? originRef.slice("origin/".length) : originRef;
|
|
9561
|
+
}
|
|
9562
|
+
function isDirtyConflictStderr(s) {
|
|
9563
|
+
return /would be overwritten|local changes|Aborting/i.test(s);
|
|
9564
|
+
}
|
|
9565
|
+
function extractStderr(err) {
|
|
9566
|
+
if (err && typeof err === "object" && "stderr" in err) {
|
|
9567
|
+
const raw = err.stderr;
|
|
9568
|
+
if (typeof raw === "string") return raw;
|
|
9569
|
+
if (raw instanceof Buffer) return raw.toString("utf8");
|
|
9570
|
+
}
|
|
9571
|
+
if (err instanceof Error) return err.message;
|
|
9572
|
+
if (typeof err === "string") return err;
|
|
9573
|
+
return "";
|
|
9574
|
+
}
|
|
9575
|
+
async function isAncestor(execFileFn, a, b, cwd, timeoutMs) {
|
|
9576
|
+
try {
|
|
9577
|
+
await git(execFileFn, ["merge-base", "--is-ancestor", a, b], cwd, timeoutMs);
|
|
9578
|
+
return true;
|
|
9579
|
+
} catch (err) {
|
|
9580
|
+
if (isSpawnEnoent(err)) throw err;
|
|
9581
|
+
return false;
|
|
9582
|
+
}
|
|
9583
|
+
}
|
|
9584
|
+
async function syncMain(repoRoot, opts = {}) {
|
|
9585
|
+
const execFileFn = opts.execFileFn ?? import_node_child_process9.execFile;
|
|
9586
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
9587
|
+
try {
|
|
9588
|
+
const originRef = await resolveOriginDefault(execFileFn, repoRoot, timeoutMs);
|
|
9589
|
+
if (!originRef) {
|
|
9590
|
+
return {
|
|
9591
|
+
status: "skipped",
|
|
9592
|
+
reason: "no-remote",
|
|
9593
|
+
detail: "origin/HEAD unset and neither origin/main nor origin/master resolves",
|
|
9594
|
+
defaultBranch: ""
|
|
9595
|
+
};
|
|
9596
|
+
}
|
|
9597
|
+
const defaultBranch = shortName(originRef);
|
|
9598
|
+
const { stdout: currentRaw } = await git(
|
|
9599
|
+
execFileFn,
|
|
9600
|
+
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
9601
|
+
repoRoot,
|
|
9602
|
+
timeoutMs
|
|
9603
|
+
);
|
|
9604
|
+
const current = currentRaw.trim();
|
|
9605
|
+
if (current !== defaultBranch) {
|
|
9606
|
+
return {
|
|
9607
|
+
status: "skipped",
|
|
9608
|
+
reason: "wrong-branch",
|
|
9609
|
+
detail: `current branch '${current}' is not the default '${defaultBranch}'`,
|
|
9610
|
+
defaultBranch
|
|
9611
|
+
};
|
|
9612
|
+
}
|
|
9613
|
+
try {
|
|
9614
|
+
await git(execFileFn, ["fetch", "origin", defaultBranch, "--quiet"], repoRoot, timeoutMs);
|
|
9615
|
+
} catch (err) {
|
|
9616
|
+
if (isSpawnEnoent(err)) throw err;
|
|
9617
|
+
return {
|
|
9618
|
+
status: "skipped",
|
|
9619
|
+
reason: "fetch-failed",
|
|
9620
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
9621
|
+
defaultBranch
|
|
9622
|
+
};
|
|
9623
|
+
}
|
|
9624
|
+
const headIsAncestor = await isAncestor(execFileFn, "HEAD", originRef, repoRoot, timeoutMs);
|
|
9625
|
+
const originIsAncestor = await isAncestor(execFileFn, originRef, "HEAD", repoRoot, timeoutMs);
|
|
9626
|
+
if (headIsAncestor && originIsAncestor) {
|
|
9627
|
+
return { status: "no-op", defaultBranch };
|
|
9628
|
+
}
|
|
9629
|
+
if (!headIsAncestor) {
|
|
9630
|
+
return {
|
|
9631
|
+
status: "skipped",
|
|
9632
|
+
reason: "diverged",
|
|
9633
|
+
detail: `local '${defaultBranch}' has commits not on '${originRef}'`,
|
|
9634
|
+
defaultBranch
|
|
9635
|
+
};
|
|
9636
|
+
}
|
|
9637
|
+
const before = (await git(execFileFn, ["rev-parse", "HEAD"], repoRoot, timeoutMs)).stdout.trim();
|
|
9638
|
+
try {
|
|
9639
|
+
await git(execFileFn, ["merge", "--ff-only", originRef], repoRoot, timeoutMs);
|
|
9640
|
+
} catch (err) {
|
|
9641
|
+
const stderr = extractStderr(err);
|
|
9642
|
+
if (isDirtyConflictStderr(stderr)) {
|
|
9643
|
+
return {
|
|
9644
|
+
status: "skipped",
|
|
9645
|
+
reason: "dirty-conflict",
|
|
9646
|
+
detail: stderr.split("\n")[0] ?? "merge --ff-only failed due to working-tree changes",
|
|
9647
|
+
defaultBranch
|
|
9648
|
+
};
|
|
9649
|
+
}
|
|
9650
|
+
throw err;
|
|
9651
|
+
}
|
|
9652
|
+
const after = (await git(execFileFn, ["rev-parse", "HEAD"], repoRoot, timeoutMs)).stdout.trim();
|
|
9653
|
+
return { status: "updated", from: before, to: after, defaultBranch };
|
|
9654
|
+
} catch (err) {
|
|
9655
|
+
return {
|
|
9656
|
+
status: "error",
|
|
9657
|
+
message: err instanceof Error ? err.message : String(err)
|
|
9658
|
+
};
|
|
9659
|
+
}
|
|
9660
|
+
}
|
|
9363
9661
|
// Annotate the CommonJS export names for ESM import in node:
|
|
9364
9662
|
0 && (module.exports = {
|
|
9365
9663
|
AnalysisArchive,
|
|
@@ -9404,6 +9702,7 @@ function launchTUI(orchestrator) {
|
|
|
9404
9702
|
savePublishedIndex,
|
|
9405
9703
|
selectCandidates,
|
|
9406
9704
|
sortCandidates,
|
|
9705
|
+
syncMain,
|
|
9407
9706
|
triageIssue,
|
|
9408
9707
|
validateWorkflowConfig
|
|
9409
9708
|
});
|