@harness-engineering/orchestrator 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2569,6 +2569,9 @@ var RoadmapTrackerAdapter = class {
2569
2569
  // src/tracker/extensions/linear.ts
2570
2570
  var import_types5 = require("@harness-engineering/types");
2571
2571
  var LINEAR_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql";
2572
+ function errorMessage(err) {
2573
+ return err instanceof Error ? err.message : String(err);
2574
+ }
2572
2575
  var LinearGraphQLClient = class {
2573
2576
  apiKey;
2574
2577
  endpoint;
@@ -2579,9 +2582,23 @@ var LinearGraphQLClient = class {
2579
2582
  this.fetchFn = opts.fetchFn ?? globalThis.fetch;
2580
2583
  }
2581
2584
  async query(query, variables) {
2582
- let res;
2585
+ const sent = await this.sendRequest(query, variables);
2586
+ if (!sent.ok) return sent;
2587
+ const res = sent.value;
2588
+ if (!res.ok) {
2589
+ return (0, import_types5.Err)(await this.httpError(res));
2590
+ }
2591
+ const parsed = await this.parseEnvelope(res);
2592
+ if (!parsed.ok) return parsed;
2593
+ const envelope = parsed.value;
2594
+ const graphqlError = envelopeError(envelope);
2595
+ if (graphqlError) return (0, import_types5.Err)(graphqlError);
2596
+ return (0, import_types5.Ok)(envelope.data ?? {});
2597
+ }
2598
+ /** POST the operation to Linear, normalizing transport throws into an `Err`. */
2599
+ async sendRequest(query, variables) {
2583
2600
  try {
2584
- res = await this.fetchFn(this.endpoint, {
2601
+ const res = await this.fetchFn(this.endpoint, {
2585
2602
  method: "POST",
2586
2603
  headers: {
2587
2604
  Authorization: this.apiKey,
@@ -2589,35 +2606,31 @@ var LinearGraphQLClient = class {
2589
2606
  },
2590
2607
  body: JSON.stringify({ query, variables: variables ?? {} })
2591
2608
  });
2609
+ return (0, import_types5.Ok)(res);
2592
2610
  } catch (err) {
2593
- return (0, import_types5.Err)(
2594
- new Error(
2595
- `Linear GraphQL request failed: ${err instanceof Error ? err.message : String(err)}`
2596
- )
2597
- );
2598
- }
2599
- if (!res.ok) {
2600
- const body = await res.text().catch(() => "");
2601
- const detail = body ? `: ${body.slice(0, 500)}` : "";
2602
- return (0, import_types5.Err)(new Error(`Linear GraphQL HTTP ${res.status}${detail}`));
2611
+ return (0, import_types5.Err)(new Error(`Linear GraphQL request failed: ${errorMessage(err)}`));
2603
2612
  }
2604
- let envelope;
2613
+ }
2614
+ /** Build the error for a non-2xx HTTP response, including a truncated body. */
2615
+ async httpError(res) {
2616
+ const body = await res.text().catch(() => "");
2617
+ const detail = body ? `: ${body.slice(0, 500)}` : "";
2618
+ return new Error(`Linear GraphQL HTTP ${res.status}${detail}`);
2619
+ }
2620
+ /** Parse the JSON envelope, normalizing parse failures into an `Err`. */
2621
+ async parseEnvelope(res) {
2605
2622
  try {
2606
- envelope = await res.json();
2623
+ return (0, import_types5.Ok)(await res.json());
2607
2624
  } catch (err) {
2608
- return (0, import_types5.Err)(
2609
- new Error(
2610
- `Linear GraphQL response was not valid JSON: ${err instanceof Error ? err.message : String(err)}`
2611
- )
2612
- );
2613
- }
2614
- if (envelope.errors && envelope.errors.length > 0) {
2615
- const message = envelope.errors.map((e) => e.message ?? "unknown error").join("; ");
2616
- return (0, import_types5.Err)(new Error(`Linear GraphQL error: ${message}`));
2625
+ return (0, import_types5.Err)(new Error(`Linear GraphQL response was not valid JSON: ${errorMessage(err)}`));
2617
2626
  }
2618
- return (0, import_types5.Ok)(envelope.data ?? {});
2619
2627
  }
2620
2628
  };
2629
+ function envelopeError(envelope) {
2630
+ if (!envelope.errors || envelope.errors.length === 0) return void 0;
2631
+ const message = envelope.errors.map((e) => e.message ?? "unknown error").join("; ");
2632
+ return new Error(`Linear GraphQL error: ${message}`);
2633
+ }
2621
2634
  var LinearGraphQLStub = class {
2622
2635
  async query(query, _variables) {
2623
2636
  console.log("Linear GraphQL query (stub):", query);
@@ -4805,18 +4818,18 @@ var AnthropicBackend = class {
4805
4818
  usage
4806
4819
  };
4807
4820
  } catch (err) {
4808
- const errorMessage = err instanceof Error ? err.message : "Anthropic request failed";
4821
+ const errorMessage2 = err instanceof Error ? err.message : "Anthropic request failed";
4809
4822
  yield {
4810
4823
  type: "error",
4811
4824
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4812
- content: errorMessage,
4825
+ content: errorMessage2,
4813
4826
  sessionId: session.sessionId
4814
4827
  };
4815
4828
  return {
4816
4829
  success: false,
4817
4830
  sessionId: session.sessionId,
4818
4831
  usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
4819
- error: errorMessage
4832
+ error: errorMessage2
4820
4833
  };
4821
4834
  }
4822
4835
  }
@@ -4907,18 +4920,18 @@ var OpenAIBackend = class {
4907
4920
  }
4908
4921
  }
4909
4922
  } catch (err) {
4910
- const errorMessage = err instanceof Error ? err.message : "OpenAI request failed";
4923
+ const errorMessage2 = err instanceof Error ? err.message : "OpenAI request failed";
4911
4924
  yield {
4912
4925
  type: "error",
4913
4926
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4914
- content: errorMessage,
4927
+ content: errorMessage2,
4915
4928
  sessionId: session.sessionId
4916
4929
  };
4917
4930
  return {
4918
4931
  success: false,
4919
4932
  sessionId: session.sessionId,
4920
4933
  usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
4921
- error: errorMessage
4934
+ error: errorMessage2
4922
4935
  };
4923
4936
  }
4924
4937
  const usage = {
@@ -5029,11 +5042,11 @@ var GeminiBackend = class {
5029
5042
  }
5030
5043
  }
5031
5044
  } catch (err) {
5032
- const errorMessage = err instanceof Error ? err.message : "Gemini request failed";
5045
+ const errorMessage2 = err instanceof Error ? err.message : "Gemini request failed";
5033
5046
  yield {
5034
5047
  type: "error",
5035
5048
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5036
- content: errorMessage,
5049
+ content: errorMessage2,
5037
5050
  sessionId: session.sessionId
5038
5051
  };
5039
5052
  return {
@@ -5046,7 +5059,7 @@ var GeminiBackend = class {
5046
5059
  cacheCreationTokens,
5047
5060
  cacheReadTokens
5048
5061
  },
5049
- error: errorMessage
5062
+ error: errorMessage2
5050
5063
  };
5051
5064
  }
5052
5065
  const usage = {
@@ -5172,18 +5185,18 @@ var LocalBackend = class {
5172
5185
  }
5173
5186
  }
5174
5187
  } catch (err) {
5175
- const errorMessage = err instanceof Error ? err.message : "Local backend request failed";
5188
+ const errorMessage2 = err instanceof Error ? err.message : "Local backend request failed";
5176
5189
  yield {
5177
5190
  type: "error",
5178
5191
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5179
- content: errorMessage,
5192
+ content: errorMessage2,
5180
5193
  sessionId: session.sessionId
5181
5194
  };
5182
5195
  return {
5183
5196
  success: false,
5184
5197
  sessionId: session.sessionId,
5185
5198
  usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
5186
- error: errorMessage
5199
+ error: errorMessage2
5187
5200
  };
5188
5201
  }
