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