@harness-engineering/orchestrator 0.15.1 → 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.js CHANGED
@@ -38,6 +38,7 @@ __export(index_exports, {
38
38
  BackendRouter: () => BackendRouter,
39
39
  CheckScriptRunner: () => CheckScriptRunner,
40
40
  ClaimManager: () => ClaimManager,
41
+ DEFAULT_POOL_STATE_PATH: () => import_local_models8.DEFAULT_POOL_STATE_PATH,
41
42
  EscalationState: () => EscalationState,
42
43
  GateNotReadyError: () => GateNotReadyError,
43
44
  GateRunError: () => GateRunError,
@@ -54,6 +55,7 @@ __export(index_exports, {
54
55
  Orchestrator: () => Orchestrator,
55
56
  OrchestratorBackendFactory: () => OrchestratorBackendFactory,
56
57
  PRDetector: () => PRDetector,
58
+ PoolStateStore: () => import_local_models8.PoolStateStore,
57
59
  PrivacyNoMatch: () => PrivacyNoMatch,
58
60
  PromotionError: () => PromotionError,
59
61
  PromptRenderer: () => PromptRenderer,
@@ -89,6 +91,7 @@ __export(index_exports, {
89
91
  createEmptyState: () => createEmptyState,
90
92
  crossFieldRoutingIssues: () => crossFieldRoutingIssues,
91
93
  defaultFetchModels: () => defaultFetchModels,
94
+ defaultLocalVerifyRunner: () => defaultLocalVerifyRunner,
92
95
  defaultPoolCapabilities: () => defaultPoolCapabilities,
93
96
  deriveSeedPaths: () => deriveSeedPaths,
94
97
  detectScopeTier: () => detectScopeTier,
@@ -122,6 +125,7 @@ __export(index_exports, {
122
125
  normalizeLocalModel: () => normalizeLocalModel,
123
126
  openSearchIndex: () => openSearchIndex,
124
127
  pilotScore: () => import_intelligence13.pilotScore,
128
+ poolStateToCandidates: () => import_local_models8.poolStateToCandidates,
125
129
  precedentLookupFromStored: () => precedentLookupFromStored,
126
130
  promote: () => promote,
127
131
  rankTriageCandidates: () => import_intelligence13.rankTriageCandidates,
@@ -150,6 +154,7 @@ __export(index_exports, {
150
154
  syncMain: () => syncMain,
151
155
  triageIssue: () => triageIssue2,
152
156
  truncateForBudget: () => truncateForBudget,
157
+ truncateGateOutput: () => truncateGateOutput,
153
158
  validateCustomTasks: () => validateCustomTasks,
154
159
  validateWorkflowConfig: () => validateWorkflowConfig,
155
160
  wireNotificationSinks: () => wireNotificationSinks,
@@ -2167,7 +2172,11 @@ var RoutingConfigSchema = import_zod.z.object({
2167
2172
  // --- AMR: opt-in adaptive-routing policy. Its PRESENCE flips the orchestrator
2168
2173
  // from identity/default dispatch to complexity-aware tier routing (default-off
2169
2174
  // when absent). Previously accepted by the runtime PUT endpoint only. ---
2170
- policy: RoutingPolicySchema.optional()
2175
+ policy: RoutingPolicySchema.optional(),
2176
+ // local-backend-full-workflow Phase 3 (D5/SC6): routes the LOCAL outcome-eval
2177
+ // gate to the primary backend when 'primary'. .strict() below means this MUST
2178
+ // be declared here or a config with workflowGates is silently rejected.
2179
+ workflowGates: import_zod.z.enum(["local", "primary"]).optional()
2171
2180
  }).strict();
2172
2181
  var CONFIDENCE = import_zod.z.enum(["low", "medium", "high"]);
2173
2182
  var V1_MAX_RATCHET_STAGE = 2;
@@ -2456,9 +2465,19 @@ var WorkflowLoader = class {
2456
2465
  if (!configResult.ok) {
2457
2466
  return (0, import_types3.Err)(configResult.error);
2458
2467
  }
2468
+ const localTemplatePath = path7.join(path7.dirname(filePath), "harness.orchestrator.local.md");
2469
+ let localPromptTemplate;
2470
+ try {
2471
+ const localContent = await fs7.readFile(localTemplatePath, "utf-8");
2472
+ const localParts = localContent.split("---");
2473
+ localPromptTemplate = localParts.length >= 3 ? localParts.slice(2).join("---").trim() : localContent.trim();
2474
+ } catch {
2475
+ localPromptTemplate = void 0;
2476
+ }
2459
2477
  return (0, import_types3.Ok)({
2460
2478
  config: configResult.value.config,
2461
2479
  promptTemplate,
2480
+ ...localPromptTemplate !== void 0 ? { localPromptTemplate } : {},
2462
2481
  warnings: configResult.value.warnings
2463
2482
  });
2464
2483
  } catch (error) {
@@ -4479,7 +4498,7 @@ var LocalModelResolver = class {
4479
4498
  if (useCase === void 0) return this.resolved;
4480
4499
  const profile = useCaseToProfile(useCase);
4481
4500
  if (profile === "general") return this.resolved;
4482
- return this.selectMatch(this.candidates(profile), this.detected);
4501
+ return this.selectMatch(this.candidates(profile, { requireToolCalling: true }), this.detected);
4483
4502
  }
4484
4503
  getStatus() {
4485
4504
  return {
@@ -4497,8 +4516,8 @@ var LocalModelResolver = class {
4497
4516
  * from pool entries (currentScore desc → ollamaName); otherwise the static
4498
4517
  * `configured` list is returned unchanged (byte-identical to pre-Phase-4).
4499
4518
  */
4500
- candidates(profile) {
4501
- return this.poolState ? (0, import_local_models.poolStateToCandidates)(this.poolState.snapshot(), profile) : [...this.configured];
4519
+ candidates(profile, opts) {
4520
+ return this.poolState ? (0, import_local_models.poolStateToCandidates)(this.poolState.snapshot(), profile, opts) : [...this.configured];
4502
4521
  }
4503
4522
  onStatusChange(handler) {
4504
4523
  this.listeners.add(handler);
@@ -6140,13 +6159,14 @@ function mapPiEvent(rawEvent, sessionId) {
6140
6159
  }
6141
6160
  return null;
6142
6161
  }
6162
+ var LOCAL_PROVIDER = "harness-local";
6143
6163
  function buildLocalModel(config) {
6144
6164
  if (!config.model) return void 0;
6145
6165
  return {
6146
6166
  id: config.model,
6147
6167
  name: config.model,
6148
6168
  api: "openai-completions",
6149
- provider: "harness-local",
6169
+ provider: LOCAL_PROVIDER,
6150
6170
  baseUrl: config.endpoint ?? "http://localhost:1234/v1",
6151
6171
  reasoning: false,
6152
6172
  input: ["text"],
@@ -6194,9 +6214,12 @@ var PiBackend = class {
6194
6214
  endpoint: this.config.endpoint,
6195
6215
  apiKey: this.config.apiKey
6196
6216
  });
6217
+ const authStorage = piSdk.AuthStorage.inMemory();
6218
+ authStorage.setRuntimeApiKey(LOCAL_PROVIDER, this.config.apiKey ?? "ollama");
6197
6219
  const { session: piSession } = await piSdk.createAgentSession({
6198
6220
  cwd: params.workspacePath,
6199
6221
  ...model !== void 0 && { model },
6222
+ authStorage,
6200
6223
  sessionManager: piSdk.SessionManager.inMemory()
6201
6224
  });
6202
6225
  const session = {
@@ -15450,6 +15473,55 @@ function normalizeHarnessCommand(command) {
15450
15473
  if (command[0] === "harness") return command;
15451
15474
  return ["harness", ...command];
15452
15475
  }
15476
+ function truncateGateOutput(output, max = 4e3) {
15477
+ const trimmed = output.trim();
15478
+ if (trimmed.length <= max) return trimmed;
15479
+ const head = trimmed.slice(0, Math.floor(max * 0.7));
15480
+ const tail = trimmed.slice(-Math.floor(max * 0.3));
15481
+ return `${head}
15482
+ \u2026 [truncated ${trimmed.length - max} chars] \u2026
15483
+ ${tail}`;
15484
+ }
15485
+ async function defaultLocalVerifyRunner(workspacePath) {
15486
+ const cp2 = await import("child_process");
15487
+ const fsMod = await import("fs");
15488
+ const pathMod = await import("path");
15489
+ const run = (script) => new Promise((resolve8) => {
15490
+ cp2.execFile(
15491
+ "pnpm",
15492
+ ["-w", "run", script],
15493
+ { cwd: workspacePath, maxBuffer: 32 * 1024 * 1024 },
15494
+ (error, stdout, stderr) => {
15495
+ if (error) {
15496
+ resolve8({
15497
+ ok: false,
15498
+ output: `${script} failed:
15499
+ ${stdout ?? ""}
15500
+ ${stderr ?? error.message}`
15501
+ });
15502
+ } else {
15503
+ resolve8({ ok: true, output: "" });
15504
+ }
15505
+ }
15506
+ );
15507
+ });
15508
+ const pkgPath = pathMod.join(workspacePath, "package.json");
15509
+ if (!fsMod.existsSync(pkgPath)) return { ok: true, output: "" };
15510
+ const scripts = (() => {
15511
+ try {
15512
+ const pkg = JSON.parse(fsMod.readFileSync(pkgPath, "utf8"));
15513
+ return pkg.scripts ?? {};
15514
+ } catch {
15515
+ return {};
15516
+ }
15517
+ })();
15518
+ for (const script of ["typecheck", "lint", "test"]) {
15519
+ if (scripts[script] === void 0) continue;
15520
+ const result = await run(script);
15521
+ if (!result.ok) return result;
15522
+ }
15523
+ return { ok: true, output: "" };
15524
+ }
15453
15525
  var Orchestrator = class extends import_node_events.EventEmitter {
15454
15526
  state;
15455
15527
  config;
@@ -15499,6 +15571,30 @@ var Orchestrator = class extends import_node_events.EventEmitter {
15499
15571
  overrideBackend;
15500
15572
  renderer;
15501
15573
  promptTemplate;
15574
+ /**
15575
+ * Backend-aware local dispatch template (Phase 1). Set from
15576
+ * `overrides.localPromptTemplate` (production: threaded by the CLI from
15577
+ * WorkflowLoader). Undefined -> resolvePromptTemplate falls back to the
15578
+ * default template (SC5).
15579
+ */
15580
+ localPromptTemplate;
15581
+ /**
15582
+ * local-backend-full-workflow Phase 2 (Option C): the verify runner the
15583
+ * local-only enforced gate (`runLocalWorkflowGate`) invokes to run the
15584
+ * project's mechanical gate (typecheck + lint + test) over the workspace.
15585
+ * Injected in tests to force fail→pass sequences; in production it defaults
15586
+ * to `defaultLocalVerifyRunner` (a thin project-script probe). Kept as a
15587
+ * field seam — mirrors how `execFileFn` is injected — so the completion path
15588
+ * is decoupled from the concrete detector.
15589
+ */
15590
+ verifyRunner;
15591
+ /**
15592
+ * Phase 2: the most recent gate-failure reason per issue, threaded into the
15593
+ * next dispatch's rendered prompt as a failure preamble (the re-prompt). Set
15594
+ * when a local gate blocks; consumed + cleared at the next `dispatchIssue`
15595
+ * render for that issue.
15596
+ */
15597
+ priorGateFailureByIssue = /* @__PURE__ */ new Map();
15502
15598
  server;
15503
15599
  interval;
15504
15600
  heartbeatInterval;
@@ -15668,6 +15764,8 @@ var Orchestrator = class extends import_node_events.EventEmitter {
15668
15764
  this.setMaxListeners(50);
15669
15765
  this.config = config;
15670
15766
  this.promptTemplate = promptTemplate;
15767
+ this.localPromptTemplate = overrides?.localPromptTemplate;
15768
+ this.verifyRunner = overrides?.verifyRunner ?? defaultLocalVerifyRunner;
15671
15769
  this.state = createEmptyState(config);
15672
15770
  this.logger = new StructuredLogger();
15673
15771
  try {
@@ -16386,6 +16484,7 @@ ${messages}`);
16386
16484
  break;
16387
16485
  case "escalate":
16388
16486
  await this.handleEscalation(effect);
16487
+ this.priorGateFailureByIssue.delete(effect.issueId);
16389
16488
  await this.persistLaneSafe(effect.issueId, "abandon");
16390
16489
  break;
16391
16490
  case "claim":
@@ -16665,6 +16764,31 @@ ${messages}`);
16665
16764
  });
16666
16765
  }
16667
16766
  }
16767
+ /**
16768
+ * Phase 1 (local-backend-full-workflow): pick the dispatch template for
16769
+ * the resolved backend. `pi`/`local` backends get the bash-shaped local
16770
+ * template when one was loaded; every other backend — and any local
16771
+ * backend with no local template loaded (SC5) — gets the default. Pure
16772
+ * over (backendName, config.agent.backends, localPromptTemplate,
16773
+ * promptTemplate); unit-tested by orchestrator.template-resolution.test.ts.
16774
+ *
16775
+ * NOTE: the local template file (`harness.orchestrator.local.md`) carries a
16776
+ * full YAML frontmatter block, but that frontmatter is **intentionally
16777
+ * ignored** at dispatch — only the markdown body is loaded (WorkflowLoader
16778
+ * strips the frontmatter) and rendered here as the prompt. The frontmatter
16779
+ * exists solely so the file is a valid, self-documenting scaffold that
16780
+ * `harness init` can drop in; the orchestrator's configuration is always
16781
+ * read from the loaded `WorkflowConfig`, never from this template file's
16782
+ * frontmatter.
16783
+ */
16784
+ resolvePromptTemplate(backendName) {
16785
+ const def = this.config.agent.backends?.[backendName];
16786
+ const isLocal = def?.type === "local" || def?.type === "pi";
16787
+ if (isLocal && this.localPromptTemplate !== void 0) {
16788
+ return this.localPromptTemplate;
16789
+ }
16790
+ return this.promptTemplate;
16791
+ }
16668
16792
  /**
16669
16793
  * Dispatches a new agent to work on an issue.
16670
16794
  *
@@ -16752,10 +16876,6 @@ ${messages}`);
16752
16876
  void executeWorkflow(ctx, workflowPlan);
16753
16877
  return;
16754
16878
  }
16755
- const prompt = await this.renderer.render(this.promptTemplate, {
16756
- issue,
16757
- attempt: attempt || 1
16758
- });
16759
16879
  const useCase = buildRoutingUseCase(issue, backend, this.skillCatalog);
16760
16880
  const invocationOverride = process.env.HARNESS_BACKEND_OVERRIDE;
16761
16881
  const routerOpts = invocationOverride ? { invocationOverride } : void 0;
@@ -16793,6 +16913,25 @@ ${messages}`);
16793
16913
  const routingDefaultScalar = routingDefault !== void 0 ? toArray(routingDefault)[0] : void 0;
16794
16914
  routedBackendName = routingDefaultScalar ?? this.config.agent.backend ?? "unknown";
16795
16915
  }
16916
+ const renderedPrompt = await this.renderer.render(
16917
+ this.resolvePromptTemplate(routedBackendName),
16918
+ {
16919
+ issue,
16920
+ attempt: attempt || 1
16921
+ }
16922
+ );
16923
+ const priorGateFailure = this.priorGateFailureByIssue.get(issue.id);
16924
+ const prompt = priorGateFailure !== void 0 ? `${renderedPrompt}
16925
+
16926
+ ## Previous attempt failed the enforced gate
16927
+
16928
+ Your prior attempt was blocked by the harness gate and re-dispatched. Fix the following before shipping:
16929
+
16930
+ ${priorGateFailure}
16931
+ ` : renderedPrompt;
16932
+ if (priorGateFailure !== void 0) {
16933
+ this.priorGateFailureByIssue.delete(issue.id);
16934
+ }
16796
16935
  const session = {
16797
16936
  sessionId: `pending-${Date.now()}`,
16798
16937
  backendName: routedBackendName,
@@ -16846,7 +16985,14 @@ ${messages}`);
16846
16985
  const activeRunner = new AgentRunner(agentBackend, {
16847
16986
  maxTurns: this.config.agent.maxTurns
16848
16987
  });
16849
- this.runAgentInBackgroundTask(issue, workspacePath, prompt, attempt, activeRunner);
16988
+ this.runAgentInBackgroundTask(
16989
+ issue,
16990
+ workspacePath,
16991
+ prompt,
16992
+ attempt,
16993
+ activeRunner,
16994
+ routedBackendName
16995
+ );
16850
16996
  } catch (error) {
16851
16997
  if (await this.handleRoutingFailure(issue, error)) {
16852
16998
  return;
@@ -16880,8 +17026,9 @@ ${messages}`);
16880
17026
  await new Promise((r) => setTimeout(r, waitTime));
16881
17027
  }
16882
17028
  }
16883
- runAgentInBackgroundTask(issue, workspacePath, prompt, attempt, runner) {
17029
+ runAgentInBackgroundTask(issue, workspacePath, prompt, attempt, runner, routedBackendName) {
16884
17030
  const activeRunner = runner;
17031
+ const gateBackendName = routedBackendName ?? this.state.running.get(issue.id)?.session?.backendName;
16885
17032
  this.logger.info(`Starting background task for ${issue.identifier}`);
16886
17033
  const abortController = new AbortController();
16887
17034
  this.abortControllers.set(issue.id, { controller: abortController, pid: null });
@@ -16918,10 +17065,7 @@ ${messages}`);
16918
17065
  await this.emitWorkerExit(issue.id, "error", attempt, "Stopped by reconciliation");
16919
17066
  }
16920
17067
  } else {
16921
- const qualityClass = await this.deriveSingleAgentQualityVerdict(issue, workspacePath);
16922
- const retroClass = await this.deriveRoutingRetrospectiveVerdict(issue, workspacePath);
16923
- const outcomeClass = qualityClass ?? retroClass;
16924
- await this.emitWorkerExit(issue.id, "normal", attempt, void 0, outcomeClass);
17068
+ await this.finalizeNormalCompletion(issue, workspacePath, attempt, gateBackendName);
16925
17069
  }
16926
17070
  } catch (error) {
16927
17071
  this.logger.error(`Agent runner failed for ${issue.identifier}`, { error: String(error) });
@@ -16939,6 +17083,93 @@ ${messages}`);
16939
17083
  this.logger.error("Fatal error in background task", { error: String(err) });
16940
17084
  });
16941
17085
  }
17086
+ /**
17087
+ * local-backend-full-workflow Phase 2 (Option C): the normal-exit completion
17088
+ * seam, extracted so the enforced-gate loop is directly testable. Runs the
17089
+ * LOCAL-ONLY enforced gate BEFORE the exit is treated as terminal; a red gate
17090
+ * routes through the SHIPPED `emitWorkerExit('error', …)` retry branch — the
17091
+ * re-dispatch IS the re-prompt (the next render threads the failure preamble),
17092
+ * and `checkRetryBudget` exhaustion queues `needs-human`. On a green gate (or a
17093
+ * non-local backend, where the gate is a no-op `{ ok: true }`), the existing
17094
+ * Claude/AMR verdict feeders run and the run completes normally — composition,
17095
+ * not replacement.
17096
+ */
17097
+ async finalizeNormalCompletion(issue, workspacePath, attempt, gateBackendName) {
17098
+ const gate = gateBackendName !== void 0 ? (
17099
+ // B3: the VERIFY sub-gate is fail-CLOSED (a gate that can't run blocks +
17100
+ // re-dispatches). The outcome-eval sub-gate is fail-OPEN by design — an
17101
+ // unreachable eval provider (incl. a workflowGates:'primary' backend that's
17102
+ // down) degrades to a neutral verdict rather than wedging EVERY local
17103
+ // dispatch; the verify gate remains the hard safety floor.
17104
+ await this.runLocalWorkflowGate(issue, workspacePath, gateBackendName)
17105
+ ) : { ok: true };
17106
+ if (!gate.ok) {
17107
+ this.priorGateFailureByIssue.set(issue.id, gate.reason);
17108
+ this.logger.info(`local workflow gate blocked ${issue.identifier}; re-dispatching (SC3)`, {
17109
+ issueId: issue.id
17110
+ });
17111
+ await this.emitWorkerExit(issue.id, "error", attempt, gate.reason);
17112
+ return;
17113
+ }
17114
+ const qualityClass = await this.deriveSingleAgentQualityVerdict(issue, workspacePath);
17115
+ const retroClass = await this.deriveRoutingRetrospectiveVerdict(issue, workspacePath);
17116
+ const outcomeClass = qualityClass ?? retroClass;
17117
+ await this.emitWorkerExit(issue.id, "normal", attempt, void 0, outcomeClass);
17118
+ }
17119
+ /**
17120
+ * local-backend-full-workflow Phase 2 (Option C): the LOCAL-ONLY enforced
17121
+ * gate. For a `pi`/`local` dispatch it runs the mechanical gate (verify =
17122
+ * typecheck+lint+test via the injected `verifyRunner`) and, on green, the
17123
+ * outcome-eval (Task 7) against the workspace branch; a red result returns a
17124
+ * blocking `{ ok: false, reason }`. The completion path routes that reason
17125
+ * through `emitWorkerExit('error', …)` so the shipped state-machine retry
17126
+ * branch re-dispatches (the re-prompt) rather than marking the run complete.
17127
+ *
17128
+ * NON-local backends (Claude/AMR) get an unconditional `{ ok: true }` — this
17129
+ * gate never touches their completion path (D2 scopes enforcement to the
17130
+ * local path only; the AMR verdict feeders keep their existing behavior).
17131
+ *
17132
+ * Fully guarded: any thrown error → a CONSERVATIVE block `{ ok: false,
17133
+ * reason: 'gate error: …' }`, mirroring the shipped fail-safe pattern — a
17134
+ * gate that cannot run is treated as red (re-dispatch), never as a silent
17135
+ * pass that could ship a bad build.
17136
+ */
17137
+ async runLocalWorkflowGate(issue, workspacePath, backendName) {
17138
+ const def = this.config.agent.backends?.[backendName];
17139
+ const isLocal = def?.type === "local" || def?.type === "pi";
17140
+ if (!isLocal) return { ok: true };
17141
+ try {
17142
+ const verify = await this.verifyRunner(workspacePath);
17143
+ if (!verify.ok) {
17144
+ return {
17145
+ ok: false,
17146
+ reason: `verify failed:
17147
+ ${truncateGateOutput(verify.output)}`
17148
+ };
17149
+ }
17150
+ if (issue.spec !== null) {
17151
+ const model = this.config.agent.routing?.policy?.acceptanceEval?.model;
17152
+ const evalClass = await this.evaluateOutcomeCore(issue, workspacePath, model, "local");
17153
+ if (evalClass === "quality-fail") {
17154
+ return {
17155
+ ok: false,
17156
+ reason: "outcome-eval returned a high-confidence NOT_SATISFIED verdict: the implementation does not satisfy the spec."
17157
+ };
17158
+ }
17159
+ }
17160
+ return { ok: true };
17161
+ } catch (err) {
17162
+ const msg = err instanceof Error ? err.message : String(err);
17163
+ this.logger.warn(
17164
+ `local workflow gate errored for ${issue.identifier}; blocking conservatively`,
17165
+ {
17166
+ issueId: issue.id,
17167
+ error: msg
17168
+ }
17169
+ );
17170
+ return { ok: false, reason: `gate error: ${msg}` };
17171
+ }
17172
+ }
16942
17173
  /**
16943
17174
  * AMR 4c (ADR 0069): the sound single-agent quality-verdict feeder. On a normal
16944
17175
  * exit, when AMR is active, run a BASELINE-RELATIVE security scan of the lines
@@ -16990,13 +17221,82 @@ ${messages}`);
16990
17221
  async deriveAcceptanceEvalVerdict(issue, workspacePath) {
16991
17222
  const acceptanceEval = this.config.agent.routing?.policy?.acceptanceEval;
16992
17223
  if (acceptanceEval?.enabled !== true || issue.spec === null) return void 0;
16993
- const provider = this.resolveComplexityProvider();
17224
+ return this.evaluateOutcomeCore(issue, workspacePath, acceptanceEval.model, "amr");
17225
+ }
17226
+ /**
17227
+ * local-backend-full-workflow Phase 2 (Option C, SC4): the SHARED outcome-eval
17228
+ * core, lifted out of `deriveAcceptanceEvalVerdict` so BOTH callers reuse the
17229
+ * same `OutcomeEvaluator` engine over the introduced diff vs the spec's
17230
+ * judgment section. It does NOT gate on `adaptiveRouter !== null` or
17231
+ * `acceptanceEval.enabled` — those guards live in the AMR caller above; the
17232
+ * LOCAL gate (D2) always evaluates when a spec is present. Maps a
17233
+ * high-confidence NOT_SATISFIED to `'quality-fail'`; conservative + fully
17234
+ * guarded: no spec / no provider / empty diff / any error → `undefined`.
17235
+ */
17236
+ /**
17237
+ * local-backend-full-workflow Phase 3 (D5/SC6): resolve the AnalysisProvider
17238
+ * for the outcome-eval gate. Caller-gated: ONLY the LOCAL gate caller consults
17239
+ * `agent.routing.workflowGates`. The AMR caller ALWAYS uses local SEL
17240
+ * (`resolveComplexityProvider`) so the AMR acceptance-eval path is byte-identical
17241
+ * (SC-neutral). When `workflowGates === 'primary'` on the local caller, resolve
17242
+ * from the primary (routing.default) backend; any miss degrades to local SEL
17243
+ * (fail-open — an unreachable stronger provider must NOT wedge the local gate;
17244
+ * see the fail-open note at the runLocalWorkflowGate call site).
17245
+ */
17246
+ resolveOutcomeEvalProvider(caller) {
17247
+ if (caller === "local" && this.config.agent.routing?.workflowGates === "primary") {
17248
+ return this.resolvePrimaryOutcomeEvalProvider() ?? this.resolveComplexityProvider();
17249
+ }
17250
+ return this.resolveComplexityProvider();
17251
+ }
17252
+ /**
17253
+ * Build an AnalysisProvider from the PRIMARY (routing.default) backend for the
17254
+ * Phase-3 `workflowGates:'primary'` seam. Reuses the shipped
17255
+ * `buildAnalysisProvider` translator but forces the router to the default
17256
+ * backend. Returns undefined (→ caller degrades to local SEL) when intelligence
17257
+ * is disabled, the factory is absent, or the default backend cannot produce a
17258
+ * provider — fully guarded, never throws.
17259
+ */
17260
+ resolvePrimaryOutcomeEvalProvider() {
17261
+ try {
17262
+ if (!this.config.intelligence?.enabled || !this.backendFactory) return void 0;
17263
+ const backends = this.config.agent.backends;
17264
+ const def0 = this.config.agent.routing?.default;
17265
+ const defaultName = Array.isArray(def0) ? def0[0] : def0;
17266
+ if (defaultName === void 0) return void 0;
17267
+ const def = backends?.[defaultName];
17268
+ if (!def) return void 0;
17269
+ return buildAnalysisProvider({
17270
+ def,
17271
+ backendName: defaultName,
17272
+ layer: "sel",
17273
+ getResolverStatusSnapshot: () => {
17274
+ const resolver = this.localResolvers.get(defaultName);
17275
+ if (!resolver) return null;
17276
+ const s = resolver.getStatus();
17277
+ return {
17278
+ available: s.available,
17279
+ resolved: s.resolved,
17280
+ configured: s.configured,
17281
+ detected: s.detected
17282
+ };
17283
+ },
17284
+ intelligence: this.config.intelligence,
17285
+ logger: this.logger
17286
+ }) ?? void 0;
17287
+ } catch {
17288
+ return void 0;
17289
+ }
17290
+ }
17291
+ async evaluateOutcomeCore(issue, workspacePath, model, caller) {
17292
+ if (issue.spec === null) return void 0;
17293
+ const provider = this.resolveOutcomeEvalProvider(caller);
16994
17294
  if (provider === void 0) return void 0;
16995
17295
  try {
16996
17296
  const diff = await this.workspace.getIntroducedDiffText(issue.identifier);
16997
17297
  if (diff.trim() === "") return void 0;
16998
17298
  const evaluator = new import_intelligence12.OutcomeEvaluator(provider, this.graphStore ?? new import_graph2.GraphStore(), {
16999
- ...acceptanceEval.model !== void 0 ? { model: acceptanceEval.model } : {}
17299
+ ...model !== void 0 ? { model } : {}
17000
17300
  });
17001
17301
  const verdict = await evaluator.evaluate({
17002
17302
  specPath: path21.join(workspacePath, issue.spec),
@@ -17008,14 +17308,17 @@ ${messages}`);
17008
17308
  });
17009
17309
  const cls = outcomeVerdictToQualityFail(verdict);
17010
17310
  if (cls === "quality-fail") {
17011
- this.logger.info("amr:quality-fail \u2014 acceptance-eval NOT_SATISFIED (high confidence)", {
17012
- issueId: issue.id,
17013
- rationale: verdict.rationale
17014
- });
17311
+ this.logger.info(
17312
+ `${caller}:quality-fail \u2014 acceptance-eval NOT_SATISFIED (high confidence)`,
17313
+ {
17314
+ issueId: issue.id,
17315
+ rationale: verdict.rationale
17316
+ }
17317
+ );
17015
17318
  }
17016
17319
  return cls;
17017
17320
  } catch (err) {
17018
- this.logger.debug("amr acceptance-eval skipped (best-effort)", {
17321
+ this.logger.debug(`${caller} acceptance-eval skipped (best-effort)`, {
17019
17322
  issueId: issue.id,
17020
17323
  error: err instanceof Error ? err.message : String(err)
17021
17324
  });
@@ -17582,6 +17885,15 @@ ${messages}`);
17582
17885
  }
17583
17886
  this.seedRecommender(candidates, "frozen");
17584
17887
  const recommend = (hardware) => this.modelRecommender(hardware);
17888
+ let localEndpoint;
17889
+ let localApiKey;
17890
+ for (const def of Object.values(this.config.agent.backends ?? {})) {
17891
+ if ((def.type === "local" || def.type === "pi") && typeof def.endpoint === "string") {
17892
+ localEndpoint = def.endpoint;
17893
+ localApiKey = def.apiKey;
17894
+ break;
17895
+ }
17896
+ }
17585
17897
  this.refreshScheduler = new import_local_models6.RefreshScheduler({
17586
17898
  runTick: () => (0, import_local_models6.runRefreshTick)({
17587
17899
  detectHardware: () => this.detectLmlmHardware(),
@@ -17599,7 +17911,14 @@ ${messages}`);
17599
17911
  target: c.target.ollamaName
17600
17912
  });
17601
17913
  }),
17602
- proposalThreshold: refreshCfg?.proposalThreshold ?? 5
17914
+ proposalThreshold: refreshCfg?.proposalThreshold ?? 5,
17915
+ ...localEndpoint !== void 0 ? {
17916
+ probeToolCalling: (ollamaName) => (0, import_local_models6.probeToolCalling)({
17917
+ model: ollamaName,
17918
+ baseUrl: localEndpoint,
17919
+ ...localApiKey !== void 0 ? { apiKey: localApiKey } : {}
17920
+ })
17921
+ } : {}
17603
17922
  }).then((result) => {
17604
17923
  void this.drainDeferredEvictions();
17605
17924
  return result;
@@ -18306,7 +18625,8 @@ async function triageIssue2(issue, deps = {}) {
18306
18625
  ...graph ? { graph } : {},
18307
18626
  ...deps.precedent ? { precedent: deps.precedent } : {},
18308
18627
  ...deps.config ? { config: deps.config } : {},
18309
- ...deps.models ? { models: deps.models } : {}
18628
+ ...deps.models ? { models: deps.models } : {},
18629
+ ...deps.modelDeferred ? { modelDeferred: true } : {}
18310
18630
  });
18311
18631
  }
18312
18632
 
@@ -18333,7 +18653,7 @@ var ForkStepSchema = import_zod18.z.object({
18333
18653
  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.';
18334
18654
  function makeSelForkGenerator(provider, input, opts = {}) {
18335
18655
  const samples = Math.max(2, opts.samples ?? 3);
18336
- const maxTokens = opts.maxTokens ?? 512;
18656
+ const maxTokens = opts.maxTokens ?? 4096;
18337
18657
  return {
18338
18658
  async next(index, priorDecisions) {
18339
18659
  const prompt = buildForkPrompt(input, priorDecisions, index);
@@ -19230,6 +19550,7 @@ function buildArchiveHooks(opts) {
19230
19550
 
19231
19551
  // src/index.ts
19232
19552
  var import_local_models7 = require("@harness-engineering/local-models");
19553
+ var import_local_models8 = require("@harness-engineering/local-models");
19233
19554
  // Annotate the CommonJS export names for ESM import in node:
19234
19555
  0 && (module.exports = {
19235
19556
  AdaptiveRouter,
@@ -19240,6 +19561,7 @@ var import_local_models7 = require("@harness-engineering/local-models");
19240
19561
  BackendRouter,
19241
19562
  CheckScriptRunner,
19242
19563
  ClaimManager,
19564
+ DEFAULT_POOL_STATE_PATH,
19243
19565
  EscalationState,
19244
19566
  GateNotReadyError,
19245
19567
  GateRunError,
@@ -19256,6 +19578,7 @@ var import_local_models7 = require("@harness-engineering/local-models");
19256
19578
  Orchestrator,
19257
19579
  OrchestratorBackendFactory,
19258
19580
  PRDetector,
19581
+ PoolStateStore,
19259
19582
  PrivacyNoMatch,
19260
19583
  PromotionError,
19261
19584
  PromptRenderer,
@@ -19291,6 +19614,7 @@ var import_local_models7 = require("@harness-engineering/local-models");
19291
19614
  createEmptyState,
19292
19615
  crossFieldRoutingIssues,
19293
19616
  defaultFetchModels,
19617
+ defaultLocalVerifyRunner,
19294
19618
  defaultPoolCapabilities,
19295
19619
  deriveSeedPaths,
19296
19620
  detectScopeTier,
@@ -19324,6 +19648,7 @@ var import_local_models7 = require("@harness-engineering/local-models");
19324
19648
  normalizeLocalModel,
19325
19649
  openSearchIndex,
19326
19650
  pilotScore,
19651
+ poolStateToCandidates,
19327
19652
  precedentLookupFromStored,
19328
19653
  promote,
19329
19654
  rankTriageCandidates,
@@ -19352,6 +19677,7 @@ var import_local_models7 = require("@harness-engineering/local-models");
19352
19677
  syncMain,
19353
19678
  triageIssue,
19354
19679
  truncateForBudget,
19680
+ truncateGateOutput,
19355
19681
  validateCustomTasks,
19356
19682
  validateWorkflowConfig,
19357
19683
  wireNotificationSinks,