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 +13 -8
- package/dist/core/consultation.d.ts +145 -0
- package/dist/core/consultation.js +207 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/plugin/config.d.ts +26 -1
- package/dist/plugin/config.js +107 -7
- package/dist/plugin/gate.d.ts +2 -0
- package/dist/plugin/gate.js +86 -2
- package/dist/plugin/index.js +210 -47
- package/dist/plugin/model-routing.d.ts +5 -2
- package/dist/plugin/model-routing.js +35 -20
- package/dist/runtime-assets.d.ts +5 -5
- package/dist/runtime-assets.js +116 -31
- package/package.json +1 -1
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
|
[](https://www.npmjs.com/package/sortie-dogs)
|
|
6
9
|
[](LICENSE)
|
|
7
10
|
[](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`,
|
|
165
|
-
|
|
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": "
|
|
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`. `
|
|
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
|
|
203
|
-
|
|
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";
|
package/dist/plugin/config.d.ts
CHANGED
|
@@ -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. */
|
package/dist/plugin/config.js
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
|
-
import { BUILT_IN_MODEL_CATALOG,
|
|
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) =>
|
|
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]) => !
|
|
260
|
+
}).filter(([role]) => !isFixedModelRole(role)));
|
|
161
261
|
const modelRouting = {
|
|
162
262
|
...Object.fromEntries(Object.entries(configured.modelRouting)
|
|
163
|
-
.filter(([role]) => !
|
|
164
|
-
...
|
|
263
|
+
.filter(([role]) => !isFixedModelRole(role))),
|
|
264
|
+
...FIXED_MODEL_ROUTING,
|
|
165
265
|
};
|
|
166
266
|
return {
|
|
167
267
|
...configured,
|
|
168
268
|
modelRouting,
|
|
169
|
-
//
|
|
170
|
-
localModelRouting: { ...(projectLayer.modelRouting ?? {}), ...
|
|
269
|
+
// Dedicated worker policy is authoritative over every configurable layer.
|
|
270
|
+
localModelRouting: { ...(projectLayer.modelRouting ?? {}), ...FIXED_MODEL_ROUTING },
|
|
171
271
|
globalModelRouting,
|
|
172
272
|
};
|
|
173
273
|
}
|