@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.d.mts +153 -3
- package/dist/index.d.ts +153 -3
- package/dist/index.js +351 -25
- package/dist/index.mjs +354 -28
- package/package.json +6 -6
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:
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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(
|
|
16972
|
-
|
|
16973
|
-
|
|
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(
|
|
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 ??
|
|
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.
|
|
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.
|
|
52
|
-
"@harness-engineering/graph": "0.11.
|
|
53
|
-
"@harness-engineering/intelligence": "0.
|
|
54
|
-
"@harness-engineering/local-models": "0.
|
|
55
|
-
"@harness-engineering/types": "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",
|