@ran-sh/dsh-crew 0.5.6 → 0.5.7

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 (39) hide show
  1. package/README.md +14 -7
  2. package/README.zh.md +12 -7
  3. package/docs/installation.md +1 -1
  4. package/docs/ui-surfaces.md +65 -46
  5. package/lib/client.js +345 -22
  6. package/official-web-bridge/lib/client.js +345 -22
  7. package/package.json +1 -1
  8. package/src/client/collapsible-sections.mjs +3 -2
  9. package/src/client/index.tsx +227 -51
  10. package/src/config-readiness.mjs +56 -5
  11. package/src/extension-contract.mjs +9 -5
  12. package/src/hub/index.mjs +854 -445
  13. package/src/hub-client.mjs +24 -4
  14. package/src/hub-compatibility.mjs +23 -7
  15. package/src/install/install.mjs +4 -4
  16. package/src/install/npx-lifecycle.mjs +263 -118
  17. package/src/job-contracts.mjs +19 -3
  18. package/src/jobs.mjs +2 -4
  19. package/src/local-request-guard.mjs +60 -0
  20. package/src/mcp-runtime.mjs +7 -5
  21. package/src/model-routing.mjs +141 -49
  22. package/src/multimodal.mjs +0 -0
  23. package/src/official-web-bridge.mjs +242 -84
  24. package/src/policy.mjs +62 -31
  25. package/src/provider-config-scrub.mjs +71 -0
  26. package/src/provider-delete-adapters.mjs +424 -0
  27. package/src/provider-health.mjs +124 -0
  28. package/src/provider-inventory.mjs +132 -0
  29. package/src/provider-lifecycle-state.mjs +81 -0
  30. package/src/provider-lifecycle.mjs +214 -0
  31. package/src/provider-profile-store.mjs +171 -0
  32. package/src/readiness-matrix.mjs +11 -5
  33. package/src/removable-waiter.mjs +29 -0
  34. package/src/runtime-identity.mjs +43 -13
  35. package/src/server.mjs +34 -16
  36. package/src/status-shard.mjs +13 -2
  37. package/src/workflow-runtime.mjs +44 -10
  38. package/windows/start-dsh-crew.cmd +3 -3
  39. package/windows/start-dsh-crew.ps1 +49 -5
