@harness-engineering/orchestrator 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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
2714
  var import_core9 = require("@harness-engineering/core");
2678
- var import_intelligence4 = require("@harness-engineering/intelligence");
2679
- var import_graph = require("@harness-engineering/graph");
2680
2715
 
2681
2716
  // src/intelligence/pipeline-runner.ts
2682
2717
  var path7 = __toESM(require("path"));
@@ -3239,6 +3274,82 @@ var CompletionHandler = class {
3239
3274
  // src/orchestrator.ts
3240
3275
  var import_core10 = require("@harness-engineering/core");
3241
3276
 
3277
+ // src/tracker/adapters/github-issues-issue-tracker.ts
3278
+ var import_types9 = require("@harness-engineering/types");
3279
+ var GitHubIssuesIssueTrackerAdapter = class {
3280
+ client;
3281
+ config;
3282
+ constructor(client, config) {
3283
+ this.client = client;
3284
+ this.config = config;
3285
+ }
3286
+ async fetchCandidateIssues() {
3287
+ return this.fetchIssuesByStates(this.config.activeStates);
3288
+ }
3289
+ async fetchIssuesByStates(stateNames) {
3290
+ const r = await this.client.fetchByStatus(
3291
+ stateNames
3292
+ );
3293
+ if (!r.ok) return (0, import_types9.Err)(r.error);
3294
+ return (0, import_types9.Ok)(r.value.map((f) => this.mapTrackedToIssue(f)));
3295
+ }
3296
+ async fetchIssueStatesByIds(issueIds) {
3297
+ const r = await this.client.fetchAll();
3298
+ if (!r.ok) return (0, import_types9.Err)(r.error);
3299
+ const wanted = new Set(issueIds);
3300
+ const out = /* @__PURE__ */ new Map();
3301
+ for (const f of r.value.features) {
3302
+ if (wanted.has(f.externalId)) out.set(f.externalId, this.mapTrackedToIssue(f));
3303
+ }
3304
+ return (0, import_types9.Ok)(out);
3305
+ }
3306
+ async claimIssue(issueId, orchestratorId) {
3307
+ const r = await this.client.claim(issueId, orchestratorId);
3308
+ if (!r.ok) return (0, import_types9.Err)(r.error);
3309
+ return (0, import_types9.Ok)(void 0);
3310
+ }
3311
+ async releaseIssue(issueId) {
3312
+ const r = await this.client.release(issueId);
3313
+ if (!r.ok) return (0, import_types9.Err)(r.error);
3314
+ return (0, import_types9.Ok)(void 0);
3315
+ }
3316
+ async markIssueComplete(issueId) {
3317
+ const r = await this.client.complete(issueId);
3318
+ if (!r.ok) return (0, import_types9.Err)(r.error);
3319
+ return (0, import_types9.Ok)(void 0);
3320
+ }
3321
+ /**
3322
+ * Project a wide-interface `TrackedFeature` onto the small-interface
3323
+ * `Issue` shape consumed by the orchestrator's tick loop.
3324
+ */
3325
+ mapTrackedToIssue(f) {
3326
+ return {
3327
+ id: f.externalId,
3328
+ identifier: f.externalId,
3329
+ title: f.name,
3330
+ description: f.summary,
3331
+ priority: null,
3332
+ state: f.status,
3333
+ branchName: null,
3334
+ url: null,
3335
+ labels: [],
3336
+ spec: f.spec,
3337
+ plans: f.plans,
3338
+ blockedBy: f.blockedBy.map(
3339
+ (b) => ({
3340
+ id: null,
3341
+ identifier: b,
3342
+ state: null
3343
+ })
3344
+ ),
3345
+ createdAt: f.createdAt,
3346
+ updatedAt: f.updatedAt,
3347
+ externalId: f.externalId,
3348
+ assignee: f.assignee
3349
+ };
3350
+ }
3351
+ };
3352
+
3242
3353
  // src/agent/runner.ts
3243
3354
  var MAX_SLEEP_MS = 12 * 60 * 6e4;
3244
3355
  function buildSleepMessage(resetsAtMs, sleepMs, requestedSleepMs, truncated) {
@@ -3539,9 +3650,17 @@ var LocalModelResolver = class {
3539
3650
 
3540
3651
  // src/agent/config-migration.ts
3541
3652
  var MIGRATION_GUIDE = "docs/guides/multi-backend-routing.md";
3542
- 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
  });
@@ -4753,9 +4815,9 @@ var PiBackend = class {
4753
4815
  piSession,
4754
4816
  unsubscribe: null
4755
4817
  };
4756
- return (0, import_types14.Ok)(session);
4818
+ return (0, import_types15.Ok)(session);
4757
4819
  } catch (err) {
4758
- return (0, import_types14.Err)({
4820
+ return (0, import_types15.Err)({
4759
4821
  category: "response_error",
4760
4822
  message: `Failed to create pi session: ${err instanceof Error ? err.message : String(err)}`
4761
4823
  });
@@ -4885,14 +4947,14 @@ var PiBackend = class {
4885
4947
  await piSession.abort();
4886
4948
  } catch {
4887
4949
  }
4888
- return (0, import_types14.Ok)(void 0);
4950
+ return (0, import_types15.Ok)(void 0);
4889
4951
  }
4890
4952
  async healthCheck() {
4891
4953
  try {
4892
4954
  await import("@mariozechner/pi-coding-agent");
4893
- return (0, import_types14.Ok)(void 0);
4955
+ return (0, import_types15.Ok)(void 0);
4894
4956
  } catch (err) {
4895
- return (0, import_types14.Err)({
4957
+ return (0, import_types15.Err)({
4896
4958
  category: "agent_not_found",
4897
4959
  message: `Pi SDK not available: ${err instanceof Error ? err.message : String(err)}`
4898
4960
  });
@@ -4955,7 +5017,7 @@ function createBackend(def) {
4955
5017
  }
4956
5018
 
4957
5019
  // src/agent/backends/container.ts
4958
- var import_types15 = require("@harness-engineering/types");
5020
+ var import_types16 = require("@harness-engineering/types");
4959
5021
  function toAgentError(message, details) {
4960
5022
  return { category: "response_error", message, details };
4961
5023
  }
@@ -4986,7 +5048,7 @@ var ContainerBackend = class {
4986
5048
  }
4987
5049
  const result = await this.secretBackend.resolveSecrets(this.secretKeys);
4988
5050
  if (!result.ok) {
4989
- return (0, import_types15.Err)(toAgentError(`Secret resolution failed: ${result.error.message}`, result.error));
5051
+ return (0, import_types16.Err)(toAgentError(`Secret resolution failed: ${result.error.message}`, result.error));
4990
5052
  }
4991
5053
  return { ok: true, value: result.value };
4992
5054
  }
@@ -5011,7 +5073,7 @@ var ContainerBackend = class {
5011
5073
  const createOpts = this.buildCreateOpts(params, envResult.value);
5012
5074
  const containerResult = await this.runtime.createContainer(createOpts);
5013
5075
  if (!containerResult.ok) {
5014
- return (0, import_types15.Err)(
5076
+ return (0, import_types16.Err)(
5015
5077
  toAgentError(
5016
5078
  `Container creation failed: ${containerResult.error.message}`,
5017
5079
  containerResult.error
@@ -5036,7 +5098,7 @@ var ContainerBackend = class {
5036
5098
  this.containerHandles.delete(session.sessionId);
5037
5099
  const removeResult = await this.runtime.removeContainer(handle);
5038
5100
  if (!removeResult.ok) {
5039
- return (0, import_types15.Err)(
5101
+ return (0, import_types16.Err)(
5040
5102
  toAgentError(
5041
5103
  `Container removal failed: ${removeResult.error.message}`,
5042
5104
  removeResult.error
@@ -5049,7 +5111,7 @@ var ContainerBackend = class {
5049
5111
  async healthCheck() {
5050
5112
  const runtimeResult = await this.runtime.healthCheck();
5051
5113
  if (!runtimeResult.ok) {
5052
- return (0, import_types15.Err)({
5114
+ return (0, import_types16.Err)({
5053
5115
  category: "agent_not_found",
5054
5116
  message: `Container runtime unhealthy: ${runtimeResult.error.message}`,
5055
5117
  details: runtimeResult.error
@@ -5061,7 +5123,7 @@ var ContainerBackend = class {
5061
5123
 
5062
5124
  // src/agent/runtime/docker.ts
5063
5125
  var import_node_child_process5 = require("child_process");
5064
- var import_types16 = require("@harness-engineering/types");
5126
+ var import_types17 = require("@harness-engineering/types");
5065
5127
  function dockerExec(args) {
5066
5128
  return new Promise((resolve6, reject) => {
5067
5129
  (0, import_node_child_process5.execFile)("docker", args, (error, stdout) => {
@@ -5094,9 +5156,9 @@ var DockerRuntime = class {
5094
5156
  args.push(opts.image);
5095
5157
  args.push("sleep", "infinity");
5096
5158
  const containerId = await dockerExec(args);
5097
- return (0, import_types16.Ok)({ containerId, runtime: this.name });
5159
+ return (0, import_types17.Ok)({ containerId, runtime: this.name });
5098
5160
  } catch (error) {
5099
- return (0, import_types16.Err)({
5161
+ return (0, import_types17.Err)({
5100
5162
  category: "container_create_failed",
5101
5163
  message: `Failed to create container: ${error instanceof Error ? error.message : String(error)}`,
5102
5164
  details: error
@@ -5140,9 +5202,9 @@ var DockerRuntime = class {
5140
5202
  async removeContainer(handle) {
5141
5203
  try {
5142
5204
  await dockerExec(["rm", "-f", handle.containerId]);
5143
- return (0, import_types16.Ok)(void 0);
5205
+ return (0, import_types17.Ok)(void 0);
5144
5206
  } catch (error) {
5145
- return (0, import_types16.Err)({
5207
+ return (0, import_types17.Err)({
5146
5208
  category: "container_remove_failed",
5147
5209
  message: `Failed to remove container: ${error instanceof Error ? error.message : String(error)}`,
5148
5210
  details: error
@@ -5152,9 +5214,9 @@ var DockerRuntime = class {
5152
5214
  async healthCheck() {
5153
5215
  try {
5154
5216
  await dockerExec(["info", "--format", "{{.ServerVersion}}"]);
5155
- return (0, import_types16.Ok)(void 0);
5217
+ return (0, import_types17.Ok)(void 0);
5156
5218
  } catch (error) {
5157
- return (0, import_types16.Err)({
5219
+ return (0, import_types17.Err)({
5158
5220
  category: "runtime_not_found",
5159
5221
  message: `Docker is not available: ${error instanceof Error ? error.message : String(error)}`,
5160
5222
  details: error
@@ -5164,7 +5226,7 @@ var DockerRuntime = class {
5164
5226
  };
5165
5227
 
5166
5228
  // src/agent/secrets/env.ts
5167
- var import_types17 = require("@harness-engineering/types");
5229
+ var import_types18 = require("@harness-engineering/types");
5168
5230
  var EnvSecretBackend = class {
5169
5231
  name = "env";
5170
5232
  async resolveSecrets(keys) {
@@ -5172,7 +5234,7 @@ var EnvSecretBackend = class {
5172
5234
  for (const key of keys) {
5173
5235
  const value = process.env[key];
5174
5236
  if (value === void 0) {
5175
- return (0, import_types17.Err)({
5237
+ return (0, import_types18.Err)({
5176
5238
  category: "secret_not_found",
5177
5239
  message: `Environment variable '${key}' is not set`,
5178
5240
  key
@@ -5180,16 +5242,16 @@ var EnvSecretBackend = class {
5180
5242
  }
5181
5243
  secrets[key] = value;
5182
5244
  }
5183
- return (0, import_types17.Ok)(secrets);
5245
+ return (0, import_types18.Ok)(secrets);
5184
5246
  }
5185
5247
  async healthCheck() {
5186
- return (0, import_types17.Ok)(void 0);
5248
+ return (0, import_types18.Ok)(void 0);
5187
5249
  }
5188
5250
  };
5189
5251
 
5190
5252
  // src/agent/secrets/onepassword.ts
5191
5253
  var import_node_child_process6 = require("child_process");
5192
- var import_types18 = require("@harness-engineering/types");
5254
+ var import_types19 = require("@harness-engineering/types");
5193
5255
  function opExec(args) {
5194
5256
  return new Promise((resolve6, reject) => {
5195
5257
  (0, import_node_child_process6.execFile)("op", args, (error, stdout) => {
@@ -5214,21 +5276,21 @@ var OnePasswordSecretBackend = class {
5214
5276
  const value = await opExec(["read", `op://${this.vault}/${key}/password`]);
5215
5277
  secrets[key] = value;
5216
5278
  } catch (error) {
5217
- return (0, import_types18.Err)({
5279
+ return (0, import_types19.Err)({
5218
5280
  category: "access_denied",
5219
5281
  message: `Failed to read secret '${key}' from 1Password: ${error instanceof Error ? error.message : String(error)}`,
5220
5282
  key
5221
5283
  });
5222
5284
  }
5223
5285
  }
5224
- return (0, import_types18.Ok)(secrets);
5286
+ return (0, import_types19.Ok)(secrets);
5225
5287
  }
5226
5288
  async healthCheck() {
5227
5289
  try {
5228
5290
  await opExec(["--version"]);
5229
- return (0, import_types18.Ok)(void 0);
5291
+ return (0, import_types19.Ok)(void 0);
5230
5292
  } catch (error) {
5231
- return (0, import_types18.Err)({
5293
+ return (0, import_types19.Err)({
5232
5294
  category: "provider_unavailable",
5233
5295
  message: `1Password CLI is not available: ${error instanceof Error ? error.message : String(error)}`
5234
5296
  });
@@ -5238,7 +5300,7 @@ var OnePasswordSecretBackend = class {
5238
5300
 
5239
5301
  // src/agent/secrets/vault.ts
5240
5302
  var import_node_child_process7 = require("child_process");
5241
- var import_types19 = require("@harness-engineering/types");
5303
+ var import_types20 = require("@harness-engineering/types");
5242
5304
  function vaultExec(args, env) {
5243
5305
  return new Promise((resolve6, reject) => {
5244
5306
  (0, import_node_child_process7.execFile)("vault", args, { env: { ...process.env, ...env } }, (error, stdout) => {
@@ -5269,11 +5331,11 @@ var VaultSecretBackend = class {
5269
5331
  } catch (error) {
5270
5332
  const msg = error instanceof Error ? error.message : String(error);
5271
5333
  const category = error instanceof SyntaxError ? "access_denied" : "access_denied";
5272
- return (0, import_types19.Err)({ category, message: `Failed to read from Vault: ${msg}` });
5334
+ return (0, import_types20.Err)({ category, message: `Failed to read from Vault: ${msg}` });
5273
5335
  }
5274
5336
  const missing = keys.find((k) => !(k in data));
5275
5337
  if (missing) {
5276
- return (0, import_types19.Err)({
5338
+ return (0, import_types20.Err)({
5277
5339
  category: "secret_not_found",
5278
5340
  message: `Secret key '${missing}' not found in Vault path '${this.path}'`,
5279
5341
  key: missing
@@ -5281,14 +5343,14 @@ var VaultSecretBackend = class {
5281
5343
  }
5282
5344
  const secrets = {};
5283
5345
  for (const key of keys) secrets[key] = data[key];
5284
- return (0, import_types19.Ok)(secrets);
5346
+ return (0, import_types20.Ok)(secrets);
5285
5347
  }
5286
5348
  async healthCheck() {
5287
5349
  try {
5288
5350
  await vaultExec(["version"]);
5289
- return (0, import_types19.Ok)(void 0);
5351
+ return (0, import_types20.Ok)(void 0);
5290
5352
  } catch (error) {
5291
- return (0, import_types19.Err)({
5353
+ return (0, import_types20.Err)({
5292
5354
  category: "provider_unavailable",
5293
5355
  message: `Vault CLI is not available: ${error instanceof Error ? error.message : String(error)}`
5294
5356
  });
@@ -5403,6 +5465,10 @@ var OrchestratorBackendFactory = class {
5403
5465
  }
5404
5466
  };
5405
5467
 
5468
+ // src/agent/intelligence-factory.ts
5469
+ var import_intelligence3 = require("@harness-engineering/intelligence");
5470
+ var import_graph = require("@harness-engineering/graph");
5471
+
5406
5472
  // src/agent/analysis-provider-factory.ts
5407
5473
  var import_intelligence2 = require("@harness-engineering/intelligence");
5408
5474
  function buildAnalysisProvider(args) {
@@ -5500,9 +5566,109 @@ function buildClaudeCliProvider(def, args, layerModel) {
5500
5566
  });
5501
5567
  }
5502
5568
 
5569
+ // src/agent/intelligence-factory.ts
5570
+ function buildIntelligencePipeline(deps) {
5571
+ const { config } = deps;
5572
+ const intel = config.intelligence;
5573
+ if (!intel?.enabled) return null;
5574
+ const selProvider = buildAnalysisProviderForLayer("sel", deps);
5575
+ if (!selProvider) return null;
5576
+ const routing = config.agent.routing;
5577
+ const peslName = routing?.intelligence?.pesl;
5578
+ const selName = routing?.intelligence?.sel ?? routing?.default;
5579
+ const peslProvider = peslName !== void 0 && peslName !== selName ? buildAnalysisProviderForLayer("pesl", deps) : null;
5580
+ const peslModel = intel.models?.pesl ?? config.agent.model;
5581
+ const graphStore = new import_graph.GraphStore();
5582
+ const pipeline = new import_intelligence3.IntelligencePipeline(selProvider, graphStore, {
5583
+ ...peslModel !== void 0 && { peslModel },
5584
+ ...peslProvider !== null && peslProvider !== void 0 && { peslProvider }
5585
+ });
5586
+ return { pipeline, graphStore };
5587
+ }
5588
+ function buildAnalysisProviderForLayer(layer, deps) {
5589
+ const { config, localResolvers, logger } = deps;
5590
+ const intel = config.intelligence;
5591
+ if (!intel?.enabled) return null;
5592
+ if (intel.provider) {
5593
+ const layerModel = layer === "sel" ? intel.models?.sel : intel.models?.pesl;
5594
+ return buildExplicitProvider(intel.provider, layerModel ?? config.agent.model, config);
5595
+ }
5596
+ const routed = resolveRoutedBackend(layer, config, logger);
5597
+ if (!routed) return null;
5598
+ const { name, def } = routed;
5599
+ const resolver = localResolvers.get(name);
5600
+ return buildAnalysisProvider({
5601
+ def,
5602
+ backendName: name,
5603
+ layer,
5604
+ // Spec 2 P3-IMP-1: a single snapshot read feeds the factory's
5605
+ // unavailable-warn diagnostic (Configured/Detected lists) and
5606
+ // collapses the two `getStatus()` calls flagged by P3-SUG-2.
5607
+ getResolverStatusSnapshot: () => {
5608
+ if (!resolver) return null;
5609
+ const status = resolver.getStatus();
5610
+ return {
5611
+ available: status.available,
5612
+ resolved: status.resolved,
5613
+ configured: status.configured,
5614
+ detected: status.detected
5615
+ };
5616
+ },
5617
+ intelligence: intel,
5618
+ logger
5619
+ });
5620
+ }
5621
+ function resolveRoutedBackend(layer, config, logger) {
5622
+ const routing = config.agent.routing;
5623
+ const backends = config.agent.backends;
5624
+ if (!routing || !backends) return null;
5625
+ const layerName = routing.intelligence?.[layer];
5626
+ const name = layerName ?? routing.default;
5627
+ const def = backends[name];
5628
+ if (!def) {
5629
+ logger.warn(
5630
+ `Intelligence pipeline: routed backend '${name}' for layer '${layer}' is not in agent.backends.`
5631
+ );
5632
+ return null;
5633
+ }
5634
+ return { name, def };
5635
+ }
5636
+ function buildExplicitProvider(provider, selModel, config) {
5637
+ if (provider.kind === "anthropic") {
5638
+ const apiKey2 = provider.apiKey ?? config.agent.apiKey ?? process.env.ANTHROPIC_API_KEY;
5639
+ if (!apiKey2) {
5640
+ throw new Error("Intelligence pipeline: no Anthropic API key found.");
5641
+ }
5642
+ return new import_intelligence3.AnthropicAnalysisProvider({
5643
+ apiKey: apiKey2,
5644
+ ...selModel !== void 0 && { defaultModel: selModel }
5645
+ });
5646
+ }
5647
+ if (provider.kind === "claude-cli") {
5648
+ return new import_intelligence3.ClaudeCliAnalysisProvider({
5649
+ command: config.agent.command,
5650
+ ...selModel !== void 0 && { defaultModel: selModel },
5651
+ ...config.intelligence?.requestTimeoutMs !== void 0 && {
5652
+ timeoutMs: config.intelligence.requestTimeoutMs
5653
+ }
5654
+ });
5655
+ }
5656
+ const apiKey = provider.apiKey ?? config.agent.apiKey ?? "ollama";
5657
+ const baseUrl = provider.baseUrl ?? "http://localhost:11434/v1";
5658
+ const intel = config.intelligence;
5659
+ return new import_intelligence3.OpenAICompatibleAnalysisProvider({
5660
+ apiKey,
5661
+ baseUrl,
5662
+ ...selModel !== void 0 && { defaultModel: selModel },
5663
+ ...intel?.requestTimeoutMs !== void 0 && { timeoutMs: intel.requestTimeoutMs },
5664
+ ...intel?.promptSuffix !== void 0 && { promptSuffix: intel.promptSuffix },
5665
+ ...intel?.jsonMode !== void 0 && { jsonMode: intel.jsonMode }
5666
+ });
5667
+ }
5668
+
5503
5669
  // src/server/http.ts
5504
5670
  var http = __toESM(require("http"));
5505
- var path13 = __toESM(require("path"));
5671
+ var path14 = __toESM(require("path"));
5506
5672
 
5507
5673
  // src/server/websocket.ts
5508
5674
  var import_ws = require("ws");
@@ -5930,7 +6096,7 @@ function extractChunks(event) {
5930
6096
  }
5931
6097
 
5932
6098
  // src/server/routes/analyze.ts
5933
- var 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
  }
@@ -7179,6 +7383,14 @@ var BUILT_IN_TASKS = [
7179
7383
  schedule: "0 7 * * *",
7180
7384
  branch: null,
7181
7385
  checkCommand: ["perf", "baselines", "update"]
7386
+ },
7387
+ {
7388
+ id: "main-sync",
7389
+ type: "housekeeping",
7390
+ description: "Fast-forward local default branch from origin",
7391
+ schedule: "*/15 * * * *",
7392
+ branch: null,
7393
+ checkCommand: ["harness", "sync-main", "--json"]
7182
7394
  }
7183
7395
  ];
7184
7396
 
@@ -7249,7 +7461,7 @@ function cronMatchesNow(expression, now) {
7249
7461
  // src/maintenance/scheduler.ts
7250
7462
  var MaintenanceScheduler = class {
7251
7463
  config;
7252
- claimManager;
7464
+ leaderElector;
7253
7465
  logger;
7254
7466
  onTaskDue;
7255
7467
  historyProvider;
@@ -7264,7 +7476,7 @@ var MaintenanceScheduler = class {
7264
7476
  activeRun = null;
7265
7477
  constructor(options) {
7266
7478
  this.config = options.config;
7267
- this.claimManager = options.claimManager;
7479
+ this.leaderElector = options.leaderElector;
7268
7480
  this.logger = options.logger;
7269
7481
  this.historyProvider = options.historyProvider ?? null;
7270
7482
  this.onTaskDue = options.onTaskDue;
@@ -7339,12 +7551,12 @@ var MaintenanceScheduler = class {
7339
7551
  await this.processQueue(queue, epochMinute);
7340
7552
  }
7341
7553
  /**
7342
- * Attempt to claim leadership via ClaimManager.
7554
+ * Attempt to claim leadership via the configured LeaderElector.
7343
7555
  * Returns true if this instance is the leader.
7344
7556
  */
7345
7557
  async attemptLeaderClaim(evalTime) {
7346
7558
  try {
7347
- const result = await this.claimManager.claimAndVerify("maintenance-leader");
7559
+ const result = await this.leaderElector.electLeader();
7348
7560
  if (!result.ok) {
7349
7561
  this.isLeader = false;
7350
7562
  this.logger.warn("Maintenance leader claim failed", { error: result.error?.message });
@@ -7423,6 +7635,7 @@ var MaintenanceScheduler = class {
7423
7635
  const history = this.historyProvider ? this.historyProvider.getHistory(200, 0) : this.internalHistory;
7424
7636
  const schedule = this.resolvedTasks.map((task) => ({
7425
7637
  taskId: task.id,
7638
+ type: task.type,
7426
7639
  nextRun: this.computeNextRun(task.schedule),
7427
7640
  lastRun: history.find((r) => r.taskId === task.id) ?? null
7428
7641
  }));
@@ -7459,9 +7672,17 @@ var MaintenanceScheduler = class {
7459
7672
  }
7460
7673
  };
7461
7674
 
7675
+ // src/maintenance/leader-elector.ts
7676
+ var import_types21 = require("@harness-engineering/types");
7677
+ var SingleProcessLeaderElector = class {
7678
+ async electLeader() {
7679
+ return (0, import_types21.Ok)("claimed");
7680
+ }
7681
+ };
7682
+
7462
7683
  // src/maintenance/reporter.ts
7463
7684
  var fs14 = __toESM(require("fs"));
7464
- var path14 = __toESM(require("path"));
7685
+ var path15 = __toESM(require("path"));
7465
7686
  var import_zod11 = require("zod");
7466
7687
  var RunResultSchema = import_zod11.z.object({
7467
7688
  taskId: import_zod11.z.string(),
@@ -7497,7 +7718,7 @@ var MaintenanceReporter = class {
7497
7718
  async load() {
7498
7719
  try {
7499
7720
  await fs14.promises.mkdir(this.persistDir, { recursive: true });
7500
- const filePath = path14.join(this.persistDir, "history.json");
7721
+ const filePath = path15.join(this.persistDir, "history.json");
7501
7722
  const data = await fs14.promises.readFile(filePath, "utf-8");
7502
7723
  const parsed = import_zod11.z.array(RunResultSchema).safeParse(JSON.parse(data));
7503
7724
  if (parsed.success) {
@@ -7533,7 +7754,7 @@ var MaintenanceReporter = class {
7533
7754
  async persist() {
7534
7755
  try {
7535
7756
  await fs14.promises.mkdir(this.persistDir, { recursive: true });
7536
- const filePath = path14.join(this.persistDir, "history.json");
7757
+ const filePath = path15.join(this.persistDir, "history.json");
7537
7758
  await fs14.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
7538
7759
  } catch (err) {
7539
7760
  this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
@@ -7754,22 +7975,39 @@ var TaskRunner = class {
7754
7975
  }
7755
7976
  /**
7756
7977
  * Housekeeping: run command directly, no AI, no PR.
7978
+ *
7979
+ * Captures stdout and parses a trailing JSON status line if present.
7980
+ * Recognized contracts:
7981
+ * - Phase 4/5 status contract (e.g., harness pulse run): success/skipped/failure/no-issues
7982
+ * - sync-main contract: updated/no-op/skipped/error → mapped onto the run-result status
7983
+ * Legacy housekeeping commands that emit no JSON keep the prior behavior:
7984
+ * status: 'success', findings: 0.
7757
7985
  */
7758
7986
  async runHousekeeping(task, startedAt) {
7759
7987
  if (!task.checkCommand || task.checkCommand.length === 0) {
7760
7988
  return this.failureResult(task.id, startedAt, "housekeeping task missing checkCommand");
7761
7989
  }
7762
- await this.commandExecutor.exec(task.checkCommand, this.cwd);
7763
- return {
7990
+ let stdout;
7991
+ try {
7992
+ const out = await this.commandExecutor.exec(task.checkCommand, this.cwd);
7993
+ stdout = out.stdout ?? "";
7994
+ } catch (err) {
7995
+ return this.failureResult(task.id, startedAt, String(err));
7996
+ }
7997
+ const parsed = parseStatusLine(stdout);
7998
+ const status = parsed?.status ?? "success";
7999
+ const result = {
7764
8000
  taskId: task.id,
7765
8001
  startedAt,
7766
8002
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
7767
- status: "success",
8003
+ status,
7768
8004
  findings: 0,
7769
8005
  fixed: 0,
7770
8006
  prUrl: null,
7771
8007
  prUpdated: false
7772
8008
  };
8009
+ if (parsed?.error) result.error = parsed.error;
8010
+ return result;
7773
8011
  }
7774
8012
  /**
7775
8013
  * Resolve which AI backend name to use for a given task.
@@ -7803,7 +8041,7 @@ function parseStatusLine(output) {
7803
8041
  const obj = JSON.parse(line);
7804
8042
  const s = obj.status;
7805
8043
  if (s === "success" || s === "skipped" || s === "failure" || s === "no-issues") {
7806
- const parsed = { status: s };
8044
+ const parsed = { status: s, rawStatus: s };
7807
8045
  if (typeof obj.candidatesFound === "number") {
7808
8046
  parsed.candidatesFound = obj.candidatesFound;
7809
8047
  }
@@ -7813,8 +8051,18 @@ function parseStatusLine(output) {
7813
8051
  if (typeof obj.reason === "string") {
7814
8052
  parsed.reason = obj.reason;
7815
8053
  }
8054
+ if (typeof obj.detail === "string" && !parsed.error) {
8055
+ parsed.error = `${parsed.reason ?? "skipped"}: ${obj.detail}`;
8056
+ }
7816
8057
  return parsed;
7817
8058
  }
8059
+ if (s === "updated" || s === "no-op") {
8060
+ return { status: "success", rawStatus: s };
8061
+ }
8062
+ if (s === "error") {
8063
+ const message = typeof obj.message === "string" ? obj.message : "unknown error";
8064
+ return { status: "failure", error: message, rawStatus: "error" };
8065
+ }
7818
8066
  } catch {
7819
8067
  }
7820
8068
  }
@@ -7895,11 +8143,15 @@ var Orchestrator = class extends import_node_events.EventEmitter {
7895
8143
  completionHandler;
7896
8144
  /** Project root directory, derived from workspace root. */
7897
8145
  get projectRoot() {
7898
- return path15.resolve(this.config.workspace.root, "..", "..");
8146
+ return path16.resolve(this.config.workspace.root, "..", "..");
7899
8147
  }
7900
8148
  enrichedSpecsByIssue = /* @__PURE__ */ new Map();
7901
8149
  /** Tracks recently-failed intelligence analysis to avoid re-requesting every tick */
7902
8150
  analysisFailureCache = /* @__PURE__ */ new Map();
8151
+ // Phase 3 added a private `roadmapMode` field used by `createTracker` to
8152
+ // guard the file-less stub. Phase 4 / S2 / D-P4-E shifted dispatch onto
8153
+ // `tracker.kind`, removing the need for the field — it is now dropped to
8154
+ // satisfy `noUnusedLocals`. See decision D-P3-orchestrator-mode-via-fs-read.
7903
8155
  /** Abort controllers and PIDs for running agent tasks — used by stopIssue to cancel in-flight work.
7904
8156
  * The PID is stored here because the running entry may be deleted by the state machine
7905
8157
  * before the stop effect executes (e.g., stall_detected removes the entry first). */
@@ -7935,14 +8187,19 @@ var Orchestrator = class extends import_node_events.EventEmitter {
7935
8187
  );
7936
8188
  }
7937
8189
  this.tracker = overrides?.tracker || this.createTracker();
7938
- this.workspace = new WorkspaceManager(config.workspace);
8190
+ this.workspace = new WorkspaceManager(config.workspace, {
8191
+ emitEvent: (event) => {
8192
+ this.server?.broadcastMaintenance("maintenance:baseref_fallback", event);
8193
+ this.emit("maintenance:baseref_fallback", event);
8194
+ }
8195
+ });
7939
8196
  this.hooks = new WorkspaceHooks(config.hooks);
7940
8197
  this.renderer = new PromptRenderer();
7941
8198
  this.overrideBackend = overrides?.backend ?? null;
7942
8199
  this.interactionQueue = new InteractionQueue(
7943
- path15.join(config.workspace.root, "..", "interactions")
8200
+ path16.join(config.workspace.root, "..", "interactions")
7944
8201
  );
7945
- this.analysisArchive = new AnalysisArchive(path15.join(config.workspace.root, "..", "analyses"));
8202
+ this.analysisArchive = new AnalysisArchive(path16.join(config.workspace.root, "..", "analyses"));
7946
8203
  const backendsMap = this.config.agent.backends ?? {};
7947
8204
  for (const [name, def] of Object.entries(backendsMap)) {
7948
8205
  if (def.type === "local" || def.type === "pi") {
@@ -7984,7 +8241,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
7984
8241
  ...overrides?.execFileFn ? { execFileFn: overrides.execFileFn } : {}
7985
8242
  });
7986
8243
  this.recorder = new StreamRecorder(
7987
- path15.resolve(config.workspace.root, "..", "streams"),
8244
+ path16.resolve(config.workspace.root, "..", "streams"),
7988
8245
  this.logger
7989
8246
  );
7990
8247
  const self = this;
@@ -8016,7 +8273,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
8016
8273
  if (config.server?.port) {
8017
8274
  this.server = new OrchestratorServer(this, config.server.port, {
8018
8275
  interactionQueue: this.interactionQueue,
8019
- plansDir: path15.resolve(config.workspace.root, "..", "docs", "plans"),
8276
+ plansDir: path16.resolve(config.workspace.root, "..", "docs", "plans"),
8020
8277
  pipeline: this.pipeline,
8021
8278
  analysisArchive: this.analysisArchive,
8022
8279
  roadmapPath: config.tracker.filePath ?? null,
@@ -8047,6 +8304,17 @@ var Orchestrator = class extends import_node_events.EventEmitter {
8047
8304
  }
8048
8305
  }
8049
8306
  createTracker() {
8307
+ if (this.config.tracker.kind === "github-issues") {
8308
+ const trackerCfg = {
8309
+ kind: "github-issues",
8310
+ repo: this.config.tracker.projectSlug ?? "",
8311
+ ...this.config.tracker.apiKey ? { token: this.config.tracker.apiKey } : {},
8312
+ ...this.config.tracker.endpoint ? { apiBase: this.config.tracker.endpoint } : {}
8313
+ };
8314
+ const clientResult = (0, import_core10.createTrackerClient)(trackerCfg);
8315
+ if (!clientResult.ok) throw clientResult.error;
8316
+ return new GitHubIssuesIssueTrackerAdapter(clientResult.value, this.config.tracker);
8317
+ }
8050
8318
  if (this.config.tracker.kind === "roadmap") {
8051
8319
  return new RoadmapTrackerAdapter(this.config.tracker);
8052
8320
  }
@@ -8062,8 +8330,8 @@ var Orchestrator = class extends import_node_events.EventEmitter {
8062
8330
  const checkRunner = {
8063
8331
  run: async (command, cwd) => {
8064
8332
  const { execFile: execFile6 } = await import("child_process");
8065
- const { promisify: promisify3 } = await import("util");
8066
- const execFileAsync = promisify3(execFile6);
8333
+ const { promisify: promisify4 } = await import("util");
8334
+ const execFileAsync = promisify4(execFile6);
8067
8335
  const [cmd, ...args] = command;
8068
8336
  if (!cmd) return { passed: true, findings: 0, output: "" };
8069
8337
  try {
@@ -8097,12 +8365,13 @@ var Orchestrator = class extends import_node_events.EventEmitter {
8097
8365
  const commandExecutor = {
8098
8366
  exec: async (command, cwd) => {
8099
8367
  const { execFile: execFile6 } = await import("child_process");
8100
- const { promisify: promisify3 } = await import("util");
8101
- const execFileAsync = promisify3(execFile6);
8368
+ const { promisify: promisify4 } = await import("util");
8369
+ const execFileAsync = promisify4(execFile6);
8102
8370
  const [cmd, ...args] = command;
8103
- if (!cmd) return;
8371
+ if (!cmd) return { stdout: "" };
8104
8372
  try {
8105
- await execFileAsync(cmd, args, { cwd, timeout: 12e4 });
8373
+ const { stdout } = await execFileAsync(cmd, args, { cwd, timeout: 12e4 });
8374
+ return { stdout: String(stdout) };
8106
8375
  } catch (err) {
8107
8376
  logger.warn("Maintenance command execution failed", {
8108
8377
  command,
@@ -8127,7 +8396,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
8127
8396
  */
8128
8397
  async initMaintenance(maintenanceConfig) {
8129
8398
  this.maintenanceReporter = new MaintenanceReporter({
8130
- persistDir: path15.join(this.projectRoot, ".harness", "maintenance"),
8399
+ persistDir: path16.join(this.projectRoot, ".harness", "maintenance"),
8131
8400
  logger: this.logger
8132
8401
  });
8133
8402
  await this.maintenanceReporter.load();
@@ -8135,7 +8404,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
8135
8404
  const reporter = this.maintenanceReporter;
8136
8405
  this.maintenanceScheduler = new MaintenanceScheduler({
8137
8406
  config: maintenanceConfig,
8138
- claimManager: this.claimManager,
8407
+ leaderElector: new SingleProcessLeaderElector(),
8139
8408
  logger: this.logger,
8140
8409
  historyProvider: reporter,
8141
8410
  onTaskDue: async (task) => {
@@ -8178,129 +8447,14 @@ var Orchestrator = class extends import_node_events.EventEmitter {
8178
8447
  }
8179
8448
  }
8180
8449
  createIntelligencePipeline() {
8181
- const 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,
8450
+ const bundle = buildIntelligencePipeline({
8451
+ config: this.config,
8452
+ localResolvers: this.localResolvers,
8249
8453
  logger: this.logger
8250
8454
  });
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
- });
8455
+ if (!bundle) return null;
8456
+ this.graphStore = bundle.graphStore;
8457
+ return bundle.pipeline;
8304
8458
  }
8305
8459
  /**
8306
8460
  * Lazily initializes the ClaimManager if it hasn't been created yet.
@@ -9360,6 +9514,150 @@ function launchTUI(orchestrator) {
9360
9514
  const { waitUntilExit } = (0, import_ink5.render)(/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Dashboard, { orchestrator }));
9361
9515
  return { waitUntilExit };
9362
9516
  }
9517
+
9518
+ // src/maintenance/sync-main.ts
9519
+ var import_node_child_process9 = require("child_process");
9520
+ var import_node_util3 = require("util");
9521
+ var DEFAULT_TIMEOUT_MS2 = 6e4;
9522
+ async function git(execFileFn, args, cwd, timeoutMs) {
9523
+ const exec = (0, import_node_util3.promisify)(execFileFn);
9524
+ const { stdout, stderr } = await exec("git", args, { cwd, timeout: timeoutMs });
9525
+ return { stdout: String(stdout), stderr: String(stderr) };
9526
+ }
9527
+ function isSpawnEnoent(err) {
9528
+ if (!err || typeof err !== "object") return false;
9529
+ const code = err.code;
9530
+ return code === "ENOENT";
9531
+ }
9532
+ async function refExists(execFileFn, ref, cwd, timeoutMs) {
9533
+ try {
9534
+ await git(execFileFn, ["rev-parse", "--verify", "--quiet", ref], cwd, timeoutMs);
9535
+ return true;
9536
+ } catch (err) {
9537
+ if (isSpawnEnoent(err)) throw err;
9538
+ return false;
9539
+ }
9540
+ }
9541
+ async function resolveOriginDefault(execFileFn, cwd, timeoutMs) {
9542
+ try {
9543
+ const { stdout } = await git(
9544
+ execFileFn,
9545
+ ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
9546
+ cwd,
9547
+ timeoutMs
9548
+ );
9549
+ const v = stdout.trim();
9550
+ if (v) return v;
9551
+ } catch (err) {
9552
+ if (isSpawnEnoent(err)) throw err;
9553
+ }
9554
+ for (const candidate of ["origin/main", "origin/master"]) {
9555
+ if (await refExists(execFileFn, candidate, cwd, timeoutMs)) return candidate;
9556
+ }
9557
+ return null;
9558
+ }
9559
+ function shortName(originRef) {
9560
+ return originRef.startsWith("origin/") ? originRef.slice("origin/".length) : originRef;
9561
+ }
9562
+ function isDirtyConflictStderr(s) {
9563
+ return /would be overwritten|local changes|Aborting/i.test(s);
9564
+ }
9565
+ function extractStderr(err) {
9566
+ if (err && typeof err === "object" && "stderr" in err) {
9567
+ const raw = err.stderr;
9568
+ if (typeof raw === "string") return raw;
9569
+ if (raw instanceof Buffer) return raw.toString("utf8");
9570
+ }
9571
+ if (err instanceof Error) return err.message;
9572
+ if (typeof err === "string") return err;
9573
+ return "";
9574
+ }
9575
+ async function isAncestor(execFileFn, a, b, cwd, timeoutMs) {
9576
+ try {
9577
+ await git(execFileFn, ["merge-base", "--is-ancestor", a, b], cwd, timeoutMs);
9578
+ return true;
9579
+ } catch (err) {
9580
+ if (isSpawnEnoent(err)) throw err;
9581
+ return false;
9582
+ }
9583
+ }
9584
+ async function syncMain(repoRoot, opts = {}) {
9585
+ const execFileFn = opts.execFileFn ?? import_node_child_process9.execFile;
9586
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
9587
+ try {
9588
+ const originRef = await resolveOriginDefault(execFileFn, repoRoot, timeoutMs);
9589
+ if (!originRef) {
9590
+ return {
9591
+ status: "skipped",
9592
+ reason: "no-remote",
9593
+ detail: "origin/HEAD unset and neither origin/main nor origin/master resolves",
9594
+ defaultBranch: ""
9595
+ };
9596
+ }
9597
+ const defaultBranch = shortName(originRef);
9598
+ const { stdout: currentRaw } = await git(
9599
+ execFileFn,
9600
+ ["rev-parse", "--abbrev-ref", "HEAD"],
9601
+ repoRoot,
9602
+ timeoutMs
9603
+ );
9604
+ const current = currentRaw.trim();
9605
+ if (current !== defaultBranch) {
9606
+ return {
9607
+ status: "skipped",
9608
+ reason: "wrong-branch",
9609
+ detail: `current branch '${current}' is not the default '${defaultBranch}'`,
9610
+ defaultBranch
9611
+ };
9612
+ }
9613
+ try {
9614
+ await git(execFileFn, ["fetch", "origin", defaultBranch, "--quiet"], repoRoot, timeoutMs);
9615
+ } catch (err) {
9616
+ if (isSpawnEnoent(err)) throw err;
9617
+ return {
9618
+ status: "skipped",
9619
+ reason: "fetch-failed",
9620
+ detail: err instanceof Error ? err.message : String(err),
9621
+ defaultBranch
9622
+ };
9623
+ }
9624
+ const headIsAncestor = await isAncestor(execFileFn, "HEAD", originRef, repoRoot, timeoutMs);
9625
+ const originIsAncestor = await isAncestor(execFileFn, originRef, "HEAD", repoRoot, timeoutMs);
9626
+ if (headIsAncestor && originIsAncestor) {
9627
+ return { status: "no-op", defaultBranch };
9628
+ }
9629
+ if (!headIsAncestor) {
9630
+ return {
9631
+ status: "skipped",
9632
+ reason: "diverged",
9633
+ detail: `local '${defaultBranch}' has commits not on '${originRef}'`,
9634
+ defaultBranch
9635
+ };
9636
+ }
9637
+ const before = (await git(execFileFn, ["rev-parse", "HEAD"], repoRoot, timeoutMs)).stdout.trim();
9638
+ try {
9639
+ await git(execFileFn, ["merge", "--ff-only", originRef], repoRoot, timeoutMs);
9640
+ } catch (err) {
9641
+ const stderr = extractStderr(err);
9642
+ if (isDirtyConflictStderr(stderr)) {
9643
+ return {
9644
+ status: "skipped",
9645
+ reason: "dirty-conflict",
9646
+ detail: stderr.split("\n")[0] ?? "merge --ff-only failed due to working-tree changes",
9647
+ defaultBranch
9648
+ };
9649
+ }
9650
+ throw err;
9651
+ }
9652
+ const after = (await git(execFileFn, ["rev-parse", "HEAD"], repoRoot, timeoutMs)).stdout.trim();
9653
+ return { status: "updated", from: before, to: after, defaultBranch };
9654
+ } catch (err) {
9655
+ return {
9656
+ status: "error",
9657
+ message: err instanceof Error ? err.message : String(err)
9658
+ };
9659
+ }
9660
+ }
9363
9661
  // Annotate the CommonJS export names for ESM import in node:
9364
9662
  0 && (module.exports = {
9365
9663
  AnalysisArchive,
@@ -9404,6 +9702,7 @@ function launchTUI(orchestrator) {
9404
9702
  savePublishedIndex,
9405
9703
  selectCandidates,
9406
9704
  sortCandidates,
9705
+ syncMain,
9407
9706
  triageIssue,
9408
9707
  validateWorkflowConfig
9409
9708
  });