snipara-companion 1.4.21 → 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
@@ -127,6 +127,7 @@ snipara-companion brief --task "ship auth hardening" --changed-files src/auth.ts
127
127
  snipara-companion timeline
128
128
  snipara-companion workflow phase-commit build --summary "tests green"
129
129
  snipara-companion workflow impact-gate
130
+ snipara-companion workflow run --adaptive-routing-dry-run --route-local-workers "document a scoped change"
130
131
  snipara-companion verify --changed-files src/auth.ts --diff-summary "auth hardening"
131
132
  snipara-companion run --task "ship auth hardening" --changed-files src/auth.ts --release
132
133
  snipara-companion handoff --summary "status command shipped" --next "publish package"
@@ -141,6 +142,9 @@ snipara-companion workflow resume --include-session-context
141
142
  - `workflow impact-gate` audits committed local workflow phases that are ahead
142
143
  of upstream but not pushed. It does not push, and dirty working-tree files are
143
144
  reported separately from the committed diff.
145
+ - `workflow run --adaptive-routing-dry-run` prints an Adaptive Work Routing
146
+ card. Add `--route-local-workers` when a strong planner should keep deep
147
+ reasoning while a local worker handles scoped execution.
144
148
  - `verify` builds a transparent verification plan from companion code impact
145
149
  signals plus local package scripts. It recommends checks; it does not claim to
146
150
  execute them.
@@ -167,6 +171,59 @@ if the hosted call still times out. A hosted final-commit timeout does not modif
167
171
  Git state. Custom final-commit categories are namespaced under `final-commit`
168
172
  before the hosted call so they stay on the handoff-only path.
169
173
 
174
+ ## Adaptive Work Routing
175
+
176
+ Adaptive Work Routing is a recommendation-first path for routing scoped work to
177
+ the right worker class, endpoint type, and cost profile without hardcoding model
178
+ names in the CLI.
179
+
180
+ ```bash
181
+ snipara-companion workflow run \
182
+ --mode full \
183
+ --adaptive-routing-dry-run \
184
+ --route-local-workers \
185
+ --routing-worker-role coding \
186
+ --routing-preferred-endpoint local \
187
+ --routing-allowed-endpoint local \
188
+ --routing-allowed-endpoint cloud \
189
+ --planner-retains-reasoning \
190
+ "Update documentation for the new gateway"
191
+ ```
192
+
193
+ The output is a routing card and handoff metadata. Companion calls the hosted
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.
205
+ Project credentials stay server-side behind the hosted gateway; local endpoints
206
+ such as Ollama, LM Studio, AnythingLLM, or other OpenAI-compatible servers must
207
+ be reachable from the worker execution environment.
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
+
170
227
  ## Verification Plans
171
228
 
