@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.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.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:
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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(
|
|
17012
|
-
|
|
17013
|
-
|
|
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(
|
|
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 ??
|
|
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,
|