@ran-sh/dsh-crew 0.5.5 → 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 +254 -87
  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
@@ -38,6 +38,7 @@ const SETTINGS_SECTION_IDS = Object.freeze([
38
38
  "adaptive",
39
39
  "runtime",
40
40
  "multimodal",
41
+ "harnessProviders",
41
42
  "providers",
42
43
  "jobs"
43
44
  ]);
@@ -375,7 +376,8 @@ const COPY = {
375
376
  adaptive: "自适应路由",
376
377
  runtime: "运行 / 生效边界",
377
378
  multimodal: "视觉与生图",
378
- providers: "自定义 Provider",
379
+ harnessProviders: "Harness Providers",
380
+ providers: "多模态适配器",
379
381
  jobs: "任务状态"
380
382
  },
381
383
  openHarness: "打开 3210 Crew Harness →",
@@ -444,6 +446,8 @@ const COPY = {
444
446
  notAdvertised: "当前未列出",
445
447
  noPriority: "未配置优先项",
446
448
  modelPoolSummary: (p, m) => `Providers: ${p} · Models: ${m}`,
449
+ healthUnprobed: "UNPROBED",
450
+ checkedAgo: (s) => `${s} 秒前检查`,
447
451
  partialCatalog: "部分 Provider 读取失败",
448
452
  catalogFailed: "无法读取 Harness 模型目录",
449
453
  removePriority: "移除",
@@ -491,6 +495,19 @@ const COPY = {
491
495
  visionProvider: "视觉 provider",
492
496
  visionModel: "视觉模型",
493
497
  imagegenProvider: "生图 provider",
498
+ harnessProviders: "Harness Providers",
499
+ harnessProviderHint: "3210 Crew Harness 的真实 Provider 注册与生命周期;不是视觉 / 生图适配器。",
500
+ providerState: (state) => `状态:${state}`,
501
+ providerModels: (count) => `${count} 个模型`,
502
+ providerJobs: (count) => `${count} 个运行任务`,
503
+ providerDeletePlan: "生成删除计划",
504
+ providerDelete: "确认删除",
505
+ providerPlanReady: "计划已生成;再次确认后才会修改 3210 配置。",
506
+ providerReplacement: "替换 Harness 默认 Provider(输入 Provider id)",
507
+ providerDeleteConfirm: "确认按计划删除这个 Harness Provider?会清理路由引用并重启 3210。",
508
+ providerDeleteBlocked: "当前 Provider 不能删除(内置、在用、已删除或缺少可用替换)。",
509
+ providerLifecycleError: "Provider 生命周期操作失败",
510
+ providerNoInventory: "暂时无法读取 3210 Provider inventory。",
494
511
  customProviders: "自定义 Provider",
495
512
  addProvider: "+ 添加 Provider",
496
513
  noCustomProviders: "暂无。添加后会出现在上方的视觉 / 生图 provider 选择里。",
@@ -544,6 +561,23 @@ const COPY = {
544
561
  roleStateHint: "auto:orchestrator 可自动派发;manual:仅显式点名;disabled:禁用。未显式设置时由协作模式推导。",
545
562
  autoReview: "自动复审",
546
563
  autoReviewHint: "worker 成功后自动运行一次 reviewer(独立模型策略,只读)",
564
+ modelOrdering: "候选排序",
565
+ orderingDesc: {
566
+ manual: "手动顺序",
567
+ "health-aware": "健康感知"
568
+ },
569
+ orderingHint: "手动 priority 始终保持顺序;健康感知仅重排未显式锁定的自动候选。",
570
+ healthGate: "健康门",
571
+ healthGateDesc: {
572
+ "hard-failures": "跳过明确不可调用",
573
+ off: "关闭健康门"
574
+ },
575
+ reviewGate: "Reviewer 验收门",
576
+ reviewGateDesc: {
577
+ required: "required · 未批准则失败",
578
+ optional: "optional · 记录但不阻断",
579
+ off: "off · 不自动复审"
580
+ },
547
581
  isolation: "工作区隔离",
548
582
  isolationHint: "worktree 为默认;非 Git 仓库时需显式 shared,否则任务以 NOT_GIT_REPOSITORY 失败。",
549
583
  isolationDesc: {
@@ -570,9 +604,8 @@ const COPY = {
570
604
  d: "接入自己的 API 或本地命令;保存后会出现在上面的视觉 / 生图 provider 选择里。"
571
605
  },
572
606
  modeDesc: {
573
- auto: "auto · 优先 hub",
574
- hub: "hub · 必须 hub",
575
- standalone: "standalone · 独立进程"
607
+ auto: "auto · 优先 3210",
608
+ hub: "hub · 必须 3210"
576
609
  },
577
610
  save: "保存",
578
611
  saved: "已保存",
@@ -654,7 +687,8 @@ const COPY = {
654
687
  adaptive: "Adaptive routing",
655
688
  runtime: "Runtime / activation boundaries",
656
689
  multimodal: "Vision & image generation",
657
- providers: "Custom providers",
690
+ harnessProviders: "Harness Providers",
691
+ providers: "Multimodal adapters",
658
692
  jobs: "Task status"
659
693
  },
660
694
  openHarness: "Open 3210 Crew Harness →",
@@ -723,6 +757,8 @@ const COPY = {
723
757
  notAdvertised: "Currently not advertised",
724
758
  noPriority: "No priority models configured",
725
759
  modelPoolSummary: (p, m) => `Providers: ${p} · Models: ${m}`,
760
+ healthUnprobed: "UNPROBED",
761
+ checkedAgo: (s) => `checked ${s}s ago`,
726
762
  partialCatalog: "Some providers failed to load",
727
763
  catalogFailed: "Unable to read Harness model catalog",
728
764
  removePriority: "Remove",
@@ -770,6 +806,19 @@ const COPY = {
770
806
  visionProvider: "Vision provider",
771
807
  visionModel: "Vision model",
772
808
  imagegenProvider: "Image-gen provider",
809
+ harnessProviders: "Harness Providers",
810
+ harnessProviderHint: "Live Provider registration and lifecycle in the 3210 Crew Harness; separate from vision / image-gen adapters.",
811
+ providerState: (state) => `State: ${state}`,
812
+ providerModels: (count) => `${count} models`,
813
+ providerJobs: (count) => `${count} running jobs`,
814
+ providerDeletePlan: "Plan deletion",
815
+ providerDelete: "Confirm delete",
816
+ providerPlanReady: "Plan ready; a second confirmation is required before changing 3210 config.",
817
+ providerReplacement: "Replacement Harness Default provider id",
818
+ providerDeleteConfirm: "Delete this Harness Provider per the plan? Routing refs will be scrubbed and 3210 restarted.",
819
+ providerDeleteBlocked: "This Provider cannot be deleted (built-in, in use, already absent, or missing a valid replacement).",
820
+ providerLifecycleError: "Provider lifecycle operation failed",
821
+ providerNoInventory: "3210 Provider inventory is temporarily unavailable.",
773
822
  customProviders: "Custom providers",
774
823
  addProvider: "+ Add provider",
775
824
  noCustomProviders: "None yet. Added providers appear in the vision / image-gen selects above.",
@@ -823,6 +872,23 @@ const COPY = {
823
872
  roleStateHint: "auto = the orchestrator may dispatch automatically; manual = only when explicitly named; disabled = off. When unset, derived from the collaboration mode.",
824
873
  autoReview: "Automatic review",
825
874
  autoReviewHint: "Run one reviewer pass (independent model policy, read-only) after a successful worker run",
875
+ modelOrdering: "Candidate ordering",
876
+ orderingDesc: {
877
+ manual: "Manual order",
878
+ "health-aware": "Health-aware"
879
+ },
880
+ orderingHint: "Explicit priority always keeps its order; health-aware mode only reorders automatic, unlocked candidates.",
881
+ healthGate: "Health gate",
882
+ healthGateDesc: {
883
+ "hard-failures": "Skip known hard failures",
884
+ off: "Health gate off"
885
+ },
886
+ reviewGate: "Reviewer acceptance gate",
887
+ reviewGateDesc: {
888
+ required: "required · fail unless approved",
889
+ optional: "optional · record without blocking",
890
+ off: "off · no automatic review"
891
+ },
826
892
  isolation: "Workspace isolation",
827
893
  isolationHint: "worktree is the default; non-git workspaces require explicit shared, otherwise jobs fail with NOT_GIT_REPOSITORY.",
828
894
  isolationDesc: {
@@ -849,9 +915,8 @@ const COPY = {
849
915
  d: "Bring your own API or local command; saved providers appear in the vision / image-gen selects above."
850
916
  },
851
917
  modeDesc: {
852
- auto: "auto · prefer hub",
853
- hub: "hub · require hub",
854
- standalone: "standalone"
918
+ auto: "auto · prefer 3210",
919
+ hub: "hub · require 3210"
855
920
  },
856
921
  save: "Save",
857
922
  saved: "Saved",
@@ -1339,6 +1404,10 @@ function WorkersPanel({ ctx }) {
1339
1404
  const [modelCatalog, setModelCatalog] = (0, react.useState)(null);
1340
1405
  const [modelCatalogError, setModelCatalogError] = (0, react.useState)("");
1341
1406
  const [modelCatalogBusy, setModelCatalogBusy] = (0, react.useState)(false);
1407
+ const [providerInventory, setProviderInventory] = (0, react.useState)(null);
1408
+ const [providerHealth, setProviderHealth] = (0, react.useState)([]);
1409
+ const [providerInventoryError, setProviderInventoryError] = (0, react.useState)("");
1410
+ const [providerLifecycleBusy, setProviderLifecycleBusy] = (0, react.useState)(null);
1342
1411
  const [modelPickerTier, setModelPickerTier] = (0, react.useState)(null);
1343
1412
  const [modelQuery, setModelQuery] = (0, react.useState)("");
1344
1413
  const [testResult, setTestResult] = (0, react.useState)(null);
@@ -1362,6 +1431,9 @@ function WorkersPanel({ ctx }) {
1362
1431
  "pro"
1363
1432
  ]));
1364
1433
  }, [modelCatalogError]);
1434
+ (0, react.useEffect)(() => {
1435
+ if (providerInventoryError) setExpandedSections((current) => openSections(current, ["harnessProviders"]));
1436
+ }, [providerInventoryError]);
1365
1437
  (0, react.useEffect)(() => {
1366
1438
  if (testResult && !testResult.busy && testResult.ok === false) setExpandedSections((current) => openSections(current, ["providers"]));
1367
1439
  }, [testResult]);
@@ -1422,11 +1494,13 @@ function WorkersPanel({ ctx }) {
1422
1494
  }, [detectSurface]);
1423
1495
  const refreshAll = (0, react.useCallback)(async () => {
1424
1496
  try {
1425
- const [j, s, c, pr] = await Promise.all([
1497
+ const [j, s, c, pr, pi, ph] = await Promise.all([
1426
1498
  get("/jobs"),
1427
1499
  get("/install/status"),
1428
1500
  get("/config"),
1429
- get("/presets")
1501
+ get("/presets"),
1502
+ get("/providers").catch(() => null),
1503
+ get("/provider-health").catch(() => null)
1430
1504
  ]);
1431
1505
  if (j.ok) setJobs(j.jobs ?? []);
1432
1506
  if (s.ok) setStatus(s.status);
@@ -1438,14 +1512,17 @@ function WorkersPanel({ ctx }) {
1438
1512
  value: x.id,
1439
1513
  label: x.name ?? x.id
1440
1514
  }))]);
1515
+ if (pi?.ok) setProviderInventory(pi);
1516
+ if (ph?.ok) setProviderHealth(ph.health ?? []);
1441
1517
  } catch {}
1442
1518
  }, [get]);
1443
1519
  (0, react.useEffect)(() => {
1444
1520
  if (surface !== CREW_UI_SURFACES.OFFICIAL) return void 0;
1445
1521
  refreshAll();
1446
1522
  const timer = setInterval(() => {
1447
- get("/jobs").then((j) => {
1523
+ Promise.all([get("/jobs"), get("/provider-health").catch(() => null)]).then(([j, health]) => {
1448
1524
  if (j.ok) setJobs(j.jobs ?? []);
1525
+ if (health?.ok) setProviderHealth(health.health ?? []);
1449
1526
  }).catch(() => {});
1450
1527
  }, 3e3);
1451
1528
  return () => clearInterval(timer);
@@ -1467,9 +1544,23 @@ function WorkersPanel({ ctx }) {
1467
1544
  setModelCatalogBusy(false);
1468
1545
  }
1469
1546
  }, [get, copy]);
1547
+ const refreshProviderInventory = (0, react.useCallback)(async () => {
1548
+ setProviderInventoryError("");
1549
+ try {
1550
+ const [result, health] = await Promise.all([get("/providers"), get("/provider-health").catch(() => null)]);
1551
+ if (!result.ok) throw new Error(result.error ?? copy.providerNoInventory);
1552
+ setProviderInventory(result);
1553
+ if (health?.ok) setProviderHealth(health.health ?? []);
1554
+ } catch (error) {
1555
+ setProviderInventoryError(error?.message ?? copy.providerNoInventory);
1556
+ }
1557
+ }, [get, copy]);
1470
1558
  (0, react.useEffect)(() => {
1471
1559
  if (surface === CREW_UI_SURFACES.OFFICIAL) refreshHarnessModels();
1472
1560
  }, [surface, refreshHarnessModels]);
1561
+ (0, react.useEffect)(() => {
1562
+ if (surface === CREW_UI_SURFACES.OFFICIAL) refreshProviderInventory();
1563
+ }, [surface, refreshProviderInventory]);
1473
1564
  const act = (0, react.useCallback)(async (target, confirmName) => {
1474
1565
  if (confirmName && !window.confirm(copy.confirmRestore(confirmName))) return;
1475
1566
  setBusy(true);
@@ -1524,6 +1615,36 @@ function WorkersPanel({ ctx }) {
1524
1615
  ...c,
1525
1616
  [key]: value
1526
1617
  }));
1618
+ const rolePolicyField = (role, patch) => {
1619
+ const effectivePatch = role === "worker" && (patch.ordering === "manual" || patch.ordering === "health-aware") ? {
1620
+ ...patch,
1621
+ adaptive: {
1622
+ ...normalizeAdaptive(config?.worker?.model_policy?.adaptive),
1623
+ enabled: patch.ordering === "health-aware"
1624
+ }
1625
+ } : patch;
1626
+ setConfig((current) => ({
1627
+ ...current,
1628
+ [role]: {
1629
+ ...current?.[role],
1630
+ model_policy: {
1631
+ ...current?.[role]?.model_policy,
1632
+ ...effectivePatch
1633
+ }
1634
+ }
1635
+ }));
1636
+ applyPatch({ [role]: { model_policy: effectivePatch } });
1637
+ };
1638
+ const reviewField = (patch) => {
1639
+ setConfig((current) => ({
1640
+ ...current,
1641
+ review: {
1642
+ ...current?.review,
1643
+ ...patch
1644
+ }
1645
+ }));
1646
+ applyPatch({ review: patch });
1647
+ };
1527
1648
  const customProviders = config?.custom_providers ?? [];
1528
1649
  const extraModels = config?.extra_models ?? {};
1529
1650
  const RESERVED_IDS = [
@@ -1748,6 +1869,59 @@ function WorkersPanel({ ctx }) {
1748
1869
  }));
1749
1870
  applyPatch(patch);
1750
1871
  };
1872
+ const deleteHarnessProvider = async (record) => {
1873
+ if (!record || record.ownership === "harness" || record.desired_state === "absent" || Number(record.references?.active_jobs ?? 0) > 0) {
1874
+ setNotice(copy.providerDeleteBlocked);
1875
+ return;
1876
+ }
1877
+ let replacementDefault;
1878
+ if (record.references?.harness_default === true) {
1879
+ const candidate = (providerInventory?.records ?? []).find((item) => item.id !== record.id && item.desired_state !== "absent" && item.ownership !== "harness" && (item.models?.length ?? 0) > 0)?.id ?? "";
1880
+ const entered = window.prompt(copy.providerReplacement, candidate);
1881
+ if (!entered?.trim()) return;
1882
+ replacementDefault = entered.trim();
1883
+ }
1884
+ setProviderLifecycleBusy(record.id);
1885
+ setNotice(copy.working);
1886
+ try {
1887
+ const encoded = encodeURIComponent(record.id);
1888
+ const planned = await post(`/providers/${encoded}/delete-plan`, replacementDefault ? { replacement_default: replacementDefault } : {});
1889
+ if (!planned.ok || !planned.plan) throw new Error(planned.code ?? copy.providerLifecycleError);
1890
+ if (!window.confirm(`${copy.providerPlanReady}\n\n${copy.providerDeleteConfirm}\n${(planned.plan.will_remove ?? []).join("\n")}`)) return;
1891
+ const path = `/providers/${encoded}`;
1892
+ const result = await readJson(await fetch(withLang(path), {
1893
+ method: "DELETE",
1894
+ headers: { "content-type": "application/json" },
1895
+ body: JSON.stringify({
1896
+ plan_id: planned.plan.plan_id,
1897
+ expected_revision: planned.plan.expected_revision,
1898
+ confirm: true,
1899
+ lang: locale === "zh" ? "zh" : "en"
1900
+ })
1901
+ }), path);
1902
+ if (!result.ok) throw new Error(result.code ?? result.error ?? copy.providerLifecycleError);
1903
+ if (result.restart_required === true && result.result?.state === "RESTART_PENDING") {
1904
+ const restartPath = "http://127.0.0.1:3080/_dsh/dsh-crew/supervisor/restart";
1905
+ const restarted = await readJson(await fetch(restartPath, {
1906
+ method: "POST",
1907
+ headers: { "content-type": "application/json" },
1908
+ body: JSON.stringify({ confirm: true })
1909
+ }), restartPath);
1910
+ if (!restarted.ok) throw new Error(restarted.code ?? restarted.error ?? copy.providerLifecycleError);
1911
+ const verified = await post(`/providers/${encoded}/verify-delete`, {
1912
+ transaction_id: planned.plan.plan_id,
1913
+ confirm: true
1914
+ });
1915
+ if (!verified.ok || verified.state !== "VERIFIED") throw new Error(verified.code ?? verified.error ?? copy.providerLifecycleError);
1916
+ }
1917
+ setNotice(copy.saved);
1918
+ await refreshProviderInventory();
1919
+ } catch (error) {
1920
+ setNotice(String(error?.message ?? error));
1921
+ } finally {
1922
+ setProviderLifecycleBusy(null);
1923
+ }
1924
+ };
1751
1925
  /** Card: title + one-line description header, then its fields. */
1752
1926
  const block = (text, fields, gridded = true) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1753
1927
  style: S.block,
@@ -1818,21 +1992,29 @@ function WorkersPanel({ ctx }) {
1818
1992
  const adaptive = normalizeAdaptive(config?.worker?.model_policy?.adaptive);
1819
1993
  const setAdaptiveLocal = (candidate) => {
1820
1994
  const next = normalizeAdaptive(candidate);
1995
+ const ordering = next.enabled ? "health-aware" : "manual";
1821
1996
  setConfig((current) => ({
1822
1997
  ...current,
1823
1998
  worker: {
1824
1999
  ...current.worker,
1825
2000
  model_policy: {
1826
2001
  ...current.worker?.model_policy,
1827
- adaptive: next
2002
+ adaptive: next,
2003
+ ordering
1828
2004
  }
1829
2005
  }
1830
2006
  }));
1831
- return next;
2007
+ return {
2008
+ next,
2009
+ ordering
2010
+ };
1832
2011
  };
1833
2012
  const saveAdaptive = (candidate) => {
1834
- const next = setAdaptiveLocal(candidate);
1835
- applyPatch({ worker: { model_policy: { adaptive: next } } });
2013
+ const { next, ordering } = setAdaptiveLocal(candidate);
2014
+ applyPatch({ worker: { model_policy: {
2015
+ adaptive: next,
2016
+ ordering
2017
+ } } });
1836
2018
  };
1837
2019
  const modelActivity = aggregateModelInvocations(jobs);
1838
2020
  const currentSurfaceResponsibilities = surfaceResponsibilities(surface);
@@ -2131,6 +2313,9 @@ function WorkersPanel({ ctx }) {
2131
2313
  model: provider?.models?.find((item) => item.id === ref.model)
2132
2314
  };
2133
2315
  };
2316
+ const modelHealth = (ref) => providerHealth.find((entry) => entry.provider === ref.provider && entry.model === ref.model && entry.fresh === true) ?? null;
2317
+ const healthLabel = (health) => health?.state ? String(health.state).toUpperCase().replace(/-/g, " ") : copy.healthUnprobed;
2318
+ const healthAge = (health) => Number.isFinite(health?.observed_at) ? copy.checkedAgo(Math.max(0, Math.round((Date.now() - health.observed_at) / 1e3))) : "";
2134
2319
  const filteredProviders = catalogProviders.map((provider) => ({
2135
2320
  ...provider,
2136
2321
  models: (provider.models ?? []).filter((model) => {
@@ -2187,6 +2372,7 @@ function WorkersPanel({ ctx }) {
2187
2372
  },
2188
2373
  children: priority.map((ref, index) => {
2189
2374
  const meta = modelMeta(ref);
2375
+ const health = modelHealth(ref);
2190
2376
  const warning = !meta.provider ? copy.providerUnavailable : !meta.model ? copy.notAdvertised : "";
2191
2377
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2192
2378
  style: {
@@ -2231,6 +2417,15 @@ function WorkersPanel({ ctx }) {
2231
2417
  ref.model
2232
2418
  ]
2233
2419
  }),
2420
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2421
+ style: {
2422
+ display: "block",
2423
+ fontSize: 10.5,
2424
+ color: health?.state === "callable" ? "#3fb950" : health ? "#c98735" : "inherit",
2425
+ opacity: health ? .9 : .5
2426
+ },
2427
+ children: [healthLabel(health), healthAge(health) ? ` · ${healthAge(health)}` : ""]
2428
+ }),
2234
2429
  warning && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2235
2430
  style: {
2236
2431
  display: "block",
@@ -2536,7 +2731,7 @@ function WorkersPanel({ ctx }) {
2536
2731
  block({
2537
2732
  t: copy.autoReview,
2538
2733
  d: copy.autoReviewHint
2539
- }, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2734
+ }, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2540
2735
  style: {
2541
2736
  ...S.field,
2542
2737
  flexDirection: "row",
@@ -2551,7 +2746,65 @@ function WorkersPanel({ ctx }) {
2551
2746
  style: { fontSize: 12.5 },
2552
2747
  children: copy.autoReview
2553
2748
  })]
2554
- }), false),
2749
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2750
+ style: S.field,
2751
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2752
+ style: S.fieldLabel,
2753
+ children: copy.reviewGate
2754
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
2755
+ value: config.review?.gate ?? "required",
2756
+ onChange: (v) => reviewField({ gate: v }),
2757
+ options: [
2758
+ "required",
2759
+ "optional",
2760
+ "off"
2761
+ ].map((value) => ({
2762
+ value,
2763
+ label: copy.reviewGateDesc[value]
2764
+ }))
2765
+ })]
2766
+ })] })),
2767
+ block({
2768
+ t: copy.modelOrdering,
2769
+ d: copy.orderingHint
2770
+ }, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: ["worker", "review"].map((role) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2771
+ style: { display: "contents" },
2772
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2773
+ style: S.field,
2774
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2775
+ style: S.fieldLabel,
2776
+ children: [
2777
+ role === "worker" ? copy.roleWorker : copy.roleReviewer,
2778
+ " · ",
2779
+ copy.modelOrdering
2780
+ ]
2781
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
2782
+ value: config?.[role]?.model_policy?.ordering ?? "manual",
2783
+ onChange: (v) => rolePolicyField(role, { ordering: v }),
2784
+ options: ["manual", "health-aware"].map((value) => ({
2785
+ value,
2786
+ label: copy.orderingDesc[value]
2787
+ }))
2788
+ })]
2789
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2790
+ style: S.field,
2791
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2792
+ style: S.fieldLabel,
2793
+ children: [
2794
+ role === "worker" ? copy.roleWorker : copy.roleReviewer,
2795
+ " · ",
2796
+ copy.healthGate
2797
+ ]
2798
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
2799
+ value: config?.[role]?.model_policy?.health_gate ?? "hard-failures",
2800
+ onChange: (v) => rolePolicyField(role, { health_gate: v }),
2801
+ options: ["hard-failures", "off"].map((value) => ({
2802
+ value,
2803
+ label: copy.healthGateDesc[value]
2804
+ }))
2805
+ })]
2806
+ })]
2807
+ }, role)) })),
2555
2808
  block({
2556
2809
  t: copy.isolation,
2557
2810
  d: copy.isolationHint
@@ -2948,11 +3201,7 @@ function WorkersPanel({ ctx }) {
2948
3201
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
2949
3202
  value: config.mode,
2950
3203
  onChange: (v) => field("mode", v),
2951
- options: [
2952
- "auto",
2953
- "hub",
2954
- "standalone"
2955
- ].map((m) => ({
3204
+ options: ["auto", "hub"].map((m) => ({
2956
3205
  value: m,
2957
3206
  label: copy.modeDesc[m]
2958
3207
  }))
@@ -3190,6 +3439,80 @@ function WorkersPanel({ ctx }) {
3190
3439
  })
3191
3440
  ] }))
3192
3441
  }),
