@ran-sh/dsh-crew 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +16 -16
- package/.claude-plugin/plugin.json +14 -14
- package/LICENSE +21 -21
- package/README.de.md +359 -359
- package/README.es.md +359 -359
- package/README.fr.md +359 -359
- package/README.hi.md +359 -359
- package/README.id.md +359 -359
- package/README.ja.md +359 -359
- package/README.ko.md +359 -359
- package/README.md +126 -126
- package/README.pt.md +359 -359
- package/README.ru.md +359 -359
- package/README.th.md +359 -359
- package/README.tr.md +359 -359
- package/README.vi.md +359 -359
- package/README.zh-TW.md +359 -359
- package/README.zh.md +116 -116
- package/agents/ds-flash.md +24 -24
- package/agents/ds-pro.md +24 -24
- package/agents/ds-reviewer.md +23 -23
- package/agents/ds-worker.md +23 -23
- package/bin/dsh-crew.mjs +16 -16
- package/codex/agents/ds-flash.toml +34 -34
- package/codex/agents/ds-pro.toml +34 -34
- package/codex/agents/ds-reviewer.toml +32 -32
- package/codex/agents/ds-worker.toml +32 -32
- package/codex/prompts/dsh-config.md +19 -19
- package/codex/prompts/dsh-status.md +5 -5
- package/commands/config.md +23 -23
- package/commands/off.md +5 -5
- package/commands/on.md +5 -5
- package/commands/status.md +9 -9
- package/cordis.patch.yml +4 -4
- package/docs/gpt-relay-extension.md +103 -103
- package/docs/installation.md +138 -138
- package/docs/job-contracts.md +103 -103
- package/docs/readiness-matrix.md +85 -85
- package/docs/ui-surfaces.md +107 -107
- package/official-web-bridge/cordis.patch.yml +4 -4
- package/official-web-bridge/entry.mjs +1 -1
- package/official-web-bridge/overlay-entry.mjs +59 -59
- package/official-web-bridge/package.json +25 -25
- package/package.json +3 -2
- package/scripts/build-client.mjs +49 -49
- package/scripts/live-crew-smoke.mjs +39 -39
- package/scripts/live-policy-matrix.mjs +177 -177
- package/scripts/policy-probe.mjs +101 -101
- package/scripts/remove-legacy-official-bridge.ps1 +89 -89
- package/scripts/setup.mjs +393 -393
- package/scripts/smoke-real.mjs +110 -110
- package/scripts/smoke.mjs +78 -78
- package/scripts/verify-crew-ui-polish.mjs +145 -145
- package/scripts/verify-history-ui.mjs +97 -97
- package/scripts/verify-installer-fix.mjs +26 -26
- package/scripts/verify-npm-install.mjs +311 -311
- package/scripts/verify-official-bridge-e2e.mjs +192 -192
- package/src/adaptive-routing.mjs +260 -260
- package/src/client/activation-summary.tsx +64 -64
- package/src/client/collapsible-sections.mjs +55 -55
- package/src/client/history-panel.tsx +108 -108
- package/src/client/host-readiness.mjs +71 -71
- package/src/client/index.tsx +1711 -1711
- package/src/client/model-callability-view.mjs +17 -17
- package/src/client/panel-chrome.tsx +40 -40
- package/src/client/quick-entry.tsx +10 -10
- package/src/client/quick-panel.tsx +275 -275
- package/src/client/readiness-envelope.mjs +34 -34
- package/src/client/surface-detection.mjs +43 -43
- package/src/config-readiness.mjs +226 -226
- package/src/credential-reference.mjs +38 -38
- package/src/delivery.mjs +205 -205
- package/src/dsh-cli-runtime.mjs +1021 -1021
- package/src/dsh-cohort.mjs +20 -20
- package/src/extension-contract.mjs +104 -104
- package/src/failure-classification.mjs +201 -201
- package/src/history/admission-gate.mjs +67 -67
- package/src/history/archive-store.mjs +272 -272
- package/src/history/cleanup-plan.mjs +89 -89
- package/src/history/http.mjs +32 -32
- package/src/history/operation.mjs +86 -86
- package/src/history/runner-detach.mjs +34 -34
- package/src/history/runner.mjs +52 -52
- package/src/history/runtime.mjs +36 -36
- package/src/history/service.mjs +177 -177
- package/src/history/state.mjs +27 -27
- package/src/hub/entry.mjs +104 -104
- package/src/hub/index.mjs +2694 -2694
- package/src/hub-client.mjs +154 -154
- package/src/hub-compatibility.mjs +42 -42
- package/src/i18n.mjs +19 -19
- package/src/information-flow.mjs +67 -67
- package/src/install/cli.mjs +28 -28
- package/src/install/install-legacy.mjs +711 -711
- package/src/install/install.mjs +483 -483
- package/src/install/npx-lifecycle.mjs +3456 -3456
- package/src/install/official-frontend-assets.mjs +78 -78
- package/src/install/official-web.mjs +95 -95
- package/src/install/payload-content.mjs +88 -88
- package/src/install/windows-startup.mjs +236 -236
- package/src/install/windows-supervisor-adapter.mjs +443 -443
- package/src/install/windows-supervisor-lifecycle.mjs +782 -782
- package/src/install/zcode.mjs +397 -397
- package/src/job-contracts.mjs +255 -255
- package/src/local-request-guard.mjs +60 -60
- package/src/mcp-runtime.mjs +340 -340
- package/src/model-callability-contract.mjs +79 -79
- package/src/model-catalog.mjs +180 -180
- package/src/model-routing.mjs +586 -586
- package/src/model-schedule.mjs +207 -207
- package/src/official-web-bridge.mjs +447 -447
- package/src/policy.mjs +235 -235
- package/src/provider-delete-adapters.mjs +1934 -1934
- package/src/provider-health.mjs +130 -130
- package/src/provider-inventory.mjs +182 -182
- package/src/provider-layer-migration-adapters.mjs +759 -759
- package/src/provider-layer-migration.mjs +198 -198
- package/src/provider-lifecycle-state.mjs +103 -103
- package/src/provider-lifecycle.mjs +252 -252
- package/src/provider-profile-store.mjs +390 -390
- package/src/provider-settings-store.mjs +633 -633
- package/src/provider-store-lock.mjs +67 -67
- package/src/readiness-matrix.mjs +181 -181
- package/src/removable-waiter.mjs +29 -29
- package/src/role-profiles.mjs +107 -107
- package/src/runtime-controls.mjs +84 -84
- package/src/runtime-identity-contract.mjs +34 -34
- package/src/runtime-identity.mjs +235 -235
- package/src/runtime-readiness-snapshot.mjs +285 -285
- package/src/server.mjs +581 -581
- package/src/session-origins.mjs +60 -60
- package/src/standalone-sdk.mjs +23 -23
- package/src/status-shard.mjs +63 -63
- package/src/structured-error-code.mjs +38 -38
- package/src/supervisor/restart-request.mjs +256 -256
- package/src/workflow-runtime.mjs +739 -739
- package/src/workflow.mjs +155 -155
- package/src/workspace-audit.mjs +231 -231
- package/src/workspace-context.mjs +146 -146
- package/src/workspace-isolation.mjs +463 -455
- package/src/workspace-readiness.mjs +32 -32
- package/statusline/statusline.sh +14 -14
- package/statusline/worker-segment.sh +35 -35
- package/windows/start-dsh-crew.cmd +57 -57
- package/windows/start-dsh-crew.ps1 +1302 -1302
- package/windows/supervisor-control.ps1 +467 -467
- package/worker.cordis.yml +67 -67
- package/zcode/agents/ds-reviewer.md +31 -31
- package/zcode/agents/ds-worker.md +31 -31
- package/zcode/commands/dsh-config.md +17 -17
- package/zcode/commands/dsh-status.md +5 -5
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
export const SETTINGS_SECTION_STORAGE_KEY = 'dsh-crew.settings-sections.v1';
|
|
2
|
-
|
|
3
|
-
export const SETTINGS_SECTION_IDS = Object.freeze([
|
|
4
|
-
'integrations',
|
|
5
|
-
'workflow',
|
|
6
|
-
'flash',
|
|
7
|
-
'pro',
|
|
8
|
-
'dispatch',
|
|
9
|
-
'adaptive',
|
|
10
|
-
'runtime',
|
|
11
|
-
'harnessProviders',
|
|
12
|
-
'jobs',
|
|
13
|
-
]);
|
|
14
|
-
|
|
15
|
-
export function createDefaultSectionState() {
|
|
16
|
-
return Object.fromEntries(SETTINGS_SECTION_IDS.map((id) => [id, id === 'workflow']));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function readSectionState(storage, key = SETTINGS_SECTION_STORAGE_KEY) {
|
|
20
|
-
const defaults = createDefaultSectionState();
|
|
21
|
-
if (!storage?.getItem) return defaults;
|
|
22
|
-
try {
|
|
23
|
-
const parsed = JSON.parse(storage.getItem(key) ?? 'null');
|
|
24
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return defaults;
|
|
25
|
-
for (const id of SETTINGS_SECTION_IDS) {
|
|
26
|
-
if (typeof parsed[id] === 'boolean') defaults[id] = parsed[id];
|
|
27
|
-
}
|
|
28
|
-
} catch {
|
|
29
|
-
return defaults;
|
|
30
|
-
}
|
|
31
|
-
return defaults;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function writeSectionState(storage, state, key = SETTINGS_SECTION_STORAGE_KEY) {
|
|
35
|
-
if (!storage?.setItem) return;
|
|
36
|
-
try {
|
|
37
|
-
storage.setItem(key, JSON.stringify(
|
|
38
|
-
Object.fromEntries(SETTINGS_SECTION_IDS.map((id) => [id, state[id] === true])),
|
|
39
|
-
));
|
|
40
|
-
} catch {
|
|
41
|
-
// Storage can be unavailable in private/locked-down browser contexts.
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function setEverySection(expanded) {
|
|
46
|
-
return Object.fromEntries(SETTINGS_SECTION_IDS.map((id) => [id, expanded === true]));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function openSections(state, ids) {
|
|
50
|
-
const next = { ...state };
|
|
51
|
-
for (const id of ids) {
|
|
52
|
-
if (SETTINGS_SECTION_IDS.includes(id)) next[id] = true;
|
|
53
|
-
}
|
|
54
|
-
return next;
|
|
55
|
-
}
|
|
1
|
+
export const SETTINGS_SECTION_STORAGE_KEY = 'dsh-crew.settings-sections.v1';
|
|
2
|
+
|
|
3
|
+
export const SETTINGS_SECTION_IDS = Object.freeze([
|
|
4
|
+
'integrations',
|
|
5
|
+
'workflow',
|
|
6
|
+
'flash',
|
|
7
|
+
'pro',
|
|
8
|
+
'dispatch',
|
|
9
|
+
'adaptive',
|
|
10
|
+
'runtime',
|
|
11
|
+
'harnessProviders',
|
|
12
|
+
'jobs',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
export function createDefaultSectionState() {
|
|
16
|
+
return Object.fromEntries(SETTINGS_SECTION_IDS.map((id) => [id, id === 'workflow']));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readSectionState(storage, key = SETTINGS_SECTION_STORAGE_KEY) {
|
|
20
|
+
const defaults = createDefaultSectionState();
|
|
21
|
+
if (!storage?.getItem) return defaults;
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(storage.getItem(key) ?? 'null');
|
|
24
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return defaults;
|
|
25
|
+
for (const id of SETTINGS_SECTION_IDS) {
|
|
26
|
+
if (typeof parsed[id] === 'boolean') defaults[id] = parsed[id];
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
return defaults;
|
|
30
|
+
}
|
|
31
|
+
return defaults;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function writeSectionState(storage, state, key = SETTINGS_SECTION_STORAGE_KEY) {
|
|
35
|
+
if (!storage?.setItem) return;
|
|
36
|
+
try {
|
|
37
|
+
storage.setItem(key, JSON.stringify(
|
|
38
|
+
Object.fromEntries(SETTINGS_SECTION_IDS.map((id) => [id, state[id] === true])),
|
|
39
|
+
));
|
|
40
|
+
} catch {
|
|
41
|
+
// Storage can be unavailable in private/locked-down browser contexts.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function setEverySection(expanded) {
|
|
46
|
+
return Object.fromEntries(SETTINGS_SECTION_IDS.map((id) => [id, expanded === true]));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function openSections(state, ids) {
|
|
50
|
+
const next = { ...state };
|
|
51
|
+
for (const id of ids) {
|
|
52
|
+
if (SETTINGS_SECTION_IDS.includes(id)) next[id] = true;
|
|
53
|
+
}
|
|
54
|
+
return next;
|
|
55
|
+
}
|
|
@@ -1,108 +1,108 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
2
|
-
|
|
3
|
-
const API = '/_dsh/dsh-crew/history/';
|
|
4
|
-
const pending = (phase?: string) => !!phase && !['IDLE', 'DONE', 'FAILED', 'ROLLED_BACK'].includes(phase);
|
|
5
|
-
const copy = {
|
|
6
|
-
zh: {
|
|
7
|
-
title: '工作区与会话清理', intro: '仅影响 3210 的工作区记录与会话日志;不删除项目文件、附件或 3080 数据。',
|
|
8
|
-
operation: '操作', archive: '归档(可恢复)', delete: '删除(不可恢复)', scope: '范围', all: '全部(含你自己的会话)', crew: '仅 Crew 创建(推荐)', worktree: '仅 Crew 工作树', before: '指定时间之前',
|
|
9
|
-
time: '创建时间早于', timeHint: '按本机时区选择,严格按创建时间筛选;包含新会话的工作区会保留。',
|
|
10
|
-
preview: '预览清理范围', confirm: '确认执行', acknowledgement: '删除确认:请输入 DELETE', idle: '会短暂停止并重启 3210;请先结束或关闭所有正在使用的 3210 会话。',
|
|
11
|
-
consent: '我已确认范围,并同意短暂重启 3210', restore: '恢复', archives: '已归档批次', empty: '暂无可恢复的归档',
|
|
12
|
-
working: '维护中,正在等待 3210 重新连接…', refresh: '刷新页面与工作区列表', recover: '恢复未完成的维护',
|
|
13
|
-
recovery: '维护尚未完成,请使用恢复按钮;若 3210 无法连接,在终端运行 dsh-crew history recover。',
|
|
14
|
-
cancelQueued: '取消尚未开始的维护', cancelQueuedHint: '仅取消尚未开始的维护,不清理任何数据;如果操作已开始,请等待完成。',
|
|
15
|
-
count: (w: number, s: number) => `${w} 个工作区 · ${s} 个会话`, confirmRestore: '恢复这一批归档?将短暂重启 3210,不会覆盖冲突数据。',
|
|
16
|
-
phase: { IDLE: '就绪', QUEUED: '已提交', STOPPING: '停止后台', APPLYING: '处理数据', STARTING: '启动后台', VERIFYING: '验证结果', DONE: '已完成', FAILED: '未执行成功', ROLLED_BACK: '已回滚', RECOVERY_REQUIRED: '需要恢复' },
|
|
17
|
-
},
|
|
18
|
-
en: {
|
|
19
|
-
title: 'Workspace & session cleanup', intro: 'Only 3210 workspace records and session logs. Project files, attachments and 3080 data are untouched.',
|
|
20
|
-
operation: 'Action', archive: 'Archive (restorable)', delete: 'Delete (permanent)', scope: 'Scope', all: 'Everything (includes your own sessions)', crew: 'Crew-created only (recommended)', worktree: 'Crew worktrees only', before: 'Before a date',
|
|
21
|
-
time: 'Created before', timeHint: 'Local timezone; strict creation-time cutoff. Workspaces with newer sessions are kept.',
|
|
22
|
-
preview: 'Preview cleanup', confirm: 'Confirm operation', acknowledgement: 'Type DELETE to confirm deletion', idle: '3210 will briefly stop and restart. End or close all active 3210 conversations first.',
|
|
23
|
-
consent: 'I reviewed the scope and agree to restart 3210', restore: 'Restore', archives: 'Archived batches', empty: 'No restorable archives',
|
|
24
|
-
working: 'Maintenance in progress; waiting for 3210 to reconnect…', refresh: 'Reload page and workspace list', recover: 'Recover unfinished maintenance',
|
|
25
|
-
recovery: 'Maintenance is unfinished. Use recovery; if 3210 is unreachable, run dsh-crew history recover in a terminal.',
|
|
26
|
-
cancelQueued: 'Cancel unstarted maintenance', cancelQueuedHint: 'Cancel only if maintenance has not started. No data is cleaned; if already running, wait for completion.',
|
|
27
|
-
count: (w: number, s: number) => `${w} workspaces · ${s} sessions`, confirmRestore: 'Restore this archive? 3210 will briefly restart; conflicting data will not be overwritten.',
|
|
28
|
-
phase: { IDLE: 'Ready', QUEUED: 'Queued', STOPPING: 'Stopping backend', APPLYING: 'Processing data', STARTING: 'Starting backend', VERIFYING: 'Verifying', DONE: 'Completed', FAILED: 'Failed', ROLLED_BACK: 'Rolled back', RECOVERY_REQUIRED: 'Recovery required' },
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export function HistoryPanel({ locale }: { locale: string }) {
|
|
33
|
-
const t = copy[locale === 'zh' ? 'zh' : 'en'];
|
|
34
|
-
const [open, setOpen] = useState(false);
|
|
35
|
-
const [operation, setOperation] = useState('archive');
|
|
36
|
-
const [scope, setScope] = useState('crew');
|
|
37
|
-
const [before, setBefore] = useState('');
|
|
38
|
-
const [preview, setPreview] = useState<any>(null);
|
|
39
|
-
const [status, setStatus] = useState<any>({ phase: 'IDLE' });
|
|
40
|
-
const [archives, setArchives] = useState<any[]>([]);
|
|
41
|
-
const [busy, setBusy] = useState(false);
|
|
42
|
-
const [consent, setConsent] = useState(false);
|
|
43
|
-
const [ack, setAck] = useState('');
|
|
44
|
-
const [notice, setNotice] = useState('');
|
|
45
|
-
const [connected, setConnected] = useState(false);
|
|
46
|
-
const locked = busy || pending(status.phase);
|
|
47
|
-
const invalidate = () => { setPreview(null); setConsent(false); setAck(''); };
|
|
48
|
-
|
|
49
|
-
async function request(action: string, body?: unknown) {
|
|
50
|
-
const res = await fetch(API + action, { cache: 'no-store', ...(body === undefined ? {} : { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }) });
|
|
51
|
-
const data = await res.json();
|
|
52
|
-
if (!res.ok || !data.ok) throw Error(data.code ?? `HTTP ${res.status}`);
|
|
53
|
-
return data;
|
|
54
|
-
}
|
|
55
|
-
useEffect(() => {
|
|
56
|
-
if (!open) return;
|
|
57
|
-
let disposed = false;
|
|
58
|
-
const poll = async () => {
|
|
59
|
-
try {
|
|
60
|
-
const [s, a] = await Promise.all([request('status'), request('archives')]);
|
|
61
|
-
if (!disposed) { setStatus(s.status); setArchives(a.archives); setConnected(true); }
|
|
62
|
-
} catch { if (!disposed) setConnected(false); }
|
|
63
|
-
};
|
|
64
|
-
void poll(); const timer = setInterval(poll, 2500);
|
|
65
|
-
return () => { disposed = true; clearInterval(timer); };
|
|
66
|
-
}, [open]);
|
|
67
|
-
|
|
68
|
-
async function act(action: string, body: any) {
|
|
69
|
-
setBusy(true); setNotice('');
|
|
70
|
-
try {
|
|
71
|
-
const result = await request(action, body);
|
|
72
|
-
if (result.preview) { setPreview(result.preview); setConsent(false); setAck(''); }
|
|
73
|
-
if (result.status) { setStatus(result.status); setPreview(null); }
|
|
74
|
-
} catch (error: any) { setNotice(String(error.message)); setPreview(null); }
|
|
75
|
-
finally { setBusy(false); }
|
|
76
|
-
}
|
|
77
|
-
const style = { border: '1px solid rgba(128,128,128,.25)', borderRadius: 8, padding: '7px 10px', color: 'inherit', background: 'transparent', font: 'inherit' };
|
|
78
|
-
return <details style={{ border: '1px solid rgba(128,128,128,.24)', borderRadius: 10, padding: '10px 13px' }} onToggle={e => setOpen(e.currentTarget.open)}>
|
|
79
|
-
<summary style={{ cursor: 'pointer', fontWeight: 650 }}>{t.title}</summary>
|
|
80
|
-
{open && <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 12 }}>
|
|
81
|
-
<p style={{ margin: 0, opacity: .75 }}>{t.intro}</p>
|
|
82
|
-
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
|
83
|
-
<label>{t.operation} <select style={style} value={operation} disabled={locked} onChange={e => { setOperation(e.target.value); invalidate(); }}><option value="archive">{t.archive}</option><option value="delete">{t.delete}</option></select></label>
|
|
84
|
-
<label>{t.scope} <select style={style} value={scope} disabled={locked} onChange={e => { setScope(e.target.value); invalidate(); }}><option value="crew">{t.crew}</option><option value="worktree">{t.worktree}</option><option value="all">{t.all}</option><option value="before">{t.before}</option></select></label>
|
|
85
|
-
</div>
|
|
86
|
-
{scope === 'before' && <label>{t.time} <input type="datetime-local" style={style} value={before} disabled={locked} onChange={e => { setBefore(e.target.value); invalidate(); }} /></label>}
|
|
87
|
-
<div style={{ fontSize: 11.5, opacity: .7 }}>{t.timeHint}</div>
|
|
88
|
-
<button type="button" style={style} disabled={locked || !connected || (scope === 'before' && !Number.isFinite(new Date(before).getTime()))} onClick={() => void act('preview', { operation, scope, ...(scope === 'before' ? { before: new Date(before).toISOString() } : {}) })}>{t.preview}</button>
|
|
89
|
-
{preview && <div style={{ ...style, display: 'flex', flexDirection: 'column', gap: 9 }}>
|
|
90
|
-
<strong>{t.count(preview.counts.workspaces, preview.counts.sessions)}</strong>
|
|
91
|
-
{preview.before && <span>{t.time}: {new Date(preview.before).toLocaleString()}</span>}
|
|
92
|
-
<details><summary>{t.preview}</summary><ul>{preview.items?.map((item: any) => <li key={item.id}>{item.title} <small>{item.id}</small></li>)}</ul><div style={{ overflowWrap: 'anywhere', fontSize: 11 }}>{preview.sessionIds?.slice(0, 100).join(', ')}{preview.sessionIds?.length > 100 ? ' …' : ''}</div></details>
|
|
93
|
-
{!preview.executable && <div role="alert">{preview.blockedReason}</div>}
|
|
94
|
-
<div>{t.idle}</div>
|
|
95
|
-
<label><input type="checkbox" checked={consent} disabled={locked} onChange={e => setConsent(e.target.checked)} /> {t.consent}</label>
|
|
96
|
-
{operation === 'delete' && <label>{t.acknowledgement} <input style={style} value={ack} disabled={locked} onChange={e => setAck(e.target.value)} /></label>}
|
|
97
|
-
<button type="button" style={style} disabled={locked || !consent || !preview.executable || (operation === 'delete' && ack !== 'DELETE')} onClick={() => void act('execute', { planId: preview.planId, confirm: true, acknowledgement: ack })}>{t.confirm}</button>
|
|
98
|
-
</div>}
|
|
99
|
-
<div role="status" aria-live="polite">{(t.phase as any)[status.phase] ?? status.phase}{status.code ? ` · ${status.code}` : ''}{notice ? ` · ${notice}` : ''}</div>
|
|
100
|
-
{!connected && <div role="status">{pending(status.phase) ? t.working : '3210 history API unavailable'}<p>{t.recovery}</p></div>}
|
|
101
|
-
{['RECOVERY_REQUIRED', 'QUEUED'].includes(status.phase) && <div><p>{status.phase === 'QUEUED' ? t.cancelQueuedHint : t.recovery}</p><button type="button" style={style} disabled={busy} onClick={() => { if (window.confirm(status.phase === 'QUEUED' ? t.cancelQueuedHint : t.recovery)) void act('recover', { confirm: true }); }}>{status.phase === 'QUEUED' ? t.cancelQueued : t.recover}</button></div>}
|
|
102
|
-
{['DONE', 'ROLLED_BACK'].includes(status.phase) && <button type="button" style={style} onClick={() => window.location.reload()}>{t.refresh}</button>}
|
|
103
|
-
<strong>{t.archives}</strong>
|
|
104
|
-
{archives.length === 0 && <span style={{ opacity: .6 }}>{t.empty}</span>}
|
|
105
|
-
{archives.map(a => <div key={a.id} style={{ ...style, display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}><span style={{ flex: 1 }}>{a.invalid ? `${a.id} · ${a.code}` : `${new Date(a.createdAt).toLocaleString()} · ${t.count(a.workspaces, a.sessions)}`}</span><button type="button" style={style} disabled={locked || a.invalid} onClick={() => { if (window.confirm(t.confirmRestore)) void act('restore', { archiveId: a.id, confirm: true }); }}>{t.restore}</button></div>)}
|
|
106
|
-
</div>}
|
|
107
|
-
</details>;
|
|
108
|
-
}
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
const API = '/_dsh/dsh-crew/history/';
|
|
4
|
+
const pending = (phase?: string) => !!phase && !['IDLE', 'DONE', 'FAILED', 'ROLLED_BACK'].includes(phase);
|
|
5
|
+
const copy = {
|
|
6
|
+
zh: {
|
|
7
|
+
title: '工作区与会话清理', intro: '仅影响 3210 的工作区记录与会话日志;不删除项目文件、附件或 3080 数据。',
|
|
8
|
+
operation: '操作', archive: '归档(可恢复)', delete: '删除(不可恢复)', scope: '范围', all: '全部(含你自己的会话)', crew: '仅 Crew 创建(推荐)', worktree: '仅 Crew 工作树', before: '指定时间之前',
|
|
9
|
+
time: '创建时间早于', timeHint: '按本机时区选择,严格按创建时间筛选;包含新会话的工作区会保留。',
|
|
10
|
+
preview: '预览清理范围', confirm: '确认执行', acknowledgement: '删除确认:请输入 DELETE', idle: '会短暂停止并重启 3210;请先结束或关闭所有正在使用的 3210 会话。',
|
|
11
|
+
consent: '我已确认范围,并同意短暂重启 3210', restore: '恢复', archives: '已归档批次', empty: '暂无可恢复的归档',
|
|
12
|
+
working: '维护中,正在等待 3210 重新连接…', refresh: '刷新页面与工作区列表', recover: '恢复未完成的维护',
|
|
13
|
+
recovery: '维护尚未完成,请使用恢复按钮;若 3210 无法连接,在终端运行 dsh-crew history recover。',
|
|
14
|
+
cancelQueued: '取消尚未开始的维护', cancelQueuedHint: '仅取消尚未开始的维护,不清理任何数据;如果操作已开始,请等待完成。',
|
|
15
|
+
count: (w: number, s: number) => `${w} 个工作区 · ${s} 个会话`, confirmRestore: '恢复这一批归档?将短暂重启 3210,不会覆盖冲突数据。',
|
|
16
|
+
phase: { IDLE: '就绪', QUEUED: '已提交', STOPPING: '停止后台', APPLYING: '处理数据', STARTING: '启动后台', VERIFYING: '验证结果', DONE: '已完成', FAILED: '未执行成功', ROLLED_BACK: '已回滚', RECOVERY_REQUIRED: '需要恢复' },
|
|
17
|
+
},
|
|
18
|
+
en: {
|
|
19
|
+
title: 'Workspace & session cleanup', intro: 'Only 3210 workspace records and session logs. Project files, attachments and 3080 data are untouched.',
|
|
20
|
+
operation: 'Action', archive: 'Archive (restorable)', delete: 'Delete (permanent)', scope: 'Scope', all: 'Everything (includes your own sessions)', crew: 'Crew-created only (recommended)', worktree: 'Crew worktrees only', before: 'Before a date',
|
|
21
|
+
time: 'Created before', timeHint: 'Local timezone; strict creation-time cutoff. Workspaces with newer sessions are kept.',
|
|
22
|
+
preview: 'Preview cleanup', confirm: 'Confirm operation', acknowledgement: 'Type DELETE to confirm deletion', idle: '3210 will briefly stop and restart. End or close all active 3210 conversations first.',
|
|
23
|
+
consent: 'I reviewed the scope and agree to restart 3210', restore: 'Restore', archives: 'Archived batches', empty: 'No restorable archives',
|
|
24
|
+
working: 'Maintenance in progress; waiting for 3210 to reconnect…', refresh: 'Reload page and workspace list', recover: 'Recover unfinished maintenance',
|
|
25
|
+
recovery: 'Maintenance is unfinished. Use recovery; if 3210 is unreachable, run dsh-crew history recover in a terminal.',
|
|
26
|
+
cancelQueued: 'Cancel unstarted maintenance', cancelQueuedHint: 'Cancel only if maintenance has not started. No data is cleaned; if already running, wait for completion.',
|
|
27
|
+
count: (w: number, s: number) => `${w} workspaces · ${s} sessions`, confirmRestore: 'Restore this archive? 3210 will briefly restart; conflicting data will not be overwritten.',
|
|
28
|
+
phase: { IDLE: 'Ready', QUEUED: 'Queued', STOPPING: 'Stopping backend', APPLYING: 'Processing data', STARTING: 'Starting backend', VERIFYING: 'Verifying', DONE: 'Completed', FAILED: 'Failed', ROLLED_BACK: 'Rolled back', RECOVERY_REQUIRED: 'Recovery required' },
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function HistoryPanel({ locale }: { locale: string }) {
|
|
33
|
+
const t = copy[locale === 'zh' ? 'zh' : 'en'];
|
|
34
|
+
const [open, setOpen] = useState(false);
|
|
35
|
+
const [operation, setOperation] = useState('archive');
|
|
36
|
+
const [scope, setScope] = useState('crew');
|
|
37
|
+
const [before, setBefore] = useState('');
|
|
38
|
+
const [preview, setPreview] = useState<any>(null);
|
|
39
|
+
const [status, setStatus] = useState<any>({ phase: 'IDLE' });
|
|
40
|
+
const [archives, setArchives] = useState<any[]>([]);
|
|
41
|
+
const [busy, setBusy] = useState(false);
|
|
42
|
+
const [consent, setConsent] = useState(false);
|
|
43
|
+
const [ack, setAck] = useState('');
|
|
44
|
+
const [notice, setNotice] = useState('');
|
|
45
|
+
const [connected, setConnected] = useState(false);
|
|
46
|
+
const locked = busy || pending(status.phase);
|
|
47
|
+
const invalidate = () => { setPreview(null); setConsent(false); setAck(''); };
|
|
48
|
+
|
|
49
|
+
async function request(action: string, body?: unknown) {
|
|
50
|
+
const res = await fetch(API + action, { cache: 'no-store', ...(body === undefined ? {} : { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }) });
|
|
51
|
+
const data = await res.json();
|
|
52
|
+
if (!res.ok || !data.ok) throw Error(data.code ?? `HTTP ${res.status}`);
|
|
53
|
+
return data;
|
|
54
|
+
}
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!open) return;
|
|
57
|
+
let disposed = false;
|
|
58
|
+
const poll = async () => {
|
|
59
|
+
try {
|
|
60
|
+
const [s, a] = await Promise.all([request('status'), request('archives')]);
|
|
61
|
+
if (!disposed) { setStatus(s.status); setArchives(a.archives); setConnected(true); }
|
|
62
|
+
} catch { if (!disposed) setConnected(false); }
|
|
63
|
+
};
|
|
64
|
+
void poll(); const timer = setInterval(poll, 2500);
|
|
65
|
+
return () => { disposed = true; clearInterval(timer); };
|
|
66
|
+
}, [open]);
|
|
67
|
+
|
|
68
|
+
async function act(action: string, body: any) {
|
|
69
|
+
setBusy(true); setNotice('');
|
|
70
|
+
try {
|
|
71
|
+
const result = await request(action, body);
|
|
72
|
+
if (result.preview) { setPreview(result.preview); setConsent(false); setAck(''); }
|
|
73
|
+
if (result.status) { setStatus(result.status); setPreview(null); }
|
|
74
|
+
} catch (error: any) { setNotice(String(error.message)); setPreview(null); }
|
|
75
|
+
finally { setBusy(false); }
|
|
76
|
+
}
|
|
77
|
+
const style = { border: '1px solid rgba(128,128,128,.25)', borderRadius: 8, padding: '7px 10px', color: 'inherit', background: 'transparent', font: 'inherit' };
|
|
78
|
+
return <details style={{ border: '1px solid rgba(128,128,128,.24)', borderRadius: 10, padding: '10px 13px' }} onToggle={e => setOpen(e.currentTarget.open)}>
|
|
79
|
+
<summary style={{ cursor: 'pointer', fontWeight: 650 }}>{t.title}</summary>
|
|
80
|
+
{open && <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 12 }}>
|
|
81
|
+
<p style={{ margin: 0, opacity: .75 }}>{t.intro}</p>
|
|
82
|
+
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
|
83
|
+
<label>{t.operation} <select style={style} value={operation} disabled={locked} onChange={e => { setOperation(e.target.value); invalidate(); }}><option value="archive">{t.archive}</option><option value="delete">{t.delete}</option></select></label>
|
|
84
|
+
<label>{t.scope} <select style={style} value={scope} disabled={locked} onChange={e => { setScope(e.target.value); invalidate(); }}><option value="crew">{t.crew}</option><option value="worktree">{t.worktree}</option><option value="all">{t.all}</option><option value="before">{t.before}</option></select></label>
|
|
85
|
+
</div>
|
|
86
|
+
{scope === 'before' && <label>{t.time} <input type="datetime-local" style={style} value={before} disabled={locked} onChange={e => { setBefore(e.target.value); invalidate(); }} /></label>}
|
|
87
|
+
<div style={{ fontSize: 11.5, opacity: .7 }}>{t.timeHint}</div>
|
|
88
|
+
<button type="button" style={style} disabled={locked || !connected || (scope === 'before' && !Number.isFinite(new Date(before).getTime()))} onClick={() => void act('preview', { operation, scope, ...(scope === 'before' ? { before: new Date(before).toISOString() } : {}) })}>{t.preview}</button>
|
|
89
|
+
{preview && <div style={{ ...style, display: 'flex', flexDirection: 'column', gap: 9 }}>
|
|
90
|
+
<strong>{t.count(preview.counts.workspaces, preview.counts.sessions)}</strong>
|
|
91
|
+
{preview.before && <span>{t.time}: {new Date(preview.before).toLocaleString()}</span>}
|
|
92
|
+
<details><summary>{t.preview}</summary><ul>{preview.items?.map((item: any) => <li key={item.id}>{item.title} <small>{item.id}</small></li>)}</ul><div style={{ overflowWrap: 'anywhere', fontSize: 11 }}>{preview.sessionIds?.slice(0, 100).join(', ')}{preview.sessionIds?.length > 100 ? ' …' : ''}</div></details>
|
|
93
|
+
{!preview.executable && <div role="alert">{preview.blockedReason}</div>}
|
|
94
|
+
<div>{t.idle}</div>
|
|
95
|
+
<label><input type="checkbox" checked={consent} disabled={locked} onChange={e => setConsent(e.target.checked)} /> {t.consent}</label>
|
|
96
|
+
{operation === 'delete' && <label>{t.acknowledgement} <input style={style} value={ack} disabled={locked} onChange={e => setAck(e.target.value)} /></label>}
|
|
97
|
+
<button type="button" style={style} disabled={locked || !consent || !preview.executable || (operation === 'delete' && ack !== 'DELETE')} onClick={() => void act('execute', { planId: preview.planId, confirm: true, acknowledgement: ack })}>{t.confirm}</button>
|
|
98
|
+
</div>}
|
|
99
|
+
<div role="status" aria-live="polite">{(t.phase as any)[status.phase] ?? status.phase}{status.code ? ` · ${status.code}` : ''}{notice ? ` · ${notice}` : ''}</div>
|
|
100
|
+
{!connected && <div role="status">{pending(status.phase) ? t.working : '3210 history API unavailable'}<p>{t.recovery}</p></div>}
|
|
101
|
+
{['RECOVERY_REQUIRED', 'QUEUED'].includes(status.phase) && <div><p>{status.phase === 'QUEUED' ? t.cancelQueuedHint : t.recovery}</p><button type="button" style={style} disabled={busy} onClick={() => { if (window.confirm(status.phase === 'QUEUED' ? t.cancelQueuedHint : t.recovery)) void act('recover', { confirm: true }); }}>{status.phase === 'QUEUED' ? t.cancelQueued : t.recover}</button></div>}
|
|
102
|
+
{['DONE', 'ROLLED_BACK'].includes(status.phase) && <button type="button" style={style} onClick={() => window.location.reload()}>{t.refresh}</button>}
|
|
103
|
+
<strong>{t.archives}</strong>
|
|
104
|
+
{archives.length === 0 && <span style={{ opacity: .6 }}>{t.empty}</span>}
|
|
105
|
+
{archives.map(a => <div key={a.id} style={{ ...style, display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}><span style={{ flex: 1 }}>{a.invalid ? `${a.id} · ${a.code}` : `${new Date(a.createdAt).toLocaleString()} · ${t.count(a.workspaces, a.sessions)}`}</span><button type="button" style={style} disabled={locked || a.invalid} onClick={() => { if (window.confirm(t.confirmRestore)) void act('restore', { archiveId: a.id, confirm: true }); }}>{t.restore}</button></div>)}
|
|
106
|
+
</div>}
|
|
107
|
+
</details>;
|
|
108
|
+
}
|
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
import { isCompleteRuntimeIdentity, isExactNativeCrewIdentity, sameCompleteRuntimeIdentity } from '../runtime-identity-contract.mjs';
|
|
2
|
-
|
|
3
|
-
export const READINESS_STATES = Object.freeze({
|
|
4
|
-
READY: 'READY',
|
|
5
|
-
DEGRADED: 'DEGRADED',
|
|
6
|
-
UNAVAILABLE: 'UNAVAILABLE',
|
|
7
|
-
UNKNOWN: 'UNKNOWN',
|
|
8
|
-
NOT_APPLICABLE: 'NOT_APPLICABLE',
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
const MAX_RUNTIME_SNAPSHOT_AGE_MS = 5 * 60 * 1000;
|
|
12
|
-
|
|
13
|
-
function componentState(status, keys, { aligned = false } = {}) {
|
|
14
|
-
if (status === undefined || status === null || typeof status !== 'object') return READINESS_STATES.UNKNOWN;
|
|
15
|
-
if (status.installed === false) return READINESS_STATES.UNAVAILABLE;
|
|
16
|
-
if (status.installed !== true || !status.components || typeof status.components !== 'object') return READINESS_STATES.UNKNOWN;
|
|
17
|
-
const evidence = keys.map((key) => status.components[key]);
|
|
18
|
-
if (evidence.some((value) => typeof value !== 'boolean')) return READINESS_STATES.UNKNOWN;
|
|
19
|
-
const ready = evidence.every(Boolean) && (!aligned || status.components.target_alignment === true);
|
|
20
|
-
return ready ? READINESS_STATES.READY : READINESS_STATES.DEGRADED;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function runtimeState(runtime, readinessSnapshot) {
|
|
24
|
-
if (readinessSnapshot && typeof readinessSnapshot === 'object') {
|
|
25
|
-
const identity = readinessSnapshot.runtime;
|
|
26
|
-
if (!isExactNativeCrewIdentity(identity)) {
|
|
27
|
-
return READINESS_STATES.UNAVAILABLE;
|
|
28
|
-
}
|
|
29
|
-
if (runtime && (!isCompleteRuntimeIdentity(runtime) || !sameCompleteRuntimeIdentity(runtime, identity))) {
|
|
30
|
-
return READINESS_STATES.UNKNOWN;
|
|
31
|
-
}
|
|
32
|
-
if (!Number.isFinite(readinessSnapshot.captured_at)) return READINESS_STATES.UNKNOWN;
|
|
33
|
-
if (readinessSnapshot.captured_at > Date.now() + 30_000) return READINESS_STATES.UNKNOWN;
|
|
34
|
-
if (Date.now() - readinessSnapshot.captured_at > MAX_RUNTIME_SNAPSHOT_AGE_MS) return READINESS_STATES.DEGRADED;
|
|
35
|
-
const row = Array.isArray(readinessSnapshot.readiness_matrix?.rows)
|
|
36
|
-
? readinessSnapshot.readiness_matrix.rows.find((entry) => entry?.id === 'hub_compatibility') : undefined;
|
|
37
|
-
if (row?.status === 'PASS') return READINESS_STATES.READY;
|
|
38
|
-
if (row?.status === 'FAIL') return READINESS_STATES.UNAVAILABLE;
|
|
39
|
-
if (row?.status === 'NOT_RUN' || row?.status === 'SKIP') return READINESS_STATES.DEGRADED;
|
|
40
|
-
return READINESS_STATES.UNKNOWN;
|
|
41
|
-
}
|
|
42
|
-
if (runtime === undefined) return READINESS_STATES.UNKNOWN;
|
|
43
|
-
if (runtime === null) return READINESS_STATES.UNAVAILABLE;
|
|
44
|
-
const exactIdentity = runtime?.ok === true && isExactNativeCrewIdentity(runtime);
|
|
45
|
-
return exactIdentity ? READINESS_STATES.READY : READINESS_STATES.UNKNOWN;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function bridgeState(surface) {
|
|
49
|
-
// The official 3080 quick surface is an OPTIONAL convenience panel. Its
|
|
50
|
-
// absence must never degrade Crew readiness: on the native 3210 control
|
|
51
|
-
// plane the bridge is simply not part of the picture.
|
|
52
|
-
if (surface === 'official-bridge') return READINESS_STATES.READY;
|
|
53
|
-
if (surface === 'native-crew-harness') return READINESS_STATES.NOT_APPLICABLE;
|
|
54
|
-
return READINESS_STATES.UNKNOWN;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Project only structured, non-secret installer/runtime evidence. */
|
|
58
|
-
export function projectHostReadiness({ installStatus, runtime, surface, readinessSnapshot } = {}) {
|
|
59
|
-
const codex = installStatus?.codex;
|
|
60
|
-
const claude = installStatus?.claude;
|
|
61
|
-
const zcode = installStatus?.zcode;
|
|
62
|
-
return [
|
|
63
|
-
{ id: 'codex_mcp', state: componentState(codex, ['mcp'], { aligned: true }) },
|
|
64
|
-
{ id: 'ds_worker', state: componentState(codex, ['worker_role'], { aligned: true }) },
|
|
65
|
-
{ id: 'ds_reviewer', state: componentState(codex, ['reviewer_role'], { aligned: true }) },
|
|
66
|
-
{ id: 'claude_plugin', state: componentState(claude, ['enabled', 'marketplace', 'snapshot', 'permissions']) },
|
|
67
|
-
{ id: 'zcode_mcp', state: componentState(zcode, ['mcp', 'policy', 'worker_agent', 'reviewer_agent', 'config_prompt', 'status_prompt', 'ownership']) },
|
|
68
|
-
{ id: 'crew_harness', state: runtimeState(runtime, readinessSnapshot), detail: runtime?.runtime_version ?? null },
|
|
69
|
-
{ id: 'official_bridge', state: bridgeState(surface) },
|
|
70
|
-
];
|
|
71
|
-
}
|
|
1
|
+
import { isCompleteRuntimeIdentity, isExactNativeCrewIdentity, sameCompleteRuntimeIdentity } from '../runtime-identity-contract.mjs';
|
|
2
|
+
|
|
3
|
+
export const READINESS_STATES = Object.freeze({
|
|
4
|
+
READY: 'READY',
|
|
5
|
+
DEGRADED: 'DEGRADED',
|
|
6
|
+
UNAVAILABLE: 'UNAVAILABLE',
|
|
7
|
+
UNKNOWN: 'UNKNOWN',
|
|
8
|
+
NOT_APPLICABLE: 'NOT_APPLICABLE',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const MAX_RUNTIME_SNAPSHOT_AGE_MS = 5 * 60 * 1000;
|
|
12
|
+
|
|
13
|
+
function componentState(status, keys, { aligned = false } = {}) {
|
|
14
|
+
if (status === undefined || status === null || typeof status !== 'object') return READINESS_STATES.UNKNOWN;
|
|
15
|
+
if (status.installed === false) return READINESS_STATES.UNAVAILABLE;
|
|
16
|
+
if (status.installed !== true || !status.components || typeof status.components !== 'object') return READINESS_STATES.UNKNOWN;
|
|
17
|
+
const evidence = keys.map((key) => status.components[key]);
|
|
18
|
+
if (evidence.some((value) => typeof value !== 'boolean')) return READINESS_STATES.UNKNOWN;
|
|
19
|
+
const ready = evidence.every(Boolean) && (!aligned || status.components.target_alignment === true);
|
|
20
|
+
return ready ? READINESS_STATES.READY : READINESS_STATES.DEGRADED;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function runtimeState(runtime, readinessSnapshot) {
|
|
24
|
+
if (readinessSnapshot && typeof readinessSnapshot === 'object') {
|
|
25
|
+
const identity = readinessSnapshot.runtime;
|
|
26
|
+
if (!isExactNativeCrewIdentity(identity)) {
|
|
27
|
+
return READINESS_STATES.UNAVAILABLE;
|
|
28
|
+
}
|
|
29
|
+
if (runtime && (!isCompleteRuntimeIdentity(runtime) || !sameCompleteRuntimeIdentity(runtime, identity))) {
|
|
30
|
+
return READINESS_STATES.UNKNOWN;
|
|
31
|
+
}
|
|
32
|
+
if (!Number.isFinite(readinessSnapshot.captured_at)) return READINESS_STATES.UNKNOWN;
|
|
33
|
+
if (readinessSnapshot.captured_at > Date.now() + 30_000) return READINESS_STATES.UNKNOWN;
|
|
34
|
+
if (Date.now() - readinessSnapshot.captured_at > MAX_RUNTIME_SNAPSHOT_AGE_MS) return READINESS_STATES.DEGRADED;
|
|
35
|
+
const row = Array.isArray(readinessSnapshot.readiness_matrix?.rows)
|
|
36
|
+
? readinessSnapshot.readiness_matrix.rows.find((entry) => entry?.id === 'hub_compatibility') : undefined;
|
|
37
|
+
if (row?.status === 'PASS') return READINESS_STATES.READY;
|
|
38
|
+
if (row?.status === 'FAIL') return READINESS_STATES.UNAVAILABLE;
|
|
39
|
+
if (row?.status === 'NOT_RUN' || row?.status === 'SKIP') return READINESS_STATES.DEGRADED;
|
|
40
|
+
return READINESS_STATES.UNKNOWN;
|
|
41
|
+
}
|
|
42
|
+
if (runtime === undefined) return READINESS_STATES.UNKNOWN;
|
|
43
|
+
if (runtime === null) return READINESS_STATES.UNAVAILABLE;
|
|
44
|
+
const exactIdentity = runtime?.ok === true && isExactNativeCrewIdentity(runtime);
|
|
45
|
+
return exactIdentity ? READINESS_STATES.READY : READINESS_STATES.UNKNOWN;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function bridgeState(surface) {
|
|
49
|
+
// The official 3080 quick surface is an OPTIONAL convenience panel. Its
|
|
50
|
+
// absence must never degrade Crew readiness: on the native 3210 control
|
|
51
|
+
// plane the bridge is simply not part of the picture.
|
|
52
|
+
if (surface === 'official-bridge') return READINESS_STATES.READY;
|
|
53
|
+
if (surface === 'native-crew-harness') return READINESS_STATES.NOT_APPLICABLE;
|
|
54
|
+
return READINESS_STATES.UNKNOWN;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Project only structured, non-secret installer/runtime evidence. */
|
|
58
|
+
export function projectHostReadiness({ installStatus, runtime, surface, readinessSnapshot } = {}) {
|
|
59
|
+
const codex = installStatus?.codex;
|
|
60
|
+
const claude = installStatus?.claude;
|
|
61
|
+
const zcode = installStatus?.zcode;
|
|
62
|
+
return [
|
|
63
|
+
{ id: 'codex_mcp', state: componentState(codex, ['mcp'], { aligned: true }) },
|
|
64
|
+
{ id: 'ds_worker', state: componentState(codex, ['worker_role'], { aligned: true }) },
|
|
65
|
+
{ id: 'ds_reviewer', state: componentState(codex, ['reviewer_role'], { aligned: true }) },
|
|
66
|
+
{ id: 'claude_plugin', state: componentState(claude, ['enabled', 'marketplace', 'snapshot', 'permissions']) },
|
|
67
|
+
{ id: 'zcode_mcp', state: componentState(zcode, ['mcp', 'policy', 'worker_agent', 'reviewer_agent', 'config_prompt', 'status_prompt', 'ownership']) },
|
|
68
|
+
{ id: 'crew_harness', state: runtimeState(runtime, readinessSnapshot), detail: runtime?.runtime_version ?? null },
|
|
69
|
+
{ id: 'official_bridge', state: bridgeState(surface) },
|
|
70
|
+
];
|
|
71
|
+
}
|