sortie-dogs 0.1.3 → 0.1.4

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
@@ -2,6 +2,9 @@
2
2
 
3
3
  **Give OpenCode a task; get a bounded, validated implementation loop instead of an open-ended agent run.**
4
4
 
5
+ > **Project status: Experimental / unsupported.** No stability, compatibility,
6
+ > or support guarantees are provided. Mk2A2 remains the canonical internal workflow.
7
+
5
8
  [![npm](https://img.shields.io/npm/v/sortie-dogs)](https://www.npmjs.com/package/sortie-dogs)
6
9
  [![license](https://img.shields.io/npm/l/sortie-dogs)](LICENSE)
7
10
  [![Node.js](https://img.shields.io/node/v/sortie-dogs)](https://www.npmjs.com/package/sortie-dogs)
@@ -161,9 +164,9 @@ scout evidence, and fewer unnecessary context or tool turns can reduce token
161
164
  use while preserving quality. Project-local routing can override either
162
165
  default.
163
166
 
164
- The `implementation`, `remediation`, and `blocker-resolution` roles always use
165
- the dedicated Sol worker; user configuration cannot replace those routes. For
166
- other explicitly routed roles, resolution is deterministic: Sortie-dogs tries
167
+ The `implementation`, `remediation`, `blocker-resolution`, and `dog-advisor`
168
+ roles always use dedicated Sol `xhigh`; user configuration cannot replace those
169
+ routes. For other explicitly routed roles, resolution is deterministic: Sortie-dogs tries
167
170
  the preferred target, then ordered fallbacks. Roles without either a built-in
168
171
  default or an explicit route keep OpenCode's already selected model.
169
172
 
@@ -177,8 +180,7 @@ default or an explicit route keep OpenCode's already selected model.
177
180
  "preferred": { "model": "openai/gpt-5.6-luna", "variant": "xhigh" }
178
181
  },
179
182
  "dog-advisor": {
180
- "preferred": { "model": "fable/opus", "variant": "thinking" },
181
- "fallback": [{ "model": "provider/general" }]
183
+ "preferred": { "model": "openai/gpt-5.6-sol", "variant": "xhigh" }
182
184
  },
183
185
  "dog-reviewer": {
184
186
  "preferred": { "model": "fable/opus", "variant": "thinking" },
@@ -187,6 +189,7 @@ default or an explicit route keep OpenCode's already selected model.
187
189
  },
188
190
  "modelCatalog": {
189
191
  "project": [
192
+ { "model": "openai/gpt-5.6-sol", "variants": ["xhigh"] },
190
193
  { "model": "openai/gpt-5.6-luna", "variants": ["xhigh"] },
191
194
  { "model": "fable/opus", "variants": ["thinking"] },
192
195
  { "model": "provider/general" }
@@ -195,12 +198,14 @@ default or an explicit route keep OpenCode's already selected model.
195
198
  }
196
199
  ```
197
200
 
198
- Save project configuration as `.opencode/sortie-dogs.json`. `modelCatalog`
201
+ Save project configuration as `.opencode/sortie-dogs.json`. The `dog-advisor`
202
+ entry above shows the built-in effective route and is not user-overridable.
203
+ `modelCatalog`
199
204
  declares provider models and named variants that are actually available;
200
205
  Sortie-dogs does not invent, probe, or translate variants. Resolution tries the
201
206
  preferred target and then its fallbacks, rejecting an explicitly routed role
202
- when no candidate appears in the catalog. The advisor and reviewer routes above
203
- are optional secondary examples; omit them when they are not needed.
207
+ when no candidate appears in the catalog. The advisor route is authoritative; the
208
+ reviewer route remains an optional secondary example.
204
209
 
205
210
  `dog-advisor` accepts bounded Strategy or SourceReview consultation from the
206
211
  coordinator. `dog-reviewer` independently checks high-risk candidates after
@@ -0,0 +1,145 @@
1
+ export declare const CONSULTATION_CAPABILITIES: readonly ["strategy", "sourceReview"];
2
+ export type ConsultationCapability = typeof CONSULTATION_CAPABILITIES[number];
3
+ export declare const CONSULTATION_ROLE_POLICY: Readonly<{
4
+ readonly strategy: "dog-advisor";
5
+ readonly sourceReview: "dog-reviewer";
6
+ }>;
7
+ export declare const STRATEGY_TRIGGERS: readonly ["architecture-choice", "cross-boundary-tradeoff", "material-uncertainty"];
8
+ export type StrategyTrigger = typeof STRATEGY_TRIGGERS[number];
9
+ export interface StrategyTriggerInput {
10
+ readonly candidateId: string;
11
+ readonly trigger?: StrategyTrigger;
12
+ readonly callsForCandidate: number;
13
+ readonly decisionAlreadyRecorded?: boolean;
14
+ readonly mechanicalChange?: boolean;
15
+ readonly sameTaskResume?: boolean;
16
+ }
17
+ /** Strategy is advisory, bounded to one call, and excluded when no design decision remains. */
18
+ export declare function shouldConsultStrategy(input: StrategyTriggerInput): boolean;
19
+ export declare const SOURCE_REVIEW_RISK_TAGS: readonly ["security", "credential", "permission", "network", "public-api", "storage-compatibility", "package", "build", "release", "migration", "concurrency", "process-io", "write-gate", "authorization"];
20
+ export type SourceReviewRiskTag = typeof SOURCE_REVIEW_RISK_TAGS[number];
21
+ export declare function isSourceReviewRiskTag(value: unknown): value is SourceReviewRiskTag;
22
+ export declare function requiresSourceReview(riskTags: readonly SourceReviewRiskTag[]): boolean;
23
+ export type SourceReviewRequirement = "SKIP_LOW_RISK" | "WAIT_CANONICAL_VALIDATION" | "REVIEW_REQUIRED" | "REVIEW_TOO_LATE" | "RISK_TAGS_INVALID";
24
+ export interface SourceReviewRequirementInput {
25
+ readonly riskTags: unknown;
26
+ readonly canonicalValidationExit?: number;
27
+ readonly stagingStarted: boolean;
28
+ }
29
+ /** Places required review strictly after canonical PASS and before staging. */
30
+ export declare function evaluateSourceReviewRequirement(input: SourceReviewRequirementInput): SourceReviewRequirement;
31
+ export type ReviewAvailability = {
32
+ readonly ok: true;
33
+ } | {
34
+ readonly ok: false;
35
+ readonly code: "REVIEW_UNAVAILABLE";
36
+ };
37
+ export declare function evaluateReviewAvailability(required: boolean, available: boolean): ReviewAvailability;
38
+ export interface StrategyConsultationRequest {
39
+ readonly requestId: string;
40
+ readonly candidateId: string;
41
+ readonly capability: "strategy";
42
+ readonly agent: string;
43
+ readonly question: string;
44
+ readonly constraints: readonly string[];
45
+ readonly options: readonly string[];
46
+ }
47
+ export interface ReviewArtifact {
48
+ readonly schemaVersion: 1;
49
+ readonly candidateId: string;
50
+ readonly sourceFingerprint: string;
51
+ readonly acceptance: readonly string[];
52
+ readonly manifest: readonly string[];
53
+ readonly riskTags: readonly SourceReviewRiskTag[];
54
+ readonly riskBearingHunks: readonly string[];
55
+ readonly validation: {
56
+ readonly command: string;
57
+ readonly exit: 0;
58
+ readonly fingerprint: string;
59
+ };
60
+ readonly invariants: readonly string[];
61
+ }
62
+ export interface SourceReviewConsultationRequest {
63
+ readonly requestId: string;
64
+ readonly candidateId: string;
65
+ readonly capability: "sourceReview";
66
+ readonly agent: string;
67
+ readonly artifact: ReviewArtifact;
68
+ }
69
+ export type ConsultationRequest = StrategyConsultationRequest | SourceReviewConsultationRequest;
70
+ export interface StrategyConsultationResult {
71
+ readonly requestId: string;
72
+ readonly candidateId: string;
73
+ readonly capability: "strategy";
74
+ readonly status: "completed";
75
+ readonly recommendation: string;
76
+ readonly considerations: readonly string[];
77
+ }
78
+ export interface UnavailableConsultationResult {
79
+ readonly requestId: string;
80
+ readonly candidateId: string;
81
+ readonly capability: ConsultationCapability;
82
+ readonly status: "unavailable";
83
+ }
84
+ export type ReviewVerdictKind = "PASS" | "MUST_FIX" | "BLOCKED";
85
+ export type ReviewFindingSeverity = "major" | "medium";
86
+ export interface ReviewFinding {
87
+ readonly severity: ReviewFindingSeverity;
88
+ readonly path: string;
89
+ readonly evidence: string;
90
+ readonly requiredFix: string;
91
+ }
92
+ export interface ReviewVerdict {
93
+ readonly verdict: ReviewVerdictKind;
94
+ readonly sourceFingerprint: string;
95
+ readonly findings: readonly ReviewFinding[];
96
+ }
97
+ export interface SourceReviewConsultationResult {
98
+ readonly requestId: string;
99
+ readonly candidateId: string;
100
+ readonly capability: "sourceReview";
101
+ readonly status: "completed";
102
+ readonly review: ReviewVerdict;
103
+ }
104
+ export type ConsultationResult = StrategyConsultationResult | SourceReviewConsultationResult | UnavailableConsultationResult;
105
+ /** Host-owned adapters implement transport. Core never executes commands or selects providers. */
106
+ export interface ConsultationAdapter {
107
+ consult(request: ConsultationRequest): Promise<ConsultationResult>;
108
+ }
109
+ export declare const MAX_REVIEW_ARTIFACT_BYTES = 30720;
110
+ export type ValidationResult<T> = {
111
+ readonly ok: true;
112
+ readonly value: T;
113
+ readonly bytes?: number;
114
+ } | {
115
+ readonly ok: false;
116
+ readonly code: string;
117
+ };
118
+ /** Strict bounded schema; unknown fields and opaque/raw payloads are rejected. */
119
+ export declare function validateReviewArtifact(value: unknown, maxBytes?: number): ValidationResult<ReviewArtifact>;
120
+ export declare function validateReviewVerdict(value: unknown): ValidationResult<ReviewVerdict>;
121
+ export interface ReviewGateInput {
122
+ readonly phase: "initial" | "verification";
123
+ readonly candidateId: string;
124
+ readonly currentSourceFingerprint: string;
125
+ readonly artifact: unknown;
126
+ readonly verdict: unknown;
127
+ readonly reviewedFingerprints: readonly string[];
128
+ /** The configured budget applies independently to each explicit review phase. */
129
+ readonly maxCallsPerCandidate: number;
130
+ readonly callsForPhase: number;
131
+ readonly initialVerdict?: ReviewVerdictKind;
132
+ readonly initialArtifact?: unknown;
133
+ readonly remediationApplied?: boolean;
134
+ readonly maxArtifactBytes?: number;
135
+ }
136
+ export type ReviewGateResult = {
137
+ readonly ok: true;
138
+ readonly permitStage: boolean;
139
+ readonly verdict: ReviewVerdictKind;
140
+ } | {
141
+ readonly ok: false;
142
+ readonly code: string;
143
+ };
144
+ /** Enforces one initial review and, only after remediation, one verification review. */
145
+ export declare function evaluateReviewGate(input: ReviewGateInput): ReviewGateResult;
@@ -0,0 +1,207 @@
1
+ export const CONSULTATION_CAPABILITIES = ["strategy", "sourceReview"];
2
+ export const CONSULTATION_ROLE_POLICY = Object.freeze({
3
+ strategy: "dog-advisor",
4
+ sourceReview: "dog-reviewer",
5
+ });
6
+ export const STRATEGY_TRIGGERS = [
7
+ "architecture-choice",
8
+ "cross-boundary-tradeoff",
9
+ "material-uncertainty",
10
+ ];
11
+ /** Strategy is advisory, bounded to one call, and excluded when no design decision remains. */
12
+ export function shouldConsultStrategy(input) {
13
+ return input.candidateId.length > 0 &&
14
+ input.trigger !== undefined &&
15
+ input.callsForCandidate < 1 &&
16
+ input.decisionAlreadyRecorded !== true &&
17
+ input.mechanicalChange !== true &&
18
+ input.sameTaskResume !== true;
19
+ }
20
+ export const SOURCE_REVIEW_RISK_TAGS = [
21
+ "security",
22
+ "credential",
23
+ "permission",
24
+ "network",
25
+ "public-api",
26
+ "storage-compatibility",
27
+ "package",
28
+ "build",
29
+ "release",
30
+ "migration",
31
+ "concurrency",
32
+ "process-io",
33
+ "write-gate",
34
+ "authorization",
35
+ ];
36
+ const riskTagSet = new Set(SOURCE_REVIEW_RISK_TAGS);
37
+ export function isSourceReviewRiskTag(value) {
38
+ return typeof value === "string" && riskTagSet.has(value);
39
+ }
40
+ export function requiresSourceReview(riskTags) {
41
+ return riskTags.length > 0;
42
+ }
43
+ /** Places required review strictly after canonical PASS and before staging. */
44
+ export function evaluateSourceReviewRequirement(input) {
45
+ if (!Array.isArray(input.riskTags))
46
+ return "RISK_TAGS_INVALID";
47
+ const riskTags = [...input.riskTags];
48
+ if (!riskTags.every(isSourceReviewRiskTag))
49
+ return "RISK_TAGS_INVALID";
50
+ if (!requiresSourceReview(riskTags))
51
+ return "SKIP_LOW_RISK";
52
+ if (input.canonicalValidationExit !== 0)
53
+ return "WAIT_CANONICAL_VALIDATION";
54
+ return input.stagingStarted ? "REVIEW_TOO_LATE" : "REVIEW_REQUIRED";
55
+ }
56
+ export function evaluateReviewAvailability(required, available) {
57
+ return required && !available ? { ok: false, code: "REVIEW_UNAVAILABLE" } : { ok: true };
58
+ }
59
+ export const MAX_REVIEW_ARTIFACT_BYTES = 30_720;
60
+ function isRecord(value) {
61
+ return value !== null && typeof value === "object" && !Array.isArray(value);
62
+ }
63
+ function hasExactKeys(value, keys) {
64
+ const actual = Object.keys(value).sort();
65
+ const expected = [...keys].sort();
66
+ return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
67
+ }
68
+ function isNonEmptyString(value) {
69
+ return typeof value === "string" && value.length > 0;
70
+ }
71
+ function isStringList(value) {
72
+ return Array.isArray(value) && value.every(isNonEmptyString);
73
+ }
74
+ /** Strict bounded schema; unknown fields and opaque/raw payloads are rejected. */
75
+ export function validateReviewArtifact(value, maxBytes = MAX_REVIEW_ARTIFACT_BYTES) {
76
+ let bytes;
77
+ try {
78
+ const serialized = JSON.stringify(value);
79
+ if (serialized === undefined)
80
+ return { ok: false, code: "ARTIFACT_NOT_JSON" };
81
+ bytes = Buffer.byteLength(serialized, "utf8");
82
+ }
83
+ catch {
84
+ return { ok: false, code: "ARTIFACT_NOT_JSON" };
85
+ }
86
+ if (!Number.isInteger(maxBytes) || maxBytes <= 0 || maxBytes > MAX_REVIEW_ARTIFACT_BYTES) {
87
+ return { ok: false, code: "ARTIFACT_LIMIT_INVALID" };
88
+ }
89
+ if (bytes > maxBytes)
90
+ return { ok: false, code: "ARTIFACT_TOO_LARGE" };
91
+ if (!isRecord(value) || !hasExactKeys(value, [
92
+ "schemaVersion",
93
+ "candidateId",
94
+ "sourceFingerprint",
95
+ "acceptance",
96
+ "manifest",
97
+ "riskTags",
98
+ "riskBearingHunks",
99
+ "validation",
100
+ "invariants",
101
+ ]))
102
+ return { ok: false, code: "ARTIFACT_SCHEMA_INVALID" };
103
+ if (value.schemaVersion !== 1 ||
104
+ !isNonEmptyString(value.candidateId) ||
105
+ !isNonEmptyString(value.sourceFingerprint) ||
106
+ !isStringList(value.acceptance) ||
107
+ !isStringList(value.manifest) ||
108
+ !Array.isArray(value.riskTags) ||
109
+ !value.riskTags.every(isSourceReviewRiskTag) ||
110
+ !isStringList(value.riskBearingHunks) ||
111
+ !isRecord(value.validation) ||
112
+ !hasExactKeys(value.validation, ["command", "exit", "fingerprint"]) ||
113
+ !isNonEmptyString(value.validation.command) ||
114
+ value.validation.exit !== 0 ||
115
+ !isNonEmptyString(value.validation.fingerprint) ||
116
+ !isStringList(value.invariants))
117
+ return { ok: false, code: "ARTIFACT_SCHEMA_INVALID" };
118
+ return { ok: true, value: value, bytes };
119
+ }
120
+ export function validateReviewVerdict(value) {
121
+ if (!isRecord(value) || !hasExactKeys(value, ["verdict", "sourceFingerprint", "findings"])) {
122
+ return { ok: false, code: "VERDICT_SCHEMA_INVALID" };
123
+ }
124
+ if (!["PASS", "MUST_FIX", "BLOCKED"].includes(value.verdict) ||
125
+ !isNonEmptyString(value.sourceFingerprint) ||
126
+ !Array.isArray(value.findings))
127
+ return { ok: false, code: "VERDICT_SCHEMA_INVALID" };
128
+ for (const finding of value.findings) {
129
+ if (!isRecord(finding) ||
130
+ !hasExactKeys(finding, ["severity", "path", "evidence", "requiredFix"]) ||
131
+ (finding.severity !== "major" && finding.severity !== "medium") ||
132
+ !isNonEmptyString(finding.path) ||
133
+ !isNonEmptyString(finding.evidence) ||
134
+ !isNonEmptyString(finding.requiredFix))
135
+ return { ok: false, code: "VERDICT_SCHEMA_INVALID" };
136
+ }
137
+ if (value.verdict === "PASS" && value.findings.length !== 0) {
138
+ return { ok: false, code: "PASS_WITH_FINDINGS" };
139
+ }
140
+ if (value.verdict !== "PASS" && value.findings.length === 0) {
141
+ return { ok: false, code: "NON_PASS_WITHOUT_FINDINGS" };
142
+ }
143
+ return { ok: true, value: value };
144
+ }
145
+ /** Enforces one initial review and, only after remediation, one verification review. */
146
+ export function evaluateReviewGate(input) {
147
+ if (!Number.isInteger(input.maxCallsPerCandidate) ||
148
+ input.maxCallsPerCandidate <= 0 ||
149
+ !Number.isInteger(input.callsForPhase) ||
150
+ input.callsForPhase < 0)
151
+ return { ok: false, code: "REVIEW_BUDGET_INVALID" };
152
+ if (input.callsForPhase >= input.maxCallsPerCandidate) {
153
+ return { ok: false, code: "REVIEW_BUDGET_EXHAUSTED" };
154
+ }
155
+ const artifact = validateReviewArtifact(input.artifact, input.maxArtifactBytes);
156
+ if (!artifact.ok)
157
+ return artifact;
158
+ const verdict = validateReviewVerdict(input.verdict);
159
+ if (!verdict.ok)
160
+ return verdict;
161
+ if (artifact.value.candidateId !== input.candidateId)
162
+ return { ok: false, code: "CANDIDATE_MISMATCH" };
163
+ if (artifact.value.sourceFingerprint !== input.currentSourceFingerprint) {
164
+ return { ok: false, code: "STALE_FINGERPRINT" };
165
+ }
166
+ if (verdict.value.sourceFingerprint !== artifact.value.sourceFingerprint) {
167
+ return { ok: false, code: "FINGERPRINT_MISMATCH" };
168
+ }
169
+ if (input.reviewedFingerprints.includes(artifact.value.sourceFingerprint)) {
170
+ return { ok: false, code: "FINGERPRINT_REUSED" };
171
+ }
172
+ if (input.phase === "initial") {
173
+ if (input.reviewedFingerprints.length !== 0)
174
+ return { ok: false, code: "REVIEW_LIMIT_REACHED" };
175
+ }
176
+ else if (input.reviewedFingerprints.length !== 1 ||
177
+ input.initialVerdict !== "MUST_FIX" ||
178
+ input.remediationApplied !== true) {
179
+ return { ok: false, code: "VERIFICATION_NOT_ALLOWED" };
180
+ }
181
+ else {
182
+ const initialArtifact = validateReviewArtifact(input.initialArtifact, input.maxArtifactBytes);
183
+ if (!initialArtifact.ok)
184
+ return { ok: false, code: "INITIAL_ARTIFACT_INVALID" };
185
+ if (!sameReviewScope(initialArtifact.value, artifact.value)) {
186
+ return { ok: false, code: "REVIEW_SCOPE_MISMATCH" };
187
+ }
188
+ }
189
+ return {
190
+ ok: true,
191
+ permitStage: verdict.value.verdict === "PASS",
192
+ verdict: verdict.value.verdict,
193
+ };
194
+ }
195
+ function sameStringList(left, right) {
196
+ return left.length === right.length && left.every((value, index) => value === right[index]);
197
+ }
198
+ function sameReviewScope(initial, verification) {
199
+ return initial.schemaVersion === verification.schemaVersion &&
200
+ initial.candidateId === verification.candidateId &&
201
+ sameStringList(initial.acceptance, verification.acceptance) &&
202
+ sameStringList(initial.manifest, verification.manifest) &&
203
+ sameStringList(initial.riskTags, verification.riskTags) &&
204
+ sameStringList(initial.riskBearingHunks, verification.riskBearingHunks) &&
205
+ sameStringList(initial.invariants, verification.invariants) &&
206
+ initial.validation.command === verification.validation.command;
207
+ }
package/dist/index.d.ts CHANGED
@@ -2,4 +2,6 @@ export { lintHandoff } from "./core/validate-semantics.js";
2
2
  export { initializeProject, ProjectInitializationError, } from "./core/initialize.js";
3
3
  export type { InitializationStatus, InitializeProjectResult, ProjectInitializationErrorCode, } from "./core/initialize.js";
4
4
  export { SortieDogsPlugin } from "./plugin/index.js";
5
+ export { CONSULTATION_CAPABILITIES, CONSULTATION_ROLE_POLICY, MAX_REVIEW_ARTIFACT_BYTES, SOURCE_REVIEW_RISK_TAGS, STRATEGY_TRIGGERS, evaluateReviewAvailability, evaluateReviewGate, evaluateSourceReviewRequirement, isSourceReviewRiskTag, requiresSourceReview, shouldConsultStrategy, validateReviewArtifact, validateReviewVerdict, } from "./core/consultation.js";
6
+ export type { ConsultationAdapter, ConsultationCapability, ConsultationRequest, ConsultationResult, ReviewArtifact, ReviewAvailability, ReviewFinding, ReviewFindingSeverity, ReviewGateInput, ReviewGateResult, ReviewVerdict, ReviewVerdictKind, SourceReviewConsultationRequest, SourceReviewConsultationResult, SourceReviewRequirement, SourceReviewRequirementInput, SourceReviewRiskTag, StrategyConsultationRequest, StrategyConsultationResult, StrategyTrigger, StrategyTriggerInput, UnavailableConsultationResult, ValidationResult, } from "./core/consultation.js";
5
7
  export type * from "./core/types.js";
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { lintHandoff } from "./core/validate-semantics.js";
2
2
  export { initializeProject, ProjectInitializationError, } from "./core/initialize.js";
3
3
  export { SortieDogsPlugin } from "./plugin/index.js";
4
+ export { CONSULTATION_CAPABILITIES, CONSULTATION_ROLE_POLICY, MAX_REVIEW_ARTIFACT_BYTES, SOURCE_REVIEW_RISK_TAGS, STRATEGY_TRIGGERS, evaluateReviewAvailability, evaluateReviewGate, evaluateSourceReviewRequirement, isSourceReviewRiskTag, requiresSourceReview, shouldConsultStrategy, validateReviewArtifact, validateReviewVerdict, } from "./core/consultation.js";
@@ -4,6 +4,28 @@ export interface SortieDogsPluginOptions {
4
4
  handoffPaths?: readonly string[];
5
5
  modelRouting?: ModelRoutingConfig;
6
6
  modelCatalog?: ModelCatalog;
7
+ consultation?: ConsultationPolicyInput;
8
+ }
9
+ export interface ConsultationPolicyInput {
10
+ readonly strategy?: Partial<StrategyConsultationPolicy>;
11
+ readonly sourceReview?: Partial<SourceReviewConsultationPolicy>;
12
+ }
13
+ export interface StrategyConsultationPolicy {
14
+ readonly agent: string;
15
+ readonly required: boolean;
16
+ readonly maxCallsPerCandidate: number;
17
+ }
18
+ export interface SourceReviewConsultationPolicy {
19
+ readonly agent: string;
20
+ readonly requiredPolicy: "risk-based";
21
+ readonly unavailable: "block-required-only";
22
+ /** Maximum calls in each explicit initial or verification phase. */
23
+ readonly maxCallsPerCandidate: number;
24
+ readonly maxArtifactBytes: number;
25
+ }
26
+ export interface ConsultationPolicy {
27
+ readonly strategy: StrategyConsultationPolicy;
28
+ readonly sourceReview: SourceReviewConsultationPolicy;
7
29
  }
8
30
  export interface ConfiguredPlugin {
9
31
  kind: "configured";
@@ -11,6 +33,7 @@ export interface ConfiguredPlugin {
11
33
  handoffPaths: readonly string[];
12
34
  modelRouting: ModelRoutingConfig;
13
35
  modelCatalog: ModelCatalog;
36
+ consultation: ConsultationPolicy;
14
37
  }
15
38
  export type PluginConfiguration = ConfiguredPlugin | {
16
39
  kind: "invalid";
@@ -22,7 +45,9 @@ export interface ConfiguredPluginSources extends ConfiguredPlugin {
22
45
  export type PluginConfigurationSources = ConfiguredPluginSources | {
23
46
  kind: "invalid";
24
47
  };
25
- export declare const DEFAULT_PLUGIN_OPTIONS: Readonly<Required<SortieDogsPluginOptions>>;
48
+ export declare const DEFAULT_PLUGIN_OPTIONS: Readonly<Omit<Required<SortieDogsPluginOptions>, "consultation"> & {
49
+ consultation: ConsultationPolicy;
50
+ }>;
26
51
  /** Merge defaults, optional project/env configuration, then the host override. */
27
52
  export declare function resolvePluginConfiguration(...values: readonly unknown[]): PluginConfiguration;
28
53
  /** Resolve the plugin's fixed source boundaries: project-local first, environment and host global. */
@@ -1,9 +1,24 @@
1
- import { BUILT_IN_MODEL_CATALOG, DEDICATED_SOL_ROUTING, RECOMMENDED_LUNA_ROUTING, isDedicatedSolRole, parseModelRoutingConfig, } from "./model-routing.js";
1
+ import { BUILT_IN_MODEL_CATALOG, FIXED_MODEL_ROUTING, RECOMMENDED_LUNA_ROUTING, isFixedModelRole, parseModelRoutingConfig, } from "./model-routing.js";
2
+ import { CONSULTATION_ROLE_POLICY } from "../core/consultation.js";
2
3
  export const DEFAULT_PLUGIN_OPTIONS = {
3
4
  operationManifestPath: "operation-manifest.json",
4
5
  handoffPaths: ["handoff.json"],
5
6
  modelRouting: RECOMMENDED_LUNA_ROUTING,
6
7
  modelCatalog: BUILT_IN_MODEL_CATALOG,
8
+ consultation: Object.freeze({
9
+ strategy: Object.freeze({
10
+ agent: CONSULTATION_ROLE_POLICY.strategy,
11
+ required: false,
12
+ maxCallsPerCandidate: 1,
13
+ }),
14
+ sourceReview: Object.freeze({
15
+ agent: CONSULTATION_ROLE_POLICY.sourceReview,
16
+ requiredPolicy: "risk-based",
17
+ unavailable: "block-required-only",
18
+ maxCallsPerCandidate: 1,
19
+ maxArtifactBytes: 30_720,
20
+ }),
21
+ }),
7
22
  };
8
23
  function isRecord(value) {
9
24
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -11,6 +26,73 @@ function isRecord(value) {
11
26
  function nonEmptyString(value) {
12
27
  return typeof value === "string" && value.length > 0;
13
28
  }
29
+ function positiveInteger(value) {
30
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
31
+ }
32
+ function parseConsultationPolicy(value) {
33
+ if (!isRecord(value) || Object.keys(value).some((key) => key !== "strategy" && key !== "sourceReview")) {
34
+ return undefined;
35
+ }
36
+ let strategy;
37
+ if (value.strategy !== undefined) {
38
+ if (!isRecord(value.strategy) || Object.keys(value.strategy).some((key) => key !== "agent" && key !== "required" && key !== "maxCallsPerCandidate"))
39
+ return undefined;
40
+ if (value.strategy.agent !== undefined &&
41
+ value.strategy.agent !== CONSULTATION_ROLE_POLICY.strategy)
42
+ return undefined;
43
+ if (value.strategy.required !== undefined && typeof value.strategy.required !== "boolean")
44
+ return undefined;
45
+ if (value.strategy.maxCallsPerCandidate !== undefined && !positiveInteger(value.strategy.maxCallsPerCandidate)) {
46
+ return undefined;
47
+ }
48
+ strategy = Object.freeze({
49
+ ...(value.strategy.agent === undefined ? {} : { agent: value.strategy.agent }),
50
+ ...(value.strategy.required === undefined ? {} : { required: value.strategy.required }),
51
+ ...(value.strategy.maxCallsPerCandidate === undefined
52
+ ? {}
53
+ : { maxCallsPerCandidate: value.strategy.maxCallsPerCandidate }),
54
+ });
55
+ }
56
+ let sourceReview;
57
+ if (value.sourceReview !== undefined) {
58
+ if (!isRecord(value.sourceReview) || Object.keys(value.sourceReview).some((key) => !["agent", "requiredPolicy", "unavailable", "maxCallsPerCandidate", "maxArtifactBytes"].includes(key)))
59
+ return undefined;
60
+ if (value.sourceReview.agent !== undefined &&
61
+ value.sourceReview.agent !== CONSULTATION_ROLE_POLICY.sourceReview)
62
+ return undefined;
63
+ if (value.sourceReview.requiredPolicy !== undefined && value.sourceReview.requiredPolicy !== "risk-based") {
64
+ return undefined;
65
+ }
66
+ if (value.sourceReview.unavailable !== undefined && value.sourceReview.unavailable !== "block-required-only") {
67
+ return undefined;
68
+ }
69
+ if (value.sourceReview.maxCallsPerCandidate !== undefined &&
70
+ !positiveInteger(value.sourceReview.maxCallsPerCandidate))
71
+ return undefined;
72
+ if (value.sourceReview.maxArtifactBytes !== undefined &&
73
+ (!positiveInteger(value.sourceReview.maxArtifactBytes) || value.sourceReview.maxArtifactBytes > 30_720))
74
+ return undefined;
75
+ sourceReview = Object.freeze({
76
+ ...(value.sourceReview.agent === undefined ? {} : { agent: value.sourceReview.agent }),
77
+ ...(value.sourceReview.requiredPolicy === undefined
78
+ ? {}
79
+ : { requiredPolicy: value.sourceReview.requiredPolicy }),
80
+ ...(value.sourceReview.unavailable === undefined
81
+ ? {}
82
+ : { unavailable: value.sourceReview.unavailable }),
83
+ ...(value.sourceReview.maxCallsPerCandidate === undefined
84
+ ? {}
85
+ : { maxCallsPerCandidate: value.sourceReview.maxCallsPerCandidate }),
86
+ ...(value.sourceReview.maxArtifactBytes === undefined
87
+ ? {}
88
+ : { maxArtifactBytes: value.sourceReview.maxArtifactBytes }),
89
+ });
90
+ }
91
+ return Object.freeze({
92
+ ...(strategy === undefined ? {} : { strategy }),
93
+ ...(sourceReview === undefined ? {} : { sourceReview }),
94
+ });
95
+ }
14
96
  function parseCatalogModels(value) {
15
97
  if (!Array.isArray(value))
16
98
  return undefined;
@@ -74,7 +156,7 @@ function parseLayer(value) {
74
156
  return {};
75
157
  if (!isRecord(value))
76
158
  return undefined;
77
- if (Object.keys(value).some((key) => key !== "operationManifestPath" && key !== "handoffPaths" && key !== "modelRouting" && key !== "modelCatalog")) {
159
+ if (Object.keys(value).some((key) => !["operationManifestPath", "handoffPaths", "modelRouting", "modelCatalog", "consultation"].includes(key))) {
78
160
  return undefined;
79
161
  }
80
162
  const manifestPath = value.operationManifestPath;
@@ -85,6 +167,9 @@ function parseLayer(value) {
85
167
  const modelCatalog = value.modelCatalog === undefined
86
168
  ? undefined
87
169
  : parseModelCatalog(value.modelCatalog);
170
+ const consultation = value.consultation === undefined
171
+ ? undefined
172
+ : parseConsultationPolicy(value.consultation);
88
173
  if (manifestPath !== undefined && (typeof manifestPath !== "string" || manifestPath.length === 0)) {
89
174
  return undefined;
90
175
  }
@@ -96,11 +181,14 @@ function parseLayer(value) {
96
181
  return undefined;
97
182
  if (value.modelCatalog !== undefined && modelCatalog === undefined)
98
183
  return undefined;
184
+ if (value.consultation !== undefined && consultation === undefined)
185
+ return undefined;
99
186
  return {
100
187
  operationManifestPath: manifestPath,
101
188
  handoffPaths: handoffPaths,
102
189
  modelRouting,
103
190
  modelCatalog,
191
+ consultation,
104
192
  };
105
193
  }
106
194
  /** Merge defaults, optional project/env configuration, then the host override. */
@@ -109,6 +197,7 @@ export function resolvePluginConfiguration(...values) {
109
197
  let handoffPaths = DEFAULT_PLUGIN_OPTIONS.handoffPaths;
110
198
  let modelRouting = DEFAULT_PLUGIN_OPTIONS.modelRouting;
111
199
  let modelCatalog = DEFAULT_PLUGIN_OPTIONS.modelCatalog;
200
+ let consultation = DEFAULT_PLUGIN_OPTIONS.consultation;
112
201
  for (const value of values) {
113
202
  const layer = parseLayer(value);
114
203
  if (layer === undefined)
@@ -129,7 +218,17 @@ export function resolvePluginConfiguration(...values) {
129
218
  }),
130
219
  };
131
220
  }
221
+ if (layer.consultation !== undefined) {
222
+ consultation = Object.freeze({
223
+ strategy: Object.freeze({ ...consultation.strategy, ...(layer.consultation.strategy ?? {}) }),
224
+ sourceReview: Object.freeze({ ...consultation.sourceReview, ...(layer.consultation.sourceReview ?? {}) }),
225
+ });
226
+ }
132
227
  }
228
+ modelRouting = {
229
+ ...Object.fromEntries(Object.entries(modelRouting).filter(([role]) => !isFixedModelRole(role))),
230
+ ...FIXED_MODEL_ROUTING,
231
+ };
133
232
  const hasRouting = Object.keys(modelRouting).length > 0;
134
233
  const hasCatalogEntries = (modelCatalog.project?.length ?? 0) + (modelCatalog.global?.length ?? 0) > 0;
135
234
  if (hasRouting && !hasCatalogEntries)
@@ -140,6 +239,7 @@ export function resolvePluginConfiguration(...values) {
140
239
  handoffPaths,
141
240
  modelRouting,
142
241
  modelCatalog,
242
+ consultation,
143
243
  };
144
244
  }
145
245
  /** Resolve the plugin's fixed source boundaries: project-local first, environment and host global. */
@@ -157,17 +257,17 @@ export function resolvePluginConfigurationSources(projectValue, environmentValue
157
257
  ...RECOMMENDED_LUNA_ROUTING,
158
258
  ...(environmentLayer.modelRouting ?? {}),
159
259
  ...(hostLayer.modelRouting ?? {}),
160
- }).filter(([role]) => !isDedicatedSolRole(role)));
260
+ }).filter(([role]) => !isFixedModelRole(role)));
161
261
  const modelRouting = {
162
262
  ...Object.fromEntries(Object.entries(configured.modelRouting)
163
- .filter(([role]) => !isDedicatedSolRole(role))),
164
- ...DEDICATED_SOL_ROUTING,
263
+ .filter(([role]) => !isFixedModelRole(role))),
264
+ ...FIXED_MODEL_ROUTING,
165
265
  };
166
266
  return {
167
267
  ...configured,
168
268
  modelRouting,
169
- // Mk2A2 worker policy is authoritative over every configurable layer.
170
- localModelRouting: { ...(projectLayer.modelRouting ?? {}), ...DEDICATED_SOL_ROUTING },
269
+ // Dedicated worker policy is authoritative over every configurable layer.
270
+ localModelRouting: { ...(projectLayer.modelRouting ?? {}), ...FIXED_MODEL_ROUTING },
171
271
  globalModelRouting,
172
272
  };
173
273
  }