5189
5202
  const usage = { inputTokens, outputTokens, totalTokens };
@@ -5519,30 +5532,8 @@ var SshBackend = class {
5519
5532
  config;
5520
5533
  spawnImpl;
5521
5534
  constructor(config) {
5522
- if (!config.host || typeof config.host !== "string") {
5523
- throw new Error("SshBackend: `host` is required");
5524
- }
5525
- if (FORBIDDEN_HOST_CHARS.test(config.host) || config.host.startsWith("-")) {
5526
- throw new Error(
5527
- `SshBackend: invalid host '${config.host}' (contains shell metacharacters or starts with '-')`
5528
- );
5529
- }
5530
- if (!config.remoteCommand || typeof config.remoteCommand !== "string") {
5531
- throw new Error("SshBackend: `remoteCommand` is required");
5532
- }
5533
- if (config.user !== void 0 && /[\s;&|`$]/.test(config.user)) {
5534
- throw new Error(`SshBackend: invalid user '${config.user}'`);
5535
- }
5536
- this.config = {
5537
- host: config.host,
5538
- remoteCommand: config.remoteCommand,
5539
- sshBinary: config.sshBinary ?? "ssh",
5540
- sshOptions: config.sshOptions ?? [],
5541
- timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
5542
- ...config.user !== void 0 ? { user: config.user } : {},
5543
- ...config.port !== void 0 ? { port: config.port } : {},
5544
- ...config.identityFile !== void 0 ? { identityFile: config.identityFile } : {}
5545
- };
5535
+ validateSshConfig(config);
5536
+ this.config = normalizeSshConfig(config);
5546
5537
  this.spawnImpl = config.spawnImpl ?? import_node_child_process5.spawn;
5547
5538
  }
5548
5539
  /**
@@ -5694,6 +5685,34 @@ var SshBackend = class {
5694
5685
  });
5695
5686
  }
5696
5687
  };
5688
+ function validateSshConfig(config) {
5689
+ if (!config.host || typeof config.host !== "string") {
5690
+ throw new Error("SshBackend: `host` is required");
5691
+ }
5692
+ if (FORBIDDEN_HOST_CHARS.test(config.host) || config.host.startsWith("-")) {
5693
+ throw new Error(
5694
+ `SshBackend: invalid host '${config.host}' (contains shell metacharacters or starts with '-')`
5695
+ );
5696
+ }
5697
+ if (!config.remoteCommand || typeof config.remoteCommand !== "string") {
5698
+ throw new Error("SshBackend: `remoteCommand` is required");
5699
+ }
5700
+ if (config.user !== void 0 && /[\s;&|`$]/.test(config.user)) {
5701
+ throw new Error(`SshBackend: invalid user '${config.user}'`);
5702
+ }
5703
+ }
5704
+ function normalizeSshConfig(config) {
5705
+ return {
5706
+ host: config.host,
5707
+ remoteCommand: config.remoteCommand,
5708
+ sshBinary: config.sshBinary ?? "ssh",
5709
+ sshOptions: config.sshOptions ?? [],
5710
+ timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
5711
+ ...config.user !== void 0 ? { user: config.user } : {},
5712
+ ...config.port !== void 0 ? { port: config.port } : {},
5713
+ ...config.identityFile !== void 0 ? { identityFile: config.identityFile } : {}
5714
+ };
5715
+ }
5697
5716
  function errResult(sessionId, message) {
5698
5717
  return {
5699
5718
  success: false,
@@ -5800,23 +5819,8 @@ var OciServerlessBackend = class extends ServerlessBackend {
5800
5819
  envSource;
5801
5820
  constructor(config) {
5802
5821
  super();
5803
- if (!config.image || typeof config.image !== "string") {
5804
- throw new Error("OciServerlessBackend: `image` is required");
5805
- }
5806
- if (FORBIDDEN_IMAGE_CHARS.test(config.image) || config.image.startsWith("-")) {
5807
- throw new Error(
5808
- `OciServerlessBackend: invalid image '${config.image}' (contains shell metacharacters or starts with '-')`
5809
- );
5810
- }
5811
- this.config = {
5812
- image: config.image,
5813
- pullPolicy: config.pullPolicy ?? "if-not-present",
5814
- runtime: config.runtime ?? "docker",
5815
- envPassthrough: config.envPassthrough ?? [],
5816
- timeoutMs: config.timeoutMs ?? DEFAULT_OCI_TIMEOUT_MS,
5817
- extraArgs: sanitizeExtraArgs(config.extraArgs),
5818
- ...config.registry !== void 0 ? { registry: config.registry } : {}
5819
- };
5822
+ validateOciImage(config.image);
5823
+ this.config = resolveOciConfig(config);
5820
5824
  this.spawnImpl = config.spawnImpl ?? import_node_child_process6.spawn;
5821
5825
  this.envSource = config.envSource ?? process.env;
5822
5826
  }
@@ -5979,6 +5983,27 @@ var OciServerlessBackend = class extends ServerlessBackend {
5979
5983
  });
5980
5984
  }
5981
5985
  };
5986
+ function validateOciImage(image) {
5987
+ if (!image || typeof image !== "string") {
5988
+ throw new Error("OciServerlessBackend: `image` is required");
5989
+ }
5990
+ if (FORBIDDEN_IMAGE_CHARS.test(image) || image.startsWith("-")) {
5991
+ throw new Error(
5992
+ `OciServerlessBackend: invalid image '${image}' (contains shell metacharacters or starts with '-')`
5993
+ );
5994
+ }
5995
+ }
5996
+ function resolveOciConfig(config) {
5997
+ return {
5998
+ image: config.image,
5999
+ pullPolicy: config.pullPolicy ?? "if-not-present",
6000
+ runtime: config.runtime ?? "docker",
6001
+ envPassthrough: config.envPassthrough ?? [],
6002
+ timeoutMs: config.timeoutMs ?? DEFAULT_OCI_TIMEOUT_MS,
6003
+ extraArgs: sanitizeExtraArgs(config.extraArgs),
6004
+ ...config.registry !== void 0 ? { registry: config.registry } : {}
6005
+ };
6006
+ }
5982
6007
  function sanitizeExtraArgs(extraArgs) {
5983
6008
  if (!extraArgs) return [];
5984
6009
  return extraArgs.filter((arg) => !BLOCKED_DOCKER_FLAGS.some((flag) => arg.startsWith(flag)));
@@ -6050,78 +6075,96 @@ function makeGetModel(model) {
6050
6075
  if (Array.isArray(model) && model.length > 0) return () => model[0] ?? null;
6051
6076
  return () => null;
6052
6077
  }
6078
+ function createClaudeBackend(def, options) {
6079
+ return new ClaudeBackend(def.command ?? "claude", {
6080
+ ...options.cacheMetrics ? { cacheMetrics: options.cacheMetrics } : {}
6081
+ });
6082
+ }
6083
+ function createAnthropicBackend(def) {
6084
+ return new AnthropicBackend({
6085
+ model: def.model,
6086
+ ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
6087
+ });
6088
+ }
6089
+ function createOpenAIBackend(def) {
6090
+ return new OpenAIBackend({
6091
+ model: def.model,
6092
+ ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
6093
+ });
6094
+ }
6095
+ function createGeminiBackend(def) {
6096
+ return new GeminiBackend({
6097
+ model: def.model,
6098
+ ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
6099
+ });
6100
+ }
6101
+ function createLocalBackend(def) {
6102
+ const isArray = Array.isArray(def.model);
6103
+ return new LocalBackend({
6104
+ endpoint: def.endpoint,
6105
+ ...typeof def.model === "string" ? { model: def.model } : {},
6106
+ ...isArray ? { getModel: makeGetModel(def.model) } : {},
6107
+ ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
6108
+ ...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
6109
+ });
6110
+ }
6111
+ function createPiBackend(def) {
6112
+ const isArray = Array.isArray(def.model);
6113
+ return new PiBackend({
6114
+ endpoint: def.endpoint,
6115
+ ...typeof def.model === "string" ? { model: def.model } : {},
6116
+ ...isArray ? { getModel: makeGetModel(def.model) } : {},
6117
+ ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
6118
+ ...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
6119
+ });
6120
+ }
6121
+ function createSshBackend(def) {
6122
+ return new SshBackend({
6123
+ host: def.host,
6124
+ remoteCommand: def.remoteCommand,
6125
+ ...def.user !== void 0 ? { user: def.user } : {},
6126
+ ...def.port !== void 0 ? { port: def.port } : {},
6127
+ ...def.identityFile !== void 0 ? { identityFile: def.identityFile } : {},
6128
+ ...def.sshOptions !== void 0 ? { sshOptions: def.sshOptions } : {},
6129
+ ...def.sshBinary !== void 0 ? { sshBinary: def.sshBinary } : {}
6130
+ });
6131
+ }
6132
+ function createServerlessBackend(def) {
6133
+ switch (def.adapter) {
6134
+ case "oci":
6135
+ return new OciServerlessBackend({
6136
+ image: def.image,
6137
+ ...def.registry !== void 0 ? { registry: def.registry } : {},
6138
+ ...def.pullPolicy !== void 0 ? { pullPolicy: def.pullPolicy } : {},
6139
+ ...def.envPassthrough !== void 0 ? { envPassthrough: def.envPassthrough } : {},
6140
+ ...def.runtime !== void 0 ? { runtime: def.runtime } : {}
6141
+ });
6142
+ default: {
6143
+ const exhaustive = def.adapter;
6144
+ throw new Error(`createBackend: unknown serverless adapter ${JSON.stringify(exhaustive)}`);
6145
+ }
6146
+ }
6147
+ }
6053
6148
  function createBackend(def, options = {}) {
6054
6149
  switch (def.type) {
6055
6150
  case "mock":
6056
6151
  return new MockBackend();
6057
6152
  case "claude":
6058
- return new ClaudeBackend(def.command ?? "claude", {
6059
- ...options.cacheMetrics ? { cacheMetrics: options.cacheMetrics } : {}
6060
- });
6153
+ return createClaudeBackend(def, options);
6061
6154
  case "anthropic":
6062
- return new AnthropicBackend({
6063
- model: def.model,
6064
- ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
6065
- });
6155
+ return createAnthropicBackend(def);
6066
6156
  case "openai":
6067
- return new OpenAIBackend({
6068
- model: def.model,
6069
- ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
6070
- });
6157
+ return createOpenAIBackend(def);
6071
6158
  case "gemini":
6072
- return new GeminiBackend({
6073
- model: def.model,
6074
- ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {}
6075
- });
6076
- case "local": {
6077
- const isArray = Array.isArray(def.model);
6078
- return new LocalBackend({
6079
- endpoint: def.endpoint,
6080
- ...typeof def.model === "string" ? { model: def.model } : {},
6081
- ...isArray ? { getModel: makeGetModel(def.model) } : {},
6082
- ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
6083
- ...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
6084
- });
6085
- }
6086
- case "pi": {
6087
- const isArray = Array.isArray(def.model);
6088
- return new PiBackend({
6089
- endpoint: def.endpoint,
6090
- ...typeof def.model === "string" ? { model: def.model } : {},
6091
- ...isArray ? { getModel: makeGetModel(def.model) } : {},
6092
- ...def.apiKey !== void 0 ? { apiKey: def.apiKey } : {},
6093
- ...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
6094
- });
6095
- }
6096
- case "ssh": {
6097
- return new SshBackend({
6098
- host: def.host,
6099
- remoteCommand: def.remoteCommand,
6100
- ...def.user !== void 0 ? { user: def.user } : {},
6101
- ...def.port !== void 0 ? { port: def.port } : {},
6102
- ...def.identityFile !== void 0 ? { identityFile: def.identityFile } : {},
6103
- ...def.sshOptions !== void 0 ? { sshOptions: def.sshOptions } : {},
6104
- ...def.sshBinary !== void 0 ? { sshBinary: def.sshBinary } : {}
6105
- });
6106
- }
6107
- case "serverless": {
6108
- switch (def.adapter) {
6109
- case "oci":
6110
- return new OciServerlessBackend({
6111
- image: def.image,
6112
- ...def.registry !== void 0 ? { registry: def.registry } : {},
6113
- ...def.pullPolicy !== void 0 ? { pullPolicy: def.pullPolicy } : {},
6114
- ...def.envPassthrough !== void 0 ? { envPassthrough: def.envPassthrough } : {},
6115
- ...def.runtime !== void 0 ? { runtime: def.runtime } : {}
6116
- });
6117
- default: {
6118
- const exhaustive = def.adapter;
6119
- throw new Error(
6120
- `createBackend: unknown serverless adapter ${JSON.stringify(exhaustive)}`
6121
- );
6122
- }
6123
- }
6124
- }
6159
+ return createGeminiBackend(def);
6160
+ case "local":
6161
+ return createLocalBackend(def);
6162
+ case "pi":
6163
+ return createPiBackend(def);
6164
+ case "ssh":
6165
+ return createSshBackend(def);
6166
+ case "serverless":
6167
+ return createServerlessBackend(def);
6125
6168
  default: {
6126
6169
  const exhaustive = def;
6127
6170
  throw new Error(`createBackend: unknown backend type ${JSON.stringify(exhaustive)}`);
@@ -9668,26 +9711,41 @@ function hasScope(held, required) {
9668
9711
  if (held.includes("admin")) return true;
9669
9712
  return held.includes(required);
9670
9713
  }
9671
- function requiredScopeForRoute(method, path24) {
9672
- const bridgeScope = requiredBridgeScope(method, path24);
9673
- if (bridgeScope) return bridgeScope;
9714
+ function exactScopeForRoute(method, path24) {
9674
9715
  if (path24 === "/api/v1/auth/token" && method === "POST") return "admin";
9675
9716
  if (path24 === "/api/v1/auth/tokens" && method === "GET") return "admin";
9676
9717
  if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path24) && method === "DELETE") return "admin";
9677
9718
  if ((path24 === "/api/state" || path24 === "/api/v1/state") && method === "GET") return "read-status";
9678
- if (path24.startsWith("/api/interactions")) return "resolve-interaction";
9679
- if (path24.startsWith("/api/plans")) return "read-status";
9680
- if (path24.startsWith("/api/analyze") || path24.startsWith("/api/analyses")) return "read-status";
9681
- if (path24.startsWith("/api/roadmap-actions")) return "modify-roadmap";
9682
- if (path24.startsWith("/api/dispatch-actions")) return "trigger-job";
9683
- if (path24.startsWith("/api/local-model") || path24.startsWith("/api/local-models"))
9684
- return "read-status";
9685
- if (path24.startsWith("/api/maintenance")) return "trigger-job";
9686
- if (path24.startsWith("/api/streams")) return "read-status";
9687
- if (path24.startsWith("/api/sessions")) return "read-status";
9688
- if (path24 === "/api/chat" || path24.startsWith("/api/chat-proxy")) return "trigger-job";
9689
9719
  return null;
9690
9720
  }
9721
+ var PREFIX_SCOPES = [
9722
+ ["/api/interactions", "resolve-interaction"],
9723
+ ["/api/plans", "read-status"],
9724
+ ["/api/analyze", "read-status"],
9725
+ ["/api/analyses", "read-status"],
9726
+ ["/api/roadmap-actions", "modify-roadmap"],
9727
+ ["/api/dispatch-actions", "trigger-job"],
9728
+ ["/api/local-model", "read-status"],
9729
+ ["/api/local-models", "read-status"],
9730
+ ["/api/maintenance", "trigger-job"],
9731
+ ["/api/streams", "read-status"],
9732
+ ["/api/sessions", "read-status"],
9733
+ ["/api/chat-proxy", "trigger-job"]
9734
+ ];
9735
+ function prefixScopeForPath(path24) {
9736
+ if (path24 === "/api/chat") return "trigger-job";
9737
+ for (const [prefix, scope] of PREFIX_SCOPES) {
9738
+ if (path24.startsWith(prefix)) return scope;
9739
+ }
9740
+ return null;
9741
+ }
9742
+ function requiredScopeForRoute(method, path24) {
9743
+ const bridgeScope = requiredBridgeScope(method, path24);
9744
+ if (bridgeScope) return bridgeScope;
9745
+ const exactScope = exactScopeForRoute(method, path24);
9746
+ if (exactScope) return exactScope;
9747
+ return prefixScopeForPath(path24);
9748
+ }
9691
9749
 
9692
9750
  // src/server/http.ts
9693
9751
  var RATE_LIMIT = Number(process.env["HARNESS_RATE_LIMIT"]) || 100;
@@ -10646,79 +10704,102 @@ function buildAttributes(payload, extras = {}) {
10646
10704
  }
10647
10705
  return attrs;
10648
10706
  }
10707
+ function extractIds(payload) {
10708
+ const correlationId = typeof payload["correlationId"] === "string" ? payload["correlationId"] : void 0;
10709
+ const taskId = typeof payload["taskId"] === "string" ? payload["taskId"] : void 0;
10710
+ return { correlationId, taskId };
10711
+ }
10712
+ function resolveActiveRun(registry, correlationId, taskId) {
10713
+ return registry.resolve({
10714
+ ...correlationId !== void 0 ? { correlationId } : {},
10715
+ ...taskId !== void 0 ? { taskId } : {}
10716
+ });
10717
+ }
10718
+ function openMaintenanceSpan(registry, correlationId, taskId) {
10719
+ const traceId = newTraceId();
10720
+ const spanId = newSpanId();
10721
+ const key = correlationId ?? taskId ?? `run_${spanId}`;
10722
+ registry.open(key, { traceId, spanId });
10723
+ return { traceId, spanId };
10724
+ }
10725
+ function closeMaintenanceSpan(registry, topic, correlationId, taskId) {
10726
+ const existing = resolveActiveRun(registry, correlationId, taskId);
10727
+ const traceId = existing?.traceId ?? newTraceId();
10728
+ const spanId = existing?.spanId ?? newSpanId();
10729
+ const statusCode = topic === TOPICS.MAINTENANCE_ERROR ? 2 : 1;
10730
+ const key = correlationId ?? taskId ?? "";
10731
+ if (key) registry.close(key);
10732
+ return { traceId, spanId, statusCode };
10733
+ }
10734
+ function childSpan(registry, correlationId, taskId) {
10735
+ const parent = resolveActiveRun(registry, correlationId, taskId);
10736
+ const plan = {
10737
+ traceId: parent?.traceId ?? newTraceId(),
10738
+ spanId: newSpanId()
10739
+ };
10740
+ if (parent?.spanId !== void 0) plan.parentSpanId = parent.spanId;
10741
+ return plan;
10742
+ }
10743
+ function planSpan(registry, topic, correlationId, taskId) {
10744
+ if (topic === TOPICS.MAINTENANCE_STARTED) {
10745
+ return openMaintenanceSpan(registry, correlationId, taskId);
10746
+ }
10747
+ if (topic === TOPICS.MAINTENANCE_COMPLETED || topic === TOPICS.MAINTENANCE_ERROR) {
10748
+ return closeMaintenanceSpan(registry, topic, correlationId, taskId);
10749
+ }
10750
+ return childSpan(registry, correlationId, taskId);
10751
+ }
10752
+ function buildSpan(topic, plan, payload) {
10753
+ const startNs = nowNs();
10754
+ return {
10755
+ traceId: plan.traceId,
10756
+ spanId: plan.spanId,
10757
+ ...plan.parentSpanId !== void 0 ? { parentSpanId: plan.parentSpanId } : {},
10758
+ name: SPAN_NAME[topic],
10759
+ kind: import_core12.SpanKind.INTERNAL,
10760
+ startTimeNs: startNs,
10761
+ endTimeNs: startNs,
10762
+ attributes: buildAttributes(payload, { "harness.topic": topic }),
10763
+ ...plan.statusCode !== void 0 ? { statusCode: plan.statusCode } : {}
10764
+ };
10765
+ }
10766
+ function buildGatewayEvent(topic, payload, correlationId) {
10767
+ return {
10768
+ id: newEventId2(),
10769
+ type: TELEMETRY_TYPE[topic],
10770
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10771
+ data: payload,
10772
+ ...correlationId !== void 0 ? { correlationId } : {}
10773
+ };
10774
+ }
10775
+ async function enqueueToMatchingSubs(store, webhookDelivery, event) {
10776
+ const subs = await store.listForEvent(event.type);
10777
+ if (subs.length === 0) return;
10778
+ const filtered = subs.filter((sub) => sub.events.some((p) => eventMatches(p, event.type)));
10779
+ for (const sub of filtered) {
10780
+ webhookDelivery.enqueue(sub, event);
10781
+ }
10782
+ }
10783
+ function makeTelemetryHandler(ctx, topic) {
10784
+ return (data) => {
10785
+ const payload = data ?? {};
10786
+ const { correlationId, taskId } = extractIds(payload);
10787
+ const plan = planSpan(ctx.registry, topic, correlationId, taskId);
10788
+ ctx.exporter.push(buildSpan(topic, plan, payload));
10789
+ void enqueueToMatchingSubs(
10790
+ ctx.store,
10791
+ ctx.webhookDelivery,
10792
+ buildGatewayEvent(topic, payload, correlationId)
10793
+ );
10794
+ };
10795
+ }
10649
10796
  function wireTelemetryFanout(params) {
10650
10797
  const { bus, exporter, webhookDelivery, store } = params;
10651
10798
  const registry = new ActiveRunRegistry();
10652
10799
  const handlers = [];
10653
- const enqueueToMatchingSubs = async (event) => {
10654
- const subs = await store.listForEvent(event.type);
10655
- if (subs.length === 0) return;
10656
- const filtered = subs.filter((sub) => sub.events.some((p) => eventMatches(p, event.type)));
10657
- for (const sub of filtered) {
10658
- webhookDelivery.enqueue(sub, event);
10659
- }
10660
- };
10661
- const makeHandler = (topic) => (data) => {
10662
- const payload = data ?? {};
10663
- const correlationId = typeof payload["correlationId"] === "string" ? payload["correlationId"] : void 0;
10664
- const taskId = typeof payload["taskId"] === "string" ? payload["taskId"] : void 0;
10665
- let traceId;
10666
- let spanId;
10667
- let parentSpanId;
10668
- let statusCode;
10669
- if (topic === TOPICS.MAINTENANCE_STARTED) {
10670
- traceId = newTraceId();
10671
- spanId = newSpanId();
10672
- const key = correlationId ?? taskId ?? `run_${spanId}`;
10673
- registry.open(key, { traceId, spanId });
10674
- } else if (topic === TOPICS.MAINTENANCE_COMPLETED || topic === TOPICS.MAINTENANCE_ERROR) {
10675
- const existing = registry.resolve({
10676
- ...correlationId !== void 0 ? { correlationId } : {},
10677
- ...taskId !== void 0 ? { taskId } : {}
10678
- });
10679
- if (existing !== void 0) {
10680
- traceId = existing.traceId;
10681
- spanId = existing.spanId;
10682
- } else {
10683
- traceId = newTraceId();
10684
- spanId = newSpanId();
10685
- }
10686
- statusCode = topic === TOPICS.MAINTENANCE_ERROR ? 2 : 1;
10687
- const key = correlationId ?? taskId ?? "";
10688
- if (key) registry.close(key);
10689
- } else {
10690
- const parent = registry.resolve({
10691
- ...correlationId !== void 0 ? { correlationId } : {},
10692
- ...taskId !== void 0 ? { taskId } : {}
10693
- });
10694
- traceId = parent?.traceId ?? newTraceId();
10695
- spanId = newSpanId();
10696
- parentSpanId = parent?.spanId;
10697
- }
10698
- const startNs = nowNs();
10699
- const span = {
10700
- traceId,
10701
- spanId,
10702
- ...parentSpanId !== void 0 ? { parentSpanId } : {},
10703
- name: SPAN_NAME[topic],
10704
- kind: import_core12.SpanKind.INTERNAL,
10705
- startTimeNs: startNs,
10706
- endTimeNs: startNs,
10707
- attributes: buildAttributes(payload, { "harness.topic": topic }),
10708
- ...statusCode !== void 0 ? { statusCode } : {}
10709
- };
10710
- exporter.push(span);
10711
- const gatewayEvent = {
10712
- id: newEventId2(),
10713
- type: TELEMETRY_TYPE[topic],
10714
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10715
- data: payload,
10716
- ...correlationId !== void 0 ? { correlationId } : {}
10717
- };
10718
- void enqueueToMatchingSubs(gatewayEvent);
10719
- };
10800
+ const ctx = { registry, exporter, webhookDelivery, store };
10720
10801
  for (const topic of Object.values(TOPICS)) {
10721
- const fn = makeHandler(topic);
10802
+ const fn = makeTelemetryHandler(ctx, topic);
10722
10803
  bus.on(topic, fn);
10723
10804
  handlers.push({ topic, fn });
10724
10805
  }
@@ -12413,39 +12494,50 @@ function classifyCheckExecutionFailure(output) {
12413
12494
  function parseStatusLine(output) {
12414
12495
  const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
12415
12496
  for (let i = lines.length - 1; i >= 0; i--) {
12416
- const line = lines[i];
12417
- if (!line || !line.startsWith("{") || !line.endsWith("}")) continue;
12418
- try {
12419
- const obj = JSON.parse(line);
12420
- const s = obj.status;
12421
- if (s === "success" || s === "skipped" || s === "failure" || s === "no-issues") {
12422
- const parsed = { status: s, rawStatus: s };
12423
- if (typeof obj.candidatesFound === "number") {
12424
- parsed.candidatesFound = obj.candidatesFound;
12425
- }
12426
- if (typeof obj.error === "string") {
12427
- parsed.error = obj.error;
12428
- }
12429
- if (typeof obj.reason === "string") {
12430
- parsed.reason = obj.reason;
12431
- }
12432
- if (typeof obj.detail === "string" && !parsed.error) {
12433
- parsed.error = `${parsed.reason ?? "skipped"}: ${obj.detail}`;
12434
- }
12435
- return parsed;
12436
- }
12437
- if (s === "updated" || s === "no-op") {
12438
- return { status: "success", rawStatus: s };
12439
- }
12440
- if (s === "error") {
12441
- const message = typeof obj.message === "string" ? obj.message : "unknown error";
12442
- return { status: "failure", error: message, rawStatus: "error" };
12443
- }
12444
- } catch {
12445
- }
12497
+ const parsed = parseStatusObjectLine(lines[i]);
12498
+ if (parsed) return parsed;
12446
12499
  }
12447
12500
  return null;
12448
12501
  }
12502
+ function parseStatusObjectLine(line) {
12503
+ if (!line || !line.startsWith("{") || !line.endsWith("}")) return null;
12504
+ let obj;
12505
+ try {
12506
+ obj = JSON.parse(line);
12507
+ } catch {
12508
+ return null;
12509
+ }
12510
+ return classifyStatusObject(obj);
12511
+ }
12512
+ var PHASE_STATUSES = /* @__PURE__ */ new Set(["success", "skipped", "failure", "no-issues"]);
12513
+ var SYNC_SUCCESS_STATUSES = /* @__PURE__ */ new Set(["updated", "no-op"]);
12514
+ function classifyStatusObject(obj) {
12515
+ const s = obj.status;
12516
+ if (typeof s !== "string") return null;
12517
+ if (PHASE_STATUSES.has(s)) return buildPhaseStatus(s, obj);
12518
+ if (SYNC_SUCCESS_STATUSES.has(s)) return { status: "success", rawStatus: s };
12519
+ if (s === "error") {
12520
+ const message = typeof obj.message === "string" ? obj.message : "unknown error";
12521
+ return { status: "failure", error: message, rawStatus: "error" };
12522
+ }
12523
+ return null;
12524
+ }
12525
+ function buildPhaseStatus(status, obj) {
12526
+ const parsed = { status, rawStatus: status };
12527
+ if (typeof obj.candidatesFound === "number") {
12528
+ parsed.candidatesFound = obj.candidatesFound;
12529
+ }
12530
+ if (typeof obj.error === "string") {
12531
+ parsed.error = obj.error;
12532
+ }
12533
+ if (typeof obj.reason === "string") {
12534
+ parsed.reason = obj.reason;
12535
+ }
12536
+ if (typeof obj.detail === "string" && !parsed.error) {
12537
+ parsed.error = `${parsed.reason ?? "skipped"}: ${obj.detail}`;
12538
+ }
12539
+ return parsed;
12540
+ }
12449
12541
 
12450
12542
  // src/maintenance/check-script-runner.ts
12451
12543
  var import_node_child_process11 = require("child_process");
@@ -13340,22 +13432,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
13340
13432
  });
13341
13433
  webhookDelivery.start();
13342
13434
  this.setupNotifications(config.notifications);
13343
- const otlpCfg = config.telemetry?.export?.otlp;
13344
- if (otlpCfg) {
13345
- this.otlpExporter = new import_core17.OTLPExporter({
13346
- endpoint: otlpCfg.endpoint,
13347
- ...otlpCfg.enabled !== void 0 ? { enabled: otlpCfg.enabled } : {},
13348
- ...otlpCfg.headers !== void 0 ? { headers: otlpCfg.headers } : {},
13349
- ...otlpCfg.flushIntervalMs !== void 0 ? { flushIntervalMs: otlpCfg.flushIntervalMs } : {},
13350
- ...otlpCfg.batchSize !== void 0 ? { batchSize: otlpCfg.batchSize } : {}
13351
- });
13352
- this.telemetryFanoutOff = wireTelemetryFanout({
13353
- bus: this,
13354
- exporter: this.otlpExporter,
13355
- webhookDelivery,
13356
- store: webhookStore
13357
- });
13358
- }
13435
+ this.setupTelemetryExport(config, webhookStore, webhookDelivery);
13359
13436
  this.server = new OrchestratorServer(this, config.server.port, {
13360
13437
  interactionQueue: this.interactionQueue,
13361
13438
  webhooks: {
@@ -13378,24 +13455,8 @@ var Orchestrator = class extends import_node_events.EventEmitter {
13378
13455
  analysisArchive: this.analysisArchive,
13379
13456
  roadmapPath: config.tracker.filePath ?? null,
13380
13457
  dispatchAdHoc: this.dispatchAdHoc.bind(this),
13381
- getLocalModelStatus: () => {
13382
- const first = this.localResolvers.values().next();
13383
- return first.done ? null : first.value.getStatus();
13384
- },
13385
- getLocalModelStatuses: () => {
13386
- const backends = this.config.agent.backends ?? {};
13387
- const out = [];
13388
- for (const [name, resolver] of this.localResolvers) {
13389
- const def = backends[name];
13390
- if (!def || def.type !== "local" && def.type !== "pi") continue;
13391
- out.push({
13392
- ...resolver.getStatus(),
13393
- backendName: name,
13394
- endpoint: def.endpoint
13395
- });
13396
- }
13397
- return out;
13398
- }
13458
+ getLocalModelStatus: () => this.getFirstLocalModelStatus(),
13459
+ getLocalModelStatuses: () => this.buildLocalModelStatuses()
13399
13460
  });
13400
13461
  this.server.setRecorder(this.recorder);
13401
13462
  this.interactionQueue.onPush((interaction) => {
@@ -13403,6 +13464,56 @@ var Orchestrator = class extends import_node_events.EventEmitter {
13403
13464
  });
13404
13465
  }
13405
13466
  }
13467
+ /**
13468
+ * Phase 5: construct the OTLP/HTTP trace exporter and wire telemetry fanout.
13469
+ * Only fires when the operator configures `telemetry.export.otlp` in
13470
+ * harness.config.json. Extracted from the server-init block in the
13471
+ * constructor to keep that block's cyclomatic complexity under threshold.
13472
+ */
13473
+ setupTelemetryExport(config, webhookStore, webhookDelivery) {
13474
+ const otlpCfg = config.telemetry?.export?.otlp;
13475
+ if (!otlpCfg) return;
13476
+ this.otlpExporter = new import_core17.OTLPExporter({
13477
+ endpoint: otlpCfg.endpoint,
13478
+ ...otlpCfg.enabled !== void 0 ? { enabled: otlpCfg.enabled } : {},
13479
+ ...otlpCfg.headers !== void 0 ? { headers: otlpCfg.headers } : {},
13480
+ ...otlpCfg.flushIntervalMs !== void 0 ? { flushIntervalMs: otlpCfg.flushIntervalMs } : {},
13481
+ ...otlpCfg.batchSize !== void 0 ? { batchSize: otlpCfg.batchSize } : {}
13482
+ });
13483
+ this.telemetryFanoutOff = wireTelemetryFanout({
13484
+ bus: this,
13485
+ exporter: this.otlpExporter,
13486
+ webhookDelivery,
13487
+ store: webhookStore
13488
+ });
13489
+ }
13490
+ /**
13491
+ * Deprecated alias for /api/v1/local-model/status (Spec 1 endpoint retained
13492
+ * as a compat shim per spec line 35; superseded by getLocalModelStatuses for
13493
+ * the multi-local UI). Returns the first-registered resolver's status.
13494
+ */
13495
+ getFirstLocalModelStatus() {
13496
+ const first = this.localResolvers.values().next();
13497
+ return first.done ? null : first.value.getStatus();
13498
+ }
13499
+ /**
13500
+ * SC38: build NamedLocalModelStatus[] from each registered resolver, tagged
13501
+ * with its backendName + endpoint from the config.
13502
+ */
13503
+ buildLocalModelStatuses() {
13504
+ const backends = this.config.agent.backends ?? {};
13505
+ const out = [];
13506
+ for (const [name, resolver] of this.localResolvers) {
13507
+ const def = backends[name];
13508
+ if (!def || def.type !== "local" && def.type !== "pi") continue;
13509
+ out.push({
13510
+ ...resolver.getStatus(),
13511
+ backendName: name,
13512
+ endpoint: def.endpoint
13513
+ });
13514
+ }
13515
+ return out;
13516
+ }
13406
13517
  createTracker() {
13407
13518
  if (this.config.tracker.kind === "github-issues") {
13408
13519
  const trackerCfg = {