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 CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  **Give OpenCode a task; get a bounded, validated implementation loop instead of an open-ended agent run.**
4
4
 
5
+ > **Project status: Experimental / unsupported.** No stability, compatibility,
6
+ > or support guarantees are provided. Mk2A2 remains the canonical internal workflow.
7
+
5
8
  [![npm](https://img.shields.io/npm/v/sortie-dogs)](https://www.npmjs.com/package/sortie-dogs)
6
9
  [![license](https://img.shields.io/npm/l/sortie-dogs)](LICENSE)
7
10
  [![Node.js](https://img.shields.io/node/v/sortie-dogs)](https://www.npmjs.com/package/sortie-dogs)
@@ -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 runtime files:
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`, and `blocker-resolution` roles always use
149
- the dedicated Sol worker; user configuration cannot replace those routes. For
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": "fable/opus", "variant": "thinking" },
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`. `modelCatalog`
201
+ Save project configuration as `.opencode/sortie-dogs.json`. The `dog-advisor`
202
+ entry above shows the built-in effective route and is not user-overridable.
203
+ `modelCatalog`
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 and reviewer routes above
187
- are optional secondary examples; omit them when they are not needed.
207
+ when no candidate appears in the catalog. The advisor route is authoritative; the
208
+ reviewer route remains an optional secondary example.
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 = "Usage: sortie-dogs init [project-root]";
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
- if (argv.length > 2 || argv[1]?.startsWith("-") === true) {
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 initialized = await initializer.initializeProject(argv[1]);
235
- process.stdout.write(initialized.status === "installed"
236
- ? `Initialized Sortie-dogs ${initialized.version}.\n`
237
- : `Sortie-dogs ${initialized.version} is already initialized.\n`);
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>;