diff --git a/cmd/creator-web/chat_test.go b/cmd/creator-web/chat_test.go
index da67997..3bd0a9e 100644
--- a/cmd/creator-web/chat_test.go
+++ b/cmd/creator-web/chat_test.go
@@ -25,6 +25,11 @@ func TestChatPageServedWithoutInternalIDs(t *testing.T) {
t.Fatalf("expected /chat status 200, got %d", recorder.Code)
}
body := recorder.Body.String()
+ for _, required := range []string{"cognitive-status", "aria-live=\"polite\""} {
+ if !strings.Contains(body, required) {
+ t.Fatalf("/chat is missing human status marker %q", required)
+ }
+ }
for _, forbidden := range []string{"provider_id", "model_id", "binding_id", "workspace-id", "session-id"} {
if strings.Contains(body, forbidden) {
t.Fatalf("/chat leaks internal field %q", forbidden)
diff --git a/cmd/creator-web/static/assets/chat.css b/cmd/creator-web/static/assets/chat.css
index a1c36df..4a83344 100644
--- a/cmd/creator-web/static/assets/chat.css
+++ b/cmd/creator-web/static/assets/chat.css
@@ -34,6 +34,12 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
.chat-header a { color: var(--ink); text-decoration: none; font: 400 24px/1 var(--serif); }
.chat-header span { color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
#active-focus { margin-left: auto; color: var(--good); }
+.cognitive-status { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap; }
+.cognitive-status::before { content: ""; width: 7px; height: 7px; flex: 0 0 7px; border-radius: 50%; background: var(--signal); }
+.cognitive-status[data-state="available"] { color: var(--good); }
+.cognitive-status[data-state="available"]::before { background: var(--good); }
+.cognitive-status[data-state="unavailable"] { color: var(--bad); }
+.cognitive-status[data-state="unavailable"]::before { background: var(--bad); }
.chat-header button { width: 38px; height: 38px; border: 1px solid var(--line); background: transparent; }
.human-nav { display: flex; align-items: center; gap: 8px; }
.nav-tab, .operator-link { height: 34px; padding: 0 12px; display: inline-flex; align-items: center; border: 1px solid var(--line); color: var(--muted); background: #0b110e; text-decoration: none; font: 11px/1 var(--mono); text-transform: uppercase; letter-spacing: .08em; }
@@ -71,6 +77,7 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
.composer button { width: 100%; }
.chat-header { gap: 10px; padding: 0 12px; }
.chat-header span { display: none; }
+ .chat-header .cognitive-status { display: inline-flex; }
.human-nav { overflow-x: auto; }
.operator-link, .nav-tab { padding: 0 9px; }
}
diff --git a/cmd/creator-web/static/assets/chat.js b/cmd/creator-web/static/assets/chat.js
index 70c6dd1..f5613d1 100644
--- a/cmd/creator-web/static/assets/chat.js
+++ b/cmd/creator-web/static/assets/chat.js
@@ -8,6 +8,7 @@ const messageInput = document.querySelector("#message");
const sendButton = document.querySelector("#send");
const logoutButton = document.querySelector("#logout");
const activeFocus = document.querySelector("#active-focus");
+const cognitiveStatus = document.querySelector("#cognitive-status");
const todayList = document.querySelector("#today-list");
const refreshToday = document.querySelector("#refresh-today");
const historyList = document.querySelector("#history-list");
@@ -16,6 +17,44 @@ const navTabs = Array.from(document.querySelectorAll(".nav-tab"));
let token = sessionStorage.getItem("domovoy.chat.token") || "";
let conversationID = sessionStorage.getItem("domovoy.chat.conversation") || "";
let localHistory = [];
+let cognitiveStatusTimer = null;
+
+function renderCognitiveStatus(state) {
+ if (!cognitiveStatus) return;
+ const normalized = String(state || "UNKNOWN").toUpperCase();
+ let displayState = "degraded";
+ if (normalized === "AVAILABLE") displayState = "available";
+ if (normalized === "UNAVAILABLE") displayState = "unavailable";
+ const labels = {
+ available: "канал мышления доступен",
+ degraded: "канал мышления работает с ограничениями",
+ unavailable: "основной канал мышления недоступен",
+ };
+ cognitiveStatus.dataset.state = displayState;
+ cognitiveStatus.textContent = labels[displayState];
+}
+
+async function loadCognitiveStatus() {
+ if (!cognitiveStatus || !token) return;
+ try {
+ const result = await api("/api/capabilities/cognitive-proposal/health", {method: "GET"});
+ renderCognitiveStatus(result.State || result.state || "UNKNOWN");
+ } catch {
+ renderCognitiveStatus("UNAVAILABLE");
+ }
+}
+
+function startCognitiveStatusPolling() {
+ if (cognitiveStatusTimer) return;
+ loadCognitiveStatus();
+ cognitiveStatusTimer = window.setInterval(loadCognitiveStatus, 30000);
+}
+
+function stopCognitiveStatusPolling() {
+ if (!cognitiveStatusTimer) return;
+ window.clearInterval(cognitiveStatusTimer);
+ cognitiveStatusTimer = null;
+}
function showChat() {
loginView.hidden = true;
@@ -23,10 +62,12 @@ function showChat() {
if (!messages.children.length) {
appendMessage("assistant", "Я рядом. Напишите обычное сообщение, без внутренних идентификаторов.");
}
+ startCognitiveStatusPolling();
messageInput.focus();
}
function showLogin() {
+ stopCognitiveStatusPolling();
chatView.hidden = true;
loginView.hidden = false;
}
diff --git a/cmd/creator-web/static/chat.html b/cmd/creator-web/static/chat.html
index 08211d3..4457ba7 100644
--- a/cmd/creator-web/static/chat.html
+++ b/cmd/creator-web/static/chat.html
@@ -33,6 +33,7 @@
Консоль
фокус: не выбран
+ проверяю канал мышления
сессия активна
diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json
index 02bc6ad..c5c79de 100644
--- a/dev/ACTIVE_TASK.json
+++ b/dev/ACTIVE_TASK.json
@@ -1,5 +1,5 @@
{
- "task_file": "dev/tasks/DEV-000075-creator-chat-primary-surface.json",
- "task_id": "DEV-000075",
+ "task_file": "dev/tasks/DEV-000076-creator-chat-cognitive-status.json",
+ "task_id": "DEV-000076",
"task_revision": 1
}
diff --git a/dev/tasks/DEV-000076-creator-chat-cognitive-status.json b/dev/tasks/DEV-000076-creator-chat-cognitive-status.json
new file mode 100644
index 0000000..0c73f29
--- /dev/null
+++ b/dev/tasks/DEV-000076-creator-chat-cognitive-status.json
@@ -0,0 +1,47 @@
+{
+ "task_id": "DEV-000076",
+ "task_revision": 1,
+ "architecture_revision": "DEV-000073-baseline",
+ "base_repo_revision": "26cf85108a63dfbd67866d9df0445dbcb54983be",
+ "risk_tier": "E",
+ "intent": "Show the human user whether Domovoy's primary cognitive channel is available without exposing provider internals or mixing technical events into the conversation.",
+ "requirement_ids": [
+ "Chat shows a human-readable cognitive channel status",
+ "Status is read-only and refreshed periodically",
+ "Chat does not expose provider, model or binding identifiers",
+ "Chat remains usable while status observation fails"
+ ],
+ "allowed_change_scope": {
+ "modules": ["cmd/creator-web"],
+ "paths": [
+ "cmd/creator-web/chat_test.go",
+ "cmd/creator-web/static/chat.html",
+ "cmd/creator-web/static/assets/chat.css",
+ "cmd/creator-web/static/assets/chat.js",
+ "dev/ACTIVE_TASK.json",
+ "dev/tasks/DEV-000076-creator-chat-cognitive-status.json"
+ ]
+ },
+ "forbidden_change_scope": {
+ "paths": ["architecture/**", "security/**", "database/**", "modules/**", "contracts/**", "adapters/**", "deploy/**"]
+ },
+ "allowed_architecture_delta": {
+ "new_cross_module_dependencies": 0,
+ "new_canonical_owners": 0,
+ "new_privileges": 0,
+ "new_third_party_dependencies": 0,
+ "new_provider_adapters": 0,
+ "new_database_owner_roles": 0
+ },
+ "contracts": {"consume": ["existing authenticated /api/capabilities/cognitive-proposal/health endpoint"], "may_change": []},
+ "semantic_owner": "none-interface-adapter",
+ "canonical_state": [],
+ "state_transitions": ["Browser polls existing cognitive capability health observation", "Browser renders coarse human-readable status"],
+ "side_effects_and_privileges": ["Read-only observation", "No new privilege", "No provider/model routing change"],
+ "persistence_and_migrations": ["No persistence or migration"],
+ "failure_retry_restart": ["Failed observation renders an unavailable/degraded notice; chat remains available", "Polling retries on the next interval"],
+ "assumptions": ["The existing health endpoint returns State or state with AVAILABLE, DEGRADED or UNAVAILABLE semantics", "The chat token authorizes the existing health endpoint"],
+ "acceptance_scenarios": ["Authenticated chat displays a status indicator", "Health polling does not reveal internal identifiers", "Mobile header keeps the status visible", "A failed health request does not disable the composer"],
+ "required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "devctl check-architecture", "git diff --check", "node --check cmd/creator-web/static/assets/chat.js"],
+ "terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
+}