3442
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CollapsibleSection, {
3443
+ sectionId: "harnessProviders",
3444
+ title: copy.sectionNames.harnessProviders,
3445
+ summary: sectionSummary(copy.providerCount(providerInventory?.records?.length ?? 0), providerInventoryError ? copy.providerLifecycleError : ""),
3446
+ expanded: !!expandedSections.harnessProviders,
3447
+ onToggle: () => toggleSection("harnessProviders"),
3448
+ children: [
3449
+ providerInventoryError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3450
+ style: {
3451
+ fontSize: 12,
3452
+ color: "#c55"
3453
+ },
3454
+ children: providerInventoryError
3455
+ }),
3456
+ !providerInventoryError && (providerInventory?.records?.length ?? 0) === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3457
+ style: {
3458
+ fontSize: 12,
3459
+ opacity: .55
3460
+ },
3461
+ children: copy.providerNoInventory
3462
+ }),
3463
+ (providerInventory?.records ?? []).map((record) => {
3464
+ const blocked = record.ownership === "harness" || record.desired_state === "absent" || Number(record.references?.active_jobs ?? 0) > 0;
3465
+ const lifecycle = record.desired_state === "absent" ? "absent" : record.lifecycle?.enabled === false ? "disabled" : record.lifecycle?.catalogued ? "catalogued" : "configured";
3466
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3467
+ style: {
3468
+ display: "flex",
3469
+ alignItems: "center",
3470
+ gap: 8,
3471
+ fontSize: 12.5,
3472
+ flexWrap: "wrap"
3473
+ },
3474
+ children: [
3475
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3476
+ style: { fontWeight: 600 },
3477
+ children: record.display_name || record.id
3478
+ }),
3479
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3480
+ style: {
3481
+ ...S.chip(record.lifecycle?.enabled === true),
3482
+ ...S.mono
3483
+ },
3484
+ children: record.id
3485
+ }),
3486
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3487
+ style: { opacity: .65 },
3488
+ children: copy.providerState(lifecycle)
3489
+ }),
3490
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3491
+ style: { opacity: .65 },
3492
+ children: copy.providerModels(record.models?.length ?? 0)
3493
+ }),
3494
+ Number(record.references?.active_jobs ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3495
+ style: { color: "#c98735" },
3496
+ children: copy.providerJobs(record.references.active_jobs)
3497
+ }),
3498
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { flex: 1 } }),
3499
+ record.ownership !== "harness" && record.desired_state !== "absent" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3500
+ type: "button",
3501
+ style: {
3502
+ ...S.btn,
3503
+ padding: "2px 8px"
3504
+ },
3505
+ disabled: blocked || providerLifecycleBusy !== null,
3506
+ onClick: () => {
3507
+ deleteHarnessProvider(record);
3508
+ },
3509
+ children: providerLifecycleBusy === record.id ? copy.working : copy.providerDeletePlan
3510
+ })
3511
+ ]
3512
+ }, record.id);
3513
+ })
3514
+ ]
3515
+ }),
3193
3516
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsibleSection, {
3194
3517
  sectionId: "providers",
3195
3518
  title: copy.sectionNames.providers,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ran-sh/dsh-crew",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "type": "module",
5
5
  "main": "./src/hub/entry.mjs",
6
6
  "bin": {
@@ -8,8 +8,9 @@ export const SETTINGS_SECTION_IDS = Object.freeze([
8
8
  'dispatch',
9
9
  'adaptive',
10
10
  'runtime',
11
- 'multimodal',
12
- 'providers',
11
+ 'multimodal',
12
+ 'harnessProviders',
13
+ 'providers',
13
14
  'jobs',
14
15
  ]);
15
16