@ran-sh/dsh-crew 0.3.0
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/.claude-plugin/marketplace.json +17 -0
- package/.claude-plugin/plugin.json +8 -0
- package/.mcp.json +8 -0
- package/LICENSE +21 -0
- package/README.de.md +359 -0
- package/README.es.md +359 -0
- package/README.fr.md +359 -0
- package/README.hi.md +359 -0
- package/README.id.md +359 -0
- package/README.ja.md +359 -0
- package/README.ko.md +359 -0
- package/README.md +360 -0
- package/README.pt.md +359 -0
- package/README.ru.md +359 -0
- package/README.th.md +359 -0
- package/README.tr.md +359 -0
- package/README.vi.md +359 -0
- package/README.zh-TW.md +359 -0
- package/README.zh.md +305 -0
- package/agents/ds-flash.md +26 -0
- package/agents/ds-pro.md +32 -0
- package/agents/ds-reviewer.md +23 -0
- package/agents/ds-worker.md +22 -0
- package/codex/agents/ds-flash.toml +30 -0
- package/codex/agents/ds-pro.toml +31 -0
- package/codex/agents/ds-reviewer.toml +28 -0
- package/codex/agents/ds-worker.toml +28 -0
- package/codex/prompts/dsh-config.md +3 -0
- package/codex/prompts/dsh-status.md +1 -0
- package/commands/config.md +11 -0
- package/commands/off.md +5 -0
- package/commands/on.md +5 -0
- package/commands/status.md +5 -0
- package/cordis.patch.yml +4 -0
- package/docs/images/dsh-crew-host.png +0 -0
- package/docs/images/dsh-crew-jobs.png +0 -0
- package/docs/images/dsh-crew-logo.png +0 -0
- package/docs/images/dsh-crew-overview.png +0 -0
- package/lib/client.js +2765 -0
- package/package.json +125 -0
- package/scripts/build-client.mjs +28 -0
- package/scripts/live-crew-smoke.mjs +39 -0
- package/scripts/live-policy-matrix.mjs +177 -0
- package/scripts/policy-probe.mjs +101 -0
- package/scripts/setup.mjs +294 -0
- package/scripts/smoke-real.mjs +110 -0
- package/scripts/smoke.mjs +78 -0
- package/scripts/verify-installer-fix.mjs +26 -0
- package/src/adaptive-routing.mjs +260 -0
- package/src/client/activation-summary.tsx +64 -0
- package/src/client/entry.tsx +236 -0
- package/src/client/index.tsx +1120 -0
- package/src/config-readiness.mjs +59 -0
- package/src/delivery.mjs +205 -0
- package/src/dsh-cli-runtime.mjs +251 -0
- package/src/failure-classification.mjs +172 -0
- package/src/hub/entry.mjs +98 -0
- package/src/hub/index.mjs +757 -0
- package/src/hub-client.mjs +132 -0
- package/src/hub-compatibility.mjs +49 -0
- package/src/i18n.mjs +19 -0
- package/src/install/cli.mjs +28 -0
- package/src/install/install-legacy.mjs +460 -0
- package/src/install/install.mjs +451 -0
- package/src/jobs.mjs +275 -0
- package/src/mcp-runtime.mjs +257 -0
- package/src/model-catalog.mjs +173 -0
- package/src/model-routing.mjs +391 -0
- package/src/multimodal.mjs +0 -0
- package/src/policy-legacy.mjs +830 -0
- package/src/policy.mjs +197 -0
- package/src/readiness-matrix.mjs +169 -0
- package/src/runtime-controls.mjs +90 -0
- package/src/runtime-identity.mjs +108 -0
- package/src/server.mjs +477 -0
- package/src/status-shard.mjs +52 -0
- package/src/structured-error-code.mjs +39 -0
- package/src/vision-route.mjs +138 -0
- package/src/workflow-runtime.mjs +567 -0
- package/src/workflow.mjs +160 -0
- package/src/workspace-audit.mjs +231 -0
- package/src/workspace-isolation.mjs +306 -0
- package/statusline/statusline.sh +14 -0
- package/statusline/worker-segment.sh +35 -0
- package/worker.cordis.yml +77 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
// v0.3 persisted-config authority facade.
|
|
2
|
+
//
|
|
3
|
+
// The v0.2 installer/config implementation is frozen in install-legacy.mjs.
|
|
4
|
+
// This module keeps all installer exports while replacing only global config
|
|
5
|
+
// read/write semantics with schema-v3 canonical authority.
|
|
6
|
+
|
|
7
|
+
export * from './install-legacy.mjs';
|
|
8
|
+
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import * as legacy from './install-legacy.mjs';
|
|
13
|
+
import { normalizeModelPriority } from '../model-routing.mjs';
|
|
14
|
+
import { runtimeActivationMetadata } from '../runtime-controls.mjs';
|
|
15
|
+
import {
|
|
16
|
+
CONFIG_SCHEMA_VERSION,
|
|
17
|
+
configHasCanonicalAuthority,
|
|
18
|
+
normalizeGlobalConfig,
|
|
19
|
+
normalizeLegacyGlobalConfig,
|
|
20
|
+
} from '../policy.mjs';
|
|
21
|
+
|
|
22
|
+
const GLOBAL_CONFIG_FILE = join(homedir(), '.config', 'dsh-crew', 'config.json');
|
|
23
|
+
const COLLABORATION_MODES = ['flash-only', 'pro-only', 'balanced', 'review-pipeline', 'custom'];
|
|
24
|
+
const TIER_STATES = ['disabled', 'manual', 'auto'];
|
|
25
|
+
|
|
26
|
+
// ---- P0 isolation contract -------------------------------------------------
|
|
27
|
+
// dsh-crew owns a dedicated DSH home and profile; repository installer/test
|
|
28
|
+
// paths must never default to the user's official ~/.dsh or its ``web`` profile.
|
|
29
|
+
// A fresh Crew Hub config points at the Crew-owned port only; the former
|
|
30
|
+
// shared-profile default 3080 is treated as a legacy value to migrate away from.
|
|
31
|
+
export const CREW_PROFILE_NAME = 'dsh-crew';
|
|
32
|
+
export const CREW_HOME_REL = join('.config', 'dsh-crew', 'harness');
|
|
33
|
+
export const CREW_DEFAULT_HUB_URL = 'http://127.0.0.1:3210';
|
|
34
|
+
export const CREW_LEGACY_HUB_URL = 'http://127.0.0.1:3080';
|
|
35
|
+
export function crewDshHome({ home = homedir() } = {}) {
|
|
36
|
+
return join(home, CREW_HOME_REL);
|
|
37
|
+
}
|
|
38
|
+
export function crewProfileDir({ home = homedir() } = {}) {
|
|
39
|
+
return join(crewDshHome({ home }), 'profiles', CREW_PROFILE_NAME);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The isolated default set: identical to the legacy defaults except the Hub URL,
|
|
43
|
+
// so fresh Crew installs never point at the official web profile.
|
|
44
|
+
const CREW_GLOBAL_CONFIG_DEFAULTS = { ...legacy.GLOBAL_CONFIG_DEFAULTS, hub_url: CREW_DEFAULT_HUB_URL };
|
|
45
|
+
|
|
46
|
+
export const GLOBAL_CONFIG_SCHEMA_VERSION = CONFIG_SCHEMA_VERSION;
|
|
47
|
+
export const GLOBAL_CONFIG_DEFAULTS = Object.freeze({
|
|
48
|
+
...CREW_GLOBAL_CONFIG_DEFAULTS,
|
|
49
|
+
config_schema_version: CONFIG_SCHEMA_VERSION,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
function readJson(file, fallback) {
|
|
53
|
+
try { return JSON.parse(readFileSync(file, 'utf8')); } catch { return fallback; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function validObject(value) {
|
|
57
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sourceVersion(stored) {
|
|
61
|
+
return Number.isInteger(stored?.config_schema_version) ? stored.config_schema_version : 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function legacySnapshotFrom(config = {}) {
|
|
65
|
+
const mode = COLLABORATION_MODES.includes(config.collaboration_mode)
|
|
66
|
+
? config.collaboration_mode
|
|
67
|
+
: (config.tier_policy === 'flash-only' ? 'flash-only'
|
|
68
|
+
: config.tier_policy === 'pro-only' ? 'pro-only' : 'balanced');
|
|
69
|
+
let flash = TIER_STATES.includes(config.flash_state) ? config.flash_state : 'auto';
|
|
70
|
+
let pro = TIER_STATES.includes(config.pro_state) ? config.pro_state : 'auto';
|
|
71
|
+
if (mode === 'flash-only') { flash = 'auto'; pro = 'disabled'; }
|
|
72
|
+
else if (mode === 'pro-only') { flash = 'disabled'; pro = 'auto'; }
|
|
73
|
+
else if (mode === 'balanced' || mode === 'review-pipeline') { flash = 'auto'; pro = 'auto'; }
|
|
74
|
+
return {
|
|
75
|
+
collaboration_mode: mode,
|
|
76
|
+
tier_policy: mode === 'flash-only' ? 'flash-only' : mode === 'pro-only' ? 'pro-only' : 'auto',
|
|
77
|
+
flash_state: flash,
|
|
78
|
+
pro_state: pro,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function attachReadMetadata(config, { version, canonical }) {
|
|
83
|
+
return {
|
|
84
|
+
...config,
|
|
85
|
+
config_schema_version: version,
|
|
86
|
+
config_authority: canonical ? 'canonical' : 'legacy-import',
|
|
87
|
+
config_migration_required: !canonical,
|
|
88
|
+
config_activation: runtimeActivationMetadata(),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function freshConfig() {
|
|
93
|
+
const imported = normalizeLegacyGlobalConfig(CREW_GLOBAL_CONFIG_DEFAULTS);
|
|
94
|
+
const normalized = normalizeGlobalConfig({
|
|
95
|
+
...imported,
|
|
96
|
+
legacy: legacySnapshotFrom(imported),
|
|
97
|
+
config_schema_version: CONFIG_SCHEMA_VERSION,
|
|
98
|
+
});
|
|
99
|
+
return attachReadMetadata(normalized, { version: CONFIG_SCHEMA_VERSION, canonical: true });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Migrate the former shared-profile Crew Hub default (3080) to the Crew-owned
|
|
104
|
+
* dedicated port on the read path so a fresh or recovered install can never
|
|
105
|
+
* silently reconnect to the official web profile. Everything else is preserved.
|
|
106
|
+
*/
|
|
107
|
+
function migrateHubUrl(config, { legacyUrl = CREW_LEGACY_HUB_URL, dedicatedUrl = CREW_DEFAULT_HUB_URL } = {}) {
|
|
108
|
+
if (validObject(config) && config.hub_url === legacyUrl) {
|
|
109
|
+
return { ...config, hub_url: dedicatedUrl, hub_url_migrated_from_legacy: true };
|
|
110
|
+
}
|
|
111
|
+
return config;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function mergeStoredGlobalConfig(stored) {
|
|
115
|
+
if (!validObject(stored)) return freshConfig();
|
|
116
|
+
|
|
117
|
+
const version = sourceVersion(stored);
|
|
118
|
+
if (configHasCanonicalAuthority(stored)) {
|
|
119
|
+
const merged = { ...CREW_GLOBAL_CONFIG_DEFAULTS, ...stored };
|
|
120
|
+
return attachReadMetadata(migrateHubUrl(normalizeGlobalConfig(merged)), { version, canonical: true });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Legacy files keep the exact v0.2 import semantics. Reading is non-mutating:
|
|
124
|
+
// migration is reported but the disk file is upgraded only on the next
|
|
125
|
+
// explicit save.
|
|
126
|
+
const legacyMerged = migrateHubUrl(legacy.mergeStoredGlobalConfig(stored));
|
|
127
|
+
const normalized = normalizeLegacyGlobalConfig(legacyMerged);
|
|
128
|
+
return attachReadMetadata(normalized, { version, canonical: false });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function readGlobalConfig({ configFile = GLOBAL_CONFIG_FILE } = {}) {
|
|
132
|
+
if (!existsSync(configFile)) return freshConfig();
|
|
133
|
+
return mergeStoredGlobalConfig(readJson(configFile, {}));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function stripReadMetadata(config) {
|
|
137
|
+
const next = { ...config };
|
|
138
|
+
delete next.config_authority;
|
|
139
|
+
delete next.config_migration_required;
|
|
140
|
+
delete next.config_activation;
|
|
141
|
+
return next;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function cloneCanonical(config) {
|
|
145
|
+
return {
|
|
146
|
+
...config,
|
|
147
|
+
execution: { ...config.execution },
|
|
148
|
+
worker: {
|
|
149
|
+
...config.worker,
|
|
150
|
+
model_policy: {
|
|
151
|
+
...config.worker?.model_policy,
|
|
152
|
+
priority: [...(config.worker?.model_policy?.priority ?? [])],
|
|
153
|
+
escalation_priority: [...(config.worker?.model_policy?.escalation_priority ?? [])],
|
|
154
|
+
escalation: { ...config.worker?.model_policy?.escalation },
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
review: {
|
|
158
|
+
...config.review,
|
|
159
|
+
model_policy: {
|
|
160
|
+
...config.review?.model_policy,
|
|
161
|
+
priority: [...(config.review?.model_policy?.priority ?? [])],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
legacy: validObject(config.legacy) ? { ...config.legacy } : legacySnapshotFrom(config),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function mergeCanonicalPatch(current, patch) {
|
|
169
|
+
const next = cloneCanonical(current);
|
|
170
|
+
if (validObject(patch.execution)) {
|
|
171
|
+
next.execution = { ...next.execution, ...patch.execution };
|
|
172
|
+
if (patch.execution.enabled !== undefined) next.subagents_enabled = Boolean(patch.execution.enabled);
|
|
173
|
+
}
|
|
174
|
+
if (validObject(patch.worker)) {
|
|
175
|
+
next.worker = {
|
|
176
|
+
...next.worker,
|
|
177
|
+
...patch.worker,
|
|
178
|
+
model_policy: validObject(patch.worker.model_policy)
|
|
179
|
+
? {
|
|
180
|
+
...next.worker?.model_policy,
|
|
181
|
+
...patch.worker.model_policy,
|
|
182
|
+
escalation: validObject(patch.worker.model_policy.escalation)
|
|
183
|
+
? { ...next.worker?.model_policy?.escalation, ...patch.worker.model_policy.escalation }
|
|
184
|
+
: next.worker?.model_policy?.escalation,
|
|
185
|
+
}
|
|
186
|
+
: next.worker?.model_policy,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (validObject(patch.review)) {
|
|
190
|
+
next.review = {
|
|
191
|
+
...next.review,
|
|
192
|
+
...patch.review,
|
|
193
|
+
model_policy: validObject(patch.review.model_policy)
|
|
194
|
+
? { ...next.review?.model_policy, ...patch.review.model_policy }
|
|
195
|
+
: next.review?.model_policy,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (validObject(patch.legacy)) next.legacy = { ...next.legacy, ...patch.legacy };
|
|
199
|
+
|
|
200
|
+
// v0.3 still has one provider-mode control. A direct canonical patch is
|
|
201
|
+
// allowed, but it cannot manufacture separate worker/reviewer authorities.
|
|
202
|
+
const providerMode = patch.worker?.provider_mode ?? patch.review?.provider_mode;
|
|
203
|
+
if (providerMode !== undefined) {
|
|
204
|
+
next.worker = { ...next.worker, provider_mode: providerMode };
|
|
205
|
+
next.review = { ...next.review, provider_mode: providerMode };
|
|
206
|
+
next.worker_provider_mode = providerMode;
|
|
207
|
+
}
|
|
208
|
+
return next;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function legacyInputFrom(next, patch) {
|
|
212
|
+
const input = { ...next };
|
|
213
|
+
delete input.worker;
|
|
214
|
+
delete input.review;
|
|
215
|
+
delete input.execution;
|
|
216
|
+
delete input.legacy;
|
|
217
|
+
delete input.config_schema_version;
|
|
218
|
+
|
|
219
|
+
// Preset commands own derived role state. Schema-v3 mirrors from the previous
|
|
220
|
+
// preset must not masquerade as explicit overrides during translation.
|
|
221
|
+
if (patch?.collaboration_mode !== undefined || patch?.tier_policy !== undefined) {
|
|
222
|
+
delete input.worker_state;
|
|
223
|
+
delete input.review_state;
|
|
224
|
+
delete input.auto_review;
|
|
225
|
+
delete input.pro_reviews_flash;
|
|
226
|
+
// Non-custom presets own both tier states. Custom deliberately keeps the
|
|
227
|
+
// previous states as its starting point.
|
|
228
|
+
if (input.collaboration_mode !== 'custom') {
|
|
229
|
+
delete input.flash_state;
|
|
230
|
+
delete input.pro_state;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Individual tier-state commands own role eligibility only; the existing
|
|
235
|
+
// role mirrors must not shadow them.
|
|
236
|
+
if (patch?.flash_state !== undefined || patch?.pro_state !== undefined) {
|
|
237
|
+
delete input.worker_state;
|
|
238
|
+
delete input.review_state;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// auto_review is itself the new compatibility input. pro_reviews_flash is
|
|
242
|
+
// older, so remove the schema-v3 auto_review mirror when translating it.
|
|
243
|
+
if (patch?.auto_review !== undefined) delete input.review_state;
|
|
244
|
+
if (patch?.pro_reviews_flash !== undefined) {
|
|
245
|
+
delete input.review_state;
|
|
246
|
+
delete input.auto_review;
|
|
247
|
+
}
|
|
248
|
+
return input;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function anyPatched(patch, keys) {
|
|
252
|
+
return keys.some((key) => patch?.[key] !== undefined);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Translate the old flat Settings/API surface into a schema-v3 canonical
|
|
257
|
+
* update without reconstructing unrelated canonical-only fields. This is the
|
|
258
|
+
* key single-authority boundary: a legacy UI command may change what it names,
|
|
259
|
+
* but cannot erase e.g. escalation.max_attempts or future canonical metadata.
|
|
260
|
+
*/
|
|
261
|
+
function mergeLegacyPatchIntoCanonical(current, compatibilityView, candidate, patch) {
|
|
262
|
+
const next = cloneCanonical({
|
|
263
|
+
...compatibilityView,
|
|
264
|
+
execution: current.execution,
|
|
265
|
+
worker: current.worker,
|
|
266
|
+
review: current.review,
|
|
267
|
+
legacy: current.legacy,
|
|
268
|
+
subagents_enabled: current.subagents_enabled,
|
|
269
|
+
main_agent_mode: current.main_agent_mode,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (patch?.subagents_enabled !== undefined) {
|
|
273
|
+
next.subagents_enabled = candidate.subagents_enabled;
|
|
274
|
+
next.execution.enabled = candidate.subagents_enabled;
|
|
275
|
+
}
|
|
276
|
+
if (patch?.main_agent_mode !== undefined) next.main_agent_mode = candidate.main_agent_mode;
|
|
277
|
+
|
|
278
|
+
const executionMap = {
|
|
279
|
+
default_effort: 'default_effort',
|
|
280
|
+
default_timeout_seconds: 'default_timeout_seconds',
|
|
281
|
+
mode: 'mode',
|
|
282
|
+
max_parallel: 'max_parallel',
|
|
283
|
+
isolation: 'isolation',
|
|
284
|
+
};
|
|
285
|
+
for (const [flat, canonical] of Object.entries(executionMap)) {
|
|
286
|
+
if (patch?.[flat] !== undefined) next.execution[canonical] = candidate.execution[canonical];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Presets own strategy + both role states + automatic-review
|
|
290
|
+
// semantics and the canonical legacy compatibility snapshot.
|
|
291
|
+
if (patch?.collaboration_mode !== undefined || patch?.tier_policy !== undefined) {
|
|
292
|
+
next.worker.state = candidate.worker.state;
|
|
293
|
+
next.review.state = candidate.review.state;
|
|
294
|
+
next.worker.model_policy.strategy = candidate.worker.model_policy.strategy;
|
|
295
|
+
next.review.auto_review = candidate.review.auto_review;
|
|
296
|
+
next.legacy = legacySnapshotFrom(candidate);
|
|
297
|
+
}
|
|
298
|
+
// Tier-state controls are narrower: changing eligibility must not reset a
|
|
299
|
+
// canonical model strategy or another future policy dimension. They still
|
|
300
|
+
// update the nested legacy tier-state snapshot because that is their owned
|
|
301
|
+
// compatibility behavior.
|
|
302
|
+
if (patch?.flash_state !== undefined || patch?.pro_state !== undefined) {
|
|
303
|
+
next.worker.state = candidate.worker.state;
|
|
304
|
+
next.review.state = candidate.review.state;
|
|
305
|
+
next.legacy = legacySnapshotFrom(candidate);
|
|
306
|
+
}
|
|
307
|
+
if (patch?.worker_state !== undefined) next.worker.state = candidate.worker.state;
|
|
308
|
+
if (patch?.review_state !== undefined) next.review.state = candidate.review.state;
|
|
309
|
+
if (patch?.auto_review !== undefined || patch?.pro_reviews_flash !== undefined) {
|
|
310
|
+
next.review.auto_review = candidate.review.auto_review;
|
|
311
|
+
next.review.state = candidate.review.state;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (patch?.worker_provider_mode !== undefined) {
|
|
315
|
+
next.worker.provider_mode = candidate.worker.provider_mode;
|
|
316
|
+
next.review.provider_mode = candidate.worker.provider_mode;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (anyPatched(patch, ['flash_model_priority', 'flash_model_priority_configured', 'flash_model_fallback'])) {
|
|
320
|
+
next.worker.model_policy.priority = [...candidate.worker.model_policy.priority];
|
|
321
|
+
next.worker.model_policy.priorityConfigured = candidate.worker.model_policy.priorityConfigured;
|
|
322
|
+
next.worker.model_policy.fallback = candidate.worker.model_policy.fallback;
|
|
323
|
+
}
|
|
324
|
+
if (anyPatched(patch, ['pro_model_priority', 'pro_model_priority_configured', 'pro_model_fallback'])) {
|
|
325
|
+
next.worker.model_policy.escalation_priority = [...candidate.worker.model_policy.escalation_priority];
|
|
326
|
+
next.worker.model_policy.escalation_priority_configured = candidate.worker.model_policy.escalation_priority_configured;
|
|
327
|
+
next.worker.model_policy.fallback = candidate.worker.model_policy.fallback;
|
|
328
|
+
next.review.model_policy.priority = [...candidate.review.model_policy.priority];
|
|
329
|
+
next.review.model_policy.priorityConfigured = candidate.review.model_policy.priorityConfigured;
|
|
330
|
+
next.review.model_policy.fallback = candidate.review.model_policy.fallback;
|
|
331
|
+
}
|
|
332
|
+
if (patch?.escalate_on_failure !== undefined) {
|
|
333
|
+
next.worker.model_policy.escalation.enabled = candidate.worker.model_policy.escalation.enabled;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return next;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Persist one schema-v3 snapshot.
|
|
341
|
+
*
|
|
342
|
+
* Flat fields remain in the JSON as compatibility mirrors for older UI/MCP
|
|
343
|
+
* builds, but schema-v3 readers ignore conflicting mirrors and trust the nested
|
|
344
|
+
* canonical snapshot. Legacy flat patches are accepted as input commands and
|
|
345
|
+
* translated only into the canonical fields they own.
|
|
346
|
+
*/
|
|
347
|
+
export function writeGlobalConfig(patch, { configFile = GLOBAL_CONFIG_FILE } = {}) {
|
|
348
|
+
mkdirSync(dirname(configFile), { recursive: true });
|
|
349
|
+
const current = readGlobalConfig({ configFile });
|
|
350
|
+
const wasCanonical = current.config_authority === 'canonical';
|
|
351
|
+
let compatibilityView = stripReadMetadata(current);
|
|
352
|
+
|
|
353
|
+
// Existing public settings surface: accept only known flat keys. Schema and
|
|
354
|
+
// read-only authority metadata are never caller-controlled.
|
|
355
|
+
for (const [key, value] of Object.entries(patch ?? {})) {
|
|
356
|
+
if (value !== undefined && key in legacy.GLOBAL_CONFIG_DEFAULTS) compatibilityView[key] = value;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
for (const tier of ['flash', 'pro']) {
|
|
360
|
+
const key = `${tier}_model_priority`;
|
|
361
|
+
if (patch?.[key] !== undefined) {
|
|
362
|
+
compatibilityView[key] = normalizeModelPriority(patch[key]);
|
|
363
|
+
compatibilityView[`${tier}_model_priority_configured`] = true;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Deprecated routing fields are input commands only. Keep their input view
|
|
368
|
+
// coherent before the frozen migration translates them.
|
|
369
|
+
if (patch?.tier_policy !== undefined) {
|
|
370
|
+
compatibilityView.tier_policy = patch.tier_policy;
|
|
371
|
+
if (patch.tier_policy === 'flash-only') compatibilityView.collaboration_mode = 'flash-only';
|
|
372
|
+
else if (patch.tier_policy === 'pro-only') compatibilityView.collaboration_mode = 'pro-only';
|
|
373
|
+
else if (compatibilityView.collaboration_mode === 'flash-only' || compatibilityView.collaboration_mode === 'pro-only') compatibilityView.collaboration_mode = 'balanced';
|
|
374
|
+
}
|
|
375
|
+
if (patch?.collaboration_mode !== undefined) {
|
|
376
|
+
if (patch.collaboration_mode === 'flash-only') compatibilityView.tier_policy = 'flash-only';
|
|
377
|
+
else if (patch.collaboration_mode === 'pro-only') compatibilityView.tier_policy = 'pro-only';
|
|
378
|
+
else compatibilityView.tier_policy = 'auto';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const candidate = normalizeLegacyGlobalConfig(legacyInputFrom(compatibilityView, patch));
|
|
382
|
+
let normalized = wasCanonical
|
|
383
|
+
? mergeLegacyPatchIntoCanonical(current, compatibilityView, candidate, patch)
|
|
384
|
+
: { ...candidate, legacy: legacySnapshotFrom(candidate) };
|
|
385
|
+
|
|
386
|
+
const hasCanonicalPatch = validObject(patch?.worker)
|
|
387
|
+
|| validObject(patch?.review)
|
|
388
|
+
|| validObject(patch?.execution)
|
|
389
|
+
|| validObject(patch?.legacy);
|
|
390
|
+
if (hasCanonicalPatch) normalized = mergeCanonicalPatch(normalized, patch);
|
|
391
|
+
|
|
392
|
+
if (!validObject(normalized.legacy)) normalized.legacy = legacySnapshotFrom(candidate);
|
|
393
|
+
normalized = normalizeGlobalConfig({
|
|
394
|
+
...normalized,
|
|
395
|
+
config_schema_version: CONFIG_SCHEMA_VERSION,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const persisted = stripReadMetadata({
|
|
399
|
+
...normalized,
|
|
400
|
+
config_schema_version: CONFIG_SCHEMA_VERSION,
|
|
401
|
+
});
|
|
402
|
+
writeFileSync(configFile, JSON.stringify(persisted, null, 2) + '\n');
|
|
403
|
+
return attachReadMetadata(normalizeGlobalConfig(persisted), {
|
|
404
|
+
version: CONFIG_SCHEMA_VERSION,
|
|
405
|
+
canonical: true,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function getGlobalConfigDiagnostics({ configFile = GLOBAL_CONFIG_FILE } = {}) {
|
|
410
|
+
if (!existsSync(configFile)) {
|
|
411
|
+
return {
|
|
412
|
+
current_schema_version: CONFIG_SCHEMA_VERSION,
|
|
413
|
+
stored_schema_version: null,
|
|
414
|
+
authority: 'canonical',
|
|
415
|
+
migration_required: false,
|
|
416
|
+
canonical_present: true,
|
|
417
|
+
legacy_mirror_conflicts: [],
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const stored = readJson(configFile, {});
|
|
422
|
+
const version = sourceVersion(stored);
|
|
423
|
+
const canonical = configHasCanonicalAuthority(stored);
|
|
424
|
+
const conflicts = [];
|
|
425
|
+
if (canonical) {
|
|
426
|
+
const effective = normalizeGlobalConfig(stored);
|
|
427
|
+
const mirrorKeys = [
|
|
428
|
+
'subagents_enabled', 'main_agent_mode', 'default_effort',
|
|
429
|
+
'default_timeout_seconds', 'mode', 'max_parallel', 'isolation',
|
|
430
|
+
'collaboration_mode', 'tier_policy', 'flash_state', 'pro_state',
|
|
431
|
+
'worker_state', 'review_state', 'auto_review', 'worker_provider_mode',
|
|
432
|
+
'flash_model_priority', 'flash_model_priority_configured',
|
|
433
|
+
'flash_model_fallback', 'pro_model_priority',
|
|
434
|
+
'pro_model_priority_configured', 'pro_model_fallback',
|
|
435
|
+
'escalate_on_failure', 'pro_reviews_flash',
|
|
436
|
+
];
|
|
437
|
+
for (const key of mirrorKeys) {
|
|
438
|
+
if (!Object.prototype.hasOwnProperty.call(stored, key)) continue;
|
|
439
|
+
if (JSON.stringify(stored[key]) !== JSON.stringify(effective[key])) conflicts.push(key);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
current_schema_version: CONFIG_SCHEMA_VERSION,
|
|
445
|
+
stored_schema_version: version,
|
|
446
|
+
authority: canonical ? 'canonical' : 'legacy-import',
|
|
447
|
+
migration_required: !canonical,
|
|
448
|
+
canonical_present: canonical,
|
|
449
|
+
legacy_mirror_conflicts: conflicts,
|
|
450
|
+
};
|
|
451
|
+
}
|