snipara-companion 1.4.22 → 2.0.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/README.md CHANGED
@@ -191,15 +191,39 @@ snipara-companion workflow run \
191
191
  ```
192
192
 
193
193
  The output is a routing card and handoff metadata. Companion calls the hosted
194
- `snipara_adaptive_routing_catalog` tool when project auth is configured, records
195
- the sanitized runtime catalog in the handoff, and falls back to local dry-run
196
- metadata when the hosted gateway is unavailable. It does not silently spawn
197
- workers. The stable contract is provider-neutral requirements such as worker
198
- role, reasoning depth, context budget, endpoint type, write scope, and fallback.
194
+ `snipara_adaptive_routing_catalog` tool when project auth and policy allow it,
195
+ records the sanitized runtime catalog in the handoff, and falls back to local
196
+ dry-run metadata when the hosted gateway is unavailable or omits
197
+ `success: true`. It does not silently spawn workers. The stable contract is
198
+ provider-neutral requirements such as worker role, reasoning depth, context
199
+ budget, endpoint type, write scope, and fallback.
200
+
201
+ Project owners can configure Adaptive Work Routing in Project > Automation:
202
+ `off`, `recommend`, or `catalog`; approval; planner-retained reasoning; allowed
203
+ endpoint types (`cloud`, `local`, `self_hosted`); worker classes; and budget
204
+ hints. `workflow run` reads that hosted policy and CLI flags cannot broaden it.
199
205
  Project credentials stay server-side behind the hosted gateway; local endpoints
200
206
  such as Ollama, LM Studio, AnythingLLM, or other OpenAI-compatible servers must
201
207
  be reachable from the worker execution environment.
202
208
 
209
+ For the open package without Snipara SaaS, add a local policy file:
210
+
211
+ ```json
212
+ {
213
+ "mode": "recommend",
214
+ "plannerRetainsReasoning": true,
215
+ "preferLocalWorkers": true,
216
+ "allowedEndpointTypes": ["local", "cloud"],
217
+ "preferredEndpointTypes": ["local"],
218
+ "allowedWorkerClasses": ["documentation", "tests", "review"],
219
+ "catalogLimit": 8
220
+ }
221
+ ```
222
+
223
+ Save it at `.snipara/adaptive-routing.json`. Without hosted configuration,
224
+ `workflow run` only emits local Adaptive Work Routing metadata and handoff files:
225
+ it does not query hosted context, call the hosted catalog, or spawn workers.
226
+
203
227
  ## Verification Plans
204
228
 
205
229
  Use `verify` when an agent asks what to prove before handoff or release:
package/dist/index.d.ts CHANGED
@@ -379,6 +379,33 @@ interface AutomationConfigBundle {
379
379
  files: AutomationConfigFile[];
380
380
  instructions: string[];
381
381
  }
382
+ interface ProjectAutomationSettings {
383
+ automationClient?: string;
384
+ autoInjectContext?: boolean;
385
+ trackAccessedFiles?: boolean;
386
+ preserveOnCompaction?: boolean;
387
+ restoreOnSessionStart?: boolean;
388
+ enrichPrompts?: boolean;
389
+ maxTokensPerQuery?: number;
390
+ searchMode?: string;
391
+ includeSummaries?: boolean;
392
+ adaptiveRoutingMode?: string;
393
+ adaptiveRoutingRequireApproval?: boolean;
394
+ adaptiveRoutingPlannerRetainsReasoning?: boolean;
395
+ adaptiveRoutingPreferLocalWorkers?: boolean;
396
+ adaptiveRoutingAllowedEndpointTypes?: string[];
397
+ adaptiveRoutingPreferredEndpointTypes?: string[];
398
+ adaptiveRoutingAllowedWorkerClasses?: string[];
399
+ adaptiveRoutingFallback?: string;
400
+ adaptiveRoutingDailyBudgetCents?: number;
401
+ adaptiveRoutingMonthlyBudgetCents?: number;
402
+ adaptiveRoutingCatalogLimit?: number;
403
+ }
404
+ interface ProjectAutomationSettingsResult {
405
+ settings: ProjectAutomationSettings;
406
+ plan?: string;
407
+ featureAvailability?: Record<string, unknown>;
408
+ }
382
409
  interface RecentAutomationEvent {
383
410
  id: string;
384
411
  sessionId: string;
@@ -986,6 +1013,7 @@ declare class RLMClient {
986
1013
  limit?: number;
987
1014
  }): Promise<RecentAutomationEventsResult>;
988
1015
  getAutomationConfigBundle(client: string): Promise<AutomationConfigBundle>;
1016
+ getAutomationSettings(): Promise<ProjectAutomationSettingsResult>;
989
1017
  evaluateStuckGuard(args?: EvaluateStuckGuardArgs): Promise<StuckGuardEvaluationResult>;
990
1018
  getStuckGuardStatus(args?: {
991
1019
  sessionId?: string;
@@ -2820,6 +2848,7 @@ interface AdaptiveModelRequirements {
2820
2848
  writeScope: string[];
2821
2849
  preferredEndpointTypes?: string[];
2822
2850
  allowedEndpointTypes?: string[];
2851
+ catalogLimit?: number;
2823
2852
  fallback: "main_agent";
2824
2853
  }
2825
2854
  interface AdaptiveRoutingCostEstimate {
@@ -2864,6 +2893,7 @@ interface AdaptiveWorkRoutingOptions {
2864
2893
  allowedEndpointTypes?: string[];
2865
2894
  workerRole?: string;
2866
2895
  plannerRetainsReasoning?: boolean;
2896
+ catalogLimit?: number;
2867
2897
  }
2868
2898
  interface OrchestratorHandoffArtifact {
2869
2899
  schemaVersion: "snipara.orchestrator.handoff.v1";
package/dist/index.js CHANGED
@@ -1024,6 +1024,44 @@ var RLMClient = class {
1024
1024
  clearTimeout(timeoutId);
1025
1025
  }
1026
1026
  }
1027
+ async getAutomationSettings() {
1028
+ if (!this.config.apiKey) {
1029
+ throw new Error("API key not configured. Run 'npx -y snipara-companion@latest init' first.");
1030
+ }
1031
+ const projectIdentifier = this.resolveProjectIdentifier();
1032
+ const projectId = encodeURIComponent(projectIdentifier);
1033
+ const url = `${this.dashboardApiUrl()}/api/projects/${projectId}/automation`;
1034
+ const controller = new AbortController();
1035
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1036
+ try {
1037
+ const response = await this.fetchWithApiKeyRetry(
1038
+ url,
1039
+ {
1040
+ method: "GET",
1041
+ headers: {
1042
+ "Content-Type": "application/json"
1043
+ },
1044
+ signal: controller.signal
1045
+ },
1046
+ projectIdentifier
1047
+ );
1048
+ clearTimeout(timeoutId);
1049
+ if (!response.ok) {
1050
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1051
+ }
1052
+ const data = await response.json();
1053
+ if (!data.success || !data.data?.settings || typeof data.data.settings !== "object") {
1054
+ throw new Error("Automation settings response was invalid");
1055
+ }
1056
+ return {
1057
+ settings: data.data.settings,
1058
+ plan: typeof data.data.plan === "string" ? data.data.plan : void 0,
1059
+ featureAvailability: data.data.featureAvailability && typeof data.data.featureAvailability === "object" && !Array.isArray(data.data.featureAvailability) ? data.data.featureAvailability : void 0
1060
+ };
1061
+ } finally {
1062
+ clearTimeout(timeoutId);
1063
+ }
1064
+ }
1027
1065
  async evaluateStuckGuard(args = {}) {
1028
1066
  if (!this.config.apiKey) {
1029
1067
  throw new Error("API key not configured. Run 'npx -y snipara-companion@latest init' first.");
@@ -11225,6 +11263,7 @@ function buildAdaptiveWorkRoutingRecommendation(options) {
11225
11263
  writeScope: changedFiles,
11226
11264
  preferredEndpointTypes,
11227
11265
  allowedEndpointTypes,
11266
+ catalogLimit: options.catalogLimit,
11228
11267
  fallback: "main_agent"
11229
11268
  });
11230
11269
  const warnings = [
@@ -11358,7 +11397,9 @@ function normalizeEndpointTypes(values) {
11358
11397
  }
11359
11398
  function compactObject2(value) {
11360
11399
  return Object.fromEntries(
11361
- Object.entries(value).filter(([, item]) => !(Array.isArray(item) && item.length === 0))
11400
+ Object.entries(value).filter(
11401
+ ([, item]) => item !== void 0 && !(Array.isArray(item) && item.length === 0)
11402
+ )
11362
11403
  );
11363
11404
  }
11364
11405
  function writeOrchestratorHandoff(options) {
@@ -14551,6 +14592,8 @@ var FINAL_COMMIT_TIMEOUT_MS = 9e4;
14551
14592
  var FINAL_COMMIT_RETRY_TIMEOUT_MS = 45e3;
14552
14593
  var FINAL_COMMIT_SUMMARY_MAX_CHARS = 1200;
14553
14594
  var FINAL_COMMIT_RETRY_SUMMARY_MAX_CHARS = 600;
14595
+ var DEFAULT_ADAPTIVE_ROUTING_CATALOG_LIMIT = 20;
14596
+ var ADAPTIVE_ROUTING_POLICY_RELATIVE_PATH = path18.join(".snipara", "adaptive-routing.json");
14554
14597
  var SHARED_CONTEXT_INTENT_PATTERN = /\b(standard|standards|convention|conventions|guideline|guidelines|best practice|best practices|policy|policies|compliance|compliant|security rules|team rules|style guide|playbook|checklist)\b/i;
14555
14598
  var DOCUMENT_SYNC_FORMATS = {
14556
14599
  ".adoc": { kind: "DOC", format: "adoc" },
@@ -17099,6 +17142,110 @@ function printAdaptiveRoutingRecommendation(routing) {
17099
17142
  }
17100
17143
  }
17101
17144
  }
17145
+ function policyValue(settings, hostedKey, localKey) {
17146
+ const record = settings;
17147
+ return record[hostedKey] ?? record[localKey];
17148
+ }
17149
+ function readLocalAdaptiveRoutingProjectPolicy() {
17150
+ const workspaceRoot = findWorkspaceRoot(process.cwd(), true);
17151
+ if (!workspaceRoot) {
17152
+ return null;
17153
+ }
17154
+ const policyPath = path18.join(workspaceRoot, ADAPTIVE_ROUTING_POLICY_RELATIVE_PATH);
17155
+ if (!fs19.existsSync(policyPath)) {
17156
+ return null;
17157
+ }
17158
+ try {
17159
+ const parsed = JSON.parse(fs19.readFileSync(policyPath, "utf8"));
17160
+ if (!isRecord9(parsed)) {
17161
+ return null;
17162
+ }
17163
+ return normalizeAdaptiveRoutingProjectPolicy(parsed, "local_file");
17164
+ } catch {
17165
+ return null;
17166
+ }
17167
+ }
17168
+ async function loadAdaptiveRoutingProjectPolicy(client, fallbackPolicy = null) {
17169
+ try {
17170
+ if (!client) {
17171
+ return fallbackPolicy;
17172
+ }
17173
+ const result = await client.getAutomationSettings();
17174
+ return normalizeAdaptiveRoutingProjectPolicy(result.settings, "hosted_project") ?? fallbackPolicy;
17175
+ } catch {
17176
+ return fallbackPolicy;
17177
+ }
17178
+ }
17179
+ function normalizeAdaptiveRoutingProjectPolicy(settings, source) {
17180
+ const mode = normalizeAdaptiveRoutingMode(policyValue(settings, "adaptiveRoutingMode", "mode"));
17181
+ if (!mode) {
17182
+ return null;
17183
+ }
17184
+ const allowedEndpointTypes = normalizeRoutingEndpointTypes(
17185
+ normalizeStringArray3(
17186
+ policyValue(settings, "adaptiveRoutingAllowedEndpointTypes", "allowedEndpointTypes")
17187
+ )
17188
+ );
17189
+ const preferredEndpointTypes = normalizeRoutingEndpointTypes(
17190
+ normalizeStringArray3(
17191
+ policyValue(settings, "adaptiveRoutingPreferredEndpointTypes", "preferredEndpointTypes")
17192
+ )
17193
+ );
17194
+ const allowedWorkerClasses = normalizeAdaptiveWorkerClasses(
17195
+ normalizeStringArray3(
17196
+ policyValue(settings, "adaptiveRoutingAllowedWorkerClasses", "allowedWorkerClasses")
17197
+ )
17198
+ );
17199
+ return {
17200
+ source,
17201
+ mode,
17202
+ requireApproval: policyValue(settings, "adaptiveRoutingRequireApproval", "requireApproval") !== false,
17203
+ plannerRetainsReasoning: policyValue(settings, "adaptiveRoutingPlannerRetainsReasoning", "plannerRetainsReasoning") !== false,
17204
+ preferLocalWorkers: policyValue(settings, "adaptiveRoutingPreferLocalWorkers", "preferLocalWorkers") === true,
17205
+ allowedEndpointTypes: allowedEndpointTypes.length > 0 ? allowedEndpointTypes : ["cloud"],
17206
+ preferredEndpointTypes,
17207
+ allowedWorkerClasses: allowedWorkerClasses.length > 0 ? allowedWorkerClasses : ["documentation", "tests", "review"],
17208
+ fallback: "main_agent",
17209
+ dailyBudgetCents: normalizeCents(
17210
+ policyValue(settings, "adaptiveRoutingDailyBudgetCents", "dailyBudgetCents")
17211
+ ),
17212
+ monthlyBudgetCents: normalizeCents(
17213
+ policyValue(settings, "adaptiveRoutingMonthlyBudgetCents", "monthlyBudgetCents")
17214
+ ),
17215
+ catalogLimit: normalizeAdaptiveRoutingCatalogLimit(
17216
+ policyValue(settings, "adaptiveRoutingCatalogLimit", "catalogLimit")
17217
+ )
17218
+ };
17219
+ }
17220
+ function resolveAdaptiveRoutingIntent(options, policy, hostedConfigured) {
17221
+ const cliRequested = shouldBuildAdaptiveRouting(options);
17222
+ if (!policy) {
17223
+ return {
17224
+ shouldBuild: cliRequested,
17225
+ shouldUseHostedCatalog: cliRequested,
17226
+ warnings: []
17227
+ };
17228
+ }
17229
+ if (policy.mode === "off") {
17230
+ return {
17231
+ shouldBuild: cliRequested,
17232
+ shouldUseHostedCatalog: false,
17233
+ warnings: cliRequested ? ["Project Adaptive Work Routing policy is off; keeping recommendation metadata local."] : []
17234
+ };
17235
+ }
17236
+ return {
17237
+ shouldBuild: true,
17238
+ shouldUseHostedCatalog: hostedConfigured && policy.mode === "catalog",
17239
+ warnings: [
17240
+ ...policy.mode === "recommend" ? [
17241
+ "Project Adaptive Work Routing policy is recommendation-only; hosted catalog lookup is disabled."
17242
+ ] : [],
17243
+ ...!hostedConfigured && policy.mode === "catalog" ? [
17244
+ "Local Adaptive Work Routing policy requested catalog mode; hosted catalog lookup is skipped without Snipara configuration."
17245
+ ] : []
17246
+ ]
17247
+ };
17248
+ }
17102
17249
  async function enrichAdaptiveRoutingWithHostedCatalog(client, routing) {
17103
17250
  try {
17104
17251
  const result = await client.callTool(
@@ -17106,23 +17253,28 @@ async function enrichAdaptiveRoutingWithHostedCatalog(client, routing) {
17106
17253
  {
17107
17254
  work_profile: routing.workProfile,
17108
17255
  model_requirements: routing.requirements,
17109
- limit: 20
17256
+ limit: routing.requirements.catalogLimit ?? DEFAULT_ADAPTIVE_ROUTING_CATALOG_LIMIT
17110
17257
  }
17111
17258
  );
17112
17259
  const catalog = normalizeAdaptiveRoutingCatalog(result.catalog);
17260
+ const gatewaySucceeded = result.success === true;
17113
17261
  const warnings = normalizeStringArray3(result.warnings) ?? [];
17262
+ const gatewayWarnings = gatewaySucceeded ? warnings : [
17263
+ ...warnings,
17264
+ "Hosted adaptive routing catalog did not return success=true; treating gateway as failed closed."
17265
+ ];
17114
17266
  const candidateCount = numberValue5(result.resolution?.candidate_count) ?? numberValue5(result.resolution?.candidateCount) ?? catalog.candidates.length;
17115
17267
  const gateway = {
17116
17268
  source: "hosted_mcp",
17117
- success: result.success !== false,
17269
+ success: gatewaySucceeded,
17118
17270
  resolutionStatus: stringValue5(result.resolution?.status),
17119
17271
  candidateCount,
17120
17272
  fallback: stringValue5(result.fallback) ?? stringValue5(result.resolution?.fallback) ?? "main_agent",
17121
- warnings
17273
+ warnings: gatewayWarnings
17122
17274
  };
17123
17275
  const reasons = [
17124
17276
  ...routing.routingCard.reasons,
17125
- candidateCount > 0 ? `hosted adaptive routing catalog returned ${candidateCount} candidate(s)` : "hosted adaptive routing catalog returned no candidates and will fail closed"
17277
+ gatewaySucceeded ? candidateCount > 0 ? `hosted adaptive routing catalog returned ${candidateCount} candidate(s)` : "hosted adaptive routing catalog returned no candidates and will fail closed" : "hosted adaptive routing catalog did not report explicit success and will fail closed"
17126
17278
  ];
17127
17279
  return {
17128
17280
  ...routing,
@@ -17131,7 +17283,7 @@ async function enrichAdaptiveRoutingWithHostedCatalog(client, routing) {
17131
17283
  routingCard: {
17132
17284
  ...routing.routingCard,
17133
17285
  reasons: uniqueStrings3(reasons),
17134
- warnings: uniqueStrings3([...routing.routingCard.warnings, ...warnings])
17286
+ warnings: uniqueStrings3([...routing.routingCard.warnings, ...gatewayWarnings])
17135
17287
  }
17136
17288
  };
17137
17289
  } catch (error) {
@@ -18308,15 +18460,15 @@ function hasReferenceProvenance(metadata) {
18308
18460
  ].some(Boolean);
18309
18461
  }
18310
18462
  function validateFreshnessPolicy(metadata, reasons) {
18311
- const policyValue = metadata.freshnessPolicy ?? metadata.freshness_policy;
18312
- if (policyValue === void 0) {
18463
+ const policyValue2 = metadata.freshnessPolicy ?? metadata.freshness_policy;
18464
+ if (policyValue2 === void 0) {
18313
18465
  return { maxAgeDays: void 0 };
18314
18466
  }
18315
- if (!isRecord9(policyValue)) {
18467
+ if (!isRecord9(policyValue2)) {
18316
18468
  reasons.push("invalid_freshness_policy");
18317
18469
  return { maxAgeDays: void 0 };
18318
18470
  }
18319
- const maxAge = policyValue.maxAgeDays ?? policyValue.max_age_days;
18471
+ const maxAge = policyValue2.maxAgeDays ?? policyValue2.max_age_days;
18320
18472
  if (maxAge === void 0) {
18321
18473
  return { maxAgeDays: void 0 };
18322
18474
  }
@@ -18637,14 +18789,26 @@ async function sharedContextCommand(options) {
18637
18789
  printSharedContextResult(result);
18638
18790
  }
18639
18791
  async function workflowRunCommand(options) {
18640
- ensureConfigured();
18641
- const client = createClient(2e4);
18642
- const adaptiveRoutingRequested = shouldBuildAdaptiveRouting(options);
18643
- const adaptiveRouting = adaptiveRoutingRequested ? await enrichAdaptiveRoutingWithHostedCatalog(client, buildWorkflowAdaptiveRouting(options)) : null;
18792
+ const hostedConfigured = isConfigured();
18793
+ const localAdaptiveRoutingPolicy = readLocalAdaptiveRoutingProjectPolicy();
18794
+ const localAdaptiveRoutingRequested = shouldBuildAdaptiveRouting(options);
18795
+ const canRunLocalAdaptiveRouting = !hostedConfigured && options.mode !== "orchestrate" && (localAdaptiveRoutingRequested || localAdaptiveRoutingPolicy !== null && localAdaptiveRoutingPolicy.mode !== "off");
18796
+ if (!hostedConfigured && !canRunLocalAdaptiveRouting) {
18797
+ ensureConfigured();
18798
+ }
18799
+ const client = hostedConfigured ? createClient(2e4) : null;
18800
+ const adaptiveRoutingPolicy = hostedConfigured ? await loadAdaptiveRoutingProjectPolicy(client, localAdaptiveRoutingPolicy) : localAdaptiveRoutingPolicy;
18801
+ const adaptiveRoutingIntent = resolveAdaptiveRoutingIntent(
18802
+ options,
18803
+ adaptiveRoutingPolicy,
18804
+ hostedConfigured
18805
+ );
18806
+ const adaptiveRoutingDryRun = adaptiveRoutingIntent.shouldBuild ? buildWorkflowAdaptiveRouting(options, adaptiveRoutingPolicy, adaptiveRoutingIntent.warnings) : null;
18807
+ const adaptiveRouting = adaptiveRoutingDryRun && adaptiveRoutingIntent.shouldUseHostedCatalog && client ? await enrichAdaptiveRoutingWithHostedCatalog(client, adaptiveRoutingDryRun) : adaptiveRoutingDryRun;
18644
18808
  const orchestratorRecommendation = getOrchestratorRecommendation(options.query, options.mode, {
18645
18809
  policyAutoRoute: options.autoRouteOrchestrator,
18646
18810
  policySource: options.orchestratorPolicySource,
18647
- adaptiveRoutingDryRun: adaptiveRoutingRequested
18811
+ adaptiveRoutingDryRun: Boolean(adaptiveRouting)
18648
18812
  });
18649
18813
  const shouldEmitOrchestratorHandoff = options.emitOrchestratorHandoff || options.autoRouteOrchestrator;
18650
18814
  const preparedHandoff = shouldEmitOrchestratorHandoff && orchestratorRecommendation ? writeOrchestratorHandoff({
@@ -18656,6 +18820,38 @@ async function workflowRunCommand(options) {
18656
18820
  mode: options.mode,
18657
18821
  adaptiveRouting
18658
18822
  }) : null;
18823
+ if (!hostedConfigured) {
18824
+ const payload2 = {
18825
+ mode: options.mode,
18826
+ effective_mode: effectiveWorkflowMode(options.mode),
18827
+ local_only: true,
18828
+ local_policy_path: localAdaptiveRoutingPolicy ? ADAPTIVE_ROUTING_POLICY_RELATIVE_PATH : null,
18829
+ orchestrator_recommendation: orchestratorRecommendation,
18830
+ orchestrator_handoff: preparedHandoff,
18831
+ adaptive_routing: adaptiveRouting,
18832
+ warnings: [
18833
+ "Hosted Snipara is not configured; workflow run is limited to local Adaptive Work Routing metadata."
18834
+ ]
18835
+ };
18836
+ if (options.json) {
18837
+ printJson2(payload2);
18838
+ return;
18839
+ }
18840
+ console.log(import_chalk9.default.bold("Local Adaptive Work Routing"));
18841
+ console.log(
18842
+ "Hosted Snipara is not configured; no context query, hosted catalog, or planner call ran."
18843
+ );
18844
+ if (adaptiveRouting) {
18845
+ printAdaptiveRoutingRecommendation(adaptiveRouting);
18846
+ }
18847
+ if (preparedHandoff) {
18848
+ printPreparedOrchestratorHandoff2(preparedHandoff);
18849
+ }
18850
+ return;
18851
+ }
18852
+ if (!client) {
18853
+ throw new Error("Hosted Snipara client unavailable after configuration check.");
18854
+ }
18659
18855
  if (options.mode === "orchestrate") {
18660
18856
  const result = await client.orchestrate(options.query, options.maxTokens);
18661
18857
  if (options.json) {
@@ -18834,22 +19030,72 @@ function shouldBuildAdaptiveRouting(options) {
18834
19030
  options.adaptiveRoutingDryRun || options.routeLocalWorkers || options.routingWorkerRole || options.routingPreferredEndpoints?.length || options.routingAllowedEndpoints?.length || options.plannerRetainsReasoning
18835
19031
  );
18836
19032
  }
18837
- function buildWorkflowAdaptiveRouting(options) {
19033
+ function buildWorkflowAdaptiveRouting(options, policy = null, intentWarnings = []) {
18838
19034
  const state = readWorkflowState2();
18839
19035
  const currentPhase = state ? currentWorkflowPhase(state) : void 0;
18840
- const preferredEndpointTypes = normalizeRoutingEndpointTypes([
19036
+ const initialPreferredEndpointTypes = normalizeRoutingEndpointTypes([
18841
19037
  ...options.routingPreferredEndpoints ?? [],
18842
- ...options.routeLocalWorkers ? ["local"] : []
19038
+ ...options.routeLocalWorkers ? ["local"] : [],
19039
+ ...policy?.preferredEndpointTypes ?? [],
19040
+ ...policy?.preferLocalWorkers ? ["local"] : []
18843
19041
  ]);
18844
- return buildAdaptiveWorkRoutingRecommendation({
19042
+ const policyAllowedEndpointTypes = policy?.allowedEndpointTypes ?? [];
19043
+ const requestedAllowedEndpointTypes = normalizeRoutingEndpointTypes(
19044
+ options.routingAllowedEndpoints
19045
+ );
19046
+ const requestedPolicyEndpointIntersection = policyAllowedEndpointTypes.length > 0 && requestedAllowedEndpointTypes.length > 0 ? intersectStrings(requestedAllowedEndpointTypes, policyAllowedEndpointTypes) : [];
19047
+ const allowedEndpointTypes = policyAllowedEndpointTypes.length > 0 ? requestedAllowedEndpointTypes.length > 0 ? requestedPolicyEndpointIntersection.length > 0 ? requestedPolicyEndpointIntersection : policyAllowedEndpointTypes : policyAllowedEndpointTypes : requestedAllowedEndpointTypes;
19048
+ const preferredEndpointTypes = allowedEndpointTypes.length > 0 ? initialPreferredEndpointTypes.filter((type) => allowedEndpointTypes.includes(type)) : initialPreferredEndpointTypes;
19049
+ const removedPreferredEndpointTypes = initialPreferredEndpointTypes.filter(
19050
+ (type) => !preferredEndpointTypes.includes(type)
19051
+ );
19052
+ const policyWarnings = [
19053
+ ...intentWarnings,
19054
+ ...requestedAllowedEndpointTypes.length > 0 && policyAllowedEndpointTypes.length > 0 && requestedPolicyEndpointIntersection.length === 0 ? [
19055
+ `Project Adaptive Work Routing policy rejected requested allowed endpoint(s): ${requestedAllowedEndpointTypes.join(", ")}.`
19056
+ ] : [],
19057
+ ...removedPreferredEndpointTypes.length > 0 ? [
19058
+ `Project Adaptive Work Routing policy removed unsupported preferred endpoint(s): ${removedPreferredEndpointTypes.join(", ")}.`
19059
+ ] : []
19060
+ ];
19061
+ const buildRecommendation = (workerRole) => buildAdaptiveWorkRoutingRecommendation({
18845
19062
  query: options.query,
18846
19063
  mode: options.mode,
18847
19064
  changedFiles: currentPhase?.files ?? [],
18848
19065
  preferredEndpointTypes,
18849
- allowedEndpointTypes: normalizeRoutingEndpointTypes(options.routingAllowedEndpoints),
18850
- workerRole: stringValue5(options.routingWorkerRole),
18851
- plannerRetainsReasoning: options.plannerRetainsReasoning ?? (options.routeLocalWorkers ? true : void 0)
19066
+ allowedEndpointTypes,
19067
+ workerRole,
19068
+ plannerRetainsReasoning: options.plannerRetainsReasoning ?? policy?.plannerRetainsReasoning ?? (options.routeLocalWorkers ? true : void 0),
19069
+ catalogLimit: policy?.catalogLimit ?? DEFAULT_ADAPTIVE_ROUTING_CATALOG_LIMIT
18852
19070
  });
19071
+ const requestedWorkerRole = stringValue5(options.routingWorkerRole);
19072
+ let routing = buildRecommendation(requestedWorkerRole);
19073
+ const allowedWorkerClasses = policy?.allowedWorkerClasses ?? [];
19074
+ if (allowedWorkerClasses.length > 0 && !isAdaptiveWorkerClassAllowed(routing.requirements.workerRole, allowedWorkerClasses)) {
19075
+ const disallowedWorkerClass = canonicalAdaptiveWorkerClass(routing.requirements.workerRole);
19076
+ const fallbackWorkerRole = selectAdaptiveWorkerRoleForPolicy(
19077
+ routing.workProfile.taskType,
19078
+ allowedWorkerClasses
19079
+ );
19080
+ routing = buildRecommendation(fallbackWorkerRole);
19081
+ policyWarnings.push(
19082
+ `Project Adaptive Work Routing policy does not allow worker class ${disallowedWorkerClass}; using ${canonicalAdaptiveWorkerClass(
19083
+ fallbackWorkerRole
19084
+ )}.`
19085
+ );
19086
+ }
19087
+ const policyReasons = policy ? [
19088
+ `project adaptive routing policy mode is ${policy.mode}`,
19089
+ `project adaptive routing allows endpoint type(s): ${policy.allowedEndpointTypes.join(", ")}`
19090
+ ] : [];
19091
+ return {
19092
+ ...routing,
19093
+ routingCard: {
19094
+ ...routing.routingCard,
19095
+ reasons: uniqueStrings3([...routing.routingCard.reasons, ...policyReasons]),
19096
+ warnings: uniqueStrings3([...routing.routingCard.warnings, ...policyWarnings])
19097
+ }
19098
+ };
18853
19099
  }
18854
19100
  function normalizeRoutingEndpointTypes(values) {
18855
19101
  return Array.from(
@@ -18858,6 +19104,60 @@ function normalizeRoutingEndpointTypes(values) {
18858
19104
  )
18859
19105
  ).sort();
18860
19106
  }
19107
+ function normalizeAdaptiveRoutingMode(value) {
19108
+ const normalized = stringValue5(value)?.toLowerCase();
19109
+ return normalized === "off" || normalized === "recommend" || normalized === "catalog" ? normalized : null;
19110
+ }
19111
+ function normalizeAdaptiveWorkerClasses(values) {
19112
+ return uniqueStrings3((values ?? []).map(canonicalAdaptiveWorkerClass)).filter(
19113
+ (value) => ["documentation", "tests", "review", "coding"].includes(value)
19114
+ );
19115
+ }
19116
+ function normalizeCents(value) {
19117
+ const parsed = numberValue5(value);
19118
+ if (parsed === void 0 || parsed < 0) {
19119
+ return 0;
19120
+ }
19121
+ return Math.floor(parsed);
19122
+ }
19123
+ function normalizeAdaptiveRoutingCatalogLimit(value) {
19124
+ const parsed = numberValue5(value);
19125
+ if (parsed === void 0 || parsed < 1) {
19126
+ return void 0;
19127
+ }
19128
+ return Math.min(Math.floor(parsed), 100);
19129
+ }
19130
+ function intersectStrings(left, right) {
19131
+ const rightSet = new Set(right);
19132
+ return left.filter((value) => rightSet.has(value));
19133
+ }
19134
+ function canonicalAdaptiveWorkerClass(value) {
19135
+ const normalized = stringValue5(value)?.toLowerCase().replace(/[-\s]/g, "_") ?? "execution";
19136
+ if (normalized === "docs" || normalized === "doc") {
19137
+ return "documentation";
19138
+ }
19139
+ if (normalized === "test" || normalized === "testing") {
19140
+ return "tests";
19141
+ }
19142
+ if (normalized === "validation" || normalized === "reviewer") {
19143
+ return "review";
19144
+ }
19145
+ if (normalized === "code" || normalized === "coder" || normalized === "implementation") {
19146
+ return "coding";
19147
+ }
19148
+ return normalized;
19149
+ }
19150
+ function isAdaptiveWorkerClassAllowed(workerRole, allowedWorkerClasses) {
19151
+ return allowedWorkerClasses.includes(canonicalAdaptiveWorkerClass(workerRole));
19152
+ }
19153
+ function selectAdaptiveWorkerRoleForPolicy(taskType, allowedWorkerClasses) {
19154
+ const preferences = taskType === "documentation" ? ["documentation", "review", "coding", "tests"] : taskType === "tests" ? ["tests", "review", "coding", "documentation"] : taskType === "coding" || taskType === "critical_code" ? ["coding", "review", "tests", "documentation"] : ["review", "coding", "documentation", "tests"];
19155
+ const selected = preferences.find((workerClass) => allowedWorkerClasses.includes(workerClass));
19156
+ return workerRoleFromAdaptiveClass(selected ?? allowedWorkerClasses[0] ?? "review");
19157
+ }
19158
+ function workerRoleFromAdaptiveClass(workerClass) {
19159
+ return workerClass === "tests" ? "testing" : workerClass;
19160
+ }
18861
19161
  function startManagedWorkflowState(options) {
18862
19162
  const existing = readWorkflowState2();
18863
19163
  if (existing && existing.status === "active" && !options.force) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snipara-companion",
3
- "version": "1.4.22",
3
+ "version": "2.0.0",
4
4
  "description": "Snipara Git-style companion CLI for local workflow continuity, hooks, hosted context, and Mini Snipara OSS workflows",
5
5
  "main": "dist/index.js",
6
6
  "bin": {