Compare commits

..
Author SHA1 Message Date
m 7eb2f5f313 feat: make chat the public starting surface
CI / test (push) Successful in 21s
CI / test (pull_request) Successful in 19s
2026-08-21 08:56:20 +03:00
m 39a27102c9 Merge pull request 'Turn cabinet sections into pages' (#75) from codex/creator-cabinet-learning-nav-v3 into main
CI / test (push) Successful in 18s
2026-08-21 02:45:28 +00:00
m 7ac9a2697c chore: align page task with main
CI / test (push) Successful in 19s
CI / test (pull_request) Successful in 19s
2026-08-21 05:44:33 +03:00
m f2e8ec08c5 feat: turn cabinet sections into pages 2026-08-21 05:44:19 +03:00
m 1cfbb4ae66 Merge pull request 'Expose learning in top navigation' (#74) from codex/creator-cabinet-learning-nav-v3 into main
CI / test (push) Successful in 19s
2026-08-21 02:29:16 +00:00
m 7618a0bcbe feat: expose learning in top navigation
CI / test (push) Successful in 19s
CI / test (pull_request) Successful in 19s
2026-08-21 05:27:58 +03:00
m dcdc9a2e64 Merge pull request 'Correct active section accessibility semantics' (#73) from codex/creator-cabinet-learning-nav-v3 into main
CI / test (push) Successful in 18s
2026-08-21 02:25:29 +00:00
m 5783faf9dc chore: align task base with merged main
CI / test (push) Successful in 18s
CI / test (pull_request) Successful in 18s
2026-08-21 05:24:05 +03:00
m 9a968fe2f6 style: format creator web test
CI / test (push) Successful in 19s
CI / test (pull_request) Successful in 18s
2026-08-21 05:23:43 +03:00
m 41d4d7202f test: cover location navigation semantics 2026-08-21 05:23:42 +03:00
m ed3932c57a fix: mark active cabinet location semantically 2026-08-21 05:23:42 +03:00
m 3367361483 Merge pull request 'Add learning section to cabinet navigation' (#72) from codex/creator-cabinet-learning-nav-v3 into main
CI / test (push) Successful in 19s
2026-08-21 02:18:16 +00:00
m 51638d6e8c feat: add learning section to cabinet navigation
CI / test (push) Successful in 18s
CI / test (pull_request) Successful in 18s
2026-08-21 05:17:08 +03:00
m 0a1ad9f7d5 Merge pull request 'Make Creator cabinet IDs easier to use' (#71) from codex/creator-cabinet-operations-v2 into main
CI / test (push) Successful in 18s
2026-08-21 02:13:09 +00:00
m 4d51e65226 feat: make creator cabinet ids easier to use
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 18s
2026-08-21 05:11:38 +03:00
27 changed files with 666 additions and 3140 deletions
-7
View File
@@ -35,13 +35,6 @@ Do not silently resolve a conflict. Return `BLOCKED_BY_SPEC_GAP` or `BLOCKED_BY_
- NO reading/copying literal passwords from other repositories into this repository.
- NO production secret in source, task packet, commit message, logs or tests.
## Deployment safety (production mutation)
- Production mutation in normal `devctl` workflow must go through `deploy prepare` then `deploy apply`.
- `deploy apply` is rejected when required inputs are `MISSING`, `EMPTY`, `INVALID`, `UNKNOWN`, `UNRESOLVED`, or `STALE`.
- Ambient shell state is not accepted as authoritative required production input; required values must come from an explicit durable source (for example `--env-file`/manifest-classified bootstrap path).
- A prepared plan must be current (inputs, compose, context, revision binding) before apply proceeds.
## Codex freedom
Inside the declared owner/module and allowed paths, choose ordinary local implementation details freely. Prefer explicit, typed, boring Go over reflection, metaprogramming or framework tricks.
-1
View File
@@ -19,7 +19,6 @@ These questions are intentionally unresolved. Codex must not silently turn tempo
| UQ-013 | Multi-user organization governance | LATER | Actor/Principal/Scope designed generally; expose single-user V1 if desired | No | Actual shared workspace requirement |
| UQ-014 | Canonical wire codec longevity | RESEARCH | Versioned schema/envelope; initial protobuf acceptable, not eternal identity | No | First non-Go module or major semantic migration |
| UQ-015 | Exact data retention/privacy policy engine | RESEARCH | Store scope/provenance/sensitivity hooks; implement concrete policies per product deployment | No | Product/legal requirements |
| UQ-016 | Generic memory/knowledge admission from ordinary interaction | RESEARCH | Treat ordinary preferences/preferences-like assertions as candidates first; no predefined per-topic memory schemas | No | When a durable knowledge/persistence mechanism is adopted with explicit policy and owner boundaries |
## 7.1 Spec-gap protocol
+23
View File
@@ -32,6 +32,29 @@ func TestChatPageServedWithoutInternalIDs(t *testing.T) {
}
}
func TestPublicSurfacesSeparateChatAndConsole(t *testing.T) {
handler, err := newHandler("http://core", "http://continuity", "http://commit", http.DefaultClient, "http://cognitive")
if err != nil {
t.Fatal(err)
}
for _, test := range []struct {
path string
must string
mustNot string
}{
{path: "/", must: "Напишите Домовому обычное сообщение", mustNot: "context-preset"},
{path: "/admin", must: "context-preset", mustNot: "Напишите Домовому обычное сообщение"},
{path: "/chat", must: "Напишите Домовому обычное сообщение", mustNot: "context-preset"},
} {
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, test.path, nil))
body := recorder.Body.String()
if recorder.Code != http.StatusOK || !strings.Contains(body, test.must) || strings.Contains(body, test.mustNot) {
t.Fatalf("%s response=%d contains expected=%t forbidden=%t", test.path, recorder.Code, strings.Contains(body, test.must), strings.Contains(body, test.mustNot))
}
}
}
func TestChatMessageUsesCoreAndLocalOnlyCognitiveGateway(t *testing.T) {
var cognitiveRequest cognitive.GatewayAdvisoryRequest
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+17 -1
View File
@@ -112,7 +112,12 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
if r.URL.Path == "/" {
w.Header().Set("Cache-Control", "no-store")
serveEmbeddedHTML(w, "chat.html")
return
}
if r.URL.Path == "/admin" {
serveEmbeddedHTML(w, "index.html")
return
}
if r.URL.Path == "/chat" {
r = r.Clone(r.Context())
@@ -122,6 +127,17 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
g.static.ServeHTTP(w, r)
}
func serveEmbeddedHTML(w http.ResponseWriter, name string) {
body, err := staticFiles.ReadFile("static/" + name)
if err != nil {
http.Error(w, "page unavailable", http.StatusNotFound)
return
}
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write(body)
}
type browserAdvisoryRequest struct {
Message string `json:"message"`
SessionID string `json:"session_id"`
+12 -1
View File
@@ -229,10 +229,21 @@ func TestConsoleAndSecurityHeaders(t *testing.T) {
if err != nil {
t.Fatal(err)
}
request := httptest.NewRequest(http.MethodGet, "/", nil)
request := httptest.NewRequest(http.MethodGet, "/admin", nil)
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "Домовой") ||
!strings.Contains(response.Body.String(), "context-preset") ||
!strings.Contains(response.Body.String(), "context-state") ||
!strings.Contains(response.Body.String(), "copy-context") ||
!strings.Contains(response.Body.String(), "К содержимому") ||
!strings.Contains(response.Body.String(), "aria-current=\"location\"") ||
!strings.Contains(response.Body.String(), "section-nav-link") ||
!strings.Contains(response.Body.String(), "data-page=\"overview\"") ||
!strings.Contains(response.Body.String(), "data-page=\"diagnostics\"") ||
!strings.Contains(response.Body.String(), "data-page=\"operations\"") ||
!strings.Contains(response.Body.String(), "data-page=\"learning\"") ||
!strings.Contains(response.Body.String(), "data-page=\"settings\"") ||
!strings.Contains(response.Body.String(), "satisfaction-dialog") || !strings.Contains(response.Body.String(), "Подтверждаю переход") ||
!strings.Contains(response.Body.String(), "action-history") || !strings.Contains(response.Body.String(), "Журнал действий") ||
!strings.Contains(response.Body.String(), "advisory-form") || !strings.Contains(response.Body.String(), "Спросить Домового") ||
+111 -14
View File
@@ -1,12 +1,12 @@
:root {
--ink: #e9eee7;
--muted: #8c9a91;
--faint: #58645d;
--base: #0b100e;
--panel: #101714;
--panel-high: #151e1a;
--line: #2a352f;
--line-soft: #1d2822;
--ink: #eef2ec;
--muted: #a7b2aa;
--faint: #718078;
--base: #0f1512;
--panel: #17201b;
--panel-high: #202b25;
--line: #34423a;
--line-soft: #26332c;
--signal: #c9a85f;
--signal-bright: #e7c97b;
--good: #78c6a3;
@@ -21,13 +21,15 @@
* { box-sizing: border-box; }
[hidden] { display: none !important; }
html { background: var(--base); color: var(--ink); font-family: var(--sans); }
body { margin: 0; min-width: 320px; min-height: 100vh; background: radial-gradient(circle at 73% -10%, rgba(83, 123, 101, .15), transparent 38%), var(--base); }
html { background: var(--base); color: var(--ink); font-family: var(--sans); scroll-behavior: smooth; }
body { margin: 0; min-width: 320px; min-height: 100vh; background: radial-gradient(circle at 73% -10%, rgba(83, 123, 101, .13), transparent 38%), var(--base); }
button, input, textarea { font: inherit; }
button { color: inherit; }
button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px solid var(--signal-bright); outline-offset: 3px; }
button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible { outline: 2px solid var(--signal-bright); outline-offset: 3px; }
.skip-link { position: fixed; top: 10px; left: 10px; z-index: 30; padding: 10px 13px; color: #11150f; background: var(--signal-bright); font: 600 10px/1 var(--mono); text-decoration: none; transform: translateY(-160%); transition: transform .18s; }
.skip-link:focus { transform: translateY(0); }
.grain { position: fixed; inset: 0; pointer-events: none; opacity: .18; z-index: 10; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 160 160' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E"); }
.grain { position: fixed; inset: 0; pointer-events: none; opacity: .045; z-index: 10; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 160 160' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E"); }
.eyebrow, .rail-index { margin: 0 0 12px; color: var(--signal); font: 600 11px/1 var(--mono); letter-spacing: .18em; text-transform: uppercase; }
.login-shell { min-height: 100vh; display: grid; place-items: center; padding: 32px 20px; position: relative; overflow: hidden; }
@@ -63,6 +65,9 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
.wordmark strong { font: 400 20px/1 var(--serif); }
.wordmark small { margin-top: 4px; color: var(--muted); font: 9px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
.topbar-actions { display: flex; align-items: center; gap: 10px; }
.topbar-nav { display: flex; align-items: center; gap: 4px; margin-right: 8px; }
.topbar-nav a { padding: 9px 10px; color: var(--muted); text-decoration: none; font: 600 9px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
.topbar-nav a:hover, .topbar-nav a:focus-visible { color: var(--ink); background: var(--panel-high); outline: 0; }
.connection { margin-right: 12px; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
.connection i { width: 7px; height: 7px; display: inline-block; margin-right: 8px; border-radius: 50%; background: var(--good); box-shadow: 0 0 14px rgba(120, 198, 163, .6); }
.connection.offline i { background: var(--bad); box-shadow: none; }
@@ -73,17 +78,55 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
.context-rail h2 { margin: 0; font: 400 29px/1.1 var(--serif); }
.rail-copy { margin: 14px 0 0; color: var(--muted); font-size: 12px; line-height: 1.5; }
.context-form { display: grid; }
.context-status-row { margin-bottom: 16px; padding: 10px 11px; display: flex; align-items: center; justify-content: space-between; gap: 10px; border: 1px solid var(--line-soft); background: rgba(120,198,163,.04); }
.context-status-label { color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .1em; }
.context-status-row strong { color: var(--warn); font: 600 9px/1 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
.context-status-row strong[data-state="ready"] { color: var(--good); }
.context-presets { display: grid; gap: 7px; margin-bottom: 14px; padding-bottom: 14px; border-bottom: 1px solid var(--line-soft); }
.context-presets label { margin: 0; }
.context-presets select { width: 100%; min-height: 38px; padding: 0 9px; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); background: #0b110e; font: 10px/1 var(--mono); }
.context-presets small { color: var(--faint); font-size: 10px; line-height: 1.35; }
.context-field { display: grid; gap: 7px; }
.context-field + .context-field { margin-top: 13px; }
.context-field label { margin: 0; }
.context-field label span { color: var(--signal); font-size: 8px; letter-spacing: .06em; }
.context-input-row { display: grid; grid-template-columns: minmax(0, 1fr) 34px; gap: 5px; }
.context-input-row input { min-width: 0; }
.copy-context { width: 34px; height: 40px; border: 1px solid var(--line); border-radius: var(--radius); color: var(--muted); background: transparent; cursor: pointer; }
.copy-context:hover { color: var(--ink); border-color: var(--faint); background: var(--panel-high); }
.id-actions { min-height: 31px; display: flex; align-items: center; gap: 7px; margin-top: 12px; }
.id-action { min-height: 30px; padding: 0 9px; border: 1px solid var(--line-soft); border-radius: var(--radius); color: var(--muted); background: transparent; cursor: pointer; font: 9px/1 var(--mono); letter-spacing: .04em; }
.id-action:hover { color: var(--ink); border-color: var(--signal); background: rgba(201,168,95,.07); }
.icon-id-action { width: 30px; padding: 0; color: var(--faint); }
.context-form label { margin-top: 15px; }
.context-form label:first-child { margin-top: 0; }
.context-form input { height: 40px; padding: 0 10px; font: 11px/1 var(--mono); }
.context-hint { margin: 12px 0 0; color: var(--faint); font-size: 10px; line-height: 1.45; }
.advanced-context { margin-top: 17px; border-top: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); }
.advanced-context summary { padding: 12px 0; color: var(--muted); cursor: pointer; list-style: none; font: 600 9px/1.2 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
.advanced-context summary::before { content: "+"; display: inline-block; width: 18px; color: var(--signal); font-size: 13px; }
.advanced-context[open] summary::before { content: "−"; }
.advanced-context > div { padding: 0 0 10px; }
.advanced-context label { margin-top: 13px; }
.context-actions { display: grid; gap: 8px; }
.context-actions .secondary-button { margin-top: 18px; }
.context-actions .text-button { margin: 0; justify-self: start; padding-inline: 0; }
.secondary-button { height: 42px; margin-top: 20px; color: #11150f; background: var(--signal); font-size: 12px; }
.section-nav { display: grid; gap: 3px; margin-top: 4px; }
.section-nav-link { display: grid; grid-template-columns: 28px 1fr; gap: 2px 10px; padding: 11px 10px; border-left: 2px solid transparent; color: var(--muted); text-decoration: none; transition: background .18s, border-color .18s, color .18s; }
.section-nav-link:hover, .section-nav-link:focus-visible, .section-nav-link.active, .section-nav-link[aria-current="location"] { border-left-color: var(--signal); color: var(--ink); background: rgba(201,168,95,.07); outline: 0; }
.section-nav-link span { grid-row: 1 / span 2; color: var(--signal); font: 9px/1.4 var(--mono); }
.section-nav-link b { font: 600 11px/1.2 var(--sans); }
.section-nav-link small { color: var(--faint); font: 9px/1.2 var(--mono); }
.session-stamp { margin-top: auto; padding-top: 20px; display: grid; gap: 6px; border-top: 1px solid var(--line); }
.session-stamp span { margin-top: 8px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .14em; }
.session-stamp strong { overflow: hidden; color: var(--muted); font: 10px/1.4 var(--mono); text-overflow: ellipsis; }
.workspace { grid-column: 2; grid-row: 2; padding: clamp(28px, 4vw, 58px); min-width: 0; }
.workspace { grid-column: 2; grid-row: 2; width: min(100%, 1740px); justify-self: start; padding: 42px clamp(24px, 3vw, 48px) 64px; min-width: 0; }
.workspace > [data-page][hidden] { display: none !important; }
.workspace > section { scroll-margin-top: 94px; }
.workspace-head { display: flex; align-items: end; justify-content: space-between; gap: 24px; margin-bottom: 34px; }
.workspace-head h1 { margin: 0; font: 400 clamp(36px, 5vw, 58px)/1 var(--serif); letter-spacing: -.035em; }
.workspace-head h1 { margin: 0; font: 400 clamp(34px, 4vw, 50px)/1 var(--serif); letter-spacing: -.035em; }
.last-update { color: var(--faint); font: 10px/1.4 var(--mono); text-align: right; }
.signal-strip { display: grid; grid-template-columns: repeat(4, 1fr); margin-bottom: 18px; border: 1px solid var(--line); background: var(--panel); }
.signal-strip div { padding: 16px 18px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border-right: 1px solid var(--line); }
@@ -94,6 +137,46 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
.signal-strip strong[data-state="error"] { color: var(--bad); }
.signal-strip strong[data-state="loading"] { color: var(--warn); animation: pulse 1.1s ease-in-out infinite; }
.diagnostics-panel, .settings-panel { margin-bottom: 18px; border: 1px solid var(--line); background: rgba(16,23,20,.82); }
.diagnostics-panel { border-color: rgba(120,198,163,.32); box-shadow: inset 3px 0 0 rgba(120,198,163,.45); }
.panel-heading { min-height: 68px; padding: 14px 18px; display: flex; align-items: center; justify-content: space-between; gap: 18px; border-bottom: 1px solid var(--line); }
.panel-heading p { margin: 0 0 6px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .13em; text-transform: uppercase; }
.panel-heading h2 { margin: 0; font: 400 23px/1 var(--serif); }
.panel-heading-tools { display: flex; align-items: center; gap: 14px; }
.panel-heading-tools > span, .settings-saved { color: var(--muted); font: 9px/1.3 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
.compact-button { width: auto; min-width: 142px; height: 38px; margin: 0; padding: 0 13px; }
.diagnostic-intro { padding: 13px 18px; display: flex; align-items: center; gap: 10px; border-bottom: 1px solid var(--line-soft); color: var(--muted); }
.diagnostic-intro p { margin: 0; font-size: 11px; line-height: 1.45; }
.diagnostic-pulse { width: 7px; height: 7px; flex: 0 0 auto; border-radius: 50%; background: var(--good); box-shadow: 0 0 12px rgba(120,198,163,.7); }
.diagnostic-list { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); }
.diagnostic-list[aria-busy="true"] { opacity: .7; }
.diagnostic-item { min-width: 0; min-height: 92px; padding: 14px 16px; display: grid; grid-template-columns: 9px 1fr auto; gap: 9px; align-items: start; border-right: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); }
.diagnostic-item:nth-child(4n) { border-right: 0; }
.diagnostic-item > i { width: 7px; height: 7px; margin-top: 4px; border-radius: 50%; background: var(--faint); }
.diagnostic-item[data-state="ok"] > i { background: var(--good); box-shadow: 0 0 9px rgba(120,198,163,.6); }
.diagnostic-item[data-state="error"] > i { background: var(--bad); }
.diagnostic-item[data-state="loading"] > i { background: var(--warn); animation: pulse 1.1s ease-in-out infinite; }
.diagnostic-item strong { display: block; color: var(--ink); font: 600 10px/1.3 var(--mono); }
.diagnostic-item span { display: block; margin-top: 6px; color: var(--muted); font-size: 10px; line-height: 1.35; }
.diagnostic-item em { color: var(--faint); font: normal 9px/1.2 var(--mono); text-transform: uppercase; }
.diagnostic-item[data-state="ok"] em { color: var(--good); }
.diagnostic-item[data-state="error"] em { color: var(--bad); }
.settings-grid { padding: 18px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 20px; }
.setting-field, .setting-check { min-width: 0; display: grid; gap: 7px; color: var(--ink); }
.setting-field > span, .setting-check b { font: 600 10px/1.2 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
.setting-field small, .setting-check small { color: var(--muted); font-size: 11px; line-height: 1.4; }
.setting-field select { width: 100%; min-height: 40px; padding: 0 10px; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); background: #0b110e; font: 11px/1 var(--mono); }
.setting-check { grid-template-columns: 18px 1fr; align-items: start; gap: 10px; padding: 10px 12px; border: 1px solid var(--line-soft); }
.setting-check input { width: 16px; height: 16px; margin: 2px 0 0; accent-color: var(--signal); }
.setting-check span { display: grid; gap: 7px; }
.settings-actions { display: flex; align-items: center; justify-content: flex-end; gap: 14px; grid-column: 1 / -1; padding-top: 2px; }
.settings-actions .text-button { margin: 0; }
html[data-density="compact"] .data-card { padding: 18px; }
html[data-density="compact"] .data-card dl div { padding-block: 10px; }
html[data-density="compact"] .activity-row { min-height: 58px; padding-block: 8px; }
html[data-density="compact"] .learning-row { min-height: 54px; padding-block: 9px; }
html[data-reduced-motion="true"] *, html[data-reduced-motion="true"] *::before, html[data-reduced-motion="true"] *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; transition-duration: .01ms !important; }
.advisor-panel { margin-bottom: 18px; padding: 24px; display: grid; grid-template-columns: minmax(220px, .72fr) minmax(320px, 1.28fr); gap: 18px 34px; border: 1px solid rgba(201,168,95,.42); background: linear-gradient(125deg, rgba(201,168,95,.1), rgba(16,23,20,.88) 44%, rgba(12,20,17,.96)); box-shadow: inset 3px 0 0 rgba(201,168,95,.58); }
.advisor-intro { align-self: center; }
.advisor-intro > p { margin: 0 0 10px; color: var(--signal); font: 600 9px/1 var(--mono); letter-spacing: .15em; }
@@ -278,6 +361,9 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
.advisor-panel { grid-template-columns: 1fr; }
.capability-card { grid-column: auto; }
.prospective-card { grid-column: auto; }
.diagnostic-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.diagnostic-item:nth-child(4n) { border-right: 1px solid var(--line-soft); }
.diagnostic-item:nth-child(2n) { border-right: 0; }
}
@media (max-width: 680px) {
@@ -285,14 +371,25 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
.console { display: block; }
.topbar { height: 66px; padding: 0 14px; }
.wordmark small, .connection, .text-button { display: none; }
.topbar-nav { display: none; }
.context-rail { padding: 24px 18px; border-right: 0; border-bottom: 1px solid var(--line); }
.rail-copy { max-width: 48ch; }
.session-stamp { margin-top: 8px; grid-template-columns: auto 1fr; align-items: center; }
.session-stamp span { margin: 0; }
.workspace { padding: 30px 16px 50px; }
.section-nav { display: flex; gap: 6px; margin: 10px -18px -24px; padding: 10px 18px; overflow-x: auto; border-top: 1px solid var(--line-soft); background: #0e1512; scrollbar-width: thin; }
.section-nav-link { min-width: 142px; padding: 10px; border-left: 0; border-bottom: 2px solid transparent; }
.section-nav-link:hover, .section-nav-link:focus-visible, .section-nav-link.active, .section-nav-link[aria-current="location"] { border-left-color: transparent; border-bottom-color: var(--signal); }
.workspace-head { align-items: start; flex-direction: column; }
.last-update { text-align: left; }
.signal-strip { grid-template-columns: 1fr; }
.panel-heading { align-items: flex-start; flex-direction: column; }
.panel-heading-tools { width: 100%; justify-content: space-between; }
.diagnostic-list { grid-template-columns: 1fr; }
.diagnostic-item, .diagnostic-item:nth-child(2n), .diagnostic-item:nth-child(4n) { border-right: 0; }
.settings-grid { grid-template-columns: 1fr; padding: 16px; }
.settings-actions { grid-column: auto; align-items: stretch; flex-direction: column; }
.settings-actions .primary-button { width: 100%; }
.signal-strip div { border-right: 0; border-bottom: 1px solid var(--line); }
.advisor-panel { padding: 20px 16px; gap: 20px; }
.advisor-controls { align-items: stretch; flex-direction: column; }
+270 -16
View File
@@ -7,14 +7,42 @@ const advisoryForm = byId("advisory-form");
const loginError = byId("login-error");
const toast = byId("toast");
const contextFields = ["session-id", "workspace-id", "task-id", "prospective-id", "capability-id"];
const settingsStorageKey = "domovoy.creator.settings";
const contextPresetsStorageKey = "domovoy.creator.context.presets";
let sessionToken = sessionStorage.getItem("domovoy.creator.session") || "";
let toastTimer;
let refreshTimer;
let currentTask = null;
let currentProspective = null;
let pendingCommand = null;
let pendingNotification = null;
let lastAuthorizedActionID = "";
let advisoryPolicy = {Mode: "deterministic", ExternalEgress: false, AcknowledgementRequired: false};
let uiSettings = {density: "comfortable", refresh: "off", motion: false};
const pageNavLinks = Array.from(document.querySelectorAll(".section-nav-link, .topbar-nav a"));
const pageViews = Array.from(document.querySelectorAll("[data-page]"));
const cabinetPages = ["overview", "diagnostics", "operations", "learning", "settings"];
function setActivePage(id, updateHistory = false) {
const page = cabinetPages.includes(id) ? id : "overview";
pageViews.forEach((view) => { view.hidden = view.dataset.page !== page; });
document.body.dataset.page = page;
pageNavLinks.forEach((link) => {
const active = link.getAttribute("href") === `#${page}`;
link.classList.toggle("active", active);
if (active) link.setAttribute("aria-current", "location");
else link.removeAttribute("aria-current");
});
if (updateHistory && window.location.hash !== `#${page}`) window.history.pushState({}, "", `#${page}`);
}
pageNavLinks.forEach((link) => link.addEventListener("click", (event) => {
event.preventDefault();
setActivePage(link.getAttribute("href").slice(1), true);
}));
window.addEventListener("popstate", () => setActivePage(window.location.hash.slice(1)));
setActivePage(window.location.hash.slice(1));
function setText(id, value, fallback = "—") {
byId(id).textContent = value === undefined || value === null || value === "" ? fallback : String(value);
@@ -39,6 +67,18 @@ function notify(message) {
toastTimer = setTimeout(() => { toast.hidden = true; }, 4200);
}
function safeClientError(error) {
const message = error && typeof error.message === "string" ? error.message : "";
switch (message) {
case "creator authentication denied":
return "Логин или пароль не принят.";
case "Сессия отклонена":
return "Сессия истекла. Войдите снова.";
default:
return "Операция недоступна. Попробуйте ещё раз.";
}
}
async function request(path, options = {}) {
const headers = new Headers(options.headers || {});
if (sessionToken) headers.set("Authorization", `Bearer ${sessionToken}`);
@@ -50,7 +90,9 @@ async function request(path, options = {}) {
}
if (!response.ok) {
const detail = (await response.text()).trim();
throw new Error(detail || `Ошибка ${response.status}`);
const error = new Error(detail || `Ошибка ${response.status}`);
error.status = response.status;
throw error;
}
return response.json();
}
@@ -64,15 +106,57 @@ function restoreContext() {
} catch (_) {
localStorage.removeItem("domovoy.creator.context");
}
renderContextState();
renderContextPresets();
}
function saveContext() {
const saved = {};
contextFields.forEach((id) => { saved[id] = byId(id).value.trim(); });
localStorage.setItem("domovoy.creator.context", JSON.stringify(saved));
renderContextState();
return saved;
}
function readContextPresets() {
try {
const value = JSON.parse(localStorage.getItem(contextPresetsStorageKey) || "[]");
return Array.isArray(value) ? value.filter((item) => item && item.session && item.workspace) : [];
} catch (_) {
localStorage.removeItem(contextPresetsStorageKey);
return [];
}
}
function renderContextState() {
const state = byId("context-state");
if (!state) return;
const ready = Boolean(byId("session-id").value.trim() && byId("workspace-id").value.trim());
state.textContent = ready ? "готов к проверке" : "не выбран";
state.dataset.state = ready ? "ready" : "empty";
}
function renderContextPresets() {
const select = byId("context-preset");
if (!select) return;
const presets = readContextPresets();
select.replaceChildren(new Option("Выберите сохранённый набор…", ""));
presets.forEach((preset, index) => {
const session = preset.session.length > 18 ? `${preset.session.slice(0, 8)}…${preset.session.slice(-6)}` : preset.session;
const workspace = preset.workspace.length > 18 ? `${preset.workspace.slice(0, 8)}…${preset.workspace.slice(-6)}` : preset.workspace;
select.appendChild(new Option(`${session} · ${workspace}`, String(index)));
});
}
function rememberContextPreset(context) {
if (!context["session-id"] || !context["workspace-id"]) return;
const next = {session: context["session-id"], workspace: context["workspace-id"], savedAt: Date.now()};
const presets = readContextPresets().filter((item) => item.session !== next.session || item.workspace !== next.workspace);
presets.unshift(next);
localStorage.setItem(contextPresetsStorageKey, JSON.stringify(presets.slice(0, 5)));
renderContextPresets();
}
async function showConsole() {
const session = await request("/api/session/current");
setText("session-actor", session.ActorID);
@@ -80,6 +164,7 @@ async function showConsole() {
setText("session-expiry", expiry);
loginView.hidden = true;
consoleView.hidden = false;
restoreSettings();
restoreContext();
await loadAdvisoryPolicy();
const context = saveContext();
@@ -88,10 +173,13 @@ async function showConsole() {
loadActionHistory();
loadLearning();
}
runDiagnostics();
}
function endSession(message = "Сессия завершена.") {
sessionToken = "";
clearInterval(refreshTimer);
refreshTimer = null;
sessionStorage.removeItem("domovoy.creator.session");
consoleView.hidden = true;
loginView.hidden = false;
@@ -128,7 +216,7 @@ loginForm.addEventListener("submit", async (event) => {
byId("creator-password").value = "";
await showConsole();
} catch (error) {
loginError.textContent = error.message === "creator authentication denied" ? "Логин или пароль не принят." : error.message;
loginError.textContent = safeClientError(error);
} finally {
button.disabled = false;
button.firstElementChild.textContent = "Открыть консоль";
@@ -144,14 +232,129 @@ byId("toggle-secret").addEventListener("click", () => {
contextForm.addEventListener("submit", (event) => {
event.preventDefault();
saveContext();
const context = saveContext();
rememberContextPreset(context);
loadDashboard();
});
for (const input of [byId("session-id"), byId("workspace-id")]) {
input.addEventListener("input", renderContextState);
}
byId("context-preset").addEventListener("change", (event) => {
const index = Number(event.target.value);
if (!Number.isInteger(index)) return;
const preset = readContextPresets()[index];
if (!preset) return;
byId("session-id").value = preset.session;
byId("workspace-id").value = preset.workspace;
renderContextState();
loadDashboard();
notify("Контекст из недавнего набора загружен.");
});
for (const button of document.querySelectorAll("[data-copy-context]")) {
button.addEventListener("click", async () => {
const value = byId(button.dataset.copyContext).value.trim();
if (!value) {
notify("Сначала укажите идентификатор.");
return;
}
try {
await navigator.clipboard.writeText(value);
notify("Идентификатор скопирован.");
} catch (_) {
notify("Не удалось скопировать идентификатор.");
}
});
}
for (const button of document.querySelectorAll("[data-copy-value], [data-use-value]")) {
button.addEventListener("click", async () => {
const source = byId(button.dataset.copyValue || button.dataset.useValue);
const value = source?.textContent.trim() || "";
if (!value || value === "—") {
notify("Сначала загрузите значение.");
return;
}
if (button.dataset.useValue) {
const target = byId(button.dataset.targetId);
if (!target) return;
target.value = value;
document.querySelector(".advanced-context").open = true;
saveContext();
notify("ID подставлен в расширенную проверку.");
return;
}
try {
await navigator.clipboard.writeText(value);
notify("ID скопирован.");
} catch (_) {
notify("Не удалось скопировать ID.");
}
});
}
byId("reset-context").addEventListener("click", () => {
contextFields.forEach((id) => { byId(id).value = id === "capability-id" ? "cognitive-proposal" : ""; });
localStorage.removeItem("domovoy.creator.context");
loadDashboard();
notify("Рабочий контекст сброшен.");
});
function restoreSettings() {
try {
const saved = JSON.parse(localStorage.getItem(settingsStorageKey) || "{}");
uiSettings = {
density: ["comfortable", "compact"].includes(saved.density) ? saved.density : "comfortable",
refresh: ["off", "30", "60", "300"].includes(saved.refresh) ? saved.refresh : "off",
motion: saved.motion === true,
};
} catch (_) {
uiSettings = {density: "comfortable", refresh: "off", motion: false};
}
byId("setting-density").value = uiSettings.density;
byId("setting-refresh").value = uiSettings.refresh;
byId("setting-motion").checked = uiSettings.motion;
applySettings();
}
function applySettings() {
document.documentElement.dataset.density = uiSettings.density;
document.documentElement.dataset.reducedMotion = uiSettings.motion ? "true" : "false";
clearInterval(refreshTimer);
refreshTimer = null;
if (sessionToken && uiSettings.refresh !== "off") {
refreshTimer = setInterval(() => loadDashboard(), Number(uiSettings.refresh) * 1000);
}
}
byId("settings-form").addEventListener("submit", (event) => {
event.preventDefault();
uiSettings = {
density: byId("setting-density").value,
refresh: byId("setting-refresh").value,
motion: byId("setting-motion").checked,
};
localStorage.setItem(settingsStorageKey, JSON.stringify(uiSettings));
applySettings();
setText("settings-saved", "Сохранено только в этом браузере");
notify("Настройки кабинета сохранены.");
});
byId("reset-settings").addEventListener("click", () => {
uiSettings = {density: "comfortable", refresh: "off", motion: false};
localStorage.removeItem(settingsStorageKey);
restoreSettings();
setText("settings-saved", "Восстановлены значения по умолчанию");
notify("Настройки сброшены.");
});
byId("refresh-all").addEventListener("click", loadDashboard);
byId("refresh-actions").addEventListener("click", loadActionHistory);
byId("refresh-learning").addEventListener("click", loadLearning);
byId("logout").addEventListener("click", () => endSession());
byId("run-diagnostics").addEventListener("click", runDiagnostics);
advisoryForm.addEventListener("submit", async (event) => {
event.preventDefault();
@@ -185,7 +388,7 @@ advisoryForm.addEventListener("submit", async (event) => {
byId("advisory-result").hidden = false;
} catch (error) {
setText("advisory-state", "ОШИБКА");
setText("advisory-text", error.message);
setText("advisory-text", safeClientError(error));
byId("advisory-result").hidden = false;
} finally {
byId("advisory-egress-ack").checked = false;
@@ -246,6 +449,57 @@ async function loadDashboard() {
setText("last-update", `Обновлено ${new Intl.DateTimeFormat("ru-RU", {hour: "2-digit", minute: "2-digit", second: "2-digit"}).format(new Date())}`);
}
async function runDiagnostics() {
const list = byId("diagnostic-list");
const summary = byId("diagnostic-summary");
const checks = [
["Сессия", "identity", () => request("/api/session/current")],
["Политика совета", "advisory", () => request("/api/advisory-policy")],
["Когнитивная возможность", "capability", () => request("/api/capabilities/cognitive-proposal/health")],
["Сегодня", "today", () => request("/api/today")],
["Внимание", "attention", () => request("/api/attention")],
["Действия", "actions", () => request("/api/actions?limit=1")],
["Learning evidence", "learning", () => request("/api/learning/recent?limit=1")],
];
list.setAttribute("aria-busy", "true");
list.replaceChildren();
checks.forEach(([label, key]) => list.append(createDiagnosticItem(label, key, "loading")));
setText("diagnostic-summary", "Проверяю…");
const results = await Promise.all(checks.map(async ([label, key, run]) => {
const started = performance.now();
try {
await run();
return {label, key, state: "ok", detail: `Ответ получен · ${Math.round(performance.now() - started)} мс`};
} catch (error) {
const detail = error?.status === 400 ? "Нужны параметры рабочего контекста" : error?.status === 401 ? "Сессия истекла" : error?.status >= 500 ? "Сервис временно недоступен" : "Проверка не пройдена";
return {label, key, state: "error", detail};
}
}));
list.replaceChildren(...results.map((result) => createDiagnosticItem(result.label, result.key, result.state, result.detail)));
list.setAttribute("aria-busy", "false");
const passed = results.filter((result) => result.state === "ok").length;
setText("diagnostic-summary", `${passed}/${results.length} проверок пройдено`);
}
function createDiagnosticItem(label, key, state, detail = "Проверяю…") {
const item = document.createElement("article");
item.className = "diagnostic-item";
item.dataset.state = state;
item.dataset.check = key;
const marker = document.createElement("i");
marker.setAttribute("aria-hidden", "true");
const copy = document.createElement("div");
const title = document.createElement("strong");
title.textContent = label;
const description = document.createElement("span");
description.textContent = detail;
copy.append(title, description);
const status = document.createElement("em");
status.textContent = state === "ok" ? "OK" : state === "error" ? "ошибка" : "…";
item.append(marker, copy, status);
return item;
}
async function loadLearning() {
const experienceList = byId("experience-list");
const reflectionList = byId("reflection-list");
@@ -272,7 +526,7 @@ async function loadLearning() {
list.replaceChildren();
const failed = document.createElement("p");
failed.className = "register-empty activity-error";
failed.textContent = error.message;
failed.textContent = safeClientError(error);
list.append(failed);
});
setText("learning-count", "ОШИБКА");
@@ -371,7 +625,7 @@ async function loadActionHistory() {
list.replaceChildren();
const failed = document.createElement("p");
failed.className = "register-empty activity-error";
failed.textContent = error.message;
failed.textContent = safeClientError(error);
list.append(failed);
setText("action-count", "ОШИБКА");
} finally {
@@ -430,7 +684,7 @@ async function reconcileHistoryAction(actionID, button) {
await loadActionHistory();
if (currentProspective) await loadProspectiveRegister(currentProspective.ID);
} catch (error) {
notify(error.message);
notify(safeClientError(error));
button.disabled = false;
button.textContent = "Сверить";
}
@@ -450,7 +704,7 @@ async function loadFocus(query) {
} catch (error) {
setSignal("focus", "ошибка", "error");
setBadge("focus", "НЕДОСТУПЕН", "bad");
setText("focus-message", error.message);
setText("focus-message", safeClientError(error));
byId("connection-state").classList.add("offline");
}
}
@@ -472,7 +726,7 @@ async function loadTask(taskID, query) {
currentTask = null;
setSignal("task", "отклонена", "error");
setBadge("task", "ВНЕ ФОКУСА", "bad");
setText("task-message", error.message);
setText("task-message", safeClientError(error));
updateActionReadiness();
}
}
@@ -511,12 +765,12 @@ async function loadProspectiveRegister(selectedID) {
list.replaceChildren();
const failed = document.createElement("p");
failed.className = "register-empty";
failed.textContent = error.message;
failed.textContent = safeClientError(error);
list.append(failed);
setText("prospective-count", "ОШИБКА");
setSignal("prospective", "ошибка", "error");
setBadge("prospective", "НЕДОСТУПНА", "bad");
setText("prospective-message", error.message);
setText("prospective-message", safeClientError(error));
}
}
@@ -572,7 +826,7 @@ async function loadProspective(prospectiveID) {
setSignal("prospective", "ошибка", "error");
setBadge("prospective", "НЕДОСТУПНА", "bad");
setText("prospective-state", "UNKNOWN");
setText("prospective-message", error.message);
setText("prospective-message", safeClientError(error));
updateActionReadiness();
}
}
@@ -594,7 +848,7 @@ async function loadCapability(capabilityID) {
setSignal("capability", "ошибка", "error");
setBadge("capability", "НЕДОСТУПНА", "bad");
setText("capability-state", "UNKNOWN");
setText("capability-reason", error.message);
setText("capability-reason", safeClientError(error));
}
}
@@ -700,7 +954,7 @@ byId("confirm-notification").addEventListener("click", async () => {
await loadActionHistory();
notify(result.State === "SUCCEEDED" ? "Receiver принял уведомление." : "Исход отправки требует сверки.");
} catch (error) {
setText("notification-confirmation-error", error.message);
setText("notification-confirmation-error", safeClientError(error));
} finally {
button.disabled = false;
button.textContent = "Подтверждаю отправку";
@@ -730,7 +984,7 @@ byId("confirm-satisfaction").addEventListener("click", async () => {
notify(result.State === "SUCCEEDED" ? "Исполнение зафиксировано через Commit." : "Исход требует проверки.");
await loadProspectiveRegister(command.ProspectiveItemID);
} catch (error) {
setText("confirmation-error", error.message);
setText("confirmation-error", safeClientError(error));
} finally {
button.disabled = false;
button.textContent = "Подтверждаю переход";
@@ -764,7 +1018,7 @@ byId("reconcile-action").addEventListener("click", async () => {
notify(result.State === "SUCCEEDED" ? "Исход подтверждён." : "Исход пока неизвестен.");
if (currentProspective) await loadProspectiveRegister(currentProspective.ID);
} catch (error) {
notify(error.message);
notify(safeClientError(error));
} finally {
button.disabled = false;
button.textContent = "Проверить исход";
+2 -2
View File
@@ -25,12 +25,12 @@
<section id="chat-view" class="chat-view" hidden>
<header class="chat-header">
<a href="/chat" aria-label="Домовой">Домовой</a>
<a href="/" aria-label="Домовой">Домовой</a>
<nav class="human-nav" aria-label="Разделы Домового">
<button class="nav-tab active" data-tab="chat-panel" type="button">Чат</button>
<button class="nav-tab" data-tab="today-panel" type="button">Сегодня</button>
<button class="nav-tab" data-tab="history-panel" type="button">История</button>
<a href="/" class="operator-link">Оператор</a>
<a href="/admin" class="operator-link">Консоль</a>
</nav>
<span id="active-focus">фокус: не выбран</span>
<span id="session-state">сессия активна</span>
+86 -20
View File
@@ -6,9 +6,10 @@
<meta name="color-scheme" content="dark">
<meta name="theme-color" content="#101714">
<title>Домовой — консоль создателя</title>
<link rel="stylesheet" href="/assets/console.css">
<link rel="stylesheet" href="/assets/console.css?v=20260821-context">
</head>
<body>
<a class="skip-link" href="#overview">К содержимому</a>
<div class="grain" aria-hidden="true"></div>
<main id="login-view" class="login-shell">
@@ -44,6 +45,13 @@
</a>
<div class="topbar-actions">
<span id="connection-state" class="connection"><i></i>система доступна</span>
<nav class="topbar-nav" aria-label="Разделы кабинета">
<a href="#overview">Обзор</a>
<a href="#diagnostics">Проверки</a>
<a href="#operations">Операции</a>
<a href="#learning">Опыт</a>
<a href="#settings">Настройки</a>
</nav>
<button id="refresh-all" class="text-button" type="button">Обновить</button>
<button id="logout" class="icon-button" type="button" aria-label="Завершить сессию" title="Выйти">↗</button>
</div>
@@ -56,18 +64,46 @@
<p class="rail-copy">Укажите рабочий контекст. Он сохраняется только в этом браузере и не даёт дополнительных полномочий.</p>
</div>
<form id="context-form" class="context-form">
<label for="session-id">Идентификатор сессии</label>
<input id="session-id" autocomplete="off" placeholder="session-id">
<label for="workspace-id">Рабочая область</label>
<input id="workspace-id" autocomplete="off" placeholder="workspace-id">
<label for="task-id">Задача</label>
<input id="task-id" autocomplete="off" placeholder="task-id">
<label for="prospective-id">Выбранное ожидание</label>
<input id="prospective-id" autocomplete="off" placeholder="prospective-item-id">
<label for="capability-id">Возможность</label>
<input id="capability-id" autocomplete="off" value="cognitive-proposal">
<button class="secondary-button" type="submit">Загрузить контекст</button>
<div class="context-status-row">
<span class="context-status-label">ТЕКУЩИЙ КОНТЕКСТ</span>
<strong id="context-state" data-state="empty">не выбран</strong>
</div>
<div class="context-presets">
<label for="context-preset">Недавние наборы</label>
<select id="context-preset">
<option value="">Выберите сохранённый набор…</option>
</select>
<small>Сохраняются только в этом браузере.</small>
</div>
<div class="context-field">
<label for="session-id">Идентификатор сессии <span>обязательно</span></label>
<div class="context-input-row"><input id="session-id" autocomplete="off" spellcheck="false" placeholder="Вставьте session-id"><button class="copy-context" data-copy-context="session-id" type="button" aria-label="Скопировать идентификатор сессии" title="Скопировать">⧉</button></div>
</div>
<div class="context-field">
<label for="workspace-id">Рабочая область <span>обязательно</span></label>
<div class="context-input-row"><input id="workspace-id" autocomplete="off" spellcheck="false" placeholder="Вставьте workspace-id"><button class="copy-context" data-copy-context="workspace-id" type="button" aria-label="Скопировать рабочую область" title="Скопировать">⧉</button></div>
</div>
<p class="context-hint">Для «Состояния» нужны только эти два значения. Задача, ожидание и возможность находятся ниже в расширенных полях.</p>
<details class="advanced-context">
<summary>Расширенные идентификаторы</summary>
<div>
<label for="task-id">Задача</label>
<input id="task-id" autocomplete="off" placeholder="task-id">
<label for="prospective-id">Выбранное ожидание</label>
<input id="prospective-id" autocomplete="off" placeholder="prospective-item-id">
<label for="capability-id">Возможность</label>
<input id="capability-id" autocomplete="off" value="cognitive-proposal">
</div>
</details>
<div class="context-actions"><button class="secondary-button" type="submit">Загрузить контекст</button><button id="reset-context" class="text-button" type="button">Сбросить</button></div>
</form>
<nav class="section-nav" aria-label="Навигация по кабинету">
<a class="section-nav-link active" aria-current="location" href="#overview"><span>01</span><b>Состояние</b><small>фокус и сигналы</small></a>
<a class="section-nav-link" href="#diagnostics"><span>02</span><b>Проверки</b><small>доступность и границы</small></a>
<a class="section-nav-link" href="#operations"><span>03</span><b>Операции</b><small>ожидания и действия</small></a>
<a class="section-nav-link" href="#learning"><span>04</span><b>Опыт</b><small>reflection и навыки</small></a>
<a class="section-nav-link" href="#settings"><span>05</span><b>Настройки</b><small>интерфейс и обновление</small></a>
</nav>
<div class="session-stamp">
<span>ACTOR</span>
<strong id="session-actor">—</strong>
@@ -77,7 +113,7 @@
</aside>
<main class="workspace">
<section class="workspace-head">
<section id="overview" class="workspace-head" data-page="overview">
<div>
<p class="eyebrow">операционная картина</p>
<h1>Состояние сейчас</h1>
@@ -85,14 +121,28 @@
<p id="last-update" class="last-update">Данные ещё не запрошены</p>
</section>
<section class="signal-strip" aria-label="Сводка состояния">
<section class="signal-strip" aria-label="Сводка состояния" data-page="overview">
<div><span>ФОКУС</span><strong id="focus-signal" data-state="idle">ожидает</strong></div>
<div><span>ЗАДАЧА</span><strong id="task-signal" data-state="idle">ожидает</strong></div>
<div><span>CONTINUITY</span><strong id="prospective-signal" data-state="idle">ожидает</strong></div>
<div><span>ВОЗМОЖНОСТЬ</span><strong id="capability-signal" data-state="idle">ожидает</strong></div>
</section>
<section class="advisor-panel" aria-labelledby="advisor-title">
<section id="diagnostics" class="diagnostics-panel" aria-labelledby="diagnostics-title" data-page="diagnostics">
<header class="panel-heading">
<div><p>Проверяемая система</p><h2 id="diagnostics-title">Диагностика кабинета</h2></div>
<div class="panel-heading-tools"><span id="diagnostic-summary">Проверка ещё не запускалась</span><button id="run-diagnostics" class="secondary-button compact-button" type="button">Проверить всё</button></div>
</header>
<div class="diagnostic-intro">
<span class="diagnostic-pulse" aria-hidden="true"></span>
<p>Здесь видны только ответы текущей авторизованной сессии. Проверка не меняет данные и помогает отличить отсутствие контекста от недоступности сервиса.</p>
</div>
<div id="diagnostic-list" class="diagnostic-list" aria-live="polite" aria-busy="false">
<p class="register-empty">Нажмите «Проверить всё», чтобы получить актуальный снимок.</p>
</div>
</section>
<section class="advisor-panel" aria-labelledby="advisor-title" data-page="diagnostics">
<div class="advisor-intro">
<p>COGNITIVE / ADVISORY V1</p>
<h2 id="advisor-title">Спросить Домового</h2>
@@ -120,7 +170,7 @@
</div>
</section>
<section class="register-panel" aria-labelledby="register-title">
<section id="operations" class="register-panel" aria-labelledby="register-title" data-page="operations">
<header>
<div><p>Continuity / ближайшие</p><h2 id="register-title">Реестр ожиданий</h2></div>
<span id="prospective-count">0 ЗАПИСЕЙ</span>
@@ -130,7 +180,7 @@
</div>
</section>
<section class="activity-panel" aria-labelledby="activity-title">
<section class="activity-panel" aria-labelledby="activity-title" data-page="operations">
<header>
<div><p>Commit / владелец</p><h2 id="activity-title">Журнал действий</h2></div>
<div class="activity-tools">
@@ -143,7 +193,7 @@
</div>
</section>
<section class="learning-panel" aria-labelledby="learning-title">
<section id="learning" class="learning-panel" aria-labelledby="learning-title" data-page="learning">
<header>
<div><p>Learning / evidence</p><h2 id="learning-title">Опыт и reflection</h2></div>
<div class="activity-tools">
@@ -167,7 +217,20 @@
</div>
</section>
<section class="data-grid">
<section id="settings" class="settings-panel" aria-labelledby="settings-title" data-page="settings">
<header class="panel-heading">
<div><p>Локальные предпочтения</p><h2 id="settings-title">Настройки рабочего кабинета</h2></div>
<span id="settings-saved" class="settings-saved">Сохраняются в этом браузере</span>
</header>
<form id="settings-form" class="settings-grid">
<label class="setting-field"><span>Плотность данных</span><small>Выберите, сколько строк помещается на экране.</small><select id="setting-density"><option value="comfortable">Комфортная</option><option value="compact">Компактная</option></select></label>
<label class="setting-field"><span>Автообновление</span><small>Повторная проверка состояния без изменения данных.</small><select id="setting-refresh"><option value="off">Выключено</option><option value="30">Каждые 30 секунд</option><option value="60">Каждую минуту</option><option value="300">Каждые 5 минут</option></select></label>
<label class="setting-check"><input id="setting-motion" type="checkbox"><span><b>Сократить анимацию</b><small>Оставить спокойный режим для длительной работы.</small></span></label>
<div class="settings-actions"><button id="save-settings" class="primary-button compact-button" type="submit"><span>Сохранить настройки</span><span aria-hidden="true">→</span></button><button id="reset-settings" class="text-button" type="button">Сбросить</button></div>
</form>
</section>
<section class="data-grid" data-page="overview">
<article class="data-card focus-card">
<header><span class="card-number">01</span><div><p>Situation</p><h2>Активный фокус</h2></div><span id="focus-badge" class="status-badge neutral">НЕ ЗАГРУЖЕН</span></header>
<dl>
@@ -188,6 +251,7 @@
<div><dt>Ревизия</dt><dd id="task-revision">—</dd></div>
</dl>
<p id="task-message" class="card-message">Укажите задачу для проверки относительно активного фокуса.</p>
<div class="id-actions"><button class="id-action" data-use-value="task-value" data-target-id="task-id" type="button">Использовать ID задачи</button><button class="id-action icon-id-action" data-copy-value="task-value" type="button" aria-label="Скопировать ID задачи" title="Скопировать ID">⧉</button></div>
</article>
<article class="data-card prospective-card">
@@ -205,6 +269,7 @@
<div><dt>Ревизия</dt><dd id="prospective-revision">—</dd></div>
</dl>
<p id="prospective-message" class="card-message">Укажите идентификатор ProspectiveItem для проверки владельца и состояния.</p>
<div class="id-actions"><button class="id-action" data-use-value="prospective-value" data-target-id="prospective-id" type="button">Использовать ID ожидания</button><button class="id-action icon-id-action" data-copy-value="prospective-value" type="button" aria-label="Скопировать ID ожидания" title="Скопировать ID">⧉</button></div>
<section class="commit-zone" aria-labelledby="commit-zone-title">
<div class="commit-zone-copy">
<p>Commit / контролируемый переход</p>
@@ -240,6 +305,7 @@
<div><dt>Ревизия</dt><dd id="capability-revision">—</dd></div>
<div><dt>Наблюдалось</dt><dd id="capability-observed">—</dd></div>
</dl>
<div class="id-actions"><button class="id-action" data-use-value="capability-value" data-target-id="capability-id" type="button">Использовать ID возможности</button><button class="id-action icon-id-action" data-copy-value="capability-value" type="button" aria-label="Скопировать ID возможности" title="Скопировать ID">⧉</button></div>
</article>
</section>
</main>
@@ -286,6 +352,6 @@
</dialog>
<div id="toast" class="toast" role="status" aria-live="polite" hidden></div>
<script src="/assets/console.js" defer></script>
<script src="/assets/console.js?v=20260821-context" defer></script>
</body>
</html>
-1611
View File
File diff suppressed because it is too large Load Diff
-644
View File
@@ -1,644 +0,0 @@
package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
)
func TestDiscoverComposeInputsRequiredAndOptional(t *testing.T) {
content := `A=${POSTGRES_SUPERUSER}
B=${POSTGRES_DB:-assistant}
C=${CREATOR_PASSWORD_FILE}
D=${FOO_CREDENTIAL_REF}
`
inputs := discoverComposeInputs(content)
if _, ok := inputs["POSTGRES_SUPERUSER"]; !ok {
t.Fatal("POSTGRES_SUPERUSER should be discovered")
}
if !inputs["POSTGRES_SUPERUSER"].required {
t.Fatalf("POSTGRES_SUPERUSER expected required")
}
if inputs["POSTGRES_DB"].required {
t.Fatalf("POSTGRES_DB expected optional")
}
if !inputs["CREATOR_PASSWORD_FILE"].required {
t.Fatalf("CREATOR_PASSWORD_FILE expected required from compose placeholder")
}
if !inputs["EPISTEMICS_POSTGRES_CREDENTIAL_REF"].required {
t.Fatalf("required production bootstrap variable should be required")
}
if got := len(inputs); got < 4 {
t.Fatalf("expected at least 4 discovered inputs, got %d", got)
}
}
func TestResolveDeploymentInputsRejectsAmbientProduction(t *testing.T) {
t.Setenv("POSTGRES_SUPERUSER", "from_ambient")
inputs := map[string]composeInput{
"POSTGRES_SUPERUSER": {name: "POSTGRES_SUPERUSER", required: true},
}
resolved, _, _, err := resolveDeploymentInputs(inputs, "", true)
if err != nil {
t.Fatalf("resolveDeploymentInputs error: %v", err)
}
if len(resolved) != 1 {
t.Fatalf("expected one resolved input, got %d", len(resolved))
}
if resolved[0].Source != "ambient" || resolved[0].Resolved {
t.Fatalf("expected unresolved ambient resolution, got source=%s resolved=%v", resolved[0].Source, resolved[0].Resolved)
}
}
func TestDeployPrepareRequiresEnvFileForProduction(t *testing.T) {
root := t.TempDir()
composeFile := filepath.Join(root, "docker-compose.prod.yml")
if err := os.WriteFile(composeFile, []byte("a=${POSTGRES_SUPERUSER}"), 0o600); err != nil {
t.Fatal(err)
}
err := deployPrepare([]string{"--target", "production", "--compose", composeFile})
if err == nil || !strings.Contains(err.Error(), "production prepare requires --env-file") {
t.Fatalf("expected missing env-file failure, got %v", err)
}
}
func TestDeployPrepareBlockedAndApplyRefusesMissingRequiredInput(t *testing.T) {
root := t.TempDir()
repoRoot, err := gitRepositoryRoot(".")
if err != nil {
t.Fatal(err)
}
composeFile := filepath.Join(root, "docker-compose.prod.yml")
if err := os.WriteFile(composeFile, []byte("a=${POSTGRES_SUPERUSER}"), 0o600); err != nil {
t.Fatal(err)
}
envFile := filepath.Join(root, "production.env")
parts := make([]string, 0, len(requiredChatOnlyInputs))
for key := range requiredChatOnlyInputs {
if key == "POSTGRES_SUPERUSER" {
continue
}
parts = append(parts, key+"=value-"+key)
}
if err := os.WriteFile(envFile, []byte(strings.Join(parts, "\n")), 0o600); err != nil {
t.Fatal(err)
}
target, err := resolveDeploymentTarget("production")
if err != nil {
t.Fatal(err)
}
target.ComposeFile = composeFile
plan, err := buildDeploymentPlan(repoRoot, target, envFile, "HEAD", 24, "")
if err != nil {
t.Fatal(err)
}
if plan.Readiness != readinessFailed {
t.Fatalf("expected blocked plan, got readiness=%s", plan.Readiness)
}
if !strings.Contains(strings.Join(plan.ReadinessReasons, "\n"), "POSTGRES_SUPERUSER") {
t.Fatalf("expected missing required input blocker for POSTGRES_SUPERUSER, got %v", plan.ReadinessReasons)
}
planPath := filepath.Join(root, "blocked-plan.json")
payload, err := json.Marshal(plan)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(planPath, payload, 0o600); err != nil {
t.Fatal(err)
}
if err := deployApply([]string{"-plan", planPath}); err == nil {
t.Fatal("expected apply refusal for blocked plan")
}
}
func TestAssertPlanCurrentRejectsStaleInputChange(t *testing.T) {
root := t.TempDir()
composeFile := filepath.Join(root, "docker-compose.prod.yml")
if err := os.WriteFile(composeFile, []byte("a=${POSTGRES_SUPERUSER}"), 0o600); err != nil {
t.Fatal(err)
}
envFile := filepath.Join(root, "deploy.env")
if err := os.WriteFile(envFile, []byte("POSTGRES_SUPERUSER=first\n"), 0o600); err != nil {
t.Fatal(err)
}
inputs, err := discoverComposeInputsFromFile(composeFile)
if err != nil {
t.Fatal(err)
}
resolved, _, _, err := resolveDeploymentInputs(inputs, envFile, true)
if err != nil {
t.Fatal(err)
}
plan := &deploymentPlan{
Version: 1,
PlanID: "stale-test",
CreatedAt: time.Now().UTC().Format(time.RFC3339),
ExpiresAt: time.Now().UTC().Add(time.Hour).Format(time.RFC3339),
Readiness: readinessReady,
ReadinessReasons: nil,
Target: "production",
ComposeFile: composeFile,
DockerContext: "docker-prod",
ProjectName: "assistant",
EnvFile: envFile,
SourceRevision: "11a6a23f6e4f44dd88265a04846419a27f54df98",
SourceRevisionMode: "explicit",
ArtifactRevision: "11a6a23f6e4f44dd88265a04846419a27f54df98",
DeploymentConfigRev: resolveConfigHash(composeFile),
CurrentProductionRev: "",
KnownGoodRevision: "",
Inputs: resolved,
Classify: map[string][]string{},
KnownInputsSource: map[string]string{},
}
target, err := resolveDeploymentTarget("production")
if err != nil {
t.Fatal(err)
}
plan.PlanBindingHash = buildPlanBindingFromInputs(
resolved,
plan.SourceRevision,
plan.ArtifactRevision,
plan.DeploymentConfigRev,
target.DockerContext,
target.Containers,
target.Volumes,
)
if err := os.WriteFile(envFile, []byte("POSTGRES_SUPERUSER=second\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := assertPlanCurrent(plan); err == nil {
t.Fatal("expected stale plan after input change")
}
}
func TestAssertPlanCurrentRejectsArtifactMismatch(t *testing.T) {
root := t.TempDir()
composeFile := filepath.Join(root, "docker-compose.prod.yml")
if err := os.WriteFile(composeFile, []byte("a=${POSTGRES_SUPERUSER}"), 0o600); err != nil {
t.Fatal(err)
}
envFile := filepath.Join(root, "deploy.env")
envParts := make([]string, 0, len(requiredChatOnlyInputs))
for key := range requiredChatOnlyInputs {
envParts = append(envParts, key+"=value-"+strings.ToLower(key))
}
envParts = append(envParts, "POSTGRES_SUPERUSER=from_file")
if err := os.WriteFile(envFile, []byte(strings.Join(envParts, "\n")), 0o600); err != nil {
t.Fatal(err)
}
inputs, err := discoverComposeInputsFromFile(composeFile)
if err != nil {
t.Fatal(err)
}
discovered, _, _, err := resolveDeploymentInputs(inputs, envFile, true)
if err != nil {
t.Fatal(err)
}
target, err := resolveDeploymentTarget("production")
if err != nil {
t.Fatal(err)
}
configRev := resolveConfigHash(composeFile)
shaA := strings.Repeat("a", 40)
shaB := strings.Repeat("b", 40)
plan := &deploymentPlan{
Version: 1,
PlanID: "artifact-mismatch",
CreatedAt: time.Now().UTC().Format(time.RFC3339),
ExpiresAt: time.Now().UTC().Add(time.Hour).Format(time.RFC3339),
Readiness: readinessReady,
Target: "production",
ComposeFile: composeFile,
DockerContext: "docker-prod",
ProjectName: "assistant",
EnvFile: envFile,
SourceRevision: shaA,
SourceRevisionMode: "explicit",
ArtifactRevision: shaA,
DeploymentConfigRev: configRev,
Inputs: discovered,
Classify: map[string][]string{},
KnownInputsSource: map[string]string{},
}
plan.PlanBindingHash = buildPlanBindingFromInputs(
discovered,
shaA,
shaA,
configRev,
target.DockerContext,
target.Containers,
target.Volumes,
)
plan.ArtifactRevision = shaB
plan.SourceRevision = shaB
if err := assertPlanCurrent(plan); err == nil {
t.Fatal("expected stale plan due artifact mismatch")
}
}
func TestMaxMigrationPrefixFromListHandlesNestedMigrationPaths(t *testing.T) {
got := maxMigrationPrefixFromList([]string{
"database/migrations/001_bootstrap.sql",
"database/migrations/007_legacy.sql",
"database/migrations/work/015_capability.sql",
"database/migrations/README.md",
"database/migrations/artifact/019_work.sql",
})
if got != "19" {
t.Fatalf("expected max migration prefix 19, got %q", got)
}
}
func TestAssertPlanCurrentRejectsMigrationUpgradeWithoutKnownGood(t *testing.T) {
root, err := gitRepositoryRoot(".")
if err != nil {
t.Fatal(err)
}
headOut, err := runGit(root, "rev-parse", "HEAD")
if err != nil {
t.Fatal(err)
}
targetRevision := strings.TrimSpace(string(headOut))
targetMaxStr, err := maxMigrationPrefixForRevision(root, targetRevision)
if err != nil {
t.Fatal(err)
}
targetMax, err := strconv.Atoi(targetMaxStr)
if err != nil {
t.Fatalf("invalid target migration max %q: %v", targetMaxStr, err)
}
revisionsOut, err := runGit(root, "rev-list", "HEAD", "--max-count", "20")
if err != nil {
t.Fatal(err)
}
ancestorList := strings.Fields(string(revisionsOut))
currentProductionRevision := ""
for _, revision := range ancestorList {
if strings.TrimSpace(revision) == targetRevision {
continue
}
revMaxStr, err := maxMigrationPrefixForRevision(root, revision)
if err != nil {
continue
}
revMax, err := strconv.Atoi(revMaxStr)
if err != nil {
continue
}
if revMax < targetMax {
currentProductionRevision = revision
break
}
}
if currentProductionRevision == "" {
t.Skip("could not find historical revision with lower migration max to validate precondition block")
}
rootDir := t.TempDir()
composeFile := filepath.Join(rootDir, "docker-compose.prod.yml")
if err := os.WriteFile(composeFile, []byte("a=${POSTGRES_SUPERUSER}\n"), 0o600); err != nil {
t.Fatal(err)
}
envFile := filepath.Join(rootDir, "bootstrap.env")
requiredValues := make([]string, 0, len(requiredChatOnlyInputs))
for key := range requiredChatOnlyInputs {
requiredValues = append(requiredValues, key+"=value-"+strings.ToLower(key))
}
if err := os.WriteFile(envFile, []byte(strings.Join(requiredValues, "\n")), 0o600); err != nil {
t.Fatal(err)
}
inputs, err := discoverComposeInputsFromFile(composeFile)
if err != nil {
t.Fatal(err)
}
discovered, _, _, err := resolveDeploymentInputs(inputs, envFile, true)
if err != nil {
t.Fatal(err)
}
target, err := resolveDeploymentTarget("production")
if err != nil {
t.Fatal(err)
}
target.ComposeFile = composeFile
plan := &deploymentPlan{
Version: 1,
PlanID: "migration-precondition",
CreatedAt: time.Now().UTC().Format(time.RFC3339),
ExpiresAt: time.Now().UTC().Add(time.Hour).Format(time.RFC3339),
Readiness: readinessReady,
ReadinessReasons: nil,
Target: "production",
ComposeFile: composeFile,
DockerContext: target.DockerContext,
ProjectName: target.ProjectName,
EnvFile: envFile,
SourceRevision: targetRevision,
SourceRevisionMode: "explicit",
ArtifactRevision: targetRevision,
DeploymentConfigRev: resolveConfigHash(composeFile),
CurrentProductionRev: currentProductionRevision,
KnownGoodRevision: "",
Classify: map[string][]string{},
Inputs: discovered,
KnownInputsSource: map[string]string{},
}
plan.Migrations, err = migrationState(root, targetRevision, currentProductionRevision)
if err != nil {
t.Fatal(err)
}
if !plan.Migrations.NeedKnownGood {
t.Fatal("expected migration state to require known good for this test")
}
plan.PlanBindingHash = buildPlanBindingFromInputs(
plan.Inputs,
plan.SourceRevision,
plan.ArtifactRevision,
plan.DeploymentConfigRev,
target.DockerContext,
target.Containers,
target.Volumes,
)
if err := assertPlanCurrent(plan); err == nil {
t.Fatal("expected assertPlanCurrent to reject migration precondition without known-good")
} else if !strings.Contains(err.Error(), "migration precondition failed") {
t.Fatalf("expected migration precondition failure, got: %v", err)
}
}
func TestDeploymentPlanDoesNotLeakSecretValues(t *testing.T) {
root, err := gitRepositoryRoot(".")
if err != nil {
t.Fatal(err)
}
target, err := resolveDeploymentTarget("production")
if err != nil {
t.Fatal(err)
}
composeFile := filepath.Join(t.TempDir(), "docker-compose.prod.yml")
if err := os.WriteFile(composeFile, []byte("a=${POSTGRES_SUPERUSER}\n"), 0o600); err != nil {
t.Fatal(err)
}
target.ComposeFile = composeFile
envFile := filepath.Join(t.TempDir(), "production.env")
if err := os.WriteFile(envFile, []byte(strings.Join([]string{
"POSTGRES_SUPERUSER_PASSWORD=super-secret-123",
"CREATOR_PASSWORD_FILE=creator-password-file-456",
"COMMIT_EXECUTOR_TOKEN=executor-token-789",
}, "\n")), 0o600); err != nil {
t.Fatal(err)
}
plan, err := buildDeploymentPlan(root, target, envFile, "HEAD", 24, "")
if err != nil {
t.Fatal(err)
}
payload, err := json.Marshal(plan)
if err != nil {
t.Fatal(err)
}
serialized := string(payload)
for _, secretValue := range []string{
"super-secret-123",
"creator-password-file-456",
"executor-token-789",
} {
if strings.Contains(serialized, secretValue) {
t.Fatalf("plain secret value leaked into plan: %q", secretValue)
}
}
if got := len(plan.SecretReferences); got < 3 {
t.Fatalf("expected at least 3 secret references in plan, got %d", got)
}
}
func TestDefaultKnownGoodManifestPathStableWithoutLocalAppData(t *testing.T) {
t.Setenv("LOCALAPPDATA", "")
t.Setenv("LOCALAPPDATA_ALT", "")
if strings.TrimSpace(defaultKnownGoodManifestPath()) != "" {
t.Fatal("expected empty path when LOCALAPPDATA is unset")
}
}
func TestDeployApplyRunsHealthGateWithReadyPlanAndSimulatedComposeDocker(t *testing.T) {
originalPath := os.Getenv("PATH")
fakeDockerDir := t.TempDir()
fakeDocker := filepath.Join(fakeDockerDir, "docker.bat")
dockerBatch := "@echo off\n" +
"if \"%~1\"==\"--context\" (\n" +
" if \"%~3\"==\"compose\" exit /B 0\n" +
" if \"%~3\"==\"inspect\" (\n" +
" if \"%~6\"==\"{{index .Config.Labels \\\"org.opencontainers.image.revision\\\"}}\" echo %FAKE_DOCKER_REVISION%\n" +
" if \"%~6\"==\"{{.State.Status}}\" echo running\n" +
" exit /B 0\n" +
" )\n" +
" if \"%~3\"==\"volume\" if \"%~4\"==\"inspect\" exit /B 0\n" +
")\n" +
"exit /B 1\n"
if err := os.WriteFile(fakeDocker, []byte(dockerBatch), 0o600); err != nil {
t.Fatal(err)
}
os.Setenv("PATH", fakeDockerDir+";"+originalPath)
defer os.Setenv("PATH", originalPath)
root, err := gitRepositoryRoot(".")
if err != nil {
t.Fatal(err)
}
target, err := resolveDeploymentTarget("production")
if err != nil {
t.Fatal(err)
}
composeFile := filepath.Join(fakeDockerDir, "docker-compose.prod.yml")
target.ComposeFile = composeFile
if err := os.WriteFile(composeFile, []byte("services:\\n app:\\n image: alpine\\n environment:\\n - POSTGRES_SUPERUSER=${POSTGRES_SUPERUSER}\\n"), 0o600); err != nil {
t.Fatal(err)
}
envFile := filepath.Join(fakeDockerDir, "production.env")
env := []string{
"POSTGRES_SUPERUSER=deploy-apply-test",
"POSTGRES_SUPERUSER_PASSWORD=pw-super",
"CONTINUITY_DB_PASSWORD=pw-cont",
"WORK_DB_PASSWORD=pw-work",
"CAPABILITY_DB_PASSWORD=pw-cap",
"COMMIT_DB_PASSWORD=pw-commit",
"AUTHORITY_DB_PASSWORD=pw-auth",
"IDENTITY_DB_PASSWORD=pw-id",
"SITUATION_DB_PASSWORD=pw-sit",
"CREATOR_USERNAME=creator",
"CREATOR_PASSWORD_FILE=creator-password",
"CREATOR_BOOTSTRAP_TOKEN=creator-bootstrap",
"CREATOR_SESSION_SIGNING_KEY=creator-session",
"COMMIT_EXECUTOR_TOKEN=commit-exec",
"EPISTEMICS_POSTGRES_CREDENTIAL_REF=epi-cred-ref",
"INTERACTION_POSTGRES_CREDENTIAL_REF=int-cred-ref",
"EPISTEMICS_POSTGRES_PASSWORD=epi-password",
"INTERACTION_POSTGRES_PASSWORD=int-password",
}
if err := os.WriteFile(envFile, []byte(strings.Join(env, "\n")), 0o600); err != nil {
t.Fatal(err)
}
knownGood, _, err := captureKnownGood(target, "")
if err != nil || strings.TrimSpace(knownGood) == "" {
knownGood = "11a6a23f6e4f44dd88265a04846419a27f54df98"
}
plan, err := buildDeploymentPlan(root, target, envFile, knownGood, 24, "")
if err != nil {
t.Fatal(err)
}
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
writer.WriteHeader(http.StatusOK)
}))
defer server.Close()
originalHealthEndpoints := append([]string{}, deploymentExternalHealthEndpoints...)
deploymentExternalHealthEndpoints = []string{server.URL + "/healthz", server.URL + "/chat"}
defer func() {
deploymentExternalHealthEndpoints = originalHealthEndpoints
}()
plan.PlanBindingHash = buildPlanBindingFromInputs(
plan.Inputs,
plan.SourceRevision,
plan.ArtifactRevision,
plan.DeploymentConfigRev,
target.DockerContext,
target.Containers,
target.Volumes,
)
planPath := filepath.Join(fakeDockerDir, "ready-plan.json")
payload, err := json.Marshal(plan)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(planPath, payload, 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("FAKE_DOCKER_REVISION", plan.ArtifactRevision)
if err := deployApply([]string{"-plan", planPath}); err != nil {
t.Fatalf("expected successful apply with simulated docker and passing health checks, got %v", err)
}
}
func TestDeployApplyHealthGateRejectsOnFailedHTTPHealth(t *testing.T) {
originalPath := os.Getenv("PATH")
fakeDockerDir := t.TempDir()
fakeDocker := filepath.Join(fakeDockerDir, "docker.bat")
dockerBatch := "@echo off\n" +
"if \"%~1\"==\"--context\" (\n" +
" if \"%~3\"==\"compose\" exit /B 0\n" +
" if \"%~3\"==\"inspect\" (\n" +
" if \"%~6\"==\"{{index .Config.Labels \\\"org.opencontainers.image.revision\\\"}}\" echo %FAKE_DOCKER_REVISION%\n" +
" if \"%~6\"==\"{{.State.Status}}\" echo running\n" +
" exit /B 0\n" +
" )\n" +
" if \"%~3\"==\"volume\" if \"%~4\"==\"inspect\" exit /B 0\n" +
")\n" +
"exit /B 1\n"
if err := os.WriteFile(fakeDocker, []byte(dockerBatch), 0o600); err != nil {
t.Fatal(err)
}
os.Setenv("PATH", fakeDockerDir+";"+originalPath)
defer os.Setenv("PATH", originalPath)
root, err := gitRepositoryRoot(".")
if err != nil {
t.Fatal(err)
}
target, err := resolveDeploymentTarget("production")
if err != nil {
t.Fatal(err)
}
composeFile := filepath.Join(fakeDockerDir, "docker-compose.prod.yml")
target.ComposeFile = composeFile
if err := os.WriteFile(composeFile, []byte("services:\\n app:\\n image: alpine\\n environment:\\n - POSTGRES_SUPERUSER=${POSTGRES_SUPERUSER}\\n"), 0o600); err != nil {
t.Fatal(err)
}
envFile := filepath.Join(fakeDockerDir, "production.env")
env := []string{
"POSTGRES_SUPERUSER=deploy-apply-test",
"POSTGRES_SUPERUSER_PASSWORD=pw-super",
"CONTINUITY_DB_PASSWORD=pw-cont",
"WORK_DB_PASSWORD=pw-work",
"CAPABILITY_DB_PASSWORD=pw-cap",
"COMMIT_DB_PASSWORD=pw-commit",
"AUTHORITY_DB_PASSWORD=pw-auth",
"IDENTITY_DB_PASSWORD=pw-id",
"SITUATION_DB_PASSWORD=pw-sit",
"CREATOR_USERNAME=creator",
"CREATOR_PASSWORD_FILE=creator-password",
"CREATOR_BOOTSTRAP_TOKEN=creator-bootstrap",
"CREATOR_SESSION_SIGNING_KEY=creator-session",
"COMMIT_EXECUTOR_TOKEN=commit-exec",
"EPISTEMICS_POSTGRES_CREDENTIAL_REF=epi-cred-ref",
"INTERACTION_POSTGRES_CREDENTIAL_REF=int-cred-ref",
"EPISTEMICS_POSTGRES_PASSWORD=epi-password",
"INTERACTION_POSTGRES_PASSWORD=int-password",
}
if err := os.WriteFile(envFile, []byte(strings.Join(env, "\n")), 0o600); err != nil {
t.Fatal(err)
}
knownGood, _, err := captureKnownGood(target, "")
if err != nil || strings.TrimSpace(knownGood) == "" {
knownGood = "11a6a23f6e4f44dd88265a04846419a27f54df98"
}
plan, err := buildDeploymentPlan(root, target, envFile, knownGood, 24, "")
if err != nil {
t.Fatal(err)
}
plan.PlanBindingHash = buildPlanBindingFromInputs(
plan.Inputs,
plan.SourceRevision,
plan.ArtifactRevision,
plan.DeploymentConfigRev,
target.DockerContext,
target.Containers,
target.Volumes,
)
planPath := filepath.Join(fakeDockerDir, "ready-plan-bad-health.json")
payload, err := json.Marshal(plan)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(planPath, payload, 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("FAKE_DOCKER_REVISION", plan.ArtifactRevision)
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
switch request.URL.Path {
case "/healthz":
writer.WriteHeader(http.StatusInternalServerError)
default:
writer.WriteHeader(http.StatusOK)
}
}))
defer server.Close()
originalHealthEndpoints := append([]string{}, deploymentExternalHealthEndpoints...)
deploymentExternalHealthEndpoints = []string{server.URL + "/healthz", server.URL + "/chat"}
defer func() {
deploymentExternalHealthEndpoints = originalHealthEndpoints
}()
if err := deployApply([]string{"-plan", planPath}); err == nil {
t.Fatalf("expected apply to fail when health check fails")
}
}
+1 -3
View File
@@ -56,8 +56,6 @@ func main() {
err = preflight(os.Args[2:])
case "check-diff":
err = checkDiff(os.Args[2:])
case "deploy":
err = deploy(os.Args[2:])
default:
usage()
os.Exit(2)
@@ -69,7 +67,7 @@ func main() {
}
func usage() {
fmt.Println("devctl doctor | check-architecture | show-registry | preflight (-active <json> | -task <json>) | check-diff [-active <json> | -task <json>] | deploy <prepare|apply> ...")
fmt.Println("devctl doctor | check-architecture | show-registry | preflight (-active <json> | -task <json>) | check-diff [-active <json> | -task <json>]")
}
func loadRegistry() (Registry, error) {
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000073-production-deployment-continuity-safe-redeploy.json",
"task_id": "DEV-000073",
"task_file": "dev/tasks/DEV-000075-creator-chat-primary-surface.json",
"task_id": "DEV-000075",
"task_revision": 1
}
@@ -5,14 +5,6 @@
"base_repo_revision": "64870798b9a50f895c99d24441703f366df5c98a",
"risk_tier": "E",
"intent": "Implement semantic interpretation and deferral boundary V1 so deterministic interpretation acts as a fast-path candidate rather than unquestionable final truth; preserve provenance between deterministic and cognitive candidates; support mixed intent and safer deferral without activating new providers.",
"non_goal_scope": [
"Do not build a dedicated naming subsystem in this task (for example, no assistant_name table/field).",
"Do not implement dedicated favorite_color/disliked_food/project_role-like preference schemas in this task.",
"Do not introduce a hardcoded 'запомни' keyword trigger or naming-specific memory trigger.",
"Do not introduce dedicated naming memory, universal ontology, or graph/knowledge DB in this task.",
"Treat naming/name-preferral utterances as generic semantic interpretation cases and test fixtures, not the target for persistence architecture changes.",
"Any future durable memory/knowledge mechanism is out of scope for this task and must be captured as a follow-up requirement."
],
"requirement_ids": [
"DEV-000071 measured baseline",
"Deterministic candidate can decline/defer",
@@ -144,11 +136,6 @@
"reality evaluation comparisons",
"production /chat acceptance"
],
"future_scope_requirements": [
"Generic interaction-to-memory pipeline (common conceptual flow): interaction -> understood meaning -> potential memory/knowledge candidate -> subject/scope/provenance -> salience/relevance -> persistence decision -> relations/associations -> retrieval -> correction/consolidation.",
"Support multiple ordinary human preference/intention domains (names, preferred forms of address, colors, dislikes, habits, relationships, project facts, machine characteristics, temporary constraints) without special schema-per-example.",
"Use existing Epistemics/Experience/Identity/Situation foundations first; introduce new canonical concepts only when requirements and architecture review justify them."
],
"terminal_gap_states": [
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_ARCHITECTURE_GAP",
@@ -1,72 +0,0 @@
{
"task_id": "DEV-000073",
"task_revision": 1,
"architecture_revision": "DEV-000072-baseline",
"base_repo_revision": "36fec6c05cdf7aaf413ffe6b9437f07b03fc7b6b",
"risk_tier": "E",
"intent": "Introduce deterministic two-phase production deployment (prepare/apply) and stale/health/state gates in devctl without changing runtime semantics, so missing required production deployment inputs can never trigger an unsafe recreate.",
"requirement_ids": [
"prepare/apply separation",
"fail-closed required input resolution",
"exact artifact pinning",
"stale plan rejection",
"known-good/rollback capture",
"health verification on apply",
"no production mutation from ambient shell"
],
"allowed_change_scope": {
"paths": [
"AGENTS.md",
"cmd/devctl/**",
"docs/DEPLOYMENT.md",
"docs/PROJECT_STATUS.md",
"docs/evidence/deployment-continuity-dev000073.md",
"architecture/UNRESOLVED_QUESTIONS.md",
"dev/tasks/DEV-000072-semantic-interpretation-deferral-boundary.json",
"docs/evidence/production-recovery-incident-20260818.md",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000073-production-deployment-continuity-safe-redeploy.json"
]
},
"forbidden_change_scope": {
"paths": []
},
"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
},
"semantic_owner": "devctl",
"contracts": {
"consume": [
"contracts.identity"
],
"may_change": []
},
"acceptance_scenarios": [
"fresh shell prepare without ambient production env fails unless required values are in --env-file",
"deploy apply refuses when plan is stale due input/config/revision changes",
"deploy prepare can create READY and BLOCKED plans with machine-readable reasons",
"deploy apply succeeds only for READY plans and verifies deployment artifacts and /health endpoints",
"known-good revision is captured deterministically from production manifest or explicit recovery snapshot",
"healthy production is preserved when PREPARE or APPLY refuse to proceed"
],
"required_tests": [
"go test ./cmd/devctl",
"go run ./cmd/devctl preflight -task dev/tasks/DEV-000073-production-deployment-continuity-safe-redeploy.json",
"go run ./cmd/devctl check-diff -task dev/tasks/DEV-000073-production-deployment-continuity-safe-redeploy.json",
"go run ./cmd/devctl deploy prepare --target production --compose deploy/docker-compose.prod.yml --env-file <production-bootstrap.env> --plan .devctl/deployment-plans/test-dev073.json",
"go run ./cmd/devctl deploy apply --plan .devctl/deployment-plans/test-dev073.json --dry-run",
"go run ./cmd/devctl deploy apply --plan .devctl/deployment-plans/missing.json"
],
"terminal_gap_states": [
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_ARCHITECTURE_GAP",
"BLOCKED_BY_MISSING_CAPABILITY",
"BLOCKED_BY_CONFLICT",
"REQUIRES_MIGRATION_DECISION"
]
}
@@ -0,0 +1,92 @@
{
"task_id": "DEV-000074",
"task_revision": 6,
"architecture_revision": "DEV-000073-baseline",
"base_repo_revision": "1cfbb4ae66e24d514e59dabafb936c35546bd418",
"risk_tier": "E",
"intent": "Make the authenticated Creator Web cabinet understandable and operable for a human by adding section navigation, read-only diagnostics, and browser-local interface preferences without changing domain state or authority boundaries.",
"requirement_ids": [
"Human can move between overview, diagnostics, operations and settings",
"Human can run all currently exposed read-only Creator checks from one diagnostic panel",
"Interface preferences remain local to the browser and never change canonical state",
"Existing explicit confirmation remains required for canonical mutations",
"Desktop and mobile layouts remain usable"
],
"allowed_change_scope": {
"modules": ["cmd/creator-web"],
"paths": [
"cmd/creator-web/static/index.html",
"cmd/creator-web/static/assets/console.css",
"cmd/creator-web/static/assets/console.js",
"cmd/creator-web/server_test.go",
"cmd/creator-web/chat_test.go",
"docs/PROJECT_STATUS.md",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000074-creator-cabinet-operations.json"
]
},
"forbidden_change_scope": {
"paths": [
"architecture/**",
"security/**",
"database/**",
"modules/**",
"contracts/**",
"adapters/**",
"cmd/core-runtime/**",
"cmd/commit-runtime/**",
"cmd/continuity-runtime/**",
"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 Creator gateway endpoints"],
"may_change": []
},
"semantic_owner": "none-interface-adapter",
"canonical_state": [],
"state_transitions": [
"diagnostic request -> read-only gateway observation",
"settings form submit -> browser-local preference update"
],
"side_effects_and_privileges": [
"No new privilege or mutation endpoint",
"No database migration",
"Existing Commit actions retain explicit confirmation dialogs",
"No deployment or neighboring project mutation"
],
"persistence_and_migrations": ["No canonical persistence; localStorage only for interface preferences"],
"failure_retry_restart": ["Diagnostic failures are shown per check without exposing upstream error details"],
"assumptions": [
"Existing Creator API routes remain the source of truth",
"Browser-local settings are disposable and do not affect domain behavior"
],
"acceptance_scenarios": [
"Authenticated cabinet exposes navigation to all four human-facing sections",
"read-only diagnostics checks session, advisory policy, capability, today, attention, actions and learning",
"density, auto-refresh and reduced-motion preferences save and reset locally",
"mobile layout remains readable",
"existing Creator Web tests pass",
"architecture and diff scope checks pass"
],
"required_tests": [
"node --check cmd/creator-web/static/assets/console.js",
"go test ./cmd/creator-web",
"go test ./...",
"go vet ./...",
"go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"go run ./cmd/devctl check-architecture",
"git diff --check",
"responsive browser inspection"
],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,47 @@
{
"task_id": "DEV-000075",
"task_revision": 1,
"architecture_revision": "DEV-000073-baseline",
"base_repo_revision": "39a27102c9665e41f29e85bc8f82a316bbbe3962",
"risk_tier": "E",
"intent": "Make live conversation with Domovoy the public starting surface and keep the Creator operator console on a separate admin route.",
"requirement_ids": [
"Public root opens the human chat",
"Operator console remains available at a separate admin route",
"Existing /chat route remains compatible",
"Chat does not expose provider, model, binding, session or workspace internals"
],
"allowed_change_scope": {
"modules": ["cmd/creator-web"],
"paths": [
"cmd/creator-web/server.go",
"cmd/creator-web/server_test.go",
"cmd/creator-web/chat_test.go",
"cmd/creator-web/static/chat.html",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000075-creator-chat-primary-surface.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 /chat and authenticated Creator endpoints"], "may_change": []},
"semantic_owner": "none-interface-adapter",
"canonical_state": [],
"state_transitions": ["GET / -> chat surface", "GET /admin -> operator console surface"],
"side_effects_and_privileges": ["No new privilege", "No canonical mutation", "No model/provider routing change"],
"persistence_and_migrations": ["No persistence or migration"],
"failure_retry_restart": ["Existing chat failure behavior remains unchanged"],
"assumptions": ["/chat is the existing human chat surface", "The operator console may be reached at /admin"],
"acceptance_scenarios": ["Root serves chat", "Admin serves console", "Chat compatibility remains", "Internal IDs remain absent from chat HTML"],
"required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "devctl check-architecture", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
-25
View File
@@ -23,31 +23,6 @@ powershell -ExecutionPolicy Bypass -File .\deploy\windows\Confirm-DomovoyProduct
The confirmation script accepts the promotion only when every project-built container has that revision label, then atomically records its exact Docker image ID in `%LOCALAPPDATA%\Domovoy\monitoring\deployment-manifest.json` and the private NAS monitoring directory. Production health compares every running label and image ID to this manifest and becomes degraded on drift. The manifest is non-secret operational evidence; it is not canonical Assistant state.
## Production deploy pipeline V1
`cmd/devctl` is now the normal production deployment entrypoint and uses two explicit stages:
- `deploy prepare` reads the compose manifest and an explicit durable bootstrap source (for production: `--env-file`) and resolves required inputs.
- `deploy apply` consumes a prepared deployment plan and is the only normal mutation path.
Safety properties:
- Missing/empty/unresolvable required input blocks prepare readiness (`BLOCKED`) and blocks apply.
- Ambient environment is not authoritative for required production inputs.
- A prepare includes the resolved source revision, config revision, secret references, resolved input classification, migration readiness, known-good snapshot (if available), and a materialized plan binding hash.
- `deploy apply` refuses to execute stale plans (expired, mismatched compose, changed source/config, input drift).
- `apply` verifies running containers, required volumes, and external health (`/healthz`, `/chat`) before returning ready.
- Rollback path to recorded known-good is prepared for recovery when explicitly allowed (`--auto-rollback`).
Operational sequence example:
```powershell
go run ./cmd/devctl deploy prepare --target production --compose deploy/docker-compose.prod.yml --env-file <fresh-production.env> --plan .devctl/deployment-plans/prod-<rev>.json
go run ./cmd/devctl deploy apply --plan .devctl/deployment-plans/prod-<rev>.json
```
`prepare`/`apply` are intentionally decoupled so a fresh Codex session with only repository checkout and durable sources can complete production deployment safely.
The first external operation adds `assistant-notification-receiver-1` on production host port `18081`. Caddy applies `deploy/caddy/assistent.caddy`, routing only `https://assistent.cin.su/delivery/*` to that port and all other paths to Creator Web on `18080`. The receiver has an isolated durable volume and no Assistant database access. Commit success means an immutable receiver receipt exists; Windows display and human reading are not Commit outcomes.
Migrations `010_capability_notification_binding.sql` and `011_commit_notification_envelope.sql` must be applied before the new runtimes start. The initial Capability binding is owner-local for `domovoy-creator-principal`, revision 1, capability `prospective-notification-delivery`, origin `https://assistent.cin.su`, delivery path `/delivery/v1/deliveries`, receipt path `/delivery/v1/receipts`, protocol `IDEMPOTENT_RECEIPT_V1`, and secret reference `domovoy-notification-write-v1`.
+2 -50
View File
@@ -1,52 +1,6 @@
# Assistant project status
Audit date: 2026-08-18. Active implementation: DEV-000072 revision 1.
## Production recovery checkpoint (2026-08-18)
- `PRODUCTION_RECOVERY_STATUS = RESTORED_TO_KNOWN_GOOD`
- `TARGET_REVISION = 11a6a23f6e4f44dd88265a04846419a27f54df98`
- `PREVIOUS_RUNNING_REVISION = 36fec6c4f8aabf5e9f5f4ac0d5e2f9c0b8a5a9f0`
- `CURRENT_RUNNING_REVISION = 11a6a23f6e4f44dd88265a04846419a27f54df98`
- `PRODUCTION_HEALTH = healthy`
- `POSTGRES_HEALTH = healthy`
- `CORE_RUNTIME_HEALTH = healthy`
- `CONTINUITY_RUNTIME_HEALTH = healthy`
- `COMMIT_RUNTIME_HEALTH = healthy`
- `CREATOR_WEB_HEALTH = healthy`
- `EXTERNAL_HEALTHZ = 200`
- `CHAT_UI = 200`
- `INCIDENT_EVIDENCE_PATH = docs/evidence/production-recovery-incident-20260818.md`
- `DEV_000072_STATUS = PRODUCTION_ACCEPTANCE_BLOCKED_BY_DEPLOYMENT_INCIDENT`
- `NEXT_REQUIRED_ACTION = run post-recovery evidence tasks for DEV-000072 acceptance and then continue with production deployment`
## DEV-000073 checkpoint
- `DEV_000073_STATUS = BLOCKED`
- `STATUS = PAUSED_PENDING_LONG_HORIZON_MODEL`
- `CURRENT_PHASE = DURABLE_BOOTSTRAP_PARTIAL_IMPLEMENTATION_STABILIZED`
- `DEV_000073_SCOPE = production deployment continuity + safe redeploy V1 (deploy prepare/apply)`
- `START_DATE = 2026-08-18`
- `ACTIVE_TASK = dev/tasks/DEV-000073-production-deployment-continuity-safe-redeploy.json`
- `LAST_VERIFIED_COMMIT = 36fec6c05cdf7aaf413ffe6b9437f07b03fc7b6b`
- `CURRENT_UNCOMMITTED_WORK = cmd/devctl/deploy.go (untracked in git index); cmd/devctl/deploy_test.go (untracked in git index); docs/evidence/deployment-continuity-dev000073.md (untracked in git index); supporting task/evidence files`
- `DEV_000073_EVIDENCE_PATH = docs/evidence/deployment-continuity-dev000073.md`
- `NEXT_ACTION = resume DEV-000073 from this checkpoint using a longer-horizon model to complete durable bootstrap review and production-safe seed plan`
- `BLOCKER = real sanitized production prepare cannot resolve all required inputs from intentional durable/protected sources`
- `GO_TEST_DEVCTL = EXECUTED_PASS`
- `CORE_OUTPUTS = cmd/devctl/deploy.go, cmd/devctl/deploy_test.go`
- `DOC_OUTPUTS = AGENTS.md, docs/DEPLOYMENT.md, docs/PROJECT_STATUS.md`
- `DEPLOYMENT_RULE = production mutation requires explicit prepared plan and fail-closed unresolved required input checks`
- `SEMANTIC_SAFETY = no runtime behavior change; no provider naming/memory changes; DEV-000072 remains unchanged and not promoted`
- `TEMP_ARTIFACT_CLEANUP = cmd/devctl/__tmp_devctl_patch_test.txt absent`
- `BLOCKERS = final durable bootstrap design/implementation review remains; pre-runtime protected Secret Resolver bridge and one-time production seed are not yet completed`
- `IMPORTANT_FINDINGS = deploy prepare/apply gates and negative path protections are functionally exercised; production continuity still depends on durable input source for real use`
- `EVIDENCE_OBTAINED = docs/evidence/deployment-continuity-dev000073.md`
- `COMPLETED = deployment prepare/apply safety mechanics; missing input fail-closed; stale-plan rejection; exact-artifact binding; secret-leak protection; health gate; migration discovery/precondition; seven-input ownership audit; canonical-vs-derived credential decision; current deploy.go partial bootstrap implementation compiles; go test ./cmd/devctl passes`
- `ARCHITECTURAL_DECISIONS_ALREADY_ACCEPTED = do not repeat seven-input ownership audit; DB CredentialRef is canonical; raw DB password is derived deployment/runtime transport; CREATOR_PASSWORD_FILE is runtime/mount transport; creator bootstrap token and session signing key require durable protected ownership`
- `IN_PROGRESS = final durable bootstrap design/implementation review; pre-runtime protected secret resolver bridge; one-time production canonical seed/migration; real fresh sanitized production prepare = READY`
- `REASON_FOR_PAUSE = available model is optimized for short interactive edits; remaining work requires long-horizon architecture + cross-component reasoning and production-state migration`
- `NEXT_REQUIRED_ACTION = resume DEV-000073 from this checkpoint using a stronger long-horizon Codex model`
Audit date: 2026-08-17. Active implementation: DEV-000072 revision 1.
## DEV-000072 checkpoint
@@ -66,10 +20,8 @@ Audit date: 2026-08-18. Active implementation: DEV-000072 revision 1.
- `RAW_MODEL_BOUNDARY = recorded explicitly: raw cognitive models identify themselves as provider/model provenance; this is not patched inside Interaction and remains a later context-compilation milestone`
- `TESTS_PASS = go test ./...; go vet ./...; go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json; go run ./cmd/devctl check-architecture; go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json; tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1; tests/owner_acl/verify.ps1 -DockerContext docker-prod -PostgresContainer assistant-postgres-1; ASSISTANT_LIVE_SEMANTIC_TRACE=1 go test ./cmd/cognitive-worker -run TestLiveInterpretationTrace -v`
- `COGNITIVE_CONTEXT_GAP = still open by design in this milestone: raw providers identify themselves as Qwen/Gemma provenance when Domovoy identity/context is not compiled into the prompt`
- `NON_GOAL_SCOPE_DEV_000072 = naming as memory domain is a semantic test case only in this task; no assistant_name subsystem, no favorite_color fields, no naming-specific memory table/schema, no explicit 'запомни'-only trigger, no universal ontology, and no graph DB are introduced for this deployment boundary`
- `GENERAL_MEMORY_REQUIREMENT = broader generic knowledge-retention admission (for preferences, names, constraints, relations, project facts, recurring patterns, and analogous domains) is out-of-scope for DEV-000072 and is recorded as UQ-016 in architecture/UNRESOLVED_QUESTIONS.md`
- `PRODUCTION_STATUS = pending promotion/deploy from this checkpoint`
- `NEXT_RECOMMENDED_STEP = finish DEV-000072 production promotion, apply migration 027, redeploy runtime, verify live /chat against the promoted artifact, then address broader memory/knowledge retention via a separate evidence-backed milestone`
- `NEXT_RECOMMENDED_STEP = finish DEV-000072 production promotion, apply migration 027, redeploy runtime, and verify live /chat against the promoted artifact`
## DEV-000071 checkpoint
@@ -1,169 +0,0 @@
# DEV-000073 Deployment Continuity Evidence (Checkpoint)
Audit date: 2026-08-18
Task ID: `DEV-000073`
Task revision: `1`
Base revision: `36fec6c05cdf7aaf413ffe6b9437f07b03fc7b6c`
## Status summary
- `DEV_000073_STATUS = BLOCKED`
- `CURRENT_PHASE = DURABLE_BOOTSTRAP_PARTIAL_IMPLEMENTATION_STABILIZED`
- `CHECKPOINT_STATUS = IN_PROGRESS`
- `LAST_VERIFIED_COMMIT = 36fec6c05cdf7aaf413ffe6b9437f07b03fc7b6b`
- `LAST_VERIFIED_TEST_COMMAND = go test ./cmd/devctl`
- `LAST_VERIFIED_TEST_RESULT = EXECUTED_PASS`
- `CURRENT_UNCOMMITTED_DIFF = cmd/devctl/deploy.go, cmd/devctl/deploy_test.go, docs/DEPLOYMENT.md, AGENTS.md, architecture/UNRESOLVED_QUESTIONS.md, dev/tasks/DEV-000073-production-deployment-continuity-safe-redeploy.json, docs/evidence/deployment-continuity-dev000073.md, docs/evidence/production-recovery-incident-20260818.md, docs/PROJECT_STATUS.md, dev/ACTIVE_TASK.json`
- `CURRENT_TASK_STATE = PAUSED_PENDING_LONG_HORIZON_MODEL`
## Executed checks
- `go test ./cmd/devctl` → `EXECUTED_PASS`
- `go run ./cmd/devctl preflight -task dev/tasks/DEV-000073-production-deployment-continuity-safe-redeploy.json` → `EXECUTED_PASS`
- `go run ./cmd/devctl check-diff -task dev/tasks/DEV-000073-production-deployment-continuity-safe-redeploy.json` → `EXECUTED_PASS`
- `go test ./cmd/devctl` targeted acceptance set (`TestDeployPrepareBlockedAndApplyRefusesMissingRequiredInput`, `TestAssertPlanCurrentRejectsStaleInputChange`, `TestAssertPlanCurrentRejectsArtifactMismatch`, `TestDeploymentPlanDoesNotLeakSecretValues`, `TestDeployApplyRunsHealthGateWithReadyPlanAndSimulatedComposeDocker`, `TestDeployApplyHealthGateRejectsOnFailedHTTPHealth`, migration precondition tests) → `EXECUTED_PASS`
## Task-specific acceptance probes
- `MISSING_INPUT_NEGATIVE_TEST = EXECUTED_PASS`
- missing required input in prepare → `readiness=BLOCKED`
- `deploy apply` with blocked plan returns non-zero
- `STALE_PLAN_NEGATIVE_TEST = EXECUTED_PASS`
- compose/input/content assumption changes in plan → `assertPlanCurrent` detects stale state
- `EXACT_ARTIFACT_NEGATIVE_TEST = EXECUTED_PASS`
- artifact change without plan refresh → `assertPlanCurrent` detects stale mismatch
- `SECRET_LEAK_TEST = EXECUTED_PASS`
- plan output and tests show no plaintext secret values; only source class and hashes persisted
- `HEALTH_GATE_TEST = EXECUTED_PASS`
- docker compose success + passing HTTP checks + docker inspect => apply may complete
- docker compose success + failed `/healthz` => apply fails
## Production continuity probe (sanitized shell)
- `PRODUCTION_REVISION_VERIFIED = 11a6a23f6e4f44dd88265a04846419a27f54df98`
- `REAL_PRODUCTION_PREPARE_READINESS = BLOCKED`
- `REAL_PRODUCTION_SANITIZED_PREPARE = NOT_READY`
- `REAL_PRODUCTION_CONFIG_SOURCE = TEMPORARY_SYNTHETIC`
- `DOCKER_PROD_BOOTSTRAP_ENV_SOURCE = secrets/docker-prod-bootstrap.env`
- `LEGACY_AMBIENT_DEPENDENCIES = none`
- `UNKNOWN_INPUTS = none`
- `KNOWN_INPUT_SOURCE_DETAIL` for required fields:
- `ASSISTANT_SOURCE_REVISION = missing`
- `CREATOR_WEB_PORT = missing`
- `NOTIFICATION_RECEIVER_PORT = missing`
- `POSTGRES_DB = file`
- all resolved/required production inputs include:
- resolved from file: `POSTGRES_SUPERUSER`, `POSTGRES_SUPERUSER_PASSWORD`, `CONTINUITY_DB_PASSWORD`, `WORK_DB_PASSWORD`, `CAPABILITY_DB_PASSWORD`, `COMMIT_DB_PASSWORD`, `AUTHORITY_DB_PASSWORD`, `IDENTITY_DB_PASSWORD`, `SITUATION_DB_PASSWORD`, `CREATOR_USERNAME`, `COMMIT_EXECUTOR_TOKEN`
- missing (blocking): `CREATOR_BOOTSTRAP_TOKEN`, `CREATOR_PASSWORD_FILE`, `CREATOR_SESSION_SIGNING_KEY`, `EPISTEMICS_POSTGRES_CREDENTIAL_REF`, `EPISTEMICS_POSTGRES_PASSWORD`, `INTERACTION_POSTGRES_CREDENTIAL_REF`, `INTERACTION_POSTGRES_PASSWORD`
## Configuration source classification (required production inputs)
- `POSTGRES_SUPERUSER -> BOOTSTRAP` (from env file path)
- `POSTGRES_SUPERUSER_PASSWORD -> SECRET_REFERENCE` (from env file path)
- `CONTINUITY_DB_PASSWORD -> SECRET_REFERENCE` (from env file path)
- `WORK_DB_PASSWORD -> SECRET_REFERENCE` (from env file path)
- `CAPABILITY_DB_PASSWORD -> SECRET_REFERENCE` (from env file path)
- `COMMIT_DB_PASSWORD -> SECRET_REFERENCE` (from env file path)
- `AUTHORITY_DB_PASSWORD -> SECRET_REFERENCE` (from env file path)
- `IDENTITY_DB_PASSWORD -> SECRET_REFERENCE` (from env file path)
- `SITUATION_DB_PASSWORD -> SECRET_REFERENCE` (from env file path)
- `CREATOR_USERNAME -> BOOTSTRAP` (from env file path)
- `CREATOR_PASSWORD_FILE -> DOMOVOY_MANAGED` (missing)
- `CREATOR_BOOTSTRAP_TOKEN -> SECRET_REFERENCE` (missing)
- `CREATOR_SESSION_SIGNING_KEY -> SECRET_REFERENCE` (missing)
- `COMMIT_EXECUTOR_TOKEN -> SECRET_REFERENCE` (from env file path)
- `EPISTEMICS_POSTGRES_CREDENTIAL_REF -> SECRET_REFERENCE` (missing)
- `INTERACTION_POSTGRES_CREDENTIAL_REF -> SECRET_REFERENCE` (missing)
- `EPISTEMICS_POSTGRES_PASSWORD -> SECRET_REFERENCE` (missing)
- `INTERACTION_POSTGRES_PASSWORD -> SECRET_REFERENCE` (missing)
- `CONFIG_SOURCE_CLASSIFICATION = TEMPORARY_SYNTHETIC`
## Migration readiness
- `CURRENT_MIGRATION_STATE_SOURCE = git ls-tree -r --name-only <revision> database/migrations`
- `CANDIDATE_MIGRATION_REQUIREMENT = current_max=27 target_max=27 status=matched need_known_good=false (for plan created from 11a6a23f6e4f...)`
- `MIGRATION_DISCOVERY_STATUS = PASS`
- `PRECONDITION_ENFORCEMENT = PASS`
- `MIGRATION_PRECONDITION_STATUS = PASS` (plan execution enforces migration precondition and rejects upgrade-without-known-good as blocking)
- `ROLLBACK_COMPATIBILITY_STATUS = not reached by this evidence run; migration check is enforced before compose mutation`
## Gaps / blockers
- `BLOCKER = REAL_PRODUCTION_SANITIZED_PREPARE depends on `secrets/docker-prod-bootstrap.env` and still has 7 required missing inputs`
- `BLOCKER = production bootstrap source is temporary/synthetic in this environment; no durable/protected production bootstrap source has been established here`
- `REASON_FOR_PAUSE = remaining work requires long-horizon model for durable bootstrap review, resolver bridge completion, and production seed migration planning`
## Evidence persisted
- `evidence/evidence file: docs/evidence/deployment-continuity-dev000073.md`
- `checkpoint: docs/PROJECT_STATUS.md`
## Durable Ownership Audit (DEV-000073)
INPUT_OWNERSHIP_MATRIX
| INPUT_NAME | WHY_NEEDED | SEMANTIC_OWNER | CANONICAL_STATE | SECRET | INTENDED_DURABLE_SOURCE | CURRENT_ACTUAL_SOURCE | RESOLUTION_PATH | WHY_CURRENT_PREPARE_CANNOT_RESOLVE_IT |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| CREATOR_BOOTSTRAP_TOKEN | bootstrap token for the Creator domain startup/authentication bootstrap flow | identity | yes | yes | durable secret input for production bootstrap (protected secret value) | missing from sanitized workspace prepare inputs | deploy.prepare file/env resolution only (`inputSourceClass=SECRET_REFERENCE`) | no durable bootstrap record supplied for this token in this environment; prepare does not have a secret backend lookup step |
| CREATOR_PASSWORD_FILE | creator password secret-file locator consumed by core runtime identity startup | identity | no | no | domovoy-managed runtime mount under `/run/secrets` (`core-runtime` creator password path) | provided in compose as a fixed container path but not as resolved prepare input | deploy.prepare accepts `_FILE` as an input class but only from env file/ambient and does not consume compose defaults | compose default is outside deploy.prepare source model, so required input remains unresolved even when container expects the path |
| CREATOR_SESSION_SIGNING_KEY | key for secure creator session signing | identity | yes | yes | durable secret input for production bootstrap (protected secret value) | missing from sanitized workspace prepare inputs | deploy.prepare file/env resolution only (`inputSourceClass=SECRET_REFERENCE`) | no durable bootstrap source in this environment; no value in env file and not provided by resolver bootstrap path |
| EPISTEMICS_POSTGRES_CREDENTIAL_REF | identity of epistemics credential record for DB secret resolution | epistemics | yes | no | protected durable secret-reference declaration for the epistemics DB credential record | missing from sanitized workspace prepare inputs | deploy.prepare expects direct env value (`SECRET_REFERENCE`) | credential reference value is absent and there is no durable bootstrap source for this reference name |
| EPISTEMICS_POSTGRES_PASSWORD | DB password transport input currently listed as required by deploy | epistemics | no | yes | derived at runtime from `EPISTEMICS_POSTGRES_CREDENTIAL_REF` + `EPISTEMICS_POSTGRES_PASSWORD_FILE` through local secret resolver | not present as input in prepare; compose sets file-based runtime path for password value | unresolved by deploy.prepare because it is treated as legacy required input, while runtime reads file-backed path | this value is rendered runtime transport data; deploy.prepare contract has no mapping from `*_CREDENTIAL_REF` to `*_PASSWORD` |
| INTERACTION_POSTGRES_CREDENTIAL_REF | identity of interaction credential record for DB secret resolution | interaction | yes | no | protected durable secret-reference declaration for the interaction DB credential record | missing from sanitized workspace prepare inputs | deploy.prepare expects direct env value (`SECRET_REFERENCE`) | credential reference value is absent and there is no durable bootstrap source for this reference name |
| INTERACTION_POSTGRES_PASSWORD | DB password transport input currently listed as required by deploy | interaction | no | yes | derived at runtime from `INTERACTION_POSTGRES_CREDENTIAL_REF` + `INTERACTION_POSTGRES_PASSWORD_FILE` through local secret resolver | not present as input in prepare; compose sets file-based runtime path for password value | unresolved by deploy.prepare because it is treated as legacy required input, while runtime reads file-backed path | this value is rendered runtime transport data; deploy.prepare contract has no mapping from `*_CREDENTIAL_REF` to `*_PASSWORD` |
CREDENTIAL_REF_PASSWORD_DUPLICATION_ASSESSMENT
`B) CredentialRef is canonical and PASSWORD is derived/rendered runtime transport value.`
Rationale:
- `cmd/core-runtime/main.go` `readCredentialRefSecret(name, refName)` rejects direct `name` secret values (`direct environment secret is forbidden`) and requires `name_FILE` + `<name>_CREDENTIAL_REF`.
- It resolves credentials through `platformsecrets.NewFileResolver` using `SecretFileRef{CredentialRef: ref, Purpose: DatabasePassword}`.
- The compose layout includes `<OWNER>_POSTGRES_CREDENTIAL_REF` and `<OWNER>_POSTGRES_PASSWORD_FILE`, reinforcing the canonical-reference + rendered-file pattern.
CREATOR_INPUT_OWNERSHIP
- `CREATOR_BOOTSTRAP_TOKEN`
- Real owner: `identity`.
- Real durable source class: protected secret (identity bootstrap token), **not** in repo and not present in sanitized prepare input.
- Current classification: missing required input in this environment.
- `CREATOR_SESSION_SIGNING_KEY`
- Real owner: `identity`.
- Real durable source class: protected secret (identity session signing key), **not** in repo and not present in sanitized prepare input.
- Current classification: missing required input in this environment.
- `CREATOR_PASSWORD_FILE`
- Real owner: `identity` (runtime auth material locator).
- It is a **rendered runtime path** backed by Domovoy/runtime secret mount conventions (`/run/secrets`) and is **not** a canonical secret value.
- Current classification: compose-fixed path, not a standalone durable deploy input for this phase.
DOCKER_PROD_BOOTSTRAP_ENV_ROLE
`TEMPORARY_SYNTHETIC`
Why: the file is a local repository bootstrap artifact used for sanitized shell-level prepare/apply probing; it is not an intentional durable/protected production source and still lacks required durable secret inputs. It is neither a Domovoy-managed state artifact nor a recoverable provenance artifact for real production bootstrap.
EXISTING_SECRET_RESOLVER_CAN_SUPPLY_MISSING_INPUTS
`partial`
- adapter? `no` (deploy.prepare does not invoke runtime secret adapter; this check is done in runtime after plan creation)
- bootstrap locator? `missing` (no durable, environment-wide bootstrap secret locator for all 7 inputs is present in this workspace)
- credential record? `partially present in contracts, not provisioned here` (code path expects `*_PASSWORD_FILE` + `*_CREDENTIAL_REF`, but missing refs/records in this environment)
- protected backend entry? `missing` (no proven production-grade protected entry available via the current prepare evidence run)
- ownership mapping? `partial` (owners/areas can be inferred from contracts and module code, but prepare-time ownership policy is not the missing-input resolver)
- pre-runtime bootstrap cycle? `not demonstrated` (no cycle blocker)
BOOTSTRAP_CYCLE_EXISTS
`no`
No real cycle was found because secret resolution uses local file-backed resolver in core runtime and is not a dependency of Domovoy boot for deployment plan creation in this repository evidence path.
MINIMUM_IMPLEMENTATION_GAP
- Provide a durable/protected bootstrap source for the remaining required identity inputs (`CREATOR_BOOTSTRAP_TOKEN`, `CREATOR_SESSION_SIGNING_KEY`, `CREATOR_PASSWORD_FILE`) and epistemics/interaction credential refs.
- Align deploy input contract so production input readiness depends on the canonical pair inputs (`*_CREDENTIAL_REF` + `*_PASSWORD_FILE`) and not legacy `<OWNER>_POSTGRES_PASSWORD` as a required direct input.
- Add evidence-backed mapping for each canonical input’s ownership/source class and bootstrap provisioning path before proceeding to acceptance/ready status.
CHECKPOINT_UPDATED = yes
EVIDENCE_PERSISTED = yes
@@ -1,166 +0,0 @@
[
{
"id": "A",
"status": 200,
"message": "ты кто",
"conversation_id": "2ec5b5b9c3e742a7a3dce894020da7ca",
"utterance_id": "86fb129b40d2f2fbb4ab798d863bf35d",
"speech_act": "QUESTION",
"cognition": "COMPLETED",
"clarification": "NOT_REQUIRED",
"project_provenance": "",
"active_focus": "",
"capability_status": "",
"capability_kind": "",
"delegation_id": "",
"delegation_state": "",
"work_planned": false,
"reminder_scheduled": false,
"reply": "Я искусственный интеллект, созданный для помощи. Как я могу быть полезен сейчас?"
},
{
"id": "B",
"status": 200,
"message": "сколько будет 2 + 2",
"conversation_id": "2ec5b5b9c3e742a7a3dce894020da7ca",
"utterance_id": "570663950030add34f71cedb24f4256d",
"speech_act": "QUESTION",
"cognition": "COMPLETED",
"clarification": "NOT_REQUIRED",
"project_provenance": "",
"active_focus": "",
"capability_status": "",
"capability_kind": "",
"delegation_id": "",
"delegation_state": "",
"work_planned": false,
"reminder_scheduled": false,
"reply": "2 + 2 equals 4."
},
{
"id": "C",
"status": 200,
"message": "как с утра",
"conversation_id": "2ec5b5b9c3e742a7a3dce894020da7ca",
"utterance_id": "008192deff274c8ab7678c180ab7e04d",
"speech_act": "QUESTION",
"cognition": "COMPLETED",
"clarification": "NOT_REQUIRED",
"project_provenance": "",
"active_focus": "",
"capability_status": "",
"capability_kind": "",
"delegation_id": "",
"delegation_state": "",
"work_planned": false,
"reminder_scheduled": false,
"reply": "Утро - это прекрасное время для начала нового дня. Начните с здорового питания, которое поможет вам начать день с бодрости и энергии. Не забудьте о hydratation, выпив适量的水. Уделите время саморазвитию, например, чтению или медитации, чтобы зарядить позитивом весь день."
},
{
"id": "D",
"status": 200,
"message": "проведи анализ: почему тормозит база, и потом добавь в задачу пометку для проверки",
"conversation_id": "2ec5b5b9c3e742a7a3dce894020da7ca",
"utterance_id": "4607460cd39d273d99ae50a7d8315517",
"speech_act": "QUESTION",
"cognition": "not_requested",
"clarification": "REQUIRED",
"project_provenance": "",
"active_focus": "",
"capability_status": "",
"capability_kind": "",
"delegation_id": "",
"delegation_state": "",
"work_planned": false,
"reminder_scheduled": false,
"reply": "У меня есть несколько возможных ожиданий. Уточните, о каком именно речь: дедлайн в пятницу; дедлайн в пятницу; дедлайн в пятницу"
},
{
"id": "E",
"message": "исправь прошлую формулировку",
"status": 502,
"body": "chat unavailable\n"
},
{
"id": "F",
"message": "обратись к модели gemma4 и спроси сколько звезд на небе",
"status": 502,
"body": "chat unavailable\n"
},
{
"id": "G",
"status": 200,
"message": "просто напомни, что нужно проверить",
"conversation_id": "2ec5b5b9c3e742a7a3dce894020da7ca",
"utterance_id": "89b69b8251258a689f38de98552cbed4",
"speech_act": "QUESTION",
"cognition": "not_requested",
"clarification": "REQUIRED",
"project_provenance": "",
"active_focus": "",
"capability_status": "",
"capability_kind": "",
"delegation_id": "",
"delegation_state": "",
"work_planned": false,
"reminder_scheduled": false,
"reply": "Нужно короткое уточнение, прежде чем выбирать capability или строить более тяжёлый маршрут."
},
{
"id": "H",
"status": 200,
"message": "Запомни тебя зовут Домовой, я тебя буду звать Домовым",
"conversation_id": "2ec5b5b9c3e742a7a3dce894020da7ca",
"utterance_id": "0c3bcf9b8f423e93b1b255aa33a557db",
"speech_act": "ASSERTION",
"cognition": "not_requested",
"clarification": "NOT_REQUIRED",
"project_provenance": "",
"active_focus": "",
"capability_status": "",
"capability_kind": "",
"delegation_id": "",
"delegation_state": "",
"work_planned": false,
"reminder_scheduled": false,
"reply": "Записал как утверждение человека. Это пока не проверенный факт."
},
{
"id": "H2",
"status": 200,
"message": "для меня ты Домовой",
"conversation_id": "2ec5b5b9c3e742a7a3dce894020da7ca",
"utterance_id": "7847420f28c85894400c6a5be3e760d0",
"speech_act": "ASSERTION",
"cognition": "not_requested",
"clarification": "NOT_REQUIRED",
"project_provenance": "",
"active_focus": "",
"capability_status": "",
"capability_kind": "",
"delegation_id": "",
"delegation_state": "",
"work_planned": false,
"reminder_scheduled": false,
"reply": "Записал как утверждение человека. Это пока не проверенный факт."
},
{
"id": "H3",
"status": 200,
"message": "давай я буду звать тебя Домовым",
"conversation_id": "2ec5b5b9c3e742a7a3dce894020da7ca",
"utterance_id": "085e4194a61cbfd097118553342e063a",
"speech_act": "ASSERTION",
"cognition": "not_requested",
"clarification": "NOT_REQUIRED",
"project_provenance": "",
"active_focus": "",
"capability_status": "",
"capability_kind": "",
"delegation_id": "",
"delegation_state": "",
"work_planned": false,
"reminder_scheduled": false,
"reply": "Записал как утверждение человека. Это пока не проверенный факт."
}
]
@@ -1,184 +0,0 @@
[
{
"id": "A",
"status": 200,
"message": "ты кто",
"conversation_id": "5e5ba4546dc20a4f6180d674a276fb5a",
"utterance_id": "7d14f020fc2134f0ba995c35ebe31523",
"speech_act": "QUESTION",
"cognition": "COMPLETED",
"clarification": "NOT_REQUIRED",
"reply": "Я искусственный интеллект, созданный для помощи. Как я могу быть полезен сейчас?",
"work": false,
"capability_status": "",
"capability_kind": "",
"delegation_state": "",
"project_provenance": "",
"active_focus": ""
},
{
"id": "B",
"status": 200,
"message": "сколько будет 2 + 2",
"conversation_id": "5e5ba4546dc20a4f6180d674a276fb5a",
"utterance_id": "0a739b4810420e0eb0615d8d4cd4b60b",
"speech_act": "QUESTION",
"cognition": "COMPLETED",
"clarification": "NOT_REQUIRED",
"reply": "2 + 2 equals 4.",
"work": false,
"capability_status": "",
"capability_kind": "",
"delegation_state": "",
"project_provenance": "",
"active_focus": ""
},
{
"id": "C",
"status": 200,
"message": "как с утра",
"conversation_id": "5e5ba4546dc20a4f6180d674a276fb5a",
"utterance_id": "4463c0cc31a552a23c0c0ba4eff06554",
"speech_act": "QUESTION",
"cognition": "COMPLETED",
"clarification": "NOT_REQUIRED",
"reply": "Утро - это прекрасное время для начала нового дня. Начните с здорового питания, которое поможет вам начать день с бодрости и энергии. Не забудьте о hydratation, выпив适量的水. Уделите время саморазвитию, например, чтению или медитации, чтобы зарядить позитивом весь день.",
"work": false,
"capability_status": "",
"capability_kind": "",
"delegation_state": "",
"project_provenance": "",
"active_focus": ""
},
{
"id": "D",
"status": 200,
"message": "проведи анализ: почему тормозит база, и потом добавь в задачу пометку для проверки",
"conversation_id": "5e5ba4546dc20a4f6180d674a276fb5a",
"utterance_id": "c80c1eb2ce08edca3add568825c444d7",
"speech_act": "QUESTION",
"cognition": "not_requested",
"clarification": "REQUIRED",
"reply": "У меня есть несколько возможных ожиданий. Уточните, о каком именно речь: дедлайн в пятницу; дедлайн в пятницу; дедлайн в пятницу",
"work": false,
"capability_status": "",
"capability_kind": "",
"delegation_state": "",
"project_provenance": "EXPLICIT",
"active_focus": "УПО"
},
{
"id": "E",
"status": 502,
"message": "исправь прошлую формулировку",
"error": "request_failed"
},
{
"id": "F",
"status": 502,
"message": "обратись к модели gemma4 и спроси сколько звезд на небе",
"error": "request_failed"
},
{
"id": "G",
"status": 200,
"message": "просто напомни, что нужно проверить",
"conversation_id": "5e5ba4546dc20a4f6180d674a276fb5a",
"utterance_id": "0519382bed09ae5ea9c9a3b2f2af2641",
"speech_act": "QUESTION",
"cognition": "not_requested",
"clarification": "REQUIRED",
"reply": "Нужно короткое уточнение, прежде чем выбирать capability или строить более тяжёлый маршрут.",
"work": false,
"capability_status": "",
"capability_kind": "",
"delegation_state": "",
"project_provenance": "",
"active_focus": ""
},
{
"id": "H",
"status": 200,
"message": "Запомни тебя зовут Домовой",
"conversation_id": "5e5ba4546dc20a4f6180d674a276fb5a",
"utterance_id": "aae512074bf81adb405c0c80331c9096",
"speech_act": "ASSERTION",
"cognition": "not_requested",
"clarification": "NOT_REQUIRED",
"reply": "Записал как утверждение человека. Это пока не проверенный факт.",
"work": false,
"capability_status": "",
"capability_kind": "",
"delegation_state": "",
"project_provenance": "",
"active_focus": ""
},
{
"id": "H2",
"status": 200,
"message": "для меня ты Домовой",
"conversation_id": "5e5ba4546dc20a4f6180d674a276fb5a",
"utterance_id": "78a171bb2f405e72f98f3b19d7422514",
"speech_act": "ASSERTION",
"cognition": "not_requested",
"clarification": "NOT_REQUIRED",
"reply": "Записал как утверждение человека. Это пока не проверенный факт.",
"work": false,
"capability_status": "",
"capability_kind": "",
"delegation_state": "",
"project_provenance": "",
"active_focus": ""
},
{
"id": "H3",
"status": 200,
"message": "давай я буду звать тебя Домовым",
"conversation_id": "5e5ba4546dc20a4f6180d674a276fb5a",
"utterance_id": "382bd2310ed4a86f671f02c45afe3aee",
"speech_act": "ASSERTION",
"cognition": "not_requested",
"clarification": "NOT_REQUIRED",
"reply": "Записал как утверждение человека. Это пока не проверенный факт.",
"work": false,
"capability_status": "",
"capability_kind": "",
"delegation_state": "",
"project_provenance": "",
"active_focus": ""
},
{
"id": "H4",
"status": 200,
"message": "Вернемся к УПО",
"conversation_id": "5e5ba4546dc20a4f6180d674a276fb5a",
"utterance_id": "8cb57dcf2599947673d1c1a4bb154266",
"speech_act": "ASSERTION",
"cognition": "not_requested",
"clarification": "NOT_REQUIRED",
"reply": "Фокус переключён: УПО.",
"work": false,
"capability_status": "",
"capability_kind": "",
"delegation_state": "",
"project_provenance": "EXPLICIT",
"active_focus": "УПО"
},
{
"id": "I",
"status": 200,
"message": "помоги ответить на письмо бухгалтеру",
"conversation_id": "5e5ba4546dc20a4f6180d674a276fb5a",
"utterance_id": "ebeacce8340779490936e702b67543fa",
"speech_act": "ASSERTION",
"cognition": "not_requested",
"clarification": "NOT_REQUIRED",
"reply": "Сейчас у меня ещё не подключена возможность отправлять почту. Я зафиксировал capability gap, без ложного выполнения.",
"work": false,
"capability_status": "UNAVAILABLE",
"capability_kind": "SEND_EMAIL",
"delegation_state": "",
"project_provenance": "",
"active_focus": ""
}
]
@@ -1,82 +0,0 @@
## Production Recovery Incident Log
Date: 2026-08-18
Objective: Restore last known-good production with minimal changes, no code deploy from DEV-000072.
### 1) Pre-recovery verification (reconfirmed)
- Last observed bad running revision: `36fec6c4f8aabf5e9f5f4ac0d5e2f9c0b8a5a9f0`.
- Last known-good revision from durable manifests: `11a6a23f6e4f44dd88265a04846419a27f54df98`.
- Known-good deployment artifact IDs were present in `docker-prod` and matched both:
- `%LOCALAPPDATA%\Domovoy\monitoring\deployment-manifest.json`
- `\\nas\MST\codex\_monitoring\domovoy\deployment-manifest.json`
- Pre-recovery container state:
- `assistant-postgres-1` was running but unhealthy.
- `assistant-notification-receiver-1` healthy.
- `assistant-core-runtime-1`, `assistant-commit-runtime-1`, `assistant-cognitive-worker-1`, `assistant-creator-web-1` were `Created`.
- `assistant-continuity-runtime-1` was restarting.
- Continuity startup logs repeatedly reported `COMMIT_EXECUTOR_TOKEN is required`.
- DB read checks were reachable through `postgres` role and owner roles existed; schema objects in main owner schemas were present, including `interaction.utterance_interpretations` + `interaction.semantic_candidates` and expected 4 semantic columns.
### 2) Recovered configuration inputs
Classification used:
- `DURABLE_CANONICAL`: long-lived non-secret production evidence.
- `PROTECTED_DURABLE`: host/docker secret artifacts that are persistent, permission-protected inputs.
- `BREAK_GLASS_RUNTIME_EVIDENCE`: local runtime incident recovery metadata used only after verification.
- `POSTGRES_SUPERUSER` / passwords:
- `DURABLE_CANONICAL`: compose contract requires them.
- `PROTECTED_DURABLE`: secret-backed postgres volumes for password material.
- `BREAK_GLASS_RUNTIME_EVIDENCE`: `secrets/production-recovery.env` provided non-empty values and enabled rollback configuration.
- `COMMIT_EXECUTOR_TOKEN`:
- `DURABLE_CANONICAL`: required by continuity/commit contracts.
- `BREAK_GLASS_RUNTIME_EVIDENCE`: populated from `secrets/production-recovery.env`.
- `CREATOR_USERNAME`:
- `BREAK_GLASS_RUNTIME_EVIDENCE`: `secrets/production-recovery.env` (`m`).
- `epistemics_db_password`, `interaction_db_password`, `creator_bootstrap_token`, `creator_password`, `creator_session_signing_key`,
`notification writer/reader tokens`:
- `PROTECTED_DURABLE`: existing Docker volumes `assistant_core_secrets`,
`assistant_notification_writer_secret`, `assistant_notification_reader_secret`.
### 3) Recovery plan (finalized)
- Target revision: `11a6a23f6e4f44dd88265a04846419a27f54df98`.
- Keep durable data volumes unchanged:
- `assistant_assistant_pg`
- `assistant_core_secrets`
- `assistant_notification_writer_secret`
- `assistant_notification_reader_secret`
- `assistant_notification_receipts`
- `assistant_nats`
- Action set:
1. Retag known-good images from local/NAS manifests as active `assistant-* : latest`.
2. Recreate assistant stack on `docker-prod` with `--env-file secrets/production-recovery.env`, `--no-build`, `--force-recreate`.
3. Verify health and endpoint checks listed in section 5.
### 4) Recovery actions executed
- Recreated all assistant services on `docker-prod` (and `nats`) from known-good image IDs.
- Loaded production recovery env (`secrets/production-recovery.env`) into compose execution.
- Confirmed required runtime env is now present in service env:
- continuity/commit `COMMIT_EXECUTOR_TOKEN` is set.
- db user/passwords + creator bootstrap wiring are set.
- Verified all services now run with revision `11a6a23f6e4f44dd88265a04846419a27f54df98`.
- No credential values were printed in logs or evidence text.
### 5) Verification results
- `POSTGRES_HEALTH`: OK (postgres accepts connections).
- `CORE_RUNTIME_HEALTH`: OK (`/healthz` returns runtime ok).
- `CONTINUITY_RUNTIME_HEALTH`: OK (`/healthz` returns runtime ok).
- `COMMIT_RUNTIME_HEALTH`: OK (`/healthz` returns runtime ok).
- `CREATOR_WEB_HEALTH`: OK (`/healthz` returns runtime ok; container healthy).
- `EXTERNAL_HEALTH`: `https://assistant.cin.su/healthz` returns `HTTP 200`.
- `CHAT_SMOKE`: `https://assistant.cin.su/chat` returns `HTTP 200`; authenticated smoke message sent successfully via `POST /api/chat/messages` after session creation (harmless message `health check`), returning `HTTP 200`.
- Production artifact confirmation succeeded for revision `11a6a23f6e4f44dd88265a04846419a27f54df98` with 7 services.
### 6) Final state
- `KNOWN_GOOD_IMAGE_STATUS`: all required known-good image IDs present and active.
- `PRODUCTION_REVISION_AFTER_RECOVERY`: `11a6a23f6e4f44dd88265a04846419a27f54df98`.
- `DATA_INTEGRITY_STATUS`: preserved; existing schemas and `interaction.utterance_interpretations` semantic columns remain present.
- `CREDENTIAL_ROTATIONS`: none (used recovered durable config; no new credentials generated).
- Remaining limitation: commit/execution of this recovery changed image tags and container recreate timing only; no code/database migrations were altered.
### 7) Task status after recovery
- `DEV-000072_STATUS`: `PRODUCTION_ACCEPTANCE_BLOCKED_BY_DEPLOYMENT_INCIDENT` remains.
- Continue production readiness requires normal restart of production acceptance run from `docs/PROJECT_STATUS` after this incident checkpoint.
-23
View File
@@ -125,10 +125,6 @@ func BuildDeterministicCandidate(message string, corrects interaction.HumanUtter
tokens := semanticTokens(message)
hasQuestionMark := strings.Contains(message, "?")
scores := semanticScores(tokens, hasQuestionMark, corrects != "")
if isIdentityNamingPreferenceDirective(tokens) {
scores[interaction.SpeechActDirective] += 5
scores[interaction.SpeechActPreference] += 4
}
primary, primaryScore := topAct(scores)
secondary, secondaryScore := secondAct(scores, primary)
if (startsWithModalRequest(tokens) || startsWithExplanatoryRequest(tokens, "расска", "раска", "объяс", "опиш", "покаж", "подскаж")) &&
@@ -174,22 +170,6 @@ func BuildDeterministicCandidate(message string, corrects interaction.HumanUtter
return candidate
}
func isIdentityNamingPreferenceDirective(tokens []string) bool {
if !containsToken(tokens, "ты") && !containsToken(tokens, "тебя") && !containsToken(tokens, "тебе") && !containsToken(tokens, "вам") {
return false
}
if containsStemInTokens(tokens, "запомни", "назови", "назвать", "назвав", "зови", "название", "называ", "обраща", "обращай", "обращайся", "звать", "зов", "зовут") {
return true
}
if containsStemInTokens(tokens, "домов", "assist", "ассист", "assistant", "assistant") {
return true
}
if containsStemInTokens(tokens, "буду", "будешь") && containsStemInTokens(tokens, "звать", "обращ", "называть") {
return true
}
return false
}
func semanticTokens(message string) []string {
lower := strings.ToLower(strings.TrimSpace(message))
replacer := strings.NewReplacer(".", " ", ",", " ", "!", " ", "?", " ", ":", " ", ";", " ", "(", " ", ")", " ", "\"", " ", "'", " ", "«", " ", "»", " ", "+", " + ", "-", " - ", "/", " / ")
@@ -389,9 +369,6 @@ func applicabilityFor(primary, secondary interaction.SpeechAct, primaryScore, se
}
func needsCognitionForCandidate(primary, secondary interaction.SpeechAct, tokens []string) bool {
if primary == interaction.SpeechActDirective && secondary == interaction.SpeechActPreference && isIdentityNamingPreferenceDirective(tokens) {
return false
}
if primary == interaction.SpeechActQuestion || primary == interaction.SpeechActRequestToInvestigate {
return true
}
-19
View File
@@ -93,25 +93,6 @@ func TestDeterministicCandidateTreatsModalExplanationRequestsAsCognitionWorthWit
}
}
func TestDeterministicCandidateClassifiesIdentityNamingPreferenceAsDirective(t *testing.T) {
for _, message := range []string{
"Запомни тебя зовут Домовой, я тебя буду звать Домовым",
"я буду звать тебя Домовым",
"давай я буду звать тебя Домовой",
"теперь буду обращаться к тебе как к Домовой",
"запомни, я называю тебя Домовым",
"для меня ты Домовой",
} {
candidate := BuildDeterministicCandidate(message, "", time.Unix(10, 0).UnixMilli())
if candidate.PrimarySpeechAct != interaction.SpeechActDirective {
t.Fatalf("expected directive for %q, got %+v", message, candidate)
}
if candidate.NeedsCognition {
t.Fatalf("expected no cognition for naming preference by default for %q, got %+v", message, candidate)
}
}
}
func TestDeterministicCandidateTreatsModelRequestAsDirectiveNeedingCognition(t *testing.T) {
candidate := BuildDeterministicCandidate("обратись к модели gemma4 и спроси сколько звезд на небе", "", time.Unix(10, 0).UnixMilli())
if candidate.PrimarySpeechAct != interaction.SpeechActDirective {
+1 -5
View File
@@ -30,10 +30,6 @@ func InterpretUtterance(utterance interaction.HumanUtterance, projects []situati
deterministicCandidate := BuildDeterministicCandidate(utterance.Content, utterance.CorrectsUtteranceID, millis)
deterministicCandidate.UtteranceID = utterance.ID
selectedCandidate := deterministicCandidate
assertionVerification := string(epistemics.VerificationNotApplicable)
if selectedCandidate.PrimarySpeechAct == interaction.SpeechActAssertion {
assertionVerification = string(epistemics.VerificationUnverified)
}
result := SemanticResult{
Interpretation: interaction.UtteranceInterpretation{
ID: interaction.InterpretationID(interpretationID),
@@ -42,7 +38,7 @@ func InterpretUtterance(utterance interaction.HumanUtterance, projects []situati
SecondarySpeechAct: selectedCandidate.SecondarySpeechAct,
ConfidenceMillis: selectedCandidate.ConfidenceMillis,
Source: string(selectedCandidate.Source),
AssertionVerification: assertionVerification,
AssertionVerification: string(epistemics.VerificationUnverified),
RequestedOutcome: selectedCandidate.RequestedOutcome,
Clarification: selectedCandidate.Clarification,
NeedsCognition: selectedCandidate.NeedsCognition,
@@ -66,16 +66,6 @@ func TestInterpretUtteranceQuestionAboutCurrentWorkUsesFocus(t *testing.T) {
}
}
func TestInterpretUtteranceIdentityNamingPreferenceNotAssertion(t *testing.T) {
result := interpretForTest(t, "Запомни тебя зовут Домовой, я тебя буду звать Домовым", nil, "")
if result.Interpretation.SpeechAct != interaction.SpeechActDirective {
t.Fatalf("expected directive for naming preference, got %s", result.Interpretation.SpeechAct)
}
if result.Interpretation.AssertionVerification == "UNVERIFIED" {
t.Fatalf("expected no assertion semantics path for naming preference, got %#v", result.Interpretation)
}
}
func TestInterpretUtteranceInheritedFocusForThere(t *testing.T) {
result := interpretForTest(t, "Добавь туда новую настройку.", activeFocus("assistant", "Assistant"), "")
if len(result.Projects) != 1 || result.Projects[0].ProjectID != "assistant" || result.Projects[0].Provenance != situation.ProjectResolutionInheritedFocus {