sortie-dogs 0.1.2 → 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 +30 -9
- package/dist/cli/main.js +21 -6
- package/dist/core/consultation.d.ts +145 -0
- package/dist/core/consultation.js +207 -0
- package/dist/core/initialize.d.ts +4 -0
- package/dist/core/initialize.js +135 -12
- 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 +39 -21
- package/dist/runtime-assets.d.ts +6 -6
- package/dist/runtime-assets.js +151 -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)
|
|
@@ -19,13 +22,29 @@ Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md)
|
|
|
19
22
|
|
|
20
23
|
## Quick start
|
|
21
24
|
|
|
22
|
-
Install the public npm package and generate the project-local
|
|
25
|
+
Install the public npm package in the project and generate the project-local
|
|
26
|
+
OpenCode runtime files:
|
|
23
27
|
|
|
24
28
|
```sh
|
|
25
29
|
npm install --save-dev sortie-dogs
|
|
26
30
|
npx sortie-dogs init .
|
|
27
31
|
```
|
|
28
32
|
|
|
33
|
+
Alternatively, install the CLI globally and initialize OpenCode's global
|
|
34
|
+
configuration:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npm install --global sortie-dogs
|
|
38
|
+
sortie-dogs init --global
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This installs the canonical runtime assets in OpenCode's global configuration,
|
|
42
|
+
so `dog-coordinator` can be selected from other projects without project-local
|
|
43
|
+
initialization. Global initialization and project-local initialization are
|
|
44
|
+
separate: `sortie-dogs init .` still writes runtime files only into that
|
|
45
|
+
project. Project-local configuration and the plugin bridge below remain
|
|
46
|
+
available when a project needs its own settings or dependency.
|
|
47
|
+
|
|
29
48
|
`dog-coordinator` and `dog-scout` default to `openai/gpt-5.6-luna`. To use a
|
|
30
49
|
different model for both roles, save this as `.opencode/sortie-dogs.json`:
|
|
31
50
|
|
|
@@ -145,9 +164,9 @@ scout evidence, and fewer unnecessary context or tool turns can reduce token
|
|
|
145
164
|
use while preserving quality. Project-local routing can override either
|
|
146
165
|
default.
|
|
147
166
|
|
|
148
|
-
The `implementation`, `remediation`,
|
|
149
|
-
|
|
150
|
-
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
|
|
151
170
|
the preferred target, then ordered fallbacks. Roles without either a built-in
|
|
152
171
|
default or an explicit route keep OpenCode's already selected model.
|
|
153
172
|
|
|
@@ -161,8 +180,7 @@ default or an explicit route keep OpenCode's already selected model.
|
|
|
161
180
|
"preferred": { "model": "openai/gpt-5.6-luna", "variant": "xhigh" }
|
|
162
181
|
},
|
|
163
182
|
"dog-advisor": {
|
|
164
|
-
"preferred": { "model": "
|
|
165
|
-
"fallback": [{ "model": "provider/general" }]
|
|
183
|
+
"preferred": { "model": "openai/gpt-5.6-sol", "variant": "xhigh" }
|
|
166
184
|
},
|
|
167
185
|
"dog-reviewer": {
|
|
168
186
|
"preferred": { "model": "fable/opus", "variant": "thinking" },
|
|
@@ -171,6 +189,7 @@ default or an explicit route keep OpenCode's already selected model.
|
|
|
171
189
|
},
|
|
172
190
|
"modelCatalog": {
|
|
173
191
|
"project": [
|
|
192
|
+
{ "model": "openai/gpt-5.6-sol", "variants": ["xhigh"] },
|
|
174
193
|
{ "model": "openai/gpt-5.6-luna", "variants": ["xhigh"] },
|
|
175
194
|
{ "model": "fable/opus", "variants": ["thinking"] },
|
|
176
195
|
{ "model": "provider/general" }
|
|
@@ -179,12 +198,14 @@ default or an explicit route keep OpenCode's already selected model.
|
|
|
179
198
|
}
|
|
180
199
|
```
|
|
181
200
|
|
|
182
|
-
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`
|
|
183
204
|
declares provider models and named variants that are actually available;
|
|
184
205
|
Sortie-dogs does not invent, probe, or translate variants. Resolution tries the
|
|
185
206
|
preferred target and then its fallbacks, rejecting an explicitly routed role
|
|
186
|
-
when no candidate appears in the catalog. The advisor
|
|
187
|
-
|
|
207
|
+
when no candidate appears in the catalog. The advisor route is authoritative; the
|
|
208
|
+
reviewer route remains an optional secondary example.
|
|
188
209
|
|
|
189
210
|
`dog-advisor` accepts bounded Strategy or SourceReview consultation from the
|
|
190
211
|
coordinator. `dog-reviewer` independently checks high-risk candidates after
|
package/dist/cli/main.js
CHANGED
|
@@ -21,7 +21,8 @@ const USAGE = `Usage: sortie-dogs lint <handoff.json> [<handoff.json> ...]
|
|
|
21
21
|
[--changed-paths-from <file|->]
|
|
22
22
|
[--changed-path <path> ...]
|
|
23
23
|
[--format text|json] [--quiet] [--strict]`;
|
|
24
|
-
const INIT_USAGE =
|
|
24
|
+
const INIT_USAGE = `Usage: sortie-dogs init [project-root]
|
|
25
|
+
sortie-dogs init --global`;
|
|
25
26
|
class InputFailure extends Error {
|
|
26
27
|
safeMessage;
|
|
27
28
|
constructor(safeMessage) {
|
|
@@ -226,15 +227,29 @@ export async function run(argv) {
|
|
|
226
227
|
process.stdout.write(`${INIT_USAGE}\n`);
|
|
227
228
|
return 0;
|
|
228
229
|
}
|
|
229
|
-
|
|
230
|
+
const global = argv[1] === "--global";
|
|
231
|
+
if (argv.length > 2 || (argv[1]?.startsWith("-") === true && !global)) {
|
|
230
232
|
process.stderr.write(`${INIT_USAGE}\n`);
|
|
231
233
|
return 2;
|
|
232
234
|
}
|
|
233
235
|
try {
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
?
|
|
237
|
-
:
|
|
236
|
+
const target = global ? await initializer.resolveGlobalConfigRoot() : undefined;
|
|
237
|
+
const initialized = global
|
|
238
|
+
? await initializer.initializeGlobal(target)
|
|
239
|
+
: await initializer.initializeProject(argv[1]);
|
|
240
|
+
if (global) {
|
|
241
|
+
process.stdout.write(initialized.status === "installed"
|
|
242
|
+
? `Initialized Sortie-dogs ${initialized.version} globally at ${target}.\n`
|
|
243
|
+
: `Sortie-dogs ${initialized.version} is already initialized globally at ${target}.\n`);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
process.stdout.write(initialized.status === "installed"
|
|
247
|
+
? `Initialized Sortie-dogs ${initialized.version}.\n`
|
|
248
|
+
: `Sortie-dogs ${initialized.version} is already initialized.\n`);
|
|
249
|
+
}
|
|
250
|
+
if (initialized.preservedLegacyPaths.length > 0) {
|
|
251
|
+
process.stdout.write(`Preserved legacy runtime files: ${initialized.preservedLegacyPaths.join(", ")}.\n`);
|
|
252
|
+
}
|
|
238
253
|
return 0;
|
|
239
254
|
}
|
|
240
255
|
catch (error) {
|
|
@@ -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
|
+
}
|
|
@@ -10,5 +10,9 @@ export declare class ProjectInitializationError extends Error {
|
|
|
10
10
|
readonly code: ProjectInitializationErrorCode;
|
|
11
11
|
constructor(code: ProjectInitializationErrorCode, message: string, options?: ErrorOptions);
|
|
12
12
|
}
|
|
13
|
+
/** Resolves the OpenCode global configuration directory without platform-specific paths. */
|
|
14
|
+
export declare function resolveGlobalConfigRoot(env?: NodeJS.ProcessEnv, home?: string): Promise<string>;
|
|
13
15
|
/** Installs the packaged runtime into one existing project without changing user settings. */
|
|
14
16
|
export declare function initializeProject(projectRoot?: string): Promise<InitializeProjectResult>;
|
|
17
|
+
/** Installs the packaged runtime into OpenCode's global configuration directory. */
|
|
18
|
+
export declare function initializeGlobal(globalRoot?: string): Promise<InitializeProjectResult>;
|