sortie-dogs 0.1.3 → 0.1.11
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 +62 -8
- package/dist/asset-version.d.ts +6 -0
- package/dist/asset-version.js +5 -0
- package/dist/core/consultation.d.ts +148 -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 +39 -2
- package/dist/plugin/config.js +143 -7
- package/dist/plugin/gate.d.ts +17 -1
- package/dist/plugin/gate.js +324 -17
- package/dist/plugin/index.d.ts +9 -0
- package/dist/plugin/index.js +763 -76
- package/dist/plugin/model-routing-hook.d.ts +2 -1
- package/dist/plugin/model-routing-hook.js +1 -0
- package/dist/plugin/model-routing.d.ts +16 -4
- package/dist/plugin/model-routing.js +65 -28
- package/dist/runtime-assets.d.ts +7 -6
- package/dist/runtime-assets.js +291 -44
- 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)
|
|
@@ -77,6 +80,55 @@ OpenCode discovers the bridge automatically; no `plugin` entry in
|
|
|
77
80
|
|
|
78
81
|
Selecting `dog-coordinator` directly also activates the workflow.
|
|
79
82
|
|
|
83
|
+
## The write gate
|
|
84
|
+
|
|
85
|
+
The write gate is opt-in per project. Without `operation-manifest.json` in the
|
|
86
|
+
project root, the plugin stays passive and never denies a tool call. Creating
|
|
87
|
+
that file is how a project opts in, so the coordinator can always create it.
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"version": "0.1.0",
|
|
92
|
+
"task_id": "add-requested-behavior",
|
|
93
|
+
"read": ["src/feature.ts", "test/feature.test.ts"],
|
|
94
|
+
"write": ["src/feature.ts", "test/feature.test.ts"],
|
|
95
|
+
"validation": ["npm test"]
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- `write` lists the only paths a bound worker may change. A listed directory
|
|
100
|
+
covers the files under it; every other entry is an exact path.
|
|
101
|
+
- `validation` lists the exact commands a bound worker may run. Build and test
|
|
102
|
+
commands cannot be classified by path, so a command is allowed only when it
|
|
103
|
+
matches a declared entry exactly. Anything else is denied as unclassified.
|
|
104
|
+
- `read` documents the intended reading scope; reads are never blocked.
|
|
105
|
+
|
|
106
|
+
`dog-coordinator` owns this file. A worker binds to it once per candidate with
|
|
107
|
+
`sortie_bind_write_gate`, and only after the coordinator's handoff has been
|
|
108
|
+
inspected. Coordinator sessions are never gated.
|
|
109
|
+
|
|
110
|
+
Optional settings in `.opencode/sortie-dogs.json`:
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"operationManifestPath": "operation-manifest.json",
|
|
115
|
+
"handoffPaths": ["handoff.json"],
|
|
116
|
+
"readOnlyTools": ["my_mcp_search"],
|
|
117
|
+
"dedicatedWorkerModel": { "model": "provider/model", "variant": "deep" }
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
- `operationManifestPath` moves the manifest; the path is project-relative.
|
|
122
|
+
- `handoffPaths` lists the handoff files the plugin inspects. A worker can only
|
|
123
|
+
bind after one of these files passes inspection, so an empty list disables
|
|
124
|
+
binding entirely.
|
|
125
|
+
- `readOnlyTools` adds host-specific tool names that never change files, such as
|
|
126
|
+
MCP tools. Unknown tools are denied for a bound session by default.
|
|
127
|
+
- `dedicatedWorkerModel` selects the single model every worker role resolves to.
|
|
128
|
+
It defaults to `openai/gpt-5.6-sol` with variant `xhigh`; declare your own when
|
|
129
|
+
that model is unavailable. Worker roles always resolve to this one target and
|
|
130
|
+
cannot be routed per role.
|
|
131
|
+
|
|
80
132
|
## Why Sortie-dogs
|
|
81
133
|
|
|
82
134
|
- **Focused when invited, invisible otherwise.** Activate it with `/sortie` or
|
|
@@ -161,9 +213,9 @@ scout evidence, and fewer unnecessary context or tool turns can reduce token
|
|
|
161
213
|
use while preserving quality. Project-local routing can override either
|
|
162
214
|
default.
|
|
163
215
|
|
|
164
|
-
The `implementation`, `remediation`,
|
|
165
|
-
|
|
166
|
-
other explicitly routed roles, resolution is deterministic: Sortie-dogs tries
|
|
216
|
+
The `implementation`, `remediation`, `blocker-resolution`, and `dog-advisor`
|
|
217
|
+
roles always use dedicated Sol `xhigh`; user configuration cannot replace those
|
|
218
|
+
routes. For other explicitly routed roles, resolution is deterministic: Sortie-dogs tries
|
|
167
219
|
the preferred target, then ordered fallbacks. Roles without either a built-in
|
|
168
220
|
default or an explicit route keep OpenCode's already selected model.
|
|
169
221
|
|
|
@@ -177,8 +229,7 @@ default or an explicit route keep OpenCode's already selected model.
|
|
|
177
229
|
"preferred": { "model": "openai/gpt-5.6-luna", "variant": "xhigh" }
|
|
178
230
|
},
|
|
179
231
|
"dog-advisor": {
|
|
180
|
-
"preferred": { "model": "
|
|
181
|
-
"fallback": [{ "model": "provider/general" }]
|
|
232
|
+
"preferred": { "model": "openai/gpt-5.6-sol", "variant": "xhigh" }
|
|
182
233
|
},
|
|
183
234
|
"dog-reviewer": {
|
|
184
235
|
"preferred": { "model": "fable/opus", "variant": "thinking" },
|
|
@@ -187,6 +238,7 @@ default or an explicit route keep OpenCode's already selected model.
|
|
|
187
238
|
},
|
|
188
239
|
"modelCatalog": {
|
|
189
240
|
"project": [
|
|
241
|
+
{ "model": "openai/gpt-5.6-sol", "variants": ["xhigh"] },
|
|
190
242
|
{ "model": "openai/gpt-5.6-luna", "variants": ["xhigh"] },
|
|
191
243
|
{ "model": "fable/opus", "variants": ["thinking"] },
|
|
192
244
|
{ "model": "provider/general" }
|
|
@@ -195,12 +247,14 @@ default or an explicit route keep OpenCode's already selected model.
|
|
|
195
247
|
}
|
|
196
248
|
```
|
|
197
249
|
|
|
198
|
-
Save project configuration as `.opencode/sortie-dogs.json`. `
|
|
250
|
+
Save project configuration as `.opencode/sortie-dogs.json`. The `dog-advisor`
|
|
251
|
+
entry above shows the built-in effective route and is not user-overridable.
|
|
252
|
+
`modelCatalog`
|
|
199
253
|
declares provider models and named variants that are actually available;
|
|
200
254
|
Sortie-dogs does not invent, probe, or translate variants. Resolution tries the
|
|
201
255
|
preferred target and then its fallbacks, rejecting an explicitly routed role
|
|
202
|
-
when no candidate appears in the catalog. The advisor
|
|
203
|
-
|
|
256
|
+
when no candidate appears in the catalog. The advisor route is authoritative; the
|
|
257
|
+
reviewer route remains an optional secondary example.
|
|
204
258
|
|
|
205
259
|
`dog-advisor` accepts bounded Strategy or SourceReview consultation from the
|
|
206
260
|
coordinator. `dog-reviewer` independently checks high-risk candidates after
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version of the installable runtime assets. Kept in its own module so the plugin can compare an
|
|
3
|
+
* installed project marker without importing every asset body.
|
|
4
|
+
*/
|
|
5
|
+
export declare const RUNTIME_ASSET_VERSION = "0.2.0-card05";
|
|
6
|
+
export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION;
|
|
@@ -0,0 +1,148 @@
|
|
|
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
|
+
/**
|
|
106
|
+
* Sole consultation transport boundary. The host supplies this adapter; core passes only the
|
|
107
|
+
* provider-, model-, variant-, and transport-neutral request and result envelopes above.
|
|
108
|
+
*/
|
|
109
|
+
export interface ConsultationAdapter {
|
|
110
|
+
consult(request: ConsultationRequest): Promise<ConsultationResult>;
|
|
111
|
+
}
|
|
112
|
+
export declare const MAX_REVIEW_ARTIFACT_BYTES = 30720;
|
|
113
|
+
export type ValidationResult<T> = {
|
|
114
|
+
readonly ok: true;
|
|
115
|
+
readonly value: T;
|
|
116
|
+
readonly bytes?: number;
|
|
117
|
+
} | {
|
|
118
|
+
readonly ok: false;
|
|
119
|
+
readonly code: string;
|
|
120
|
+
};
|
|
121
|
+
/** Strict bounded schema; unknown fields and opaque/raw payloads are rejected. */
|
|
122
|
+
export declare function validateReviewArtifact(value: unknown, maxBytes?: number): ValidationResult<ReviewArtifact>;
|
|
123
|
+
export declare function validateReviewVerdict(value: unknown): ValidationResult<ReviewVerdict>;
|
|
124
|
+
export interface ReviewGateInput {
|
|
125
|
+
readonly phase: "initial" | "verification";
|
|
126
|
+
readonly candidateId: string;
|
|
127
|
+
readonly currentSourceFingerprint: string;
|
|
128
|
+
readonly artifact: unknown;
|
|
129
|
+
readonly verdict: unknown;
|
|
130
|
+
readonly reviewedFingerprints: readonly string[];
|
|
131
|
+
/** The configured budget applies independently to each explicit review phase. */
|
|
132
|
+
readonly maxCallsPerCandidate: number;
|
|
133
|
+
readonly callsForPhase: number;
|
|
134
|
+
readonly initialVerdict?: ReviewVerdictKind;
|
|
135
|
+
readonly initialArtifact?: unknown;
|
|
136
|
+
readonly remediationApplied?: boolean;
|
|
137
|
+
readonly maxArtifactBytes?: number;
|
|
138
|
+
}
|
|
139
|
+
export type ReviewGateResult = {
|
|
140
|
+
readonly ok: true;
|
|
141
|
+
readonly permitStage: boolean;
|
|
142
|
+
readonly verdict: ReviewVerdictKind;
|
|
143
|
+
} | {
|
|
144
|
+
readonly ok: false;
|
|
145
|
+
readonly code: string;
|
|
146
|
+
};
|
|
147
|
+
/** Enforces one initial review and, only after remediation, one verification review. */
|
|
148
|
+
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
|
@@ -1,16 +1,51 @@
|
|
|
1
|
-
import { type ModelCatalog, type ModelRoutingConfig } from "./model-routing.js";
|
|
1
|
+
import { type ModelCatalog, type ModelRoutingConfig, type ModelTarget } from "./model-routing.js";
|
|
2
2
|
export interface SortieDogsPluginOptions {
|
|
3
3
|
operationManifestPath?: string;
|
|
4
4
|
handoffPaths?: readonly string[];
|
|
5
|
+
/**
|
|
6
|
+
* Host-specific tool names that never change project files, such as MCP tools. Names accumulate
|
|
7
|
+
* across layers because each layer describes a different part of the same environment.
|
|
8
|
+
*/
|
|
9
|
+
readOnlyTools?: readonly string[];
|
|
10
|
+
/**
|
|
11
|
+
* The single model every dedicated worker role resolves to. Worker routing stays fixed to one
|
|
12
|
+
* target; a host that cannot serve the shipped target declares its own here.
|
|
13
|
+
*/
|
|
14
|
+
dedicatedWorkerModel?: ModelTarget;
|
|
5
15
|
modelRouting?: ModelRoutingConfig;
|
|
6
16
|
modelCatalog?: ModelCatalog;
|
|
17
|
+
consultation?: ConsultationPolicyInput;
|
|
18
|
+
}
|
|
19
|
+
export interface ConsultationPolicyInput {
|
|
20
|
+
readonly strategy?: Partial<StrategyConsultationPolicy>;
|
|
21
|
+
readonly sourceReview?: Partial<SourceReviewConsultationPolicy>;
|
|
22
|
+
}
|
|
23
|
+
export interface StrategyConsultationPolicy {
|
|
24
|
+
readonly agent: string;
|
|
25
|
+
readonly required: boolean;
|
|
26
|
+
readonly maxCallsPerCandidate: number;
|
|
27
|
+
}
|
|
28
|
+
export interface SourceReviewConsultationPolicy {
|
|
29
|
+
readonly agent: string;
|
|
30
|
+
readonly requiredPolicy: "risk-based";
|
|
31
|
+
readonly unavailable: "block-required-only";
|
|
32
|
+
/** Maximum calls in each explicit initial or verification phase. */
|
|
33
|
+
readonly maxCallsPerCandidate: number;
|
|
34
|
+
readonly maxArtifactBytes: number;
|
|
35
|
+
}
|
|
36
|
+
export interface ConsultationPolicy {
|
|
37
|
+
readonly strategy: StrategyConsultationPolicy;
|
|
38
|
+
readonly sourceReview: SourceReviewConsultationPolicy;
|
|
7
39
|
}
|
|
8
40
|
export interface ConfiguredPlugin {
|
|
9
41
|
kind: "configured";
|
|
10
42
|
operationManifestPath: string;
|
|
11
43
|
handoffPaths: readonly string[];
|
|
44
|
+
readOnlyTools: readonly string[];
|
|
45
|
+
dedicatedWorkerModel: ModelTarget;
|
|
12
46
|
modelRouting: ModelRoutingConfig;
|
|
13
47
|
modelCatalog: ModelCatalog;
|
|
48
|
+
consultation: ConsultationPolicy;
|
|
14
49
|
}
|
|
15
50
|
export type PluginConfiguration = ConfiguredPlugin | {
|
|
16
51
|
kind: "invalid";
|
|
@@ -22,7 +57,9 @@ export interface ConfiguredPluginSources extends ConfiguredPlugin {
|
|
|
22
57
|
export type PluginConfigurationSources = ConfiguredPluginSources | {
|
|
23
58
|
kind: "invalid";
|
|
24
59
|
};
|
|
25
|
-
export declare const DEFAULT_PLUGIN_OPTIONS: Readonly<Required<SortieDogsPluginOptions
|
|
60
|
+
export declare const DEFAULT_PLUGIN_OPTIONS: Readonly<Omit<Required<SortieDogsPluginOptions>, "consultation"> & {
|
|
61
|
+
consultation: ConsultationPolicy;
|
|
62
|
+
}>;
|
|
26
63
|
/** Merge defaults, optional project/env configuration, then the host override. */
|
|
27
64
|
export declare function resolvePluginConfiguration(...values: readonly unknown[]): PluginConfiguration;
|
|
28
65
|
/** Resolve the plugin's fixed source boundaries: project-local first, environment and host global. */
|