@ran-sh/dsh-crew 0.3.1 → 0.3.2

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.
Files changed (78) hide show
  1. package/.claude-plugin/marketplace.json +17 -17
  2. package/.claude-plugin/plugin.json +8 -8
  3. package/.mcp.json +8 -8
  4. package/LICENSE +21 -21
  5. package/README.de.md +359 -359
  6. package/README.es.md +359 -359
  7. package/README.fr.md +359 -359
  8. package/README.hi.md +359 -359
  9. package/README.id.md +359 -359
  10. package/README.ja.md +359 -359
  11. package/README.ko.md +359 -359
  12. package/README.md +140 -140
  13. package/README.pt.md +359 -359
  14. package/README.ru.md +359 -359
  15. package/README.th.md +359 -359
  16. package/README.tr.md +359 -359
  17. package/README.vi.md +359 -359
  18. package/README.zh-TW.md +359 -359
  19. package/README.zh.md +140 -140
  20. package/agents/ds-flash.md +26 -26
  21. package/agents/ds-pro.md +32 -32
  22. package/agents/ds-reviewer.md +23 -23
  23. package/agents/ds-worker.md +22 -22
  24. package/codex/agents/ds-flash.toml +30 -30
  25. package/codex/agents/ds-pro.toml +31 -31
  26. package/codex/agents/ds-reviewer.toml +28 -28
  27. package/codex/agents/ds-worker.toml +28 -28
  28. package/codex/prompts/dsh-config.md +3 -3
  29. package/codex/prompts/dsh-status.md +1 -1
  30. package/commands/config.md +11 -11
  31. package/commands/off.md +5 -5
  32. package/commands/on.md +5 -5
  33. package/commands/status.md +5 -5
  34. package/cordis.patch.yml +4 -4
  35. package/lib/client.js +2765 -2765
  36. package/package.json +127 -127
  37. package/scripts/build-client.mjs +28 -28
  38. package/scripts/live-crew-smoke.mjs +39 -39
  39. package/scripts/live-policy-matrix.mjs +177 -177
  40. package/scripts/policy-probe.mjs +101 -101
  41. package/scripts/setup.mjs +295 -294
  42. package/scripts/smoke-real.mjs +110 -110
  43. package/scripts/smoke.mjs +78 -78
  44. package/scripts/verify-installer-fix.mjs +26 -26
  45. package/scripts/verify-npm-install.mjs +297 -276
  46. package/src/adaptive-routing.mjs +260 -260
  47. package/src/client/activation-summary.tsx +64 -64
  48. package/src/client/entry.tsx +236 -236
  49. package/src/client/index.tsx +1120 -1120
  50. package/src/config-readiness.mjs +59 -59
  51. package/src/delivery.mjs +205 -205
  52. package/src/dsh-cli-runtime.mjs +435 -251
  53. package/src/failure-classification.mjs +172 -172
  54. package/src/hub/entry.mjs +98 -98
  55. package/src/hub-client.mjs +132 -132
  56. package/src/hub-compatibility.mjs +49 -49
  57. package/src/i18n.mjs +19 -19
  58. package/src/install/cli.mjs +28 -28
  59. package/src/install/install-legacy.mjs +460 -460
  60. package/src/install/install.mjs +451 -451
  61. package/src/mcp-runtime.mjs +257 -257
  62. package/src/model-catalog.mjs +173 -173
  63. package/src/model-routing.mjs +391 -391
  64. package/src/policy.mjs +197 -197
  65. package/src/readiness-matrix.mjs +169 -169
  66. package/src/runtime-controls.mjs +90 -90
  67. package/src/runtime-identity.mjs +108 -108
  68. package/src/server.mjs +477 -477
  69. package/src/status-shard.mjs +52 -52
  70. package/src/structured-error-code.mjs +38 -38
  71. package/src/vision-route.mjs +138 -138
  72. package/src/workflow-runtime.mjs +573 -567
  73. package/src/workflow.mjs +160 -160
  74. package/src/workspace-audit.mjs +231 -231
  75. package/src/workspace-isolation.mjs +365 -306
  76. package/statusline/statusline.sh +14 -14
  77. package/statusline/worker-segment.sh +35 -35
  78. package/worker.cordis.yml +77 -77