172
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 {
@@ -2837,10 +2866,24 @@ interface AdaptiveRoutingCard {
2837
2866
  reasons: string[];
2838
2867
  warnings: string[];
2839
2868
  }
2869
+ interface AdaptiveRoutingGatewayStatus {
2870
+ source: "hosted_mcp";
2871
+ success: boolean;
2872
+ resolutionStatus?: string;
2873
+ candidateCount: number;
2874
+ fallback?: string;
2875
+ warnings: string[];
2876
+ }
2877
+ interface AdaptiveRoutingRuntimeCatalog {
2878
+ version?: string;
2879
+ candidates: Array<Record<string, unknown>>;
2880
+ }
2840
2881
  interface AdaptiveWorkRoutingRecommendation {
2841
2882
  workProfile: AdaptiveWorkProfile;
2842
2883
  requirements: AdaptiveModelRequirements;
2843
2884
  routingCard: AdaptiveRoutingCard;
2885
+ gateway?: AdaptiveRoutingGatewayStatus;
2886
+ runtimeCatalog?: AdaptiveRoutingRuntimeCatalog;
2844
2887
  }
2845
2888
  interface AdaptiveWorkRoutingOptions {
2846
2889
  query: string;
@@ -2850,6 +2893,7 @@ interface AdaptiveWorkRoutingOptions {
2850
2893
  allowedEndpointTypes?: string[];
2851
2894
  workerRole?: string;
2852
2895
  plannerRetainsReasoning?: boolean;
2896
+ catalogLimit?: number;
2853
2897
  }
2854
2898
  interface OrchestratorHandoffArtifact {
2855
2899
  schemaVersion: "snipara.orchestrator.handoff.v1";
@@ -2877,6 +2921,8 @@ interface OrchestratorHandoffArtifact {
2877
2921
  workProfile?: AdaptiveWorkProfile;
2878
2922
  requirements?: AdaptiveModelRequirements;
2879
2923
  routingCard?: AdaptiveRoutingCard;
2924
+ gateway?: AdaptiveRoutingGatewayStatus;
2925
+ runtimeCatalog?: AdaptiveRoutingRuntimeCatalog;
2880
2926
  };
2881
2927
  task: {
2882
2928
  title: string;
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.");
@@ -11144,7 +11182,9 @@ function buildOrchestratorHandoff(options) {
11144
11182
  ...options.adaptiveRouting ? {
11145
11183
  workProfile: options.adaptiveRouting.workProfile,
11146
11184
  requirements: options.adaptiveRouting.requirements,
11147
- routingCard: options.adaptiveRouting.routingCard
11185
+ routingCard: options.adaptiveRouting.routingCard,
11186
+ ...options.adaptiveRouting.gateway ? { gateway: options.adaptiveRouting.gateway } : {},
11187
+ ...options.adaptiveRouting.runtimeCatalog ? { runtimeCatalog: options.adaptiveRouting.runtimeCatalog } : {}
11148
11188
  } : {}
11149
11189
  },
11150
11190
  task: {
@@ -11223,6 +11263,7 @@ function buildAdaptiveWorkRoutingRecommendation(options) {
11223
11263
  writeScope: changedFiles,
11224
11264
  preferredEndpointTypes,
11225
11265
  allowedEndpointTypes,
11266
+ catalogLimit: options.catalogLimit,
11226
11267
  fallback: "main_agent"
11227
11268
  });
11228
11269
  const warnings = [
@@ -11356,7 +11397,9 @@ function normalizeEndpointTypes(values) {
11356
11397
  }
11357
11398
  function compactObject2(value) {
11358
11399
  return Object.fromEntries(
11359
- 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
+ )
11360
11403
  );
11361
11404
  }
11362
11405
  function writeOrchestratorHandoff(options) {
@@ -14549,6 +14592,8 @@ var FINAL_COMMIT_TIMEOUT_MS = 9e4;
14549
14592
  var FINAL_COMMIT_RETRY_TIMEOUT_MS = 45e3;
14550
14593
  var FINAL_COMMIT_SUMMARY_MAX_CHARS = 1200;
14551
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");
14552
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;
14553
14598
  var DOCUMENT_SYNC_FORMATS = {
14554
14599
  ".adoc": { kind: "DOC", format: "adoc" },
@@ -14648,6 +14693,20 @@ function stringValue5(value) {
14648
14693
  const stringified = String(value).trim();
14649
14694
  return stringified || void 0;
14650
14695
  }
14696
+ function numberValue5(value) {
14697
+ if (typeof value === "number" && Number.isFinite(value)) {
14698
+ return value;
14699
+ }
14700
+ const text = stringValue5(value);
14701
+ if (!text) {
14702
+ return void 0;
14703
+ }
14704
+ const parsed = Number(text);
14705
+ return Number.isFinite(parsed) ? parsed : void 0;
14706
+ }
14707
+ function uniqueStrings3(values) {
14708
+ return Array.from(new Set(values.filter((value) => value.trim().length > 0)));
14709
+ }
14651
14710
  function normalizeEnum(value) {
14652
14711
  return stringValue5(value)?.replace(/[-\s]/g, "_").toUpperCase() ?? "";
14653
14712
  }
@@ -17064,6 +17123,12 @@ function printAdaptiveRoutingRecommendation(routing) {
17064
17123
  if (routing.requirements.plannerRetainsReasoning) {
17065
17124
  printKeyValue("Planner retains reasoning:", "yes");
17066
17125
  }
17126
+ if (routing.gateway) {
17127
+ printKeyValue(
17128
+ "Hosted catalog:",
17129
+ routing.gateway.success ? `${routing.gateway.candidateCount} candidate(s), ${routing.gateway.resolutionStatus ?? "ready"}` : "unavailable"
17130
+ );
17131
+ }
17067
17132
  if (routing.routingCard.reasons.length > 0) {
17068
17133
  console.log("Reasons:");
17069
17134
  for (const reason of routing.routingCard.reasons) {
@@ -17077,6 +17142,182 @@ function printAdaptiveRoutingRecommendation(routing) {
17077
17142
  }
17078
17143
  }
17079
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
+ }
17249
+ async function enrichAdaptiveRoutingWithHostedCatalog(client, routing) {
17250
+ try {
17251
+ const result = await client.callTool(
17252
+ "snipara_adaptive_routing_catalog",
17253
+ {
17254
+ work_profile: routing.workProfile,
17255
+ model_requirements: routing.requirements,
17256
+ limit: routing.requirements.catalogLimit ?? DEFAULT_ADAPTIVE_ROUTING_CATALOG_LIMIT
17257
+ }
17258
+ );
17259
+ const catalog = normalizeAdaptiveRoutingCatalog(result.catalog);
17260
+ const gatewaySucceeded = result.success === true;
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
+ ];
17266
+ const candidateCount = numberValue5(result.resolution?.candidate_count) ?? numberValue5(result.resolution?.candidateCount) ?? catalog.candidates.length;
17267
+ const gateway = {
17268
+ source: "hosted_mcp",
17269
+ success: gatewaySucceeded,
17270
+ resolutionStatus: stringValue5(result.resolution?.status),
17271
+ candidateCount,
17272
+ fallback: stringValue5(result.fallback) ?? stringValue5(result.resolution?.fallback) ?? "main_agent",
17273
+ warnings: gatewayWarnings
17274
+ };
17275
+ const reasons = [
17276
+ ...routing.routingCard.reasons,
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"
17278
+ ];
17279
+ return {
17280
+ ...routing,
17281
+ gateway,
17282
+ runtimeCatalog: catalog,
17283
+ routingCard: {
17284
+ ...routing.routingCard,
17285
+ reasons: uniqueStrings3(reasons),
17286
+ warnings: uniqueStrings3([...routing.routingCard.warnings, ...gatewayWarnings])
17287
+ }
17288
+ };
17289
+ } catch (error) {
17290
+ const warning = `Hosted adaptive routing catalog unavailable; keeping local dry-run metadata (${toPreview(error)}).`;
17291
+ return {
17292
+ ...routing,
17293
+ gateway: {
17294
+ source: "hosted_mcp",
17295
+ success: false,
17296
+ resolutionStatus: "unavailable",
17297
+ candidateCount: 0,
17298
+ fallback: "main_agent",
17299
+ warnings: [warning]
17300
+ },
17301
+ routingCard: {
17302
+ ...routing.routingCard,
17303
+ warnings: uniqueStrings3([...routing.routingCard.warnings, warning])
17304
+ }
17305
+ };
17306
+ }
17307
+ }
17308
+ function normalizeAdaptiveRoutingCatalog(value) {
17309
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
17310
+ return { candidates: [] };
17311
+ }
17312
+ const record = value;
17313
+ const candidates = Array.isArray(record.candidates) ? record.candidates.filter(
17314
+ (candidate) => Boolean(candidate) && typeof candidate === "object" && !Array.isArray(candidate)
17315
+ ) : [];
17316
+ return {
17317
+ version: stringValue5(record.version),
17318
+ candidates
17319
+ };
17320
+ }
17080
17321
  function printUploadResult(path20, result) {
17081
17322
  printKeyValue("Uploaded:", path20);
17082
17323
  printCompactObject(result, ["message", "document_id", "documentId", "version", "status"]);
@@ -18219,15 +18460,15 @@ function hasReferenceProvenance(metadata) {
18219
18460
  ].some(Boolean);
18220
18461
  }
18221
18462
  function validateFreshnessPolicy(metadata, reasons) {
18222
- const policyValue = metadata.freshnessPolicy ?? metadata.freshness_policy;
18223
- if (policyValue === void 0) {
18463
+ const policyValue2 = metadata.freshnessPolicy ?? metadata.freshness_policy;
18464
+ if (policyValue2 === void 0) {
18224
18465
  return { maxAgeDays: void 0 };
18225
18466
  }
18226
- if (!isRecord9(policyValue)) {
18467
+ if (!isRecord9(policyValue2)) {
18227
18468
  reasons.push("invalid_freshness_policy");
18228
18469
  return { maxAgeDays: void 0 };
18229
18470
  }
18230
- const maxAge = policyValue.maxAgeDays ?? policyValue.max_age_days;
18471
+ const maxAge = policyValue2.maxAgeDays ?? policyValue2.max_age_days;
18231
18472
  if (maxAge === void 0) {
18232
18473
  return { maxAgeDays: void 0 };
18233
18474
  }
@@ -18548,14 +18789,26 @@ async function sharedContextCommand(options) {
18548
18789
  printSharedContextResult(result);
18549
18790
  }
18550
18791
  async function workflowRunCommand(options) {
18551
- ensureConfigured();
18552
- const client = createClient(2e4);
18553
- const adaptiveRoutingRequested = shouldBuildAdaptiveRouting(options);
18554
- const adaptiveRouting = adaptiveRoutingRequested ? 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;
18555
18808
  const orchestratorRecommendation = getOrchestratorRecommendation(options.query, options.mode, {
18556
18809
  policyAutoRoute: options.autoRouteOrchestrator,
18557
18810
  policySource: options.orchestratorPolicySource,
18558
- adaptiveRoutingDryRun: adaptiveRoutingRequested
18811
+ adaptiveRoutingDryRun: Boolean(adaptiveRouting)
18559
18812
  });
18560
18813
  const shouldEmitOrchestratorHandoff = options.emitOrchestratorHandoff || options.autoRouteOrchestrator;
18561
18814
  const preparedHandoff = shouldEmitOrchestratorHandoff && orchestratorRecommendation ? writeOrchestratorHandoff({
@@ -18567,6 +18820,38 @@ async function workflowRunCommand(options) {
18567
18820
  mode: options.mode,
18568
18821
  adaptiveRouting
18569
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
+ }
18570
18855
  if (options.mode === "orchestrate") {
18571
18856
  const result = await client.orchestrate(options.query, options.maxTokens);
18572
18857
  if (options.json) {
@@ -18745,22 +19030,72 @@ function shouldBuildAdaptiveRouting(options) {
18745
19030
  options.adaptiveRoutingDryRun || options.routeLocalWorkers || options.routingWorkerRole || options.routingPreferredEndpoints?.length || options.routingAllowedEndpoints?.length || options.plannerRetainsReasoning
18746
19031
  );
18747
19032
  }
18748
- function buildWorkflowAdaptiveRouting(options) {
19033
+ function buildWorkflowAdaptiveRouting(options, policy = null, intentWarnings = []) {
18749
19034
  const state = readWorkflowState2();
18750
19035
  const currentPhase = state ? currentWorkflowPhase(state) : void 0;
18751
- const preferredEndpointTypes = normalizeRoutingEndpointTypes([
19036
+ const initialPreferredEndpointTypes = normalizeRoutingEndpointTypes([
18752
19037
  ...options.routingPreferredEndpoints ?? [],
18753
- ...options.routeLocalWorkers ? ["local"] : []
19038
+ ...options.routeLocalWorkers ? ["local"] : [],
19039
+ ...policy?.preferredEndpointTypes ?? [],
19040
+ ...policy?.preferLocalWorkers ? ["local"] : []
18754
19041
  ]);
18755
- 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({
18756
19062
  query: options.query,
18757
19063
  mode: options.mode,
18758
19064
  changedFiles: currentPhase?.files ?? [],
18759
19065
  preferredEndpointTypes,
18760
- allowedEndpointTypes: normalizeRoutingEndpointTypes(options.routingAllowedEndpoints),
18761
- workerRole: stringValue5(options.routingWorkerRole),
18762
- 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
18763
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
+ };
18764
19099
  }
18765
19100
  function normalizeRoutingEndpointTypes(values) {
18766
19101
  return Array.from(
@@ -18769,6 +19104,60 @@ function normalizeRoutingEndpointTypes(values) {
18769
19104
  )
18770
19105
  ).sort();
18771
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
+ }
18772
19161
  function startManagedWorkflowState(options) {
18773
19162
  const existing = readWorkflowState2();
18774
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.21",
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": {