@harness-engineering/orchestrator 0.13.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.mjs CHANGED
@@ -1911,26 +1911,60 @@ var ModelSchema = z.union([z.string().min(1), z.array(z.string().min(1)).nonempt
1911
1911
  message: "model must be a non-empty string or array of strings"
1912
1912
  })
1913
1913
  });
1914
+ var CAPABILITY_TIER = z.enum(["fast", "standard", "strong"]);
1915
+ var COMPLEXITY_LEVEL = z.enum(["trivial", "simple", "moderate", "complex"]);
1916
+ var PRIVACY_CLASS = z.enum(["on-device", "pooled-isolated", "byo-endpoint", "shared-cloud"]);
1917
+ var BackendCapabilitiesSchema = z.object({
1918
+ tier: CAPABILITY_TIER,
1919
+ costPer1kTokens: z.number().nonnegative(),
1920
+ privacyClass: PRIVACY_CLASS,
1921
+ contextWindow: z.number().int().positive(),
1922
+ vision: z.boolean().optional(),
1923
+ toolUse: z.boolean().optional()
1924
+ }).strict();
1925
+ var RoutingPolicySchema = z.object({
1926
+ complexityTierMatrix: z.record(COMPLEXITY_LEVEL, CAPABILITY_TIER).optional(),
1927
+ skillTierOverrides: z.record(z.string(), CAPABILITY_TIER).optional(),
1928
+ privacyFloor: PRIVACY_CLASS.optional(),
1929
+ budget: z.object({
1930
+ capUsd: z.number(),
1931
+ degradeAtPct: z.number().optional(),
1932
+ onBudgetExhausted: z.enum(["degrade", "pause", "human"])
1933
+ }).optional(),
1934
+ sensitivePaths: z.array(z.string()).optional(),
1935
+ escalationThreshold: z.number().optional(),
1936
+ // string[], NOT the finite BackendDef['type'] union: an unknown provider must
1937
+ // fail CLOSED at tier selection, not reject the whole policy (Shuttle wire).
1938
+ allowedProviders: z.array(z.string()).optional(),
1939
+ acceptanceEval: z.object({
1940
+ enabled: z.boolean(),
1941
+ model: z.string().optional()
1942
+ }).optional()
1943
+ });
1914
1944
  var BackendDefSchema = z.discriminatedUnion("type", [
1915
- z.object({ type: z.literal("mock") }).strict(),
1945
+ z.object({ type: z.literal("mock"), capabilities: BackendCapabilitiesSchema.optional() }).strict(),
1916
1946
  z.object({
1917
1947
  type: z.literal("claude"),
1918
- command: z.string().optional()
1948
+ command: z.string().optional(),
1949
+ capabilities: BackendCapabilitiesSchema.optional()
1919
1950
  }).strict(),
1920
1951
  z.object({
1921
1952
  type: z.literal("anthropic"),
1922
1953
  model: z.string().min(1),
1923
- apiKey: z.string().optional()
1954
+ apiKey: z.string().optional(),
1955
+ capabilities: BackendCapabilitiesSchema.optional()
1924
1956
  }).strict(),
1925
1957
  z.object({
1926
1958
  type: z.literal("openai"),
1927
1959
  model: z.string().min(1),
1928
- apiKey: z.string().optional()
1960
+ apiKey: z.string().optional(),
1961
+ capabilities: BackendCapabilitiesSchema.optional()
1929
1962
  }).strict(),
1930
1963
  z.object({
1931
1964
  type: z.literal("gemini"),
1932
1965
  model: z.string().min(1),
1933
- apiKey: z.string().optional()
1966
+ apiKey: z.string().optional(),
1967
+ capabilities: BackendCapabilitiesSchema.optional()
1934
1968
  }).strict(),
1935
1969
  z.object({
1936
1970
  type: z.literal("local"),
@@ -1938,7 +1972,8 @@ var BackendDefSchema = z.discriminatedUnion("type", [
1938
1972
  model: ModelSchema,
1939
1973
  apiKey: z.string().optional(),
1940
1974
  timeoutMs: z.number().int().positive().optional(),
1941
- probeIntervalMs: z.number().int().min(1e3).optional()
1975
+ probeIntervalMs: z.number().int().min(1e3).optional(),
1976
+ capabilities: BackendCapabilitiesSchema.optional()
1942
1977
  }).strict(),
1943
1978
  z.object({
1944
1979
  type: z.literal("pi"),
@@ -1946,7 +1981,8 @@ var BackendDefSchema = z.discriminatedUnion("type", [
1946
1981
  model: ModelSchema,
1947
1982
  apiKey: z.string().optional(),
1948
1983
  timeoutMs: z.number().int().positive().optional(),
1949
- probeIntervalMs: z.number().int().min(1e3).optional()
1984
+ probeIntervalMs: z.number().int().min(1e3).optional(),
1985
+ capabilities: BackendCapabilitiesSchema.optional()
1950
1986
  }).strict()
1951
1987
  ]);
1952
1988
  var RoutingValueSchema = z.union([
@@ -1971,7 +2007,36 @@ var RoutingConfigSchema = z.object({
1971
2007
  }).strict().optional(),
1972
2008
  // --- Spec B Phase 0: new optional maps (resolver wired in Phase 1) ---
1973
2009
  skills: z.record(z.string().min(1), RoutingValueSchema).optional(),
1974
- modes: z.record(z.string().min(1), RoutingValueSchema).optional()
2010
+ modes: z.record(z.string().min(1), RoutingValueSchema).optional(),
2011
+ // --- AMR: opt-in adaptive-routing policy. Its PRESENCE flips the orchestrator
2012
+ // from identity/default dispatch to complexity-aware tier routing (default-off
2013
+ // when absent). Previously accepted by the runtime PUT endpoint only. ---
2014
+ policy: RoutingPolicySchema.optional()
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()
1975
2040
  }).strict();
1976
2041
  var WorkflowStepSchema = z.object({
1977
2042
  skill: z.string().min(1),
@@ -2010,12 +2075,12 @@ var BackendsMapSchema = z2.record(z2.string(), BackendDefSchema);
2010
2075
  function crossFieldRoutingIssues(backends, routing) {
2011
2076
  const issues = [];
2012
2077
  const names = new Set(Object.keys(backends));
2013
- const checkRef = (path24, value) => {
2078
+ const checkRef = (path25, value) => {
2014
2079
  if (value === void 0) return;
2015
2080
  const entries = Array.isArray(value) ? value : [value];
2016
2081
  entries.forEach((name, idx) => {
2017
2082
  if (names.has(name)) return;
2018
- const pathWithIdx = Array.isArray(value) ? [...path24, String(idx)] : path24;
2083
+ const pathWithIdx = Array.isArray(value) ? [...path25, String(idx)] : path25;
2019
2084
  issues.push({
2020
2085
  path: pathWithIdx,
2021
2086
  message: `routing.${pathWithIdx.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
@@ -2112,6 +2177,10 @@ function validateWorkflowConfig(config, options = {}) {
2112
2177
  const parsed = z2.array(StagedWorkflowDeclSchema).safeParse(c.workflows);
2113
2178
  if (!parsed.success) return Err(new Error(`workflows: ${parsed.error.message}`));
2114
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
+ }
2115
2184
  return Ok2({ config, warnings });
2116
2185
  }
2117
2186
  function getDefaultConfig() {
@@ -3366,6 +3435,68 @@ import { randomUUID as randomUUID5 } from "crypto";
3366
3435
  import { RoutingError as RoutingError3 } from "@harness-engineering/types";
3367
3436
  import { writeTaint, SecurityScanner as SecurityScanner2 } from "@harness-engineering/core";
3368
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
3369
3500
  import { GraphStore as GraphStore2 } from "@harness-engineering/graph";
3370
3501
 
3371
3502
  // src/core/stall-detector.ts
@@ -3949,7 +4080,8 @@ var CompletionHandler = class {
3949
4080
  import {
3950
4081
  GitHubIssuesSyncAdapter as GitHubIssuesSyncAdapter3,
3951
4082
  loadTrackerSyncConfig as loadTrackerSyncConfig3,
3952
- createTrackerClient as createTrackerClient2
4083
+ createTrackerClient as createTrackerClient2,
4084
+ eventSourcing as eventSourcing2
3953
4085
  } from "@harness-engineering/core";
3954
4086
 
3955
4087
  // src/tracker/adapters/github-issues-issue-tracker.ts
@@ -4661,11 +4793,11 @@ function detectLegacyFields(agent) {
4661
4793
  }
4662
4794
  function buildCase1Warnings(presentLegacy, suppressLocalGroup) {
4663
4795
  const warnings = [];
4664
- for (const path24 of presentLegacy) {
4665
- if (CASE1_ALWAYS_SUPPRESS.has(path24)) continue;
4666
- if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path24)) continue;
4796
+ for (const path25 of presentLegacy) {
4797
+ if (CASE1_ALWAYS_SUPPRESS.has(path25)) continue;
4798
+ if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path25)) continue;
4667
4799
  warnings.push(
4668
- `Ignoring legacy field '${path24}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
4800
+ `Ignoring legacy field '${path25}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
4669
4801
  );
4670
4802
  }
4671
4803
  return warnings;
@@ -4693,7 +4825,7 @@ function migrateAgentConfig(agent) {
4693
4825
  }
4694
4826
  const { backends, routing } = synthesizeBackendsAndRouting(agent);
4695
4827
  const warnings = presentLegacy.map(
4696
- (path24) => `Deprecated config field '${path24}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
4828
+ (path25) => `Deprecated config field '${path25}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
4697
4829
  );
4698
4830
  return {
4699
4831
  config: { ...agent, backends, routing },
@@ -4778,12 +4910,12 @@ var BackendRouter = class {
4778
4910
  */
4779
4911
  resolve(useCase, opts) {
4780
4912
  const startedAt = performance.now();
4781
- const path24 = [];
4913
+ const path25 = [];
4782
4914
  const tryChain = (source, value) => {
4783
4915
  if (value === void 0) return void 0;
4784
4916
  for (const name of toArray(value)) {
4785
4917
  const step = { source, candidate: name, outcome: "considered" };
4786
- path24.push(step);
4918
+ path25.push(step);
4787
4919
  if (this.backends[name]) {
4788
4920
  step.outcome = "chosen";
4789
4921
  return name;
@@ -4802,7 +4934,7 @@ var BackendRouter = class {
4802
4934
  return {
4803
4935
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4804
4936
  useCase,
4805
- resolutionPath: path24,
4937
+ resolutionPath: path25,
4806
4938
  backendName,
4807
4939
  backendType: def.type,
4808
4940
  durationMs: performance.now() - startedAt
@@ -4835,7 +4967,7 @@ var BackendRouter = class {
4835
4967
  if (fromDefault) return emitAndReturn(decide(fromDefault));
4836
4968
  const knownList = Object.keys(this.backends).join(", ") || "(none)";
4837
4969
  throw new Error(
4838
- `BackendRouter.resolve: routing.default produced no available backend for useCase=${JSON.stringify(useCase)}. Resolution path: ${JSON.stringify(path24)}. Known backends: [${knownList}].`
4970
+ `BackendRouter.resolve: routing.default produced no available backend for useCase=${JSON.stringify(useCase)}. Resolution path: ${JSON.stringify(path25)}. Known backends: [${knownList}].`
4839
4971
  );
4840
4972
  }
4841
4973
  /**
@@ -4928,7 +5060,7 @@ var BackendRouter = class {
4928
5060
  check(`modes.${mode}`, value);
4929
5061
  }
4930
5062
  if (missing.length > 0) {
4931
- const detail = missing.map(({ path: path24, name }) => `routing.${path24} -> '${name}'`).join("; ");
5063
+ const detail = missing.map(({ path: path25, name }) => `routing.${path25} -> '${name}'`).join("; ");
4932
5064
  const known_ = [...known].join(", ") || "(none)";
4933
5065
  throw new Error(
4934
5066
  `BackendRouter: routing references unknown backend(s): ${detail}. Defined backends: [${known_}].`
@@ -8281,7 +8413,7 @@ function buildRoutingUseCase(issue, backendParam, catalog) {
8281
8413
 
8282
8414
  // src/agent/live-classify.ts
8283
8415
  import {
8284
- classify
8416
+ classify as classify2
8285
8417
  } from "@harness-engineering/intelligence";
8286
8418
  var CONSERVATIVE = {
8287
8419
  level: "moderate",
@@ -8305,7 +8437,7 @@ function makeLiveClassify(resolveProvider) {
8305
8437
  riskHigh,
8306
8438
  prompt: taskText.prompt
8307
8439
  };
8308
- return classify(input, resolveProvider());
8440
+ return classify2(input, resolveProvider());
8309
8441
  };
8310
8442
  }
8311
8443
 
@@ -10705,7 +10837,12 @@ function deriveTraceCost(body, decision, def, routing, backends) {
10705
10837
  backends
10706
10838
  );
10707
10839
  const estCostUsd = estimateCost(costedDef, { useCase: body.useCase });
10708
- return { tierRequired, estCostUsd, costedBackendName: costedName };
10840
+ return {
10841
+ tierRequired,
10842
+ estCostUsd,
10843
+ costedBackendName: costedName,
10844
+ costedBackendType: costedDef.type
10845
+ };
10709
10846
  }
10710
10847
  function selectCostedBackend(tierRequired, decision, def, routing, backends) {
10711
10848
  const registry = buildCapabilityRegistry(backends);
@@ -10770,7 +10907,7 @@ async function handleTrace(req, res, deps) {
10770
10907
  opts
10771
10908
  );
10772
10909
  if (r.data.complexity !== void 0 || r.data.risk !== void 0) {
10773
- const { tierRequired, estCostUsd, costedBackendName } = deriveTraceCost(
10910
+ const { tierRequired, estCostUsd, costedBackendName, costedBackendType } = deriveTraceCost(
10774
10911
  r.data,
10775
10912
  decision,
10776
10913
  def,
@@ -10782,9 +10919,12 @@ async function handleTrace(req, res, deps) {
10782
10919
  def: { type: def.type },
10783
10920
  tierRequired,
10784
10921
  estCostUsd,
10785
- // Name the backend the cost belongs to so operators see tier↔cost↔backend
10786
- // are consistent (was implicit + divergent before this fix).
10787
- costedBackendName
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
10788
10928
  });
10789
10929
  return true;
10790
10930
  }
@@ -10794,26 +10934,6 @@ async function handleTrace(req, res, deps) {
10794
10934
  }
10795
10935
  return true;
10796
10936
  }
10797
- var CAPABILITY_TIER = z14.enum(["fast", "standard", "strong"]);
10798
- var COMPLEXITY_LEVEL = z14.enum(["trivial", "simple", "moderate", "complex"]);
10799
- var PRIVACY_CLASS = z14.enum(["on-device", "byo-endpoint", "shared-cloud"]);
10800
- var RoutingPolicySchema = z14.object({
10801
- complexityTierMatrix: z14.record(COMPLEXITY_LEVEL, CAPABILITY_TIER).optional(),
10802
- skillTierOverrides: z14.record(z14.string(), CAPABILITY_TIER).optional(),
10803
- privacyFloor: PRIVACY_CLASS.optional(),
10804
- budget: z14.object({
10805
- capUsd: z14.number(),
10806
- degradeAtPct: z14.number().optional(),
10807
- onBudgetExhausted: z14.enum(["degrade", "pause", "human"])
10808
- }).optional(),
10809
- sensitivePaths: z14.array(z14.string()).optional(),
10810
- escalationThreshold: z14.number().optional(),
10811
- allowedProviders: z14.array(z14.string()).optional(),
10812
- acceptanceEval: z14.object({
10813
- enabled: z14.boolean(),
10814
- model: z14.string().optional()
10815
- }).optional()
10816
- });
10817
10937
  async function handlePolicy(req, res, deps) {
10818
10938
  if (!deps.ingestRoutingPolicy || deps.router === null) return unavailable(res);
10819
10939
  let raw;
@@ -11366,8 +11486,8 @@ function parseToken(raw) {
11366
11486
  return { id: raw.slice(0, dot), secret: raw.slice(dot + 1) };
11367
11487
  }
11368
11488
  var TokenStore = class {
11369
- constructor(path24) {
11370
- this.path = path24;
11489
+ constructor(path25) {
11490
+ this.path = path25;
11371
11491
  }
11372
11492
  path;
11373
11493
  cache = null;
@@ -11474,8 +11594,8 @@ import { appendFile, mkdir as mkdir8 } from "fs/promises";
11474
11594
  import { dirname as dirname6 } from "path";
11475
11595
  import { AuthAuditEntrySchema } from "@harness-engineering/types";
11476
11596
  var AuditLogger = class {
11477
- constructor(path24, opts = {}) {
11478
- this.path = path24;
11597
+ constructor(path25, opts = {}) {
11598
+ this.path = path25;
11479
11599
  this.opts = opts;
11480
11600
  }
11481
11601
  path;
@@ -11713,9 +11833,9 @@ var V1_BRIDGE_ROUTES = [
11713
11833
  function isV1Bridge(method, url) {
11714
11834
  return V1_BRIDGE_ROUTES.some((r) => r.method === method && r.pattern.test(url));
11715
11835
  }
11716
- function requiredBridgeScope(method, path24) {
11836
+ function requiredBridgeScope(method, path25) {
11717
11837
  for (const r of V1_BRIDGE_ROUTES) {
11718
- if (r.method === method && r.pattern.test(path24)) return r.scope;
11838
+ if (r.method === method && r.pattern.test(path25)) return r.scope;
11719
11839
  }
11720
11840
  return null;
11721
11841
  }
@@ -11725,11 +11845,11 @@ function hasScope(held, required) {
11725
11845
  if (held.includes("admin")) return true;
11726
11846
  return held.includes(required);
11727
11847
  }
11728
- function exactScopeForRoute(method, path24) {
11729
- if (path24 === "/api/v1/auth/token" && method === "POST") return "admin";
11730
- if (path24 === "/api/v1/auth/tokens" && method === "GET") return "admin";
11731
- if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path24) && method === "DELETE") return "admin";
11732
- if ((path24 === "/api/state" || path24 === "/api/v1/state") && method === "GET") return "read-status";
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";
11733
11853
  return null;
11734
11854
  }
11735
11855
  var PREFIX_SCOPES = [
@@ -11746,19 +11866,19 @@ var PREFIX_SCOPES = [
11746
11866
  ["/api/sessions", "read-status"],
11747
11867
  ["/api/chat-proxy", "trigger-job"]
11748
11868
  ];
11749
- function prefixScopeForPath(path24) {
11750
- if (path24 === "/api/chat") return "trigger-job";
11869
+ function prefixScopeForPath(path25) {
11870
+ if (path25 === "/api/chat") return "trigger-job";
11751
11871
  for (const [prefix, scope] of PREFIX_SCOPES) {
11752
- if (path24.startsWith(prefix)) return scope;
11872
+ if (path25.startsWith(prefix)) return scope;
11753
11873
  }
11754
11874
  return null;
11755
11875
  }
11756
- function requiredScopeForRoute(method, path24) {
11757
- const bridgeScope = requiredBridgeScope(method, path24);
11876
+ function requiredScopeForRoute(method, path25) {
11877
+ const bridgeScope = requiredBridgeScope(method, path25);
11758
11878
  if (bridgeScope) return bridgeScope;
11759
- const exactScope = exactScopeForRoute(method, path24);
11879
+ const exactScope = exactScopeForRoute(method, path25);
11760
11880
  if (exactScope) return exactScope;
11761
- return prefixScopeForPath(path24);
11881
+ return prefixScopeForPath(path25);
11762
11882
  }
11763
11883
 
11764
11884
  // src/server/http.ts
@@ -12287,8 +12407,8 @@ function genSecret2() {
12287
12407
  return randomBytes3(32).toString("base64url");
12288
12408
  }
12289
12409
  var WebhookStore = class {
12290
- constructor(path24) {
12291
- this.path = path24;
12410
+ constructor(path25) {
12411
+ this.path = path25;
12292
12412
  }
12293
12413
  path;
12294
12414
  cache = null;
@@ -15141,8 +15261,8 @@ function validateCheckShape(prefix, task, errors) {
15141
15261
  });
15142
15262
  }
15143
15263
  if (hasScript) {
15144
- const path24 = task.checkScript?.path;
15145
- if (!path24 || path24.trim().length === 0) {
15264
+ const path25 = task.checkScript?.path;
15265
+ if (!path25 || path25.trim().length === 0) {
15146
15266
  errors.push({ path: `${prefix}.checkScript.path`, message: "checkScript.path is required" });
15147
15267
  }
15148
15268
  if (task.checkScript?.timeoutMs !== void 0 && task.checkScript.timeoutMs <= 0) {
@@ -15268,9 +15388,9 @@ function handleEdge(top, next, color, stack, errors, reported) {
15268
15388
  stack.push({ id: next, nextIdx: 0, path: [...top.path, next] });
15269
15389
  }
15270
15390
  }
15271
- function reportCycle(path24, next, errors, reported) {
15272
- const cycleStart = path24.indexOf(next);
15273
- const cyclePath = cycleStart >= 0 ? [...path24.slice(cycleStart), next] : [...path24, next];
15391
+ function reportCycle(path25, next, errors, reported) {
15392
+ const cycleStart = path25.indexOf(next);
15393
+ const cyclePath = cycleStart >= 0 ? [...path25.slice(cycleStart), next] : [...path25, next];
15274
15394
  const key = cyclePath.join("\u2192");
15275
15395
  if (reported.has(key)) return;
15276
15396
  reported.add(key);
@@ -16758,7 +16878,9 @@ ${messages}`);
16758
16878
  await this.emitWorkerExit(issue.id, "error", attempt, "Stopped by reconciliation");
16759
16879
  }
16760
16880
  } else {
16761
- const outcomeClass = await this.deriveSingleAgentQualityVerdict(issue, workspacePath);
16881
+ const qualityClass = await this.deriveSingleAgentQualityVerdict(issue, workspacePath);
16882
+ const retroClass = await this.deriveRoutingRetrospectiveVerdict(issue, workspacePath);
16883
+ const outcomeClass = qualityClass ?? retroClass;
16762
16884
  await this.emitWorkerExit(issue.id, "normal", attempt, void 0, outcomeClass);
16763
16885
  }
16764
16886
  } catch (error) {
@@ -16860,6 +16982,150 @@ ${messages}`);
16860
16982
  return void 0;
16861
16983
  }
16862
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
+ }
16863
17129
  /**
16864
17130
  * Informs the state machine that an agent worker has exited.
16865
17131
  */
@@ -17946,6 +18212,341 @@ function launchTUI(orchestrator) {
17946
18212
  return { waitUntilExit };
17947
18213
  }
17948
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
+
17949
18550
  // src/maintenance/sync-main.ts
17950
18551
  import { execFile as nodeExecFile2 } from "child_process";
17951
18552
  import { promisify as promisify5 } from "util";
@@ -18139,8 +18740,8 @@ function selectTasks(tasks, history, filter) {
18139
18740
  }
18140
18741
 
18141
18742
  // src/sessions/search-index.ts
18142
- import * as fs16 from "fs";
18143
- import * as path22 from "path";
18743
+ import * as fs17 from "fs";
18744
+ import * as path23 from "path";
18144
18745
  import Database2 from "better-sqlite3";
18145
18746
  import { INDEXED_FILE_KINDS } from "@harness-engineering/types";
18146
18747
  var SEARCH_INDEX_FILE = "search-index.sqlite";
@@ -18185,7 +18786,7 @@ function normalizeFts5Query(query) {
18185
18786
  return query.split(/\s+/).filter((tok) => tok.length > 0).map((tok) => `"${tok.replace(/"/g, '""')}"`).join(" ");
18186
18787
  }
18187
18788
  function searchIndexPath(projectPath) {
18188
- return path22.join(projectPath, ".harness", SEARCH_INDEX_FILE);
18789
+ return path23.join(projectPath, ".harness", SEARCH_INDEX_FILE);
18189
18790
  }
18190
18791
  var FILE_KIND_TO_FILENAME = {
18191
18792
  summary: "summary.md",
@@ -18200,7 +18801,7 @@ var SqliteSearchIndex = class {
18200
18801
  removeSessionStmt;
18201
18802
  totalStmt;
18202
18803
  constructor(dbPath) {
18203
- fs16.mkdirSync(path22.dirname(dbPath), { recursive: true });
18804
+ fs17.mkdirSync(path23.dirname(dbPath), { recursive: true });
18204
18805
  this.db = new Database2(dbPath);
18205
18806
  this.db.pragma("journal_mode = WAL");
18206
18807
  this.db.pragma("synchronous = NORMAL");
@@ -18305,14 +18906,14 @@ function indexSessionDirectory(idx, args) {
18305
18906
  let docsWritten = 0;
18306
18907
  for (const kind of kinds) {
18307
18908
  const fileName = FILE_KIND_TO_FILENAME[kind];
18308
- const filePath = path22.join(args.sessionDir, fileName);
18309
- if (!fs16.existsSync(filePath)) continue;
18310
- let body = fs16.readFileSync(filePath, "utf8");
18909
+ const filePath = path23.join(args.sessionDir, fileName);
18910
+ if (!fs17.existsSync(filePath)) continue;
18911
+ let body = fs17.readFileSync(filePath, "utf8");
18311
18912
  if (Buffer.byteLength(body, "utf8") > cap) {
18312
18913
  body = body.slice(0, cap) + "\n\n[TRUNCATED]";
18313
18914
  }
18314
- const stat = fs16.statSync(filePath);
18315
- const relPath = path22.relative(args.projectPath, filePath).replaceAll("\\", "/");
18915
+ const stat = fs17.statSync(filePath);
18916
+ const relPath = path23.relative(args.projectPath, filePath).replaceAll("\\", "/");
18316
18917
  idx.upsertSessionDoc({
18317
18918
  sessionId: args.sessionId,
18318
18919
  archived: args.archived,
@@ -18327,17 +18928,17 @@ function indexSessionDirectory(idx, args) {
18327
18928
  }
18328
18929
  function reindexFromArchive(projectPath, opts = {}) {
18329
18930
  const start = Date.now();
18330
- const archiveBase = path22.join(projectPath, ".harness", "archive", "sessions");
18931
+ const archiveBase = path23.join(projectPath, ".harness", "archive", "sessions");
18331
18932
  const idx = openSearchIndex(projectPath);
18332
18933
  try {
18333
18934
  idx.resetArchived();
18334
18935
  let sessionsIndexed = 0;
18335
18936
  let docsWritten = 0;
18336
- if (fs16.existsSync(archiveBase)) {
18337
- const entries = fs16.readdirSync(archiveBase, { withFileTypes: true });
18937
+ if (fs17.existsSync(archiveBase)) {
18938
+ const entries = fs17.readdirSync(archiveBase, { withFileTypes: true });
18338
18939
  for (const entry of entries) {
18339
18940
  if (!entry.isDirectory()) continue;
18340
- const sessionDir = path22.join(archiveBase, entry.name);
18941
+ const sessionDir = path23.join(archiveBase, entry.name);
18341
18942
  const result = indexSessionDirectory(idx, {
18342
18943
  sessionId: entry.name,
18343
18944
  sessionDir,
@@ -18357,8 +18958,8 @@ function reindexFromArchive(projectPath, opts = {}) {
18357
18958
  }
18358
18959
 
18359
18960
  // src/sessions/summarize.ts
18360
- import * as fs17 from "fs";
18361
- import * as path23 from "path";
18961
+ import * as fs18 from "fs";
18962
+ import * as path24 from "path";
18362
18963
  import {
18363
18964
  SessionSummarySchema
18364
18965
  } from "@harness-engineering/types";
@@ -18388,10 +18989,10 @@ var USER_PROMPT_PREAMBLE = `Below are the archived files for a single harness-en
18388
18989
  function readInputCorpus(archiveDir) {
18389
18990
  const parts = [];
18390
18991
  for (const { filename, kind } of SUMMARY_INPUT_FILES) {
18391
- const p = path23.join(archiveDir, filename);
18392
- if (!fs17.existsSync(p)) continue;
18992
+ const p = path24.join(archiveDir, filename);
18993
+ if (!fs18.existsSync(p)) continue;
18393
18994
  try {
18394
- const content = fs17.readFileSync(p, "utf8");
18995
+ const content = fs18.readFileSync(p, "utf8");
18395
18996
  if (content.trim().length === 0) continue;
18396
18997
  parts.push(`## FILE: ${kind}
18397
18998
 
@@ -18442,7 +19043,7 @@ function renderLlmSummaryMarkdown(summary, meta) {
18442
19043
  return lines.join("\n");
18443
19044
  }
18444
19045
  function writeStubMarkdown(archiveDir, reason) {
18445
- const filePath = path23.join(archiveDir, LLM_SUMMARY_FILE);
19046
+ const filePath = path24.join(archiveDir, LLM_SUMMARY_FILE);
18446
19047
  const body = `---
18447
19048
  generatedAt: ${(/* @__PURE__ */ new Date()).toISOString()}
18448
19049
  schemaVersion: 1
@@ -18453,12 +19054,12 @@ status: failed
18453
19054
 
18454
19055
  - reason: ${reason}
18455
19056
  `;
18456
- fs17.writeFileSync(filePath, body, "utf8");
19057
+ fs18.writeFileSync(filePath, body, "utf8");
18457
19058
  return filePath;
18458
19059
  }
18459
19060
  async function summarizeArchivedSession(ctx) {
18460
19061
  const writeStubOnError = ctx.writeStubOnError ?? true;
18461
- if (!fs17.existsSync(ctx.archiveDir)) {
19062
+ if (!fs18.existsSync(ctx.archiveDir)) {
18462
19063
  return Err23(new Error(`archive directory not found: ${ctx.archiveDir}`));
18463
19064
  }
18464
19065
  const corpus = readInputCorpus(ctx.archiveDir);
@@ -18519,9 +19120,9 @@ async function summarizeArchivedSession(ctx) {
18519
19120
  outputTokens: response.tokenUsage.outputTokens,
18520
19121
  schemaVersion: 1
18521
19122
  };
18522
- const filePath = path23.join(ctx.archiveDir, LLM_SUMMARY_FILE);
19123
+ const filePath = path24.join(ctx.archiveDir, LLM_SUMMARY_FILE);
18523
19124
  const body = renderLlmSummaryMarkdown(parsed.data, meta);
18524
- fs17.writeFileSync(filePath, body, "utf8");
19125
+ fs18.writeFileSync(filePath, body, "utf8");
18525
19126
  return Ok24({ summary: parsed.data, meta, filePath });
18526
19127
  }
18527
19128
  function isSummaryEnabled(config) {
@@ -18602,6 +19203,7 @@ import { discoverCandidates } from "@harness-engineering/local-models";
18602
19203
  export {
18603
19204
  AdaptiveRouter,
18604
19205
  AnalysisArchive,
19206
+ BRAINSTORM_RUBRIC,
18605
19207
  BUILT_IN_TASKS,
18606
19208
  BackendDefSchema,
18607
19209
  BackendRouter,
@@ -18644,8 +19246,10 @@ export {
18644
19246
  WorkspaceManager,
18645
19247
  applyEvent,
18646
19248
  artifactPresenceFromIssue,
19249
+ brainstormInputFromIssue,
18647
19250
  buildArchiveHooks,
18648
19251
  buildCapabilityRegistry,
19252
+ buildProbeInput,
18649
19253
  buildWorkflowContext,
18650
19254
  calculateRetryDelay,
18651
19255
  canDispatch,
@@ -18665,6 +19269,7 @@ export {
18665
19269
  emitProposalApproved,
18666
19270
  emitProposalCreated,
18667
19271
  emitProposalRejected,
19272
+ enrichIssueWithSpec,
18668
19273
  estimateCost,
18669
19274
  explicitFindingsCount,
18670
19275
  extractHighlights,
@@ -18679,22 +19284,30 @@ export {
18679
19284
  launchTUI,
18680
19285
  loadPublishedIndex,
18681
19286
  makeBackendResolver,
19287
+ makeGraphScope,
19288
+ makeSelForkGenerator,
19289
+ markApprovedForDispatch,
18682
19290
  migrateAgentConfig,
18683
19291
  normalizeFts5Query,
18684
19292
  normalizeHarnessCommand,
18685
19293
  normalizeLocalModel,
18686
19294
  openSearchIndex,
19295
+ pilotScore,
19296
+ precedentLookupFromStored,
18687
19297
  promote,
19298
+ rankTriageCandidates,
18688
19299
  reconcile,
18689
19300
  recoverFindingsCount,
18690
19301
  reindexFromArchive,
18691
19302
  renderAnalysisComment,
18692
19303
  renderLlmSummaryMarkdown,
18693
19304
  renderPRComment,
19305
+ renderSpecMarkdown,
18694
19306
  resolveEscalationConfig,
18695
19307
  resolveOrchestratorId,
18696
19308
  routeIssue,
18697
19309
  routingWarnings,
19310
+ runBrainstormForIssue,
18698
19311
  runGate,
18699
19312
  runHarnessCheck,
18700
19313
  savePublishedIndex,
@@ -18702,10 +19315,11 @@ export {
18702
19315
  selectCandidates,
18703
19316
  selectCheapestQualifying,
18704
19317
  selectTasks,
19318
+ slugFor,
18705
19319
  sortCandidates,
18706
19320
  summarizeArchivedSession,
18707
19321
  syncMain,
18708
- triageIssue,
19322
+ triageIssue2 as triageIssue,
18709
19323
  truncateForBudget,
18710
19324
  validateCustomTasks,
18711
19325
  validateWorkflowConfig,