@harness-engineering/orchestrator 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -2011,7 +2011,11 @@ var RoutingConfigSchema = z.object({
2011
2011
  // --- AMR: opt-in adaptive-routing policy. Its PRESENCE flips the orchestrator
2012
2012
  // from identity/default dispatch to complexity-aware tier routing (default-off
2013
2013
  // when absent). Previously accepted by the runtime PUT endpoint only. ---
2014
- policy: RoutingPolicySchema.optional()
2014
+ policy: RoutingPolicySchema.optional(),
2015
+ // local-backend-full-workflow Phase 3 (D5/SC6): routes the LOCAL outcome-eval
2016
+ // gate to the primary backend when 'primary'. .strict() below means this MUST
2017
+ // be declared here or a config with workflowGates is silently rejected.
2018
+ workflowGates: z.enum(["local", "primary"]).optional()
2015
2019
  }).strict();
2016
2020
  var CONFIDENCE = z.enum(["low", "medium", "high"]);
2017
2021
  var V1_MAX_RATCHET_STAGE = 2;
@@ -2300,9 +2304,19 @@ var WorkflowLoader = class {
2300
2304
  if (!configResult.ok) {
2301
2305
  return Err2(configResult.error);
2302
2306
  }
2307
+ const localTemplatePath = path7.join(path7.dirname(filePath), "harness.orchestrator.local.md");
2308
+ let localPromptTemplate;
2309
+ try {
2310
+ const localContent = await fs7.readFile(localTemplatePath, "utf-8");
2311
+ const localParts = localContent.split("---");
2312
+ localPromptTemplate = localParts.length >= 3 ? localParts.slice(2).join("---").trim() : localContent.trim();
2313
+ } catch {
2314
+ localPromptTemplate = void 0;
2315
+ }
2303
2316
  return Ok3({
2304
2317
  config: configResult.value.config,
2305
2318
  promptTemplate,
2319
+ ...localPromptTemplate !== void 0 ? { localPromptTemplate } : {},
2306
2320
  warnings: configResult.value.warnings
2307
2321
  });
2308
2322
  } catch (error) {
@@ -4346,7 +4360,7 @@ var LocalModelResolver = class {
4346
4360
  if (useCase === void 0) return this.resolved;
4347
4361
  const profile = useCaseToProfile(useCase);
4348
4362
  if (profile === "general") return this.resolved;
4349
- return this.selectMatch(this.candidates(profile), this.detected);
4363
+ return this.selectMatch(this.candidates(profile, { requireToolCalling: true }), this.detected);
4350
4364
  }
4351
4365
  getStatus() {
4352
4366
  return {
@@ -4364,8 +4378,8 @@ var LocalModelResolver = class {
4364
4378
  * from pool entries (currentScore desc → ollamaName); otherwise the static
4365
4379
  * `configured` list is returned unchanged (byte-identical to pre-Phase-4).
4366
4380
  */
4367
- candidates(profile) {
4368
- return this.poolState ? poolStateToCandidates(this.poolState.snapshot(), profile) : [...this.configured];
4381
+ candidates(profile, opts) {
4382
+ return this.poolState ? poolStateToCandidates(this.poolState.snapshot(), profile, opts) : [...this.configured];
4369
4383
  }
4370
4384
  onStatusChange(handler) {
4371
4385
  this.listeners.add(handler);
@@ -4552,7 +4566,8 @@ import {
4552
4566
  createNativeRecommender,
4553
4567
  loadFrozenCandidates,
4554
4568
  selectCandidates as selectCandidates2,
4555
- curationFromCandidates
4569
+ curationFromCandidates,
4570
+ probeToolCalling
4556
4571
  } from "@harness-engineering/local-models";
4557
4572
  import { createModelProposal as createModelProposal2, listProposals as listProposals2, updateProposal as updateProposal5 } from "@harness-engineering/core";
4558
4573
 
@@ -6036,13 +6051,14 @@ function mapPiEvent(rawEvent, sessionId) {
6036
6051
  }
6037
6052
  return null;
6038
6053
  }
6054
+ var LOCAL_PROVIDER = "harness-local";
6039
6055
  function buildLocalModel(config) {
6040
6056
  if (!config.model) return void 0;
6041
6057
  return {
6042
6058
  id: config.model,
6043
6059
  name: config.model,
6044
6060
  api: "openai-completions",
6045
- provider: "harness-local",
6061
+ provider: LOCAL_PROVIDER,
6046
6062
  baseUrl: config.endpoint ?? "http://localhost:1234/v1",
6047
6063
  reasoning: false,
6048
6064
  input: ["text"],
@@ -6090,9 +6106,12 @@ var PiBackend = class {
6090
6106
  endpoint: this.config.endpoint,
6091
6107
  apiKey: this.config.apiKey
6092
6108
  });
6109
+ const authStorage = piSdk.AuthStorage.inMemory();
6110
+ authStorage.setRuntimeApiKey(LOCAL_PROVIDER, this.config.apiKey ?? "ollama");
6093
6111
  const { session: piSession } = await piSdk.createAgentSession({
6094
6112
  cwd: params.workspacePath,
6095
6113
  ...model !== void 0 && { model },
6114
+ authStorage,
6096
6115
  sessionManager: piSdk.SessionManager.inMemory()
6097
6116
  });
6098
6117
  const session = {
@@ -13468,7 +13487,7 @@ var StructuredLogger = class {
13468
13487
 
13469
13488
  // src/workspace/config-scanner.ts
13470
13489
  import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
13471
- import { join as join15, relative as relative2 } from "path";
13490
+ import { join as join16, relative as relative2 } from "path";
13472
13491
  import {
13473
13492
  scanForInjection,
13474
13493
  SecurityScanner,
@@ -13514,7 +13533,7 @@ async function scanWorkspaceConfig(workspacePath) {
13514
13533
  const scanner = new SecurityScanner(parseSecurityConfig({}));
13515
13534
  const results = [];
13516
13535
  for (const configFile of CONFIG_FILES) {
13517
- const result = await scanSingleFile(join15(workspacePath, configFile), workspacePath, scanner);
13536
+ const result = await scanSingleFile(join16(workspacePath, configFile), workspacePath, scanner);
13518
13537
  if (result) results.push(result);
13519
13538
  }
13520
13539
  return { exitCode: computeScanExitCode(results), results };
@@ -15410,6 +15429,55 @@ function normalizeHarnessCommand(command) {
15410
15429
  if (command[0] === "harness") return command;
15411
15430
  return ["harness", ...command];
15412
15431
  }
15432
+ function truncateGateOutput(output, max = 4e3) {
15433
+ const trimmed = output.trim();
15434
+ if (trimmed.length <= max) return trimmed;
15435
+ const head = trimmed.slice(0, Math.floor(max * 0.7));
15436
+ const tail = trimmed.slice(-Math.floor(max * 0.3));
15437
+ return `${head}
15438
+ \u2026 [truncated ${trimmed.length - max} chars] \u2026
15439
+ ${tail}`;
15440
+ }
15441
+ async function defaultLocalVerifyRunner(workspacePath) {
15442
+ const cp2 = await import("child_process");
15443
+ const fsMod = await import("fs");
15444
+ const pathMod = await import("path");
15445
+ const run = (script) => new Promise((resolve8) => {
15446
+ cp2.execFile(
15447
+ "pnpm",
15448
+ ["-w", "run", script],
15449
+ { cwd: workspacePath, maxBuffer: 32 * 1024 * 1024 },
15450
+ (error, stdout, stderr) => {
15451
+ if (error) {
15452
+ resolve8({
15453
+ ok: false,
15454
+ output: `${script} failed:
15455
+ ${stdout ?? ""}
15456
+ ${stderr ?? error.message}`
15457
+ });
15458
+ } else {
15459
+ resolve8({ ok: true, output: "" });
15460
+ }
15461
+ }
15462
+ );
15463
+ });
15464
+ const pkgPath = pathMod.join(workspacePath, "package.json");
15465
+ if (!fsMod.existsSync(pkgPath)) return { ok: true, output: "" };
15466
+ const scripts = (() => {
15467
+ try {
15468
+ const pkg = JSON.parse(fsMod.readFileSync(pkgPath, "utf8"));
15469
+ return pkg.scripts ?? {};
15470
+ } catch {
15471
+ return {};
15472
+ }
15473
+ })();
15474
+ for (const script of ["typecheck", "lint", "test"]) {
15475
+ if (scripts[script] === void 0) continue;
15476
+ const result = await run(script);
15477
+ if (!result.ok) return result;
15478
+ }
15479
+ return { ok: true, output: "" };
15480
+ }
15413
15481
  var Orchestrator = class extends EventEmitter {
15414
15482
  state;
15415
15483
  config;
@@ -15459,6 +15527,30 @@ var Orchestrator = class extends EventEmitter {
15459
15527
  overrideBackend;
15460
15528
  renderer;
15461
15529
  promptTemplate;
15530
+ /**
15531
+ * Backend-aware local dispatch template (Phase 1). Set from
15532
+ * `overrides.localPromptTemplate` (production: threaded by the CLI from
15533
+ * WorkflowLoader). Undefined -> resolvePromptTemplate falls back to the
15534
+ * default template (SC5).
15535
+ */
15536
+ localPromptTemplate;
15537
+ /**
15538
+ * local-backend-full-workflow Phase 2 (Option C): the verify runner the
15539
+ * local-only enforced gate (`runLocalWorkflowGate`) invokes to run the
15540
+ * project's mechanical gate (typecheck + lint + test) over the workspace.
15541
+ * Injected in tests to force fail→pass sequences; in production it defaults
15542
+ * to `defaultLocalVerifyRunner` (a thin project-script probe). Kept as a
15543
+ * field seam — mirrors how `execFileFn` is injected — so the completion path
15544
+ * is decoupled from the concrete detector.
15545
+ */
15546
+ verifyRunner;
15547
+ /**
15548
+ * Phase 2: the most recent gate-failure reason per issue, threaded into the
15549
+ * next dispatch's rendered prompt as a failure preamble (the re-prompt). Set
15550
+ * when a local gate blocks; consumed + cleared at the next `dispatchIssue`
15551
+ * render for that issue.
15552
+ */
15553
+ priorGateFailureByIssue = /* @__PURE__ */ new Map();
15462
15554
  server;
15463
15555
  interval;
15464
15556
  heartbeatInterval;
@@ -15628,6 +15720,8 @@ var Orchestrator = class extends EventEmitter {
15628
15720
  this.setMaxListeners(50);
15629
15721
  this.config = config;
15630
15722
  this.promptTemplate = promptTemplate;
15723
+ this.localPromptTemplate = overrides?.localPromptTemplate;
15724
+ this.verifyRunner = overrides?.verifyRunner ?? defaultLocalVerifyRunner;
15631
15725
  this.state = createEmptyState(config);
15632
15726
  this.logger = new StructuredLogger();
15633
15727
  try {
@@ -16346,6 +16440,7 @@ ${messages}`);
16346
16440
  break;
16347
16441
  case "escalate":
16348
16442
  await this.handleEscalation(effect);
16443
+ this.priorGateFailureByIssue.delete(effect.issueId);
16349
16444
  await this.persistLaneSafe(effect.issueId, "abandon");
16350
16445
  break;
16351
16446
  case "claim":
@@ -16625,6 +16720,31 @@ ${messages}`);
16625
16720
  });
16626
16721
  }
16627
16722
  }
16723
+ /**
16724
+ * Phase 1 (local-backend-full-workflow): pick the dispatch template for
16725
+ * the resolved backend. `pi`/`local` backends get the bash-shaped local
16726
+ * template when one was loaded; every other backend — and any local
16727
+ * backend with no local template loaded (SC5) — gets the default. Pure
16728
+ * over (backendName, config.agent.backends, localPromptTemplate,
16729
+ * promptTemplate); unit-tested by orchestrator.template-resolution.test.ts.
16730
+ *
16731
+ * NOTE: the local template file (`harness.orchestrator.local.md`) carries a
16732
+ * full YAML frontmatter block, but that frontmatter is **intentionally
16733
+ * ignored** at dispatch — only the markdown body is loaded (WorkflowLoader
16734
+ * strips the frontmatter) and rendered here as the prompt. The frontmatter
16735
+ * exists solely so the file is a valid, self-documenting scaffold that
16736
+ * `harness init` can drop in; the orchestrator's configuration is always
16737
+ * read from the loaded `WorkflowConfig`, never from this template file's
16738
+ * frontmatter.
16739
+ */
16740
+ resolvePromptTemplate(backendName) {
16741
+ const def = this.config.agent.backends?.[backendName];
16742
+ const isLocal = def?.type === "local" || def?.type === "pi";
16743
+ if (isLocal && this.localPromptTemplate !== void 0) {
16744
+ return this.localPromptTemplate;
16745
+ }
16746
+ return this.promptTemplate;
16747
+ }
16628
16748
  /**
16629
16749
  * Dispatches a new agent to work on an issue.
16630
16750
  *
@@ -16712,10 +16832,6 @@ ${messages}`);
16712
16832
  void executeWorkflow(ctx, workflowPlan);
16713
16833
  return;
16714
16834
  }
16715
- const prompt = await this.renderer.render(this.promptTemplate, {
16716
- issue,
16717
- attempt: attempt || 1
16718
- });
16719
16835
  const useCase = buildRoutingUseCase(issue, backend, this.skillCatalog);
16720
16836
  const invocationOverride = process.env.HARNESS_BACKEND_OVERRIDE;
16721
16837
  const routerOpts = invocationOverride ? { invocationOverride } : void 0;
@@ -16753,6 +16869,25 @@ ${messages}`);
16753
16869
  const routingDefaultScalar = routingDefault !== void 0 ? toArray(routingDefault)[0] : void 0;
16754
16870
  routedBackendName = routingDefaultScalar ?? this.config.agent.backend ?? "unknown";
16755
16871
  }
16872
+ const renderedPrompt = await this.renderer.render(
16873
+ this.resolvePromptTemplate(routedBackendName),
16874
+ {
16875
+ issue,
16876
+ attempt: attempt || 1
16877
+ }
16878
+ );
16879
+ const priorGateFailure = this.priorGateFailureByIssue.get(issue.id);
16880
+ const prompt = priorGateFailure !== void 0 ? `${renderedPrompt}
16881
+
16882
+ ## Previous attempt failed the enforced gate
16883
+
16884
+ Your prior attempt was blocked by the harness gate and re-dispatched. Fix the following before shipping:
16885
+
16886
+ ${priorGateFailure}
16887
+ ` : renderedPrompt;
16888
+ if (priorGateFailure !== void 0) {
16889
+ this.priorGateFailureByIssue.delete(issue.id);
16890
+ }
16756
16891
  const session = {
16757
16892
  sessionId: `pending-${Date.now()}`,
16758
16893
  backendName: routedBackendName,
@@ -16806,7 +16941,14 @@ ${messages}`);
16806
16941
  const activeRunner = new AgentRunner(agentBackend, {
16807
16942
  maxTurns: this.config.agent.maxTurns
16808
16943
  });
16809
- this.runAgentInBackgroundTask(issue, workspacePath, prompt, attempt, activeRunner);
16944
+ this.runAgentInBackgroundTask(
16945
+ issue,
16946
+ workspacePath,
16947
+ prompt,
16948
+ attempt,
16949
+ activeRunner,
16950
+ routedBackendName
16951
+ );
16810
16952
  } catch (error) {
16811
16953
  if (await this.handleRoutingFailure(issue, error)) {
16812
16954
  return;
@@ -16840,8 +16982,9 @@ ${messages}`);
16840
16982
  await new Promise((r) => setTimeout(r, waitTime));
16841
16983
  }
16842
16984
  }
16843
- runAgentInBackgroundTask(issue, workspacePath, prompt, attempt, runner) {
16985
+ runAgentInBackgroundTask(issue, workspacePath, prompt, attempt, runner, routedBackendName) {
16844
16986
  const activeRunner = runner;
16987
+ const gateBackendName = routedBackendName ?? this.state.running.get(issue.id)?.session?.backendName;
16845
16988
  this.logger.info(`Starting background task for ${issue.identifier}`);
16846
16989
  const abortController = new AbortController();
16847
16990
  this.abortControllers.set(issue.id, { controller: abortController, pid: null });
@@ -16878,10 +17021,7 @@ ${messages}`);
16878
17021
  await this.emitWorkerExit(issue.id, "error", attempt, "Stopped by reconciliation");
16879
17022
  }
16880
17023
  } else {
16881
- const qualityClass = await this.deriveSingleAgentQualityVerdict(issue, workspacePath);
16882
- const retroClass = await this.deriveRoutingRetrospectiveVerdict(issue, workspacePath);
16883
- const outcomeClass = qualityClass ?? retroClass;
16884
- await this.emitWorkerExit(issue.id, "normal", attempt, void 0, outcomeClass);
17024
+ await this.finalizeNormalCompletion(issue, workspacePath, attempt, gateBackendName);
16885
17025
  }
16886
17026
  } catch (error) {
16887
17027
  this.logger.error(`Agent runner failed for ${issue.identifier}`, { error: String(error) });
@@ -16899,6 +17039,93 @@ ${messages}`);
16899
17039
  this.logger.error("Fatal error in background task", { error: String(err) });
16900
17040
  });
16901
17041
  }
17042
+ /**
17043
+ * local-backend-full-workflow Phase 2 (Option C): the normal-exit completion
17044
+ * seam, extracted so the enforced-gate loop is directly testable. Runs the
17045
+ * LOCAL-ONLY enforced gate BEFORE the exit is treated as terminal; a red gate
17046
+ * routes through the SHIPPED `emitWorkerExit('error', …)` retry branch — the
17047
+ * re-dispatch IS the re-prompt (the next render threads the failure preamble),
17048
+ * and `checkRetryBudget` exhaustion queues `needs-human`. On a green gate (or a
17049
+ * non-local backend, where the gate is a no-op `{ ok: true }`), the existing
17050
+ * Claude/AMR verdict feeders run and the run completes normally — composition,
17051
+ * not replacement.
17052
+ */
17053
+ async finalizeNormalCompletion(issue, workspacePath, attempt, gateBackendName) {
17054
+ const gate = gateBackendName !== void 0 ? (
17055
+ // B3: the VERIFY sub-gate is fail-CLOSED (a gate that can't run blocks +
17056
+ // re-dispatches). The outcome-eval sub-gate is fail-OPEN by design — an
17057
+ // unreachable eval provider (incl. a workflowGates:'primary' backend that's
17058
+ // down) degrades to a neutral verdict rather than wedging EVERY local
17059
+ // dispatch; the verify gate remains the hard safety floor.
17060
+ await this.runLocalWorkflowGate(issue, workspacePath, gateBackendName)
17061
+ ) : { ok: true };
17062
+ if (!gate.ok) {
17063
+ this.priorGateFailureByIssue.set(issue.id, gate.reason);
17064
+ this.logger.info(`local workflow gate blocked ${issue.identifier}; re-dispatching (SC3)`, {
17065
+ issueId: issue.id
17066
+ });
17067
+ await this.emitWorkerExit(issue.id, "error", attempt, gate.reason);
17068
+ return;
17069
+ }
17070
+ const qualityClass = await this.deriveSingleAgentQualityVerdict(issue, workspacePath);
17071
+ const retroClass = await this.deriveRoutingRetrospectiveVerdict(issue, workspacePath);
17072
+ const outcomeClass = qualityClass ?? retroClass;
17073
+ await this.emitWorkerExit(issue.id, "normal", attempt, void 0, outcomeClass);
17074
+ }
17075
+ /**
17076
+ * local-backend-full-workflow Phase 2 (Option C): the LOCAL-ONLY enforced
17077
+ * gate. For a `pi`/`local` dispatch it runs the mechanical gate (verify =
17078
+ * typecheck+lint+test via the injected `verifyRunner`) and, on green, the
17079
+ * outcome-eval (Task 7) against the workspace branch; a red result returns a
17080
+ * blocking `{ ok: false, reason }`. The completion path routes that reason
17081
+ * through `emitWorkerExit('error', …)` so the shipped state-machine retry
17082
+ * branch re-dispatches (the re-prompt) rather than marking the run complete.
17083
+ *
17084
+ * NON-local backends (Claude/AMR) get an unconditional `{ ok: true }` — this
17085
+ * gate never touches their completion path (D2 scopes enforcement to the
17086
+ * local path only; the AMR verdict feeders keep their existing behavior).
17087
+ *
17088
+ * Fully guarded: any thrown error → a CONSERVATIVE block `{ ok: false,
17089
+ * reason: 'gate error: …' }`, mirroring the shipped fail-safe pattern — a
17090
+ * gate that cannot run is treated as red (re-dispatch), never as a silent
17091
+ * pass that could ship a bad build.
17092
+ */
17093
+ async runLocalWorkflowGate(issue, workspacePath, backendName) {
17094
+ const def = this.config.agent.backends?.[backendName];
17095
+ const isLocal = def?.type === "local" || def?.type === "pi";
17096
+ if (!isLocal) return { ok: true };
17097
+ try {
17098
+ const verify = await this.verifyRunner(workspacePath);
17099
+ if (!verify.ok) {
17100
+ return {
17101
+ ok: false,
17102
+ reason: `verify failed:
17103
+ ${truncateGateOutput(verify.output)}`
17104
+ };
17105
+ }
17106
+ if (issue.spec !== null) {
17107
+ const model = this.config.agent.routing?.policy?.acceptanceEval?.model;
17108
+ const evalClass = await this.evaluateOutcomeCore(issue, workspacePath, model, "local");
17109
+ if (evalClass === "quality-fail") {
17110
+ return {
17111
+ ok: false,
17112
+ reason: "outcome-eval returned a high-confidence NOT_SATISFIED verdict: the implementation does not satisfy the spec."
17113
+ };
17114
+ }
17115
+ }
17116
+ return { ok: true };
17117
+ } catch (err) {
17118
+ const msg = err instanceof Error ? err.message : String(err);
17119
+ this.logger.warn(
17120
+ `local workflow gate errored for ${issue.identifier}; blocking conservatively`,
17121
+ {
17122
+ issueId: issue.id,
17123
+ error: msg
17124
+ }
17125
+ );
17126
+ return { ok: false, reason: `gate error: ${msg}` };
17127
+ }
17128
+ }
16902
17129
  /**
16903
17130
  * AMR 4c (ADR 0069): the sound single-agent quality-verdict feeder. On a normal
16904
17131
  * exit, when AMR is active, run a BASELINE-RELATIVE security scan of the lines
@@ -16950,13 +17177,82 @@ ${messages}`);
16950
17177
  async deriveAcceptanceEvalVerdict(issue, workspacePath) {
16951
17178
  const acceptanceEval = this.config.agent.routing?.policy?.acceptanceEval;
16952
17179
  if (acceptanceEval?.enabled !== true || issue.spec === null) return void 0;
16953
- const provider = this.resolveComplexityProvider();
17180
+ return this.evaluateOutcomeCore(issue, workspacePath, acceptanceEval.model, "amr");
17181
+ }
17182
+ /**
17183
+ * local-backend-full-workflow Phase 2 (Option C, SC4): the SHARED outcome-eval
17184
+ * core, lifted out of `deriveAcceptanceEvalVerdict` so BOTH callers reuse the
17185
+ * same `OutcomeEvaluator` engine over the introduced diff vs the spec's
17186
+ * judgment section. It does NOT gate on `adaptiveRouter !== null` or
17187
+ * `acceptanceEval.enabled` — those guards live in the AMR caller above; the
17188
+ * LOCAL gate (D2) always evaluates when a spec is present. Maps a
17189
+ * high-confidence NOT_SATISFIED to `'quality-fail'`; conservative + fully
17190
+ * guarded: no spec / no provider / empty diff / any error → `undefined`.
17191
+ */
17192
+ /**
17193
+ * local-backend-full-workflow Phase 3 (D5/SC6): resolve the AnalysisProvider
17194
+ * for the outcome-eval gate. Caller-gated: ONLY the LOCAL gate caller consults
17195
+ * `agent.routing.workflowGates`. The AMR caller ALWAYS uses local SEL
17196
+ * (`resolveComplexityProvider`) so the AMR acceptance-eval path is byte-identical
17197
+ * (SC-neutral). When `workflowGates === 'primary'` on the local caller, resolve
17198
+ * from the primary (routing.default) backend; any miss degrades to local SEL
17199
+ * (fail-open — an unreachable stronger provider must NOT wedge the local gate;
17200
+ * see the fail-open note at the runLocalWorkflowGate call site).
17201
+ */
17202
+ resolveOutcomeEvalProvider(caller) {
17203
+ if (caller === "local" && this.config.agent.routing?.workflowGates === "primary") {
17204
+ return this.resolvePrimaryOutcomeEvalProvider() ?? this.resolveComplexityProvider();
17205
+ }
17206
+ return this.resolveComplexityProvider();
17207
+ }
17208
+ /**
17209
+ * Build an AnalysisProvider from the PRIMARY (routing.default) backend for the
17210
+ * Phase-3 `workflowGates:'primary'` seam. Reuses the shipped
17211
+ * `buildAnalysisProvider` translator but forces the router to the default
17212
+ * backend. Returns undefined (→ caller degrades to local SEL) when intelligence
17213
+ * is disabled, the factory is absent, or the default backend cannot produce a
17214
+ * provider — fully guarded, never throws.
17215
+ */
17216
+ resolvePrimaryOutcomeEvalProvider() {
17217
+ try {
17218
+ if (!this.config.intelligence?.enabled || !this.backendFactory) return void 0;
17219
+ const backends = this.config.agent.backends;
17220
+ const def0 = this.config.agent.routing?.default;
17221
+ const defaultName = Array.isArray(def0) ? def0[0] : def0;
17222
+ if (defaultName === void 0) return void 0;
17223
+ const def = backends?.[defaultName];
17224
+ if (!def) return void 0;
17225
+ return buildAnalysisProvider({
17226
+ def,
17227
+ backendName: defaultName,
17228
+ layer: "sel",
17229
+ getResolverStatusSnapshot: () => {
17230
+ const resolver = this.localResolvers.get(defaultName);
17231
+ if (!resolver) return null;
17232
+ const s = resolver.getStatus();
17233
+ return {
17234
+ available: s.available,
17235
+ resolved: s.resolved,
17236
+ configured: s.configured,
17237
+ detected: s.detected
17238
+ };
17239
+ },
17240
+ intelligence: this.config.intelligence,
17241
+ logger: this.logger
17242
+ }) ?? void 0;
17243
+ } catch {
17244
+ return void 0;
17245
+ }
17246
+ }
17247
+ async evaluateOutcomeCore(issue, workspacePath, model, caller) {
17248
+ if (issue.spec === null) return void 0;
17249
+ const provider = this.resolveOutcomeEvalProvider(caller);
16954
17250
  if (provider === void 0) return void 0;
16955
17251
  try {
16956
17252
  const diff = await this.workspace.getIntroducedDiffText(issue.identifier);
16957
17253
  if (diff.trim() === "") return void 0;
16958
17254
  const evaluator = new OutcomeEvaluator(provider, this.graphStore ?? new GraphStore2(), {
16959
- ...acceptanceEval.model !== void 0 ? { model: acceptanceEval.model } : {}
17255
+ ...model !== void 0 ? { model } : {}
16960
17256
  });
16961
17257
  const verdict = await evaluator.evaluate({
16962
17258
  specPath: path21.join(workspacePath, issue.spec),
@@ -16968,14 +17264,17 @@ ${messages}`);
16968
17264
  });
16969
17265
  const cls = outcomeVerdictToQualityFail(verdict);
16970
17266
  if (cls === "quality-fail") {
16971
- this.logger.info("amr:quality-fail \u2014 acceptance-eval NOT_SATISFIED (high confidence)", {
16972
- issueId: issue.id,
16973
- rationale: verdict.rationale
16974
- });
17267
+ this.logger.info(
17268
+ `${caller}:quality-fail \u2014 acceptance-eval NOT_SATISFIED (high confidence)`,
17269
+ {
17270
+ issueId: issue.id,
17271
+ rationale: verdict.rationale
17272
+ }
17273
+ );
16975
17274
  }
16976
17275
  return cls;
16977
17276
  } catch (err) {
16978
- this.logger.debug("amr acceptance-eval skipped (best-effort)", {
17277
+ this.logger.debug(`${caller} acceptance-eval skipped (best-effort)`, {
16979
17278
  issueId: issue.id,
16980
17279
  error: err instanceof Error ? err.message : String(err)
16981
17280
  });
@@ -17542,6 +17841,15 @@ ${messages}`);
17542
17841
  }
17543
17842
  this.seedRecommender(candidates, "frozen");
17544
17843
  const recommend = (hardware) => this.modelRecommender(hardware);
17844
+ let localEndpoint;
17845
+ let localApiKey;
17846
+ for (const def of Object.values(this.config.agent.backends ?? {})) {
17847
+ if ((def.type === "local" || def.type === "pi") && typeof def.endpoint === "string") {
17848
+ localEndpoint = def.endpoint;
17849
+ localApiKey = def.apiKey;
17850
+ break;
17851
+ }
17852
+ }
17545
17853
  this.refreshScheduler = new RefreshScheduler({
17546
17854
  runTick: () => runRefreshTick({
17547
17855
  detectHardware: () => this.detectLmlmHardware(),
@@ -17559,7 +17867,14 @@ ${messages}`);
17559
17867
  target: c.target.ollamaName
17560
17868
  });
17561
17869
  }),
17562
- proposalThreshold: refreshCfg?.proposalThreshold ?? 5
17870
+ proposalThreshold: refreshCfg?.proposalThreshold ?? 5,
17871
+ ...localEndpoint !== void 0 ? {
17872
+ probeToolCalling: (ollamaName) => probeToolCalling({
17873
+ model: ollamaName,
17874
+ baseUrl: localEndpoint,
17875
+ ...localApiKey !== void 0 ? { apiKey: localApiKey } : {}
17876
+ })
17877
+ } : {}
17563
17878
  }).then((result) => {
17564
17879
  void this.drainDeferredEvictions();
17565
17880
  return result;
@@ -18271,7 +18586,8 @@ async function triageIssue2(issue, deps = {}) {
18271
18586
  ...graph ? { graph } : {},
18272
18587
  ...deps.precedent ? { precedent: deps.precedent } : {},
18273
18588
  ...deps.config ? { config: deps.config } : {},
18274
- ...deps.models ? { models: deps.models } : {}
18589
+ ...deps.models ? { models: deps.models } : {},
18590
+ ...deps.modelDeferred ? { modelDeferred: true } : {}
18275
18591
  });
18276
18592
  }
18277
18593
 
@@ -18301,7 +18617,7 @@ var ForkStepSchema = z18.object({
18301
18617
  var BRAINSTORM_RUBRIC = 'You are running an AUTONOMOUS brainstorm over one backlog item: enumerate each design fork (a decision with mutually-exclusive options), then recommend a default for the NEXT unresolved fork with a self-assessed confidence. Report confidence HIGH only when the recommendation is clearly correct on technical grounds alone. Report LOW (so a human decides) whenever the fork involves ANY of: a product or UX tradeoff, an unrevealed business priority, a security-sensitive or irreversible/outward-facing action, or a genuine judgment call between comparable options. Never invent forks to pad the spec; set done=true once every real fork is resolved. Be conservative \u2014 a false "high" is far more costly than asking a human.';
18302
18618
  function makeSelForkGenerator(provider, input, opts = {}) {
18303
18619
  const samples = Math.max(2, opts.samples ?? 3);
18304
- const maxTokens = opts.maxTokens ?? 512;
18620
+ const maxTokens = opts.maxTokens ?? 4096;
18305
18621
  return {
18306
18622
  async next(index, priorDecisions) {
18307
18623
  const prompt = buildForkPrompt(input, priorDecisions, index);
@@ -19200,6 +19516,11 @@ function buildArchiveHooks(opts) {
19200
19516
 
19201
19517
  // src/index.ts
19202
19518
  import { discoverCandidates } from "@harness-engineering/local-models";
19519
+ import {
19520
+ PoolStateStore as PoolStateStore2,
19521
+ poolStateToCandidates as poolStateToCandidates3,
19522
+ DEFAULT_POOL_STATE_PATH
19523
+ } from "@harness-engineering/local-models";
19203
19524
  export {
19204
19525
  AdaptiveRouter,
19205
19526
  AnalysisArchive,
@@ -19209,6 +19530,7 @@ export {
19209
19530
  BackendRouter,
19210
19531
  CheckScriptRunner,
19211
19532
  ClaimManager,
19533
+ DEFAULT_POOL_STATE_PATH,
19212
19534
  EscalationState,
19213
19535
  GateNotReadyError,
19214
19536
  GateRunError,
@@ -19225,6 +19547,7 @@ export {
19225
19547
  Orchestrator,
19226
19548
  OrchestratorBackendFactory,
19227
19549
  PRDetector,
19550
+ PoolStateStore2 as PoolStateStore,
19228
19551
  PrivacyNoMatch,
19229
19552
  PromotionError,
19230
19553
  PromptRenderer,
@@ -19260,6 +19583,7 @@ export {
19260
19583
  createEmptyState,
19261
19584
  crossFieldRoutingIssues,
19262
19585
  defaultFetchModels,
19586
+ defaultLocalVerifyRunner,
19263
19587
  defaultPoolCapabilities,
19264
19588
  deriveSeedPaths,
19265
19589
  detectScopeTier,
@@ -19293,6 +19617,7 @@ export {
19293
19617
  normalizeLocalModel,
19294
19618
  openSearchIndex,
19295
19619
  pilotScore,
19620
+ poolStateToCandidates3 as poolStateToCandidates,
19296
19621
  precedentLookupFromStored,
19297
19622
  promote,
19298
19623
  rankTriageCandidates,
@@ -19321,6 +19646,7 @@ export {
19321
19646
  syncMain,
19322
19647
  triageIssue2 as triageIssue,
19323
19648
  truncateForBudget,
19649
+ truncateGateOutput,
19324
19650
  validateCustomTasks,
19325
19651
  validateWorkflowConfig,
19326
19652
  wireNotificationSinks,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-engineering/orchestrator",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Orchestrator daemon for dispatching coding agents to issues",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -48,11 +48,11 @@
48
48
  "ws": "^8.21.0",
49
49
  "yaml": "^2.8.3",
50
50
  "zod": "^3.25.76",
51
- "@harness-engineering/core": "0.37.0",
52
- "@harness-engineering/graph": "0.11.8",
53
- "@harness-engineering/intelligence": "0.7.0",
54
- "@harness-engineering/local-models": "0.5.2",
55
- "@harness-engineering/types": "0.22.0"
51
+ "@harness-engineering/core": "0.37.1",
52
+ "@harness-engineering/graph": "0.11.9",
53
+ "@harness-engineering/intelligence": "0.9.0",
54
+ "@harness-engineering/local-models": "0.6.0",
55
+ "@harness-engineering/types": "0.23.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@asteasolutions/zod-to-openapi": "^7.3.0",