@@ -1,391 +1,391 @@
1
- // Pure Harness-backed worker model selection. A worker tier describes a role,
2
- // not a fixed model: explicit provider/model priorities win, fresh configs may
3
- // use a tier-specific preferred model id, and Harness Default is the final
4
- // fallback. Catalog membership is advisory; provider registration is the
5
- // routing boundary.
6
-
7
- import { rankAdaptiveCandidates } from './adaptive-routing.mjs';
8
-
9
- export const DEFAULT_TIER_MODEL_PREFERENCES = Object.freeze({
10
- flash: 'deepseek-v4-flash',
11
- pro: 'deepseek-v4-pro',
12
- });
13
-
14
- // v0.2 role → default preferred model class. A role describes who does the
15
- // work; the model class is only the fresh-config recommendation until a
16
- // priority list or Harness Default takes over.
17
- export const DEFAULT_ROLE_MODEL_PREFERENCES = Object.freeze({
18
- worker: 'deepseek-v4-flash',
19
- reviewer: 'deepseek-v4-pro',
20
- });
21
-
22
- export const MODEL_FALLBACKS = ['harness-default'];
23
- export const NO_WORKER_MODEL_AVAILABLE = 'NO_WORKER_MODEL_AVAILABLE';
24
- export const MODEL_SELECTION_TRACE_VERSION = 1;
25
- export const MODEL_SELECTION_REASON_CODES = Object.freeze({
26
- PROVIDER_UNAVAILABLE: 'PROVIDER_UNAVAILABLE',
27
- PREFERRED_MODEL_UNAVAILABLE: 'PREFERRED_MODEL_UNAVAILABLE',
28
- PREFERRED_MODEL_AMBIGUOUS: 'PREFERRED_MODEL_AMBIGUOUS',
29
- ADAPTIVE_DEPRIORITIZED: 'ADAPTIVE_DEPRIORITIZED',
30
- HARNESS_DEFAULT_INVALID: 'HARNESS_DEFAULT_INVALID',
31
- HARNESS_DEFAULT_PROVIDER_UNAVAILABLE: 'HARNESS_DEFAULT_PROVIDER_UNAVAILABLE',
32
- PRIMARY_CANDIDATES_EXHAUSTED: 'PRIMARY_CANDIDATES_EXHAUSTED',
33
- ESCALATION_CANDIDATES_EXHAUSTED: 'ESCALATION_CANDIDATES_EXHAUSTED',
34
- NO_AVAILABLE_MODEL: 'NO_AVAILABLE_MODEL',
35
- });
36
-
37
- export function normalizeModelRef(raw) {
38
- if (!raw || typeof raw !== 'object') return null;
39
- const provider = typeof raw.provider === 'string' ? raw.provider.trim() : '';
40
- const model = typeof raw.model === 'string' ? raw.model.trim() : '';
41
- return provider && model ? { provider, model } : null;
42
- }
43
-
44
- export function modelRefKey(raw) {
45
- const ref = normalizeModelRef(raw);
46
- return ref ? `${ref.provider}\0${ref.model}` : '';
47
- }
48
-
49
- export function normalizeModelPriority(raw) {
50
- if (!Array.isArray(raw)) return [];
51
- const seen = new Set();
52
- const result = [];
53
- for (const value of raw) {
54
- const ref = normalizeModelRef(value);
55
- if (!ref) continue;
56
- const key = modelRefKey(ref);
57
- if (seen.has(key)) continue;
58
- seen.add(key);
59
- result.push(ref);
60
- }
61
- return result;
62
- }
63
-
64
- function providerMap(catalog) {
65
- const map = new Map();
66
- for (const raw of catalog?.providers ?? []) {
67
- if (!raw || typeof raw.id !== 'string' || raw.id === '') continue;
68
- map.set(raw.id, raw);
69
- }
70
- return map;
71
- }
72
-
73
- function normalizeAttempt(value) {
74
- return Number.isInteger(value) && value >= 0 ? value : 0;
75
- }
76
-
77
- function normalizeModelClassHint(value) {
78
- return value === 'flash' || value === 'pro' ? value : null;
79
- }
80
-
81
- function traceBase({
82
- role = 'worker',
83
- logicalAttempt = 0,
84
- modelClassHint = null,
85
- strategy = 'legacy-tier',
86
- candidateSet = 'primary',
87
- escalationReason = null,
88
- } = {}) {
89
- return {
90
- version: MODEL_SELECTION_TRACE_VERSION,
91
- role: role === 'reviewer' ? 'reviewer' : 'worker',
92
- logical_attempt: normalizeAttempt(logicalAttempt),
93
- model_class_hint: normalizeModelClassHint(modelClassHint),
94
- strategy: typeof strategy === 'string' && strategy ? strategy : 'legacy-tier',
95
- candidate_set: typeof candidateSet === 'string' && candidateSet ? candidateSet : 'primary',
96
- ordered_candidates: [],
97
- selected: null,
98
- selection_source: null,
99
- fallback_reason: null,
100
- escalation_reason: typeof escalationReason === 'string' && escalationReason ? escalationReason : null,
101
- };
102
- }
103
-
104
- function candidateDecision(ref, source, status, { reasonCode, advertised } = {}) {
105
- return {
106
- provider: ref?.provider ?? null,
107
- model: ref?.model ?? null,
108
- source,
109
- status,
110
- ...(reasonCode ? { reason_code: reasonCode } : {}),
111
- ...(advertised === false ? { advertised: false } : {}),
112
- };
113
- }
114
-
115
- function selectTrace(trace, ref, source, { advertised, fallbackReason } = {}) {
116
- trace.ordered_candidates.push(candidateDecision(ref, source, 'selected', { advertised }));
117
- trace.selected = { provider: ref.provider, model: ref.model, source };
118
- trace.selection_source = source;
119
- trace.fallback_reason = fallbackReason ?? null;
120
- return trace;
121
- }
122
-
123
- function rankForTrace(trace, candidates, {
124
- adaptive,
125
- adaptiveHealth,
126
- explicitPriority = false,
127
- } = {}) {
128
- const ranked = rankAdaptiveCandidates(candidates, {
129
- config: adaptive,
130
- healthStore: adaptiveHealth,
131
- role: trace.role,
132
- explicitPriority,
133
- });
134
- if (ranked.trace.enabled) trace.adaptive = ranked.trace;
135
- return ranked;
136
- }
137
-
138
- /**
139
- * Build a one-candidate trace for transports that intentionally bypass the
140
- * Harness catalog (DeepSeek Official strict mode / standalone legacy mode).
141
- */
142
- export function buildDirectSelectionTrace({
143
- role = 'worker',
144
- logicalAttempt = 0,
145
- modelClassHint = null,
146
- strategy = 'legacy-strict',
147
- candidateSet = 'primary',
148
- provider,
149
- model,
150
- source = 'legacy-strict',
151
- escalationReason = null,
152
- } = {}) {
153
- const trace = traceBase({ role, logicalAttempt, modelClassHint, strategy, candidateSet, escalationReason });
154
- const ref = normalizeModelRef({ provider, model });
155
- if (!ref) {
156
- trace.fallback_reason = MODEL_SELECTION_REASON_CODES.NO_AVAILABLE_MODEL;
157
- return trace;
158
- }
159
- return selectTrace(trace, ref, source);
160
- }
161
-
162
- /**
163
- * Add workflow-only context after a transport has resolved the model. This is
164
- * intentionally metadata-only: it never changes provider/model selection.
165
- */
166
- export function enrichSelectionTrace(trace, {
167
- role,
168
- logicalAttempt,
169
- modelClassHint,
170
- escalationReason,
171
- } = {}) {
172
- const base = trace && typeof trace === 'object'
173
- ? {
174
- ...trace,
175
- ordered_candidates: Array.isArray(trace.ordered_candidates)
176
- ? trace.ordered_candidates.map((item) => ({ ...item }))
177
- : [],
178
- selected: trace.selected && typeof trace.selected === 'object' ? { ...trace.selected } : null,
179
- ...(trace.adaptive && typeof trace.adaptive === 'object'
180
- ? {
181
- adaptive: {
182
- ...trace.adaptive,
183
- candidates: Array.isArray(trace.adaptive.candidates)
184
- ? trace.adaptive.candidates.map((item) => ({ ...item }))
185
- : [],
186
- },
187
- }
188
- : {}),
189
- }
190
- : traceBase({ role, logicalAttempt, modelClassHint, escalationReason });
191
- if (role === 'worker' || role === 'reviewer') base.role = role;
192
- if (Number.isInteger(logicalAttempt) && logicalAttempt >= 0) base.logical_attempt = logicalAttempt;
193
- if (modelClassHint === 'flash' || modelClassHint === 'pro' || modelClassHint === null) {
194
- base.model_class_hint = modelClassHint;
195
- }
196
- if (typeof escalationReason === 'string' && escalationReason) base.escalation_reason = escalationReason;
197
- else if (logicalAttempt === 0) base.escalation_reason = null;
198
- return base;
199
- }
200
-
201
- export function resolveWorkerModel({
202
- tier,
203
- priority,
204
- priorityConfigured = false,
205
- catalog,
206
- harnessDefault,
207
- fallback = 'harness-default',
208
- preferredModelId = DEFAULT_TIER_MODEL_PREFERENCES[tier],
209
- traceContext = {},
210
- adaptive,
211
- adaptiveHealth,
212
- } = {}) {
213
- const providers = providerMap(catalog);
214
- const normalizedPriority = normalizeModelPriority(priority);
215
- const prioritySource = typeof traceContext.prioritySource === 'string' && traceContext.prioritySource
216
- ? traceContext.prioritySource
217
- : 'priority';
218
- const trace = traceBase({
219
- role: traceContext.role ?? 'worker',
220
- logicalAttempt: traceContext.logicalAttempt ?? 0,
221
- modelClassHint: traceContext.modelClassHint ?? null,
222
- strategy: traceContext.strategy ?? 'legacy-tier',
223
- candidateSet: traceContext.candidateSet ?? 'primary',
224
- escalationReason: traceContext.escalationReason ?? null,
225
- });
226
-
227
- // Explicit priority — including an intentionally empty configured list — is
228
- // authoritative. Adaptive mode records that bypass but never reorders it.
229
- if (priorityConfigured || normalizedPriority.length > 0) {
230
- rankForTrace(trace, normalizedPriority, { adaptive, adaptiveHealth, explicitPriority: true });
231
- }
232
-
233
- for (let index = 0; index < normalizedPriority.length; index++) {
234
- const ref = normalizedPriority[index];
235
- const provider = providers.get(ref.provider);
236
- if (!provider) {
237
- trace.ordered_candidates.push(candidateDecision(ref, prioritySource, 'skipped', {
238
- reasonCode: MODEL_SELECTION_REASON_CODES.PROVIDER_UNAVAILABLE,
239
- }));
240
- continue;
241
- }
242
- const advertised = (provider.models ?? []).some((model) => model?.id === ref.model);
243
- selectTrace(trace, ref, prioritySource, { advertised });
244
- return {
245
- ok: true,
246
- ...ref,
247
- source: 'priority',
248
- matchedPriorityIndex: index,
249
- ...(advertised ? {} : { advertised: false }),
250
- selection_trace: trace,
251
- };
252
- }
253
-
254
- // A manually managed list, including an intentionally empty list, replaces
255
- // the fresh-config recommendation rather than silently re-inserting it.
256
- if (!priorityConfigured && normalizedPriority.length === 0 && typeof preferredModelId === 'string') {
257
- const matches = [];
258
- for (const provider of providers.values()) {
259
- if ((provider.models ?? []).some((model) => model?.id === preferredModelId)) {
260
- matches.push({ provider: provider.id, model: preferredModelId });
261
- }
262
- }
263
- if (matches.length === 1) {
264
- rankForTrace(trace, matches, { adaptive, adaptiveHealth });
265
- selectTrace(trace, matches[0], 'preferred-default');
266
- return { ok: true, ...matches[0], source: 'preferred-default', selection_trace: trace };
267
- }
268
- if (matches.length > 1) {
269
- const preferredProvider = normalizeModelRef(harnessDefault)?.provider;
270
- const deterministicMatch = matches.find((candidate) => candidate.provider === preferredProvider) ?? null;
271
- const baseline = deterministicMatch
272
- ? [deterministicMatch, ...matches.filter((candidate) => candidate.provider !== deterministicMatch.provider)]
273
- : matches;
274
- const ranked = rankForTrace(trace, baseline, { adaptive, adaptiveHealth });
275
- const adaptiveChoice = ranked.trace.decision_supported ? ranked.candidates[0] : null;
276
- const match = adaptiveChoice ?? deterministicMatch;
277
- if (match) {
278
- const decisionOrder = adaptiveChoice ? ranked.candidates : matches;
279
- for (const candidate of decisionOrder) {
280
- if (candidate.provider === match.provider && candidate.model === match.model) continue;
281
- trace.ordered_candidates.push(candidateDecision(candidate, 'preferred-default', 'skipped', {
282
- reasonCode: adaptiveChoice
283
- ? MODEL_SELECTION_REASON_CODES.ADAPTIVE_DEPRIORITIZED
284
- : MODEL_SELECTION_REASON_CODES.PREFERRED_MODEL_AMBIGUOUS,
285
- }));
286
- }
287
- selectTrace(trace, match, 'preferred-default');
288
- return { ok: true, ...match, source: 'preferred-default', selection_trace: trace };
289
- }
290
- for (const candidate of matches) {
291
- trace.ordered_candidates.push(candidateDecision(candidate, 'preferred-default', 'skipped', {
292
- reasonCode: MODEL_SELECTION_REASON_CODES.PREFERRED_MODEL_AMBIGUOUS,
293
- }));
294
- }
295
- } else {
296
- rankForTrace(trace, [], { adaptive, adaptiveHealth });
297
- trace.ordered_candidates.push(candidateDecision(
298
- { provider: null, model: preferredModelId },
299
- 'preferred-default',
300
- 'skipped',
301
- { reasonCode: MODEL_SELECTION_REASON_CODES.PREFERRED_MODEL_UNAVAILABLE },
302
- ));
303
- }
304
- }
305
-
306
- if (adaptive?.enabled === true && trace.adaptive === undefined) {
307
- rankForTrace(trace, [], { adaptive, adaptiveHealth });
308
- }
309
-
310
- if (fallback === 'harness-default') {
311
- const defaultRef = normalizeModelRef(harnessDefault);
312
- if (!defaultRef) {
313
- trace.ordered_candidates.push(candidateDecision(null, 'harness-default', 'skipped', {
314
- reasonCode: MODEL_SELECTION_REASON_CODES.HARNESS_DEFAULT_INVALID,
315
- }));
316
- } else if (!providers.has(defaultRef.provider)) {
317
- trace.ordered_candidates.push(candidateDecision(defaultRef, 'harness-default', 'skipped', {
318
- reasonCode: MODEL_SELECTION_REASON_CODES.HARNESS_DEFAULT_PROVIDER_UNAVAILABLE,
319
- }));
320
- } else {
321
- const fallbackReason = traceContext.candidateSet === 'escalation'
322
- ? MODEL_SELECTION_REASON_CODES.ESCALATION_CANDIDATES_EXHAUSTED
323
- : MODEL_SELECTION_REASON_CODES.PRIMARY_CANDIDATES_EXHAUSTED;
324
- selectTrace(trace, defaultRef, 'harness-default', { fallbackReason });
325
- return {
326
- ok: true,
327
- ...defaultRef,
328
- source: 'harness-default',
329
- ...(typeof harnessDefault.reasoningEffort === 'string' && harnessDefault.reasoningEffort
330
- ? { reasoningEffort: harnessDefault.reasoningEffort }
331
- : {}),
332
- selection_trace: trace,
333
- };
334
- }
335
- }
336
- trace.fallback_reason = MODEL_SELECTION_REASON_CODES.NO_AVAILABLE_MODEL;
337
- return {
338
- ok: false,
339
- code: NO_WORKER_MODEL_AVAILABLE,
340
- message: `No Harness model is available for the ${tier ?? 'requested'} worker.`,
341
- selection_trace: trace,
342
- };
343
- }
344
-
345
- /**
346
- * v0.2 role-based model selection. Turns a role's model policy (from
347
- * policy.resolveModelPolicy) into an ordered selection:
348
- * attempt 0 → policy.priority (primary / cheap candidates)
349
- * attempt >= 1 → policy.escalation_priority (strong / escalation candidates)
350
- * otherwise → Harness Default fallback
351
- * The output shape matches resolveWorkerModel (provider/model/source/...) plus
352
- * role + attempt so selection provenance lands in job metadata.
353
- */
354
- export function resolveModel({
355
- role = 'worker',
356
- attempt = 0,
357
- policy,
358
- catalog,
359
- harnessDefault,
360
- adaptiveHealth,
361
- } = {}) {
362
- const p = policy && typeof policy === 'object' ? policy : {};
363
- const escalated = Number.isInteger(attempt) && attempt > 0;
364
- const candidates = escalated ? p.escalation_priority : p.priority;
365
- const configured = escalated
366
- ? p.escalation_priority_configured === true || (Array.isArray(candidates) && candidates.length > 0)
367
- : p.priorityConfigured === true || (Array.isArray(candidates) && candidates.length > 0);
368
- // Escalation never re-picks the fresh "preferred" role default: an empty
369
- // escalation pool falls through to Harness Default instead.
370
- const preferredModelId = escalated ? undefined : DEFAULT_ROLE_MODEL_PREFERENCES[role] ?? undefined;
371
- const result = resolveWorkerModel({
372
- tier: role,
373
- priority: candidates,
374
- priorityConfigured: configured,
375
- catalog,
376
- harnessDefault,
377
- fallback: p.fallback === 'harness-default' ? 'harness-default' : 'harness-default',
378
- preferredModelId,
379
- adaptive: p.adaptive,
380
- adaptiveHealth,
381
- traceContext: {
382
- role,
383
- logicalAttempt: attempt,
384
- strategy: p.strategy ?? (role === 'reviewer' ? 'strong' : 'balanced'),
385
- candidateSet: escalated ? 'escalation' : 'primary',
386
- prioritySource: escalated ? 'escalation-priority' : 'priority',
387
- },
388
- });
389
- if (!result.ok) return { ...result, role, attempt };
390
- return { ...result, role, attempt };
391
- }
1
+ // Pure Harness-backed worker model selection. A worker tier describes a role,
2
+ // not a fixed model: explicit provider/model priorities win, fresh configs may
3
+ // use a tier-specific preferred model id, and Harness Default is the final
4
+ // fallback. Catalog membership is advisory; provider registration is the
5
+ // routing boundary.
6
+
7
+ import { rankAdaptiveCandidates } from './adaptive-routing.mjs';
8
+
9
+ export const DEFAULT_TIER_MODEL_PREFERENCES = Object.freeze({
10
+ flash: 'deepseek-v4-flash',
11
+ pro: 'deepseek-v4-pro',
12
+ });
13
+
14
+ // v0.2 role → default preferred model class. A role describes who does the
15
+ // work; the model class is only the fresh-config recommendation until a
16
+ // priority list or Harness Default takes over.
17
+ export const DEFAULT_ROLE_MODEL_PREFERENCES = Object.freeze({
18
+ worker: 'deepseek-v4-flash',
19
+ reviewer: 'deepseek-v4-pro',
20
+ });
21
+
22
+ export const MODEL_FALLBACKS = ['harness-default'];
23
+ export const NO_WORKER_MODEL_AVAILABLE = 'NO_WORKER_MODEL_AVAILABLE';
24
+ export const MODEL_SELECTION_TRACE_VERSION = 1;
25
+ export const MODEL_SELECTION_REASON_CODES = Object.freeze({
26
+ PROVIDER_UNAVAILABLE: 'PROVIDER_UNAVAILABLE',
27
+ PREFERRED_MODEL_UNAVAILABLE: 'PREFERRED_MODEL_UNAVAILABLE',
28
+ PREFERRED_MODEL_AMBIGUOUS: 'PREFERRED_MODEL_AMBIGUOUS',
29
+ ADAPTIVE_DEPRIORITIZED: 'ADAPTIVE_DEPRIORITIZED',
30
+ HARNESS_DEFAULT_INVALID: 'HARNESS_DEFAULT_INVALID',
31
+ HARNESS_DEFAULT_PROVIDER_UNAVAILABLE: 'HARNESS_DEFAULT_PROVIDER_UNAVAILABLE',
32
+ PRIMARY_CANDIDATES_EXHAUSTED: 'PRIMARY_CANDIDATES_EXHAUSTED',
33
+ ESCALATION_CANDIDATES_EXHAUSTED: 'ESCALATION_CANDIDATES_EXHAUSTED',
34
+ NO_AVAILABLE_MODEL: 'NO_AVAILABLE_MODEL',
35
+ });
36
+
37
+ export function normalizeModelRef(raw) {
38
+ if (!raw || typeof raw !== 'object') return null;
39
+ const provider = typeof raw.provider === 'string' ? raw.provider.trim() : '';
40
+ const model = typeof raw.model === 'string' ? raw.model.trim() : '';
41
+ return provider && model ? { provider, model } : null;
42
+ }
43
+
44
+ export function modelRefKey(raw) {
45
+ const ref = normalizeModelRef(raw);
46
+ return ref ? `${ref.provider}\0${ref.model}` : '';
47
+ }
48
+
49
+ export function normalizeModelPriority(raw) {
50
+ if (!Array.isArray(raw)) return [];
51
+ const seen = new Set();
52
+ const result = [];
53
+ for (const value of raw) {
54
+ const ref = normalizeModelRef(value);
55
+ if (!ref) continue;
56
+ const key = modelRefKey(ref);
57
+ if (seen.has(key)) continue;
58
+ seen.add(key);
59
+ result.push(ref);
60
+ }
61
+ return result;
62
+ }
63
+
64
+ function providerMap(catalog) {
65
+ const map = new Map();
66
+ for (const raw of catalog?.providers ?? []) {
67
+ if (!raw || typeof raw.id !== 'string' || raw.id === '') continue;
68
+ map.set(raw.id, raw);
69
+ }
70
+ return map;
71
+ }
72
+
73
+ function normalizeAttempt(value) {
74
+ return Number.isInteger(value) && value >= 0 ? value : 0;
75
+ }
76
+
77
+ function normalizeModelClassHint(value) {
78
+ return value === 'flash' || value === 'pro' ? value : null;
79
+ }
80
+
81
+ function traceBase({
82
+ role = 'worker',
83
+ logicalAttempt = 0,
84
+ modelClassHint = null,
85
+ strategy = 'legacy-tier',
86
+ candidateSet = 'primary',
87
+ escalationReason = null,
88
+ } = {}) {
89
+ return {
90
+ version: MODEL_SELECTION_TRACE_VERSION,
91
+ role: role === 'reviewer' ? 'reviewer' : 'worker',
92
+ logical_attempt: normalizeAttempt(logicalAttempt),
93
+ model_class_hint: normalizeModelClassHint(modelClassHint),
94
+ strategy: typeof strategy === 'string' && strategy ? strategy : 'legacy-tier',
95
+ candidate_set: typeof candidateSet === 'string' && candidateSet ? candidateSet : 'primary',
96
+ ordered_candidates: [],
97
+ selected: null,
98
+ selection_source: null,
99
+ fallback_reason: null,
100
+ escalation_reason: typeof escalationReason === 'string' && escalationReason ? escalationReason : null,
101
+ };
102
+ }
103
+
104
+ function candidateDecision(ref, source, status, { reasonCode, advertised } = {}) {
105
+ return {
106
+ provider: ref?.provider ?? null,
107
+ model: ref?.model ?? null,
108
+ source,
109
+ status,
110
+ ...(reasonCode ? { reason_code: reasonCode } : {}),
111
+ ...(advertised === false ? { advertised: false } : {}),
112
+ };
113
+ }
114
+
115
+ function selectTrace(trace, ref, source, { advertised, fallbackReason } = {}) {
116
+ trace.ordered_candidates.push(candidateDecision(ref, source, 'selected', { advertised }));
117
+ trace.selected = { provider: ref.provider, model: ref.model, source };
118
+ trace.selection_source = source;
119
+ trace.fallback_reason = fallbackReason ?? null;
120
+ return trace;
121
+ }
122
+
123
+ function rankForTrace(trace, candidates, {
124
+ adaptive,
125
+ adaptiveHealth,
126
+ explicitPriority = false,
127
+ } = {}) {
128
+ const ranked = rankAdaptiveCandidates(candidates, {
129
+ config: adaptive,
130
+ healthStore: adaptiveHealth,
131
+ role: trace.role,
132
+ explicitPriority,
133
+ });
134
+ if (ranked.trace.enabled) trace.adaptive = ranked.trace;
135
+ return ranked;
136
+ }
137
+
138
+ /**
139
+ * Build a one-candidate trace for transports that intentionally bypass the
140
+ * Harness catalog (DeepSeek Official strict mode / standalone legacy mode).
141
+ */
142
+ export function buildDirectSelectionTrace({
143
+ role = 'worker',
144
+ logicalAttempt = 0,
145
+ modelClassHint = null,
146
+ strategy = 'legacy-strict',
147
+ candidateSet = 'primary',
148
+ provider,
149
+ model,
150
+ source = 'legacy-strict',
151
+ escalationReason = null,
152
+ } = {}) {
153
+ const trace = traceBase({ role, logicalAttempt, modelClassHint, strategy, candidateSet, escalationReason });
154
+ const ref = normalizeModelRef({ provider, model });
155
+ if (!ref) {
156
+ trace.fallback_reason = MODEL_SELECTION_REASON_CODES.NO_AVAILABLE_MODEL;
157
+ return trace;
158
+ }
159
+ return selectTrace(trace, ref, source);
160
+ }
161
+
162
+ /**
163
+ * Add workflow-only context after a transport has resolved the model. This is
164
+ * intentionally metadata-only: it never changes provider/model selection.
165
+ */
166
+ export function enrichSelectionTrace(trace, {
167
+ role,
168
+ logicalAttempt,
169
+ modelClassHint,
170
+ escalationReason,
171
+ } = {}) {
172
+ const base = trace && typeof trace === 'object'
173
+ ? {
174
+ ...trace,
175
+ ordered_candidates: Array.isArray(trace.ordered_candidates)
176
+ ? trace.ordered_candidates.map((item) => ({ ...item }))
177
+ : [],
178
+ selected: trace.selected && typeof trace.selected === 'object' ? { ...trace.selected } : null,
179
+ ...(trace.adaptive && typeof trace.adaptive === 'object'
180
+ ? {
181
+ adaptive: {
182
+ ...trace.adaptive,
183
+ candidates: Array.isArray(trace.adaptive.candidates)
184
+ ? trace.adaptive.candidates.map((item) => ({ ...item }))
185
+ : [],
186
+ },
187
+ }
188
+ : {}),
189
+ }
190
+ : traceBase({ role, logicalAttempt, modelClassHint, escalationReason });
191
+ if (role === 'worker' || role === 'reviewer') base.role = role;
192
+ if (Number.isInteger(logicalAttempt) && logicalAttempt >= 0) base.logical_attempt = logicalAttempt;
193
+ if (modelClassHint === 'flash' || modelClassHint === 'pro' || modelClassHint === null) {
194
+ base.model_class_hint = modelClassHint;
195
+ }
196
+ if (typeof escalationReason === 'string' && escalationReason) base.escalation_reason = escalationReason;
197
+ else if (logicalAttempt === 0) base.escalation_reason = null;
198
+ return base;
199
+ }
200
+
201
+ export function resolveWorkerModel({
202
+ tier,
203
+ priority,
204
+ priorityConfigured = false,
205
+ catalog,
206
+ harnessDefault,
207
+ fallback = 'harness-default',
208
+ preferredModelId = DEFAULT_TIER_MODEL_PREFERENCES[tier],
209
+ traceContext = {},
210
+ adaptive,
211
+ adaptiveHealth,
212
+ } = {}) {
213
+ const providers = providerMap(catalog);
214
+ const normalizedPriority = normalizeModelPriority(priority);
215
+ const prioritySource = typeof traceContext.prioritySource === 'string' && traceContext.prioritySource
216
+ ? traceContext.prioritySource
217
+ : 'priority';
218
+ const trace = traceBase({
219
+ role: traceContext.role ?? 'worker',
220
+ logicalAttempt: traceContext.logicalAttempt ?? 0,
221
+ modelClassHint: traceContext.modelClassHint ?? null,
222
+ strategy: traceContext.strategy ?? 'legacy-tier',
223
+ candidateSet: traceContext.candidateSet ?? 'primary',
224
+ escalationReason: traceContext.escalationReason ?? null,
225
+ });
226
+
227
+ // Explicit priority — including an intentionally empty configured list — is
228
+ // authoritative. Adaptive mode records that bypass but never reorders it.
229
+ if (priorityConfigured || normalizedPriority.length > 0) {
230
+ rankForTrace(trace, normalizedPriority, { adaptive, adaptiveHealth, explicitPriority: true });
231
+ }
232
+
233
+ for (let index = 0; index < normalizedPriority.length; index++) {
234
+ const ref = normalizedPriority[index];
235
+ const provider = providers.get(ref.provider);
236
+ if (!provider) {
237
+ trace.ordered_candidates.push(candidateDecision(ref, prioritySource, 'skipped', {
238
+ reasonCode: MODEL_SELECTION_REASON_CODES.PROVIDER_UNAVAILABLE,
239
+ }));
240
+ continue;
241
+ }
242
+ const advertised = (provider.models ?? []).some((model) => model?.id === ref.model);
243
+ selectTrace(trace, ref, prioritySource, { advertised });
244
+ return {
245
+ ok: true,
246
+ ...ref,
247
+ source: 'priority',
248
+ matchedPriorityIndex: index,
249
+ ...(advertised ? {} : { advertised: false }),
250
+ selection_trace: trace,
251
+ };
252
+ }
253
+
254
+ // A manually managed list, including an intentionally empty list, replaces
255
+ // the fresh-config recommendation rather than silently re-inserting it.
256
+ if (!priorityConfigured && normalizedPriority.length === 0 && typeof preferredModelId === 'string') {
257
+ const matches = [];
258
+ for (const provider of providers.values()) {
259
+ if ((provider.models ?? []).some((model) => model?.id === preferredModelId)) {
260
+ matches.push({ provider: provider.id, model: preferredModelId });
261
+ }
262
+ }
263
+ if (matches.length === 1) {
264
+ rankForTrace(trace, matches, { adaptive, adaptiveHealth });
265
+ selectTrace(trace, matches[0], 'preferred-default');
266
+ return { ok: true, ...matches[0], source: 'preferred-default', selection_trace: trace };
267
+ }
268
+ if (matches.length > 1) {
269
+ const preferredProvider = normalizeModelRef(harnessDefault)?.provider;
270
+ const deterministicMatch = matches.find((candidate) => candidate.provider === preferredProvider) ?? null;
271
+ const baseline = deterministicMatch
272
+ ? [deterministicMatch, ...matches.filter((candidate) => candidate.provider !== deterministicMatch.provider)]
273
+ : matches;
274
+ const ranked = rankForTrace(trace, baseline, { adaptive, adaptiveHealth });
275
+ const adaptiveChoice = ranked.trace.decision_supported ? ranked.candidates[0] : null;
276
+ const match = adaptiveChoice ?? deterministicMatch;
277
+ if (match) {
278
+ const decisionOrder = adaptiveChoice ? ranked.candidates : matches;
279
+ for (const candidate of decisionOrder) {
280
+ if (candidate.provider === match.provider && candidate.model === match.model) continue;
281
+ trace.ordered_candidates.push(candidateDecision(candidate, 'preferred-default', 'skipped', {
282
+ reasonCode: adaptiveChoice
283
+ ? MODEL_SELECTION_REASON_CODES.ADAPTIVE_DEPRIORITIZED
284
+ : MODEL_SELECTION_REASON_CODES.PREFERRED_MODEL_AMBIGUOUS,
285
+ }));
286
+ }
287
+ selectTrace(trace, match, 'preferred-default');
288
+ return { ok: true, ...match, source: 'preferred-default', selection_trace: trace };
289
+ }
290
+ for (const candidate of matches) {
291
+ trace.ordered_candidates.push(candidateDecision(candidate, 'preferred-default', 'skipped', {
292
+ reasonCode: MODEL_SELECTION_REASON_CODES.PREFERRED_MODEL_AMBIGUOUS,
293
+ }));
294
+ }
295
+ } else {
296
+ rankForTrace(trace, [], { adaptive, adaptiveHealth });
297
+ trace.ordered_candidates.push(candidateDecision(
298
+ { provider: null, model: preferredModelId },
299
+ 'preferred-default',
300
+ 'skipped',
301
+ { reasonCode: MODEL_SELECTION_REASON_CODES.PREFERRED_MODEL_UNAVAILABLE },
302
+ ));
303
+ }
304
+ }
305
+
306
+ if (adaptive?.enabled === true && trace.adaptive === undefined) {
307
+ rankForTrace(trace, [], { adaptive, adaptiveHealth });
308
+ }
309
+
310
+ if (fallback === 'harness-default') {
311
+ const defaultRef = normalizeModelRef(harnessDefault);
312
+ if (!defaultRef) {
313
+ trace.ordered_candidates.push(candidateDecision(null, 'harness-default', 'skipped', {
314
+ reasonCode: MODEL_SELECTION_REASON_CODES.HARNESS_DEFAULT_INVALID,
315
+ }));
316
+ } else if (!providers.has(defaultRef.provider)) {
317
+ trace.ordered_candidates.push(candidateDecision(defaultRef, 'harness-default', 'skipped', {
318
+ reasonCode: MODEL_SELECTION_REASON_CODES.HARNESS_DEFAULT_PROVIDER_UNAVAILABLE,
319
+ }));
320
+ } else {
321
+ const fallbackReason = traceContext.candidateSet === 'escalation'
322
+ ? MODEL_SELECTION_REASON_CODES.ESCALATION_CANDIDATES_EXHAUSTED
323
+ : MODEL_SELECTION_REASON_CODES.PRIMARY_CANDIDATES_EXHAUSTED;
324
+ selectTrace(trace, defaultRef, 'harness-default', { fallbackReason });
325
+ return {
326
+ ok: true,
327
+ ...defaultRef,
328
+ source: 'harness-default',
329
+ ...(typeof harnessDefault.reasoningEffort === 'string' && harnessDefault.reasoningEffort
330
+ ? { reasoningEffort: harnessDefault.reasoningEffort }
331
+ : {}),
332
+ selection_trace: trace,
333
+ };
334
+ }
335
+ }
336
+ trace.fallback_reason = MODEL_SELECTION_REASON_CODES.NO_AVAILABLE_MODEL;
337
+ return {
338
+ ok: false,
339
+ code: NO_WORKER_MODEL_AVAILABLE,
340
+ message: `No Harness model is available for the ${tier ?? 'requested'} worker.`,
341
+ selection_trace: trace,
342
+ };
343
+ }
344
+
345
+ /**
346
+ * v0.2 role-based model selection. Turns a role's model policy (from
347
+ * policy.resolveModelPolicy) into an ordered selection:
348
+ * attempt 0 → policy.priority (primary / cheap candidates)
349
+ * attempt >= 1 → policy.escalation_priority (strong / escalation candidates)
350
+ * otherwise → Harness Default fallback
351
+ * The output shape matches resolveWorkerModel (provider/model/source/...) plus
352
+ * role + attempt so selection provenance lands in job metadata.
353
+ */
354
+ export function resolveModel({
355
+ role = 'worker',
356
+ attempt = 0,
357
+ policy,
358
+ catalog,
359
+ harnessDefault,
360
+ adaptiveHealth,
361
+ } = {}) {
362
+ const p = policy && typeof policy === 'object' ? policy : {};
363
+ const escalated = Number.isInteger(attempt) && attempt > 0;
364
+ const candidates = escalated ? p.escalation_priority : p.priority;
365
+ const configured = escalated
366
+ ? p.escalation_priority_configured === true || (Array.isArray(candidates) && candidates.length > 0)
367
+ : p.priorityConfigured === true || (Array.isArray(candidates) && candidates.length > 0);
368
+ // Escalation never re-picks the fresh "preferred" role default: an empty
369
+ // escalation pool falls through to Harness Default instead.
370
+ const preferredModelId = escalated ? undefined : DEFAULT_ROLE_MODEL_PREFERENCES[role] ?? undefined;
371
+ const result = resolveWorkerModel({
372
+ tier: role,
373
+ priority: candidates,
374
+ priorityConfigured: configured,
375
+ catalog,
376
+ harnessDefault,
377
+ fallback: p.fallback === 'harness-default' ? 'harness-default' : 'harness-default',
378
+ preferredModelId,
379
+ adaptive: p.adaptive,
380
+ adaptiveHealth,
381
+ traceContext: {
382
+ role,
383
+ logicalAttempt: attempt,
384
+ strategy: p.strategy ?? (role === 'reviewer' ? 'strong' : 'balanced'),
385
+ candidateSet: escalated ? 'escalation' : 'primary',
386
+ prioritySource: escalated ? 'escalation-priority' : 'priority',
387
+ },
388
+ });
389
+ if (!result.ok) return { ...result, role, attempt };
390
+ return { ...result, role, attempt };
391
+ }