@@ -52,7 +52,7 @@ const COPY = {
52
52
  expandAll: '全部展开', collapseAll: '全部折叠',
53
53
  modelCount: (count: number) => `${count} 个模型`, providerCount: (count: number) => `${count} 个 Provider`,
54
54
  jobCount: (count: number) => `${count} 个任务`, runningCount: (count: number) => `${count} 个运行中`,
55
- sectionNames: { integrations: 'Codex / Claude / ZCode 集成状态', workflow: 'Crew 工作流设置', flash: 'Worker / Flash', pro: 'Reviewer / Pro', dispatch: '模型优先级与派发', adaptive: '自适应路由', runtime: '运行 / 生效边界', multimodal: '视觉与生图', providers: '自定义 Provider', jobs: '任务状态' },
55
+ sectionNames: { integrations: 'Codex / Claude / ZCode 集成状态', workflow: 'Crew 工作流设置', flash: 'Worker / Flash', pro: 'Reviewer / Pro', dispatch: '模型优先级与派发', adaptive: '自适应路由', runtime: '运行 / 生效边界', multimodal: '视觉与生图', harnessProviders: 'Harness Providers', providers: '多模态适配器', jobs: '任务状态' },
56
56
  openHarness: '打开 3210 Crew Harness →',
57
57
  harnessHint: '底层 Provider、Harness Models 与运行时配置',
58
58
  hostReadiness: '宿主集成就绪度', hostReadinessHint: '只使用结构化安装与运行时证据;缺少证据不会显示 READY。',
@@ -79,7 +79,8 @@ const COPY = {
79
79
  workerModels: 'Worker 模型', refreshHarnessModels: '刷新 Harness 模型', refreshingModels: '刷新中…',
80
80
  modelPriority: '模型优先级', addPriorityModel: '+ 添加优先模型', modelSearch: '搜索 Provider 或 Model…',
81
81
  harnessDefault: 'Harness 默认模型', providerUnavailable: 'Provider 不可用', notAdvertised: '当前未列出',
82
- noPriority: '未配置优先项', modelPoolSummary: (p: number, m: number) => `Providers: ${p} · Models: ${m}`,
82
+ noPriority: '未配置优先项', modelPoolSummary: (p: number, m: number) => `Providers: ${p} · Models: ${m}`,
83
+ healthUnprobed: 'UNPROBED', checkedAgo: (s: number) => `${s} 秒前检查`,
83
84
  partialCatalog: '部分 Provider 读取失败', catalogFailed: '无法读取 Harness 模型目录', removePriority: '移除', moveUp: '上移', moveDown: '下移',
84
85
  needsModelSelection: '多个 Provider 提供默认偏好模型,且 Harness Default 无法消歧;当前将回退到 Harness Default,请手动选择。',
85
86
  responsibilities: '职责',
@@ -100,9 +101,15 @@ const COPY = {
100
101
  tierPolicyDesc: { auto: 'auto · orchestrator 自选', 'flash-only': '只用 flash', 'pro-only': '只用 pro' },
101
102
  escalateHint: 'flash 失败自动用 pro 重试一次',
102
103
  presetFlash: 'flash 模式', presetPro: 'pro 模式',
103
- multimodal: '多模态',
104
- visionProvider: '视觉 provider', visionModel: '视觉模型', imagegenProvider: '生图 provider',
105
- customProviders: '自定义 Provider', addProvider: '+ 添加 Provider',
104
+ multimodal: '多模态',
105
+ visionProvider: '视觉 provider', visionModel: '视觉模型', imagegenProvider: '生图 provider',
106
+ harnessProviders: 'Harness Providers', harnessProviderHint: '3210 Crew Harness 的真实 Provider 注册与生命周期;不是视觉 / 生图适配器。',
107
+ providerState: (state: string) => `状态:${state}`, providerModels: (count: number) => `${count} 个模型`, providerJobs: (count: number) => `${count} 个运行任务`,
108
+ providerDeletePlan: '生成删除计划', providerDelete: '确认删除', providerPlanReady: '计划已生成;再次确认后才会修改 3210 配置。',
109
+ providerReplacement: '替换 Harness 默认 Provider(输入 Provider id)', providerDeleteConfirm: '确认按计划删除这个 Harness Provider?会清理路由引用并重启 3210。',
110
+ providerDeleteBlocked: '当前 Provider 不能删除(内置、在用、已删除或缺少可用替换)。', providerLifecycleError: 'Provider 生命周期操作失败',
111
+ providerNoInventory: '暂时无法读取 3210 Provider inventory。',
112
+ customProviders: '自定义 Provider', addProvider: '+ 添加 Provider',
106
113
  noCustomProviders: '暂无。添加后会出现在上方的视觉 / 生图 provider 选择里。',
107
114
  providerName: '名称', modelsField: '模型列表(逗号分隔)',
108
115
  providerType: '接入方式', typeApi: 'API · 兼容 OpenAI 接口', typeCli: 'CLI · 本地命令',
@@ -131,7 +138,11 @@ const COPY = {
131
138
  roleWorker: 'Worker 角色(执行)', roleReviewer: 'Reviewer 角色(独立审查)',
132
139
  workerStateLabel: 'Worker 状态', reviewStateLabel: 'Reviewer 状态',
133
140
  roleStateHint: 'auto:orchestrator 可自动派发;manual:仅显式点名;disabled:禁用。未显式设置时由协作模式推导。',
134
- autoReview: '自动复审', autoReviewHint: 'worker 成功后自动运行一次 reviewer(独立模型策略,只读)',
141
+ autoReview: '自动复审', autoReviewHint: 'worker 成功后自动运行一次 reviewer(独立模型策略,只读)',
142
+ modelOrdering: '候选排序', orderingDesc: { manual: '手动顺序', 'health-aware': '健康感知' },
143
+ orderingHint: '手动 priority 始终保持顺序;健康感知仅重排未显式锁定的自动候选。',
144
+ healthGate: '健康门', healthGateDesc: { 'hard-failures': '跳过明确不可调用', off: '关闭健康门' },
145
+ reviewGate: 'Reviewer 验收门', reviewGateDesc: { required: 'required · 未批准则失败', optional: 'optional · 记录但不阻断', off: 'off · 不自动复审' },
135
146
  isolation: '工作区隔离', isolationHint: 'worktree 为默认;非 Git 仓库时需显式 shared,否则任务以 NOT_GIT_REPOSITORY 失败。',
136
147
  isolationDesc: { worktree: 'worktree · 每个 coding worker 独立临时 git worktree(主工作区不动)', shared: 'shared · 旧版就地执行' },
137
148
  maxParallel: '最大并行', maxParallelHint: '同时运行的工作流上限(1–16);超出进入队列。',
@@ -140,7 +151,7 @@ const COPY = {
140
151
  cardRuntime: { t: '执行与连接', d: 'worker 会话跑在本实例内还是独立进程,单个任务的超时上限,以及 CC / Codex 探测本实例的地址。' },
141
152
  cardMM: { t: '视觉与生图', d: '给纯文本的 DSH 模型借来眼睛和画笔:describe_image、会话贴图转写与 generate_image 都用这里的设置。' },
142
153
  cardCustomProv: { t: '自定义 Provider', d: '接入自己的 API 或本地命令;保存后会出现在上面的视觉 / 生图 provider 选择里。' },
143
- modeDesc: { auto: 'auto · 优先 hub', hub: 'hub · 必须 hub', standalone: 'standalone · 独立进程' },
154
+ modeDesc: { auto: 'auto · 优先 3210', hub: 'hub · 必须 3210' },
144
155
  save: '保存', saved: '已保存', jobs: 'Worker 任务', empty: '当前没有 Worker / Reviewer 任务。',
145
156
  adaptiveTitle: '自适应模型路由(实验)', adaptiveHint: '默认关闭;只重排系统自动候选,手动模型优先级始终保持原序。健康信号仅来自本进程已观察到的成功、失败、超时与粗粒度延迟,重启后清空。',
146
157
  adaptiveEnabled: '启用自适应路由', adaptiveWindow: '健康窗口', adaptiveSamples: '最少样本', adaptiveBoundary: '下一个工作流生效',
@@ -178,7 +189,7 @@ const COPY = {
178
189
  expandAll: 'Expand all', collapseAll: 'Collapse all',
179
190
  modelCount: (count: number) => `${count} models`, providerCount: (count: number) => `${count} providers`,
180
191
  jobCount: (count: number) => `${count} jobs`, runningCount: (count: number) => `${count} running`,
181
- sectionNames: { integrations: 'Codex / Claude / ZCode integration status', workflow: 'Crew workflow settings', flash: 'Worker / Flash', pro: 'Reviewer / Pro', dispatch: 'Model priority & dispatch', adaptive: 'Adaptive routing', runtime: 'Runtime / activation boundaries', multimodal: 'Vision & image generation', providers: 'Custom providers', jobs: 'Task status' },
192
+ sectionNames: { integrations: 'Codex / Claude / ZCode integration status', workflow: 'Crew workflow settings', flash: 'Worker / Flash', pro: 'Reviewer / Pro', dispatch: 'Model priority & dispatch', adaptive: 'Adaptive routing', runtime: 'Runtime / activation boundaries', multimodal: 'Vision & image generation', harnessProviders: 'Harness Providers', providers: 'Multimodal adapters', jobs: 'Task status' },
182
193
  openHarness: 'Open 3210 Crew Harness →',
183
194
  harnessHint: 'Low-level providers, Harness Models, and runtime configuration',
184
195
  hostReadiness: 'Host integration readiness', hostReadinessHint: 'Uses structured installer and runtime evidence only; missing evidence is never READY.',
@@ -205,7 +216,8 @@ const COPY = {
205
216
  workerModels: 'Worker Models', refreshHarnessModels: 'Refresh Harness Models', refreshingModels: 'Refreshing…',
206
217
  modelPriority: 'Model Priority', addPriorityModel: '+ Add Priority Model', modelSearch: 'Search Provider or Model…',
207
218
  harnessDefault: 'Harness Default', providerUnavailable: 'Provider unavailable', notAdvertised: 'Currently not advertised',
208
- noPriority: 'No priority models configured', modelPoolSummary: (p: number, m: number) => `Providers: ${p} · Models: ${m}`,
219
+ noPriority: 'No priority models configured', modelPoolSummary: (p: number, m: number) => `Providers: ${p} · Models: ${m}`,
220
+ healthUnprobed: 'UNPROBED', checkedAgo: (s: number) => `checked ${s}s ago`,
209
221
  partialCatalog: 'Some providers failed to load', catalogFailed: 'Unable to read Harness model catalog', removePriority: 'Remove', moveUp: 'Move up', moveDown: 'Move down',
210
222
  needsModelSelection: 'Multiple providers advertise the preferred model and Harness Default cannot disambiguate them. Falling back to Harness Default; choose a priority model explicitly.',
211
223
  responsibilities: 'Responsibilities',
@@ -226,9 +238,15 @@ const COPY = {
226
238
  tierPolicyDesc: { auto: 'auto · orchestrator picks', 'flash-only': 'flash only', 'pro-only': 'pro only' },
227
239
  escalateHint: 'retry a failed flash run once on pro',
228
240
  presetFlash: 'flash preset', presetPro: 'pro preset',
229
- multimodal: 'Multimodal',
230
- visionProvider: 'Vision provider', visionModel: 'Vision model', imagegenProvider: 'Image-gen provider',
231
- customProviders: 'Custom providers', addProvider: '+ Add provider',
241
+ multimodal: 'Multimodal',
242
+ visionProvider: 'Vision provider', visionModel: 'Vision model', imagegenProvider: 'Image-gen provider',
243
+ harnessProviders: 'Harness Providers', harnessProviderHint: 'Live Provider registration and lifecycle in the 3210 Crew Harness; separate from vision / image-gen adapters.',
244
+ providerState: (state: string) => `State: ${state}`, providerModels: (count: number) => `${count} models`, providerJobs: (count: number) => `${count} running jobs`,
245
+ providerDeletePlan: 'Plan deletion', providerDelete: 'Confirm delete', providerPlanReady: 'Plan ready; a second confirmation is required before changing 3210 config.',
246
+ providerReplacement: 'Replacement Harness Default provider id', providerDeleteConfirm: 'Delete this Harness Provider per the plan? Routing refs will be scrubbed and 3210 restarted.',
247
+ providerDeleteBlocked: 'This Provider cannot be deleted (built-in, in use, already absent, or missing a valid replacement).', providerLifecycleError: 'Provider lifecycle operation failed',
248
+ providerNoInventory: '3210 Provider inventory is temporarily unavailable.',
249
+ customProviders: 'Custom providers', addProvider: '+ Add provider',
232
250
  noCustomProviders: 'None yet. Added providers appear in the vision / image-gen selects above.',
233
251
  providerName: 'Name', modelsField: 'Models (comma-separated)',
234
252
  providerType: 'Connection', typeApi: 'API · OpenAI-compatible', typeCli: 'CLI · local command',
@@ -257,7 +275,11 @@ const COPY = {
257
275
  roleWorker: 'Worker role (execution)', roleReviewer: 'Reviewer role (independent review)',
258
276
  workerStateLabel: 'Worker state', reviewStateLabel: 'Reviewer state',
259
277
  roleStateHint: 'auto = the orchestrator may dispatch automatically; manual = only when explicitly named; disabled = off. When unset, derived from the collaboration mode.',
260
- autoReview: 'Automatic review', autoReviewHint: 'Run one reviewer pass (independent model policy, read-only) after a successful worker run',
278
+ autoReview: 'Automatic review', autoReviewHint: 'Run one reviewer pass (independent model policy, read-only) after a successful worker run',
279
+ modelOrdering: 'Candidate ordering', orderingDesc: { manual: 'Manual order', 'health-aware': 'Health-aware' },
280
+ orderingHint: 'Explicit priority always keeps its order; health-aware mode only reorders automatic, unlocked candidates.',
281
+ healthGate: 'Health gate', healthGateDesc: { 'hard-failures': 'Skip known hard failures', off: 'Health gate off' },
282
+ reviewGate: 'Reviewer acceptance gate', reviewGateDesc: { required: 'required · fail unless approved', optional: 'optional · record without blocking', off: 'off · no automatic review' },
261
283
  isolation: 'Workspace isolation', isolationHint: 'worktree is the default; non-git workspaces require explicit shared, otherwise jobs fail with NOT_GIT_REPOSITORY.',
262
284
  isolationDesc: { worktree: 'worktree · each coding worker runs in its own temporary git worktree (primary untouched)', shared: 'shared · legacy in-place execution' },
263
285
  maxParallel: 'Max parallel', maxParallelHint: 'Upper bound of concurrently running workflows (1–16); extra ones queue.',
@@ -266,7 +288,7 @@ const COPY = {
266
288
  cardRuntime: { t: 'Execution & connection', d: 'Whether worker sessions run inside this instance or a separate process, the per-job timeout, and the address CC / Codex probes.' },
267
289
  cardMM: { t: 'Vision & image generation', d: "Lends the harness's text-only models eyes and a brush: describe_image, pasted-image transcription and generate_image all use these settings." },
268
290
  cardCustomProv: { t: 'Custom providers', d: 'Bring your own API or local command; saved providers appear in the vision / image-gen selects above.' },
269
- modeDesc: { auto: 'auto · prefer hub', hub: 'hub · require hub', standalone: 'standalone' },
291
+ modeDesc: { auto: 'auto · prefer 3210', hub: 'hub · require 3210' },
270
292
  save: 'Save', saved: 'Saved', jobs: 'Worker jobs', empty: 'No Worker / Reviewer jobs yet.',
271
293
  adaptiveTitle: 'Adaptive model routing (experimental)', adaptiveHint: 'Off by default. Only system-derived candidates may be reordered; explicit model priorities always keep their order. Health uses only process-local success, failure, timeout, and coarse latency observations and resets on restart.',
272
294
  adaptiveEnabled: 'Enable adaptive routing', adaptiveWindow: 'Health window', adaptiveSamples: 'Minimum samples', adaptiveBoundary: 'Effective for the next workflow',
@@ -499,9 +521,13 @@ function WorkersPanel({ ctx }: { ctx: any }) {
499
521
  vision_command: string; imagegen_command: string;
500
522
  } | null>(null);
501
523
  const [modelAdd, setModelAdd] = useState<string | null>(null);
502
- const [modelCatalog, setModelCatalog] = useState<any>(null);
503
- const [modelCatalogError, setModelCatalogError] = useState('');
504
- const [modelCatalogBusy, setModelCatalogBusy] = useState(false);
524
+ const [modelCatalog, setModelCatalog] = useState<any>(null);
525
+ const [modelCatalogError, setModelCatalogError] = useState('');
526
+ const [modelCatalogBusy, setModelCatalogBusy] = useState(false);
527
+ const [providerInventory, setProviderInventory] = useState<any>(null);
528
+ const [providerHealth, setProviderHealth] = useState<any[]>([]);
529
+ const [providerInventoryError, setProviderInventoryError] = useState('');
530
+ const [providerLifecycleBusy, setProviderLifecycleBusy] = useState<string | null>(null);
505
531
  const [modelPickerTier, setModelPickerTier] = useState<'flash' | 'pro' | null>(null);
506
532
  const [modelQuery, setModelQuery] = useState('');
507
533
  const [testResult, setTestResult] = useState<{ key: string; ok?: boolean; steps?: any[]; error?: string; busy: boolean } | null>(null);
@@ -518,9 +544,13 @@ function WorkersPanel({ ctx }: { ctx: any }) {
518
544
  writeSectionState(sectionStorage(), expandedSections);
519
545
  }, [expandedSections]);
520
546
 
521
- useEffect(() => {
522
- if (modelCatalogError) setExpandedSections((current) => openSections(current, ['workflow', 'flash', 'pro']));
523
- }, [modelCatalogError]);
547
+ useEffect(() => {
548
+ if (modelCatalogError) setExpandedSections((current) => openSections(current, ['workflow', 'flash', 'pro']));
549
+ }, [modelCatalogError]);
550
+
551
+ useEffect(() => {
552
+ if (providerInventoryError) setExpandedSections((current) => openSections(current, ['harnessProviders']));
553
+ }, [providerInventoryError]);
524
554
 
525
555
  useEffect(() => {
526
556
  if (testResult && !testResult.busy && testResult.ok === false) {
@@ -579,25 +609,35 @@ function WorkersPanel({ ctx }: { ctx: any }) {
579
609
 
580
610
  const refreshAll = useCallback(async () => {
581
611
  try {
582
- const [j, s, c, pr] = await Promise.all([get('/jobs'), get('/install/status'), get('/config'), get('/presets')]);
612
+ const [j, s, c, pr, pi, ph] = await Promise.all([
613
+ get('/jobs'), get('/install/status'), get('/config'), get('/presets'),
614
+ get('/providers').catch(() => null), get('/provider-health').catch(() => null),
615
+ ]);
583
616
  if (j.ok) setJobs(j.jobs ?? []);
584
617
  if (s.ok) setStatus(s.status);
585
618
  if (c.ok) setConfig((prev: any) => prev ?? c.config);
586
- if (pr.ok) setPresetOptions([
619
+ if (pr.ok) setPresetOptions([
587
620
  { value: 'default', label: copy.presetDefault(pr.defaultId) },
588
621
  ...(pr.presets ?? []).map((x: any) => ({ value: x.id, label: x.name ?? x.id })),
589
- ]);
622
+ ]);
623
+ if (pi?.ok) setProviderInventory(pi);
624
+ if (ph?.ok) setProviderHealth(ph.health ?? []);
590
625
  } catch { /* instance restarting */ }
591
626
  }, [get]);
592
627
 
593
628
  useEffect(() => {
594
629
  if (surface !== CREW_UI_SURFACES.OFFICIAL) return undefined;
595
630
  void refreshAll();
596
- const timer = setInterval(() => { void get('/jobs').then((j) => { if (j.ok) setJobs(j.jobs ?? []); }).catch(() => {}); }, 3000);
631
+ const timer = setInterval(() => {
632
+ void Promise.all([get('/jobs'), get('/provider-health').catch(() => null)]).then(([j, health]) => {
633
+ if (j.ok) setJobs(j.jobs ?? []);
634
+ if (health?.ok) setProviderHealth(health.health ?? []);
635
+ }).catch(() => {});
636
+ }, 3000);
597
637
  return () => clearInterval(timer);
598
638
  }, [surface, refreshAll, get]);
599
639
 
600
- const refreshHarnessModels = useCallback(async () => {
640
+ const refreshHarnessModels = useCallback(async () => {
601
641
  setModelCatalogBusy(true);
602
642
  setModelCatalogError('');
603
643
  try {
@@ -609,11 +649,27 @@ function WorkersPanel({ ctx }: { ctx: any }) {
609
649
  } finally {
610
650
  setModelCatalogBusy(false);
611
651
  }
612
- }, [get, copy]);
652
+ }, [get, copy]);
653
+
654
+ const refreshProviderInventory = useCallback(async () => {
655
+ setProviderInventoryError('');
656
+ try {
657
+ const [result, health] = await Promise.all([get('/providers'), get('/provider-health').catch(() => null)]);
658
+ if (!result.ok) throw new Error(result.error ?? copy.providerNoInventory);
659
+ setProviderInventory(result);
660
+ if (health?.ok) setProviderHealth(health.health ?? []);
661
+ } catch (error: any) {
662
+ setProviderInventoryError(error?.message ?? copy.providerNoInventory);
663
+ }
664
+ }, [get, copy]);
613
665
 
614
666
  useEffect(() => {
615
667
  if (surface === CREW_UI_SURFACES.OFFICIAL) void refreshHarnessModels();
616
668
  }, [surface, refreshHarnessModels]);
669
+
670
+ useEffect(() => {
671
+ if (surface === CREW_UI_SURFACES.OFFICIAL) void refreshProviderInventory();
672
+ }, [surface, refreshProviderInventory]);
617
673
 
618
674
  const act = useCallback(async (target: string, confirmName?: string) => {
619
675
  if (confirmName && !window.confirm(copy.confirmRestore(confirmName))) return;
@@ -648,8 +704,25 @@ function WorkersPanel({ ctx }: { ctx: any }) {
648
704
  setConfig((c: any) => ({ ...c, [key]: value }));
649
705
  void applyPatch({ [key]: value });
650
706
  };
651
- /** Text/number inputs: track locally, apply on blur. */
652
- const fieldLocal = (key: string, value: any) => setConfig((c: any) => ({ ...c, [key]: value }));
707
+ /** Text/number inputs: track locally, apply on blur. */
708
+ const fieldLocal = (key: string, value: any) => setConfig((c: any) => ({ ...c, [key]: value }));
709
+ const rolePolicyField = (role: 'worker' | 'review', patch: Record<string, any>) => {
710
+ const effectivePatch = role === 'worker' && (patch.ordering === 'manual' || patch.ordering === 'health-aware')
711
+ ? { ...patch, adaptive: { ...normalizeAdaptive(config?.worker?.model_policy?.adaptive), enabled: patch.ordering === 'health-aware' } }
712
+ : patch;
713
+ setConfig((current: any) => ({
714
+ ...current,
715
+ [role]: {
716
+ ...current?.[role],
717
+ model_policy: { ...current?.[role]?.model_policy, ...effectivePatch },
718
+ },
719
+ }));
720
+ void applyPatch({ [role]: { model_policy: effectivePatch } });
721
+ };
722
+ const reviewField = (patch: Record<string, any>) => {
723
+ setConfig((current: any) => ({ ...current, review: { ...current?.review, ...patch } }));
724
+ void applyPatch({ review: patch });
725
+ };
653
726
 
654
727
  // --- custom providers & user-added models -------------------------------
655
728
  const customProviders: any[] = config?.custom_providers ?? [];
@@ -757,16 +830,61 @@ function WorkersPanel({ ctx }: { ctx: any }) {
757
830
  </div>
758
831
  );
759
832
  };
760
- const deleteProvider = (id: string) => {
833
+ const deleteProvider = (id: string) => {
761
834
  if (!config) return;
762
835
  const patch: any = { custom_providers: customProviders.filter((p) => p.id !== id) };
763
836
  if (config.vision_provider === id) { patch.vision_provider = 'claude-code'; patch.vision_model = 'haiku'; }
764
837
  if (config.imagegen_provider === id) patch.imagegen_provider = 'codex';
765
838
  setConfig((c: any) => ({ ...c, ...patch }));
766
839
  void applyPatch(patch);
767
- };
768
-
769
- /** Card: title + one-line description header, then its fields. */
840
+ };
841
+
842
+ const deleteHarnessProvider = async (record: any) => {
843
+ if (!record || record.ownership === 'harness' || record.desired_state === 'absent' || Number(record.references?.active_jobs ?? 0) > 0) {
844
+ setNotice(copy.providerDeleteBlocked);
845
+ return;
846
+ }
847
+ let replacementDefault: string | undefined;
848
+ if (record.references?.harness_default === true) {
849
+ const candidate = (providerInventory?.records ?? []).find((item: any) => item.id !== record.id
850
+ && item.desired_state !== 'absent' && item.ownership !== 'harness' && (item.models?.length ?? 0) > 0)?.id ?? '';
851
+ const entered = window.prompt(copy.providerReplacement, candidate);
852
+ if (!entered?.trim()) return;
853
+ replacementDefault = entered.trim();
854
+ }
855
+ setProviderLifecycleBusy(record.id);
856
+ setNotice(copy.working);
857
+ try {
858
+ const encoded = encodeURIComponent(record.id);
859
+ const planned = await post(`/providers/${encoded}/delete-plan`, replacementDefault ? { replacement_default: replacementDefault } : {});
860
+ if (!planned.ok || !planned.plan) throw new Error(planned.code ?? copy.providerLifecycleError);
861
+ if (!window.confirm(`${copy.providerPlanReady}\n\n${copy.providerDeleteConfirm}\n${(planned.plan.will_remove ?? []).join('\n')}`)) return;
862
+ const path = `/providers/${encoded}`;
863
+ const result = await readJson(await fetch(withLang(path), {
864
+ method: 'DELETE', headers: { 'content-type': 'application/json' },
865
+ body: JSON.stringify({ plan_id: planned.plan.plan_id, expected_revision: planned.plan.expected_revision, confirm: true, lang: locale === 'zh' ? 'zh' : 'en' }),
866
+ }), path);
867
+ if (!result.ok) throw new Error(result.code ?? result.error ?? copy.providerLifecycleError);
868
+ if (result.restart_required === true && result.result?.state === 'RESTART_PENDING') {
869
+ const restartPath = 'http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart';
870
+ const restarted = await readJson(await fetch(restartPath, {
871
+ method: 'POST', headers: { 'content-type': 'application/json' },
872
+ body: JSON.stringify({ confirm: true }),
873
+ }), restartPath);
874
+ if (!restarted.ok) throw new Error(restarted.code ?? restarted.error ?? copy.providerLifecycleError);
875
+ const verified = await post(`/providers/${encoded}/verify-delete`, { transaction_id: planned.plan.plan_id, confirm: true });
876
+ if (!verified.ok || verified.state !== 'VERIFIED') throw new Error(verified.code ?? verified.error ?? copy.providerLifecycleError);
877
+ }
878
+ setNotice(copy.saved);
879
+ await refreshProviderInventory();
880
+ } catch (error: any) {
881
+ setNotice(String(error?.message ?? error));
882
+ } finally {
883
+ setProviderLifecycleBusy(null);
884
+ }
885
+ };
886
+
887
+ /** Card: title + one-line description header, then its fields. */
770
888
  const block = (text: { t: string; d: string }, fields: any, gridded = true) => (
771
889
  <div style={S.block}>
772
890
  <div>
@@ -793,18 +911,19 @@ function WorkersPanel({ ctx }: { ctx: any }) {
793
911
  const adaptive = normalizeAdaptive(config?.worker?.model_policy?.adaptive);
794
912
  const setAdaptiveLocal = (candidate: AdaptiveConfig) => {
795
913
  const next = normalizeAdaptive(candidate);
914
+ const ordering = next.enabled ? 'health-aware' : 'manual';
796
915
  setConfig((current: any) => ({
797
916
  ...current,
798
917
  worker: {
799
918
  ...current.worker,
800
- model_policy: { ...current.worker?.model_policy, adaptive: next },
919
+ model_policy: { ...current.worker?.model_policy, adaptive: next, ordering },
801
920
  },
802
921
  }));
803
- return next;
922
+ return { next, ordering };
804
923
  };
805
924
  const saveAdaptive = (candidate: AdaptiveConfig) => {
806
- const next = setAdaptiveLocal(candidate);
807
- void applyPatch({ worker: { model_policy: { adaptive: next } } });
925
+ const { next, ordering } = setAdaptiveLocal(candidate);
926
+ void applyPatch({ worker: { model_policy: { adaptive: next, ordering } } });
808
927
  };
809
928
  const modelActivity = aggregateModelInvocations(jobs);
810
929
  const currentSurfaceResponsibilities = surfaceResponsibilities(surface);
@@ -903,11 +1022,16 @@ function WorkersPanel({ ctx }: { ctx: any }) {
903
1022
  setConfig((current: any) => ({ ...current, [key]: list, [`${tier}_model_priority_configured`]: true }));
904
1023
  void applyPatch({ [key]: list });
905
1024
  };
906
- const modelMeta = (ref: any) => {
1025
+ const modelMeta = (ref: any) => {
907
1026
  const provider = catalogProviders.find((item) => item.id === ref.provider);
908
1027
  const model = provider?.models?.find((item: any) => item.id === ref.model);
909
- return { provider, model };
910
- };
1028
+ return { provider, model };
1029
+ };
1030
+ const modelHealth = (ref: any) => providerHealth.find((entry: any) => entry.provider === ref.provider && entry.model === ref.model && entry.fresh === true) ?? null;
1031
+ const healthLabel = (health: any) => health?.state ? String(health.state).toUpperCase().replace(/-/g, ' ') : copy.healthUnprobed;
1032
+ const healthAge = (health: any) => Number.isFinite(health?.observed_at)
1033
+ ? copy.checkedAgo(Math.max(0, Math.round((Date.now() - health.observed_at) / 1000)))
1034
+ : '';
911
1035
  const filteredProviders = catalogProviders.map((provider) => ({
912
1036
  ...provider,
913
1037
  models: (provider.models ?? []).filter((model: any) => {
@@ -929,15 +1053,19 @@ function WorkersPanel({ ctx }: { ctx: any }) {
929
1053
  {priority.length === 0 && <div style={{ fontSize: 12, opacity: 0.55, marginBottom: 6 }}>{copy.noPriority}</div>}
930
1054
  {ambiguousPreferred && <div style={{ fontSize: 11.5, color: '#c98735', marginBottom: 6 }}>{copy.needsModelSelection}</div>}
931
1055
  <div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
932
- {priority.map((ref, index) => {
933
- const meta = modelMeta(ref);
934
- const warning = !meta.provider ? copy.providerUnavailable : !meta.model ? copy.notAdvertised : '';
1056
+ {priority.map((ref, index) => {
1057
+ const meta = modelMeta(ref);
1058
+ const health = modelHealth(ref);
1059
+ const warning = !meta.provider ? copy.providerUnavailable : !meta.model ? copy.notAdvertised : '';
935
1060
  return (
936
1061
  <div key={refKey(ref)} style={{ display: 'flex', gap: 7, alignItems: 'center', padding: '6px 8px', border: '1px solid rgba(128,128,128,0.2)', borderRadius: 6 }}>
937
1062
  <span style={{ opacity: 0.55, width: 18 }}>{index + 1}.</span>
938
1063
  <span style={{ flex: 1, minWidth: 0 }}>
939
1064
  <span style={{ display: 'block', fontSize: 12.5 }}>{meta.model?.name ?? ref.model}</span>
940
- <span style={{ display: 'block', fontSize: 10.5, opacity: 0.58, fontFamily: 'monospace' }}>{ref.provider} / {ref.model}</span>
1065
+ <span style={{ display: 'block', fontSize: 10.5, opacity: 0.58, fontFamily: 'monospace' }}>{ref.provider} / {ref.model}</span>
1066
+ <span style={{ display: 'block', fontSize: 10.5, color: health?.state === 'callable' ? '#3fb950' : health ? '#c98735' : 'inherit', opacity: health ? 0.9 : 0.5 }}>
1067
+ {healthLabel(health)}{healthAge(health) ? ` · ${healthAge(health)}` : ''}
1068
+ </span>
941
1069
  {warning && <span style={{ display: 'block', fontSize: 10.5, color: '#c98735' }}>{warning}</span>}
942
1070
  </span>
943
1071
  <button type="button" style={{ ...S.btn, padding: '2px 7px' }} title={copy.moveUp} disabled={index === 0}
@@ -1029,12 +1157,31 @@ function WorkersPanel({ ctx }: { ctx: any }) {
1029
1157
  <CustomSelect value={config.review_state ?? 'auto'} onChange={(v) => field('review_state', v)}
1030
1158
  options={(['auto', 'manual', 'disabled'] as const).map((s) => ({ value: s, label: copy.tierStateDesc[s] }))} /></label>
1031
1159
  </>))}
1032
- {block({ t: copy.autoReview, d: copy.autoReviewHint }, (
1033
- <label style={{ ...S.field, flexDirection: 'row' as const, alignItems: 'center', gap: 6 }}>
1034
- <input type="checkbox" checked={config.auto_review ?? !!config.pro_reviews_flash} onChange={(e) => field('auto_review', e.target.checked)} />
1035
- <span style={{ fontSize: 12.5 }}>{copy.autoReview}</span>
1036
- </label>
1037
- ), false)}
1160
+ {block({ t: copy.autoReview, d: copy.autoReviewHint }, (
1161
+ <>
1162
+ <label style={{ ...S.field, flexDirection: 'row' as const, alignItems: 'center', gap: 6 }}>
1163
+ <input type="checkbox" checked={config.auto_review ?? !!config.pro_reviews_flash} onChange={(e) => field('auto_review', e.target.checked)} />
1164
+ <span style={{ fontSize: 12.5 }}>{copy.autoReview}</span>
1165
+ </label>
1166
+ <label style={S.field}><span style={S.fieldLabel}>{copy.reviewGate}</span>
1167
+ <CustomSelect value={config.review?.gate ?? 'required'} onChange={(v) => reviewField({ gate: v })}
1168
+ options={(['required', 'optional', 'off'] as const).map((value) => ({ value, label: copy.reviewGateDesc[value] }))} /></label>
1169
+ </>
1170
+ ))}
1171
+ {block({ t: copy.modelOrdering, d: copy.orderingHint }, (<>
1172
+ {(['worker', 'review'] as const).map((role) => (
1173
+ <div key={role} style={{ display: 'contents' }}>
1174
+ <label style={S.field}><span style={S.fieldLabel}>{role === 'worker' ? copy.roleWorker : copy.roleReviewer} · {copy.modelOrdering}</span>
1175
+ <CustomSelect value={config?.[role]?.model_policy?.ordering ?? 'manual'}
1176
+ onChange={(v) => rolePolicyField(role, { ordering: v })}
1177
+ options={(['manual', 'health-aware'] as const).map((value) => ({ value, label: copy.orderingDesc[value] }))} /></label>
1178
+ <label style={S.field}><span style={S.fieldLabel}>{role === 'worker' ? copy.roleWorker : copy.roleReviewer} · {copy.healthGate}</span>
1179
+ <CustomSelect value={config?.[role]?.model_policy?.health_gate ?? 'hard-failures'}
1180
+ onChange={(v) => rolePolicyField(role, { health_gate: v })}
1181
+ options={(['hard-failures', 'off'] as const).map((value) => ({ value, label: copy.healthGateDesc[value] }))} /></label>
1182
+ </div>
1183
+ ))}
1184
+ </>))}
1038
1185
  {block({ t: copy.isolation, d: copy.isolationHint }, (<>
1039
1186
  <label style={S.field}><span style={S.fieldLabel}>{copy.isolation}</span>
1040
1187
  <CustomSelect value={config.isolation ?? 'worktree'} onChange={(v) => field('isolation', v)}
@@ -1142,7 +1289,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
1142
1289
  {block(copy.cardRuntime, (<>
1143
1290
  <label style={S.field}><span style={S.fieldLabel}>{copy.mode}</span>
1144
1291
  <CustomSelect value={config.mode} onChange={(v) => field('mode', v)}
1145
- options={(['auto', 'hub', 'standalone'] as const).map((m) => ({ value: m, label: copy.modeDesc[m] }))} /></label>
1292
+ options={(['auto', 'hub'] as const).map((m) => ({ value: m, label: copy.modeDesc[m] }))} /></label>
1146
1293
  <label style={S.field}><span style={S.fieldLabel}>{copy.timeout}</span>
1147
1294
  <input style={{ ...S.input, width: '100%', boxSizing: 'border-box' as const }} type="number" min={60} max={7200} value={config.default_timeout_seconds}
1148
1295
  onChange={(e) => fieldLocal('default_timeout_seconds', Number(e.target.value))}
@@ -1227,7 +1374,36 @@ function WorkersPanel({ ctx }: { ctx: any }) {
1227
1374
  </>))}
1228
1375
  </CollapsibleSection>
1229
1376
 
1230
- <CollapsibleSection sectionId="providers" title={copy.sectionNames.providers}
1377
+ <CollapsibleSection sectionId="harnessProviders" title={copy.sectionNames.harnessProviders}
1378
+ summary={sectionSummary(copy.providerCount(providerInventory?.records?.length ?? 0), providerInventoryError ? copy.providerLifecycleError : '')}
1379
+ expanded={!!expandedSections.harnessProviders} onToggle={() => toggleSection('harnessProviders')}>
1380
+ {providerInventoryError && <div style={{ fontSize: 12, color: '#c55' }}>{providerInventoryError}</div>}
1381
+ {!providerInventoryError && (providerInventory?.records?.length ?? 0) === 0 && (
1382
+ <div style={{ fontSize: 12, opacity: 0.55 }}>{copy.providerNoInventory}</div>
1383
+ )}
1384
+ {(providerInventory?.records ?? []).map((record: any) => {
1385
+ const blocked = record.ownership === 'harness' || record.desired_state === 'absent' || Number(record.references?.active_jobs ?? 0) > 0;
1386
+ const lifecycle = record.desired_state === 'absent' ? 'absent' : record.lifecycle?.enabled === false ? 'disabled' : record.lifecycle?.catalogued ? 'catalogued' : 'configured';
1387
+ return (
1388
+ <div key={record.id} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5, flexWrap: 'wrap' as const }}>
1389
+ <span style={{ fontWeight: 600 }}>{record.display_name || record.id}</span>
1390
+ <span style={{ ...S.chip(record.lifecycle?.enabled === true), ...S.mono }}>{record.id}</span>
1391
+ <span style={{ opacity: 0.65 }}>{copy.providerState(lifecycle)}</span>
1392
+ <span style={{ opacity: 0.65 }}>{copy.providerModels(record.models?.length ?? 0)}</span>
1393
+ {Number(record.references?.active_jobs ?? 0) > 0 && <span style={{ color: '#c98735' }}>{copy.providerJobs(record.references.active_jobs)}</span>}
1394
+ <span style={{ flex: 1 }} />
1395
+ {record.ownership !== 'harness' && record.desired_state !== 'absent' && (
1396
+ <button type="button" style={{ ...S.btn, padding: '2px 8px' }} disabled={blocked || providerLifecycleBusy !== null}
1397
+ onClick={() => { void deleteHarnessProvider(record); }}>
1398
+ {providerLifecycleBusy === record.id ? copy.working : copy.providerDeletePlan}
1399
+ </button>
1400
+ )}
1401
+ </div>
1402
+ );
1403
+ })}
1404
+ </CollapsibleSection>
1405
+
1406
+ <CollapsibleSection sectionId="providers" title={copy.sectionNames.providers}
1231
1407
  summary={sectionSummary(copy.providerCount(customProviders.length), testResult?.ok === false && copy.testFail)}
1232
1408
  expanded={!!expandedSections.providers} onToggle={() => toggleSection('providers')}>
1233
1409
  {block(copy.cardCustomProv, (<>
@@ -1,4 +1,5 @@
1
- import { buildReadinessMatrix, READINESS_REASON_CODES } from './readiness-matrix.mjs';
1
+ import { buildReadinessMatrix, READINESS_REASON_CODES } from './readiness-matrix.mjs';
2
+ import { PRODUCTION_EXECUTION_PLANE, PRODUCTION_PROFILE, PRODUCTION_LISTEN_PORT } from './runtime-identity.mjs';
2
3
 
3
4
  function warningCodes(catalogBody) {
4
5
  const hints = Array.isArray(catalogBody?.health?.hints) ? catalogBody.health.hints : [];
@@ -8,15 +9,41 @@ function warningCodes(catalogBody) {
8
9
  }
9
10
 
10
11
  function verifiedHubJob(job) {
12
+ const context = job?.execution_context;
11
13
  return job?.status === 'done'
12
14
  && job?.task_status === 'success'
13
15
  && job?.delivery_complete === true
14
- && job?.workspace_evidence_ok === true;
16
+ && job?.workspace_evidence_ok === true
17
+ && context?.execution_plane === PRODUCTION_EXECUTION_PLANE
18
+ && context?.profile === PRODUCTION_PROFILE
19
+ && Number(context?.listen_port) === PRODUCTION_LISTEN_PORT
20
+ && typeof context?.runtime_id === 'string'
21
+ && context.runtime_id.trim().length > 0;
22
+ }
23
+
24
+ function structurallyConsistentProviderInventory(body) {
25
+ if (!body || typeof body !== 'object' || body.ok === false || !Array.isArray(body.records)) return false;
26
+ return body.records.every((record) => {
27
+ if (!record || typeof record.id !== 'string' || record.id.trim() === '') return false;
28
+ if (!['present', 'absent'].includes(record.desired_state)) return false;
29
+ if (!record.lifecycle || typeof record.lifecycle !== 'object' || Array.isArray(record.lifecycle)) return false;
30
+ for (const field of ['installed', 'configured', 'enabled', 'catalogued']) {
31
+ if (typeof record.lifecycle[field] !== 'boolean') return false;
32
+ }
33
+ if (record.desired_state === 'absent' && record.lifecycle.enabled === true) return false;
34
+ return true;
35
+ });
15
36
  }
16
37
 
17
38
  export function buildHubExecutionRows(hubJobs = []) {
18
39
  const jobs = Array.isArray(hubJobs) ? hubJobs : [];
19
40
  const workerPassed = jobs.some((job) => job?.role === 'worker' && verifiedHubJob(job));
41
+ const workerPrimaryPassed = jobs.some((job) => job?.role === 'worker'
42
+ && verifiedHubJob(job)
43
+ && Number(job?.attempt ?? job?.selection_trace?.logical_attempt ?? 0) === 0);
44
+ const workerEscalationPassed = jobs.some((job) => job?.role === 'worker'
45
+ && verifiedHubJob(job)
46
+ && (Number(job?.attempt ?? job?.selection_trace?.logical_attempt ?? 0) > 0));
20
47
  const reviewerPassed = jobs.some((job) => job?.role === 'reviewer'
21
48
  && verifiedHubJob(job)
22
49
  && job?.review_verdict === 'approve');
@@ -24,6 +51,16 @@ export function buildHubExecutionRows(hubJobs = []) {
24
51
  workerPassed
25
52
  ? { id: 'model_execution', status: 'PASS', reason_code: 'REAL_EXECUTION_PASSED', evidence_source: 'hub-jobs' }
26
53
  : { id: 'model_execution', status: 'NOT_RUN', reason_code: 'NO_EXECUTION_EVIDENCE', evidence_source: 'none' },
54
+ workerPrimaryPassed
55
+ ? { id: 'worker_primary_callable', status: 'PASS', reason_code: 'WORKER_PRIMARY_CALLABLE', evidence_source: 'hub-jobs' }
56
+ : { id: 'worker_primary_callable', status: 'NOT_RUN', reason_code: 'NO_EXECUTION_EVIDENCE', evidence_source: 'none' },
57
+ workerEscalationPassed
58
+ ? { id: 'worker_escalation_callable', status: 'PASS', reason_code: 'WORKER_ESCALATION_CALLABLE', evidence_source: 'hub-jobs' }
59
+ : { id: 'worker_escalation_callable', status: 'NOT_RUN', reason_code: 'NO_EXECUTION_EVIDENCE', evidence_source: 'none' },
60
+ reviewerPassed
61
+ ? { id: 'reviewer_primary_callable', status: 'PASS', reason_code: 'REVIEWER_PRIMARY_CALLABLE', evidence_source: 'hub-jobs' }
62
+ : { id: 'reviewer_primary_callable', status: 'NOT_RUN', reason_code: 'NO_EXECUTION_EVIDENCE', evidence_source: 'none' },
63
+ { id: 'provider_lifecycle_consistent', status: 'NOT_RUN', reason_code: 'NO_EXECUTION_EVIDENCE', evidence_source: 'none' },
27
64
  reviewerPassed
28
65
  ? { id: 'reviewer_pipeline', status: 'PASS', reason_code: 'REAL_REVIEW_PASSED', evidence_source: 'hub-jobs' }
29
66
  : { id: 'reviewer_pipeline', status: 'NOT_RUN', reason_code: 'NO_EXECUTION_EVIDENCE', evidence_source: 'none' },
@@ -41,6 +78,8 @@ export function buildConfigReadinessMatrix({
41
78
  workerProviderMode = null,
42
79
  providerCatalogChecked = false,
43
80
  providerCatalogBody = null,
81
+ providerInventoryChecked = false,
82
+ providerInventoryBody = null,
44
83
  hubJobsChecked = false,
45
84
  hubJobsBody = null,
46
85
  } = {}) {
@@ -66,19 +105,31 @@ export function buildConfigReadinessMatrix({
66
105
  };
67
106
  }
68
107
  }
69
- if (
108
+ if (
70
109
  workerProviderMode !== 'deepseek-official'
71
110
  && hubCompatibility?.compatible === true
72
111
  && providerCatalogChecked
73
112
  && catalogResponseOk
74
113
  && warnings.length > 0
75
- ) {
114
+ ) {
76
115
  evidence.provider_catalog = {
77
116
  status: 'FAIL',
78
117
  reason_code: READINESS_REASON_CODES.PROVIDER_CATALOG_HEALTH_WARNING,
79
118
  evidence_source: 'harness-catalog',
80
119
  };
81
- }
120
+ }
121
+
122
+ if (providerInventoryChecked) {
123
+ evidence.provider_lifecycle_consistent = structurallyConsistentProviderInventory(providerInventoryBody)
124
+ ? { status: 'PASS', reason_code: READINESS_REASON_CODES.PROVIDER_LIFECYCLE_CONSISTENT, evidence_source: 'provider-inventory' }
125
+ : {
126
+ status: 'FAIL',
127
+ reason_code: providerInventoryBody?.ok === false
128
+ ? READINESS_REASON_CODES.PROVIDER_INVENTORY_UNAVAILABLE
129
+ : READINESS_REASON_CODES.PROVIDER_LIFECYCLE_INCONSISTENT,
130
+ evidence_source: 'provider-inventory',
131
+ };
132
+ }
82
133
 
83
134
  const matrix = buildReadinessMatrix({
84
135
  platform,
@@ -37,10 +37,11 @@ function readinessFromRow(entry, { pass = 'READY', notRun = 'DEGRADED' } = {}) {
37
37
  export function buildExtensionContract({ config = {}, readinessMatrix = {}, workspace = null, profiles = null, runtime = null } = {}) {
38
38
  const workerEnabled = config.subagents_enabled !== false && config.worker_state !== 'disabled';
39
39
  const reviewerEnabled = config.subagents_enabled !== false && config.review_state !== 'disabled';
40
- const realModelEvidence = ['model_execution', 'deepseek_flash', 'deepseek_pro', 'opencode_go_mimo_qwen']
40
+ const realModelEvidence = ['worker_primary_callable', 'model_execution', 'worker_escalation_callable', 'deepseek_flash', 'deepseek_pro']
41
41
  .map((id) => row(readinessMatrix, id))
42
42
  .find((entry) => entry?.status === 'PASS');
43
- const catalogEvidence = row(readinessMatrix, 'provider_catalog');
43
+ const catalogEvidence = row(readinessMatrix, 'provider_catalog');
44
+ const lifecycleEvidence = row(readinessMatrix, 'provider_lifecycle_consistent');
44
45
  const modelReadiness = catalogEvidence?.status === 'FAIL'
45
46
  ? readinessFromRow(catalogEvidence)
46
47
  : realModelEvidence
@@ -48,9 +49,12 @@ export function buildExtensionContract({ config = {}, readinessMatrix = {}, work
48
49
  : catalogEvidence?.status === 'PASS' || catalogEvidence?.status === 'SKIP'
49
50
  ? component('DEGRADED', 'MODEL_CATALOG_ONLY')
50
51
  : component('UNAVAILABLE', catalogEvidence?.reason_code ?? 'NO_EVIDENCE');
51
- const components = {
52
- harness: readinessFromRow(row(readinessMatrix, 'hub_compatibility')),
53
- model: modelReadiness,
52
+ const components = {
53
+ harness: readinessFromRow(row(readinessMatrix, 'hub_compatibility')),
54
+ provider_lifecycle: lifecycleEvidence
55
+ ? readinessFromRow(lifecycleEvidence, { notRun: 'DEGRADED' })
56
+ : component('DEGRADED', 'PROVIDER_LIFECYCLE_NOT_CHECKED'),
57
+ model: modelReadiness,
54
58
  workspace: workspaceComponent(workspace),
55
59
  reviewer: reviewerEnabled
56
60
  ? readinessFromRow(row(readinessMatrix, 'reviewer_pipeline'))