@harness-engineering/orchestrator 0.3.2 → 0.4.1

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.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
- const scopeTier = detectScopeTier(issue, artifactPresenceFromIssue(issue));
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 = (path16, name) => {
1929
+ const checkRef = (path17, name) => {
1920
1930
  if (name !== void 0 && !names.has(name)) {
1921
1931
  issues.push({
1922
- path: path16,
1923
- message: `routing.${path16.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
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
- constructor(config) {
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. Common fallbacks: `origin/main`, `origin/master`, `main`, `master`.
2369
- * 4. `HEAD` as an ultimate fallback (preserves old behavior for unusual
2370
- * repos without any of the above).
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", "main", "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 path15 = __toESM(require("path"));
2712
+ var path16 = __toESM(require("path"));
2676
2713
  var import_node_crypto7 = require("crypto");
2677
- var import_core9 = require("@harness-engineering/core");
2678
- var import_intelligence4 = require("@harness-engineering/intelligence");
2679
- var import_graph = require("@harness-engineering/graph");
2714
+ var import_core10 = require("@harness-engineering/core");
2680
2715
 
2681
2716
  // src/intelligence/pipeline-runner.ts
2682
2717
  var path7 = __toESM(require("path"));
@@ -3237,7 +3272,83 @@ var CompletionHandler = class {
3237
3272
  };
3238
3273
 
3239
3274
  // src/orchestrator.ts
3240
- var import_core10 = require("@harness-engineering/core");
3275
+ var import_core11 = require("@harness-engineering/core");
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
+ };
3241
3352
 
3242
3353
  // src/agent/runner.ts
3243
3354
  var MAX_SLEEP_MS = 12 * 60 * 6e4;
@@ -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
- function migrateAgentConfig(agent) {
3543
- const warnings = [];
3544
- const legacyFields = [
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
- const presentLegacy = legacyFields.filter((f) => f.present).map((f) => f.path);
3557
- const CASE1_ALWAYS_SUPPRESS = /* @__PURE__ */ new Set(["agent.backend"]);
3558
- const CASE1_LOCAL_GROUP = /* @__PURE__ */ new Set([
3559
- "agent.localBackend",
3560
- "agent.localEndpoint",
3561
- "agent.localModel",
3562
- "agent.localApiKey",
3563
- "agent.localTimeoutMs",
3564
- "agent.localProbeIntervalMs"
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
- const backends = {};
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
- const autoExec = agent.escalation?.autoExecute ?? [];
3587
- if (backends.local !== void 0) {
3588
- for (const tier of autoExec) {
3589
- routing[tier] = "local";
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
- for (const path16 of presentLegacy) {
3593
- warnings.push(
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
- if (agent.model === void 0) {
3618
- throw new Error("migrateAgentConfig: agent.backend='anthropic' requires agent.model");
3619
- }
3620
- const def = { type: "anthropic", model: agent.model };
3621
- if (agent.apiKey !== void 0) def.apiKey = agent.apiKey;
3622
- return def;
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
- if (agent.localEndpoint === void 0 || agent.localModel === void 0) {
3685
- throw new Error(
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 = (path16, name) => {
3762
- if (name !== void 0 && !known.has(name)) missing.push({ path: path16, name });
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: path16, name }) => `routing.${path16} -> '${name}'`).join("; ");
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 import_types9 = require("@harness-engineering/types");
3847
+ var import_types10 = require("@harness-engineering/types");
3786
3848
  function resolveExitCode(code, command, resolve6) {
3787
3849
  if (code === 0) {
3788
- resolve6((0, import_types9.Ok)(void 0));
3850
+ resolve6((0, import_types10.Ok)(void 0));
3789
3851
  } else {
3790
3852
  resolve6(
3791
- (0, import_types9.Err)({
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, import_types9.Err)({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
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, import_types9.Ok)(session);
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, import_types9.Ok)(void 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 import_types10 = require("@harness-engineering/types");
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, import_types10.Err)({
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, import_types10.Ok)(session);
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, import_types10.Ok)(void 0);
4292
+ return (0, import_types11.Ok)(void 0);
4231
4293
  }
4232
4294
  async healthCheck() {
4233
4295
  if (!this.config.apiKey) {
4234
- return (0, import_types10.Err)({
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, import_types10.Ok)(void 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 import_types11 = require("@harness-engineering/types");
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, import_types11.Err)({
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, import_types11.Ok)(session);
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, import_types11.Ok)(void 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, import_types11.Ok)(void 0);
4417
+ return (0, import_types12.Ok)(void 0);
4356
4418
  } catch (err) {
4357
- return (0, import_types11.Err)({
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 import_types12 = require("@harness-engineering/types");
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, import_types12.Err)({
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, import_types12.Ok)(session);
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, import_types12.Ok)(void 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, import_types12.Ok)(void 0);
4535
+ return (0, import_types13.Ok)(void 0);
4474
4536
  } catch (err) {
4475
- return (0, import_types12.Err)({
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 import_types13 = require("@harness-engineering/types");
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, import_types13.Err)({
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, import_types13.Ok)(session);
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, import_types13.Ok)(void 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, import_types13.Ok)(void 0);
4660
+ return (0, import_types14.Ok)(void 0);
4599
4661
  } catch (err) {
4600
- return (0, import_types13.Err)({
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 import_types14 = require("@harness-engineering/types");
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, import_types14.Err)({
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
  });
@@ -4742,7 +4804,6 @@ var PiBackend = class {
4742
4804
  const { session: piSession } = await piSdk.createAgentSession({
4743
4805
  cwd: params.workspacePath,
4744
4806
  ...model !== void 0 && { model },
4745
- tools: piSdk.codingTools,
4746
4807
  sessionManager: piSdk.SessionManager.inMemory()
4747
4808
  });
4748
4809
  const session = {
@@ -4753,9 +4814,9 @@ var PiBackend = class {
4753
4814
  piSession,
4754
4815
  unsubscribe: null
4755
4816
  };
4756
- return (0, import_types14.Ok)(session);
4817
+ return (0, import_types15.Ok)(session);
4757
4818
  } catch (err) {
4758
- return (0, import_types14.Err)({
4819
+ return (0, import_types15.Err)({
4759
4820
  category: "response_error",
4760
4821
  message: `Failed to create pi session: ${err instanceof Error ? err.message : String(err)}`
4761
4822
  });
@@ -4885,14 +4946,14 @@ var PiBackend = class {
4885
4946
  await piSession.abort();
4886
4947
  } catch {
4887
4948
  }
4888
- return (0, import_types14.Ok)(void 0);
4949
+ return (0, import_types15.Ok)(void 0);
4889
4950
  }
4890
4951
  async healthCheck() {
4891
4952
  try {
4892
4953
  await import("@mariozechner/pi-coding-agent");
4893
- return (0, import_types14.Ok)(void 0);
4954
+ return (0, import_types15.Ok)(void 0);
4894
4955
  } catch (err) {
4895
- return (0, import_types14.Err)({
4956
+ return (0, import_types15.Err)({
4896
4957
  category: "agent_not_found",
4897
4958
  message: `Pi SDK not available: ${err instanceof Error ? err.message : String(err)}`
4898
4959
  });
@@ -4955,7 +5016,7 @@ function createBackend(def) {
4955
5016
  }
4956
5017
 
4957
5018
  // src/agent/backends/container.ts
4958
- var import_types15 = require("@harness-engineering/types");
5019
+ var import_types16 = require("@harness-engineering/types");
4959
5020
  function toAgentError(message, details) {
4960
5021
  return { category: "response_error", message, details };
4961
5022
  }
@@ -4986,7 +5047,7 @@ var ContainerBackend = class {
4986
5047
  }
4987
5048
  const result = await this.secretBackend.resolveSecrets(this.secretKeys);
4988
5049
  if (!result.ok) {
4989
- return (0, import_types15.Err)(toAgentError(`Secret resolution failed: ${result.error.message}`, result.error));
5050
+ return (0, import_types16.Err)(toAgentError(`Secret resolution failed: ${result.error.message}`, result.error));
4990
5051
  }
4991
5052
  return { ok: true, value: result.value };
4992
5053
  }
@@ -5011,7 +5072,7 @@ var ContainerBackend = class {
5011
5072
  const createOpts = this.buildCreateOpts(params, envResult.value);
5012
5073
  const containerResult = await this.runtime.createContainer(createOpts);
5013
5074
  if (!containerResult.ok) {
5014
- return (0, import_types15.Err)(
5075
+ return (0, import_types16.Err)(
5015
5076
  toAgentError(
5016
5077
  `Container creation failed: ${containerResult.error.message}`,
5017
5078
  containerResult.error
@@ -5036,7 +5097,7 @@ var ContainerBackend = class {
5036
5097
  this.containerHandles.delete(session.sessionId);
5037
5098
  const removeResult = await this.runtime.removeContainer(handle);
5038
5099
  if (!removeResult.ok) {
5039
- return (0, import_types15.Err)(
5100
+ return (0, import_types16.Err)(
5040
5101
  toAgentError(
5041
5102
  `Container removal failed: ${removeResult.error.message}`,
5042
5103
  removeResult.error
@@ -5049,7 +5110,7 @@ var ContainerBackend = class {
5049
5110
  async healthCheck() {
5050
5111
  const runtimeResult = await this.runtime.healthCheck();
5051
5112
  if (!runtimeResult.ok) {
5052
- return (0, import_types15.Err)({
5113
+ return (0, import_types16.Err)({
5053
5114
  category: "agent_not_found",
5054
5115
  message: `Container runtime unhealthy: ${runtimeResult.error.message}`,
5055
5116
  details: runtimeResult.error
@@ -5061,7 +5122,7 @@ var ContainerBackend = class {
5061
5122
 
5062
5123
  // src/agent/runtime/docker.ts
5063
5124
  var import_node_child_process5 = require("child_process");
5064
- var import_types16 = require("@harness-engineering/types");
5125
+ var import_types17 = require("@harness-engineering/types");
5065
5126
  function dockerExec(args) {
5066
5127
  return new Promise((resolve6, reject) => {
5067
5128
  (0, import_node_child_process5.execFile)("docker", args, (error, stdout) => {
@@ -5094,9 +5155,9 @@ var DockerRuntime = class {
5094
5155
  args.push(opts.image);
5095
5156
  args.push("sleep", "infinity");
5096
5157
  const containerId = await dockerExec(args);
5097
- return (0, import_types16.Ok)({ containerId, runtime: this.name });
5158
+ return (0, import_types17.Ok)({ containerId, runtime: this.name });
5098
5159
  } catch (error) {
5099
- return (0, import_types16.Err)({
5160
+ return (0, import_types17.Err)({
5100
5161
  category: "container_create_failed",
5101
5162
  message: `Failed to create container: ${error instanceof Error ? error.message : String(error)}`,
5102
5163
  details: error
@@ -5140,9 +5201,9 @@ var DockerRuntime = class {
5140
5201
  async removeContainer(handle) {
5141
5202
  try {
5142
5203
  await dockerExec(["rm", "-f", handle.containerId]);
5143
- return (0, import_types16.Ok)(void 0);
5204
+ return (0, import_types17.Ok)(void 0);
5144
5205
  } catch (error) {
5145
- return (0, import_types16.Err)({
5206
+ return (0, import_types17.Err)({
5146
5207
  category: "container_remove_failed",
5147
5208
  message: `Failed to remove container: ${error instanceof Error ? error.message : String(error)}`,
5148
5209
  details: error
@@ -5152,9 +5213,9 @@ var DockerRuntime = class {
5152
5213
  async healthCheck() {
5153
5214
  try {
5154
5215
  await dockerExec(["info", "--format", "{{.ServerVersion}}"]);
5155
- return (0, import_types16.Ok)(void 0);
5216
+ return (0, import_types17.Ok)(void 0);
5156
5217
  } catch (error) {
5157
- return (0, import_types16.Err)({
5218
+ return (0, import_types17.Err)({
5158
5219
  category: "runtime_not_found",
5159
5220
  message: `Docker is not available: ${error instanceof Error ? error.message : String(error)}`,
5160
5221
  details: error
@@ -5164,7 +5225,7 @@ var DockerRuntime = class {
5164
5225
  };
5165
5226
 
5166
5227
  // src/agent/secrets/env.ts
5167
- var import_types17 = require("@harness-engineering/types");
5228
+ var import_types18 = require("@harness-engineering/types");
5168
5229
  var EnvSecretBackend = class {
5169
5230
  name = "env";
5170
5231
  async resolveSecrets(keys) {
@@ -5172,7 +5233,7 @@ var EnvSecretBackend = class {
5172
5233
  for (const key of keys) {
5173
5234
  const value = process.env[key];
5174
5235
  if (value === void 0) {
5175
- return (0, import_types17.Err)({
5236
+ return (0, import_types18.Err)({
5176
5237
  category: "secret_not_found",
5177
5238
  message: `Environment variable '${key}' is not set`,
5178
5239
  key
@@ -5180,16 +5241,16 @@ var EnvSecretBackend = class {
5180
5241
  }
5181
5242
  secrets[key] = value;
5182
5243
  }
5183
- return (0, import_types17.Ok)(secrets);
5244
+ return (0, import_types18.Ok)(secrets);
5184
5245
  }
5185
5246
  async healthCheck() {
5186
- return (0, import_types17.Ok)(void 0);
5247
+ return (0, import_types18.Ok)(void 0);
5187
5248
  }
5188
5249
  };
5189
5250
 
5190
5251
  // src/agent/secrets/onepassword.ts
5191
5252
  var import_node_child_process6 = require("child_process");
5192
- var import_types18 = require("@harness-engineering/types");
5253
+ var import_types19 = require("@harness-engineering/types");
5193
5254
  function opExec(args) {
5194
5255
  return new Promise((resolve6, reject) => {
5195
5256
  (0, import_node_child_process6.execFile)("op", args, (error, stdout) => {
@@ -5214,21 +5275,21 @@ var OnePasswordSecretBackend = class {
5214
5275
  const value = await opExec(["read", `op://${this.vault}/${key}/password`]);
5215
5276
  secrets[key] = value;
5216
5277
  } catch (error) {
5217
- return (0, import_types18.Err)({
5278
+ return (0, import_types19.Err)({
5218
5279
  category: "access_denied",
5219
5280
  message: `Failed to read secret '${key}' from 1Password: ${error instanceof Error ? error.message : String(error)}`,
5220
5281
  key
5221
5282
  });
5222
5283
  }
5223
5284
  }
5224
- return (0, import_types18.Ok)(secrets);
5285
+ return (0, import_types19.Ok)(secrets);
5225
5286
  }
5226
5287
  async healthCheck() {
5227
5288
  try {
5228
5289
  await opExec(["--version"]);
5229
- return (0, import_types18.Ok)(void 0);
5290
+ return (0, import_types19.Ok)(void 0);
5230
5291
  } catch (error) {
5231
- return (0, import_types18.Err)({
5292
+ return (0, import_types19.Err)({
5232
5293
  category: "provider_unavailable",
5233
5294
  message: `1Password CLI is not available: ${error instanceof Error ? error.message : String(error)}`
5234
5295
  });
@@ -5238,7 +5299,7 @@ var OnePasswordSecretBackend = class {
5238
5299
 
5239
5300
  // src/agent/secrets/vault.ts
5240
5301
  var import_node_child_process7 = require("child_process");
5241
- var import_types19 = require("@harness-engineering/types");
5302
+ var import_types20 = require("@harness-engineering/types");
5242
5303
  function vaultExec(args, env) {
5243
5304
  return new Promise((resolve6, reject) => {
5244
5305
  (0, import_node_child_process7.execFile)("vault", args, { env: { ...process.env, ...env } }, (error, stdout) => {
@@ -5269,11 +5330,11 @@ var VaultSecretBackend = class {
5269
5330
  } catch (error) {
5270
5331
  const msg = error instanceof Error ? error.message : String(error);
5271
5332
  const category = error instanceof SyntaxError ? "access_denied" : "access_denied";
5272
- return (0, import_types19.Err)({ category, message: `Failed to read from Vault: ${msg}` });
5333
+ return (0, import_types20.Err)({ category, message: `Failed to read from Vault: ${msg}` });
5273
5334
  }
5274
5335
  const missing = keys.find((k) => !(k in data));
5275
5336
  if (missing) {
5276
- return (0, import_types19.Err)({
5337
+ return (0, import_types20.Err)({
5277
5338
  category: "secret_not_found",
5278
5339
  message: `Secret key '${missing}' not found in Vault path '${this.path}'`,
5279
5340
  key: missing
@@ -5281,14 +5342,14 @@ var VaultSecretBackend = class {
5281
5342
  }
5282
5343
  const secrets = {};
5283
5344
  for (const key of keys) secrets[key] = data[key];
5284
- return (0, import_types19.Ok)(secrets);
5345
+ return (0, import_types20.Ok)(secrets);
5285
5346
  }
5286
5347
  async healthCheck() {
5287
5348
  try {
5288
5349
  await vaultExec(["version"]);
5289
- return (0, import_types19.Ok)(void 0);
5350
+ return (0, import_types20.Ok)(void 0);
5290
5351
  } catch (error) {
5291
- return (0, import_types19.Err)({
5352
+ return (0, import_types20.Err)({
5292
5353
  category: "provider_unavailable",
5293
5354
  message: `Vault CLI is not available: ${error instanceof Error ? error.message : String(error)}`
5294
5355
  });
@@ -5403,6 +5464,10 @@ var OrchestratorBackendFactory = class {
5403
5464
  }
5404
5465
  };
5405
5466
 
5467
+ // src/agent/intelligence-factory.ts
5468
+ var import_intelligence3 = require("@harness-engineering/intelligence");
5469
+ var import_graph = require("@harness-engineering/graph");
5470
+
5406
5471
  // src/agent/analysis-provider-factory.ts
5407
5472
  var import_intelligence2 = require("@harness-engineering/intelligence");
5408
5473
  function buildAnalysisProvider(args) {
@@ -5500,9 +5565,110 @@ function buildClaudeCliProvider(def, args, layerModel) {
5500
5565
  });
5501
5566
  }
5502
5567
 
5568
+ // src/agent/intelligence-factory.ts
5569
+ function buildIntelligencePipeline(deps) {
5570
+ const { config } = deps;
5571
+ const intel = config.intelligence;
5572
+ if (!intel?.enabled) return null;
5573
+ const selProvider = buildAnalysisProviderForLayer("sel", deps);
5574
+ if (!selProvider) return null;
5575
+ const routing = config.agent.routing;
5576
+ const peslName = routing?.intelligence?.pesl;
5577
+ const selName = routing?.intelligence?.sel ?? routing?.default;
5578
+ const peslProvider = peslName !== void 0 && peslName !== selName ? buildAnalysisProviderForLayer("pesl", deps) : null;
5579
+ const peslModel = intel.models?.pesl ?? config.agent.model;
5580
+ const graphStore = new import_graph.GraphStore();
5581
+ const pipeline = new import_intelligence3.IntelligencePipeline(selProvider, graphStore, {
5582
+ ...peslModel !== void 0 && { peslModel },
5583
+ ...peslProvider !== null && peslProvider !== void 0 && { peslProvider }
5584
+ });
5585
+ return { pipeline, graphStore };
5586
+ }
5587
+ function buildAnalysisProviderForLayer(layer, deps) {
5588
+ const { config, localResolvers, logger } = deps;
5589
+ const intel = config.intelligence;
5590
+ if (!intel?.enabled) return null;
5591
+ if (intel.provider) {
5592
+ const layerModel = layer === "sel" ? intel.models?.sel : intel.models?.pesl;
5593
+ return buildExplicitProvider(intel.provider, layerModel ?? config.agent.model, config);
5594
+ }
5595
+ const routed = resolveRoutedBackend(layer, config, logger);
5596
+ if (!routed) return null;
5597
+ const { name, def } = routed;
5598
+ const resolver = localResolvers.get(name);
5599
+ return buildAnalysisProvider({
5600
+ def,
5601
+ backendName: name,
5602
+ layer,
5603
+ // Spec 2 P3-IMP-1: a single snapshot read feeds the factory's
5604
+ // unavailable-warn diagnostic (Configured/Detected lists) and
5605
+ // collapses the two `getStatus()` calls flagged by P3-SUG-2.
5606
+ getResolverStatusSnapshot: () => {
5607
+ if (!resolver) return null;
5608
+ const status = resolver.getStatus();
5609
+ return {
5610
+ available: status.available,
5611
+ resolved: status.resolved,
5612
+ configured: status.configured,
5613
+ detected: status.detected
5614
+ };
5615
+ },
5616
+ intelligence: intel,
5617
+ logger
5618
+ });
5619
+ }
5620
+ function resolveRoutedBackend(layer, config, logger) {
5621
+ const routing = config.agent.routing;
5622
+ const backends = config.agent.backends;
5623
+ if (!routing || !backends) return null;
5624
+ const layerName = routing.intelligence?.[layer];
5625
+ const name = layerName ?? routing.default;
5626
+ const def = backends[name];
5627
+ if (!def) {
5628
+ logger.warn(
5629
+ `Intelligence pipeline: routed backend '${name}' for layer '${layer}' is not in agent.backends.`
5630
+ );
5631
+ return null;
5632
+ }
5633
+ return { name, def };
5634
+ }
5635
+ function buildExplicitProvider(provider, selModel, config) {
5636
+ if (provider.kind === "anthropic") {
5637
+ const apiKey2 = provider.apiKey ?? config.agent.apiKey ?? process.env.ANTHROPIC_API_KEY;
5638
+ if (!apiKey2) {
5639
+ throw new Error("Intelligence pipeline: no Anthropic API key found.");
5640
+ }
5641
+ return new import_intelligence3.AnthropicAnalysisProvider({
5642
+ apiKey: apiKey2,
5643
+ ...selModel !== void 0 && { defaultModel: selModel }
5644
+ });
5645
+ }
5646
+ if (provider.kind === "claude-cli") {
5647
+ return new import_intelligence3.ClaudeCliAnalysisProvider({
5648
+ command: config.agent.command,
5649
+ ...selModel !== void 0 && { defaultModel: selModel },
5650
+ ...config.intelligence?.requestTimeoutMs !== void 0 && {
5651
+ timeoutMs: config.intelligence.requestTimeoutMs
5652
+ }
5653
+ });
5654
+ }
5655
+ const apiKey = provider.apiKey ?? config.agent.apiKey ?? "ollama";
5656
+ const baseUrl = provider.baseUrl ?? "http://localhost:11434/v1";
5657
+ const intel = config.intelligence;
5658
+ return new import_intelligence3.OpenAICompatibleAnalysisProvider({
5659
+ apiKey,
5660
+ baseUrl,
5661
+ ...selModel !== void 0 && { defaultModel: selModel },
5662
+ ...intel?.requestTimeoutMs !== void 0 && { timeoutMs: intel.requestTimeoutMs },
5663
+ ...intel?.promptSuffix !== void 0 && { promptSuffix: intel.promptSuffix },
5664
+ ...intel?.jsonMode !== void 0 && { jsonMode: intel.jsonMode }
5665
+ });
5666
+ }
5667
+
5503
5668
  // src/server/http.ts
5504
5669
  var http = __toESM(require("http"));
5505
- var path13 = __toESM(require("path"));
5670
+ var path14 = __toESM(require("path"));
5671
+ var import_core8 = require("@harness-engineering/core");
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 import_intelligence3 = require("@harness-engineering/intelligence");
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, import_intelligence3.manualToRawWorkItem)({
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, import_intelligence3.scoreToConcernSignals)(score);
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 path10 = __toESM(require("path"));
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) || path10.basename(id) === id && !id.includes("..");
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(path10.posix.sep);
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
- path10.join(sessionsDir, entry.name, "session.json"),
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(path10.join(sessionsDir, id, "session.json"), "utf-8");
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 = path10.join(sessionsDir, session.sessionId);
6607
+ const sessionDir = path11.join(sessionsDir, session.sessionId);
6399
6608
  await fs11.mkdir(sessionDir, { recursive: true });
6400
- await fs11.writeFile(path10.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
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 = path10.join(sessionsDir, id, "session.json");
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(path10.join(sessionsDir, id), { recursive: true, force: true });
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 path11 = __toESM(require("path"));
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 = path11.posix.join(path11.posix.sep, "api", path11.posix.sep);
6592
- const wsPath = path11.posix.join(path11.posix.sep, "ws");
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 = path11.join(dashboardDir, urlPath === "/" ? "index.html" : urlPath);
6596
- const resolved = path11.resolve(requestedPath);
6597
- if (!resolved.startsWith(path11.resolve(dashboardDir))) {
6598
- return serveFile(path11.join(dashboardDir, "index.html"), res);
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 = path11.join(dashboardDir, "index.html");
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 = path11.extname(filePath).toLowerCase();
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 path12 = __toESM(require("path"));
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 = path12.join(this.plansDir, filename);
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 ?? path13.resolve("docs", "plans");
6742
- this.dashboardDir = deps?.dashboardDir ?? path13.resolve("packages", "dashboard", "dist", "client");
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 ?? path13.resolve(".harness", "sessions");
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 error details)
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
- if (this.interactionQueue && handleInteractionsRoute(req, res, this.interactionQueue)) {
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
  }
@@ -6900,6 +7104,7 @@ var OrchestratorServer = class {
6900
7104
  return this.broadcaster.clientCount;
6901
7105
  }
6902
7106
  async start() {
7107
+ (0, import_core8.assertPortUsable)(this.port, "orchestrator");
6903
7108
  if (this.interactionQueue) {
6904
7109
  this.planWatcher = new PlanWatcher(this.plansDir, this.interactionQueue);
6905
7110
  this.planWatcher.start();
@@ -6964,7 +7169,7 @@ var StructuredLogger = class {
6964
7169
  // src/workspace/config-scanner.ts
6965
7170
  var import_node_fs = require("fs");
6966
7171
  var import_node_path = require("path");
6967
- var import_core8 = require("@harness-engineering/core");
7172
+ var import_core9 = require("@harness-engineering/core");
6968
7173
  var CONFIG_FILES = ["CLAUDE.md", "AGENTS.md", ".gemini/settings.json", "skill.yaml"];
6969
7174
  var BLOCKING_INJECTION_PREFIXES = ["INJ-UNI-", "INJ-REROL-"];
6970
7175
  var DOWNGRADED_SECURITY_RULES = /* @__PURE__ */ new Set(["SEC-AGT-006"]);
@@ -6986,25 +7191,25 @@ async function scanSingleFile(filePath, targetDir, scanner) {
6986
7191
  } catch {
6987
7192
  return null;
6988
7193
  }
6989
- const injectionFindings = (0, import_core8.scanForInjection)(content);
6990
- const findings = (0, import_core8.mapInjectionFindings)(injectionFindings);
7194
+ const injectionFindings = (0, import_core9.scanForInjection)(content);
7195
+ const findings = (0, import_core9.mapInjectionFindings)(injectionFindings);
6991
7196
  const secFindings = await scanner.scanFile(filePath);
6992
- findings.push(...(0, import_core8.mapSecurityFindings)(secFindings, findings));
7197
+ findings.push(...(0, import_core9.mapSecurityFindings)(secFindings, findings));
6993
7198
  const adjusted = adjustFindingSeverity(findings);
6994
7199
  return {
6995
7200
  file: (0, import_node_path.relative)(targetDir, filePath).replaceAll("\\", "/"),
6996
7201
  findings: adjusted,
6997
- overallSeverity: (0, import_core8.computeOverallSeverity)(adjusted)
7202
+ overallSeverity: (0, import_core9.computeOverallSeverity)(adjusted)
6998
7203
  };
6999
7204
  }
7000
7205
  async function scanWorkspaceConfig(workspacePath) {
7001
- const scanner = new import_core8.SecurityScanner((0, import_core8.parseSecurityConfig)({}));
7206
+ const scanner = new import_core9.SecurityScanner((0, import_core9.parseSecurityConfig)({}));
7002
7207
  const results = [];
7003
7208
  for (const configFile of CONFIG_FILES) {
7004
7209
  const result = await scanSingleFile((0, import_node_path.join)(workspacePath, configFile), workspacePath, scanner);
7005
7210
  if (result) results.push(result);
7006
7211
  }
7007
- return { exitCode: (0, import_core8.computeScanExitCode)(results), results };
7212
+ return { exitCode: (0, import_core9.computeScanExitCode)(results), results };
7008
7213
  }
7009
7214
 
7010
7215
  // src/maintenance/task-registry.ts
@@ -7179,6 +7384,14 @@ var BUILT_IN_TASKS = [
7179
7384
  schedule: "0 7 * * *",
7180
7385
  branch: null,
7181
7386
  checkCommand: ["perf", "baselines", "update"]
7387
+ },
7388
+ {
7389
+ id: "main-sync",
7390
+ type: "housekeeping",
7391
+ description: "Fast-forward local default branch from origin",
7392
+ schedule: "*/15 * * * *",
7393
+ branch: null,
7394
+ checkCommand: ["harness", "sync-main", "--json"]
7182
7395
  }
7183
7396
  ];
7184
7397
 
@@ -7249,7 +7462,7 @@ function cronMatchesNow(expression, now) {
7249
7462
  // src/maintenance/scheduler.ts
7250
7463
  var MaintenanceScheduler = class {
7251
7464
  config;
7252
- claimManager;
7465
+ leaderElector;
7253
7466
  logger;
7254
7467
  onTaskDue;
7255
7468
  historyProvider;
@@ -7264,7 +7477,7 @@ var MaintenanceScheduler = class {
7264
7477
  activeRun = null;
7265
7478
  constructor(options) {
7266
7479
  this.config = options.config;
7267
- this.claimManager = options.claimManager;
7480
+ this.leaderElector = options.leaderElector;
7268
7481
  this.logger = options.logger;
7269
7482
  this.historyProvider = options.historyProvider ?? null;
7270
7483
  this.onTaskDue = options.onTaskDue;
@@ -7339,12 +7552,12 @@ var MaintenanceScheduler = class {
7339
7552
  await this.processQueue(queue, epochMinute);
7340
7553
  }
7341
7554
  /**
7342
- * Attempt to claim leadership via ClaimManager.
7555
+ * Attempt to claim leadership via the configured LeaderElector.
7343
7556
  * Returns true if this instance is the leader.
7344
7557
  */
7345
7558
  async attemptLeaderClaim(evalTime) {
7346
7559
  try {
7347
- const result = await this.claimManager.claimAndVerify("maintenance-leader");
7560
+ const result = await this.leaderElector.electLeader();
7348
7561
  if (!result.ok) {
7349
7562
  this.isLeader = false;
7350
7563
  this.logger.warn("Maintenance leader claim failed", { error: result.error?.message });
@@ -7423,6 +7636,7 @@ var MaintenanceScheduler = class {
7423
7636
  const history = this.historyProvider ? this.historyProvider.getHistory(200, 0) : this.internalHistory;
7424
7637
  const schedule = this.resolvedTasks.map((task) => ({
7425
7638
  taskId: task.id,
7639
+ type: task.type,
7426
7640
  nextRun: this.computeNextRun(task.schedule),
7427
7641
  lastRun: history.find((r) => r.taskId === task.id) ?? null
7428
7642
  }));
@@ -7459,9 +7673,17 @@ var MaintenanceScheduler = class {
7459
7673
  }
7460
7674
  };
7461
7675
 
7676
+ // src/maintenance/leader-elector.ts
7677
+ var import_types21 = require("@harness-engineering/types");
7678
+ var SingleProcessLeaderElector = class {
7679
+ async electLeader() {
7680
+ return (0, import_types21.Ok)("claimed");
7681
+ }
7682
+ };
7683
+
7462
7684
  // src/maintenance/reporter.ts
7463
7685
  var fs14 = __toESM(require("fs"));
7464
- var path14 = __toESM(require("path"));
7686
+ var path15 = __toESM(require("path"));
7465
7687
  var import_zod11 = require("zod");
7466
7688
  var RunResultSchema = import_zod11.z.object({
7467
7689
  taskId: import_zod11.z.string(),
@@ -7497,7 +7719,7 @@ var MaintenanceReporter = class {
7497
7719
  async load() {
7498
7720
  try {
7499
7721
  await fs14.promises.mkdir(this.persistDir, { recursive: true });
7500
- const filePath = path14.join(this.persistDir, "history.json");
7722
+ const filePath = path15.join(this.persistDir, "history.json");
7501
7723
  const data = await fs14.promises.readFile(filePath, "utf-8");
7502
7724
  const parsed = import_zod11.z.array(RunResultSchema).safeParse(JSON.parse(data));
7503
7725
  if (parsed.success) {
@@ -7533,7 +7755,7 @@ var MaintenanceReporter = class {
7533
7755
  async persist() {
7534
7756
  try {
7535
7757
  await fs14.promises.mkdir(this.persistDir, { recursive: true });
7536
- const filePath = path14.join(this.persistDir, "history.json");
7758
+ const filePath = path15.join(this.persistDir, "history.json");
7537
7759
  await fs14.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
7538
7760
  } catch (err) {
7539
7761
  this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
@@ -7754,22 +7976,39 @@ var TaskRunner = class {
7754
7976
  }
7755
7977
  /**
7756
7978
  * Housekeeping: run command directly, no AI, no PR.
7979
+ *
7980
+ * Captures stdout and parses a trailing JSON status line if present.
7981
+ * Recognized contracts:
7982
+ * - Phase 4/5 status contract (e.g., harness pulse run): success/skipped/failure/no-issues
7983
+ * - sync-main contract: updated/no-op/skipped/error → mapped onto the run-result status
7984
+ * Legacy housekeeping commands that emit no JSON keep the prior behavior:
7985
+ * status: 'success', findings: 0.
7757
7986
  */
7758
7987
  async runHousekeeping(task, startedAt) {
7759
7988
  if (!task.checkCommand || task.checkCommand.length === 0) {
7760
7989
  return this.failureResult(task.id, startedAt, "housekeeping task missing checkCommand");
7761
7990
  }
7762
- await this.commandExecutor.exec(task.checkCommand, this.cwd);
7763
- return {
7991
+ let stdout;
7992
+ try {
7993
+ const out = await this.commandExecutor.exec(task.checkCommand, this.cwd);
7994
+ stdout = out.stdout ?? "";
7995
+ } catch (err) {
7996
+ return this.failureResult(task.id, startedAt, String(err));
7997
+ }
7998
+ const parsed = parseStatusLine(stdout);
7999
+ const status = parsed?.status ?? "success";
8000
+ const result = {
7764
8001
  taskId: task.id,
7765
8002
  startedAt,
7766
8003
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
7767
- status: "success",
8004
+ status,
7768
8005
  findings: 0,
7769
8006
  fixed: 0,
7770
8007
  prUrl: null,
7771
8008
  prUpdated: false
7772
8009
  };
8010
+ if (parsed?.error) result.error = parsed.error;
8011
+ return result;
7773
8012
  }
7774
8013
  /**
7775
8014
  * Resolve which AI backend name to use for a given task.
@@ -7803,7 +8042,7 @@ function parseStatusLine(output) {
7803
8042
  const obj = JSON.parse(line);
7804
8043
  const s = obj.status;
7805
8044
  if (s === "success" || s === "skipped" || s === "failure" || s === "no-issues") {
7806
- const parsed = { status: s };
8045
+ const parsed = { status: s, rawStatus: s };
7807
8046
  if (typeof obj.candidatesFound === "number") {
7808
8047
  parsed.candidatesFound = obj.candidatesFound;
7809
8048
  }
@@ -7813,8 +8052,18 @@ function parseStatusLine(output) {
7813
8052
  if (typeof obj.reason === "string") {
7814
8053
  parsed.reason = obj.reason;
7815
8054
  }
8055
+ if (typeof obj.detail === "string" && !parsed.error) {
8056
+ parsed.error = `${parsed.reason ?? "skipped"}: ${obj.detail}`;
8057
+ }
7816
8058
  return parsed;
7817
8059
  }
8060
+ if (s === "updated" || s === "no-op") {
8061
+ return { status: "success", rawStatus: s };
8062
+ }
8063
+ if (s === "error") {
8064
+ const message = typeof obj.message === "string" ? obj.message : "unknown error";
8065
+ return { status: "failure", error: message, rawStatus: "error" };
8066
+ }
7818
8067
  } catch {
7819
8068
  }
7820
8069
  }
@@ -7895,11 +8144,15 @@ var Orchestrator = class extends import_node_events.EventEmitter {
7895
8144
  completionHandler;
7896
8145
  /** Project root directory, derived from workspace root. */
7897
8146
  get projectRoot() {
7898
- return path15.resolve(this.config.workspace.root, "..", "..");
8147
+ return path16.resolve(this.config.workspace.root, "..", "..");
7899
8148
  }
7900
8149
  enrichedSpecsByIssue = /* @__PURE__ */ new Map();
7901
8150
  /** Tracks recently-failed intelligence analysis to avoid re-requesting every tick */
7902
8151
  analysisFailureCache = /* @__PURE__ */ new Map();
8152
+ // Phase 3 added a private `roadmapMode` field used by `createTracker` to
8153
+ // guard the file-less stub. Phase 4 / S2 / D-P4-E shifted dispatch onto
8154
+ // `tracker.kind`, removing the need for the field — it is now dropped to
8155
+ // satisfy `noUnusedLocals`. See decision D-P3-orchestrator-mode-via-fs-read.
7903
8156
  /** Abort controllers and PIDs for running agent tasks — used by stopIssue to cancel in-flight work.
7904
8157
  * The PID is stored here because the running entry may be deleted by the state machine
7905
8158
  * before the stop effect executes (e.g., stall_detected removes the entry first). */
@@ -7935,14 +8188,19 @@ var Orchestrator = class extends import_node_events.EventEmitter {
7935
8188
  );
7936
8189
  }
7937
8190
  this.tracker = overrides?.tracker || this.createTracker();
7938
- this.workspace = new WorkspaceManager(config.workspace);
8191
+ this.workspace = new WorkspaceManager(config.workspace, {
8192
+ emitEvent: (event) => {
8193
+ this.server?.broadcastMaintenance("maintenance:baseref_fallback", event);
8194
+ this.emit("maintenance:baseref_fallback", event);
8195
+ }
8196
+ });
7939
8197
  this.hooks = new WorkspaceHooks(config.hooks);
7940
8198
  this.renderer = new PromptRenderer();
7941
8199
  this.overrideBackend = overrides?.backend ?? null;
7942
8200
  this.interactionQueue = new InteractionQueue(
7943
- path15.join(config.workspace.root, "..", "interactions")
8201
+ path16.join(config.workspace.root, "..", "interactions")
7944
8202
  );
7945
- this.analysisArchive = new AnalysisArchive(path15.join(config.workspace.root, "..", "analyses"));
8203
+ this.analysisArchive = new AnalysisArchive(path16.join(config.workspace.root, "..", "analyses"));
7946
8204
  const backendsMap = this.config.agent.backends ?? {};
7947
8205
  for (const [name, def] of Object.entries(backendsMap)) {
7948
8206
  if (def.type === "local" || def.type === "pi") {
@@ -7984,7 +8242,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
7984
8242
  ...overrides?.execFileFn ? { execFileFn: overrides.execFileFn } : {}
7985
8243
  });
7986
8244
  this.recorder = new StreamRecorder(
7987
- path15.resolve(config.workspace.root, "..", "streams"),
8245
+ path16.resolve(config.workspace.root, "..", "streams"),
7988
8246
  this.logger
7989
8247
  );
7990
8248
  const self = this;
@@ -8016,7 +8274,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
8016
8274
  if (config.server?.port) {
8017
8275
  this.server = new OrchestratorServer(this, config.server.port, {
8018
8276
  interactionQueue: this.interactionQueue,
8019
- plansDir: path15.resolve(config.workspace.root, "..", "docs", "plans"),
8277
+ plansDir: path16.resolve(config.workspace.root, "..", "docs", "plans"),
8020
8278
  pipeline: this.pipeline,
8021
8279
  analysisArchive: this.analysisArchive,
8022
8280
  roadmapPath: config.tracker.filePath ?? null,
@@ -8047,6 +8305,17 @@ var Orchestrator = class extends import_node_events.EventEmitter {
8047
8305
  }
8048
8306
  }
8049
8307
  createTracker() {
8308
+ if (this.config.tracker.kind === "github-issues") {
8309
+ const trackerCfg = {
8310
+ kind: "github-issues",
8311
+ repo: this.config.tracker.projectSlug ?? "",
8312
+ ...this.config.tracker.apiKey ? { token: this.config.tracker.apiKey } : {},
8313
+ ...this.config.tracker.endpoint ? { apiBase: this.config.tracker.endpoint } : {}
8314
+ };
8315
+ const clientResult = (0, import_core11.createTrackerClient)(trackerCfg);
8316
+ if (!clientResult.ok) throw clientResult.error;
8317
+ return new GitHubIssuesIssueTrackerAdapter(clientResult.value, this.config.tracker);
8318
+ }
8050
8319
  if (this.config.tracker.kind === "roadmap") {
8051
8320
  return new RoadmapTrackerAdapter(this.config.tracker);
8052
8321
  }
@@ -8062,8 +8331,8 @@ var Orchestrator = class extends import_node_events.EventEmitter {
8062
8331
  const checkRunner = {
8063
8332
  run: async (command, cwd) => {
8064
8333
  const { execFile: execFile6 } = await import("child_process");
8065
- const { promisify: promisify3 } = await import("util");
8066
- const execFileAsync = promisify3(execFile6);
8334
+ const { promisify: promisify4 } = await import("util");
8335
+ const execFileAsync = promisify4(execFile6);
8067
8336
  const [cmd, ...args] = command;
8068
8337
  if (!cmd) return { passed: true, findings: 0, output: "" };
8069
8338
  try {
@@ -8097,12 +8366,13 @@ var Orchestrator = class extends import_node_events.EventEmitter {
8097
8366
  const commandExecutor = {
8098
8367
  exec: async (command, cwd) => {
8099
8368
  const { execFile: execFile6 } = await import("child_process");
8100
- const { promisify: promisify3 } = await import("util");
8101
- const execFileAsync = promisify3(execFile6);
8369
+ const { promisify: promisify4 } = await import("util");
8370
+ const execFileAsync = promisify4(execFile6);
8102
8371
  const [cmd, ...args] = command;
8103
- if (!cmd) return;
8372
+ if (!cmd) return { stdout: "" };
8104
8373
  try {
8105
- await execFileAsync(cmd, args, { cwd, timeout: 12e4 });
8374
+ const { stdout } = await execFileAsync(cmd, args, { cwd, timeout: 12e4 });
8375
+ return { stdout: String(stdout) };
8106
8376
  } catch (err) {
8107
8377
  logger.warn("Maintenance command execution failed", {
8108
8378
  command,
@@ -8127,7 +8397,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
8127
8397
  */
8128
8398
  async initMaintenance(maintenanceConfig) {
8129
8399
  this.maintenanceReporter = new MaintenanceReporter({
8130
- persistDir: path15.join(this.projectRoot, ".harness", "maintenance"),
8400
+ persistDir: path16.join(this.projectRoot, ".harness", "maintenance"),
8131
8401
  logger: this.logger
8132
8402
  });
8133
8403
  await this.maintenanceReporter.load();
@@ -8135,7 +8405,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
8135
8405
  const reporter = this.maintenanceReporter;
8136
8406
  this.maintenanceScheduler = new MaintenanceScheduler({
8137
8407
  config: maintenanceConfig,
8138
- claimManager: this.claimManager,
8408
+ leaderElector: new SingleProcessLeaderElector(),
8139
8409
  logger: this.logger,
8140
8410
  historyProvider: reporter,
8141
8411
  onTaskDue: async (task) => {
@@ -8178,129 +8448,14 @@ var Orchestrator = class extends import_node_events.EventEmitter {
8178
8448
  }
8179
8449
  }
8180
8450
  createIntelligencePipeline() {
8181
- const intel = this.config.intelligence;
8182
- if (!intel?.enabled) return null;
8183
- const selProvider = this.createAnalysisProvider("sel");
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,
8451
+ const bundle = buildIntelligencePipeline({
8452
+ config: this.config,
8453
+ localResolvers: this.localResolvers,
8249
8454
  logger: this.logger
8250
8455
  });
8251
- }
8252
- /**
8253
- * Look up the routed BackendDef for an intelligence layer, falling
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
- });
8456
+ if (!bundle) return null;
8457
+ this.graphStore = bundle.graphStore;
8458
+ return bundle.pipeline;
8304
8459
  }
8305
8460
  /**
8306
8461
  * Lazily initializes the ClaimManager if it hasn't been created yet.
@@ -8681,12 +8836,12 @@ var Orchestrator = class extends import_node_events.EventEmitter {
8681
8836
  async postLifecycleComment(identifier, externalId, event) {
8682
8837
  try {
8683
8838
  if (!externalId) return;
8684
- const trackerConfig = (0, import_core10.loadTrackerSyncConfig)(this.projectRoot);
8839
+ const trackerConfig = (0, import_core11.loadTrackerSyncConfig)(this.projectRoot);
8685
8840
  if (!trackerConfig) return;
8686
8841
  const token = process.env.GITHUB_TOKEN;
8687
8842
  if (!token) return;
8688
8843
  const orchestratorId = await this.orchestratorIdPromise;
8689
- const adapter = new import_core10.GitHubIssuesSyncAdapter({ token, config: trackerConfig });
8844
+ const adapter = new import_core11.GitHubIssuesSyncAdapter({ token, config: trackerConfig });
8690
8845
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
8691
8846
  const actionMap = {
8692
8847
  claimed: "Dispatching agent for autonomous execution",
@@ -8759,7 +8914,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
8759
8914
  ...f.line !== void 0 ? { line: f.line } : {}
8760
8915
  }))
8761
8916
  );
8762
- (0, import_core9.writeTaint)(
8917
+ (0, import_core10.writeTaint)(
8763
8918
  workspacePath,
8764
8919
  issue.id,
8765
8920
  "Medium-severity injection patterns found in workspace config files",
@@ -9360,6 +9515,150 @@ function launchTUI(orchestrator) {
9360
9515
  const { waitUntilExit } = (0, import_ink5.render)(/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Dashboard, { orchestrator }));
9361
9516
  return { waitUntilExit };
9362
9517
  }
9518
+
9519
+ // src/maintenance/sync-main.ts
9520
+ var import_node_child_process9 = require("child_process");
9521
+ var import_node_util3 = require("util");
9522
+ var DEFAULT_TIMEOUT_MS2 = 6e4;
9523
+ async function git(execFileFn, args, cwd, timeoutMs) {
9524
+ const exec = (0, import_node_util3.promisify)(execFileFn);
9525
+ const { stdout, stderr } = await exec("git", args, { cwd, timeout: timeoutMs });
9526
+ return { stdout: String(stdout), stderr: String(stderr) };
9527
+ }
9528
+ function isSpawnEnoent(err) {
9529
+ if (!err || typeof err !== "object") return false;
9530
+ const code = err.code;
9531
+ return code === "ENOENT";
9532
+ }
9533
+ async function refExists(execFileFn, ref, cwd, timeoutMs) {
9534
+ try {
9535
+ await git(execFileFn, ["rev-parse", "--verify", "--quiet", ref], cwd, timeoutMs);
9536
+ return true;
9537
+ } catch (err) {
9538
+ if (isSpawnEnoent(err)) throw err;
9539
+ return false;
9540
+ }
9541
+ }
9542
+ async function resolveOriginDefault(execFileFn, cwd, timeoutMs) {
9543
+ try {
9544
+ const { stdout } = await git(
9545
+ execFileFn,
9546
+ ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
9547
+ cwd,
9548
+ timeoutMs
9549
+ );
9550
+ const v = stdout.trim();
9551
+ if (v) return v;
9552
+ } catch (err) {
9553
+ if (isSpawnEnoent(err)) throw err;
9554
+ }
9555
+ for (const candidate of ["origin/main", "origin/master"]) {
9556
+ if (await refExists(execFileFn, candidate, cwd, timeoutMs)) return candidate;
9557
+ }
9558
+ return null;
9559
+ }
9560
+ function shortName(originRef) {
9561
+ return originRef.startsWith("origin/") ? originRef.slice("origin/".length) : originRef;
9562
+ }
9563
+ function isDirtyConflictStderr(s) {
9564
+ return /would be overwritten|local changes|Aborting/i.test(s);
9565
+ }
9566
+ function extractStderr(err) {
9567
+ if (err && typeof err === "object" && "stderr" in err) {
9568
+ const raw = err.stderr;
9569
+ if (typeof raw === "string") return raw;
9570
+ if (raw instanceof Buffer) return raw.toString("utf8");
9571
+ }
9572
+ if (err instanceof Error) return err.message;
9573
+ if (typeof err === "string") return err;
9574
+ return "";
9575
+ }
9576
+ async function isAncestor(execFileFn, a, b, cwd, timeoutMs) {
9577
+ try {
9578
+ await git(execFileFn, ["merge-base", "--is-ancestor", a, b], cwd, timeoutMs);
9579
+ return true;
9580
+ } catch (err) {
9581
+ if (isSpawnEnoent(err)) throw err;
9582
+ return false;
9583
+ }
9584
+ }
9585
+ async function syncMain(repoRoot, opts = {}) {
9586
+ const execFileFn = opts.execFileFn ?? import_node_child_process9.execFile;
9587
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
9588
+ try {
9589
+ const originRef = await resolveOriginDefault(execFileFn, repoRoot, timeoutMs);
9590
+ if (!originRef) {
9591
+ return {
9592
+ status: "skipped",
9593
+ reason: "no-remote",
9594
+ detail: "origin/HEAD unset and neither origin/main nor origin/master resolves",
9595
+ defaultBranch: ""
9596
+ };
9597
+ }
9598
+ const defaultBranch = shortName(originRef);
9599
+ const { stdout: currentRaw } = await git(
9600
+ execFileFn,
9601
+ ["rev-parse", "--abbrev-ref", "HEAD"],
9602
+ repoRoot,
9603
+ timeoutMs
9604
+ );
9605
+ const current = currentRaw.trim();
9606
+ if (current !== defaultBranch) {
9607
+ return {
9608
+ status: "skipped",
9609
+ reason: "wrong-branch",
9610
+ detail: `current branch '${current}' is not the default '${defaultBranch}'`,
9611
+ defaultBranch
9612
+ };
9613
+ }
9614
+ try {
9615
+ await git(execFileFn, ["fetch", "origin", defaultBranch, "--quiet"], repoRoot, timeoutMs);
9616
+ } catch (err) {
9617
+ if (isSpawnEnoent(err)) throw err;
9618
+ return {
9619
+ status: "skipped",
9620
+ reason: "fetch-failed",
9621
+ detail: err instanceof Error ? err.message : String(err),
9622
+ defaultBranch
9623
+ };
9624
+ }
9625
+ const headIsAncestor = await isAncestor(execFileFn, "HEAD", originRef, repoRoot, timeoutMs);
9626
+ const originIsAncestor = await isAncestor(execFileFn, originRef, "HEAD", repoRoot, timeoutMs);
9627
+ if (headIsAncestor && originIsAncestor) {
9628
+ return { status: "no-op", defaultBranch };
9629
+ }
9630
+ if (!headIsAncestor) {
9631
+ return {
9632
+ status: "skipped",
9633
+ reason: "diverged",
9634
+ detail: `local '${defaultBranch}' has commits not on '${originRef}'`,
9635
+ defaultBranch
9636
+ };
9637
+ }
9638
+ const before = (await git(execFileFn, ["rev-parse", "HEAD"], repoRoot, timeoutMs)).stdout.trim();
9639
+ try {
9640
+ await git(execFileFn, ["merge", "--ff-only", originRef], repoRoot, timeoutMs);
9641
+ } catch (err) {
9642
+ const stderr = extractStderr(err);
9643
+ if (isDirtyConflictStderr(stderr)) {
9644
+ return {
9645
+ status: "skipped",
9646
+ reason: "dirty-conflict",
9647
+ detail: stderr.split("\n")[0] ?? "merge --ff-only failed due to working-tree changes",
9648
+ defaultBranch
9649
+ };
9650
+ }
9651
+ throw err;
9652
+ }
9653
+ const after = (await git(execFileFn, ["rev-parse", "HEAD"], repoRoot, timeoutMs)).stdout.trim();
9654
+ return { status: "updated", from: before, to: after, defaultBranch };
9655
+ } catch (err) {
9656
+ return {
9657
+ status: "error",
9658
+ message: err instanceof Error ? err.message : String(err)
9659
+ };
9660
+ }
9661
+ }
9363
9662
  // Annotate the CommonJS export names for ESM import in node:
9364
9663
  0 && (module.exports = {
9365
9664
  AnalysisArchive,
@@ -9404,6 +9703,7 @@ function launchTUI(orchestrator) {
9404
9703
  savePublishedIndex,
9405
9704
  selectCandidates,
9406
9705
  sortCandidates,
9706
+ syncMain,
9407
9707
  triageIssue,
9408
9708
  validateWorkflowConfig
9409
9709
  });