@harness-engineering/orchestrator 0.14.0 → 0.15.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 +252 -22
- package/dist/index.d.ts +252 -22
- package/dist/index.js +699 -105
- package/dist/index.mjs +664 -70
- package/package.json +6 -6
package/dist/index.mjs
CHANGED
|
@@ -2013,6 +2013,31 @@ var RoutingConfigSchema = z.object({
|
|
|
2013
2013
|
// when absent). Previously accepted by the runtime PUT endpoint only. ---
|
|
2014
2014
|
policy: RoutingPolicySchema.optional()
|
|
2015
2015
|
}).strict();
|
|
2016
|
+
var CONFIDENCE = z.enum(["low", "medium", "high"]);
|
|
2017
|
+
var V1_MAX_RATCHET_STAGE = 2;
|
|
2018
|
+
var RATCHET_STAGE = z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]).refine((s) => s <= V1_MAX_RATCHET_STAGE, {
|
|
2019
|
+
message: `ratchetStage ${">"} ${V1_MAX_RATCHET_STAGE} is deferred post-v1 (stages 3\u20134 not yet supported); use 1 or 2`
|
|
2020
|
+
});
|
|
2021
|
+
var RoadmapAutoTriageSchema = z.object({
|
|
2022
|
+
enabled: z.boolean(),
|
|
2023
|
+
schedule: z.string().min(1).optional(),
|
|
2024
|
+
ratchetStage: RATCHET_STAGE,
|
|
2025
|
+
thresholds: z.object({
|
|
2026
|
+
dispatchConfidence: CONFIDENCE,
|
|
2027
|
+
boundedScopeMax: z.number(),
|
|
2028
|
+
brainstormConfidence: z.number(),
|
|
2029
|
+
exceededByBands: z.number(),
|
|
2030
|
+
ratchetAdvanceRate: z.number(),
|
|
2031
|
+
ratchetMinSample: z.number()
|
|
2032
|
+
}).strict(),
|
|
2033
|
+
depthBudget: z.object({
|
|
2034
|
+
trivial: z.number(),
|
|
2035
|
+
simple: z.number()
|
|
2036
|
+
}).strict()
|
|
2037
|
+
}).strict();
|
|
2038
|
+
var RoadmapConfigSchema = z.object({
|
|
2039
|
+
autoTriage: RoadmapAutoTriageSchema.optional()
|
|
2040
|
+
}).strict();
|
|
2016
2041
|
var WorkflowStepSchema = z.object({
|
|
2017
2042
|
skill: z.string().min(1),
|
|
2018
2043
|
produces: z.string().min(1),
|
|
@@ -2050,12 +2075,12 @@ var BackendsMapSchema = z2.record(z2.string(), BackendDefSchema);
|
|
|
2050
2075
|
function crossFieldRoutingIssues(backends, routing) {
|
|
2051
2076
|
const issues = [];
|
|
2052
2077
|
const names = new Set(Object.keys(backends));
|
|
2053
|
-
const checkRef = (
|
|
2078
|
+
const checkRef = (path25, value) => {
|
|
2054
2079
|
if (value === void 0) return;
|
|
2055
2080
|
const entries = Array.isArray(value) ? value : [value];
|
|
2056
2081
|
entries.forEach((name, idx) => {
|
|
2057
2082
|
if (names.has(name)) return;
|
|
2058
|
-
const pathWithIdx = Array.isArray(value) ? [...
|
|
2083
|
+
const pathWithIdx = Array.isArray(value) ? [...path25, String(idx)] : path25;
|
|
2059
2084
|
issues.push({
|
|
2060
2085
|
path: pathWithIdx,
|
|
2061
2086
|
message: `routing.${pathWithIdx.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
|
|
@@ -2152,6 +2177,10 @@ function validateWorkflowConfig(config, options = {}) {
|
|
|
2152
2177
|
const parsed = z2.array(StagedWorkflowDeclSchema).safeParse(c.workflows);
|
|
2153
2178
|
if (!parsed.success) return Err(new Error(`workflows: ${parsed.error.message}`));
|
|
2154
2179
|
}
|
|
2180
|
+
if (c.roadmap !== void 0) {
|
|
2181
|
+
const parsed = RoadmapConfigSchema.safeParse(c.roadmap);
|
|
2182
|
+
if (!parsed.success) return Err(new Error(`roadmap: ${parsed.error.message}`));
|
|
2183
|
+
}
|
|
2155
2184
|
return Ok2({ config, warnings });
|
|
2156
2185
|
}
|
|
2157
2186
|
function getDefaultConfig() {
|
|
@@ -3406,6 +3435,68 @@ import { randomUUID as randomUUID5 } from "crypto";
|
|
|
3406
3435
|
import { RoutingError as RoutingError3 } from "@harness-engineering/types";
|
|
3407
3436
|
import { writeTaint, SecurityScanner as SecurityScanner2 } from "@harness-engineering/core";
|
|
3408
3437
|
import { OutcomeEvaluator } from "@harness-engineering/intelligence";
|
|
3438
|
+
|
|
3439
|
+
// src/agent/triage-outcome.ts
|
|
3440
|
+
import {
|
|
3441
|
+
classify,
|
|
3442
|
+
compareToPrediction,
|
|
3443
|
+
aggregatePrecedent
|
|
3444
|
+
} from "@harness-engineering/intelligence";
|
|
3445
|
+
function signalsFromDiff(hunks, textOnly) {
|
|
3446
|
+
const files = /* @__PURE__ */ new Set();
|
|
3447
|
+
const layers = /* @__PURE__ */ new Set();
|
|
3448
|
+
let addedLines = 0;
|
|
3449
|
+
for (const h of hunks) {
|
|
3450
|
+
files.add(h.file);
|
|
3451
|
+
layers.add(layerOfPath(h.file));
|
|
3452
|
+
addedLines += h.addedContent === "" ? 0 : h.addedContent.split("\n").length;
|
|
3453
|
+
}
|
|
3454
|
+
return {
|
|
3455
|
+
...textOnly,
|
|
3456
|
+
filesTouched: files.size,
|
|
3457
|
+
layersTouched: layers.size,
|
|
3458
|
+
blastRadius: addedLines
|
|
3459
|
+
};
|
|
3460
|
+
}
|
|
3461
|
+
function layerOfPath(file) {
|
|
3462
|
+
const parts = file.split("/").filter((p) => p.length > 0);
|
|
3463
|
+
if (parts.length === 0) return "<root>";
|
|
3464
|
+
if (parts[0] === "packages" && parts.length >= 4 && parts[2] === "src") {
|
|
3465
|
+
return `${parts[1] ?? ""}/${parts[3] ?? ""}`;
|
|
3466
|
+
}
|
|
3467
|
+
return parts.length === 1 ? "<root>" : parts[0] ?? "<root>";
|
|
3468
|
+
}
|
|
3469
|
+
async function runRetrospective(deps, provider) {
|
|
3470
|
+
const signals = signalsFromDiff(deps.hunks, deps.textOnly);
|
|
3471
|
+
const actual = await classify(
|
|
3472
|
+
{ signals, phase: "post-diff", riskHigh: deps.riskHigh, prompt: deps.prompt },
|
|
3473
|
+
provider
|
|
3474
|
+
);
|
|
3475
|
+
const comparison = deps.comparatorConfig ? compareToPrediction(deps.prediction, actual, deps.comparatorConfig) : compareToPrediction(deps.prediction, actual);
|
|
3476
|
+
return { actual, comparison };
|
|
3477
|
+
}
|
|
3478
|
+
function buildTriageOutcomeInput(externalId, shapeKey, result) {
|
|
3479
|
+
return {
|
|
3480
|
+
externalId,
|
|
3481
|
+
shapeKey,
|
|
3482
|
+
actual: result.actual,
|
|
3483
|
+
exceededBy: result.comparison.exceededBy,
|
|
3484
|
+
matched: result.comparison.matched
|
|
3485
|
+
};
|
|
3486
|
+
}
|
|
3487
|
+
function precedentLookupFromStored(records) {
|
|
3488
|
+
const bridged = records.map((r) => ({
|
|
3489
|
+
externalId: "",
|
|
3490
|
+
shapeKey: r.shapeKey,
|
|
3491
|
+
ts: "",
|
|
3492
|
+
...r.outcome ? { outcome: { actual: void 0, exceededBy: 0, matched: r.outcome.matched } } : {}
|
|
3493
|
+
}));
|
|
3494
|
+
return {
|
|
3495
|
+
rateForShape: (shapeKey) => aggregatePrecedent(bridged, shapeKey)
|
|
3496
|
+
};
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3499
|
+
// src/orchestrator.ts
|
|
3409
3500
|
import { GraphStore as GraphStore2 } from "@harness-engineering/graph";
|
|
3410
3501
|
|
|
3411
3502
|
// src/core/stall-detector.ts
|
|
@@ -3989,7 +4080,8 @@ var CompletionHandler = class {
|
|
|
3989
4080
|
import {
|
|
3990
4081
|
GitHubIssuesSyncAdapter as GitHubIssuesSyncAdapter3,
|
|
3991
4082
|
loadTrackerSyncConfig as loadTrackerSyncConfig3,
|
|
3992
|
-
createTrackerClient as createTrackerClient2
|
|
4083
|
+
createTrackerClient as createTrackerClient2,
|
|
4084
|
+
eventSourcing as eventSourcing2
|
|
3993
4085
|
} from "@harness-engineering/core";
|
|
3994
4086
|
|
|
3995
4087
|
// src/tracker/adapters/github-issues-issue-tracker.ts
|
|
@@ -4701,11 +4793,11 @@ function detectLegacyFields(agent) {
|
|
|
4701
4793
|
}
|
|
4702
4794
|
function buildCase1Warnings(presentLegacy, suppressLocalGroup) {
|
|
4703
4795
|
const warnings = [];
|
|
4704
|
-
for (const
|
|
4705
|
-
if (CASE1_ALWAYS_SUPPRESS.has(
|
|
4706
|
-
if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(
|
|
4796
|
+
for (const path25 of presentLegacy) {
|
|
4797
|
+
if (CASE1_ALWAYS_SUPPRESS.has(path25)) continue;
|
|
4798
|
+
if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path25)) continue;
|
|
4707
4799
|
warnings.push(
|
|
4708
|
-
`Ignoring legacy field '${
|
|
4800
|
+
`Ignoring legacy field '${path25}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
|
|
4709
4801
|
);
|
|
4710
4802
|
}
|
|
4711
4803
|
return warnings;
|
|
@@ -4733,7 +4825,7 @@ function migrateAgentConfig(agent) {
|
|
|
4733
4825
|
}
|
|
4734
4826
|
const { backends, routing } = synthesizeBackendsAndRouting(agent);
|
|
4735
4827
|
const warnings = presentLegacy.map(
|
|
4736
|
-
(
|
|
4828
|
+
(path25) => `Deprecated config field '${path25}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
|
|
4737
4829
|
);
|
|
4738
4830
|
return {
|
|
4739
4831
|
config: { ...agent, backends, routing },
|
|
@@ -4818,12 +4910,12 @@ var BackendRouter = class {
|
|
|
4818
4910
|
*/
|
|
4819
4911
|
resolve(useCase, opts) {
|
|
4820
4912
|
const startedAt = performance.now();
|
|
4821
|
-
const
|
|
4913
|
+
const path25 = [];
|
|
4822
4914
|
const tryChain = (source, value) => {
|
|
4823
4915
|
if (value === void 0) return void 0;
|
|
4824
4916
|
for (const name of toArray(value)) {
|
|
4825
4917
|
const step = { source, candidate: name, outcome: "considered" };
|
|
4826
|
-
|
|
4918
|
+
path25.push(step);
|
|
4827
4919
|
if (this.backends[name]) {
|
|
4828
4920
|
step.outcome = "chosen";
|
|
4829
4921
|
return name;
|
|
@@ -4842,7 +4934,7 @@ var BackendRouter = class {
|
|
|
4842
4934
|
return {
|
|
4843
4935
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4844
4936
|
useCase,
|
|
4845
|
-
resolutionPath:
|
|
4937
|
+
resolutionPath: path25,
|
|
4846
4938
|
backendName,
|
|
4847
4939
|
backendType: def.type,
|
|
4848
4940
|
durationMs: performance.now() - startedAt
|
|
@@ -4875,7 +4967,7 @@ var BackendRouter = class {
|
|
|
4875
4967
|
if (fromDefault) return emitAndReturn(decide(fromDefault));
|
|
4876
4968
|
const knownList = Object.keys(this.backends).join(", ") || "(none)";
|
|
4877
4969
|
throw new Error(
|
|
4878
|
-
`BackendRouter.resolve: routing.default produced no available backend for useCase=${JSON.stringify(useCase)}. Resolution path: ${JSON.stringify(
|
|
4970
|
+
`BackendRouter.resolve: routing.default produced no available backend for useCase=${JSON.stringify(useCase)}. Resolution path: ${JSON.stringify(path25)}. Known backends: [${knownList}].`
|
|
4879
4971
|
);
|
|
4880
4972
|
}
|
|
4881
4973
|
/**
|
|
@@ -4968,7 +5060,7 @@ var BackendRouter = class {
|
|
|
4968
5060
|
check(`modes.${mode}`, value);
|
|
4969
5061
|
}
|
|
4970
5062
|
if (missing.length > 0) {
|
|
4971
|
-
const detail = missing.map(({ path:
|
|
5063
|
+
const detail = missing.map(({ path: path25, name }) => `routing.${path25} -> '${name}'`).join("; ");
|
|
4972
5064
|
const known_ = [...known].join(", ") || "(none)";
|
|
4973
5065
|
throw new Error(
|
|
4974
5066
|
`BackendRouter: routing references unknown backend(s): ${detail}. Defined backends: [${known_}].`
|
|
@@ -8321,7 +8413,7 @@ function buildRoutingUseCase(issue, backendParam, catalog) {
|
|
|
8321
8413
|
|
|
8322
8414
|
// src/agent/live-classify.ts
|
|
8323
8415
|
import {
|
|
8324
|
-
classify
|
|
8416
|
+
classify as classify2
|
|
8325
8417
|
} from "@harness-engineering/intelligence";
|
|
8326
8418
|
var CONSERVATIVE = {
|
|
8327
8419
|
level: "moderate",
|
|
@@ -8345,7 +8437,7 @@ function makeLiveClassify(resolveProvider) {
|
|
|
8345
8437
|
riskHigh,
|
|
8346
8438
|
prompt: taskText.prompt
|
|
8347
8439
|
};
|
|
8348
|
-
return
|
|
8440
|
+
return classify2(input, resolveProvider());
|
|
8349
8441
|
};
|
|
8350
8442
|
}
|
|
8351
8443
|
|
|
@@ -10745,7 +10837,12 @@ function deriveTraceCost(body, decision, def, routing, backends) {
|
|
|
10745
10837
|
backends
|
|
10746
10838
|
);
|
|
10747
10839
|
const estCostUsd = estimateCost(costedDef, { useCase: body.useCase });
|
|
10748
|
-
return {
|
|
10840
|
+
return {
|
|
10841
|
+
tierRequired,
|
|
10842
|
+
estCostUsd,
|
|
10843
|
+
costedBackendName: costedName,
|
|
10844
|
+
costedBackendType: costedDef.type
|
|
10845
|
+
};
|
|
10749
10846
|
}
|
|
10750
10847
|
function selectCostedBackend(tierRequired, decision, def, routing, backends) {
|
|
10751
10848
|
const registry = buildCapabilityRegistry(backends);
|
|
@@ -10810,7 +10907,7 @@ async function handleTrace(req, res, deps) {
|
|
|
10810
10907
|
opts
|
|
10811
10908
|
);
|
|
10812
10909
|
if (r.data.complexity !== void 0 || r.data.risk !== void 0) {
|
|
10813
|
-
const { tierRequired, estCostUsd, costedBackendName } = deriveTraceCost(
|
|
10910
|
+
const { tierRequired, estCostUsd, costedBackendName, costedBackendType } = deriveTraceCost(
|
|
10814
10911
|
r.data,
|
|
10815
10912
|
decision,
|
|
10816
10913
|
def,
|
|
@@ -10822,9 +10919,12 @@ async function handleTrace(req, res, deps) {
|
|
|
10822
10919
|
def: { type: def.type },
|
|
10823
10920
|
tierRequired,
|
|
10824
10921
|
estCostUsd,
|
|
10825
|
-
//
|
|
10826
|
-
//
|
|
10827
|
-
|
|
10922
|
+
// The backend AMR would ACTUALLY dispatch to at this tier (cheapest
|
|
10923
|
+
// qualifying) + its type — distinct from the identity-chain `decision`
|
|
10924
|
+
// above. The CLI shows this as the effective backend so tier↔cost↔backend
|
|
10925
|
+
// read consistently (was implicit + divergent before).
|
|
10926
|
+
costedBackendName,
|
|
10927
|
+
costedBackendType
|
|
10828
10928
|
});
|
|
10829
10929
|
return true;
|
|
10830
10930
|
}
|
|
@@ -11386,8 +11486,8 @@ function parseToken(raw) {
|
|
|
11386
11486
|
return { id: raw.slice(0, dot), secret: raw.slice(dot + 1) };
|
|
11387
11487
|
}
|
|
11388
11488
|
var TokenStore = class {
|
|
11389
|
-
constructor(
|
|
11390
|
-
this.path =
|
|
11489
|
+
constructor(path25) {
|
|
11490
|
+
this.path = path25;
|
|
11391
11491
|
}
|
|
11392
11492
|
path;
|
|
11393
11493
|
cache = null;
|
|
@@ -11494,8 +11594,8 @@ import { appendFile, mkdir as mkdir8 } from "fs/promises";
|
|
|
11494
11594
|
import { dirname as dirname6 } from "path";
|
|
11495
11595
|
import { AuthAuditEntrySchema } from "@harness-engineering/types";
|
|
11496
11596
|
var AuditLogger = class {
|
|
11497
|
-
constructor(
|
|
11498
|
-
this.path =
|
|
11597
|
+
constructor(path25, opts = {}) {
|
|
11598
|
+
this.path = path25;
|
|
11499
11599
|
this.opts = opts;
|
|
11500
11600
|
}
|
|
11501
11601
|
path;
|
|
@@ -11733,9 +11833,9 @@ var V1_BRIDGE_ROUTES = [
|
|
|
11733
11833
|
function isV1Bridge(method, url) {
|
|
11734
11834
|
return V1_BRIDGE_ROUTES.some((r) => r.method === method && r.pattern.test(url));
|
|
11735
11835
|
}
|
|
11736
|
-
function requiredBridgeScope(method,
|
|
11836
|
+
function requiredBridgeScope(method, path25) {
|
|
11737
11837
|
for (const r of V1_BRIDGE_ROUTES) {
|
|
11738
|
-
if (r.method === method && r.pattern.test(
|
|
11838
|
+
if (r.method === method && r.pattern.test(path25)) return r.scope;
|
|
11739
11839
|
}
|
|
11740
11840
|
return null;
|
|
11741
11841
|
}
|
|
@@ -11745,11 +11845,11 @@ function hasScope(held, required) {
|
|
|
11745
11845
|
if (held.includes("admin")) return true;
|
|
11746
11846
|
return held.includes(required);
|
|
11747
11847
|
}
|
|
11748
|
-
function exactScopeForRoute(method,
|
|
11749
|
-
if (
|
|
11750
|
-
if (
|
|
11751
|
-
if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(
|
|
11752
|
-
if ((
|
|
11848
|
+
function exactScopeForRoute(method, path25) {
|
|
11849
|
+
if (path25 === "/api/v1/auth/token" && method === "POST") return "admin";
|
|
11850
|
+
if (path25 === "/api/v1/auth/tokens" && method === "GET") return "admin";
|
|
11851
|
+
if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path25) && method === "DELETE") return "admin";
|
|
11852
|
+
if ((path25 === "/api/state" || path25 === "/api/v1/state") && method === "GET") return "read-status";
|
|
11753
11853
|
return null;
|
|
11754
11854
|
}
|
|
11755
11855
|
var PREFIX_SCOPES = [
|
|
@@ -11766,19 +11866,19 @@ var PREFIX_SCOPES = [
|
|
|
11766
11866
|
["/api/sessions", "read-status"],
|
|
11767
11867
|
["/api/chat-proxy", "trigger-job"]
|
|
11768
11868
|
];
|
|
11769
|
-
function prefixScopeForPath(
|
|
11770
|
-
if (
|
|
11869
|
+
function prefixScopeForPath(path25) {
|
|
11870
|
+
if (path25 === "/api/chat") return "trigger-job";
|
|
11771
11871
|
for (const [prefix, scope] of PREFIX_SCOPES) {
|
|
11772
|
-
if (
|
|
11872
|
+
if (path25.startsWith(prefix)) return scope;
|
|
11773
11873
|
}
|
|
11774
11874
|
return null;
|
|
11775
11875
|
}
|
|
11776
|
-
function requiredScopeForRoute(method,
|
|
11777
|
-
const bridgeScope = requiredBridgeScope(method,
|
|
11876
|
+
function requiredScopeForRoute(method, path25) {
|
|
11877
|
+
const bridgeScope = requiredBridgeScope(method, path25);
|
|
11778
11878
|
if (bridgeScope) return bridgeScope;
|
|
11779
|
-
const exactScope = exactScopeForRoute(method,
|
|
11879
|
+
const exactScope = exactScopeForRoute(method, path25);
|
|
11780
11880
|
if (exactScope) return exactScope;
|
|
11781
|
-
return prefixScopeForPath(
|
|
11881
|
+
return prefixScopeForPath(path25);
|
|
11782
11882
|
}
|
|
11783
11883
|
|
|
11784
11884
|
// src/server/http.ts
|
|
@@ -12307,8 +12407,8 @@ function genSecret2() {
|
|
|
12307
12407
|
return randomBytes3(32).toString("base64url");
|
|
12308
12408
|
}
|
|
12309
12409
|
var WebhookStore = class {
|
|
12310
|
-
constructor(
|
|
12311
|
-
this.path =
|
|
12410
|
+
constructor(path25) {
|
|
12411
|
+
this.path = path25;
|
|
12312
12412
|
}
|
|
12313
12413
|
path;
|
|
12314
12414
|
cache = null;
|
|
@@ -15161,8 +15261,8 @@ function validateCheckShape(prefix, task, errors) {
|
|
|
15161
15261
|
});
|
|
15162
15262
|
}
|
|
15163
15263
|
if (hasScript) {
|
|
15164
|
-
const
|
|
15165
|
-
if (!
|
|
15264
|
+
const path25 = task.checkScript?.path;
|
|
15265
|
+
if (!path25 || path25.trim().length === 0) {
|
|
15166
15266
|
errors.push({ path: `${prefix}.checkScript.path`, message: "checkScript.path is required" });
|
|
15167
15267
|
}
|
|
15168
15268
|
if (task.checkScript?.timeoutMs !== void 0 && task.checkScript.timeoutMs <= 0) {
|
|
@@ -15288,9 +15388,9 @@ function handleEdge(top, next, color, stack, errors, reported) {
|
|
|
15288
15388
|
stack.push({ id: next, nextIdx: 0, path: [...top.path, next] });
|
|
15289
15389
|
}
|
|
15290
15390
|
}
|
|
15291
|
-
function reportCycle(
|
|
15292
|
-
const cycleStart =
|
|
15293
|
-
const cyclePath = cycleStart >= 0 ? [...
|
|
15391
|
+
function reportCycle(path25, next, errors, reported) {
|
|
15392
|
+
const cycleStart = path25.indexOf(next);
|
|
15393
|
+
const cyclePath = cycleStart >= 0 ? [...path25.slice(cycleStart), next] : [...path25, next];
|
|
15294
15394
|
const key = cyclePath.join("\u2192");
|
|
15295
15395
|
if (reported.has(key)) return;
|
|
15296
15396
|
reported.add(key);
|
|
@@ -16778,7 +16878,9 @@ ${messages}`);
|
|
|
16778
16878
|
await this.emitWorkerExit(issue.id, "error", attempt, "Stopped by reconciliation");
|
|
16779
16879
|
}
|
|
16780
16880
|
} else {
|
|
16781
|
-
const
|
|
16881
|
+
const qualityClass = await this.deriveSingleAgentQualityVerdict(issue, workspacePath);
|
|
16882
|
+
const retroClass = await this.deriveRoutingRetrospectiveVerdict(issue, workspacePath);
|
|
16883
|
+
const outcomeClass = qualityClass ?? retroClass;
|
|
16782
16884
|
await this.emitWorkerExit(issue.id, "normal", attempt, void 0, outcomeClass);
|
|
16783
16885
|
}
|
|
16784
16886
|
} catch (error) {
|
|
@@ -16880,6 +16982,150 @@ ${messages}`);
|
|
|
16880
16982
|
return void 0;
|
|
16881
16983
|
}
|
|
16882
16984
|
}
|
|
16985
|
+
/**
|
|
16986
|
+
* Roadmap Auto-Triage Phase 4 (SC1–SC3, SC5, SC7): the post-diff routing
|
|
16987
|
+
* retrospective — a SIBLING quality-verdict source to the 4c feeder above,
|
|
16988
|
+
* extending (not replacing) the proven escalation path. On a normal exit, when AMR
|
|
16989
|
+
* is live AND auto-triage is enabled AND this unit carries a stored pre-dispatch
|
|
16990
|
+
* prediction, it classifies the ACTUAL introduced diff at full strength
|
|
16991
|
+
* (phase:'post-diff') and grades it against that prediction:
|
|
16992
|
+
* - MATCH → records the graded outcome + annotates the PR "AI autonomous —
|
|
16993
|
+
* verify" (stage-2 human-verify handling, SC4) → returns `undefined`
|
|
16994
|
+
* (neutral: a match is not an escalation).
|
|
16995
|
+
* - MISMATCH (diff exceeded prediction, or a missing/garbled prediction, or ANY
|
|
16996
|
+
* error) → records the outcome (when a shape is known) + returns
|
|
16997
|
+
* `'quality-fail'`, which climbs the coherence unit's escalation floor
|
|
16998
|
+
* via `recordAmrOutcome`; exhaustion queues `needs-human` (SC3/SC7).
|
|
16999
|
+
*
|
|
17000
|
+
* Fail-safe & guarded (SC7): an error never a silent pass — it takes the MISMATCH
|
|
17001
|
+
* (block+escalate) path. No-op (byte-identical) when AMR is off, auto-triage is
|
|
17002
|
+
* off, or the unit was not dispatched through triage (no stored prediction) — the
|
|
17003
|
+
* last means an ordinary non-triaged run is never graded.
|
|
17004
|
+
*/
|
|
17005
|
+
async deriveRoutingRetrospectiveVerdict(issue, _workspacePath) {
|
|
17006
|
+
if (this.adaptiveRouter === null) return void 0;
|
|
17007
|
+
if (this.config.roadmap?.autoTriage?.enabled !== true) return void 0;
|
|
17008
|
+
const externalId = issue.externalId;
|
|
17009
|
+
if (externalId === null || externalId === "") return void 0;
|
|
17010
|
+
let record;
|
|
17011
|
+
try {
|
|
17012
|
+
const loaded = await eventSourcing2.loadTriageRecords(this.projectRoot);
|
|
17013
|
+
if (!loaded.ok) throw loaded.error;
|
|
17014
|
+
record = loaded.value.find((r) => r.externalId === externalId);
|
|
17015
|
+
} catch (err) {
|
|
17016
|
+
this.logger.debug("amr retrospective skipped (record load failed)", {
|
|
17017
|
+
issueId: issue.id,
|
|
17018
|
+
error: err instanceof Error ? err.message : String(err)
|
|
17019
|
+
});
|
|
17020
|
+
if (issue.spec !== null && issue.spec !== void 0 && issue.spec !== "") {
|
|
17021
|
+
this.logger.info(
|
|
17022
|
+
"amr:quality-fail \u2014 spec-bearing (triaged) unit but the triage store is unreadable (fail-safe block: a triaged unit whose prediction we cannot read never silently passes)",
|
|
17023
|
+
{ issueId: issue.id, externalId }
|
|
17024
|
+
);
|
|
17025
|
+
return "quality-fail";
|
|
17026
|
+
}
|
|
17027
|
+
return void 0;
|
|
17028
|
+
}
|
|
17029
|
+
if (record === void 0) return void 0;
|
|
17030
|
+
const stored = record.prediction;
|
|
17031
|
+
if (stored === void 0) {
|
|
17032
|
+
this.logger.info(
|
|
17033
|
+
"amr:quality-fail \u2014 triaged unit has no stored prediction (fail-safe block)",
|
|
17034
|
+
{
|
|
17035
|
+
issueId: issue.id,
|
|
17036
|
+
externalId
|
|
17037
|
+
}
|
|
17038
|
+
);
|
|
17039
|
+
return "quality-fail";
|
|
17040
|
+
}
|
|
17041
|
+
const prediction = {
|
|
17042
|
+
verdict: stored.verdict,
|
|
17043
|
+
levers: stored.levers,
|
|
17044
|
+
scopeEstimate: stored.scopeEstimate,
|
|
17045
|
+
ratchetStage: stored.ratchetStage
|
|
17046
|
+
};
|
|
17047
|
+
try {
|
|
17048
|
+
const hunks = await this.workspace.getIntroducedDiff(issue.identifier);
|
|
17049
|
+
const taskText = buildTaskText(issue);
|
|
17050
|
+
const exceededByBands = this.config.roadmap?.autoTriage?.thresholds?.exceededByBands ?? 1;
|
|
17051
|
+
const result = await runRetrospective(
|
|
17052
|
+
{
|
|
17053
|
+
hunks,
|
|
17054
|
+
textOnly: {
|
|
17055
|
+
descriptionLength: taskText.descriptionLength,
|
|
17056
|
+
specExists: taskText.specExists,
|
|
17057
|
+
acceptanceMeasurable: taskText.acceptanceMeasurable
|
|
17058
|
+
},
|
|
17059
|
+
prediction,
|
|
17060
|
+
riskHigh: false,
|
|
17061
|
+
prompt: taskText.prompt,
|
|
17062
|
+
comparatorConfig: { exceededByBands }
|
|
17063
|
+
},
|
|
17064
|
+
this.resolveComplexityProvider()
|
|
17065
|
+
);
|
|
17066
|
+
const outcomeInput = buildTriageOutcomeInput(externalId, record.shapeKey, result);
|
|
17067
|
+
const recorded = await eventSourcing2.recordTriageOutcome(this.projectRoot, outcomeInput);
|
|
17068
|
+
if (!recorded.ok) {
|
|
17069
|
+
this.logger.warn("amr retrospective outcome record failed (best-effort)", {
|
|
17070
|
+
issueId: issue.id,
|
|
17071
|
+
error: recorded.error.message
|
|
17072
|
+
});
|
|
17073
|
+
}
|
|
17074
|
+
if (result.comparison.action === "block-escalate") {
|
|
17075
|
+
this.logger.info("amr:quality-fail \u2014 post-diff retrospective mispredict (block+escalate)", {
|
|
17076
|
+
issueId: issue.id,
|
|
17077
|
+
externalId,
|
|
17078
|
+
predicted: prediction.verdict.level,
|
|
17079
|
+
actual: result.actual.level,
|
|
17080
|
+
exceededBy: result.comparison.exceededBy
|
|
17081
|
+
});
|
|
17082
|
+
return "quality-fail";
|
|
17083
|
+
}
|
|
17084
|
+
await this.annotateRetrospectiveMatch(issue, externalId, result.actual.level);
|
|
17085
|
+
return void 0;
|
|
17086
|
+
} catch (err) {
|
|
17087
|
+
this.logger.info("amr:quality-fail \u2014 post-diff retrospective errored (fail-safe block)", {
|
|
17088
|
+
issueId: issue.id,
|
|
17089
|
+
externalId,
|
|
17090
|
+
error: err instanceof Error ? err.message : String(err)
|
|
17091
|
+
});
|
|
17092
|
+
return "quality-fail";
|
|
17093
|
+
}
|
|
17094
|
+
}
|
|
17095
|
+
/**
|
|
17096
|
+
* v1 stage-2 match handling (SC4): annotate the unit's PR/issue with a "verify this
|
|
17097
|
+
* autonomous change" note so a human reviews every autonomous PR. Best-effort — a
|
|
17098
|
+
* missing tracker config / token / a failed comment never breaks completion (the
|
|
17099
|
+
* grade already recorded above). Mirrors `postLifecycleComment`'s adapter wiring.
|
|
17100
|
+
*/
|
|
17101
|
+
async annotateRetrospectiveMatch(issue, externalId, actualLevel) {
|
|
17102
|
+
try {
|
|
17103
|
+
const trackerConfig = loadTrackerSyncConfig3(this.projectRoot);
|
|
17104
|
+
if (!trackerConfig) return;
|
|
17105
|
+
const token = process.env.GITHUB_TOKEN;
|
|
17106
|
+
if (!token) return;
|
|
17107
|
+
const orchestratorId = await this.orchestratorIdPromise;
|
|
17108
|
+
const adapter = new GitHubIssuesSyncAdapter3({ token, config: trackerConfig });
|
|
17109
|
+
const body = [
|
|
17110
|
+
`**AI autonomous change \u2014 please verify** \`${orchestratorId}\``,
|
|
17111
|
+
"",
|
|
17112
|
+
"The post-diff retrospective classified this change WITHIN its pre-dispatch",
|
|
17113
|
+
`prediction (actual complexity: \`${actualLevel}\`). Under autonomy ratchet`,
|
|
17114
|
+
"stage 2, a human must verify every autonomous PR before it merges."
|
|
17115
|
+
].join("\n");
|
|
17116
|
+
const result = await adapter.addComment(externalId, body);
|
|
17117
|
+
if (!result.ok) {
|
|
17118
|
+
this.logger.warn(`amr retrospective annotation failed for ${issue.identifier}`, {
|
|
17119
|
+
error: result.error.message
|
|
17120
|
+
});
|
|
17121
|
+
}
|
|
17122
|
+
} catch (err) {
|
|
17123
|
+
this.logger.debug("amr retrospective annotation skipped (best-effort)", {
|
|
17124
|
+
issueId: issue.id,
|
|
17125
|
+
error: err instanceof Error ? err.message : String(err)
|
|
17126
|
+
});
|
|
17127
|
+
}
|
|
17128
|
+
}
|
|
16883
17129
|
/**
|
|
16884
17130
|
* Informs the state machine that an agent worker has exited.
|
|
16885
17131
|
*/
|
|
@@ -17966,6 +18212,341 @@ function launchTUI(orchestrator) {
|
|
|
17966
18212
|
return { waitUntilExit };
|
|
17967
18213
|
}
|
|
17968
18214
|
|
|
18215
|
+
// src/agent/triage-wiring.ts
|
|
18216
|
+
import { CascadeSimulator } from "@harness-engineering/graph";
|
|
18217
|
+
import {
|
|
18218
|
+
runScopingProbe,
|
|
18219
|
+
extractEntities,
|
|
18220
|
+
rankTriageCandidates,
|
|
18221
|
+
pilotScore
|
|
18222
|
+
} from "@harness-engineering/intelligence";
|
|
18223
|
+
function buildProbeInput(issue) {
|
|
18224
|
+
const taskText = buildTaskText(issue);
|
|
18225
|
+
const combined = `${issue.title ?? ""}
|
|
18226
|
+
${issue.description ?? ""}`.trim();
|
|
18227
|
+
return {
|
|
18228
|
+
// An item lacking a stable externalId is not dispatch-eligible (proposal Assumptions);
|
|
18229
|
+
// we still probe it (read-only report), keyed by its identifier as a fallback.
|
|
18230
|
+
externalId: issue.externalId ?? issue.identifier ?? issue.id,
|
|
18231
|
+
taskText,
|
|
18232
|
+
entityCandidates: extractEntities(combined),
|
|
18233
|
+
labels: issue.labels ?? []
|
|
18234
|
+
};
|
|
18235
|
+
}
|
|
18236
|
+
function findNode(store, candidate) {
|
|
18237
|
+
const trimmed = candidate.trim();
|
|
18238
|
+
if (trimmed.length === 0) return null;
|
|
18239
|
+
const byName = store.findNodes({ name: trimmed });
|
|
18240
|
+
if (byName.length > 0) return byName[0] ?? null;
|
|
18241
|
+
const byPath = store.findNodes({ path: trimmed });
|
|
18242
|
+
if (byPath.length > 0) return byPath[0] ?? null;
|
|
18243
|
+
const byId = store.getNode(`file:${trimmed}`);
|
|
18244
|
+
if (byId) return byId;
|
|
18245
|
+
return null;
|
|
18246
|
+
}
|
|
18247
|
+
function makeGraphScope(store) {
|
|
18248
|
+
const simulator = new CascadeSimulator(store);
|
|
18249
|
+
return {
|
|
18250
|
+
resolve(candidate) {
|
|
18251
|
+
try {
|
|
18252
|
+
const node = findNode(store, candidate);
|
|
18253
|
+
if (!node) return null;
|
|
18254
|
+
const result = simulator.simulate(node.id);
|
|
18255
|
+
return {
|
|
18256
|
+
candidate,
|
|
18257
|
+
nodeId: node.id,
|
|
18258
|
+
blastRadius: result.summary.totalAffected
|
|
18259
|
+
};
|
|
18260
|
+
} catch {
|
|
18261
|
+
return null;
|
|
18262
|
+
}
|
|
18263
|
+
}
|
|
18264
|
+
};
|
|
18265
|
+
}
|
|
18266
|
+
async function triageIssue2(issue, deps = {}) {
|
|
18267
|
+
const input = buildProbeInput(issue);
|
|
18268
|
+
const graph = deps.graphStore ? makeGraphScope(deps.graphStore) : void 0;
|
|
18269
|
+
return runScopingProbe(input, {
|
|
18270
|
+
...deps.provider ? { provider: deps.provider } : {},
|
|
18271
|
+
...graph ? { graph } : {},
|
|
18272
|
+
...deps.precedent ? { precedent: deps.precedent } : {},
|
|
18273
|
+
...deps.config ? { config: deps.config } : {},
|
|
18274
|
+
...deps.models ? { models: deps.models } : {}
|
|
18275
|
+
});
|
|
18276
|
+
}
|
|
18277
|
+
|
|
18278
|
+
// src/agent/brainstorm-wiring.ts
|
|
18279
|
+
import * as fs16 from "fs";
|
|
18280
|
+
import * as path22 from "path";
|
|
18281
|
+
import { z as z18 } from "zod";
|
|
18282
|
+
import {
|
|
18283
|
+
runAutoBrainstorm,
|
|
18284
|
+
depthForLevel
|
|
18285
|
+
} from "@harness-engineering/intelligence";
|
|
18286
|
+
var ForkStepSchema = z18.object({
|
|
18287
|
+
done: z18.boolean().describe("true when there are no more design forks to decide"),
|
|
18288
|
+
forkId: z18.string().describe('short stable id for this fork, e.g. "storage-backend"').optional(),
|
|
18289
|
+
question: z18.string().describe("the design question this fork decides").optional(),
|
|
18290
|
+
options: z18.array(z18.string()).describe("the mutually-exclusive options considered").optional(),
|
|
18291
|
+
// The AUTHORITATIVE recommendation validity checks (non-empty AND one of `options`) live in
|
|
18292
|
+
// the GENERATOR, where `fork.options` is known and where a degenerate value must map to a
|
|
18293
|
+
// `confidence:'low'` DOWNGRADE (→ runner halts with reason 'low-confidence'), not a hard
|
|
18294
|
+
// schema/provider throw. We deliberately keep this `optional()`/unconstrained so an empty or
|
|
18295
|
+
// absent recommendation still reaches the generator's semantic gate rather than short-
|
|
18296
|
+
// circuiting to `halted{ reason:'error' }`. See makeSelForkGenerator's SAFETY GATE 2.
|
|
18297
|
+
recommendation: z18.string().describe("the recommended option (one of options)").optional(),
|
|
18298
|
+
confidence: z18.enum(["high", "medium", "low"]).describe("self-assessed confidence in the recommendation").optional(),
|
|
18299
|
+
rationale: z18.string().describe("why this option").optional()
|
|
18300
|
+
});
|
|
18301
|
+
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
|
+
function makeSelForkGenerator(provider, input, opts = {}) {
|
|
18303
|
+
const samples = Math.max(2, opts.samples ?? 3);
|
|
18304
|
+
const maxTokens = opts.maxTokens ?? 512;
|
|
18305
|
+
return {
|
|
18306
|
+
async next(index, priorDecisions) {
|
|
18307
|
+
const prompt = buildForkPrompt(input, priorDecisions, index);
|
|
18308
|
+
const steps = [];
|
|
18309
|
+
for (let s = 0; s < samples; s++) {
|
|
18310
|
+
const { result } = await provider.analyze({
|
|
18311
|
+
prompt,
|
|
18312
|
+
systemPrompt: BRAINSTORM_RUBRIC,
|
|
18313
|
+
responseSchema: ForkStepSchema,
|
|
18314
|
+
maxTokens,
|
|
18315
|
+
...opts.model !== void 0 ? { model: opts.model } : {}
|
|
18316
|
+
});
|
|
18317
|
+
steps.push(result);
|
|
18318
|
+
}
|
|
18319
|
+
if (steps.some((st) => st.done)) return null;
|
|
18320
|
+
const first = steps[0];
|
|
18321
|
+
if (!first) return null;
|
|
18322
|
+
const fork = {
|
|
18323
|
+
id: first.forkId ?? `fork-${index}`,
|
|
18324
|
+
question: first.question ?? "unspecified fork",
|
|
18325
|
+
options: first.options ?? []
|
|
18326
|
+
};
|
|
18327
|
+
const recommendation = first.recommendation ?? "";
|
|
18328
|
+
const normQ = (q) => (q ?? "").trim().toLowerCase();
|
|
18329
|
+
const firstForkId = first.forkId ?? "";
|
|
18330
|
+
const firstQuestion = normQ(first.question);
|
|
18331
|
+
const identityStable = steps.every(
|
|
18332
|
+
(st) => (st.forkId ?? "") === firstForkId && normQ(st.question) === firstQuestion
|
|
18333
|
+
);
|
|
18334
|
+
const recStable = steps.every((st) => (st.recommendation ?? "") === recommendation);
|
|
18335
|
+
const recEmpty = recommendation.trim() === "";
|
|
18336
|
+
const recNotAnOption = !recEmpty && fork.options.length > 0 && !fork.options.includes(recommendation);
|
|
18337
|
+
const stable = identityStable && recStable && !recEmpty && !recNotAnOption;
|
|
18338
|
+
const reported = first.confidence ?? "low";
|
|
18339
|
+
const confidence = stable ? reported : "low";
|
|
18340
|
+
let downgradeReason = "";
|
|
18341
|
+
if (!identityStable) {
|
|
18342
|
+
downgradeReason = `fork identity drift across ${samples} samples (self-consistency downgrade to low): ` + steps.map((st) => `${st.forkId ?? "\u2205"}:"${(st.question ?? "\u2205").trim()}"`).join(" / ");
|
|
18343
|
+
} else if (recEmpty) {
|
|
18344
|
+
downgradeReason = "recommendation was empty/absent (a content-free recommendation is not a confident decision; downgrade to low)";
|
|
18345
|
+
} else if (recNotAnOption) {
|
|
18346
|
+
downgradeReason = `recommendation '${recommendation}' is not one of the offered options [${fork.options.join(", ")}] (downgrade to low)`;
|
|
18347
|
+
} else if (!recStable) {
|
|
18348
|
+
downgradeReason = `recommendation was unstable across ${samples} samples (self-consistency downgrade to low): ` + steps.map((st) => st.recommendation ?? "\u2205").join(" / ");
|
|
18349
|
+
}
|
|
18350
|
+
return {
|
|
18351
|
+
fork,
|
|
18352
|
+
recommendation,
|
|
18353
|
+
confidence,
|
|
18354
|
+
rationale: stable ? first.rationale ?? "" : downgradeReason
|
|
18355
|
+
};
|
|
18356
|
+
}
|
|
18357
|
+
};
|
|
18358
|
+
}
|
|
18359
|
+
function buildForkPrompt(input, priorDecisions, index) {
|
|
18360
|
+
const resolved = priorDecisions.length === 0 ? "(none yet)" : priorDecisions.map((d, i) => `${i + 1}. ${d.fork.question} \u2192 ${d.recommendation}`).join("\n");
|
|
18361
|
+
return `Item: ${input.title}
|
|
18362
|
+
Summary: ${input.summary}
|
|
18363
|
+
Complexity: ${input.level}
|
|
18364
|
+
|
|
18365
|
+
Forks already resolved:
|
|
18366
|
+
${resolved}
|
|
18367
|
+
|
|
18368
|
+
Decide fork #${index + 1}: the NEXT unresolved design fork. If every real fork is already resolved, return { "done": true }.`;
|
|
18369
|
+
}
|
|
18370
|
+
function brainstormInputFromIssue(issue, level) {
|
|
18371
|
+
return {
|
|
18372
|
+
externalId: issue.externalId ?? issue.identifier ?? issue.id,
|
|
18373
|
+
title: issue.title ?? issue.identifier ?? issue.id,
|
|
18374
|
+
summary: issue.description ?? "",
|
|
18375
|
+
level
|
|
18376
|
+
};
|
|
18377
|
+
}
|
|
18378
|
+
async function runBrainstormForIssue(issue, level, deps) {
|
|
18379
|
+
const input = brainstormInputFromIssue(issue, level);
|
|
18380
|
+
const generator = deps.generator ?? (deps.provider ? makeSelForkGenerator(deps.provider, input, deps.generatorOptions ?? {}) : void 0);
|
|
18381
|
+
if (!generator) {
|
|
18382
|
+
return {
|
|
18383
|
+
outcome: {
|
|
18384
|
+
kind: "halted",
|
|
18385
|
+
fork: { id: "no-generator", question: "no fork generator or provider wired", options: [] },
|
|
18386
|
+
reason: "error",
|
|
18387
|
+
detail: "runBrainstormForIssue requires either an injected generator or a SEL provider"
|
|
18388
|
+
}
|
|
18389
|
+
};
|
|
18390
|
+
}
|
|
18391
|
+
const depth = depthForLevel(level);
|
|
18392
|
+
const outcome = await runAutoBrainstorm(input, generator, depth);
|
|
18393
|
+
if (outcome.kind !== "completed") {
|
|
18394
|
+
return { outcome };
|
|
18395
|
+
}
|
|
18396
|
+
const result = { outcome };
|
|
18397
|
+
if (deps.docsRoot) {
|
|
18398
|
+
try {
|
|
18399
|
+
result.specPath = writeSpecDoc(deps.docsRoot, outcome.spec);
|
|
18400
|
+
} catch {
|
|
18401
|
+
}
|
|
18402
|
+
}
|
|
18403
|
+
if (deps.rescore) {
|
|
18404
|
+
try {
|
|
18405
|
+
const enrichedIssue = enrichIssueWithSpec(issue, outcome.spec);
|
|
18406
|
+
result.rescore = await triageIssue2(enrichedIssue, {
|
|
18407
|
+
...deps.rescore.graphStore ? { graphStore: deps.rescore.graphStore } : {},
|
|
18408
|
+
...deps.rescore.provider ? { provider: deps.rescore.provider } : {},
|
|
18409
|
+
...deps.rescore.precedent ? { precedent: deps.rescore.precedent } : {},
|
|
18410
|
+
...deps.rescore.config ? { config: deps.rescore.config } : {},
|
|
18411
|
+
...deps.rescore.models ? { models: deps.rescore.models } : {}
|
|
18412
|
+
});
|
|
18413
|
+
} catch {
|
|
18414
|
+
}
|
|
18415
|
+
}
|
|
18416
|
+
return result;
|
|
18417
|
+
}
|
|
18418
|
+
function slugFor(spec) {
|
|
18419
|
+
const base = spec.title || spec.externalId;
|
|
18420
|
+
return base.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "item";
|
|
18421
|
+
}
|
|
18422
|
+
function renderSpecMarkdown(spec) {
|
|
18423
|
+
const lines = [];
|
|
18424
|
+
lines.push(`# ${spec.title}`);
|
|
18425
|
+
lines.push("");
|
|
18426
|
+
lines.push("Status: **Auto-drafted (autonomous brainstorm \u2014 awaiting human review)**");
|
|
18427
|
+
lines.push("");
|
|
18428
|
+
lines.push(`Item: \`${spec.externalId}\``);
|
|
18429
|
+
lines.push("");
|
|
18430
|
+
lines.push("## Summary");
|
|
18431
|
+
lines.push("");
|
|
18432
|
+
lines.push(spec.summary || "_No summary provided._");
|
|
18433
|
+
lines.push("");
|
|
18434
|
+
lines.push("## Design decisions");
|
|
18435
|
+
lines.push("");
|
|
18436
|
+
if (spec.decisions.length === 0) {
|
|
18437
|
+
lines.push("_No forks required a decision \u2014 the item was unambiguous._");
|
|
18438
|
+
} else {
|
|
18439
|
+
for (const [i, d] of spec.decisions.entries()) {
|
|
18440
|
+
lines.push(`### ${i + 1}. ${d.fork.question}`);
|
|
18441
|
+
lines.push("");
|
|
18442
|
+
if (d.fork.options.length > 0) {
|
|
18443
|
+
lines.push(`Options considered: ${d.fork.options.map((o) => `\`${o}\``).join(", ")}`);
|
|
18444
|
+
lines.push("");
|
|
18445
|
+
}
|
|
18446
|
+
lines.push(`**Recommendation:** ${d.recommendation} (confidence: ${d.confidence})`);
|
|
18447
|
+
lines.push("");
|
|
18448
|
+
if (d.rationale) {
|
|
18449
|
+
lines.push(d.rationale);
|
|
18450
|
+
lines.push("");
|
|
18451
|
+
}
|
|
18452
|
+
}
|
|
18453
|
+
}
|
|
18454
|
+
lines.push("---");
|
|
18455
|
+
lines.push("");
|
|
18456
|
+
lines.push(
|
|
18457
|
+
"_This spec was drafted autonomously. Every fork above cleared the high-confidence bar; any fork the system could not confidently resolve would have halted the brainstorm and handed that fork to a human instead._"
|
|
18458
|
+
);
|
|
18459
|
+
lines.push("");
|
|
18460
|
+
return lines.join("\n");
|
|
18461
|
+
}
|
|
18462
|
+
function writeSpecDoc(docsRoot, spec) {
|
|
18463
|
+
const dir = path22.join(docsRoot, "changes", slugFor(spec));
|
|
18464
|
+
fs16.mkdirSync(dir, { recursive: true });
|
|
18465
|
+
const file = path22.join(dir, "proposal.md");
|
|
18466
|
+
fs16.writeFileSync(file, renderSpecMarkdown(spec), "utf-8");
|
|
18467
|
+
return file;
|
|
18468
|
+
}
|
|
18469
|
+
function enrichIssueWithSpec(issue, spec) {
|
|
18470
|
+
const decisionText = spec.decisions.map((d) => `${d.fork.question} \u2192 ${d.recommendation}. ${d.rationale}`).join("\n");
|
|
18471
|
+
const enrichedDescription = [issue.description ?? "", decisionText].filter(Boolean).join("\n\n");
|
|
18472
|
+
return {
|
|
18473
|
+
...issue,
|
|
18474
|
+
description: enrichedDescription,
|
|
18475
|
+
// The item now HAS a drafted spec — reflect it so the re-score's spec-exists signal is true.
|
|
18476
|
+
spec: issue.spec ?? `changes/${slugFor(spec)}/proposal.md`
|
|
18477
|
+
};
|
|
18478
|
+
}
|
|
18479
|
+
|
|
18480
|
+
// src/agent/triage-mark.ts
|
|
18481
|
+
import { slugifyFeatureName } from "@harness-engineering/core";
|
|
18482
|
+
import { dispatchableShapeKey } from "@harness-engineering/intelligence";
|
|
18483
|
+
var ELIGIBLE_STATUS = "planned";
|
|
18484
|
+
async function markApprovedForDispatch(items, deps) {
|
|
18485
|
+
const { store, config, recordPrediction, selfAssignee } = deps;
|
|
18486
|
+
if (!config.enabled) {
|
|
18487
|
+
return { ok: true, value: { marked: [], skipped: [] } };
|
|
18488
|
+
}
|
|
18489
|
+
const loaded = await store.load();
|
|
18490
|
+
if (!loaded.ok) return { ok: false, error: loaded.error };
|
|
18491
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
18492
|
+
for (const m of loaded.value.milestones) {
|
|
18493
|
+
for (const f of m.features) bySlug.set(slugifyFeatureName(f.name), f);
|
|
18494
|
+
}
|
|
18495
|
+
const marked = [];
|
|
18496
|
+
const skipped = [];
|
|
18497
|
+
for (const it of items) {
|
|
18498
|
+
const externalId = it.candidate.externalId;
|
|
18499
|
+
if (!it.candidate.humanApproved) {
|
|
18500
|
+
skipped.push({ externalId, reason: "not-approved" });
|
|
18501
|
+
continue;
|
|
18502
|
+
}
|
|
18503
|
+
const slug = slugifyFeatureName(it.featureName);
|
|
18504
|
+
const target = bySlug.get(slug);
|
|
18505
|
+
if (!target) {
|
|
18506
|
+
skipped.push({ externalId, reason: "feature-not-found" });
|
|
18507
|
+
continue;
|
|
18508
|
+
}
|
|
18509
|
+
if (selfAssignee !== void 0 && target.assignee != null && target.assignee !== selfAssignee) {
|
|
18510
|
+
skipped.push({ externalId, reason: "assigned-elsewhere" });
|
|
18511
|
+
continue;
|
|
18512
|
+
}
|
|
18513
|
+
const mutate = (f) => ({
|
|
18514
|
+
...f,
|
|
18515
|
+
spec: it.specPath,
|
|
18516
|
+
status: isActiveStatus(f.status) ? f.status : ELIGIBLE_STATUS
|
|
18517
|
+
});
|
|
18518
|
+
const patched = await store.patchFeature(slug, mutate);
|
|
18519
|
+
if (!patched.ok) return { ok: false, error: patched.error };
|
|
18520
|
+
const key = dispatchableShapeKey(it.labels, it.verdict.level);
|
|
18521
|
+
const prediction = {
|
|
18522
|
+
externalId,
|
|
18523
|
+
shapeKey: key,
|
|
18524
|
+
verdict: it.verdict,
|
|
18525
|
+
// `levers` is an OPAQUE DIAGNOSTIC SNAPSHOT (the verdict's static signals), NOT the
|
|
18526
|
+
// typed ProbeLevers. The Phase-4 comparator grades on `verdict.level` +
|
|
18527
|
+
// `scopeEstimate` ONLY (see retrospective.ts) and never reads `levers`, so this
|
|
18528
|
+
// human-legible signal dump cannot mislead the grade. The full typed ProbeLevers
|
|
18529
|
+
// are not threaded here on purpose (it would balloon ReadyCandidate→marker for a
|
|
18530
|
+
// field nothing downstream consumes); `record.ts` re-documents the field to match.
|
|
18531
|
+
levers: it.verdict.signals,
|
|
18532
|
+
scopeEstimate: it.scopeEstimate,
|
|
18533
|
+
// Prefer the per-shape evidence-derived stage (SC6) when the caller resolved one;
|
|
18534
|
+
// fall back to the uniform config stage (Phase-3 behavior). Cold-start ⇒ the caller
|
|
18535
|
+
// resolves stage 1, so this is byte-identical to `config.ratchetStage` at cold-start.
|
|
18536
|
+
ratchetStage: it.effectiveStage ?? config.ratchetStage
|
|
18537
|
+
};
|
|
18538
|
+
const recorded = await recordPrediction(prediction);
|
|
18539
|
+
if (!recorded.ok) {
|
|
18540
|
+
return { ok: false, error: recorded.error ?? new Error("recordPrediction failed") };
|
|
18541
|
+
}
|
|
18542
|
+
marked.push(externalId);
|
|
18543
|
+
}
|
|
18544
|
+
return { ok: true, value: { marked, skipped } };
|
|
18545
|
+
}
|
|
18546
|
+
function isActiveStatus(status) {
|
|
18547
|
+
return status === "planned" || status === "in-progress";
|
|
18548
|
+
}
|
|
18549
|
+
|
|
17969
18550
|
// src/maintenance/sync-main.ts
|
|
17970
18551
|
import { execFile as nodeExecFile2 } from "child_process";
|
|
17971
18552
|
import { promisify as promisify5 } from "util";
|
|
@@ -18159,8 +18740,8 @@ function selectTasks(tasks, history, filter) {
|
|
|
18159
18740
|
}
|
|
18160
18741
|
|
|
18161
18742
|
// src/sessions/search-index.ts
|
|
18162
|
-
import * as
|
|
18163
|
-
import * as
|
|
18743
|
+
import * as fs17 from "fs";
|
|
18744
|
+
import * as path23 from "path";
|
|
18164
18745
|
import Database2 from "better-sqlite3";
|
|
18165
18746
|
import { INDEXED_FILE_KINDS } from "@harness-engineering/types";
|
|
18166
18747
|
var SEARCH_INDEX_FILE = "search-index.sqlite";
|
|
@@ -18205,7 +18786,7 @@ function normalizeFts5Query(query) {
|
|
|
18205
18786
|
return query.split(/\s+/).filter((tok) => tok.length > 0).map((tok) => `"${tok.replace(/"/g, '""')}"`).join(" ");
|
|
18206
18787
|
}
|
|
18207
18788
|
function searchIndexPath(projectPath) {
|
|
18208
|
-
return
|
|
18789
|
+
return path23.join(projectPath, ".harness", SEARCH_INDEX_FILE);
|
|
18209
18790
|
}
|
|
18210
18791
|
var FILE_KIND_TO_FILENAME = {
|
|
18211
18792
|
summary: "summary.md",
|
|
@@ -18220,7 +18801,7 @@ var SqliteSearchIndex = class {
|
|
|
18220
18801
|
removeSessionStmt;
|
|
18221
18802
|
totalStmt;
|
|
18222
18803
|
constructor(dbPath) {
|
|
18223
|
-
|
|
18804
|
+
fs17.mkdirSync(path23.dirname(dbPath), { recursive: true });
|
|
18224
18805
|
this.db = new Database2(dbPath);
|
|
18225
18806
|
this.db.pragma("journal_mode = WAL");
|
|
18226
18807
|
this.db.pragma("synchronous = NORMAL");
|
|
@@ -18325,14 +18906,14 @@ function indexSessionDirectory(idx, args) {
|
|
|
18325
18906
|
let docsWritten = 0;
|
|
18326
18907
|
for (const kind of kinds) {
|
|
18327
18908
|
const fileName = FILE_KIND_TO_FILENAME[kind];
|
|
18328
|
-
const filePath =
|
|
18329
|
-
if (!
|
|
18330
|
-
let body =
|
|
18909
|
+
const filePath = path23.join(args.sessionDir, fileName);
|
|
18910
|
+
if (!fs17.existsSync(filePath)) continue;
|
|
18911
|
+
let body = fs17.readFileSync(filePath, "utf8");
|
|
18331
18912
|
if (Buffer.byteLength(body, "utf8") > cap) {
|
|
18332
18913
|
body = body.slice(0, cap) + "\n\n[TRUNCATED]";
|
|
18333
18914
|
}
|
|
18334
|
-
const stat =
|
|
18335
|
-
const relPath =
|
|
18915
|
+
const stat = fs17.statSync(filePath);
|
|
18916
|
+
const relPath = path23.relative(args.projectPath, filePath).replaceAll("\\", "/");
|
|
18336
18917
|
idx.upsertSessionDoc({
|
|
18337
18918
|
sessionId: args.sessionId,
|
|
18338
18919
|
archived: args.archived,
|
|
@@ -18347,17 +18928,17 @@ function indexSessionDirectory(idx, args) {
|
|
|
18347
18928
|
}
|
|
18348
18929
|
function reindexFromArchive(projectPath, opts = {}) {
|
|
18349
18930
|
const start = Date.now();
|
|
18350
|
-
const archiveBase =
|
|
18931
|
+
const archiveBase = path23.join(projectPath, ".harness", "archive", "sessions");
|
|
18351
18932
|
const idx = openSearchIndex(projectPath);
|
|
18352
18933
|
try {
|
|
18353
18934
|
idx.resetArchived();
|
|
18354
18935
|
let sessionsIndexed = 0;
|
|
18355
18936
|
let docsWritten = 0;
|
|
18356
|
-
if (
|
|
18357
|
-
const entries =
|
|
18937
|
+
if (fs17.existsSync(archiveBase)) {
|
|
18938
|
+
const entries = fs17.readdirSync(archiveBase, { withFileTypes: true });
|
|
18358
18939
|
for (const entry of entries) {
|
|
18359
18940
|
if (!entry.isDirectory()) continue;
|
|
18360
|
-
const sessionDir =
|
|
18941
|
+
const sessionDir = path23.join(archiveBase, entry.name);
|
|
18361
18942
|
const result = indexSessionDirectory(idx, {
|
|
18362
18943
|
sessionId: entry.name,
|
|
18363
18944
|
sessionDir,
|
|
@@ -18377,8 +18958,8 @@ function reindexFromArchive(projectPath, opts = {}) {
|
|
|
18377
18958
|
}
|
|
18378
18959
|
|
|
18379
18960
|
// src/sessions/summarize.ts
|
|
18380
|
-
import * as
|
|
18381
|
-
import * as
|
|
18961
|
+
import * as fs18 from "fs";
|
|
18962
|
+
import * as path24 from "path";
|
|
18382
18963
|
import {
|
|
18383
18964
|
SessionSummarySchema
|
|
18384
18965
|
} from "@harness-engineering/types";
|
|
@@ -18408,10 +18989,10 @@ var USER_PROMPT_PREAMBLE = `Below are the archived files for a single harness-en
|
|
|
18408
18989
|
function readInputCorpus(archiveDir) {
|
|
18409
18990
|
const parts = [];
|
|
18410
18991
|
for (const { filename, kind } of SUMMARY_INPUT_FILES) {
|
|
18411
|
-
const p =
|
|
18412
|
-
if (!
|
|
18992
|
+
const p = path24.join(archiveDir, filename);
|
|
18993
|
+
if (!fs18.existsSync(p)) continue;
|
|
18413
18994
|
try {
|
|
18414
|
-
const content =
|
|
18995
|
+
const content = fs18.readFileSync(p, "utf8");
|
|
18415
18996
|
if (content.trim().length === 0) continue;
|
|
18416
18997
|
parts.push(`## FILE: ${kind}
|
|
18417
18998
|
|
|
@@ -18462,7 +19043,7 @@ function renderLlmSummaryMarkdown(summary, meta) {
|
|
|
18462
19043
|
return lines.join("\n");
|
|
18463
19044
|
}
|
|
18464
19045
|
function writeStubMarkdown(archiveDir, reason) {
|
|
18465
|
-
const filePath =
|
|
19046
|
+
const filePath = path24.join(archiveDir, LLM_SUMMARY_FILE);
|
|
18466
19047
|
const body = `---
|
|
18467
19048
|
generatedAt: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
18468
19049
|
schemaVersion: 1
|
|
@@ -18473,12 +19054,12 @@ status: failed
|
|
|
18473
19054
|
|
|
18474
19055
|
- reason: ${reason}
|
|
18475
19056
|
`;
|
|
18476
|
-
|
|
19057
|
+
fs18.writeFileSync(filePath, body, "utf8");
|
|
18477
19058
|
return filePath;
|
|
18478
19059
|
}
|
|
18479
19060
|
async function summarizeArchivedSession(ctx) {
|
|
18480
19061
|
const writeStubOnError = ctx.writeStubOnError ?? true;
|
|
18481
|
-
if (!
|
|
19062
|
+
if (!fs18.existsSync(ctx.archiveDir)) {
|
|
18482
19063
|
return Err23(new Error(`archive directory not found: ${ctx.archiveDir}`));
|
|
18483
19064
|
}
|
|
18484
19065
|
const corpus = readInputCorpus(ctx.archiveDir);
|
|
@@ -18539,9 +19120,9 @@ async function summarizeArchivedSession(ctx) {
|
|
|
18539
19120
|
outputTokens: response.tokenUsage.outputTokens,
|
|
18540
19121
|
schemaVersion: 1
|
|
18541
19122
|
};
|
|
18542
|
-
const filePath =
|
|
19123
|
+
const filePath = path24.join(ctx.archiveDir, LLM_SUMMARY_FILE);
|
|
18543
19124
|
const body = renderLlmSummaryMarkdown(parsed.data, meta);
|
|
18544
|
-
|
|
19125
|
+
fs18.writeFileSync(filePath, body, "utf8");
|
|
18545
19126
|
return Ok24({ summary: parsed.data, meta, filePath });
|
|
18546
19127
|
}
|
|
18547
19128
|
function isSummaryEnabled(config) {
|
|
@@ -18622,6 +19203,7 @@ import { discoverCandidates } from "@harness-engineering/local-models";
|
|
|
18622
19203
|
export {
|
|
18623
19204
|
AdaptiveRouter,
|
|
18624
19205
|
AnalysisArchive,
|
|
19206
|
+
BRAINSTORM_RUBRIC,
|
|
18625
19207
|
BUILT_IN_TASKS,
|
|
18626
19208
|
BackendDefSchema,
|
|
18627
19209
|
BackendRouter,
|
|
@@ -18664,8 +19246,10 @@ export {
|
|
|
18664
19246
|
WorkspaceManager,
|
|
18665
19247
|
applyEvent,
|
|
18666
19248
|
artifactPresenceFromIssue,
|
|
19249
|
+
brainstormInputFromIssue,
|
|
18667
19250
|
buildArchiveHooks,
|
|
18668
19251
|
buildCapabilityRegistry,
|
|
19252
|
+
buildProbeInput,
|
|
18669
19253
|
buildWorkflowContext,
|
|
18670
19254
|
calculateRetryDelay,
|
|
18671
19255
|
canDispatch,
|
|
@@ -18685,6 +19269,7 @@ export {
|
|
|
18685
19269
|
emitProposalApproved,
|
|
18686
19270
|
emitProposalCreated,
|
|
18687
19271
|
emitProposalRejected,
|
|
19272
|
+
enrichIssueWithSpec,
|
|
18688
19273
|
estimateCost,
|
|
18689
19274
|
explicitFindingsCount,
|
|
18690
19275
|
extractHighlights,
|
|
@@ -18699,22 +19284,30 @@ export {
|
|
|
18699
19284
|
launchTUI,
|
|
18700
19285
|
loadPublishedIndex,
|
|
18701
19286
|
makeBackendResolver,
|
|
19287
|
+
makeGraphScope,
|
|
19288
|
+
makeSelForkGenerator,
|
|
19289
|
+
markApprovedForDispatch,
|
|
18702
19290
|
migrateAgentConfig,
|
|
18703
19291
|
normalizeFts5Query,
|
|
18704
19292
|
normalizeHarnessCommand,
|
|
18705
19293
|
normalizeLocalModel,
|
|
18706
19294
|
openSearchIndex,
|
|
19295
|
+
pilotScore,
|
|
19296
|
+
precedentLookupFromStored,
|
|
18707
19297
|
promote,
|
|
19298
|
+
rankTriageCandidates,
|
|
18708
19299
|
reconcile,
|
|
18709
19300
|
recoverFindingsCount,
|
|
18710
19301
|
reindexFromArchive,
|
|
18711
19302
|
renderAnalysisComment,
|
|
18712
19303
|
renderLlmSummaryMarkdown,
|
|
18713
19304
|
renderPRComment,
|
|
19305
|
+
renderSpecMarkdown,
|
|
18714
19306
|
resolveEscalationConfig,
|
|
18715
19307
|
resolveOrchestratorId,
|
|
18716
19308
|
routeIssue,
|
|
18717
19309
|
routingWarnings,
|
|
19310
|
+
runBrainstormForIssue,
|
|
18718
19311
|
runGate,
|
|
18719
19312
|
runHarnessCheck,
|
|
18720
19313
|
savePublishedIndex,
|
|
@@ -18722,10 +19315,11 @@ export {
|
|
|
18722
19315
|
selectCandidates,
|
|
18723
19316
|
selectCheapestQualifying,
|
|
18724
19317
|
selectTasks,
|
|
19318
|
+
slugFor,
|
|
18725
19319
|
sortCandidates,
|
|
18726
19320
|
summarizeArchivedSession,
|
|
18727
19321
|
syncMain,
|
|
18728
|
-
triageIssue,
|
|
19322
|
+
triageIssue2 as triageIssue,
|
|
18729
19323
|
truncateForBudget,
|
|
18730
19324
|
validateCustomTasks,
|
|
18731
19325
|
validateWorkflowConfig,
|