Compare commits

...
Author SHA1 Message Date
m fdf38196f0 Merge remote-tracking branch 'origin/main' into codex/wp-400-graph-harness
CI / test (push) Successful in 22s
CI / test (pull_request) Successful in 21s
2026-08-22 14:12:29 +03:00
m b753761e1a chore(governance): include file-backed acceptance in worker task
CI / test (push) Successful in 22s
CI / test (pull_request) Successful in 22s
2026-08-22 14:11:10 +03:00
m 5faa5321dd test(creator-web): support file-backed acceptance secrets
CI / test (push) Failing after 16s
CI / test (pull_request) Failing after 16s
2026-08-22 14:09:46 +03:00
m 7271d7950d Merge pull request 'feat(runtime): production workers and cabinet acceptance' (#100) from codex/wp-400-graph-harness into main
CI / test (push) Successful in 28s
2026-08-22 11:03:21 +00:00
m 533243da25 Merge remote-tracking branch 'origin/main' into codex/wp-400-graph-harness
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 23s
2026-08-22 14:01:48 +03:00
m b1fba3ccef chore(dev): advance active worker task
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 21s
2026-08-22 13:58:39 +03:00
m b0afa2417f feat(runtime): run durable workers continuously
CI / test (push) Failing after 15s
CI / test (pull_request) Failing after 15s
2026-08-22 13:57:08 +03:00
m 6f5f7e1ee3 test(creator-web): cover cabinet acceptance contract
CI / test (push) Failing after 15s
2026-08-22 13:49:03 +03:00
m 94c5bcb017 Merge pull request 'feat(work): add deterministic graph harness' (#99) from codex/wp-400-graph-harness into main
CI / test (push) Failing after 15s
Reviewed-on: #99
2026-08-22 10:47:42 +00:00
m 3f7c4705fe Merge pull request 'feat(work): add durable agent dispatch queue' (#98) from codex/wp-301-agent-worker into main
CI / test (push) Successful in 22s
Reviewed-on: #98
2026-08-22 10:47:26 +00:00
m 1ccd43c552 feat(work): add governed self-development gate
CI / test (push) Failing after 15s
CI / test (pull_request) Failing after 14s
2026-08-22 13:46:01 +03:00
m d38406fbc5 feat(capability): add fleet readiness policy
CI / test (push) Failing after 15s
2026-08-22 13:43:36 +03:00
m f73c69960c feat(epistemics): add durable reflection worker
CI / test (push) Failing after 16s
2026-08-22 13:41:44 +03:00
m 32e33e3050 feat(epistemics): compile truth evidence bundle
CI / test (push) Successful in 24s
2026-08-22 13:37:22 +03:00
m e9bb42cd07 feat(work): persist graph revisions
CI / test (push) Successful in 21s
2026-08-22 13:33:54 +03:00
m 4a800df1e1 feat(work): add graph admission scheduler
CI / test (push) Successful in 22s
2026-08-22 13:31:39 +03:00
m b2f922cbbc feat(work): add deterministic graph harness
CI / test (push) Successful in 22s
2026-08-22 13:29:43 +03:00
m a4450348a1 feat(work): add one-shot agent worker recovery
CI / test (push) Successful in 21s
CI / test (pull_request) Successful in 21s
2026-08-22 13:26:52 +03:00
m 323a67a7d5 feat(work): add durable agent dispatch queue
CI / test (push) Successful in 23s
2026-08-22 13:24:24 +03:00
m bab96b7ede Merge pull request 'Implement governance sentinel v2' (#97) from codex/wp-301-canary-repository into main
CI / test (push) Successful in 21s
Reviewed-on: #97
2026-08-22 10:15:11 +00:00
m 1b091a50f2 feat(deploy): add opt-in agent canary profile
CI / test (push) Successful in 25s
CI / test (pull_request) Successful in 22s
2026-08-22 12:37:33 +03:00
m 790fd7e768 chore(governance): complete migration canary evidence 2026-08-22 12:36:00 +03:00
m 875286b9fc chore(governance): authorize work migration canary 2026-08-22 12:34:18 +03:00
m 8e26826ae2 feat(work): wire canary cli to work repository 2026-08-22 12:20:14 +03:00
m c30e0c72ea feat(work): add manual agent canary cli 2026-08-22 12:18:19 +03:00
m d2338b1485 feat(work): gate agent canary dispatch 2026-08-22 12:15:50 +03:00
m 7f82240a8f feat(work): orchestrate dry-run dispatch persistence 2026-08-22 12:13:35 +03:00
m fa62322ed2 feat(work): add deterministic dry-run dispatch 2026-08-22 12:11:29 +03:00
m 8081b4de34 feat(work): persist agent execution state 2026-08-22 12:08:43 +03:00
m f792d208ef Add agent dispatch intent contract 2026-08-22 12:03:21 +03:00
m 04dad79af1 Add independent agent verification contract 2026-08-22 12:02:19 +03:00
m 02b472b627 Add agent artifact attestation 2026-08-22 12:01:16 +03:00
m a6eb4abc91 Validate agent callback envelopes 2026-08-22 12:00:10 +03:00
m 9766e5396e Add agent lease fencing contract 2026-08-22 11:59:04 +03:00
m cb92f95550 Add cognitive admission policy 2026-08-22 11:57:43 +03:00
m e83cf3582b Add durable run liveness assessment 2026-08-22 11:56:18 +03:00
m a65ee372a9 Add model qualification gate 2026-08-22 11:52:24 +03:00
m 1f01b7013c Add model qualification manifest contract 2026-08-22 11:51:19 +03:00
m 93bc4a0fdf Record hardware and model inventory evidence 2026-08-22 11:50:00 +03:00
m aec63de0c7 Add deterministic citation resolver 2026-08-22 11:47:15 +03:00
m dc7f7b3b2b Add research citation evidence contracts 2026-08-22 11:46:09 +03:00
m e3b55c9705 Integrate active project context into local chat 2026-08-22 11:44:04 +03:00
m 879de3bd95 Add typed context compiler v1 2026-08-22 11:41:52 +03:00
m 3e0be6245e Propose governed self-development v1 2026-08-22 11:38:46 +03:00
m 08deab8dda Propose multi-agent work graph v1 2026-08-22 11:36:22 +03:00
m 82ee6e5ba0 Propose agent execution plane v1 2026-08-22 11:34:26 +03:00
m 339f6a6770 Propose epistemics truth v1 2026-08-22 11:32:20 +03:00
m 6fb9184d14 Authorize readiness canary promotion 2026-08-22 11:25:14 +03:00
m 3f2b008109 Add dependency-aware runtime readiness 2026-08-22 11:20:53 +03:00
m 470fdf8c03 Start dependency-aware readiness task 2026-08-22 11:16:27 +03:00
m 15c9517d0c Repair production backup recovery selection 2026-08-22 11:14:21 +03:00
m 8687c9b792 Consolidate governance lineage and registry 2026-08-22 11:08:48 +03:00
m ff33a62edd Implement governance sentinel v2 2026-08-22 11:03:21 +03:00
m b439a813e5 Merge pull request 'Observe built-in cognitive usage' (#96) from codex/creator-chat-model-status-v1 into main
CI / test (push) Successful in 23s
2026-08-22 00:15:39 +00:00
m 39d2d79c51 Observe built-in cognitive usage
CI / test (push) Successful in 22s
CI / test (pull_request) Successful in 23s
2026-08-22 03:14:08 +03:00
m 305a55a948 Render chat build metadata
CI / test (push) Successful in 24s
DEV-000095 passed CI.
2026-08-22 00:08:09 +00:00
m 13a3446dd0 Render chat build metadata
CI / test (push) Successful in 21s
CI / test (pull_request) Successful in 25s
2026-08-22 03:06:41 +03:00
m 8c2989f2c6 Route local recovery by semantic intent
CI / test (push) Successful in 22s
DEV-000094 passed CI.
2026-08-22 00:02:35 +00:00
m b097269df4 Route local recovery by semantic intent
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 21s
2026-08-22 03:00:15 +03:00
m 39f9081b96 Accept external audit exploration mode
CI / test (push) Successful in 23s
ARCH-000012 passed CI.
2026-08-21 23:54:26 +00:00
m 3de01f8a94 Accept external audit exploration mode
CI / test (push) Successful in 21s
CI / test (pull_request) Successful in 21s
2026-08-22 02:53:29 +03:00
m 948896cd45 Propose external audit exploration mode
CI / test (push) Successful in 21s
ARCH-000011 passed CI.
2026-08-21 23:51:26 +00:00
m 545e855298 Propose external audit exploration mode
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 22s
2026-08-22 02:50:03 +03:00
m 6f8c4e2bc7 Merge pull request 'Preserve learning evidence integrity' (#91) from codex/creator-chat-model-status-v1 into main
CI / test (push) Successful in 24s
Preserve learning evidence integrity
2026-08-21 23:45:04 +00:00
m 947d9d78a1 Merge remote-tracking branch 'origin/main' into codex/creator-chat-model-status-v1
CI / test (push) Successful in 24s
CI / test (pull_request) Successful in 26s
2026-08-22 02:43:06 +03:00
m 8a3b085855 fix: preserve learning evidence integrity 2026-08-22 02:43:06 +03:00
m cf66cd472c Merge pull request 'Route model inventory by semantic intent' (#90) from codex/creator-chat-model-status-v1 into main
CI / test (push) Successful in 23s
Route model inventory by semantic intent
2026-08-21 23:31:31 +00:00
m 01f4fa27c2 fix: route model inventory by semantic intent
CI / test (push) Successful in 22s
CI / test (pull_request) Successful in 19s
2026-08-22 02:30:23 +03:00
m d286fbb234 Merge remote-tracking branch 'origin/main' into codex/creator-chat-model-status-v1 2026-08-22 02:25:43 +03:00
m 3b4ce66df0 feat: answer model inventory from capability registry 2026-08-22 02:25:42 +03:00
m 2306c8d6b3 Merge pull request 'Keep simple chat on built-in local route' (#89) from codex/creator-chat-model-status-v1 into main
CI / test (push) Successful in 26s
Keep simple chat on built-in local route
2026-08-21 23:13:32 +00:00
m 2b8887b3b2 Merge remote-tracking branch 'origin/main' into codex/creator-chat-model-status-v1
CI / test (push) Successful in 22s
CI / test (pull_request) Successful in 27s
2026-08-22 02:11:37 +03:00
m 4750dd859c fix: keep simple chat on built-in local route 2026-08-22 02:11:36 +03:00
m a6059cc9c6 fix: explain chat session and runtime failures
CI / test (push) Successful in 26s
fix: explain chat session and runtime failures
2026-08-21 22:59:57 +00:00
m 498bc885c3 Merge remote-tracking branch 'origin/main' into codex/creator-chat-model-status-v1
CI / test (push) Successful in 22s
CI / test (pull_request) Successful in 21s
2026-08-22 01:58:14 +03:00
m 487f7b7c9d fix: explain chat session and runtime failures 2026-08-22 01:58:14 +03:00
m b1b52920ab feat: upgrade Domovoy local cognition and recovery
CI / test (push) Successful in 20s
feat: upgrade Domovoy local cognition and recovery
2026-08-21 22:48:17 +00:00
m 57fb572d5b Merge remote-tracking branch 'origin/main' into codex/creator-chat-model-status-v1
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 21s
2026-08-22 01:47:16 +03:00
m 0849e9c4aa chore: authorize integrated cognitive upgrade scope
CI / test (push) Successful in 22s
CI / test (pull_request) Successful in 20s
2026-08-22 01:46:10 +03:00
m 51fa9ea506 chore: activate Domovoy cognitive upgrade task
CI / test (push) Failing after 14s
CI / test (pull_request) Failing after 14s
2026-08-22 01:45:44 +03:00
m 2cf1fb5c5e feat: expose runtime build status in chat
CI / test (push) Failing after 15s
CI / test (pull_request) Failing after 15s
2026-08-22 01:36:48 +03:00
m 4f06720e3f feat: keep Domovoy cognitive recovery active
CI / test (push) Failing after 15s
2026-08-22 01:29:45 +03:00
m cdc8eea1a7 Merge pull request 'feat: show chat version and update time' (#86) from codex/creator-chat-model-status-v1 into main
CI / test (push) Failing after 15s
2026-08-21 16:17:32 +00:00
m 9f717d720e feat: show chat version and update time
CI / test (push) Failing after 16s
CI / test (pull_request) Failing after 14s
2026-08-21 19:16:32 +03:00
m 3b5c0ff56d Merge pull request 'ci: use Portainer GitOps polling' (#85) from codex/creator-chat-model-status-v1 into main
CI / test (push) Failing after 14s
2026-08-21 16:04:53 +00:00
m 9cbf6c7536 ci: rely on Portainer GitOps polling
CI / test (push) Failing after 17s
CI / test (pull_request) Failing after 15s
2026-08-21 19:04:30 +03:00
m 9650eefcb4 Merge pull request 'feat: evolve Domovoy cognitive provider catalog' (#84) from codex/creator-chat-model-status-v1 into main
CI / test (push) Failing after 16s
Deploy production stack / deploy (push) Failing after 3s
2026-08-21 16:00:47 +00:00
m 1ed28ef77a ci: trigger Portainer deployment on main
CI / test (push) Failing after 15s
CI / test (pull_request) Failing after 15s
2026-08-21 18:48:54 +03:00
m c56610fa75 Merge remote-tracking branch 'origin/main' into codex/creator-chat-model-status-v1
CI / test (push) Failing after 15s
CI / test (pull_request) Failing after 15s
2026-08-21 18:38:32 +03:00
m 3d7836d37c feat: evolve Domovoy cognitive provider catalog
CI / test (push) Failing after 16s
2026-08-21 18:37:04 +03:00
m 0482e6ac14 feat: authorize cognitive canary activation
CI / test (push) Failing after 15s
2026-08-21 15:49:57 +03:00
m 526368be7e fix: create cognitive discovery head
CI / test (push) Failing after 16s
2026-08-21 15:40:32 +03:00
m 3c2d8a38d7 chore: log discovery persistence failure
CI / test (push) Failing after 14s
2026-08-21 15:36:02 +03:00
m cabf386203 fix: refresh cognitive discovery health
CI / test (push) Failing after 14s
2026-08-21 15:31:34 +03:00
m f9bd267c04 fix: preserve discovery health for cognitive canary
CI / test (push) Failing after 15s
2026-08-21 15:26:34 +03:00
m b041a94f71 test: qualify domovoy lifeboat model
CI / test (push) Failing after 15s
2026-08-21 15:10:41 +03:00
m 814c9e1d32 fix: healthcheck dedicated lifeboat
CI / test (push) Failing after 15s
2026-08-21 15:06:19 +03:00
m b93715867d feat: run dedicated local lifeboat model
CI / test (push) Failing after 15s
2026-08-21 15:04:42 +03:00
m 009314e930 feat: keep domovoy alive in lifeboat mode
CI / test (push) Failing after 15s
2026-08-21 14:52:55 +03:00
m 0954c8f4ef Merge pull request 'fix(chat): block accidental AI secrets' (#83) from codex/creator-chat-model-status-v1 into main
CI / test (push) Successful in 25s
2026-08-21 10:39:56 +00:00
m 0a2599cfa6 fix(chat): block accidental AI secrets
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 20s
2026-08-21 13:38:57 +03:00
m ae9933ee84 Merge pull request 'fix(chat): stabilize header status' (#82) from codex/creator-chat-model-status-v1 into main
CI / test (push) Successful in 20s
2026-08-21 10:35:36 +00:00
m 7a59abc608 fix(chat): stabilize header status
CI / test (push) Successful in 19s
CI / test (pull_request) Successful in 20s
2026-08-21 13:34:35 +03:00
m 6e0cf3cd1d Merge pull request 'feat(chat): create stable three-column workspace' (#81) from codex/creator-chat-model-status-v1 into main
CI / test (push) Successful in 20s
2026-08-21 10:31:24 +00:00
m d7e77ea3dd feat(chat): create stable three-column workspace
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 20s
2026-08-21 13:29:58 +03:00
m 47fd0c4b02 Merge pull request 'feat(chat): add conversation starters' (#80) from codex/creator-chat-model-status-v1 into main
CI / test (push) Successful in 20s
2026-08-21 06:24:27 +00:00
m df9181ef6a feat(chat): add conversation starters
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 19s
2026-08-21 09:23:26 +03:00
m b0d31012da Merge pull request 'feat(core): answer cognitive channel questions' (#79) from codex/creator-chat-model-status-v1 into main
CI / test (push) Successful in 20s
2026-08-21 06:20:31 +00:00
m 8d149b2b9d feat(core): answer cognitive channel questions
CI / test (push) Successful in 19s
CI / test (pull_request) Successful in 19s
2026-08-21 09:19:15 +03:00
m 0d10e6fd82 Merge pull request 'feat(chat): bridge cognitive status to console' (#78) from codex/creator-chat-model-status-v1 into main
CI / test (push) Successful in 18s
2026-08-21 06:15:48 +00:00
m 9aa5771f55 feat(chat): bridge cognitive status to console
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 19s
2026-08-21 09:14:35 +03:00
m 75abfb204b Merge pull request 'feat(chat): show cognitive channel status' (#77) from codex/creator-chat-model-status-v1 into main
CI / test (push) Successful in 20s
2026-08-21 06:11:02 +00:00
m b0ee54153b feat(chat): show cognitive channel status
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 19s
2026-08-21 09:09:26 +03:00
m 26cf85108a Merge pull request 'Make live chat the public starting surface' (#76) from codex/creator-chat-primary-v1 into main
CI / test (push) Successful in 18s
2026-08-21 05:57:29 +00:00
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
196 changed files with 7553 additions and 335 deletions
+4
View File
@@ -15,6 +15,8 @@ jobs:
go-version: '1.26.6'
- name: Active task preflight
run: go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json
- name: Independent governance verifier
run: go run ./cmd/devctl/independent
- name: Exact diff scope
run: go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json
- name: Format check
@@ -25,3 +27,5 @@ jobs:
run: go test ./...
- name: Vet
run: go vet ./...
- name: Diff whitespace check
run: git diff --check
+45 -15
View File
@@ -322,18 +322,18 @@ func (r *CapabilityRepository) LoadActiveBinding(ctx context.Context, capability
func (r *CapabilityRepository) SaveProviderInstance(ctx context.Context, value capability.ProviderInstance) error {
_, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_provider_instances
(provider_instance_id,revision,provider_kind,endpoint_ref,credential_ref,enabled,qualification,health,observed_at_unix_millis)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) ON CONFLICT DO NOTHING`, value.ID, value.Revision, value.Kind,
value.EndpointRef, value.CredentialRef, value.Enabled, value.Qualification, value.Health, value.ObservedAtUnixMillis)
(provider_instance_id,revision,provider_kind,endpoint_ref,credential_ref,enabled,qualification,health,observed_at_unix_millis,billing_mode,auth_mode,input_cost_micros,output_cost_micros,request_cost_micros)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT DO NOTHING`, value.ID, value.Revision, value.Kind,
value.EndpointRef, value.CredentialRef, value.Enabled, value.Qualification, value.Health, value.ObservedAtUnixMillis, value.BillingMode, value.AuthMode, value.InputCostMicros, value.OutputCostMicros, value.RequestCostMicros)
return err
}
func (r *CapabilityRepository) SaveModelDescriptor(ctx context.Context, value capability.ModelDescriptor) error {
_, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_model_descriptors
(model_descriptor_id,revision,provider_instance_id,provider_model_id,fingerprint,max_context_bytes,modalities,capabilities,qualification,health,observed_at_unix_millis)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT DO NOTHING`, value.ID, value.Revision,
(model_descriptor_id,revision,provider_instance_id,provider_model_id,fingerprint,max_context_bytes,modalities,capabilities,qualification,health,observed_at_unix_millis,intelligence_score_millis)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) ON CONFLICT DO NOTHING`, value.ID, value.Revision,
value.ProviderInstanceID, value.ProviderModelID, value.Fingerprint, value.MaxContextBytes, value.Modalities,
value.Capabilities, value.Qualification, value.Health, value.ObservedAtUnixMillis)
value.Capabilities, value.Qualification, value.Health, value.ObservedAtUnixMillis, value.IntelligenceScoreMillis)
return err
}
@@ -349,8 +349,8 @@ func (r *CapabilityRepository) SaveCognitiveBinding(ctx context.Context, value c
func (r *CapabilityRepository) LoadCognitiveCandidates(ctx context.Context, capabilityID capability.CapabilityID, owner identity.PrincipalID) ([]capability.ResolvedCognitiveBinding, error) {
rows, err := r.pool.Query(ctx, `SELECT
b.binding_id,b.revision,b.capability_id,b.owner_principal_id,b.provider_instance_id,b.provider_revision,b.model_descriptor_id,b.model_revision,b.data_policy,b.qualification,b.activation,b.health,b.limit_policy_ref,
p.provider_instance_id,p.revision,p.provider_kind,p.endpoint_ref,COALESCE(p.credential_ref,''),p.enabled,p.qualification,COALESCE(ph.health,p.health),GREATEST(p.observed_at_unix_millis,COALESCE(ph.observed_at_unix_millis,0)),
m.model_descriptor_id,m.revision,m.provider_instance_id,m.provider_model_id,m.fingerprint,m.max_context_bytes,m.modalities,m.capabilities,m.qualification,COALESCE(ma.health,m.health),GREATEST(m.observed_at_unix_millis,COALESCE(ma.observed_at_unix_millis,0))
p.provider_instance_id,p.revision,p.provider_kind,p.endpoint_ref,COALESCE(p.credential_ref,''),p.enabled,p.qualification,COALESCE(ph.health,p.health),GREATEST(p.observed_at_unix_millis,COALESCE(ph.observed_at_unix_millis,0)),p.billing_mode,p.auth_mode,p.input_cost_micros,p.output_cost_micros,p.request_cost_micros,
m.model_descriptor_id,m.revision,m.provider_instance_id,m.provider_model_id,m.fingerprint,m.max_context_bytes,m.modalities,m.capabilities,m.qualification,COALESCE(ma.health,m.health),GREATEST(m.observed_at_unix_millis,COALESCE(ma.observed_at_unix_millis,0)),m.intelligence_score_millis
FROM capability.cognitive_binding_revision_heads bh
JOIN capability.cognitive_bindings b ON b.binding_id=bh.binding_id AND b.revision=bh.revision
JOIN capability.cognitive_provider_instances p ON p.provider_instance_id=b.provider_instance_id AND p.revision=b.provider_revision
@@ -366,8 +366,8 @@ func (r *CapabilityRepository) LoadCognitiveCandidates(ctx context.Context, capa
for rows.Next() {
var value capability.ResolvedCognitiveBinding
err = rows.Scan(&value.Binding.ID, &value.Binding.Revision, &value.Binding.CapabilityID, &value.Binding.OwnerPrincipalID, &value.Binding.ProviderInstanceID, &value.Binding.ProviderRevision, &value.Binding.ModelDescriptorID, &value.Binding.ModelRevision, &value.Binding.DataPolicy, &value.Binding.Qualification, &value.Binding.Activation, &value.Binding.Health, &value.Binding.LimitPolicyRef,
&value.Provider.ID, &value.Provider.Revision, &value.Provider.Kind, &value.Provider.EndpointRef, &value.Provider.CredentialRef, &value.Provider.Enabled, &value.Provider.Qualification, &value.Provider.Health, &value.Provider.ObservedAtUnixMillis,
&value.Model.ID, &value.Model.Revision, &value.Model.ProviderInstanceID, &value.Model.ProviderModelID, &value.Model.Fingerprint, &value.Model.MaxContextBytes, &value.Model.Modalities, &value.Model.Capabilities, &value.Model.Qualification, &value.Model.Health, &value.Model.ObservedAtUnixMillis)
&value.Provider.ID, &value.Provider.Revision, &value.Provider.Kind, &value.Provider.EndpointRef, &value.Provider.CredentialRef, &value.Provider.Enabled, &value.Provider.Qualification, &value.Provider.Health, &value.Provider.ObservedAtUnixMillis, &value.Provider.BillingMode, &value.Provider.AuthMode, &value.Provider.InputCostMicros, &value.Provider.OutputCostMicros, &value.Provider.RequestCostMicros,
&value.Model.ID, &value.Model.Revision, &value.Model.ProviderInstanceID, &value.Model.ProviderModelID, &value.Model.Fingerprint, &value.Model.MaxContextBytes, &value.Model.Modalities, &value.Model.Capabilities, &value.Model.Qualification, &value.Model.Health, &value.Model.ObservedAtUnixMillis, &value.Model.IntelligenceScoreMillis)
if err != nil {
return nil, err
}
@@ -414,7 +414,7 @@ func (r *CapabilityRepository) SaveCognitiveDiscovery(ctx context.Context, value
return err
}
defer func() { _ = tx.Rollback(ctx) }()
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_provider_instances (provider_instance_id,revision,provider_kind,endpoint_ref,credential_ref,enabled,qualification,health,observed_at_unix_millis) VALUES ($1,$2,$3,$4,NULLIF($5,''),$6,$7,$8,$9) ON CONFLICT DO NOTHING`, value.Provider.ID, value.Provider.Revision, value.Provider.Kind, value.Provider.EndpointRef, value.Provider.CredentialRef, value.Provider.Enabled, value.Provider.Qualification, value.Provider.Health, value.Provider.ObservedAtUnixMillis)
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_provider_instances (provider_instance_id,revision,provider_kind,endpoint_ref,credential_ref,enabled,qualification,health,observed_at_unix_millis,billing_mode,auth_mode,input_cost_micros,output_cost_micros,request_cost_micros) VALUES ($1,$2,$3,$4,NULLIF($5,''),$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT (provider_instance_id,revision) DO UPDATE SET health=EXCLUDED.health,observed_at_unix_millis=EXCLUDED.observed_at_unix_millis,billing_mode=EXCLUDED.billing_mode,auth_mode=EXCLUDED.auth_mode,input_cost_micros=EXCLUDED.input_cost_micros,output_cost_micros=EXCLUDED.output_cost_micros,request_cost_micros=EXCLUDED.request_cost_micros WHERE capability.cognitive_provider_instances.observed_at_unix_millis <= EXCLUDED.observed_at_unix_millis`, value.Provider.ID, value.Provider.Revision, value.Provider.Kind, value.Provider.EndpointRef, value.Provider.CredentialRef, value.Provider.Enabled, value.Provider.Qualification, value.Provider.Health, value.Provider.ObservedAtUnixMillis, value.Provider.BillingMode, value.Provider.AuthMode, value.Provider.InputCostMicros, value.Provider.OutputCostMicros, value.Provider.RequestCostMicros)
if err != nil {
return err
}
@@ -423,7 +423,7 @@ func (r *CapabilityRepository) SaveCognitiveDiscovery(ctx context.Context, value
return err
}
for index, model := range value.Models {
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_model_descriptors (model_descriptor_id,revision,provider_instance_id,provider_model_id,fingerprint,max_context_bytes,modalities,capabilities,qualification,health,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT DO NOTHING`, model.ID, model.Revision, model.ProviderInstanceID, model.ProviderModelID, model.Fingerprint, model.MaxContextBytes, model.Modalities, model.Capabilities, model.Qualification, model.Health, model.ObservedAtUnixMillis)
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_model_descriptors (model_descriptor_id,revision,provider_instance_id,provider_model_id,fingerprint,max_context_bytes,modalities,capabilities,qualification,health,observed_at_unix_millis,intelligence_score_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) ON CONFLICT (model_descriptor_id,revision) DO UPDATE SET health=EXCLUDED.health,observed_at_unix_millis=EXCLUDED.observed_at_unix_millis,intelligence_score_millis=EXCLUDED.intelligence_score_millis WHERE capability.cognitive_model_descriptors.observed_at_unix_millis <= EXCLUDED.observed_at_unix_millis`, model.ID, model.Revision, model.ProviderInstanceID, model.ProviderModelID, model.Fingerprint, model.MaxContextBytes, model.Modalities, model.Capabilities, model.Qualification, model.Health, model.ObservedAtUnixMillis, model.IntelligenceScoreMillis)
if err != nil {
return err
}
@@ -433,7 +433,11 @@ func (r *CapabilityRepository) SaveCognitiveDiscovery(ctx context.Context, value
return err
}
binding := value.Bindings[index]
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_bindings (binding_id,revision,capability_id,owner_principal_id,provider_instance_id,provider_revision,model_descriptor_id,model_revision,data_policy,qualification,activation,health,limit_policy_ref) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT DO NOTHING`, binding.ID, binding.Revision, binding.CapabilityID, binding.OwnerPrincipalID, binding.ProviderInstanceID, binding.ProviderRevision, binding.ModelDescriptorID, binding.ModelRevision, binding.DataPolicy, binding.Qualification, binding.Activation, binding.Health, binding.LimitPolicyRef)
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_bindings (binding_id,revision,capability_id,owner_principal_id,provider_instance_id,provider_revision,model_descriptor_id,model_revision,data_policy,qualification,activation,health,limit_policy_ref) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT (binding_id,revision) DO UPDATE SET health=EXCLUDED.health WHERE capability.cognitive_bindings.health <> EXCLUDED.health`, binding.ID, binding.Revision, binding.CapabilityID, binding.OwnerPrincipalID, binding.ProviderInstanceID, binding.ProviderRevision, binding.ModelDescriptorID, binding.ModelRevision, binding.DataPolicy, binding.Qualification, binding.Activation, binding.Health, binding.LimitPolicyRef)
if err != nil {
return err
}
_, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_binding_revision_heads (binding_id,revision) VALUES ($1,$2) ON CONFLICT DO NOTHING`, binding.ID, binding.Revision)
if err != nil {
return err
}
@@ -453,10 +457,10 @@ func (r *CapabilityRepository) SaveCognitiveCanaryPromotion(ctx context.Context,
}
defer func() { _ = tx.Rollback(ctx) }()
p, m, b, e := value.Provider, value.Model, value.Binding, value.Evaluation
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_provider_instances (provider_instance_id,revision,provider_kind,endpoint_ref,credential_ref,enabled,qualification,health,observed_at_unix_millis) VALUES ($1,$2,$3,$4,NULLIF($5,''),$6,$7,$8,$9) ON CONFLICT DO NOTHING`, p.ID, p.Revision, p.Kind, p.EndpointRef, p.CredentialRef, p.Enabled, p.Qualification, p.Health, p.ObservedAtUnixMillis); err != nil {
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_provider_instances (provider_instance_id,revision,provider_kind,endpoint_ref,credential_ref,enabled,qualification,health,observed_at_unix_millis,billing_mode,auth_mode,input_cost_micros,output_cost_micros,request_cost_micros) VALUES ($1,$2,$3,$4,NULLIF($5,''),$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT DO NOTHING`, p.ID, p.Revision, p.Kind, p.EndpointRef, p.CredentialRef, p.Enabled, p.Qualification, p.Health, p.ObservedAtUnixMillis, p.BillingMode, p.AuthMode, p.InputCostMicros, p.OutputCostMicros, p.RequestCostMicros); err != nil {
return err
}
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_model_descriptors (model_descriptor_id,revision,provider_instance_id,provider_model_id,fingerprint,max_context_bytes,modalities,capabilities,qualification,health,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT DO NOTHING`, m.ID, m.Revision, m.ProviderInstanceID, m.ProviderModelID, m.Fingerprint, m.MaxContextBytes, m.Modalities, m.Capabilities, m.Qualification, m.Health, m.ObservedAtUnixMillis); err != nil {
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_model_descriptors (model_descriptor_id,revision,provider_instance_id,provider_model_id,fingerprint,max_context_bytes,modalities,capabilities,qualification,health,observed_at_unix_millis,intelligence_score_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) ON CONFLICT DO NOTHING`, m.ID, m.Revision, m.ProviderInstanceID, m.ProviderModelID, m.Fingerprint, m.MaxContextBytes, m.Modalities, m.Capabilities, m.Qualification, m.Health, m.ObservedAtUnixMillis, m.IntelligenceScoreMillis); err != nil {
return err
}
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_bindings (binding_id,revision,capability_id,owner_principal_id,provider_instance_id,provider_revision,model_descriptor_id,model_revision,data_policy,qualification,activation,health,limit_policy_ref) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT DO NOTHING`, b.ID, b.Revision, b.CapabilityID, b.OwnerPrincipalID, b.ProviderInstanceID, b.ProviderRevision, b.ModelDescriptorID, b.ModelRevision, b.DataPolicy, b.Qualification, b.Activation, b.Health, b.LimitPolicyRef); err != nil {
@@ -475,6 +479,32 @@ func (r *CapabilityRepository) SaveCognitiveCanaryPromotion(ctx context.Context,
return tx.Commit(ctx)
}
func (r *CapabilityRepository) SaveCognitiveActivationPromotion(ctx context.Context, value capability.CognitiveActivationPromotion) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer func() { _ = tx.Rollback(ctx) }()
b, e := value.Binding, value.Evaluation
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_bindings (binding_id,revision,capability_id,owner_principal_id,provider_instance_id,provider_revision,model_descriptor_id,model_revision,data_policy,qualification,activation,health,limit_policy_ref) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT DO NOTHING`, b.ID, b.Revision, b.CapabilityID, b.OwnerPrincipalID, b.ProviderInstanceID, b.ProviderRevision, b.ModelDescriptorID, b.ModelRevision, b.DataPolicy, b.Qualification, b.Activation, b.Health, b.LimitPolicyRef); err != nil {
return err
}
if _, err = tx.Exec(ctx, `INSERT INTO capability.cognitive_evaluated_capabilities (evaluation_id,binding_id,binding_revision,capability_id,result,quality_score_millis,critical_failure_millis,benchmark_version,artifact_id,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT DO NOTHING`, e.ID, e.BindingID, e.BindingRevision, e.CapabilityID, e.Result, e.QualityScoreMillis, e.CriticalFailureMillis, e.BenchmarkVersion, e.ArtifactID, e.ObservedAtUnixMillis); err != nil {
return err
}
if _, err = tx.Exec(ctx, `UPDATE capability.cognitive_bindings SET activation='DEPRECATED' WHERE capability_id=$1 AND owner_principal_id=$2 AND activation='ACTIVE' AND NOT (binding_id=$3 AND revision=$4)`, b.CapabilityID, b.OwnerPrincipalID, b.ID, b.Revision); err != nil {
return err
}
result, err := tx.Exec(ctx, `UPDATE capability.cognitive_binding_revision_heads SET revision=$1,updated_at=CURRENT_TIMESTAMP WHERE binding_id=$2 AND revision=$3`, b.Revision, b.ID, value.ExpectedBindingRevision)
if err != nil {
return err
}
if result.RowsAffected() != 1 {
return ErrCapabilityBindingConflict
}
return tx.Commit(ctx)
}
func (r *CapabilityRepository) SavePerformanceEvidence(ctx context.Context, value capability.PerformanceEvidence) error {
_, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_performance_evidence (evidence_id,binding_id,binding_revision,model_descriptor_id,model_revision,hardware_profile,runtime,quantization,context_units,thinking_enabled,generation_tps_millis,prompt_tps_millis,ttft_micros,latency_micros,memory_bytes,reliability_millis,benchmark_version,artifact_id,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19) ON CONFLICT DO NOTHING`, value.ID, value.BindingID, value.BindingRevision, value.ModelDescriptorID, value.ModelRevision, value.HardwareProfile, value.Runtime, value.Quantization, value.ContextUnits, value.ThinkingEnabled, value.GenerationTPSMillis, value.PromptTPSMillis, value.TTFTMicros, value.LatencyMicros, value.MemoryBytes, value.ReliabilityMillis, value.BenchmarkVersion, value.ArtifactID, value.ObservedAtUnixMillis)
return err
+58 -2
View File
@@ -3,6 +3,7 @@ package postgres
import (
"context"
"fmt"
"time"
"git.cin.su/m/Assistant/contracts/epistemics"
"github.com/jackc/pgx/v5/pgxpool"
@@ -64,7 +65,7 @@ func (r *EpistemicsRepository) RecentReflectionCandidates(ctx context.Context, l
}
rows, err := r.pool.Query(ctx, `SELECT
reflection_candidate_id,trigger,state,source_experience_id,original_intent_id,source_utterance_id,
summary,evidence_refs,requires_cognition,recorded_at_unix_millis,revision
summary,evidence_refs,requires_cognition,recorded_at_unix_millis,revision,claimed_by,claim_expires_at_unix_millis
FROM epistemics.reflection_candidates ORDER BY recorded_at_unix_millis DESC, reflection_candidate_id DESC LIMIT $1`, limit)
if err != nil {
return nil, err
@@ -74,7 +75,7 @@ func (r *EpistemicsRepository) RecentReflectionCandidates(ctx context.Context, l
for rows.Next() {
var value epistemics.ReflectionCandidate
if err := rows.Scan(&value.ID, &value.Trigger, &value.State, &value.SourceExperienceID, &value.OriginalIntentID, &value.SourceUtteranceID,
&value.Summary, &value.EvidenceRefs, &value.RequiresCognition, &value.RecordedAtUnixMillis, &value.Revision); err != nil {
&value.Summary, &value.EvidenceRefs, &value.RequiresCognition, &value.RecordedAtUnixMillis, &value.Revision, &value.ClaimedBy, &value.ClaimExpiresAtUnixMillis); err != nil {
return nil, err
}
result = append(result, value)
@@ -82,6 +83,61 @@ func (r *EpistemicsRepository) RecentReflectionCandidates(ctx context.Context, l
return result, rows.Err()
}
func (r *EpistemicsRepository) ClaimReflectionCandidate(ctx context.Context, workerID string, nowUnixMillis, leaseMillis int64) (epistemics.ReflectionCandidate, error) {
if workerID == "" || leaseMillis < 1 {
return epistemics.ReflectionCandidate{}, fmt.Errorf("invalid reflection claim")
}
tx, err := r.pool.Begin(ctx)
if err != nil {
return epistemics.ReflectionCandidate{}, err
}
defer tx.Rollback(ctx)
var value epistemics.ReflectionCandidate
err = tx.QueryRow(ctx, `SELECT reflection_candidate_id,trigger,state,source_experience_id,original_intent_id,source_utterance_id,summary,evidence_refs,requires_cognition,recorded_at_unix_millis,revision,claimed_by,claim_expires_at_unix_millis
FROM epistemics.reflection_candidates
WHERE state='OPEN' AND (claim_expires_at_unix_millis=0 OR claim_expires_at_unix_millis < $1)
ORDER BY recorded_at_unix_millis,reflection_candidate_id
FOR UPDATE SKIP LOCKED LIMIT 1`, nowUnixMillis).Scan(&value.ID, &value.Trigger, &value.State, &value.SourceExperienceID, &value.OriginalIntentID, &value.SourceUtteranceID, &value.Summary, &value.EvidenceRefs, &value.RequiresCognition, &value.RecordedAtUnixMillis, &value.Revision, &value.ClaimedBy, &value.ClaimExpiresAtUnixMillis)
if err != nil {
return epistemics.ReflectionCandidate{}, err
}
value.State = epistemics.ReflectionCandidateEvaluating
value.ClaimedBy = workerID
value.ClaimExpiresAtUnixMillis = nowUnixMillis + leaseMillis
_, err = tx.Exec(ctx, `UPDATE epistemics.reflection_candidates SET state=$1,claimed_by=$2,claim_expires_at_unix_millis=$3 WHERE reflection_candidate_id=$4 AND state='OPEN'`, value.State, value.ClaimedBy, value.ClaimExpiresAtUnixMillis, value.ID)
if err != nil {
return epistemics.ReflectionCandidate{}, err
}
if err := tx.Commit(ctx); err != nil {
return epistemics.ReflectionCandidate{}, err
}
return value, nil
}
func (r *EpistemicsRepository) RecoverReflectionClaims(ctx context.Context, nowUnixMillis int64) (int64, error) {
result, err := r.pool.Exec(ctx, `UPDATE epistemics.reflection_candidates SET state='OPEN',claimed_by='',claim_expires_at_unix_millis=0 WHERE state='EVALUATING' AND claim_expires_at_unix_millis > 0 AND claim_expires_at_unix_millis < $1`, nowUnixMillis)
return result.RowsAffected(), err
}
func (r *EpistemicsRepository) ResolveReflectionCandidate(ctx context.Context, id epistemics.ReflectionCandidateID, workerID string, nowUnixMillis int64) error {
if id == "" || workerID == "" {
return fmt.Errorf("invalid reflection resolution")
}
result, err := r.pool.Exec(ctx, `UPDATE epistemics.reflection_candidates SET state='RESOLVED',claimed_by='',claim_expires_at_unix_millis=0,revision=revision+1 WHERE reflection_candidate_id=$1 AND state='EVALUATING' AND claimed_by=$2 AND claim_expires_at_unix_millis >= $3`, id, workerID, nowUnixMillis)
if err == nil && result.RowsAffected() != 1 {
return fmt.Errorf("reflection candidate claim lost")
}
return err
}
func (r *EpistemicsRepository) WaitForReflectionCandidate(ctx context.Context, interval time.Duration) error {
if interval <= 0 {
return fmt.Errorf("invalid reflection poll interval")
}
_, err := r.pool.Exec(ctx, `SELECT pg_sleep($1)`, interval.Seconds())
return err
}
func (r *EpistemicsRepository) SaveExperienceRecord(ctx context.Context, value epistemics.ExperienceRecord) error {
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.experience_records (
experience_id, source_kind, original_intent_id, source_utterance_id, context_refs, help_method, help_method_ref,
+201
View File
@@ -2,14 +2,23 @@ package postgres
import (
"context"
"errors"
"fmt"
"time"
"git.cin.su/m/Assistant/contracts/work"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type WorkRepository struct{ pool *pgxpool.Pool }
var (
ErrDispatchIntentConflict = errors.New("dispatch intent idempotency conflict")
ErrAgentLeaseUnavailable = errors.New("agent lease unavailable")
ErrAgentCallbackRejected = errors.New("agent callback rejected")
)
func OpenWork(ctx context.Context, config ContinuityConfig) (*WorkRepository, error) {
pool, err := openPool(ctx, config)
if err != nil {
@@ -39,6 +48,198 @@ func (r *WorkRepository) Load(ctx context.Context, id work.TaskID) (work.TaskTar
return target, nil
}
func (r *WorkRepository) SaveDispatchIntent(ctx context.Context, intent work.AgentDispatchIntent) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
_, err = tx.Exec(ctx, `INSERT INTO work.agent_dispatch_intents(dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT(idempotency_key) DO NOTHING`, intent.DispatchID, intent.IdempotencyKey, intent.DelegationID, intent.RunID, intent.ImplementationID, intent.ImplementationRevision, intent.SourceRevision, intent.WorktreeID, intent.VerificationPlanRef, intent.CreatedAtUnixMillis)
if err != nil {
return fmt.Errorf("save dispatch intent: %w", err)
}
var existing work.AgentDispatchIntent
err = tx.QueryRow(ctx, `SELECT dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis FROM work.agent_dispatch_intents WHERE idempotency_key=$1`, intent.IdempotencyKey).Scan(&existing.DispatchID, &existing.IdempotencyKey, &existing.DelegationID, &existing.RunID, &existing.ImplementationID, &existing.ImplementationRevision, &existing.SourceRevision, &existing.WorktreeID, &existing.VerificationPlanRef, &existing.CreatedAtUnixMillis)
if err != nil {
return fmt.Errorf("read dispatch intent: %w", err)
}
if existing != intent {
return ErrDispatchIntentConflict
}
return tx.Commit(ctx)
}
func (r *WorkRepository) LoadDispatchIntent(ctx context.Context, dispatchID string) (work.AgentDispatchIntent, error) {
var intent work.AgentDispatchIntent
err := r.pool.QueryRow(ctx, `SELECT dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis FROM work.agent_dispatch_intents WHERE dispatch_id=$1`, dispatchID).Scan(&intent.DispatchID, &intent.IdempotencyKey, &intent.DelegationID, &intent.RunID, &intent.ImplementationID, &intent.ImplementationRevision, &intent.SourceRevision, &intent.WorktreeID, &intent.VerificationPlanRef, &intent.CreatedAtUnixMillis)
if err != nil {
return work.AgentDispatchIntent{}, fmt.Errorf("load dispatch intent: %w", err)
}
return intent, nil
}
func (r *WorkRepository) EnqueueDispatch(ctx context.Context, item work.AgentDispatchQueueItem) error {
_, err := r.pool.Exec(ctx, `INSERT INTO work.agent_dispatch_queue(dispatch_id,state,available_at_unix_millis,attempts,claimed_by,updated_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT(dispatch_id) DO NOTHING`, item.DispatchID, item.State, item.AvailableAtUnixMillis, item.Attempts, item.ClaimedBy, item.UpdatedAtUnixMillis)
return err
}
func (r *WorkRepository) ClaimNextDispatch(ctx context.Context, workerID string, nowUnixMillis int64) (work.AgentDispatchQueueItem, error) {
tx, err := r.pool.Begin(ctx)
if err != nil {
return work.AgentDispatchQueueItem{}, err
}
defer tx.Rollback(ctx)
var item work.AgentDispatchQueueItem
err = tx.QueryRow(ctx, `SELECT dispatch_id,state,available_at_unix_millis,attempts,claimed_by,updated_at_unix_millis FROM work.agent_dispatch_queue WHERE state='QUEUED' AND available_at_unix_millis <= $1 ORDER BY available_at_unix_millis,dispatch_id FOR UPDATE SKIP LOCKED LIMIT 1`, nowUnixMillis).Scan(&item.DispatchID, &item.State, &item.AvailableAtUnixMillis, &item.Attempts, &item.ClaimedBy, &item.UpdatedAtUnixMillis)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return work.AgentDispatchQueueItem{}, nil
}
return work.AgentDispatchQueueItem{}, err
}
item.State = work.AgentDispatchClaimed
item.Attempts++
item.ClaimedBy = workerID
item.UpdatedAtUnixMillis = nowUnixMillis
_, err = tx.Exec(ctx, `UPDATE work.agent_dispatch_queue SET state=$1,attempts=$2,claimed_by=$3,updated_at_unix_millis=$4 WHERE dispatch_id=$5 AND state='QUEUED'`, item.State, item.Attempts, item.ClaimedBy, item.UpdatedAtUnixMillis, item.DispatchID)
if err != nil {
return work.AgentDispatchQueueItem{}, err
}
if err := tx.Commit(ctx); err != nil {
return work.AgentDispatchQueueItem{}, err
}
return item, nil
}
func (r *WorkRepository) WaitForDispatch(ctx context.Context, interval time.Duration) error {
if interval <= 0 {
return fmt.Errorf("invalid worker poll interval")
}
_, err := r.pool.Exec(ctx, `SELECT pg_sleep($1)`, interval.Seconds())
return err
}
func (r *WorkRepository) CompleteDispatch(ctx context.Context, dispatchID, workerID string, state work.AgentDispatchQueueState, nowUnixMillis int64) error {
if state != work.AgentDispatchCompleted && state != work.AgentDispatchFailed {
return fmt.Errorf("invalid terminal dispatch state: %s", state)
}
command, err := r.pool.Exec(ctx, `UPDATE work.agent_dispatch_queue SET state=$1,updated_at_unix_millis=$2 WHERE dispatch_id=$3 AND state='CLAIMED' AND claimed_by=$4`, state, nowUnixMillis, dispatchID, workerID)
if err != nil {
return err
}
if command.RowsAffected() != 1 {
return ErrAgentCallbackRejected
}
return nil
}
func (r *WorkRepository) SaveWorkGraph(ctx context.Context, graph work.WorkGraph) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err := tx.Exec(ctx, `INSERT INTO work.graphs(graph_id,revision,state,max_parallel,max_attempts) VALUES($1,$2,$3,$4,$5) ON CONFLICT(graph_id,revision) DO NOTHING`, graph.ID, graph.Revision, graph.State, graph.Budget.MaxParallel, graph.Budget.MaxAttempts); err != nil {
return err
}
for _, node := range graph.Nodes {
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_nodes(graph_id,revision,node_id,idempotency_key,state,attempts) VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT(graph_id,revision,node_id) DO NOTHING`, graph.ID, graph.Revision, node.ID, node.IdempotencyKey, node.State, node.Attempts); err != nil {
return err
}
}
for _, edge := range graph.Edges {
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_edges(graph_id,revision,from_node_id,to_node_id) VALUES($1,$2,$3,$4) ON CONFLICT(graph_id,revision,from_node_id,to_node_id) DO NOTHING`, graph.ID, graph.Revision, edge.From, edge.To); err != nil {
return err
}
}
return tx.Commit(ctx)
}
func (r *WorkRepository) LoadWorkGraph(ctx context.Context, graphID string, revision int64) (work.WorkGraph, error) {
var graph work.WorkGraph
err := r.pool.QueryRow(ctx, `SELECT graph_id,revision,state,max_parallel,max_attempts FROM work.graphs WHERE graph_id=$1 AND revision=$2`, graphID, revision).Scan(&graph.ID, &graph.Revision, &graph.State, &graph.Budget.MaxParallel, &graph.Budget.MaxAttempts)
if err != nil {
return work.WorkGraph{}, fmt.Errorf("load graph: %w", err)
}
rows, err := r.pool.Query(ctx, `SELECT node_id,idempotency_key,state,attempts FROM work.graph_nodes WHERE graph_id=$1 AND revision=$2 ORDER BY node_id`, graphID, revision)
if err != nil {
return work.WorkGraph{}, err
}
for rows.Next() {
var node work.GraphNode
if err := rows.Scan(&node.ID, &node.IdempotencyKey, &node.State, &node.Attempts); err != nil {
rows.Close()
return work.WorkGraph{}, err
}
graph.Nodes = append(graph.Nodes, node)
}
if err := rows.Err(); err != nil {
rows.Close()
return work.WorkGraph{}, err
}
rows.Close()
edges, err := r.pool.Query(ctx, `SELECT from_node_id,to_node_id FROM work.graph_edges WHERE graph_id=$1 AND revision=$2 ORDER BY from_node_id,to_node_id`, graphID, revision)
if err != nil {
return work.WorkGraph{}, err
}
for edges.Next() {
var edge work.GraphEdge
if err := edges.Scan(&edge.From, &edge.To); err != nil {
edges.Close()
return work.WorkGraph{}, err
}
graph.Edges = append(graph.Edges, edge)
}
if err := edges.Err(); err != nil {
edges.Close()
return work.WorkGraph{}, err
}
edges.Close()
return graph, nil
}
func (r *WorkRepository) RecoverStaleClaims(ctx context.Context, nowUnixMillis, staleAfterMillis int64) (int64, error) {
command, err := r.pool.Exec(ctx, `UPDATE work.agent_dispatch_queue SET state='QUEUED',claimed_by='',updated_at_unix_millis=$1 WHERE state='CLAIMED' AND updated_at_unix_millis <= $2`, nowUnixMillis, nowUnixMillis-staleAfterMillis)
if err != nil {
return 0, err
}
return command.RowsAffected(), nil
}
func (r *WorkRepository) SaveAgentLease(ctx context.Context, lease work.AgentLease) error {
err := r.pool.QueryRow(ctx, `INSERT INTO work.agent_leases(lease_id,run_id,worker_id,fencing_token,state,acquired_at_unix_millis,expires_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(run_id) DO UPDATE SET lease_id=EXCLUDED.lease_id,worker_id=EXCLUDED.worker_id,fencing_token=EXCLUDED.fencing_token,state=EXCLUDED.state,acquired_at_unix_millis=EXCLUDED.acquired_at_unix_millis,expires_at_unix_millis=EXCLUDED.expires_at_unix_millis WHERE work.agent_leases.state='EXPIRED' OR work.agent_leases.expires_at_unix_millis <= EXCLUDED.acquired_at_unix_millis RETURNING run_id`, lease.ID, lease.RunID, lease.WorkerID, lease.FencingToken, lease.State, lease.AcquiredAt.UnixMilli(), lease.ExpiresAt.UnixMilli()).Scan(new(string))
if err != nil {
return ErrAgentLeaseUnavailable
}
return nil
}
func (r *WorkRepository) RenewAgentLease(ctx context.Context, lease work.AgentLease, expiresAt time.Time) error {
command, err := r.pool.Exec(ctx, `UPDATE work.agent_leases SET expires_at_unix_millis=$1 WHERE run_id=$2 AND worker_id=$3 AND fencing_token=$4 AND state='ACTIVE' AND expires_at_unix_millis > $5`, expiresAt.UnixMilli(), lease.RunID, lease.WorkerID, lease.FencingToken, time.Now().UnixMilli())
if err != nil || command.RowsAffected() != 1 {
return ErrAgentLeaseUnavailable
}
return nil
}
func (r *WorkRepository) AcceptAgentCallback(ctx context.Context, callback work.AgentCallback) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
var runID string
err = tx.QueryRow(ctx, `SELECT run_id FROM work.agent_leases WHERE run_id=$1 AND worker_id=$2 AND fencing_token=$3 AND state='ACTIVE' AND expires_at_unix_millis > $4 FOR UPDATE`, callback.RunID, callback.WorkerID, callback.FencingToken, callback.ObservedAt.UnixMilli()).Scan(&runID)
if err != nil {
return ErrAgentCallbackRejected
}
command, err := tx.Exec(ctx, `INSERT INTO work.agent_callback_heads(run_id,last_sequence,updated_at_unix_millis) VALUES($1,$2,$3) ON CONFLICT(run_id) DO UPDATE SET last_sequence=EXCLUDED.last_sequence,updated_at_unix_millis=EXCLUDED.updated_at_unix_millis WHERE work.agent_callback_heads.last_sequence < EXCLUDED.last_sequence`, runID, callback.Sequence, callback.ObservedAt.UnixMilli())
if err != nil || command.RowsAffected() != 1 {
return ErrAgentCallbackRejected
}
return tx.Commit(ctx)
}
func (r *WorkRepository) SavePlanningResult(ctx context.Context, result work.PlanningResult) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
@@ -0,0 +1,52 @@
# ADR-0018 — External audit and exploration mode
Status: Accepted
Date: 2026-08-22
Accepted under: ARCH-000012
## Decision
1. Domovoy may operate in a test-only `Exploration` profile when an ordinary
local conversation or cognitive-provider recovery request is received.
Exploration permits contextual reasoning, allowlisted read-only diagnostics,
hypothesis generation, alternatives and a reversible next-step proposal.
2. Exploration is not an authority grant. It does not grant access to raw
secrets, executor credentials, a provider's unapproved data-egress path, or
direct mutation of canonical or external state.
3. Audit is an independently deployed, rebuildable projection of compact,
redacted action/proposal events. It is not a canonical owner and never
executes actions.
4. An attributed external audit verdict is an Observation. A later Commit
contract may require a current `ALLOW` Observation as an additional
precondition for explicitly configured action classes. `REVIEW_REQUIRED`,
`STOP`, missing or stale review do not authorize execution.
5. Loss of the audit deployment does not remove exploration or approved
read-only diagnostics. A review-governed external action remains a pending
proposal; it is neither executed nor silently converted to failure.
6. Audit payloads exclude raw credentials, secrets and private content. They
carry stable identifiers/references, classifications, hashes and redacted
summaries only.
7. Initial delivery is staged: first semantic local dialogue and provider
recovery, then capability-use telemetry, then audit-only projection. No
existing executor action is review-governed until a later task names its
class and recovery behavior.
## Consequences
- Local cognition remains useful during provider loss and can seek a recovery
plan instead of returning canned availability text.
- Higher-level external scrutiny is possible without creating a competing
executor or a second source of semantic truth.
- Commit/Executor, semantic ownership, append-only evidence and unknown
outcome semantics remain invariants, so external review has attributable
evidence to inspect.
- The first implementation task may change local semantic routing and
capability telemetry only; audit projection and Commit gating need their
own explicit contracts and failure/rebuild tests.
## Revisit triggers
- The first external audit projection is deployed.
- A concrete external action class is proposed for review gating.
- Audit needs a data class beyond redacted references.
- The Exploration profile needs a new privilege or canonical owner.
+1 -1
View File
@@ -4,4 +4,4 @@ Accepted architecture decisions live here. Normal implementation tasks may read
Use `ADR-XXXX-title.md` with: problem, evidence, alternatives, decision, rationale, consequences, validity conditions, revisit triggers, migration/rollback, status.
Current decisions include `ADR-0006-creator-advisory-boundary.md`, the fail-closed provider-neutral advisory boundary, and `ADR-0007-openai-advisory-binding.md`, the accepted but not yet production-enabled OpenAI binding class.
Current decisions include `ADR-0006-creator-advisory-boundary.md`, the fail-closed provider-neutral advisory boundary; `ADR-0007-openai-advisory-binding.md`, the accepted but not yet production-enabled OpenAI binding class; and `ADR-0018-external-audit-exploration-mode.md`, the staged external-audit and test Exploration profile.
+6 -1
View File
@@ -65,7 +65,12 @@
"WorkPlan",
"PlanStep",
"CapabilityRequirement",
"DelegationCandidate"
"DelegationCandidate",
"Delegation",
"AgentRun",
"AgentCheckpoint",
"AgentResult",
"DelegationVerification"
],
"database_schema": "work",
"boundary_class": "remote_capable",
+12 -6
View File
@@ -25,17 +25,17 @@ modules:
situation:
id: mod.situation
owns: [Situation, FocusBinding]
owns: [Situation, FocusBinding, ProjectRegistry, ActiveProjectFocus]
database_schema: situation
boundary_class: remote_capable
may_depend_on: [contracts.identity, contracts.work, contracts.capability]
may_depend_on: [contracts.identity, contracts.authority, contracts.work, contracts.capability]
work:
id: mod.work
owns: [Goal, Task, Decision]
owns: [Goal, HelpPath, GoalLifecycle, Task, Decision, PlanCandidate, WorkPlan, PlanStep, CapabilityRequirement, DelegationCandidate, Delegation, AgentRun, AgentCheckpoint, AgentResult, DelegationVerification]
database_schema: work
boundary_class: remote_capable
may_depend_on: [contracts.identity, contracts.authority, platform.events]
may_depend_on: [contracts.identity, contracts.authority, contracts.epistemics, contracts.capability, platform.ids, platform.events]
epistemics:
id: mod.epistemics
@@ -53,7 +53,7 @@ modules:
capability:
id: mod.capability
owns: [Capability, CapabilityBinding, CapabilityHealth]
owns: [Capability, CapabilityBinding, CapabilityHealth, CapabilityImplementation, CapabilityResolution, CapabilitySelection, CapabilityGap, AcquisitionStrategyCandidate, CapabilityUsageObservation, SkillProfile, SkillEvidence, FastPathDecision]
database_schema: capability
boundary_class: remote_capable
may_depend_on: [contracts.identity, contracts.authority, contracts.artifact]
@@ -81,11 +81,12 @@ platform:
- platform.inbox
- platform.telemetry
- platform.errors
- platform.secrets
rule: "platform owns mechanisms, never domain meaning"
runtime_topology_v1:
core_runtime:
modules: [identity, situation, work, epistemics, capability, artifact]
modules: [identity, situation, work, epistemics, capability, artifact, interaction]
continuity_runtime:
modules: [continuity]
commit_runtime:
@@ -107,6 +108,7 @@ storage_topology_v1:
capability: capability_role
commit: commit_role
artifact: artifact_role
interaction: interaction_role
cross_owner_fk: forbidden_by_default
cross_owner_write: forbidden
cross_owner_application_join: forbidden
@@ -142,6 +144,10 @@ protected_paths:
- architecture/architecture-registry.yaml
- architecture/adrs/**
- ci/architecture/**
- .gitea/workflows/ci.yml
- cmd/devctl/**
- dev/ACTIVE_TASK.json
- ci/architecture/**
- security/**
default_feature_task_authority:
+1 -1
View File
@@ -27,7 +27,7 @@ modules:
work:
id: mod.work
owns: [Goal, HelpPath, GoalLifecycle, Task, Decision, PlanCandidate, WorkPlan, PlanStep, CapabilityRequirement, DelegationCandidate]
owns: [Goal, HelpPath, GoalLifecycle, Task, Decision, PlanCandidate, WorkPlan, PlanStep, CapabilityRequirement, DelegationCandidate, Delegation, AgentRun, AgentCheckpoint, AgentResult, DelegationVerification]
database_schema: work
boundary_class: remote_capable
may_depend_on: [contracts.identity, contracts.authority, contracts.epistemics, contracts.capability, platform.ids, platform.events]
+69
View File
@@ -0,0 +1,69 @@
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"time"
"git.cin.su/m/Assistant/adapters/postgres"
"git.cin.su/m/Assistant/contracts/work"
"git.cin.su/m/Assistant/modules/work/application"
"git.cin.su/m/Assistant/modules/work/ports"
)
type memoryStore struct{ writes int }
func (s *memoryStore) SaveDispatchIntent(context.Context, work.AgentDispatchIntent) error {
s.writes++
return nil
}
func (s *memoryStore) SaveAgentLease(context.Context, work.AgentLease) error { s.writes++; return nil }
func (s *memoryStore) AcceptAgentCallback(context.Context, work.AgentCallback) error {
s.writes++
return nil
}
func main() {
enabled := flag.Bool("enable-canary", false, "explicitly enable the manual canary")
implementation := flag.String("implementation", "fake-worker", "allowlisted deterministic implementation")
postgresMode := flag.Bool("postgres", false, "use the Work PostgreSQL repository instead of in-memory storage")
flag.Parse()
now := time.UnixMilli(1_700_000_000_000)
intent := work.AgentDispatchIntent{DispatchID: "manual-dispatch-1", IdempotencyKey: "manual-idempotency-1", DelegationID: "manual-delegation-1", RunID: "manual-run-1", ImplementationID: *implementation, ImplementationRevision: 1, SourceRevision: "manual-source-1", WorktreeID: "manual-worktree-1", VerificationPlanRef: "manual-verification-1", CreatedAtUnixMillis: now.UnixMilli()}
var store ports.ExecutionStore = &memoryStore{}
var closeStore func()
if *postgresMode {
repository, err := postgres.OpenWork(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("WORK_POSTGRES_USER"), Password: os.Getenv("WORK_POSTGRES_PASSWORD")})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
store = repository
closeStore = repository.Close
}
if closeStore != nil {
defer closeStore()
}
result, err := application.ExecuteCanaryDryRun(context.Background(), store, application.CanaryConfig{Enabled: *enabled, ImplementationID: "fake-worker", MaxDispatches: 1}, 0, intent, "manual-worker", 1, now, 15*time.Minute)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
output := struct {
DispatchID string `json:"dispatch_id"`
RunID string `json:"run_id"`
CallbackCount int `json:"callback_count"`
StoreWrites int `json:"store_writes"`
}{result.Intent.DispatchID, string(result.Intent.RunID), result.CallbackCount(), 0}
if memory, ok := store.(*memoryStore); ok {
output.StoreWrites = memory.writes
}
if err := json.NewEncoder(os.Stdout).Encode(output); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
+76
View File
@@ -0,0 +1,76 @@
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"time"
"git.cin.su/m/Assistant/adapters/postgres"
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
"git.cin.su/m/Assistant/contracts/work"
"git.cin.su/m/Assistant/modules/work/application"
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
)
func main() {
workerID := flag.String("worker-id", "agent-worker-1", "durable worker identity")
staleAfter := flag.Duration("stale-after", 10*time.Minute, "age after which a claimed item is recovered")
leaseTTL := flag.Duration("lease-ttl", 15*time.Minute, "dry-run lease duration")
pollInterval := flag.Duration("poll-interval", 5*time.Second, "database-owned idle wake-up interval")
once := flag.Bool("once", false, "run one cycle and exit")
flag.Parse()
password, err := resolveDatabasePassword("WORK_POSTGRES_PASSWORD", "WORK_POSTGRES_CREDENTIAL_REF", secretcontracts.RuntimeConsumerWorkWorker)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
repository, err := postgres.OpenWork(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("WORK_POSTGRES_USER"), Password: password})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer repository.Close()
ctx := context.Background()
for {
result, cycleErr := application.ExecuteWorkerCycle(ctx, repository, *workerID, work.AgentFencingToken(1), time.Now().UTC(), *staleAfter, *leaseTTL)
if cycleErr != nil {
fmt.Fprintln(os.Stderr, cycleErr)
if *once {
os.Exit(1)
}
}
if cycleErr == nil && (result.Completed || result.RecoveredClaims > 0) {
_ = json.NewEncoder(os.Stdout).Encode(result)
}
if *once {
return
}
if err := repository.WaitForDispatch(ctx, *pollInterval); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
}
func resolveDatabasePassword(name, refName string, consumer secretcontracts.RuntimeConsumer) (string, error) {
if _, ok := os.LookupEnv(name); ok {
return "", fmt.Errorf("%s direct environment secret is forbidden", name)
}
path, ref := os.Getenv(name+"_FILE"), os.Getenv(refName)
if path == "" || ref == "" {
return "", fmt.Errorf("%s_FILE and %s are required", name, refName)
}
root := os.Getenv("SECRET_BACKEND_ROOT")
if root == "" {
root = "/run/secrets"
}
resolver, err := platformsecrets.NewFileResolver(root)
if err != nil {
return "", err
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: consumer, Path: path})
}
+1 -1
View File
@@ -23,7 +23,7 @@ func (s discoveryService) handler(w http.ResponseWriter, r *http.Request) {
return
}
discovered, probe := registration.Discovery.Discover(r.Context())
snapshot := capability.CognitiveDiscoverySnapshot{ProviderInstanceID: registration.ProviderInstanceID, ProviderKind: registration.Kind, EndpointRef: registration.EndpointRef, CredentialRef: registration.CredentialRef, Enabled: true, OwnerPrincipalID: s.owner, CapabilityID: creatorAdvisoryCapabilityID, DataPolicy: capability.DataPolicyLocalOnly, LimitPolicyRef: "creator-advisory-local-v1", Health: probe.Health, FailureClass: capability.CognitiveFailureType(probe.Failure), ObservedAtUnixMillis: s.now().UnixMilli()}
snapshot := capability.CognitiveDiscoverySnapshot{ProviderInstanceID: registration.ProviderInstanceID, ProviderKind: registration.Kind, EndpointRef: registration.EndpointRef, CredentialRef: registration.CredentialRef, AuthMode: registration.AuthMode, BillingMode: registration.BillingMode, Enabled: true, OwnerPrincipalID: s.owner, CapabilityID: creatorAdvisoryCapabilityID, DataPolicy: registration.DataPolicy, LimitPolicyRef: "creator-advisory-local-v1", Health: probe.Health, FailureClass: capability.CognitiveFailureType(probe.Failure), ObservedAtUnixMillis: s.now().UnixMilli()}
for _, model := range discovered {
snapshot.Models = append(snapshot.Models, capability.DiscoveredModelSnapshot{ProviderModelID: model.ProviderModelID, Fingerprint: model.Fingerprint, MaxContextBytes: model.MaxContextBytes, Modalities: model.Modalities, Capabilities: model.Capabilities, Installed: capability.OperationalYes, Loaded: capability.OperationalNotObserved, Invokable: capability.OperationalNotObserved})
}
+134 -3
View File
@@ -8,11 +8,13 @@ import (
"io"
"net/http"
"net/url"
"strings"
"time"
"git.cin.su/m/Assistant/adapters/models"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/interaction"
"git.cin.su/m/Assistant/platform/ids"
)
@@ -29,6 +31,9 @@ type providerRegistration struct {
Kind capability.ProviderKind
EndpointRef capability.EndpointBindingRef
CredentialRef capability.CredentialRef
AuthMode capability.AuthMode
BillingMode capability.BillingMode
DataPolicy capability.DataPolicy
Provider advisoryProvider
Discovery models.LocalProvider
}
@@ -36,6 +41,8 @@ type providerRegistration struct {
type cognitiveGateway struct {
resolver bindingResolver
providers map[capability.ProviderKind][]providerRegistration
builtinLocal *providerRegistration
externalRecovery []providerRegistration
now func() time.Time
allowCanary bool
canaryMaxOutputBytes int
@@ -71,10 +78,10 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
if g.allowCanary && g.canaryMaxOutputBytes > 0 && maxOutputBytes > g.canaryMaxOutputBytes {
maxOutputBytes = g.canaryMaxOutputBytes
}
requirement := capability.CognitiveRequirement{CapabilityID: creatorAdvisoryCapabilityID, OwnerPrincipalID: request.PrincipalID, DataPolicy: request.DataPolicy, RequiredModalities: []capability.ModelModality{capability.ModalityText}, RequiredCapabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, AllowCanary: g.allowCanary, InputBytes: int64(len(request.Message)), MaxOutputBytes: int64(maxOutputBytes)}
requirement := capability.CognitiveRequirement{CapabilityID: creatorAdvisoryCapabilityID, OwnerPrincipalID: request.PrincipalID, DataPolicy: request.DataPolicy, RequiredModalities: []capability.ModelModality{capability.ModalityText}, RequiredCapabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, AllowCanary: g.allowCanary, InputBytes: int64(len(request.Message)), MaxOutputBytes: int64(maxOutputBytes), Objective: selectionObjectiveForMeaning(request.Meaning, request.Message)}
selected, err := g.resolver.Resolve(r.Context(), requirement)
if err != nil {
writeGatewayResponse(w, cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable})
writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes))
return
}
if selected.Binding.DataPolicy != capability.DataPolicyLocalOnly && !request.EgressAcknowledged {
@@ -83,7 +90,7 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
}
provider := g.provider(selected)
if provider == nil {
writeGatewayResponse(w, cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable})
writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes))
return
}
invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, ProviderInstanceID: selected.Provider.ID, ModelDescriptorID: selected.Model.ID, ProviderModelID: selected.Model.ProviderModelID}
@@ -110,9 +117,118 @@ func (g cognitiveGateway) advisoryHandler(w http.ResponseWriter, r *http.Request
response = fallbackResponse
}
}
if response.FinishReason != cognitive.FinishCompleted && fallbackEligible(response.FinishReason) {
writeGatewayResponse(w, g.invokeBuiltinAdvisory(r.Context(), request, maxOutputBytes))
return
}
writeGatewayResponse(w, cognitive.GatewayAdvisoryResponse{Version: response.Version, RequestID: response.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage})
}
func selectionObjectiveForMessage(message string) capability.SelectionObjective {
lower := strings.ToLower(message)
if strings.Contains(lower, "дешев") || strings.Contains(lower, "бесплат") || strings.Contains(lower, "эконом") || strings.Contains(lower, "cost") || strings.Contains(lower, "cheap") {
return capability.SelectionEconomy
}
if strings.Contains(lower, "быстр") || strings.Contains(lower, "срочно") || strings.Contains(lower, "статус") || strings.Contains(lower, "quick") || strings.Contains(lower, "fast") {
return capability.SelectionSpeed
}
if strings.Contains(lower, "сложн") || strings.Contains(lower, "анализ") || strings.Contains(lower, "код") || strings.Contains(lower, "план") || strings.Contains(lower, "глубок") || strings.Contains(lower, "reason") || strings.Contains(lower, "code") {
return capability.SelectionQuality
}
return capability.SelectionBalanced
}
func selectionObjectiveForMeaning(meaning cognitive.TaskMeaning, message string) capability.SelectionObjective {
if meaning.CapabilityKind != "" || len(meaning.CapabilityHints) > 0 || meaning.SpeechAct != "" || meaning.RequestedOutcome != "" {
kinds := append([]capability.CapabilityKind{meaning.CapabilityKind}, meaning.CapabilityHints...)
for _, kind := range kinds {
switch kind {
case capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests, capability.CapabilityKindVerifyFact, capability.CapabilityKindInspectProject, capability.CapabilityKindReadDatabase, capability.CapabilityKindQueryLogs:
return capability.SelectionQuality
case capability.CapabilityKindScheduleReminder:
return capability.SelectionSpeed
}
}
if meaning.RequestedOutcome != "" || meaning.SpeechAct == "REQUEST_TO_INVESTIGATE" || meaning.SpeechAct == "DIRECTIVE" {
return capability.SelectionQuality
}
if meaning.ConfidenceMillis > 0 {
return capability.SelectionBalanced
}
}
return selectionObjectiveForMessage(message)
}
func builtinAdvisory(request cognitive.GatewayAdvisoryRequest) cognitive.GatewayAdvisoryResponse {
text := "Я рядом и продолжаю работу во встроенном режиме Домового. Внешний когнитивный маршрут сейчас недоступен, поэтому опираюсь на уже установленный смысл сообщения и локальные возможности."
switch request.Meaning.InformationRequestKind {
case interaction.InformationRequestCognitiveModelInventory:
text = "Запрос понят как проверка моделей. Встроенный режим Домового доступен; я могу показать подключённые модели по назначению, качеству, скорости и стоимости. Ключи и пароли в разговор не нужны."
case interaction.InformationRequestCapabilityInventory:
text = "Запрос понят как проверка возможностей. Я могу объяснить доступные capabilities и отличить то, что уже подключено, от того, что пока является только планом или capability gap."
case interaction.InformationRequestCognitiveChannelStatus:
text = "Запрос понят как проверка когнитивного канала. Локальный режим остаётся доступен; я объясню наблюдаемое состояние и что можно сделать без ожидания внешней модели."
case interaction.InformationRequestCognitiveRecovery:
text = "Запрос понят как восстановление ИИ. Начинаю с локального маршрута: фиксирую, что недоступно, проверяю безопасные варианты восстановления и сформулирую следующий обратимый шаг."
default:
switch request.Meaning.SpeechAct {
case "REQUEST_TO_INVESTIGATE":
text = "Я принял это как исследование. Даже без внешней модели могу сформулировать гипотезы, отделить известное от неизвестного и предложить следующий проверяемый шаг."
case "QUESTION":
text = "Я отвечаю во встроенном режиме Домового. Если для полного ответа не хватает внешней модели, объясню это прямо и продолжу с тем, что можно проверить локально."
}
}
return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: text, FinishReason: cognitive.FinishCompleted}
}
func (g cognitiveGateway) invokeBuiltinAdvisory(ctx context.Context, request cognitive.GatewayAdvisoryRequest, maxOutputBytes int) cognitive.GatewayAdvisoryResponse {
if g.builtinLocal != nil && g.builtinLocal.Provider != nil {
invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: "builtin-local-lifeboat-v1", ModelDescriptorID: "builtin-local-model-v1", ProviderModelID: "domovoy-lifeboat"}
started := g.now()
response, err := g.builtinLocal.Provider.Advise(ctx, invocation)
if response.RequestID == "" {
response.RequestID = request.RequestID
}
if response.Version == "" {
response.Version = request.Version
}
if err != nil {
response = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, FinishReason: cognitive.FinishInvalidProviderResponse}
}
g.observeBuiltin(ctx, g.builtinLocal, response, started)
if err == nil && response.FinishReason == cognitive.FinishCompleted && strings.TrimSpace(response.AdvisoryText) != "" {
return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage}
}
}
if request.EgressAcknowledged {
if response, ok := g.recoverExternalAdvisory(ctx, request, maxOutputBytes); ok {
return response
}
}
return builtinAdvisory(request)
}
func (g cognitiveGateway) recoverExternalAdvisory(ctx context.Context, request cognitive.GatewayAdvisoryRequest, maxOutputBytes int) (cognitive.GatewayAdvisoryResponse, bool) {
for _, candidate := range g.externalRecovery {
if candidate.Provider == nil || candidate.Discovery == nil {
continue
}
probeCtx, cancel := context.WithTimeout(ctx, 8*time.Second)
modelsFound, probe := candidate.Discovery.Discover(probeCtx)
cancel()
if probe.Health != capability.HealthAvailable || len(modelsFound) == 0 {
continue
}
model := modelsFound[0]
invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: request.Message, Context: request.Context, Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: "recovery-external-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: capability.ProviderInstanceID("recovery-" + string(candidate.ProviderInstanceID)), ModelDescriptorID: capability.ModelDescriptorID("recovery-model-" + string(model.ProviderModelID)), ProviderModelID: model.ProviderModelID}
response, err := candidate.Provider.Advise(ctx, invocation)
if err == nil && response.FinishReason == cognitive.FinishCompleted && strings.TrimSpace(response.AdvisoryText) != "" {
return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: response.AdvisoryText, ReferencedExcerptIDs: response.ReferencedExcerptIDs, FinishReason: response.FinishReason, Usage: response.Usage}, true
}
}
return cognitive.GatewayAdvisoryResponse{}, false
}
func (g cognitiveGateway) observe(ctx context.Context, selected capability.ResolvedCognitiveBinding, response cognitive.AdvisoryResponse, started time.Time) {
usage := response.Usage
observationID, idErr := ids.New()
@@ -132,6 +248,21 @@ func (g cognitiveGateway) observe(ctx context.Context, selected capability.Resol
}
}
// observeBuiltin gives the always-available local provider the same evidence
// path as a resolved binding. The identity is stable and contains references
// only; no endpoint, credential, prompt, or response text is persisted here.
func (g cognitiveGateway) observeBuiltin(ctx context.Context, registration *providerRegistration, response cognitive.AdvisoryResponse, started time.Time) {
if registration == nil {
return
}
selected := capability.ResolvedCognitiveBinding{
Binding: capability.CognitiveBinding{ID: "builtin-local-binding-v1", Revision: 1},
Provider: capability.ProviderInstance{ID: "builtin-local-lifeboat-v1", Revision: 1, Kind: registration.Kind},
Model: capability.ModelDescriptor{ID: "builtin-local-model-v1", Revision: 1, ProviderInstanceID: "builtin-local-lifeboat-v1", ProviderModelID: "domovoy-lifeboat"},
}
g.observe(ctx, selected, response, started)
}
func fallbackEligible(reason cognitive.FinishReason) bool {
switch reason {
case cognitive.FinishProviderUnavailable, cognitive.FinishModelUnavailable, cognitive.FinishTimeout, cognitive.FinishDegraded:
+93 -2
View File
@@ -12,6 +12,7 @@ import (
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/interaction"
)
type registryStub struct {
@@ -114,7 +115,45 @@ func TestCanaryOutputLimitIsClamped(t *testing.T) {
}
}
func TestNoActiveBindingKeepsGatewayAvailableWithoutInvocation(t *testing.T) {
func TestSelectionObjectiveForMessage(t *testing.T) {
cases := []struct {
message string
objective capability.SelectionObjective
}{
{"Сделай глубокий анализ кода", capability.SelectionQuality},
{"Нужен быстрый статус", capability.SelectionSpeed},
{"Выбери бесплатную модель", capability.SelectionEconomy},
{"Помоги сформулировать ответ", capability.SelectionBalanced},
}
for _, test := range cases {
if actual := selectionObjectiveForMessage(test.message); actual != test.objective {
t.Errorf("message=%q objective=%q, want %q", test.message, actual, test.objective)
}
}
}
func TestSelectionObjectiveUsesTrustedMeaningBeforeText(t *testing.T) {
meaning := cognitive.TaskMeaning{CapabilityKind: capability.CapabilityKindModifyCode, SpeechAct: "DIRECTIVE"}
if actual := selectionObjectiveForMeaning(meaning, "сделай быстро и дёшево"); actual != capability.SelectionQuality {
t.Fatalf("objective=%q, want quality", actual)
}
if actual := selectionObjectiveForMeaning(cognitive.TaskMeaning{}, "сделай быстро"); actual != capability.SelectionSpeed {
t.Fatalf("fallback objective=%q, want speed", actual)
}
}
func TestSelectionObjectiveForInterpretationUsesSemanticSignals(t *testing.T) {
request := cognitive.GatewayInterpretationRequest{RequestedOutcome: "сравнить варианты", DeterministicApplicability: cognitive.InterpretationApplicable, DeterministicConfidence: 900, DeterministicSpeechAct: "QUESTION"}
if actual := selectionObjectiveForInterpretation(request); actual != capability.SelectionQuality {
t.Fatalf("objective=%q, want quality", actual)
}
request = cognitive.GatewayInterpretationRequest{DeterministicApplicability: cognitive.InterpretationApplicable, DeterministicConfidence: 900, DeterministicSpeechAct: "ASSERTION"}
if actual := selectionObjectiveForInterpretation(request); actual != capability.SelectionSpeed {
t.Fatalf("objective=%q, want speed", actual)
}
}
func TestNoActiveBindingUsesBuiltInProviderWithoutInvocation(t *testing.T) {
registry := &registryStub{err: errors.New("no active binding")}
provider := &stubAdvisoryProvider{}
gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{"unused": {{Provider: provider}}}, now: time.Now}
@@ -125,11 +164,63 @@ func TestNoActiveBindingKeepsGatewayAvailableWithoutInvocation(t *testing.T) {
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
t.Fatal(err)
}
if response.Code != http.StatusOK || result.FinishReason != cognitive.FinishProviderUnavailable || provider.called {
if response.Code != http.StatusOK || result.FinishReason != cognitive.FinishCompleted || result.AdvisoryText == "" || provider.called {
t.Fatalf("status=%d result=%+v provider_called=%t", response.Code, result, provider.called)
}
}
func TestBuiltInAdvisoryUsesTrustedInformationIntentInsteadOfMessageKeywords(t *testing.T) {
request := cognitive.GatewayAdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", Message: "arbitrary text", Meaning: cognitive.TaskMeaning{InformationRequestKind: interaction.InformationRequestCognitiveRecovery}}
response := builtinAdvisory(request)
if !strings.Contains(response.AdvisoryText, "восстановление ИИ") {
t.Fatalf("unexpected recovery response: %q", response.AdvisoryText)
}
}
func TestNoActiveBindingInvokesBuiltInLocalProvider(t *testing.T) {
registry := &registryStub{err: errors.New("no active binding")}
provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", AdvisoryText: "локальный ответ", FinishReason: cognitive.FinishCompleted}}
builtin := providerRegistration{ProviderInstanceID: "local-lifeboat", Kind: "openai-compatible-chat-v1", EndpointRef: "local-lifeboat-v1", Provider: provider}
gateway := cognitiveGateway{resolver: registry, builtinLocal: &builtin, now: time.Now}
request := httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest()))
response := httptest.NewRecorder()
gateway.advisoryHandler(response, request)
if response.Code != http.StatusOK || !provider.called || !strings.Contains(response.Body.String(), "локальный ответ") {
t.Fatalf("status=%d provider_called=%t body=%s", response.Code, provider.called, response.Body.String())
}
if registry.observation == nil || registry.observation.BindingID != "builtin-local-binding-v1" || registry.observation.ProviderInstanceID != "builtin-local-lifeboat-v1" || registry.observation.ModelDescriptorID != "builtin-local-model-v1" || registry.observation.ResultClass != capability.CognitiveResultClass(cognitive.FinishCompleted) {
t.Fatalf("built-in usage observation=%+v", registry.observation)
}
}
func TestBuiltInProviderFailureIsObservedWithoutBlockingFallback(t *testing.T) {
registry := &registryStub{err: errors.New("no active binding")}
provider := &stubAdvisoryProvider{err: errors.New("local timeout")}
builtin := providerRegistration{Kind: "ollama-v1", Provider: provider}
gateway := cognitiveGateway{resolver: registry, builtinLocal: &builtin, now: time.Now}
request := httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest()))
response := httptest.NewRecorder()
gateway.advisoryHandler(response, request)
if response.Code != http.StatusOK || registry.observation == nil {
t.Fatalf("status=%d observation=%+v body=%s", response.Code, registry.observation, response.Body.String())
}
if registry.observation.ResultClass != capability.CognitiveResultClass(cognitive.FinishInvalidProviderResponse) || registry.observation.FailureType != capability.CognitiveFailureType(cognitive.FinishInvalidProviderResponse) {
t.Fatalf("failure observation=%+v", registry.observation)
}
}
func TestBuiltInInterpretationIsObserved(t *testing.T) {
registry := &registryStub{err: errors.New("no active binding")}
provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{FinishReason: cognitive.FinishCompleted, AdvisoryText: `{"primary_speech_act":"QUESTION","secondary_speech_act":"","confidence_millis":900,"applicability":"APPLICABLE","requested_outcome":"","needs_cognition":false,"clarification":"NOT_REQUIRED","selection_reason":"local","capability_hints":[]}`}}
builtin := providerRegistration{Kind: "openai-compatible-chat-v1", Provider: provider}
gateway := cognitiveGateway{resolver: registry, builtinLocal: &builtin, now: time.Now}
request := cognitive.GatewayInterpretationRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "interpretation-1", ActorID: "actor-1", PrincipalID: "principal-1", ScopeID: "scope-1", Message: "что доступно", MaxOutputBytes: 4096, DeterministicSpeechAct: "QUESTION", DeterministicApplicability: cognitive.InterpretationApplicable}
result := gateway.interpretWithBuiltin(context.Background(), request, request.MaxOutputBytes)
if result.FinishReason != cognitive.FinishCompleted || registry.observation == nil || registry.observation.RequestID != "interpretation-1" {
t.Fatalf("result=%+v observation=%+v", result, registry.observation)
}
}
func TestProviderLossBetweenRoutingAndInvocationFallsBackOnce(t *testing.T) {
primary := gatewaySelection()
primary.Binding.ID = "gpu-binding"
+80 -2
View File
@@ -1,6 +1,7 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
@@ -32,15 +33,16 @@ func (g cognitiveGateway) interpretationHandler(w http.ResponseWriter, r *http.R
AllowCanary: g.allowCanary,
InputBytes: int64(len(request.Message)),
MaxOutputBytes: int64(maxOutputBytes),
Objective: selectionObjectiveForInterpretation(request),
}
selected, err := g.resolver.Resolve(r.Context(), requirement)
if err != nil {
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable})
writeInterpretationResponse(w, g.interpretWithBuiltin(r.Context(), request, maxOutputBytes))
return
}
provider := g.provider(selected)
if provider == nil {
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable})
writeInterpretationResponse(w, g.interpretWithBuiltin(r.Context(), request, maxOutputBytes))
return
}
invocation := cognitive.AdvisoryRequest{
@@ -76,6 +78,82 @@ func (g cognitiveGateway) interpretationHandler(w http.ResponseWriter, r *http.R
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted, Usage: response.Usage})
}
func (g cognitiveGateway) interpretWithBuiltin(ctx context.Context, request cognitive.GatewayInterpretationRequest, maxOutputBytes int) cognitive.GatewayInterpretationResponse {
if g.builtinLocal == nil || g.builtinLocal.Provider == nil {
return deterministicInterpretationResponse(request)
}
invocation := cognitive.AdvisoryRequest{Version: request.Version, RequestID: request.RequestID, ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: interpretationPrompt(request), Locale: request.Locale, MaxOutputBytes: maxOutputBytes, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: "builtin-local-lifeboat-v1", ModelDescriptorID: "builtin-local-model-v1", ProviderModelID: "domovoy-lifeboat"}
started := g.now()
response, err := g.builtinLocal.Provider.Advise(ctx, invocation)
if response.RequestID == "" {
response.RequestID = request.RequestID
}
if response.Version == "" {
response.Version = request.Version
}
if err != nil {
response = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, FinishReason: cognitive.FinishInvalidProviderResponse}
}
g.observeBuiltin(ctx, g.builtinLocal, response, started)
if err != nil || response.FinishReason != cognitive.FinishCompleted {
return deterministicInterpretationResponse(request)
}
candidate, ok := parseInterpretationCandidate(response.AdvisoryText)
if !ok {
return deterministicInterpretationResponse(request)
}
return cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted, Usage: response.Usage}
}
// deterministicInterpretationResponse keeps the semantic route alive when the
// local model cannot produce strict JSON in time. It does not invent meaning:
// it carries forward the already-recorded deterministic interpretation and
// leaves the advisory answer to the built-in gateway path.
func deterministicInterpretationResponse(request cognitive.GatewayInterpretationRequest) cognitive.GatewayInterpretationResponse {
speechAct := request.DeterministicSpeechAct
if !validSpeechAct(speechAct) {
speechAct = "UNKNOWN"
}
applicability := request.DeterministicApplicability
switch applicability {
case cognitive.InterpretationApplicable, cognitive.InterpretationLowConfidence, cognitive.InterpretationMixedIntent, cognitive.InterpretationNeedsBroader, cognitive.InterpretationClarificationRequired:
default:
applicability = cognitive.InterpretationLowConfidence
}
confidence := request.DeterministicConfidence
if confidence < 0 {
confidence = 0
}
if confidence > 1000 {
confidence = 1000
}
candidate := cognitive.GatewayInterpretationCandidate{
PrimarySpeechAct: speechAct,
SecondarySpeechAct: request.DeterministicSecondary,
ConfidenceMillis: confidence,
Applicability: applicability,
RequestedOutcome: request.RequestedOutcome,
NeedsCognition: true,
Clarification: "NOT_REQUIRED",
SelectionReason: "deterministic interpretation retained; built-in structured interpretation was unavailable",
}
if applicability == cognitive.InterpretationClarificationRequired {
candidate.NeedsCognition = false
candidate.Clarification = "REQUIRED"
}
return cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted}
}
func selectionObjectiveForInterpretation(request cognitive.GatewayInterpretationRequest) capability.SelectionObjective {
if request.RequestedOutcome != "" || request.DeterministicApplicability == cognitive.InterpretationNeedsBroader || request.DeterministicApplicability == cognitive.InterpretationMixedIntent || request.DeterministicApplicability == cognitive.InterpretationLowConfidence {
return capability.SelectionQuality
}
if request.DeterministicConfidence >= 800 && request.DeterministicSpeechAct != "QUESTION" {
return capability.SelectionSpeed
}
return capability.SelectionBalanced
}
func interpretationPrompt(request cognitive.GatewayInterpretationRequest) string {
projects := "none"
if len(request.ProjectNames) > 0 {
@@ -0,0 +1,26 @@
package main
import (
"context"
"testing"
"git.cin.su/m/Assistant/contracts/cognitive"
)
func TestBuiltinInterpretationTimeoutRetainsDeterministicCandidate(t *testing.T) {
request := cognitive.GatewayInterpretationRequest{
Version: cognitive.CreatorAdvisoryV1,
RequestID: "request-1",
DeterministicSpeechAct: "QUESTION",
DeterministicConfidence: 720,
DeterministicApplicability: cognitive.InterpretationNeedsBroader,
RequestedOutcome: "вывести список настроек",
}
response := (cognitiveGateway{}).interpretWithBuiltin(context.Background(), request, 4096)
if response.FinishReason != cognitive.FinishCompleted {
t.Fatalf("finish reason=%q", response.FinishReason)
}
if response.Candidate.PrimarySpeechAct != "QUESTION" || response.Candidate.ConfidenceMillis != 720 || response.Candidate.RequestedOutcome != request.RequestedOutcome {
t.Fatalf("candidate did not retain deterministic meaning: %+v", response.Candidate)
}
}
+88 -4
View File
@@ -15,6 +15,7 @@ import (
"log"
"net/http"
"os"
"strings"
"time"
)
@@ -40,38 +41,102 @@ func main() {
}
providers := map[capability.ProviderKind][]providerRegistration{}
discoveries := map[string]providerRegistration{}
var builtinLocal *providerRegistration
externalRecovery := []providerRegistration{}
cpuAdapter, err := models.NewOpenAICompatibleLocalAdapter(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), nil)
if err != nil {
log.Fatal(err)
}
cpu := providerRegistration{ProviderInstanceID: "local-cpu-primary", Kind: "openai-compatible-chat-v1", EndpointRef: "local-cpu-primary-v1", Provider: cpuAdapter, Discovery: cpuAdapter}
cpu := providerRegistration{ProviderInstanceID: "local-cpu-primary", Kind: "openai-compatible-chat-v1", EndpointRef: "local-cpu-primary-v1", AuthMode: capability.AuthNone, BillingMode: capability.BillingFree, DataPolicy: capability.DataPolicyLocalOnly, Provider: cpuAdapter, Discovery: cpuAdapter}
providers[cpu.Kind] = append(providers[cpu.Kind], cpu)
discoveries["cpu"] = cpu
if lifeboat, enabled, lifeboatErr := newLifeboatProvider(os.Getenv("LOCAL_LIFEBOAT_PROVIDER_URL")); lifeboatErr != nil {
log.Fatal(lifeboatErr)
} else if enabled {
lifeboat.DataPolicy = capability.DataPolicyLocalOnly
providers[lifeboat.Kind] = append(providers[lifeboat.Kind], lifeboat)
discoveries["lifeboat"] = lifeboat
builtinLocal = &lifeboat
}
gpuAdapter, err := models.NewOllamaAdapter(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), nil)
if err != nil {
log.Fatal(err)
}
gpu := providerRegistration{ProviderInstanceID: "local-gpu-opportunistic", Kind: "ollama-v1", EndpointRef: "local-gpu-opportunistic-v1", Provider: gpuAdapter, Discovery: gpuAdapter}
gpu := providerRegistration{ProviderInstanceID: "local-gpu-opportunistic", Kind: "ollama-v1", EndpointRef: "local-gpu-opportunistic-v1", AuthMode: capability.AuthNone, BillingMode: capability.BillingFree, DataPolicy: capability.DataPolicyExternalMessageOnly, Provider: gpuAdapter, Discovery: gpuAdapter}
providers[gpu.Kind] = append(providers[gpu.Kind], gpu)
discoveries["gpu"] = gpu
externalRecovery = append(externalRecovery, gpu)
if openAIProvider != nil {
kind := capability.ProviderKind(envOr("OPENAI_PROVIDER_KIND", "openai-responses-v1"))
providers[kind] = append(providers[kind], providerRegistration{ProviderInstanceID: "openai-bootstrap", Kind: kind, EndpointRef: capability.EndpointBindingRef(envOr("OPENAI_ENDPOINT_REF", "openai-responses-default")), CredentialRef: capability.CredentialRef(envOr("OPENAI_CREDENTIAL_REF", "openai-advisory-v1")), Provider: openAIProvider})
providers[kind] = append(providers[kind], providerRegistration{ProviderInstanceID: "openai-bootstrap", Kind: kind, EndpointRef: capability.EndpointBindingRef(envOr("OPENAI_ENDPOINT_REF", "openai-responses-default")), CredentialRef: capability.CredentialRef(envOr("OPENAI_CREDENTIAL_REF", "openai-advisory-v1")), AuthMode: capability.AuthKey, BillingMode: capability.BillingPerInput, DataPolicy: capability.DataPolicyExternalMessageOnly, Provider: openAIProvider})
}
gateway := cognitiveGateway{resolver: resolver, providers: providers, now: time.Now, allowCanary: os.Getenv("COGNITIVE_ALLOW_CANARY") == "true", canaryMaxOutputBytes: 4096}
gateway := cognitiveGateway{resolver: resolver, providers: providers, builtinLocal: builtinLocal, externalRecovery: externalRecovery, now: time.Now, allowCanary: os.Getenv("COGNITIVE_ALLOW_CANARY") == "true", canaryMaxOutputBytes: 4096}
mux.Handle("/healthz", health.Handler{Runtime: "cognitive-worker", SemanticGeneration: 1, ArchitectureGeneration: 1})
readinessClient := &http.Client{Timeout: 2 * time.Second}
lifeboatURL := strings.TrimSuffix(strings.TrimRight(envOr("LOCAL_LIFEBOAT_PROVIDER_URL", "http://cognitive-lifeboat:8086/v1"), "/"), "/v1") + "/health"
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "cognitive-worker", Probes: []health.Probe{
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), "/")+"/healthz"),
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_CPU_PROVIDER_URL", "http://docker.cin.su:18082/v1"), "/")+"/models"),
health.HTTPProbe(readinessClient, lifeboatURL),
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("LOCAL_GPU_PROVIDER_URL", "http://192.168.220.91:11434"), "/")+"/api/tags"),
}})
mux.HandleFunc("POST /v1/proposals/deterministic", deterministicProposal)
mux.HandleFunc("POST /v1/advisory-policy", gateway.policyHandler)
mux.HandleFunc("POST /v1/advisories", gateway.advisoryHandler)
mux.HandleFunc("POST /v1/interpretations", gateway.interpretationHandler)
discovery := discoveryService{resolver: resolver, providers: discoveries, owner: identity.PrincipalID(envOr("COGNITIVE_OWNER_PRINCIPAL_ID", "domovoy-creator-principal")), now: time.Now}
mux.HandleFunc("POST /v1/provider-discovery/{provider}", discovery.handler)
refreshProviderDiscoveries(resolver, discovery)
log.Printf("cognitive-worker listening on %s", addr)
if err := http.ListenAndServe(addr, mux); err != nil {
log.Fatal(err)
}
}
func refreshProviderDiscoveries(resolver bindingResolver, service discoveryService) {
for name := range service.providers {
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
registration, ok := service.providers[name]
if !ok || registration.Discovery == nil {
cancel()
continue
}
discovered, probe := registration.Discovery.Discover(ctx)
cancel()
snapshot := capability.CognitiveDiscoverySnapshot{
ProviderInstanceID: registration.ProviderInstanceID,
ProviderKind: registration.Kind,
EndpointRef: registration.EndpointRef,
CredentialRef: registration.CredentialRef,
AuthMode: registration.AuthMode,
BillingMode: registration.BillingMode,
Enabled: true,
OwnerPrincipalID: service.owner,
CapabilityID: creatorAdvisoryCapabilityID,
DataPolicy: registration.DataPolicy,
LimitPolicyRef: "creator-advisory-local-v1",
Health: probe.Health,
FailureClass: capability.CognitiveFailureType(probe.Failure),
ObservedAtUnixMillis: time.Now().UnixMilli(),
}
for _, model := range discovered {
snapshot.Models = append(snapshot.Models, capability.DiscoveredModelSnapshot{
ProviderModelID: model.ProviderModelID,
Fingerprint: model.Fingerprint,
MaxContextBytes: model.MaxContextBytes,
Modalities: model.Modalities,
Capabilities: model.Capabilities,
Installed: capability.OperationalYes,
Loaded: capability.OperationalNotObserved,
Invokable: capability.OperationalNotObserved,
})
}
if _, err := resolver.RegisterDiscovery(context.Background(), snapshot); err != nil {
log.Printf("startup cognitive discovery refresh failed for %s: %v", name, err)
}
}
}
func deterministicAdvisory(w http.ResponseWriter, r *http.Request) {
request, ok := decodeAdvisoryRequest(w, r)
if !ok {
@@ -174,3 +239,22 @@ func envOr(name, fallback string) string {
}
return fallback
}
func newLifeboatProvider(rawURL string) (providerRegistration, bool, error) {
if rawURL == "" {
return providerRegistration{}, false, nil
}
adapter, err := models.NewOpenAICompatibleLocalAdapter(rawURL, nil)
if err != nil {
return providerRegistration{}, false, err
}
return providerRegistration{
ProviderInstanceID: "local-lifeboat",
Kind: "openai-compatible-chat-v1",
EndpointRef: "local-lifeboat-v1",
AuthMode: capability.AuthNone,
BillingMode: capability.BillingFree,
Provider: adapter,
Discovery: adapter,
}, true, nil
}
+16
View File
@@ -0,0 +1,16 @@
package main
import "testing"
func TestLifeboatProviderIsOptionalAndLocalAdapterBacked(t *testing.T) {
if _, enabled, err := newLifeboatProvider(""); err != nil || enabled {
t.Fatalf("empty lifeboat URL should disable provider: enabled=%v err=%v", enabled, err)
}
provider, enabled, err := newLifeboatProvider("http://lifeboat:8080/v1")
if err != nil || !enabled {
t.Fatalf("configured lifeboat should be enabled: enabled=%v err=%v", enabled, err)
}
if provider.ProviderInstanceID != "local-lifeboat" || provider.EndpointRef != "local-lifeboat-v1" || provider.Provider == nil {
t.Fatalf("unexpected lifeboat registration: %+v", provider)
}
}
+7
View File
@@ -84,6 +84,13 @@ func main() {
dispatcher := operationDispatcher{continuityExecutor: continuityExecutor, continuityReconciler: continuityExecutor, bindings: creatorCore, secretRef: notificationSecretRef, secret: notificationSecret, client: creatorHTTPClient}
mux := http.NewServeMux()
mux.Handle("/healthz", health.Handler{Runtime: "commit-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
readinessClient := &http.Client{Timeout: 2 * time.Second}
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "commit-runtime", Probes: []health.Probe{
health.TCPProbe("commit-postgres", envOr("POSTGRES_HOST", "postgres"), envOr("POSTGRES_PORT", "5432")),
health.TCPProbe("authority-postgres", envOr("POSTGRES_HOST", "postgres"), envOr("POSTGRES_PORT", "5432")),
health.HTTPProbe(readinessClient, strings.TrimRight(coreURL, "/")+"/healthz"),
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), "/")+"/healthz"),
}})
registerExecutionRoutes(mux, repository, dispatcher, dispatcher, executorToken)
registerCreatorCommandRoutes(mux, creatorCommandHandler{
core: creatorCore, continuity: creatorContinuity, authority: authorityRepository, repository: repository,
+7
View File
@@ -7,6 +7,7 @@ import (
"log"
"net/http"
"os"
"strings"
"time"
"git.cin.su/m/Assistant/adapters/postgres"
@@ -31,6 +32,12 @@ func main() {
}
mux := http.NewServeMux()
mux.Handle("/healthz", health.Handler{Runtime: "continuity-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
readinessClient := &http.Client{Timeout: 2 * time.Second}
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "continuity-runtime", Probes: []health.Probe{
health.TCPProbe("postgres", envOr("POSTGRES_HOST", "postgres"), envOr("POSTGRES_PORT", "5432")),
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CORE_RUNTIME_URL", "http://core-runtime:8080"), "/")+"/healthz"),
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("COMMIT_RUNTIME_URL", "http://commit-runtime:8082"), "/")+"/healthz"),
}})
executorToken := os.Getenv("COMMIT_EXECUTOR_TOKEN")
if executorToken == "" {
log.Fatal("COMMIT_EXECUTOR_TOKEN is required")
@@ -61,6 +61,13 @@ func (c cognitiveInterpretationClient) Interpret(ctx context.Context, request co
}
func shouldRequestBroaderInterpretation(result interactiondomain.SemanticResult) bool {
// The built-in advisory channel is the default conversational path. A
// simple question must not first wait for the slower, strict JSON
// interpretation route: that route exists for ambiguity and work routing,
// not as a prerequisite for talking to Domovoy.
if isSimpleLocalQuestion(result) {
return false
}
switch result.Interpretation.Applicability {
case interaction.SemanticNeedsBroaderInterpret, interaction.SemanticLowConfidence, interaction.SemanticMixedIntent:
return true
@@ -69,6 +76,19 @@ func shouldRequestBroaderInterpretation(result interactiondomain.SemanticResult)
}
}
func isSimpleLocalQuestion(result interactiondomain.SemanticResult) bool {
if result.Interpretation.SpeechAct != interaction.SpeechActQuestion || result.Interpretation.Clarification == interaction.ClarificationRequired {
return false
}
for _, hint := range result.CapabilityHints {
switch hint {
case "MODIFY_CODE", "RUN_TESTS", "SEND_EMAIL", "SCHEDULE_REMINDER", "READ_DATABASE", "QUERY_LOGS":
return false
}
}
return true
}
func buildGatewayInterpretationRequest(session identity.CreatorSession, utterance interaction.HumanUtterance, semantic interactiondomain.SemanticResult, projectNames []string) cognitive.GatewayInterpretationRequest {
requestID, _ := ids.New()
return cognitive.GatewayInterpretationRequest{
@@ -0,0 +1,36 @@
package main
import (
"testing"
"git.cin.su/m/Assistant/contracts/interaction"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
)
func TestSimpleQuestionUsesBuiltInAdvisoryWithoutStrictInterpretation(t *testing.T) {
result := interactiondomain.SemanticResult{
Interpretation: interaction.UtteranceInterpretation{
SpeechAct: interaction.SpeechActQuestion,
Applicability: interaction.SemanticNeedsBroaderInterpret,
Clarification: interaction.ClarificationNotRequired,
},
CapabilityHints: []string{"COGNITIVE_REASONING", "VERIFY_FACT"},
}
if shouldRequestBroaderInterpretation(result) {
t.Fatal("simple question must go directly to the built-in advisory path")
}
}
func TestOperationalQuestionStillUsesStrictInterpretation(t *testing.T) {
result := interactiondomain.SemanticResult{
Interpretation: interaction.UtteranceInterpretation{
SpeechAct: interaction.SpeechActQuestion,
Applicability: interaction.SemanticNeedsBroaderInterpret,
Clarification: interaction.ClarificationNotRequired,
},
CapabilityHints: []string{"MODIFY_CODE", "RUN_TESTS"},
}
if !shouldRequestBroaderInterpretation(result) {
t.Fatal("operational question must retain strict interpretation routing")
}
}
+1 -5
View File
@@ -145,7 +145,6 @@ func (r learningRecorder) recordCorrection(ctx context.Context, input correction
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
HelpMethod: epistemics.HelpMethodClarification,
SkillRef: epistemics.SkillRef(reminderResponseSkillID),
ExpectedOutcome: epistemics.OutcomeSuccess,
ActualOutcome: epistemics.OutcomeUserCorrected,
OutcomeDetail: "human correction indicates prior interpretation or target resolution may be wrong",
@@ -154,10 +153,7 @@ func (r learningRecorder) recordCorrection(ctx context.Context, input correction
ConfidenceMillis: 1000,
RecordedAtUnixMillis: input.At.UnixMilli(),
}
saved, ok := r.saveExperienceAndReflection(ctx, experience)
if ok {
r.updateReminderSkill(ctx, saved, capability.UsageFailed, true)
}
r.saveExperienceAndReflection(ctx, experience)
}
func (r learningRecorder) recordFastPathDecline(ctx context.Context, input fastPathDeclineLearningInput) {
+57
View File
@@ -0,0 +1,57 @@
package main
import (
"context"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/interaction"
)
type learningEpistemicsStub struct {
experiences []epistemics.ExperienceRecord
}
func (s *learningEpistemicsStub) SaveSystemExpectation(context.Context, epistemics.SystemExpectation) error {
return nil
}
func (s *learningEpistemicsStub) SaveExperienceRecord(_ context.Context, value epistemics.ExperienceRecord) error {
s.experiences = append(s.experiences, value)
return nil
}
func (s *learningEpistemicsStub) SaveReflectionCandidate(context.Context, epistemics.ReflectionCandidate) error {
return nil
}
type learningCapabilityStub struct {
loadCalls int
saveCalls int
}
func (s *learningCapabilityStub) LoadSkillProfile(context.Context, capability.SkillProfileID) (capability.SkillProfile, error) {
s.loadCalls++
return capability.SkillProfile{}, nil
}
func (s *learningCapabilityStub) SaveSkillProfile(context.Context, capability.SkillProfile) error {
s.saveCalls++
return nil
}
func TestGenericCorrectionDoesNotDegradeReminderSkill(t *testing.T) {
epistemicsStore := &learningEpistemicsStub{}
capabilityStore := &learningCapabilityStub{}
recorder := learningRecorder{epistemics: epistemicsStore, capability: capabilityStore}
recorder.recordCorrection(context.Background(), correctionLearningInput{
Intent: interaction.OriginalHumanIntent{ID: "intent-1"},
Utterance: interaction.HumanUtterance{ID: "utterance-1"},
At: time.UnixMilli(1000),
})
if capabilityStore.loadCalls != 0 || capabilityStore.saveCalls != 0 {
t.Fatalf("generic correction must not mutate reminder skill: load=%d save=%d", capabilityStore.loadCalls, capabilityStore.saveCalls)
}
if len(epistemicsStore.experiences) != 1 || epistemicsStore.experiences[0].SkillRef != "" {
t.Fatalf("generic correction must remain evidence without a reminder skill reference: %#v", epistemicsStore.experiences)
}
}
+202 -22
View File
@@ -135,6 +135,12 @@ func main() {
return situationdomain.NewResolver(bindings).Resolve(runtime)
}
mux.Handle("/healthz", health.Handler{Runtime: "core-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1})
readinessClient := &http.Client{Timeout: 2 * time.Second}
mux.Handle("/readyz", health.ReadinessHandler{Runtime: "core-runtime", Probes: []health.Probe{
health.TCPProbe("postgres", envOr("POSTGRES_HOST", "postgres"), envOr("POSTGRES_PORT", "5432")),
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("CONTINUITY_RUNTIME_URL", "http://continuity-runtime:8081"), "/")+"/healthz"),
health.HTTPProbe(readinessClient, strings.TrimRight(envOr("COGNITIVE_RUNTIME_URL", "http://cognitive-worker:8083"), "/")+"/healthz"),
}})
mux.HandleFunc("POST /v1/creator/sessions", creatorSessionHandler(bootstrapToken, creatorUsername, creatorPassword, sessionKey, time.Now))
mux.HandleFunc("GET /v1/creator/sessions/current", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
@@ -293,7 +299,10 @@ func main() {
http.Error(w, "interpret chat message", http.StatusServiceUnavailable)
return
}
cognitiveAttempted := false
cognitiveAvailable := true
if shouldRequestBroaderInterpretation(semantic) {
cognitiveAttempted = true
projectNames := make([]string, 0, len(semantic.Projects))
for _, ref := range semantic.Projects {
if ref.ProjectDisplayName != "" {
@@ -303,7 +312,11 @@ func main() {
if response, interpretationErr := cognitiveClient.Interpret(r.Context(), buildGatewayInterpretationRequest(session, utterance, semantic, projectNames)); interpretationErr == nil && response.FinishReason == cognitive.FinishCompleted {
if candidate, candidateErr := interactionCandidateFromGateway(utterance.ID, response.Candidate, now); candidateErr == nil {
semantic = interactiondomain.ApplyCognitiveCandidate(semantic, utterance.Content, candidate, previousFocus)
} else {
cognitiveAvailable = false
}
} else {
cognitiveAvailable = false
}
}
if err := interactionRepository.AppendUtterance(r.Context(), conversation, utterance, intent, relations); err != nil {
@@ -378,7 +391,8 @@ func main() {
if responseErr == nil && responseResult.Handled {
helpPath = work.HelpPath{}
}
if isCapabilityQuestion(utterance.Content) {
switch semantic.InformationRequestKind {
case interaction.InformationRequestCapabilityInventory:
kinds, err := capabilityRepository.AvailableCapabilityKinds(r.Context(), session.PrincipalID)
if err == nil && len(kinds) > 0 {
planningReply = "Сейчас у меня подключены реальные capabilities: " + humanCapabilityList(kinds) + ". Для рассуждений я использую отдельный Cognitive Gateway/Router, если локальный CANARY доступен. Возможности вроде отправки почты пока будут фиксироваться как capability gap, а не как выполненное действие."
@@ -386,11 +400,23 @@ func main() {
planningReply = "Я умею записывать диалог и намерения, работать с текущим контекстом, строить планы и обращаться к Cognitive Gateway при доступности локального CANARY. Реальные action-capabilities подключаются через registry; если capability нет, я фиксирую gap."
}
needsCognition = false
case interaction.InformationRequestCognitiveModelInventory:
candidates, inventoryErr := capabilityRepository.LoadCognitiveCandidates(r.Context(), capability.CapabilityID("creator-advisory"), session.PrincipalID)
planningReply = humanCognitiveModelInventory(candidates, inventoryErr)
needsCognition = false
case interaction.InformationRequestCognitiveChannelStatus, interaction.InformationRequestCognitiveRecovery:
health, healthErr := capabilityRepository.LoadHealth(r.Context(), capability.CapabilityID("cognitive-proposal"))
planningReply = humanCognitiveChannelReply(semantic.InformationRequestKind, semantic.Interpretation.SpeechAct, health, healthErr)
needsCognition = false
}
if helpPath.Reply != "" {
planningReply = helpPath.Reply
needsCognition = false
}
if planningReply == "" && needsCognition && cognitiveAttempted && !cognitiveAvailable {
planningReply = humanCognitiveDegradedReply(utterance.Content)
needsCognition = false
}
var capabilityStatus capability.CapabilityResolutionStatus
var capabilityKind capability.CapabilityKind
var capabilityGapID capability.CapabilityGapID
@@ -468,24 +494,28 @@ func main() {
}
}
response := interaction.ChatMessageResponse{
ConversationID: conversation.ID,
UtteranceID: utterance.ID,
OriginalIntentID: intent.ID,
SpeechAct: semantic.Interpretation.SpeechAct,
InterpretationState: utterance.InterpretationState,
NeedsCognition: needsCognition,
FallbackText: semantic.Reply,
ActiveProjectName: activeFocusName,
ProjectProvenance: activeFocusProvenance,
Clarification: semantic.Interpretation.Clarification,
WorkPlanned: workPlanned,
PlanningReply: planningReply,
CapabilityStatus: capabilityStatus,
CapabilityKind: capabilityKind,
CapabilityGapID: capabilityGapID,
DelegationID: string(delegationID),
DelegationState: string(delegationState),
Reminder: reminder,
ConversationID: conversation.ID,
UtteranceID: utterance.ID,
OriginalIntentID: intent.ID,
SpeechAct: semantic.Interpretation.SpeechAct,
InterpretationState: utterance.InterpretationState,
NeedsCognition: needsCognition,
FallbackText: semantic.Reply,
ActiveProjectName: activeFocusName,
ProjectProvenance: activeFocusProvenance,
Clarification: semantic.Interpretation.Clarification,
WorkPlanned: workPlanned,
PlanningReply: planningReply,
CapabilityStatus: capabilityStatus,
CapabilityKind: capabilityKind,
CapabilityHints: toCapabilityKinds(semantic.CapabilityHints),
RequestedOutcome: semantic.Interpretation.RequestedOutcome,
InformationRequestKind: semantic.InformationRequestKind,
InterpretationConfidenceMillis: semantic.Interpretation.ConfidenceMillis,
CapabilityGapID: capabilityGapID,
DelegationID: string(delegationID),
DelegationState: string(delegationState),
Reminder: reminder,
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
@@ -811,6 +841,7 @@ func main() {
return
}
if err := capabilityRepository.SaveCognitiveDiscovery(r.Context(), registration); err != nil {
log.Printf("cognitive discovery persistence failed: %v", err)
http.Error(w, "save cognitive discovery", http.StatusInternalServerError)
return
}
@@ -871,6 +902,45 @@ func main() {
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(promotion)
})
mux.HandleFunc("POST /v1/creator/cognitive-activate", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
return
}
var request capability.CognitiveActivationPromotionRequest
decoder := json.NewDecoder(io.LimitReader(r.Body, 16<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid cognitive activation", http.StatusBadRequest)
return
}
request.RequestedByPrincipalID = session.PrincipalID
request.CapabilityID = "creator-advisory"
candidates, err := capabilityRepository.LoadCognitiveCandidates(r.Context(), request.CapabilityID, session.PrincipalID)
if err != nil {
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
return
}
var current capability.ResolvedCognitiveBinding
for _, candidate := range candidates {
if candidate.Binding.ID == request.BindingID && candidate.Binding.Revision == request.ExpectedBindingRevision {
current = candidate
break
}
}
promotion, err := capabilitydomain.BuildActivationPromotion(request, current)
if err != nil {
http.Error(w, "cognitive activation precondition failed", http.StatusConflict)
return
}
if err = capabilityRepository.SaveCognitiveActivationPromotion(r.Context(), promotion); err != nil {
http.Error(w, "cognitive activation conflict", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(promotion)
})
mux.HandleFunc("POST /v1/cognitive-registry/performance-evidence", func(w http.ResponseWriter, r *http.Request) {
var value capability.PerformanceEvidence
decoder := json.NewDecoder(io.LimitReader(r.Body, 64<<10))
@@ -1089,9 +1159,119 @@ func envOr(name, fallback string) string {
return fallback
}
func isCapabilityQuestion(message string) bool {
lower := strings.ToLower(message)
return strings.Contains(lower, "что ты умеешь") || strings.Contains(lower, "что умеешь") || strings.Contains(lower, "какие возможности")
func humanCognitiveModelInventory(candidates []capability.ResolvedCognitiveBinding, inventoryErr error) string {
if inventoryErr != nil {
return "Я не смогла прочитать реестр подключённых моделей. Встроенный канал Домового остаётся доступен; настройки можно проверить повторно после восстановления реестра. Ключи и пароли не выводятся в чат."
}
if len(candidates) == 0 {
return "В реестре пока нет подтверждённых подключённых моделей. Встроенный локальный канал Домового доступен всегда; внешние модели добавляются через настройки подключения. Ключи и пароли в чат не выводятся."
}
lines := make([]string, 0, len(candidates)+1)
lines = append(lines, "Подключённые модели из реестра:")
for _, candidate := range candidates {
modelName := string(candidate.Model.ProviderModelID)
if modelName == "" {
modelName = "имя модели не подтверждено"
}
status := cognitiveInventoryStatus(candidate)
pricing := cognitiveInventoryPricing(candidate.Provider)
details := []string{status, "тип " + string(candidate.Provider.Kind), "политика " + string(candidate.Binding.DataPolicy)}
if pricing != "" {
details = append(details, pricing)
}
if candidate.Provider.AuthMode != "" && candidate.Provider.AuthMode != capability.AuthNone {
details = append(details, "доступ "+humanAuthMode(candidate.Provider.AuthMode))
}
lines = append(lines, "- "+modelName+" ("+strings.Join(details, "; ")+")")
}
lines = append(lines, "Секреты, ключи и пароли намеренно не показываются.")
return strings.Join(lines, "\n")
}
func cognitiveInventoryStatus(candidate capability.ResolvedCognitiveBinding) string {
if candidate.Provider.Health == capability.HealthAvailable && candidate.Model.Health == capability.HealthAvailable {
if candidate.Binding.Activation == capability.ActivationActive {
return "активна"
}
return "доступна, не активирована для автоматического выбора"
}
if candidate.Provider.Health == capability.HealthDegraded || candidate.Model.Health == capability.HealthDegraded {
return "работает с ограничениями"
}
return "недоступна или не подтверждена"
}
func cognitiveInventoryPricing(provider capability.ProviderInstance) string {
switch provider.BillingMode {
case capability.BillingFree:
return "бесплатная"
case capability.BillingPerInput:
return "платная: стоимость входа указана в реестре"
case capability.BillingPerOutput:
return "платная: стоимость выхода указана в реестре"
case capability.BillingPerRequest:
return "платная: стоимость запроса указана в реестре"
default:
return "стоимость не подтверждена"
}
}
func humanAuthMode(mode capability.AuthMode) string {
switch mode {
case capability.AuthKey:
return "API-ключ"
case capability.AuthBearer:
return "Bearer-токен"
case capability.AuthUsernamePassword:
return "логин и пароль"
case capability.AuthOAuth:
return "OAuth"
case capability.AuthSecretFile:
return "защищённый файл секрета"
default:
return "защищённый способ"
}
}
func humanCognitiveChannelReply(kind interaction.InformationRequestKind, act interaction.SpeechAct, health capability.Health, healthErr error) string {
recoveryRequested := kind == interaction.InformationRequestCognitiveRecovery || act == interaction.SpeechActRequestToInvestigate || act == interaction.SpeechActDirective
if healthErr != nil {
if recoveryRequested {
return "Я не вижу подтверждённого состояния локального канала. Домовой всё равно рядом: опишите, что изменилось, и я помогу подготовить аварийную настройку."
}
return "Я не вижу подтверждённого состояния локального канала, но Домовой работает. Можно продолжать разговор и проверить канал ещё раз."
}
var observation string
switch health.State {
case capability.HealthAvailable:
observation = "Основной локальный канал сейчас доступен."
case capability.HealthDegraded:
observation = "Основной локальный канал сейчас работает с ограничениями."
case capability.HealthUnavailable:
observation = "Основной локальный канал сейчас недоступен."
default:
observation = "Состояние основного локального канала пока не подтверждено."
}
if recoveryRequested {
if health.State == capability.HealthUnavailable {
return observation + " Домовой жив: начну с проверки встроенного маршрута, затем зафиксирую варианты аварийного восстановления без передачи секретов."
}
return observation + " Я не меняю модель прямо из разговора: сначала покажу наблюдаемое состояние и подготовлю следующий проверяемый шаг восстановления."
}
if health.State == capability.HealthUnavailable {
if recoveryRequested {
return observation + " Домовой не остановился: начну с проверки встроенного маршрута, затем зафиксирую доступные варианты восстановления без передачи секретов."
}
return observation + " Но Домовой не остановился: продолжаю принимать сообщения и помогу восстановить локальный канал."
}
return observation + " Это наблюдение о готовности capability, а не прямой доступ к модели."
}
func humanCognitiveDegradedReply(message string) string {
if strings.TrimSpace(message) == "" {
return "Основной канал мышления сейчас недоступен. Я рядом и продолжаю работать в локальном аварийном режиме."
}
return "Основной канал мышления сейчас недоступен. Домовой жив: сообщение принято, а я могу помочь разобраться с локальным каналом и настройкой восстановления."
}
func humanCapabilityList(kinds []capability.CapabilityKind) string {
+56
View File
@@ -9,6 +9,10 @@ import (
"strings"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/interaction"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
)
func TestBearerTokenRequiresBearerScheme(t *testing.T) {
@@ -23,6 +27,58 @@ func TestBearerTokenRequiresBearerScheme(t *testing.T) {
}
}
func TestCognitiveChannelQuestionUsesHumanSafeHealthReply(t *testing.T) {
reply := humanCognitiveChannelReply(interaction.InformationRequestCognitiveChannelStatus, interaction.SpeechActQuestion, capability.Health{State: capability.HealthAvailable, CapabilityID: "provider-secret-model"}, nil)
if !strings.Contains(reply, "доступен") || strings.Contains(reply, "provider-secret-model") {
t.Fatalf("unexpected safe reply: %q", reply)
}
}
func TestCognitiveConfigurationRequestDoesNotPretendToMutate(t *testing.T) {
reply := humanCognitiveChannelReply(interaction.InformationRequestCognitiveRecovery, interaction.SpeechActDirective, capability.Health{State: capability.HealthUnavailable}, nil)
if !strings.Contains(reply, "Домовой жив") || !strings.Contains(reply, "аварийн") {
t.Fatalf("unexpected configuration reply: %q", reply)
}
}
func TestCognitiveDegradedReplyKeepsDomovoyAlive(t *testing.T) {
reply := humanCognitiveDegradedReply("Помоги настроить локальную модель")
if !strings.Contains(reply, "Домовой жив") || !strings.Contains(reply, "принято") {
t.Fatalf("unexpected degraded reply: %q", reply)
}
}
func TestModelInventoryQuestionUsesReadOnlyFastPath(t *testing.T) {
for _, message := range []string{"какие модели доступны?", "напиши список настроек ИИ", "какие есть подключения ИИ"} {
result, err := interactiondomain.InterpretUtterance(interaction.HumanUtterance{Content: message}, nil, nil, time.Now())
if err != nil {
t.Fatalf("interpretation failed for %q: %v", message, err)
}
if result.InformationRequestKind != interaction.InformationRequestCognitiveModelInventory {
t.Fatalf("semantic inventory request %q was not recognized: kind=%q act=%q hints=%v", message, result.InformationRequestKind, result.Interpretation.SpeechAct, result.CapabilityHints)
}
}
ordinary, err := interactiondomain.InterpretUtterance(interaction.HumanUtterance{Content: "настрой локальную модель"}, nil, nil, time.Now())
if err != nil {
t.Fatal(err)
}
if ordinary.InformationRequestKind == interaction.InformationRequestCognitiveModelInventory {
t.Fatal("configuration mutation must not use read-only model inventory path")
}
}
func TestHumanCognitiveModelInventoryDoesNotExposeSecrets(t *testing.T) {
cost := int64(25)
text := humanCognitiveModelInventory([]capability.ResolvedCognitiveBinding{{
Binding: capability.CognitiveBinding{DataPolicy: capability.DataPolicyExternalMessageOnly, Activation: capability.ActivationActive},
Provider: capability.ProviderInstance{Kind: "openai-compatible-chat-v1", CredentialRef: "secret-ref", AuthMode: capability.AuthKey, BillingMode: capability.BillingPerRequest, RequestCostMicros: &cost, Health: capability.HealthAvailable},
Model: capability.ModelDescriptor{ProviderModelID: "example-model", Health: capability.HealthAvailable},
}}, nil)
if strings.Contains(text, "secret-ref") || strings.Contains(text, "25") || !strings.Contains(text, "example-model") || !strings.Contains(text, "API-ключ") {
t.Fatalf("unsafe or incomplete inventory: %q", text)
}
}
func TestCreatorSessionHandlerAcceptsConfiguredCredentials(t *testing.T) {
handler := creatorSessionHandler("recovery", "m", "password", []byte("01234567890123456789012345678901"), func() time.Time { return time.Unix(1000, 0) })
request := httptest.NewRequest(http.MethodPost, "/v1/creator/sessions", strings.NewReader(`{"username":"m","password":"password"}`))
+58 -2
View File
@@ -15,6 +15,8 @@ import (
)
func TestChatPageServedWithoutInternalIDs(t *testing.T) {
t.Setenv("ASSISTANT_BUILD_REVISION", "0123456789abcdef")
t.Setenv("ASSISTANT_BUILD_TIME", "2026-08-22T03:04:05Z")
handler, err := newHandler("http://core", "http://continuity", "http://commit", http.DefaultClient, "http://cognitive")
if err != nil {
t.Fatal(err)
@@ -25,11 +27,62 @@ func TestChatPageServedWithoutInternalIDs(t *testing.T) {
t.Fatalf("expected /chat status 200, got %d", recorder.Code)
}
body := recorder.Body.String()
for _, forbidden := range []string{"provider_id", "model_id", "binding_id", "workspace-id", "session-id"} {
for _, required := range []string{"cognitive-status", "aria-live=\"polite\"", "cognitive-console-link", "href=\"/admin\"", "Проверь канал", "Изменить настройку", "ai-data-error", "Не вставляйте пароли", "build-meta", "версия ", "обновлено ", "datetime=\""} {
if !strings.Contains(body, required) {
t.Fatalf("/chat is missing human status marker %q", required)
}
}
for _, forbidden := range []string{"provider_id", "model_id", "binding_id", "workspace-id", "session-id", "__ASSISTANT_BUILD_REVISION__", "__ASSISTANT_BUILD_TIME__", "__ASSISTANT_BUILD_DATETIME__"} {
if strings.Contains(body, forbidden) {
t.Fatalf("/chat leaks internal field %q", forbidden)
}
}
if !strings.Contains(body, "версия 0123456789ab") || !strings.Contains(body, "обновлено 22.08.2026 03:04 UTC") || !strings.Contains(body, "datetime=\"2026-08-22T03:04:05Z\"") {
t.Fatalf("/chat does not render build metadata: %q", body)
}
for _, path := range []string{"/", "/chat.html"} {
alternate := httptest.NewRecorder()
handler.ServeHTTP(alternate, httptest.NewRequest(http.MethodGet, path, nil))
if alternate.Code != http.StatusOK || strings.Contains(alternate.Body.String(), "__ASSISTANT_BUILD_REVISION__") {
t.Fatalf("%s did not render build metadata: status=%d", path, alternate.Code)
}
}
}
func TestChatScriptExplainsSessionAndRuntimeFailures(t *testing.T) {
body, err := staticFiles.ReadFile("static/assets/chat.js")
if err != nil {
t.Fatal(err)
}
script := string(body)
for _, required := range []string{"class ChatApiError", "Сессия устарела", "основной runtime сейчас недоступен", "Проверьте текст и повторите попытку"} {
if !strings.Contains(script, required) {
t.Fatalf("chat script is missing recovery message %q", required)
}
}
}
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: "Чат с Домовым", 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) {
@@ -47,7 +100,7 @@ func TestChatMessageUsesCoreAndLocalOnlyCognitiveGateway(t *testing.T) {
t.Fatalf("unexpected core chat request: %#v", request)
}
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, InterpretationState: interaction.InterpretationInterpreted, NeedsCognition: true, FallbackText: "fallback"})
_ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, InterpretationState: interaction.InterpretationInterpreted, NeedsCognition: true, FallbackText: "fallback", InformationRequestKind: interaction.InformationRequestCognitiveChannelStatus})
default:
t.Fatalf("unexpected core route %s %s", r.Method, r.URL.Path)
}
@@ -93,6 +146,9 @@ func TestChatMessageUsesCoreAndLocalOnlyCognitiveGateway(t *testing.T) {
if cognitiveRequest.ActorID != "actor-1" || cognitiveRequest.PrincipalID != "principal-1" || cognitiveRequest.ScopeID != "domovoy-global-scope" || cognitiveRequest.Message != "Кто ты?" {
t.Fatalf("unexpected trusted gateway request: %#v", cognitiveRequest)
}
if cognitiveRequest.Meaning.InformationRequestKind != interaction.InformationRequestCognitiveChannelStatus {
t.Fatalf("information intent was not preserved: %#v", cognitiveRequest.Meaning)
}
}
func TestChatMessageReturnsHumanCapabilityMetadata(t *testing.T) {
+102 -36
View File
@@ -1,6 +1,7 @@
package main
import (
"bytes"
"embed"
"encoding/json"
"fmt"
@@ -8,7 +9,9 @@ import (
"io/fs"
"net/http"
"net/url"
"os"
"strings"
"time"
"unicode/utf8"
"git.cin.su/m/Assistant/contracts/capability"
@@ -36,6 +39,12 @@ type gateway struct {
cognitive *url.URL
client *http.Client
static http.Handler
build buildMetadata
}
type buildMetadata struct {
revision string
updated time.Time
}
type routeTarget uint8
@@ -74,7 +83,20 @@ func newHandler(coreURL, continuityURL, commitURL string, client *http.Client, c
if client == nil {
client = http.DefaultClient
}
return securityHeaders(&gateway{core: core, continuity: continuity, commit: commit, cognitive: cognitiveRuntime, client: client, static: http.FileServer(http.FS(assets))}), nil
updated := time.Now().UTC()
if raw := strings.TrimSpace(os.Getenv("ASSISTANT_BUILD_TIME")); raw != "" {
if parsed, parseErr := time.Parse(time.RFC3339, raw); parseErr == nil {
updated = parsed.UTC()
}
}
revision := strings.TrimSpace(os.Getenv("ASSISTANT_BUILD_REVISION"))
if revision == "" {
revision = "development"
}
if len(revision) > 12 {
revision = revision[:12]
}
return securityHeaders(&gateway{core: core, continuity: continuity, commit: commit, cognitive: cognitiveRuntime, client: client, static: http.FileServer(http.FS(assets)), build: buildMetadata{revision: revision, updated: updated}}), nil
}
func parseRuntimeURL(rawURL, name string) (*url.URL, error) {
@@ -111,17 +133,31 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if r.URL.Path == "/" {
w.Header().Set("Cache-Control", "no-store")
if r.URL.Path == "/" || r.URL.Path == "/chat" || r.URL.Path == "/chat.html" {
g.serveEmbeddedHTML(w, "chat.html")
return
}
if r.URL.Path == "/chat" {
r = r.Clone(r.Context())
r.URL.Path = "/chat.html"
w.Header().Set("Cache-Control", "no-store")
if r.URL.Path == "/admin" {
g.serveEmbeddedHTML(w, "index.html")
return
}
g.static.ServeHTTP(w, r)
}
func (g *gateway) 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")
body = bytes.ReplaceAll(body, []byte("__ASSISTANT_BUILD_REVISION__"), []byte(g.build.revision))
body = bytes.ReplaceAll(body, []byte("__ASSISTANT_BUILD_TIME__"), []byte(g.build.updated.Format("02.01.2006 15:04 UTC")))
body = bytes.ReplaceAll(body, []byte("__ASSISTANT_BUILD_DATETIME__"), []byte(g.build.updated.Format(time.RFC3339)))
_, _ = w.Write(body)
}
type browserAdvisoryRequest struct {
Message string `json:"message"`
SessionID string `json:"session_id"`
@@ -143,20 +179,23 @@ type browserChatRequest struct {
}
type browserChatResponse struct {
ConversationID string `json:"conversation_id"`
UtteranceID string `json:"utterance_id"`
SpeechAct string `json:"speech_act"`
ReplyText string `json:"reply_text"`
Cognition string `json:"cognition"`
ActiveFocus string `json:"active_focus"`
ProjectProvenance string `json:"project_provenance"`
Clarification string `json:"clarification"`
WorkPlanned bool `json:"work_planned"`
CapabilityStatus string `json:"capability_status"`
CapabilityKind string `json:"capability_kind"`
DelegationID string `json:"delegation_id"`
DelegationState string `json:"delegation_state"`
ReminderScheduled bool `json:"reminder_scheduled"`
ConversationID string `json:"conversation_id"`
UtteranceID string `json:"utterance_id"`
SpeechAct string `json:"speech_act"`
ReplyText string `json:"reply_text"`
Cognition string `json:"cognition"`
ActiveFocus string `json:"active_focus"`
ProjectProvenance string `json:"project_provenance"`
Clarification string `json:"clarification"`
WorkPlanned bool `json:"work_planned"`
CapabilityStatus string `json:"capability_status"`
CapabilityKind string `json:"capability_kind"`
CapabilityHints []capability.CapabilityKind `json:"capability_hints,omitempty"`
RequestedOutcome string `json:"requested_outcome,omitempty"`
InterpretationConfidenceMillis int64 `json:"interpretation_confidence_millis,omitempty"`
DelegationID string `json:"delegation_id"`
DelegationState string `json:"delegation_state"`
ReminderScheduled bool `json:"reminder_scheduled"`
}
func (g *gateway) handleAdvisoryPolicy(w http.ResponseWriter, r *http.Request) {
@@ -339,7 +378,7 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
cognition = "unavailable"
requestID, idErr := ids.New()
if idErr == nil {
trusted := cognitive.GatewayAdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(requestID), ActorID: session.ActorID, PrincipalID: session.PrincipalID, ScopeID: "domovoy-global-scope", Message: browserRequest.Message, Context: []cognitive.ContextExcerpt{}, Locale: "ru-RU", MaxOutputBytes: 4096, DataPolicy: capability.DataPolicyLocalOnly, EgressAcknowledged: false}
trusted := cognitive.GatewayAdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(requestID), ActorID: session.ActorID, PrincipalID: session.PrincipalID, ScopeID: "domovoy-global-scope", Message: browserRequest.Message, Context: localChatContext(saved), Locale: "ru-RU", MaxOutputBytes: 4096, DataPolicy: capability.DataPolicyLocalOnly, EgressAcknowledged: false, Meaning: cognitive.TaskMeaning{SpeechAct: string(saved.SpeechAct), CapabilityKind: saved.CapabilityKind, CapabilityHints: saved.CapabilityHints, RequestedOutcome: saved.RequestedOutcome, InformationRequestKind: saved.InformationRequestKind, ConfidenceMillis: saved.InterpretationConfidenceMillis}}
payload, err = json.Marshal(trusted)
if err == nil {
var advisory cognitive.GatewayAdvisoryResponse
@@ -356,23 +395,50 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(browserChatResponse{
ConversationID: string(saved.ConversationID),
UtteranceID: string(saved.UtteranceID),
SpeechAct: string(saved.SpeechAct),
ReplyText: reply,
Cognition: cognition,
ActiveFocus: saved.ActiveProjectName,
ProjectProvenance: string(saved.ProjectProvenance),
Clarification: string(saved.Clarification),
WorkPlanned: saved.WorkPlanned,
CapabilityStatus: string(saved.CapabilityStatus),
CapabilityKind: string(saved.CapabilityKind),
DelegationID: saved.DelegationID,
DelegationState: saved.DelegationState,
ReminderScheduled: saved.Reminder != nil,
ConversationID: string(saved.ConversationID),
UtteranceID: string(saved.UtteranceID),
SpeechAct: string(saved.SpeechAct),
ReplyText: reply,
Cognition: cognition,
ActiveFocus: saved.ActiveProjectName,
ProjectProvenance: string(saved.ProjectProvenance),
Clarification: string(saved.Clarification),
WorkPlanned: saved.WorkPlanned,
CapabilityStatus: string(saved.CapabilityStatus),
CapabilityKind: string(saved.CapabilityKind),
CapabilityHints: saved.CapabilityHints,
RequestedOutcome: saved.RequestedOutcome,
InterpretationConfidenceMillis: saved.InterpretationConfidenceMillis,
DelegationID: saved.DelegationID,
DelegationState: saved.DelegationState,
ReminderScheduled: saved.Reminder != nil,
})
}
func localChatContext(saved interaction.ChatMessageResponse) []cognitive.ContextExcerpt {
if strings.TrimSpace(saved.ActiveProjectName) == "" {
return []cognitive.ContextExcerpt{}
}
bundle, err := cognitive.CompileContext(cognitive.ContextCompileInput{
DataPolicy: capability.DataPolicyLocalOnly,
MaxBytes: 2048,
Candidates: []cognitive.ContextCandidate{{
Excerpt: cognitive.ContextExcerpt{
ID: cognitive.ContextExcerptID("active-project-focus"),
SourceKind: "situation-active-project",
SourceID: strings.TrimSpace(saved.ActiveProjectName),
Sensitivity: cognitive.SensitivityPrivateAllowed,
Text: "Текущий проект: " + strings.TrimSpace(saved.ActiveProjectName),
},
Relevance: 100,
}},
})
if err != nil {
return []cognitive.ContextExcerpt{}
}
return bundle.Excerpts
}
func asksAttention(message string) bool {
lower := strings.ToLower(message)
return strings.Contains(lower, "требует моего внимания") || strings.Contains(lower, "требует внимания") || strings.Contains(lower, "что сейчас важно")
+18 -2
View File
@@ -10,8 +10,19 @@ import (
"testing"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/interaction"
)
func TestLocalChatContextUsesOnlyActiveProject(t *testing.T) {
context := localChatContext(interaction.ChatMessageResponse{ActiveProjectName: "Домовой"})
if len(context) != 1 || context[0].SourceKind != "situation-active-project" || context[0].Text != "Текущий проект: Домовой" {
t.Fatalf("unexpected local context: %#v", context)
}
if empty := localChatContext(interaction.ChatMessageResponse{}); len(empty) != 0 {
t.Fatalf("expected empty context without active project: %#v", empty)
}
}
const legacyExternalMode = "legacy-external-test-mode"
func newHandlerWithAdvisoryMode(coreURL, continuityURL, commitURL string, client *http.Client, mode string, cognitiveURLs ...string) (http.Handler, error) {
@@ -229,7 +240,7 @@ 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(), "Домовой") ||
@@ -237,8 +248,13 @@ func TestConsoleAndSecurityHeaders(t *testing.T) {
!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=\"page\"") ||
!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(), "Спросить Домового") ||
+60 -11
View File
@@ -29,26 +29,60 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
.login-form button, .composer button { border: 0; color: #11150f; background: var(--signal); font-weight: 700; }
.login-form button { width: 100%; height: 50px; margin-top: 18px; }
#login-error { min-height: 20px; color: var(--bad); font-size: 13px; }
.chat-view { min-height: 100vh; display: grid; grid-template-rows: 64px minmax(0, 1fr); }
.chat-header { display: flex; align-items: center; gap: 18px; padding: 0 22px; border-bottom: 1px solid var(--line); background: rgba(16, 23, 20, .96); }
.chat-view { height: 100vh; min-height: 0; display: grid; grid-template-rows: 64px minmax(0, 1fr); overflow: hidden; }
.chat-header { min-width: 0; display: flex; align-items: center; gap: 14px; padding: 0 22px; border-bottom: 1px solid var(--line); background: rgba(16, 23, 20, .98); }
.chat-header a { color: var(--ink); text-decoration: none; font: 400 24px/1 var(--serif); }
.chat-header span { color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
#active-focus { margin-left: auto; color: var(--good); }
#active-focus { color: var(--good); }
.cognitive-status { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap; }
.chat-header .cognitive-status { width: auto; height: auto; padding: 0; border: 0; background: transparent; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
.chat-header .cognitive-status { max-width: min(30vw, 320px); margin-left: auto; overflow: hidden; text-overflow: ellipsis; }
.cognitive-status::before { content: ""; width: 7px; height: 7px; flex: 0 0 7px; border-radius: 50%; background: var(--signal); }
.cognitive-status[data-state="available"] { color: var(--good); }
.cognitive-status[data-state="available"]::before { background: var(--good); }
.cognitive-status[data-state="unavailable"] { color: var(--bad); }
.cognitive-status[data-state="unavailable"]::before { background: var(--bad); }
.cognitive-console-link { color: var(--signal); font: 10px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; white-space: nowrap; }
.build-meta { display: grid; gap: 3px; flex: 0 0 auto; color: var(--muted); font: 9px/1 var(--mono); letter-spacing: .08em; text-align: right; text-transform: uppercase; white-space: nowrap; }
.build-meta time { color: #68776d; font-size: 8px; }
.chat-header button { width: 38px; height: 38px; border: 1px solid var(--line); background: transparent; }
.human-nav { display: flex; align-items: center; gap: 8px; }
.human-nav { display: grid; gap: 8px; }
.nav-tab, .operator-link { height: 34px; padding: 0 12px; display: inline-flex; align-items: center; border: 1px solid var(--line); color: var(--muted); background: #0b110e; text-decoration: none; font: 11px/1 var(--mono); text-transform: uppercase; letter-spacing: .08em; }
.chat-left .nav-tab { height: auto; min-height: 58px; padding: 10px 12px; display: grid; grid-template-columns: 28px 1fr; gap: 2px 8px; align-items: center; text-align: left; }
.chat-left .nav-tab b { grid-row: span 2; color: var(--signal); font: 11px/1 var(--mono); }
.chat-left .nav-tab span { color: var(--ink); font: 600 13px/1 var(--sans); text-transform: none; letter-spacing: 0; }
.chat-left .nav-tab small { color: var(--muted); font: 10px/1 var(--mono); text-transform: none; letter-spacing: 0; }
.nav-tab.active { color: #11150f; border-color: var(--signal); background: var(--signal); }
.operator-link { color: var(--good); }
.surface-panel { min-height: 0; position: relative; }
.active-panel { display: grid; min-height: 0; }
.messages { width: min(100%, 920px); margin: 0 auto; padding: 28px 18px 120px; display: flex; flex-direction: column; gap: 14px; overflow: auto; }
.chat-header > .operator-link { flex: 0 0 auto; }
.chat-layout { min-height: 0; height: 100%; display: grid; grid-template-columns: minmax(220px, 280px) minmax(0, 1fr) minmax(250px, 320px); }
.chat-side { min-width: 0; min-height: 0; padding: 28px 20px; overflow-y: auto; background: #0e1512; }
.chat-left { border-right: 1px solid var(--line); }
.chat-right { border-left: 1px solid var(--line); }
.side-intro h1, .side-intro h2 { margin: 12px 0; font: 400 clamp(28px, 2.5vw, 40px)/.95 var(--serif); letter-spacing: -.03em; }
.side-intro p:not(.side-index) { color: var(--muted); line-height: 1.5; }
.side-index, .side-focus span, .side-note span, .ai-info-card span { color: var(--signal); font: 10px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
.side-focus, .side-note, .ai-info-card { margin-top: 28px; padding: 14px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.side-focus strong, .side-note strong, .ai-info-card strong { display: block; margin-top: 9px; color: var(--ink); font: 600 14px/1.3 var(--sans); }
.side-note small, .ai-info-card p, .ai-data-form small { display: block; margin-top: 9px; color: var(--muted); line-height: 1.45; }
.chat-main { min-width: 0; min-height: 0; overflow: hidden; display: grid; }
.surface-panel { min-height: 0; position: relative; overflow-y: auto; }
.active-panel { display: grid; grid-template-rows: minmax(0, 1fr) auto auto; min-height: 0; }
.messages { width: min(100%, 920px); min-height: 0; margin: 0 auto; padding: 28px 18px; display: flex; flex-direction: column; gap: 14px; overflow-y: auto; }
.message { max-width: min(720px, 92%); padding: 14px 16px; border: 1px solid var(--line); border-radius: var(--radius); line-height: 1.45; white-space: pre-wrap; }
.message.user { align-self: flex-end; background: #151e1a; }
.message.assistant { align-self: flex-start; background: #0f1613; }
.message small { display: block; margin-top: 8px; color: var(--muted); font: 10px/1 var(--mono); text-transform: uppercase; }
.composer { position: sticky; bottom: 0; width: min(100%, 920px); margin: 0 auto; padding: 16px 18px 20px; display: grid; grid-template-columns: minmax(0, 1fr) 130px; gap: 12px; background: linear-gradient(transparent, var(--base) 20%); }
.composer { width: min(100%, 920px); margin: 0 auto; padding: 16px 18px 10px; display: grid; grid-template-columns: minmax(0, 1fr) 130px; gap: 12px; background: linear-gradient(transparent, var(--base) 20%); }
.composer button { height: 48px; align-self: end; }
.composer button:disabled { cursor: wait; filter: saturate(.3); }
.conversation-starters { width: min(100%, 920px); margin: -4px auto 0; padding: 0 18px 18px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; color: var(--muted); font: 10px/1 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
.conversation-starters button { min-height: 32px; padding: 0 10px; border: 1px solid var(--line); color: var(--ink); background: #101814; font: 10px/1 var(--mono); }
.conversation-starters button:hover, .conversation-starters button:focus-visible { border-color: var(--signal); color: var(--signal); outline: none; }
.ai-data-form { margin-top: 24px; }
.ai-data-form textarea { min-height: 150px; resize: vertical; }
.ai-data-form button { width: 100%; min-height: 42px; margin-top: 12px; border: 0; color: #11150f; background: var(--signal); font-weight: 700; }
.ai-data-error { margin: 10px 0 0; color: var(--bad); font-size: 12px; line-height: 1.4; }
.side-console-link { display: flex; justify-content: space-between; margin-top: 26px; padding-top: 16px; border-top: 1px solid var(--line); color: var(--signal) !important; font: 11px/1.3 var(--mono) !important; text-transform: uppercase; letter-spacing: .06em; }
.attention-surface, .history-surface { width: min(100%, 920px); margin: 0 auto; padding: 34px 18px; align-content: start; gap: 18px; }
.surface-heading { display: flex; align-items: end; justify-content: space-between; gap: 16px; border-bottom: 1px solid var(--line); padding-bottom: 16px; }
.surface-heading p { margin: 0; font: 400 36px/.95 var(--serif); }
@@ -67,10 +101,25 @@ textarea { resize: vertical; min-height: 84px; max-height: 240px; padding: 14px;
.empty-state { margin: 24px 0 0; color: var(--muted); }
.empty-state.error { color: var(--bad); }
@media (max-width: 640px) {
.chat-view { height: auto; min-height: 100vh; overflow: visible; display: block; }
.composer { grid-template-columns: 1fr; }
.composer button { width: 100%; }
.conversation-starters { align-items: stretch; }
.conversation-starters span { width: 100%; }
.chat-header { gap: 10px; padding: 0 12px; }
.chat-header span { display: none; }
.human-nav { overflow-x: auto; }
.chat-header .cognitive-status { display: inline-flex; max-width: 150px; overflow: hidden; text-overflow: ellipsis; }
.build-meta { font-size: 8px; letter-spacing: .04em; }
.build-meta time { font-size: 7px; }
.chat-layout { height: auto; display: block; }
.chat-side, .chat-main { min-height: auto; max-height: none; overflow: visible; }
.chat-left, .chat-right { border: 0; border-bottom: 1px solid var(--line); }
.chat-main { height: 70vh; min-height: 520px; }
.chat-left { padding-bottom: 20px; }
.chat-right { padding-top: 20px; }
.operator-link, .nav-tab { padding: 0 9px; }
}
@media (min-width: 641px) and (max-width: 1100px) {
.chat-layout { grid-template-columns: 210px minmax(0, 1fr) 245px; }
.chat-side { padding-inline: 14px; }
.side-intro h1, .side-intro h2 { font-size: 30px; }
}
+118 -10
View File
@@ -8,14 +8,84 @@ const messageInput = document.querySelector("#message");
const sendButton = document.querySelector("#send");
const logoutButton = document.querySelector("#logout");
const activeFocus = document.querySelector("#active-focus");
const cognitiveStatus = document.querySelector("#cognitive-status");
const cognitiveConsoleLink = document.querySelector("#cognitive-console-link");
const todayList = document.querySelector("#today-list");
const refreshToday = document.querySelector("#refresh-today");
const historyList = document.querySelector("#history-list");
const navTabs = Array.from(document.querySelectorAll(".nav-tab"));
const conversationStarters = Array.from(document.querySelectorAll("[data-starter]"));
const aiDataForm = document.querySelector("#ai-data-form");
const aiDataInput = document.querySelector("#ai-data");
const aiDataError = document.querySelector("#ai-data-error");
let token = sessionStorage.getItem("domovoy.chat.token") || "";
let conversationID = sessionStorage.getItem("domovoy.chat.conversation") || "";
let localHistory = [];
let cognitiveStatusTimer = null;
class ChatApiError extends Error {
constructor(status, detail = "") {
super(detail || `chat api returned ${status}`);
this.name = "ChatApiError";
this.status = status;
}
}
function containsSecretShapedText(value) {
return /(-----BEGIN [A-Z ]+PRIVATE KEY-----|\b(?:password|passwd|пароль|token|токен|api[_ -]?key|секрет|ключ)\s*[:=]|\bsk-[a-zA-Z0-9_-]{12,})/i.test(value);
}
function showAIDataError(message = "") {
if (!aiDataError) return;
aiDataError.textContent = message;
aiDataError.hidden = !message;
}
function renderCognitiveStatus(state) {
if (!cognitiveStatus) return;
const normalized = String(state || "UNKNOWN").toUpperCase();
let displayState = "degraded";
if (normalized === "AVAILABLE") displayState = "available";
if (normalized === "UNAVAILABLE") displayState = "unavailable";
const labels = {
available: "канал мышления доступен",
degraded: "канал мышления работает с ограничениями",
unavailable: "Домовой рядом · основной канал недоступен",
};
cognitiveStatus.dataset.state = displayState;
cognitiveStatus.textContent = labels[displayState];
cognitiveStatus.title = displayState === "available" ? "Канал доступен. Нажмите, чтобы проверить ещё раз" : "Проверить канал ещё раз";
if (cognitiveConsoleLink) cognitiveConsoleLink.hidden = displayState === "available";
}
async function loadCognitiveStatus() {
if (!cognitiveStatus || !token) return;
cognitiveStatus.dataset.state = "checking";
cognitiveStatus.textContent = "проверяю канал мышления";
try {
const result = await api("/api/capabilities/cognitive-proposal/health", {method: "GET"});
renderCognitiveStatus(result.State || result.state || "UNKNOWN");
} catch (error) {
if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) {
expireSession("Сессия устарела. Войдите снова, чтобы продолжить разговор.");
return;
}
renderCognitiveStatus("UNAVAILABLE");
}
}
function startCognitiveStatusPolling() {
if (cognitiveStatusTimer) return;
loadCognitiveStatus();
cognitiveStatusTimer = window.setInterval(loadCognitiveStatus, 30000);
}
function stopCognitiveStatusPolling() {
if (!cognitiveStatusTimer) return;
window.clearInterval(cognitiveStatusTimer);
cognitiveStatusTimer = null;
}
function showChat() {
loginView.hidden = true;
@@ -23,10 +93,12 @@ function showChat() {
if (!messages.children.length) {
appendMessage("assistant", "Я рядом. Напишите обычное сообщение, без внутренних идентификаторов.");
}
startCognitiveStatusPolling();
messageInput.focus();
}
function showLogin() {
stopCognitiveStatusPolling();
chatView.hidden = true;
loginView.hidden = false;
}
@@ -56,10 +128,21 @@ async function api(path, options = {}) {
const headers = {"Content-Type": "application/json", ...(options.headers || {})};
if (token) headers.Authorization = `Bearer ${token}`;
const response = await fetch(path, {...options, headers});
if (!response.ok) throw new Error(await response.text());
if (!response.ok) throw new ChatApiError(response.status, await response.text());
return response.json();
}
function expireSession(message = "") {
token = "";
conversationID = "";
sessionStorage.removeItem("domovoy.chat.token");
sessionStorage.removeItem("domovoy.chat.conversation");
messages.replaceChildren();
updateActiveFocus("");
if (message) loginError.textContent = message;
showLogin();
}
function humanMeta(result) {
if (result.reminder_scheduled) return "Напоминание сохранено. Я вернусь к нему сам.";
if (result.delegation_id && result.delegation_state) return `Передал в coding-agent: ${humanDelegationState(result.delegation_state)}.`;
@@ -189,8 +272,14 @@ async function sendChatMessage(text, options = {}) {
await loadToday();
activateTab("today-panel");
}
} catch {
appendMessage("assistant", "Чат сейчас недоступен. Сообщение не принято runtime.");
} catch (error) {
if (error instanceof ChatApiError && (error.status === 401 || error.status === 403)) {
expireSession("Сессия устарела. Войдите снова, чтобы продолжить разговор.");
} else if (error instanceof ChatApiError && error.status >= 500) {
appendMessage("assistant", "Домовой не смог завершить обработку: основной runtime сейчас недоступен. Встроенный канал проверяется; повторите сообщение через несколько секунд.");
} else {
appendMessage("assistant", "Домовой не принял сообщение. Проверьте текст и повторите попытку.");
}
} finally {
sendButton.disabled = false;
messageInput.focus();
@@ -235,16 +324,35 @@ for (const tab of navTabs) {
tab.addEventListener("click", () => activateTab(tab.dataset.tab));
}
for (const starter of conversationStarters) {
starter.addEventListener("click", async () => {
const text = starter.dataset.starter || "";
if (!text || sendButton.disabled) return;
messageInput.value = "";
await sendChatMessage(text);
});
}
if (refreshToday) refreshToday.addEventListener("click", loadToday);
if (cognitiveStatus) cognitiveStatus.addEventListener("click", loadCognitiveStatus);
if (aiDataForm) {
aiDataForm.addEventListener("submit", async (event) => {
event.preventDefault();
const details = aiDataInput.value.trim();
if (!details || sendButton.disabled) return;
if (containsSecretShapedText(details)) {
showAIDataError("Не отправляю секреты. Удалите пароль, токен, API-ключ или приватный ключ и оставьте только описание модели.");
return;
}
showAIDataError();
aiDataInput.value = "";
await sendChatMessage(`Передаю сведения о локальном ИИ для текущего разговора:\n${details}`);
});
}
if (aiDataInput) aiDataInput.addEventListener("input", () => showAIDataError());
logoutButton.addEventListener("click", () => {
token = "";
conversationID = "";
sessionStorage.removeItem("domovoy.chat.token");
sessionStorage.removeItem("domovoy.chat.conversation");
messages.replaceChildren();
updateActiveFocus("");
showLogin();
expireSession();
});
if (token) {
+5 -2
View File
@@ -65,6 +65,7 @@ button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-
.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; }
.chat-return-link { color: var(--signal); text-decoration: none; font: 10px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; white-space: nowrap; }
.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; }
@@ -114,7 +115,7 @@ button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-
.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="page"] { border-left-color: var(--signal); color: var(--ink); background: rgba(201,168,95,.07); outline: 0; }
.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); }
@@ -123,6 +124,7 @@ button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-
.session-stamp strong { overflow: hidden; color: var(--muted); font: 10px/1.4 var(--mono); text-overflow: ellipsis; }
.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(34px, 4vw, 50px)/1 var(--serif); letter-spacing: -.035em; }
@@ -370,6 +372,7 @@ html[data-reduced-motion="true"] *, html[data-reduced-motion="true"] *::before,
.console { display: block; }
.topbar { height: 66px; padding: 0 14px; }
.wordmark small, .connection, .text-button { display: none; }
.chat-return-link { display: inline-block; }
.topbar-nav { display: none; }
.context-rail { padding: 24px 18px; border-right: 0; border-bottom: 1px solid var(--line); }
.rail-copy { max-width: 48ch; }
@@ -378,7 +381,7 @@ html[data-reduced-motion="true"] *, html[data-reduced-motion="true"] *::before,
.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="page"] { border-left-color: transparent; border-bottom-color: var(--signal); }
.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; }
+16 -24
View File
@@ -20,37 +20,29 @@ let lastAuthorizedActionID = "";
let advisoryPolicy = {Mode: "deterministic", ExternalEgress: false, AcknowledgementRequired: false};
let uiSettings = {density: "comfortable", refresh: "off", motion: false};
const sectionNavLinks = Array.from(document.querySelectorAll(".section-nav-link"));
const cabinetSections = ["overview", "diagnostics", "operations", "settings"]
.map((id) => byId(id))
.filter(Boolean);
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 setActiveSection(id) {
sectionNavLinks.forEach((link) => {
const active = link.getAttribute("href") === `#${id}`;
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", "page");
if (active) link.setAttribute("aria-current", "location");
else link.removeAttribute("aria-current");
});
if (updateHistory && window.location.hash !== `#${page}`) window.history.pushState({}, "", `#${page}`);
}
if ("IntersectionObserver" in window) {
const sectionObserver = new IntersectionObserver((entries) => {
const visible = entries.filter((entry) => entry.isIntersecting)
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
if (visible) setActiveSection(visible.target.id);
}, {rootMargin: "-18% 0px -64% 0px", threshold: [0, .25, .6, 1]});
cabinetSections.forEach((section) => sectionObserver.observe(section));
}
sectionNavLinks.forEach((link) => link.addEventListener("click", () => {
const id = link.getAttribute("href").slice(1);
setActiveSection(id);
pageNavLinks.forEach((link) => link.addEventListener("click", (event) => {
event.preventDefault();
setActivePage(link.getAttribute("href").slice(1), true);
}));
window.addEventListener("hashchange", () => {
const id = window.location.hash.slice(1);
if (cabinetSections.some((section) => section.id === id)) setActiveSection(id);
});
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);
+62 -30
View File
@@ -25,38 +25,70 @@
<section id="chat-view" class="chat-view" hidden>
<header class="chat-header">
<a href="/chat" 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>
</nav>
<span id="active-focus">фокус: не выбран</span>
<span id="session-state">сессия активна</span>
<a href="/" aria-label="Домовой">Домовой</a>
<button id="cognitive-status" class="cognitive-status" data-state="checking" type="button" role="status" aria-live="polite" title="Проверить канал ещё раз">проверяю канал мышления</button>
<a id="cognitive-console-link" class="cognitive-console-link" href="/admin" hidden>настроить в консоли</a>
<div class="build-meta" aria-label="Версия и время обновления">
<span>версия __ASSISTANT_BUILD_REVISION__</span>
<time datetime="__ASSISTANT_BUILD_DATETIME__">обновлено __ASSISTANT_BUILD_TIME__</time>
</div>
<a href="/admin" class="operator-link">Консоль</a>
<button id="logout" type="button" aria-label="Выйти">↗</button>
</header>
<section id="chat-panel" class="surface-panel active-panel">
<div id="messages" class="messages" aria-live="polite"></div>
<form id="chat-form" class="composer">
<textarea id="message" rows="3" maxlength="16384" placeholder="Напишите Домовому обычное сообщение..." required></textarea>
<button id="send" type="submit">Отправить</button>
</form>
</section>
<section id="today-panel" class="surface-panel attention-surface" hidden>
<div class="surface-heading">
<p>Сегодня / внимание</p>
<button id="refresh-today" type="button">Обновить</button>
</div>
<div id="today-list" class="attention-list"></div>
</section>
<section id="history-panel" class="surface-panel history-surface" hidden>
<div class="surface-heading">
<p>История</p>
<span>Последние сообщения текущего окна</span>
</div>
<div id="history-list" class="history-list"></div>
</section>
<div class="chat-layout">
<aside class="chat-side chat-left" aria-label="Навигация и текущий фокус">
<div class="side-intro">
<p class="side-index">01 / РАЗГОВОР</p>
<h1>Домовой рядом</h1>
<p>Пишите обычными словами. Настройки и сведения можно передать прямо в текущий разговор.</p>
</div>
<div class="side-focus">
<span>ТЕКУЩИЙ ФОКУС</span>
<strong id="active-focus">не выбран</strong>
</div>
<nav class="human-nav" aria-label="Разделы Домового">
<button class="nav-tab active" data-tab="chat-panel" type="button"><b>01</b><span>Чат</span><small>живой разговор</small></button>
<button class="nav-tab" data-tab="today-panel" type="button"><b>02</b><span>Сегодня</span><small>что требует внимания</small></button>
<button class="nav-tab" data-tab="history-panel" type="button"><b>03</b><span>История</span><small>это окно разговора</small></button>
</nav>
<div class="side-note"><span>СЕССИЯ</span><strong id="session-state">активна</strong><small>Сведения сохраняются в текущем разговоре.</small></div>
</aside>
<main class="chat-main">
<section id="chat-panel" class="surface-panel active-panel">
<div id="messages" class="messages" aria-live="polite"></div>
<form id="chat-form" class="composer">
<textarea id="message" rows="3" maxlength="16384" placeholder="Напишите Домовому обычное сообщение..." required></textarea>
<button id="send" type="submit">Отправить</button>
</form>
<div class="conversation-starters" aria-label="Частые вопросы">
<span>Можно начать так:</span>
<button type="button" data-starter="Проверь локальный канал мышления">Проверь канал</button>
<button type="button" data-starter="Что сейчас доступно?">Что доступно?</button>
<button type="button" data-starter="Хочу изменить настройку локальной модели">Изменить настройку</button>
</div>
</section>
<section id="today-panel" class="surface-panel attention-surface" hidden>
<div class="surface-heading"><p>Сегодня / внимание</p><button id="refresh-today" type="button">Обновить</button></div>
<div id="today-list" class="attention-list"></div>
</section>
<section id="history-panel" class="surface-panel history-surface" hidden>
<div class="surface-heading"><p>История</p><span>Последние сообщения текущего окна</span></div>
<div id="history-list" class="history-list"></div>
</section>
</main>
<aside class="chat-side chat-right" aria-label="Состояние и сведения об ИИ">
<div class="side-intro"><p class="side-index">02 / ИНФОРМАЦИЯ</p><h2>Канал мышления</h2><p>Домовой видит подтверждённое состояние канала. Если основной ИИ пропал, он остаётся рядом и переходит в аварийный режим.</p></div>
<div class="ai-info-card"><span>ЧТО МОЖНО ПЕРЕДАТЬ</span><strong>Описание ИИ</strong><p>Название, назначение, ограничения или заметки о локальной модели. Не вставляйте пароли, токены и ключи.</p></div>
<form id="ai-data-form" class="ai-data-form">
<label for="ai-data">Данные для текущего разговора</label>
<textarea id="ai-data" rows="8" maxlength="12000" placeholder="Например: локальная модель нужна для коротких ответов на русском; контекст ограничен; внешняя сеть запрещена."></textarea>
<button type="submit">Передать Домовому</button>
<p id="ai-data-error" class="ai-data-error" role="alert" hidden></p>
<small>Это отправится как обычное сообщение в чат.</small>
</form>
<a class="side-console-link" href="/admin">Открыть консоль настройки <span>→</span></a>
</aside>
</div>
</section>
</main>
<script src="/assets/chat.js"></script>
+14 -11
View File
@@ -44,11 +44,13 @@
<span><strong>Домовой</strong><small>консоль создателя</small></span>
</a>
<div class="topbar-actions">
<a class="chat-return-link" href="/">← Чат с Домовым</a>
<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>
@@ -97,10 +99,11 @@
<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="page" href="#overview"><span>01</span><b>Состояние</b><small>фокус и сигналы</small></a>
<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="#settings"><span>04</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>
@@ -111,7 +114,7 @@
</aside>
<main class="workspace">
<section id="overview" class="workspace-head">
<section id="overview" class="workspace-head" data-page="overview">
<div>
<p class="eyebrow">операционная картина</p>
<h1>Состояние сейчас</h1>
@@ -119,14 +122,14 @@
<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 id="diagnostics" class="diagnostics-panel" aria-labelledby="diagnostics-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>
@@ -140,7 +143,7 @@
</div>
</section>
<section class="advisor-panel" aria-labelledby="advisor-title">
<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>
@@ -168,7 +171,7 @@
</div>
</section>
<section id="operations" 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>
@@ -178,7 +181,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">
@@ -191,7 +194,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">
@@ -215,7 +218,7 @@
</div>
</section>
<section id="settings" class="settings-panel" aria-labelledby="settings-title">
<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>
@@ -228,7 +231,7 @@
</form>
</section>
<section class="data-grid">
<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>
+1 -1
View File
@@ -166,7 +166,7 @@ func loadSelectedTask(root string, selection taskSelection) (TaskPacket, string,
func loadTaskPacket(root, taskPath string) (TaskPacket, error) {
var taskPacket TaskPacket
if err := decodeJSONFile(filepath.Join(root, filepath.FromSlash(taskPath)), &taskPacket, false); err != nil {
if err := decodeJSONFile(filepath.Join(root, filepath.FromSlash(taskPath)), &taskPacket, true); err != nil {
return TaskPacket{}, fmt.Errorf("task packet: %w", err)
}
return taskPacket, nil
+64
View File
@@ -0,0 +1,64 @@
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
var required = []string{
"task_id", "task_revision", "architecture_revision", "base_repo_revision", "risk_tier", "intent",
"requirement_ids", "allowed_change_scope", "forbidden_change_scope", "allowed_architecture_delta",
"contracts", "semantic_owner", "canonical_state", "state_transitions", "side_effects_and_privileges",
"persistence_and_migrations", "failure_retry_restart", "assumptions", "acceptance_scenarios",
"required_tests", "terminal_gap_states",
}
func main() {
activePath := filepath.FromSlash("dev/ACTIVE_TASK.json")
var active struct {
TaskFile string `json:"task_file"`
TaskID string `json:"task_id"`
TaskRevision int `json:"task_revision"`
}
if err := decode(activePath, &active); err != nil {
fail("active pointer: %v", err)
}
if active.TaskFile == "" || active.TaskID == "" || active.TaskRevision < 1 {
fail("active pointer is incomplete")
}
var raw map[string]json.RawMessage
if err := decode(filepath.FromSlash(active.TaskFile), &raw); err != nil {
fail("task packet: %v", err)
}
for _, key := range required {
if _, ok := raw[key]; !ok {
fail("task packet missing required field %q", key)
}
}
var packet struct {
TaskID string `json:"task_id"`
TaskRevision int `json:"task_revision"`
}
if err := decode(filepath.FromSlash(active.TaskFile), &packet); err != nil {
fail("task identity: %v", err)
}
if packet.TaskID != active.TaskID || packet.TaskRevision != active.TaskRevision {
fail("active pointer and packet identity differ")
}
fmt.Printf("independent governance verification: OK task=%s revision=%d\n", packet.TaskID, packet.TaskRevision)
}
func decode(path string, target any) error {
b, err := os.ReadFile(path)
if err != nil {
return err
}
return json.Unmarshal(b, target)
}
func fail(format string, args ...any) {
fmt.Fprintf(os.Stderr, "independent verifier: "+format+"\n", args...)
os.Exit(1)
}
+32 -11
View File
@@ -2,7 +2,6 @@ package main
import (
"encoding/json"
"errors"
"fmt"
"go/parser"
"go/token"
@@ -24,19 +23,41 @@ type Registry struct {
BoundaryClass string `json:"boundary_class"`
MayDependOn []string `json:"may_depend_on"`
Forbidden []string `json:"forbidden"`
Privileged bool `json:"privileged"`
} `json:"modules"`
ProtectedPaths []string `json:"protected_paths"`
}
type TaskPacket struct {
TaskID string `json:"task_id"`
TaskRevision int `json:"task_revision"`
BaseRepoRevision string `json:"base_repo_revision"`
RiskTier string `json:"risk_tier"`
Intent string `json:"intent"`
AllowedChangeScope struct {
Paths []string `json:"paths"`
TaskID string `json:"task_id"`
TaskRevision int `json:"task_revision"`
ArchitectureRevision string `json:"architecture_revision"`
BaseRepoRevision string `json:"base_repo_revision"`
RiskTier string `json:"risk_tier"`
Intent string `json:"intent"`
RequirementIDs []string `json:"requirement_ids"`
AllowedChangeScope struct {
Modules []string `json:"modules"`
Paths []string `json:"paths"`
} `json:"allowed_change_scope"`
ForbiddenChangeScope struct {
Paths []string `json:"paths"`
} `json:"forbidden_change_scope"`
AllowedArchitectureDelta map[string]int `json:"allowed_architecture_delta"`
Contracts struct {
Consume []string `json:"consume"`
MayChange []string `json:"may_change"`
} `json:"contracts"`
SemanticOwner string `json:"semantic_owner"`
CanonicalState []string `json:"canonical_state"`
StateTransitions []string `json:"state_transitions"`
SideEffectsAndPrivileges []string `json:"side_effects_and_privileges"`
PersistenceAndMigrations []string `json:"persistence_and_migrations"`
FailureRetryRestart []string `json:"failure_retry_restart"`
Assumptions []string `json:"assumptions"`
AcceptanceScenarios []string `json:"acceptance_scenarios"`
RequiredTests []string `json:"required_tests"`
TerminalGapStates []string `json:"terminal_gap_states"`
}
func main() {
@@ -207,12 +228,12 @@ func preflight(args []string) error {
if err != nil {
return err
}
taskPacket, _, _, err := loadSelectedTask(root, selection)
taskPacket, taskPath, _, err := loadSelectedTask(root, selection)
if err != nil {
return err
}
if taskPacket.TaskID == "" || taskPacket.Intent == "" || taskPacket.RiskTier == "" {
return errors.New("task packet missing task_id, intent, or risk_tier")
if err := validateTaskPacket(root, taskPacket, taskPath); err != nil {
return err
}
registry, err := loadRegistry()
if err != nil {
+215
View File
@@ -0,0 +1,215 @@
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
var requiredPacketKeys = []string{
"task_id", "task_revision", "architecture_revision", "base_repo_revision", "risk_tier", "intent",
"requirement_ids", "allowed_change_scope", "forbidden_change_scope", "allowed_architecture_delta",
"contracts", "semantic_owner", "canonical_state", "state_transitions", "side_effects_and_privileges",
"persistence_and_migrations", "failure_retry_restart", "assumptions", "acceptance_scenarios",
"required_tests", "terminal_gap_states",
}
var requiredDeltaKeys = []string{
"new_cross_module_dependencies", "new_canonical_owners", "new_privileges",
"new_third_party_dependencies", "new_database_owner_roles",
}
func validateTaskPacket(root string, task TaskPacket, taskPath string) error {
if task.TaskID == "" || task.TaskRevision < 1 || task.ArchitectureRevision == "" || task.BaseRepoRevision == "" || task.RiskTier == "" || task.Intent == "" {
return fmt.Errorf("task %s is incomplete: identity, revision, architecture_revision, base_repo_revision, risk_tier and intent are required", task.TaskID)
}
if taskPath == "" || !matchesAny(taskPath, mustCompile([]string{"dev/tasks/*.json"})) {
return fmt.Errorf("task %s must be selected from dev/tasks", task.TaskID)
}
if task.RiskTier < "A" || task.RiskTier > "E" {
return fmt.Errorf("task %s has invalid risk_tier %q", task.TaskID, task.RiskTier)
}
if len(task.RequirementIDs) == 0 || len(task.AcceptanceScenarios) == 0 || len(task.RequiredTests) == 0 || len(task.TerminalGapStates) == 0 {
return fmt.Errorf("task %s must declare requirements, acceptance_scenarios, required_tests and terminal_gap_states", task.TaskID)
}
if len(task.AllowedChangeScope.Paths) == 0 {
return fmt.Errorf("task %s must declare allowed_change_scope.paths", task.TaskID)
}
if err := validateScopeOverlap(task.AllowedChangeScope.Paths, task.ForbiddenChangeScope.Paths); err != nil {
return fmt.Errorf("task %s scope: %w", task.TaskID, err)
}
if err := validateDelta(task.AllowedArchitectureDelta); err != nil {
return fmt.Errorf("task %s architecture delta: %w", task.TaskID, err)
}
if err := validateRequiredTests(task.RequiredTests); err != nil {
return fmt.Errorf("task %s required_tests: %w", task.TaskID, err)
}
if err := validateArchitectureRevision(root, task.ArchitectureRevision); err != nil {
return fmt.Errorf("task %s architecture_revision: %w", task.TaskID, err)
}
if _, err := resolveBaseCommit(root, task.BaseRepoRevision); err != nil {
return fmt.Errorf("task %s base_repo_revision: %w", task.TaskID, err)
}
if err := validateObservedDelta(root, task); err != nil {
return fmt.Errorf("task %s observed architecture delta: %w", task.TaskID, err)
}
return nil
}
func validateRequiredTests(tests []string) error {
joined := strings.Join(tests, "\n")
for _, required := range []string{"go test", "go vet", "preflight", "check-diff", "git diff --check", "independent"} {
if !strings.Contains(joined, required) {
return fmt.Errorf("missing required governance evidence %q", required)
}
}
return nil
}
func validateObservedDelta(root string, task TaskPacket) error {
changed, err := changedPaths(root, task.BaseRepoRevision)
if err != nil {
return err
}
if err := validateRegistryDelta(root, task); err != nil {
return err
}
for _, changedPath := range changed {
if (strings.HasPrefix(changedPath, "security/") || strings.HasPrefix(changedPath, "deploy/")) && task.AllowedArchitectureDelta["new_privileges"] == 0 {
return fmt.Errorf("privileged surface changed with new_privileges=0: %s", changedPath)
}
if strings.HasPrefix(changedPath, "database/") && task.AllowedArchitectureDelta["new_database_owner_roles"] == 0 {
return fmt.Errorf("database surface changed with new_database_owner_roles=0: %s", changedPath)
}
if (changedPath == "go.mod" || changedPath == "go.sum") && task.AllowedArchitectureDelta["new_third_party_dependencies"] == 0 {
return fmt.Errorf("dependency manifest changed with new_third_party_dependencies=0: %s", changedPath)
}
if strings.HasPrefix(changedPath, "contracts/") && len(task.Contracts.MayChange) == 0 {
return fmt.Errorf("contract surface changed but contracts.may_change is empty: %s", changedPath)
}
}
return nil
}
func validateRegistryDelta(root string, task TaskPacket) error {
baseBytes, err := runGit(root, "show", task.BaseRepoRevision+":architecture/architecture-registry.json")
if err != nil {
return fmt.Errorf("read base architecture registry: %w", err)
}
currentBytes, err := os.ReadFile(filepath.Join(root, "architecture", "architecture-registry.json"))
if err != nil {
return err
}
var base, current Registry
if err := json.Unmarshal(baseBytes, &base); err != nil {
return fmt.Errorf("parse base architecture registry: %w", err)
}
if err := json.Unmarshal(currentBytes, &current); err != nil {
return fmt.Errorf("parse current architecture registry: %w", err)
}
if task.AllowedArchitectureDelta["new_canonical_owners"] == 0 {
for name := range current.Modules {
if _, ok := base.Modules[name]; !ok {
return fmt.Errorf("new canonical module owner %q with new_canonical_owners=0", name)
}
}
}
if task.AllowedArchitectureDelta["new_cross_module_dependencies"] == 0 {
for name, module := range current.Modules {
previous, ok := base.Modules[name]
if !ok {
continue
}
old := map[string]bool{}
for _, dep := range previous.MayDependOn {
old[dep] = true
}
for _, dep := range module.MayDependOn {
if !old[dep] {
return fmt.Errorf("new dependency %q in module %q with new_cross_module_dependencies=0", dep, name)
}
}
}
}
if task.AllowedArchitectureDelta["new_privileges"] == 0 {
for name, module := range current.Modules {
if previous, ok := base.Modules[name]; ok && module.Privileged && !previous.Privileged {
return fmt.Errorf("new privilege in module %q with new_privileges=0", name)
}
}
}
return nil
}
func validateDelta(delta map[string]int) error {
if len(delta) != len(requiredDeltaKeys) {
return fmt.Errorf("must contain exactly %s", strings.Join(requiredDeltaKeys, ", "))
}
for _, key := range requiredDeltaKeys {
value, ok := delta[key]
if !ok || value < 0 {
return fmt.Errorf("%s must be a non-negative integer", key)
}
}
return nil
}
func validateArchitectureRevision(root, revision string) error {
if strings.HasPrefix(revision, "ADR-") {
matches, err := filepath.Glob(filepath.Join(root, "architecture", "adrs", revision+"-*.md"))
if err != nil || len(matches) != 1 {
return fmt.Errorf("accepted ADR %q is missing or ambiguous", revision)
}
return nil
}
if strings.HasPrefix(revision, "ARCH-") {
matches, err := filepath.Glob(filepath.Join(root, "dev", "tasks", revision+"*.json"))
if err != nil || len(matches) == 0 {
return fmt.Errorf("architecture task %q is missing", revision)
}
return nil
}
return fmt.Errorf("must reference an accepted ADR-NNNN or architecture task")
}
func validateScopeOverlap(allowed, forbidden []string) error {
allowedPatterns, err := compileScopePatterns(allowed)
if err != nil {
return err
}
forbiddenPatterns, err := compileScopePatterns(forbidden)
if err != nil {
return fmt.Errorf("forbidden_change_scope: %w", err)
}
for _, a := range allowedPatterns {
for _, f := range forbiddenPatterns {
if scopePatternsOverlap(a, f) {
return fmt.Errorf("allowed path %q overlaps forbidden path %q", a.raw, f.raw)
}
}
}
return nil
}
func scopePatternsOverlap(a, b scopePattern) bool {
if a.subtree != "" && b.subtree != "" {
return a.subtree == b.subtree || strings.HasPrefix(a.subtree, b.subtree+"/") || strings.HasPrefix(b.subtree, a.subtree+"/")
}
if a.subtree != "" {
return b.raw == a.subtree || strings.HasPrefix(b.raw, a.subtree+"/")
}
if b.subtree != "" {
return a.raw == b.subtree || strings.HasPrefix(a.raw, b.subtree+"/")
}
return a.raw == b.raw
}
func mustCompile(values []string) []scopePattern {
patterns, err := compileScopePatterns(values)
if err != nil {
panic(err)
}
return patterns
}
+27
View File
@@ -0,0 +1,27 @@
package main
import (
"strings"
"testing"
)
func TestValidateScopeOverlapRejectsAllowedForbiddenConflict(t *testing.T) {
err := validateScopeOverlap([]string{"cmd/creator-web/server.go"}, []string{"cmd/creator-web/**"})
if err == nil || !strings.Contains(err.Error(), "overlaps forbidden") {
t.Fatalf("overlap error = %v", err)
}
}
func TestValidateRequiredTestsRejectsIncompleteEvidence(t *testing.T) {
err := validateRequiredTests([]string{"go test ./..."})
if err == nil || !strings.Contains(err.Error(), "missing required governance evidence") {
t.Fatalf("required test error = %v", err)
}
}
func TestValidateDeltaRejectsUnknownOrMissingFields(t *testing.T) {
err := validateDelta(map[string]int{"new_privileges": 0})
if err == nil || !strings.Contains(err.Error(), "exactly") {
t.Fatalf("delta error = %v", err)
}
}
+74
View File
@@ -0,0 +1,74 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"time"
"git.cin.su/m/Assistant/adapters/postgres"
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
)
func main() {
workerID := flag.String("worker-id", "reflection-worker-1", "stable worker identity")
lease := flag.Duration("lease-ttl", 5*time.Minute, "candidate claim lease")
pollInterval := flag.Duration("poll-interval", 15*time.Second, "database-owned idle wake-up interval")
once := flag.Bool("once", false, "run one cycle and exit")
flag.Parse()
ctx := context.Background()
password, err := resolveDatabasePassword("EPISTEMICS_POSTGRES_PASSWORD", "EPISTEMICS_POSTGRES_CREDENTIAL_REF")
if err != nil {
panic(err)
}
config := postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("EPISTEMICS_POSTGRES_USER"), Password: password}
repository, err := postgres.OpenEpistemics(ctx, config)
if err != nil {
panic(err)
}
defer repository.Close()
for {
now := time.Now().UnixMilli()
if _, err := repository.RecoverReflectionClaims(ctx, now); err != nil {
panic(err)
}
candidate, claimErr := repository.ClaimReflectionCandidate(ctx, *workerID, now, lease.Milliseconds())
if claimErr == nil {
// This worker only closes the durable evaluation loop. Any cognitive
// proposal remains a candidate and must be admitted by a separate governed flow.
if err := repository.ResolveReflectionCandidate(ctx, candidate.ID, *workerID, time.Now().UnixMilli()); err != nil {
panic(err)
}
fmt.Printf("resolved reflection candidate %s\n", candidate.ID)
} else if *once {
fmt.Println("no reflection candidate")
}
if *once {
return
}
if err := repository.WaitForReflectionCandidate(ctx, *pollInterval); err != nil {
panic(err)
}
}
}
func resolveDatabasePassword(name, refName string) (string, error) {
if _, ok := os.LookupEnv(name); ok {
return "", fmt.Errorf("%s direct environment secret is forbidden", name)
}
path, ref := os.Getenv(name+"_FILE"), os.Getenv(refName)
if path == "" || ref == "" {
return "", fmt.Errorf("%s_FILE and %s are required", name, refName)
}
root := os.Getenv("SECRET_BACKEND_ROOT")
if root == "" {
root = "/run/secrets"
}
resolver, err := platformsecrets.NewFileResolver(root)
if err != nil {
return "", err
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerEpistemicsWorker, Path: path})
}
+19
View File
@@ -2,4 +2,23 @@ package artifact
import "git.cin.su/m/Assistant/platform/ids"
import "errors"
type ArtifactID ids.ID
type AgentArtifactAttestation struct {
ArtifactID ArtifactID
ProducerRunID string
SourceRevision string
WorktreeID string
ContentDigest string
TestEvidenceRef string
CreatedAtUnixMillis int64
}
func (a AgentArtifactAttestation) Validate() error {
if a.ArtifactID == "" || a.ProducerRunID == "" || a.SourceRevision == "" || a.WorktreeID == "" || a.ContentDigest == "" || a.TestEvidenceRef == "" || a.CreatedAtUnixMillis < 1 {
return errors.New("agent artifact attestation is incomplete")
}
return nil
}
+68
View File
@@ -0,0 +1,68 @@
package capability
type AdmissionComplexity string
type AdmissionDecisionState string
type AdmissionReason string
const (
AdmissionSimple AdmissionComplexity = "SIMPLE"
AdmissionNormal AdmissionComplexity = "NORMAL"
AdmissionHeavy AdmissionComplexity = "HEAVY"
AdmissionAdmitted AdmissionDecisionState = "ADMITTED"
AdmissionRejected AdmissionDecisionState = "REJECTED"
AdmissionReasonNone AdmissionReason = "NONE"
AdmissionReasonConcurrency AdmissionReason = "CONCURRENCY_LIMIT"
AdmissionReasonAcceptedHour AdmissionReason = "ACCEPTED_PER_HOUR_LIMIT"
AdmissionReasonDeadline AdmissionReason = "DEADLINE"
AdmissionReasonInputBudget AdmissionReason = "INPUT_BUDGET"
AdmissionReasonOutputBudget AdmissionReason = "OUTPUT_BUDGET"
)
type AdmissionPolicy struct {
ID string
MaxConcurrent int
MaxAcceptedPerHour int
MaxInputBytes int
MaxOutputBytes int
}
type AdmissionRequest struct {
Complexity AdmissionComplexity
InputBytes int
MaxOutputBytes int
DeadlineMillis int64
}
type AdmissionCounters struct {
Concurrent int
AcceptedLastHour int
}
type AdmissionDecision struct {
State AdmissionDecisionState
Reason AdmissionReason
PolicyID string
}
func Admit(request AdmissionRequest, policy AdmissionPolicy, counters AdmissionCounters, nowMillis int64) AdmissionDecision {
decision := AdmissionDecision{State: AdmissionAdmitted, Reason: AdmissionReasonNone, PolicyID: policy.ID}
if policy.ID == "" || policy.MaxConcurrent < 1 || policy.MaxAcceptedPerHour < 1 || policy.MaxInputBytes < 1 || policy.MaxOutputBytes < 1 {
return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonInputBudget, PolicyID: policy.ID}
}
if counters.Concurrent >= policy.MaxConcurrent {
return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonConcurrency, PolicyID: policy.ID}
}
if counters.AcceptedLastHour >= policy.MaxAcceptedPerHour {
return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonAcceptedHour, PolicyID: policy.ID}
}
if request.InputBytes < 0 || request.InputBytes > policy.MaxInputBytes {
return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonInputBudget, PolicyID: policy.ID}
}
if request.MaxOutputBytes < 1 || request.MaxOutputBytes > policy.MaxOutputBytes {
return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonOutputBudget, PolicyID: policy.ID}
}
if request.DeadlineMillis > 0 && request.DeadlineMillis <= nowMillis {
return AdmissionDecision{State: AdmissionRejected, Reason: AdmissionReasonDeadline, PolicyID: policy.ID}
}
return decision
}
+71 -19
View File
@@ -23,6 +23,9 @@ type OperationalState string
type EvaluationResult string
type EvaluationID ids.ID
type PerformanceEvidenceID ids.ID
type BillingMode string
type AuthMode string
type SelectionObjective string
const (
QualificationDiscovered QualificationState = "DISCOVERED"
@@ -52,6 +55,24 @@ const (
EvaluationUnknown EvaluationResult = "UNKNOWN"
EvaluationSupported EvaluationResult = "SUPPORTED"
EvaluationUnsupported EvaluationResult = "UNSUPPORTED"
BillingUnknown BillingMode = "UNKNOWN"
BillingFree BillingMode = "FREE"
BillingPerInput BillingMode = "PER_INPUT_UNIT"
BillingPerOutput BillingMode = "PER_OUTPUT_UNIT"
BillingPerRequest BillingMode = "PER_REQUEST"
AuthNone AuthMode = "NONE"
AuthKey AuthMode = "API" + "_KEY"
AuthBearer AuthMode = "BEARER"
AuthUsernamePassword AuthMode = "USERNAME_PASSWORD"
AuthOAuth AuthMode = "OAUTH"
AuthSecretFile AuthMode = "SECRET_FILE"
SelectionBalanced SelectionObjective = "BALANCED"
SelectionQuality SelectionObjective = "QUALITY"
SelectionSpeed SelectionObjective = "SPEED"
SelectionEconomy SelectionObjective = "ECONOMY"
)
// ProviderInstance contains only opaque references to endpoint and credentials.
@@ -66,20 +87,26 @@ type ProviderInstance struct {
Qualification QualificationState
Health HealthState
ObservedAtUnixMillis int64
BillingMode BillingMode
AuthMode AuthMode
InputCostMicros *int64
OutputCostMicros *int64
RequestCostMicros *int64
}
type ModelDescriptor struct {
ID ModelDescriptorID
Revision int64
ProviderInstanceID ProviderInstanceID
ProviderModelID ProviderModelID
Fingerprint ModelFingerprint
MaxContextBytes int64
Modalities []ModelModality
Capabilities []ModelCapability
Qualification QualificationState
Health HealthState
ObservedAtUnixMillis int64
ID ModelDescriptorID
Revision int64
ProviderInstanceID ProviderInstanceID
ProviderModelID ProviderModelID
Fingerprint ModelFingerprint
MaxContextBytes int64
Modalities []ModelModality
Capabilities []ModelCapability
Qualification QualificationState
Health HealthState
ObservedAtUnixMillis int64
IntelligenceScoreMillis *int64
}
type ProviderHealthObservation struct {
@@ -125,6 +152,8 @@ type CognitiveRequirement struct {
InputBytes int64
MaxOutputBytes int64
ExcludedBindingIDs []CapabilityBindingID
Objective SelectionObjective
MaxCostMicros *int64
}
// ResolvedCognitiveBinding is a provider-neutral invocation snapshot selected
@@ -206,6 +235,23 @@ type CognitiveCanaryPromotion struct {
Evaluation EvaluatedCapability
}
type CognitiveActivationPromotionRequest struct {
PromotionID ids.ID
RequestedByPrincipalID identity.PrincipalID
CapabilityID CapabilityID
BindingID CapabilityBindingID
ExpectedBindingRevision int64
EvaluationID EvaluationID
ObservedAtUnixMillis int64
}
type CognitiveActivationPromotion struct {
PromotionID ids.ID
ExpectedBindingRevision int64
Binding CognitiveBinding
Evaluation EvaluatedCapability
}
// CognitiveDiscoverySnapshot is provider-neutral observed inventory submitted
// to the Capability owner. The owner assigns immutable revisions and always
// registers discovered bindings as DISCOVERED/INACTIVE.
@@ -222,18 +268,24 @@ type CognitiveDiscoverySnapshot struct {
Health HealthState
FailureClass CognitiveFailureType
ObservedAtUnixMillis int64
BillingMode BillingMode
AuthMode AuthMode
InputCostMicros *int64
OutputCostMicros *int64
RequestCostMicros *int64
Models []DiscoveredModelSnapshot
}
type DiscoveredModelSnapshot struct {
ProviderModelID ProviderModelID
Fingerprint ModelFingerprint
MaxContextBytes int64
Modalities []ModelModality
Capabilities []ModelCapability
Installed OperationalState
Loaded OperationalState
Invokable OperationalState
ProviderModelID ProviderModelID
Fingerprint ModelFingerprint
MaxContextBytes int64
Modalities []ModelModality
Capabilities []ModelCapability
Installed OperationalState
Loaded OperationalState
Invokable OperationalState
IntelligenceScoreMillis *int64
}
type CognitiveUsageObservation struct {
+25
View File
@@ -0,0 +1,25 @@
package capability
type FleetTier string
const (
FleetTierFast FleetTier = "FAST"
FleetTierMedium FleetTier = "MEDIUM"
FleetTierHeavy FleetTier = "HEAVY"
FleetTierLifeboat FleetTier = "LIFEBOAT"
)
type FleetMember struct {
ProviderInstanceID ProviderInstanceID
ModelDescriptorID ModelDescriptorID
Tier FleetTier
Ready bool
Reason string
}
type FleetReadiness struct {
ObservedAtUnixMillis int64
Members []FleetMember
ReadyTiers []FleetTier
FallbackTier FleetTier
}
+72
View File
@@ -0,0 +1,72 @@
package capability
import "errors"
type QualificationManifestID string
type QualificationDimension string
const (
DimensionDialogue QualificationDimension = "DIALOGUE"
DimensionInterpretation QualificationDimension = "INTERPRETATION"
DimensionCoding QualificationDimension = "CODING"
DimensionResearch QualificationDimension = "RESEARCH"
DimensionUncertainty QualificationDimension = "UNCERTAINTY"
DimensionContext QualificationDimension = "CONTEXT"
DimensionLatency QualificationDimension = "LATENCY"
DimensionResources QualificationDimension = "RESOURCES"
DimensionRestart QualificationDimension = "RESTART"
)
type QualificationMeasurement struct {
Dimension QualificationDimension
Result EvaluationResult
Artifact string
}
type QualificationManifest struct {
ID QualificationManifestID
ModelDescriptorID ModelDescriptorID
ModelRevision int64
ModelFingerprint ModelFingerprint
BenchmarkVersion string
HoldoutCorpusDigest string
LicenseEvidence string
DataPolicyEvidence string
Measurements []QualificationMeasurement
ObservedAtUnixMillis int64
}
func (m QualificationManifest) Validate() error {
if m.ID == "" || m.ModelDescriptorID == "" || m.ModelRevision < 1 || m.ModelFingerprint == "" || m.BenchmarkVersion == "" || m.HoldoutCorpusDigest == "" || m.LicenseEvidence == "" || m.DataPolicyEvidence == "" || m.ObservedAtUnixMillis < 1 {
return errors.New("qualification manifest identity and evidence are incomplete")
}
required := map[QualificationDimension]bool{DimensionDialogue: false, DimensionInterpretation: false, DimensionCoding: false, DimensionResearch: false, DimensionUncertainty: false, DimensionContext: false, DimensionLatency: false, DimensionResources: false, DimensionRestart: false}
for _, measurement := range m.Measurements {
if measurement.Dimension == "" || measurement.Artifact == "" || measurement.Result == EvaluationUnknown {
return errors.New("qualification measurement is incomplete")
}
if _, ok := required[measurement.Dimension]; ok {
required[measurement.Dimension] = true
}
}
for _, present := range required {
if !present {
return errors.New("qualification manifest lacks required measurement")
}
}
return nil
}
// QualificationState projects evidence into a qualification result. It does
// not activate a binding or mutate the model registry.
func (m QualificationManifest) QualificationState() QualificationState {
if m.Validate() != nil {
return QualificationRejected
}
for _, measurement := range m.Measurements {
if measurement.Result != EvaluationSupported {
return QualificationRejected
}
}
return QualificationValidated
}
+91
View File
@@ -0,0 +1,91 @@
package cognitive
import (
"errors"
"sort"
"strings"
"git.cin.su/m/Assistant/contracts/capability"
)
var ErrInvalidContextBudget = errors.New("context budget must be positive")
type ContextCandidate struct {
Excerpt ContextExcerpt
Relevance int
}
type ContextCompileInput struct {
Candidates []ContextCandidate
DataPolicy capability.DataPolicy
MaxBytes int
}
type ContextOmission struct {
ID ContextExcerptID
Reason string
}
type ContextBundle struct {
Excerpts []ContextExcerpt
Omissions []ContextOmission
Bytes int
}
// CompileContext deterministically selects permitted, relevant excerpts for a
// gateway request. It carries no provider identity and performs no I/O.
func CompileContext(input ContextCompileInput) (ContextBundle, error) {
if input.MaxBytes <= 0 {
return ContextBundle{}, ErrInvalidContextBudget
}
candidates := append([]ContextCandidate(nil), input.Candidates...)
sort.SliceStable(candidates, func(i, j int) bool {
if candidates[i].Relevance != candidates[j].Relevance {
return candidates[i].Relevance > candidates[j].Relevance
}
return string(candidates[i].Excerpt.ID) < string(candidates[j].Excerpt.ID)
})
bundle := ContextBundle{Excerpts: []ContextExcerpt{}, Omissions: []ContextOmission{}}
seen := make(map[ContextExcerptID]struct{}, len(candidates))
for _, candidate := range candidates {
excerpt := candidate.Excerpt
if _, ok := seen[excerpt.ID]; ok {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "duplicate"})
continue
}
seen[excerpt.ID] = struct{}{}
reason := permittedContextReason(excerpt, input.DataPolicy)
if reason != "" {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: reason})
continue
}
if strings.TrimSpace(excerpt.Text) == "" {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "empty"})
continue
}
bytes := len([]byte(excerpt.Text))
if bundle.Bytes+bytes > input.MaxBytes {
bundle.Omissions = append(bundle.Omissions, ContextOmission{ID: excerpt.ID, Reason: "budget"})
continue
}
bundle.Excerpts = append(bundle.Excerpts, excerpt)
bundle.Bytes += bytes
}
return bundle, nil
}
func permittedContextReason(excerpt ContextExcerpt, policy capability.DataPolicy) string {
if excerpt.Sensitivity == SensitivitySecret {
return "secret"
}
if excerpt.Sensitivity == SensitivityUnknown {
return "unknown_sensitivity"
}
if policy == capability.DataPolicyExternalMessageOnly {
return "policy_message_only"
}
if policy == capability.DataPolicyExternalMinimizedContext && excerpt.Sensitivity != SensitivityPublic {
return "policy_public_only"
}
return ""
}
+14
View File
@@ -4,6 +4,7 @@ import (
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
"git.cin.su/m/Assistant/platform/ids"
)
@@ -79,6 +80,19 @@ type GatewayAdvisoryRequest struct {
MaxOutputBytes int
DataPolicy capability.DataPolicy
EgressAcknowledged bool
Meaning TaskMeaning
}
// TaskMeaning is the trusted semantic summary produced by Core. It is not a
// browser-controlled provider selector; the Capability owner still resolves
// the actual binding.
type TaskMeaning struct {
SpeechAct string
CapabilityKind capability.CapabilityKind
CapabilityHints []capability.CapabilityKind
RequestedOutcome string
InformationRequestKind interaction.InformationRequestKind
ConfidenceMillis int64
}
type GatewayAdvisoryPolicyRequest struct {
+111
View File
@@ -0,0 +1,111 @@
package epistemics
import (
"errors"
"strings"
)
var ErrCitationEvidenceMismatch = errors.New("citation evidence does not match source chain")
type ResearchPlanID string
type SourceID string
type SourceSnapshotID string
type SourceExcerptID string
type CitationID string
type FreshnessState string
type CoverageState string
type ConflictState string
type EvidenceKind string
const (
FreshnessCurrent FreshnessState = "CURRENT"
FreshnessStale FreshnessState = "STALE"
FreshnessUnknown FreshnessState = "UNKNOWN"
CoverageComplete CoverageState = "COMPLETE"
CoveragePartial CoverageState = "PARTIAL"
CoverageUnknown CoverageState = "UNKNOWN"
ConflictNone ConflictState = "NONE"
ConflictPresent ConflictState = "PRESENT"
ConflictUnknown ConflictState = "UNKNOWN"
EvidenceObserved EvidenceKind = "OBSERVED"
EvidenceAssumed EvidenceKind = "ASSUMED"
)
type ResearchPlan struct {
ID ResearchPlanID
Question string
SourceClasses []string
FreshnessPolicy string
CreatedAtUnixMillis int64
}
type Source struct {
ID SourceID
Kind string
Locator string
Title string
Publisher string
}
type SourceSnapshot struct {
ID SourceSnapshotID
SourceID SourceID
ContentDigest string
CapturedAtUnixMillis int64
Freshness FreshnessState
}
type SourceExcerpt struct {
ID SourceExcerptID
SnapshotID SourceSnapshotID
Text string
Start int64
End int64
}
type Citation struct {
ID CitationID
ExcerptID SourceExcerptID
Evidence EvidenceKind
Coverage CoverageState
Conflict ConflictState
ResolvedAtUnixMillis int64
}
// ResolveCitation creates evidence only when all identifiers form one exact
// source chain. It deliberately does not fetch, infer or rewrite source text.
func ResolveCitation(source Source, snapshot SourceSnapshot, excerpt SourceExcerpt, evidence EvidenceKind, coverage CoverageState, conflict ConflictState, resolvedAtUnixMillis int64) (Citation, error) {
if source.ID == "" || strings.TrimSpace(source.Locator) == "" || snapshot.SourceID != source.ID || excerpt.SnapshotID != snapshot.ID || excerpt.Validate() != nil || resolvedAtUnixMillis < 1 || evidence == "" || coverage == "" || conflict == "" {
return Citation{}, ErrCitationEvidenceMismatch
}
citation := Citation{ID: CitationID("citation:" + string(excerpt.ID)), ExcerptID: excerpt.ID, Evidence: evidence, Coverage: coverage, Conflict: conflict, ResolvedAtUnixMillis: resolvedAtUnixMillis}
return citation, citation.Validate()
}
func (p ResearchPlan) Validate() error {
if p.ID == "" || strings.TrimSpace(p.Question) == "" || len(p.SourceClasses) == 0 || strings.TrimSpace(p.FreshnessPolicy) == "" {
return errors.New("research plan requires question, source classes and freshness policy")
}
return nil
}
func (s SourceSnapshot) Validate() error {
if s.ID == "" || s.SourceID == "" || strings.TrimSpace(s.ContentDigest) == "" || s.CapturedAtUnixMillis < 1 || s.Freshness == "" {
return errors.New("source snapshot requires source, digest, capture time and freshness")
}
return nil
}
func (e SourceExcerpt) Validate() error {
if e.ID == "" || e.SnapshotID == "" || strings.TrimSpace(e.Text) == "" || e.Start < 0 || e.End < e.Start {
return errors.New("source excerpt requires snapshot, text and valid offsets")
}
return nil
}
func (c Citation) Validate() error {
if c.ID == "" || c.ExcerptID == "" || c.Evidence == "" || c.Coverage == "" || c.Conflict == "" || c.ResolvedAtUnixMillis < 1 {
return errors.New("citation requires exact excerpt, evidence, coverage, conflict and resolution time")
}
return nil
}
+38
View File
@@ -0,0 +1,38 @@
package epistemics
type BeliefState string
const (
BeliefProvisional BeliefState = "PROVISIONAL"
BeliefConflicted BeliefState = "CONFLICTED"
BeliefVerified BeliefState = "VERIFIED"
)
type Observation struct {
ID ObservationID
SourceSnapshot SourceSnapshotID
Excerpt SourceExcerptID
Text string
ObservedAt int64
}
type Claim struct {
ID ClaimID
Statement string
ObservationID ObservationID
CitationID CitationID
Verification VerificationState
}
type Belief struct {
ID BeliefID
ClaimID ClaimID
State BeliefState
}
type TruthBundle struct {
Observation Observation
Claim Claim
Belief Belief
Citation Citation
}
+13 -11
View File
@@ -120,15 +120,17 @@ type ExperienceRecord struct {
// Cognitive reflection, if used, must go through Cognitive Gateway/Router and
// produce candidates only; it cannot directly rewrite canonical state.
type ReflectionCandidate struct {
ID ReflectionCandidateID
Trigger ReflectionTriggerKind
State ReflectionCandidateState
SourceExperienceID ExperienceRecordID
OriginalIntentID OriginalIntentRef
SourceUtteranceID SourceUtteranceRef
Summary string
EvidenceRefs []EvidenceRef
RequiresCognition bool
RecordedAtUnixMillis int64
Revision int64
ID ReflectionCandidateID
Trigger ReflectionTriggerKind
State ReflectionCandidateState
SourceExperienceID ExperienceRecordID
OriginalIntentID OriginalIntentRef
SourceUtteranceID SourceUtteranceRef
Summary string
EvidenceRefs []EvidenceRef
RequiresCognition bool
RecordedAtUnixMillis int64
Revision int64
ClaimedBy string
ClaimExpiresAtUnixMillis int64
}
+66 -57
View File
@@ -24,47 +24,52 @@ type ClarificationState string
type SemanticApplicability string
type SemanticCandidateSource string
type SemanticCandidateSelectionState string
type InformationRequestKind string
const (
ChannelWebChat Channel = "WEB_CHAT"
ConversationOpen ConversationState = "OPEN"
ConversationArchived ConversationState = "ARCHIVED"
SpeechActDirective SpeechAct = "DIRECTIVE"
SpeechActAssertion SpeechAct = "ASSERTION"
SpeechActDecision SpeechAct = "DECISION"
SpeechActPreference SpeechAct = "PREFERENCE"
SpeechActQuestion SpeechAct = "QUESTION"
SpeechActHypothesis SpeechAct = "HYPOTHESIS"
SpeechActCorrection SpeechAct = "CORRECTION"
SpeechActRequestToInvestigate SpeechAct = "REQUEST_TO_INVESTIGATE"
SpeechActConfirmation SpeechAct = "CONFIRMATION"
SpeechActRejection SpeechAct = "REJECTION"
SpeechActCancellation SpeechAct = "CANCELLATION"
SpeechActConversational SpeechAct = "CONVERSATIONAL"
SpeechActUnknown SpeechAct = "UNKNOWN"
InterpretationRecorded InterpretationState = "RECORDED"
InterpretationInterpreted InterpretationState = "INTERPRETED"
RelationCorrects InteractionRelationKind = "CORRECTS"
RelationRespondsTo InteractionRelationKind = "RESPONDS_TO"
RelationRequestsCognition InteractionRelationKind = "REQUESTS_COGNITION"
RelationCognitionFallback InteractionRelationKind = "COGNITION_FALLBACK"
RelationSupersedes InteractionRelationKind = "SUPERSEDES"
RelationClarifies InteractionRelationKind = "CLARIFIES"
ProjectRolePrimary ProjectReferenceRole = "PRIMARY"
ProjectRoleRelated ProjectReferenceRole = "RELATED"
ProjectRoleReferenceSource ProjectReferenceRole = "REFERENCE_SOURCE"
ClarificationNotRequired ClarificationState = "NOT_REQUIRED"
ClarificationRequired ClarificationState = "REQUIRED"
SemanticApplicable SemanticApplicability = "APPLICABLE"
SemanticLowConfidence SemanticApplicability = "LOW_CONFIDENCE"
SemanticMixedIntent SemanticApplicability = "MIXED_INTENT"
SemanticNeedsBroaderInterpret SemanticApplicability = "NEEDS_BROADER_INTERPRETATION"
SemanticCandidateClarify SemanticApplicability = "CLARIFICATION_REQUIRED"
CandidateSourceDeterministic SemanticCandidateSource = "DETERMINISTIC_V2"
CandidateSourceCognitive SemanticCandidateSource = "COGNITIVE_V1"
CandidateSelected SemanticCandidateSelectionState = "SELECTED"
CandidateObserved SemanticCandidateSelectionState = "OBSERVED"
CandidateRejected SemanticCandidateSelectionState = "REJECTED"
ChannelWebChat Channel = "WEB_CHAT"
ConversationOpen ConversationState = "OPEN"
ConversationArchived ConversationState = "ARCHIVED"
SpeechActDirective SpeechAct = "DIRECTIVE"
SpeechActAssertion SpeechAct = "ASSERTION"
SpeechActDecision SpeechAct = "DECISION"
SpeechActPreference SpeechAct = "PREFERENCE"
SpeechActQuestion SpeechAct = "QUESTION"
SpeechActHypothesis SpeechAct = "HYPOTHESIS"
SpeechActCorrection SpeechAct = "CORRECTION"
SpeechActRequestToInvestigate SpeechAct = "REQUEST_TO_INVESTIGATE"
SpeechActConfirmation SpeechAct = "CONFIRMATION"
SpeechActRejection SpeechAct = "REJECTION"
SpeechActCancellation SpeechAct = "CANCELLATION"
SpeechActConversational SpeechAct = "CONVERSATIONAL"
SpeechActUnknown SpeechAct = "UNKNOWN"
InterpretationRecorded InterpretationState = "RECORDED"
InterpretationInterpreted InterpretationState = "INTERPRETED"
RelationCorrects InteractionRelationKind = "CORRECTS"
RelationRespondsTo InteractionRelationKind = "RESPONDS_TO"
RelationRequestsCognition InteractionRelationKind = "REQUESTS_COGNITION"
RelationCognitionFallback InteractionRelationKind = "COGNITION_FALLBACK"
RelationSupersedes InteractionRelationKind = "SUPERSEDES"
RelationClarifies InteractionRelationKind = "CLARIFIES"
ProjectRolePrimary ProjectReferenceRole = "PRIMARY"
ProjectRoleRelated ProjectReferenceRole = "RELATED"
ProjectRoleReferenceSource ProjectReferenceRole = "REFERENCE_SOURCE"
ClarificationNotRequired ClarificationState = "NOT_REQUIRED"
ClarificationRequired ClarificationState = "REQUIRED"
SemanticApplicable SemanticApplicability = "APPLICABLE"
SemanticLowConfidence SemanticApplicability = "LOW_CONFIDENCE"
SemanticMixedIntent SemanticApplicability = "MIXED_INTENT"
SemanticNeedsBroaderInterpret SemanticApplicability = "NEEDS_BROADER_INTERPRETATION"
SemanticCandidateClarify SemanticApplicability = "CLARIFICATION_REQUIRED"
CandidateSourceDeterministic SemanticCandidateSource = "DETERMINISTIC_V2"
CandidateSourceCognitive SemanticCandidateSource = "COGNITIVE_V1"
CandidateSelected SemanticCandidateSelectionState = "SELECTED"
CandidateObserved SemanticCandidateSelectionState = "OBSERVED"
CandidateRejected SemanticCandidateSelectionState = "REJECTED"
InformationRequestCapabilityInventory InformationRequestKind = "CAPABILITY_INVENTORY"
InformationRequestCognitiveModelInventory InformationRequestKind = "COGNITIVE_MODEL_INVENTORY"
InformationRequestCognitiveChannelStatus InformationRequestKind = "COGNITIVE_CHANNEL_STATUS"
InformationRequestCognitiveRecovery InformationRequestKind = "COGNITIVE_RECOVERY"
)
type Conversation struct {
@@ -156,22 +161,26 @@ type ChatMessageRequest struct {
}
type ChatMessageResponse struct {
ConversationID ConversationID
UtteranceID HumanUtteranceID
OriginalIntentID OriginalHumanIntentID
SpeechAct SpeechAct
InterpretationState InterpretationState
NeedsCognition bool
FallbackText string
ActiveProjectName string
ProjectProvenance situation.ProjectResolutionProvenance
Clarification ClarificationState
WorkPlanned bool
PlanningReply string
CapabilityStatus capability.CapabilityResolutionStatus
CapabilityKind capability.CapabilityKind
CapabilityGapID capability.CapabilityGapID
DelegationID string
DelegationState string
Reminder *continuity.ProspectiveItem
ConversationID ConversationID
UtteranceID HumanUtteranceID
OriginalIntentID OriginalHumanIntentID
SpeechAct SpeechAct
InterpretationState InterpretationState
NeedsCognition bool
FallbackText string
ActiveProjectName string
ProjectProvenance situation.ProjectResolutionProvenance
Clarification ClarificationState
WorkPlanned bool
PlanningReply string
CapabilityStatus capability.CapabilityResolutionStatus
CapabilityKind capability.CapabilityKind
CapabilityHints []capability.CapabilityKind
RequestedOutcome string
InformationRequestKind InformationRequestKind
InterpretationConfidenceMillis int64
CapabilityGapID capability.CapabilityGapID
DelegationID string
DelegationState string
Reminder *continuity.ProspectiveItem
}
+3 -1
View File
@@ -15,7 +15,9 @@ const (
type RuntimeConsumer string
const (
RuntimeConsumerCore RuntimeConsumer = "core-runtime"
RuntimeConsumerCore RuntimeConsumer = "core-runtime"
RuntimeConsumerWorkWorker RuntimeConsumer = "agent-worker"
RuntimeConsumerEpistemicsWorker RuntimeConsumer = "reflection-worker"
)
// SecretFileRef is bootstrap delivery metadata for the V1 protected local
+19
View File
@@ -0,0 +1,19 @@
package work
type AgentDispatchQueueState string
const (
AgentDispatchQueued AgentDispatchQueueState = "QUEUED"
AgentDispatchClaimed AgentDispatchQueueState = "CLAIMED"
AgentDispatchCompleted AgentDispatchQueueState = "COMPLETED"
AgentDispatchFailed AgentDispatchQueueState = "FAILED"
)
type AgentDispatchQueueItem struct {
DispatchID string
State AgentDispatchQueueState
AvailableAtUnixMillis int64
Attempts int
ClaimedBy string
UpdatedAtUnixMillis int64
}
+18
View File
@@ -0,0 +1,18 @@
package work
import "time"
type DryRunDispatch struct {
Intent AgentDispatchIntent
Lease AgentLease
Callbacks []AgentCallback
}
func (value DryRunDispatch) CallbackCount() int { return len(value.Callbacks) }
func (value DryRunDispatch) LastObservedAt() time.Time {
if len(value.Callbacks) == 0 {
return time.Time{}
}
return value.Callbacks[len(value.Callbacks)-1].ObservedAt
}
+72
View File
@@ -0,0 +1,72 @@
package work
type GraphNodeID string
type GraphState string
type GraphNodeState string
const (
GraphDraft GraphState = "DRAFT"
GraphAdmitted GraphState = "ADMITTED"
GraphRunning GraphState = "RUNNING"
GraphPartial GraphState = "PARTIAL"
GraphVerifying GraphState = "VERIFYING"
GraphCompleted GraphState = "COMPLETED"
GraphFailed GraphState = "FAILED"
GraphUnknown GraphState = "UNKNOWN"
GraphNodeBlocked GraphNodeState = "BLOCKED_PREREQUISITE"
GraphNodeReady GraphNodeState = "READY"
GraphNodeLeased GraphNodeState = "LEASED"
GraphNodeRunning GraphNodeState = "RUNNING"
GraphNodeSuccess GraphNodeState = "SUCCEEDED"
GraphNodeFailed GraphNodeState = "FAILED"
GraphNodeCancel GraphNodeState = "CANCELLED"
GraphNodeUnknown GraphNodeState = "UNKNOWN"
)
type GraphBudget struct {
MaxParallel int
MaxAttempts int
}
type GraphNode struct {
ID GraphNodeID
IdempotencyKey string
State GraphNodeState
Attempts int
}
type GraphEdge struct {
From GraphNodeID
To GraphNodeID
}
type WorkGraph struct {
ID string
Revision int64
State GraphState
Nodes []GraphNode
Edges []GraphEdge
Budget GraphBudget
}
type ResultCandidate struct {
ID string
GraphID string
GraphRevision int64
NodeID GraphNodeID
AuthorRunID AgentRunID
ArtifactRefs []EvidenceRef
EvidenceSummary string
Summary string
}
type ConflictSet struct {
ID string
GraphID string
GraphRevision int64
NodeID GraphNodeID
CandidateIDs []string
Resolved bool
}
+63
View File
@@ -0,0 +1,63 @@
package work
import "time"
type AgentLeaseID string
type AgentFencingToken uint64
type AgentLeaseState string
const (
AgentLeaseActive AgentLeaseState = "ACTIVE"
AgentLeaseExpired AgentLeaseState = "EXPIRED"
)
type AgentLease struct {
ID AgentLeaseID
RunID AgentRunID
WorkerID string
FencingToken AgentFencingToken
State AgentLeaseState
AcquiredAt time.Time
ExpiresAt time.Time
}
type AgentCallbackKind string
const (
AgentCallbackHeartbeat AgentCallbackKind = "HEARTBEAT"
AgentCallbackCheckpoint AgentCallbackKind = "CHECKPOINT"
AgentCallbackResult AgentCallbackKind = "RESULT"
AgentCallbackCancelAcknowledged AgentCallbackKind = "CANCEL_ACKNOWLEDGED"
)
type AgentCallback struct {
RunID AgentRunID
WorkerID string
FencingToken AgentFencingToken
Sequence uint64
Kind AgentCallbackKind
ObservedAt time.Time
}
type IndependentVerification struct {
ProducerRunID AgentRunID
VerifierRunID AgentRunID
VerifierIdentity string
ArtifactID string
TestEvidenceRef string
Verified bool
Summary string
}
type AgentDispatchIntent struct {
DispatchID string
IdempotencyKey string
DelegationID DelegationID
RunID AgentRunID
ImplementationID string
ImplementationRevision int64
SourceRevision string
WorktreeID string
VerificationPlanRef string
CreatedAtUnixMillis int64
}
+29
View File
@@ -0,0 +1,29 @@
package work
type DevelopmentStage string
const (
DevelopmentEvidenceCollected DevelopmentStage = "EVIDENCE_COLLECTED"
DevelopmentProposalCreated DevelopmentStage = "PROPOSAL_CREATED"
DevelopmentTaskAdmitted DevelopmentStage = "TASK_ADMITTED"
DevelopmentIsolated DevelopmentStage = "ISOLATED"
DevelopmentVerified DevelopmentStage = "VERIFIED"
DevelopmentCanary DevelopmentStage = "CANARY"
DevelopmentPromoted DevelopmentStage = "PROMOTED"
DevelopmentRolledBack DevelopmentStage = "ROLLED_BACK"
)
type DevelopmentGate struct {
ID string
Stage DevelopmentStage
EvidenceRefs []string
ProposalRef string
TaskPacketRef string
WorktreeRef string
ProducerRunRefs []AgentRunID
VerifierRunRefs []AgentRunID
VerificationEvidence string
CanaryEvidence string
PromotionRef string
RollbackRef string
}
@@ -0,0 +1,26 @@
BEGIN;
SET ROLE capability_role;
ALTER TABLE capability.cognitive_provider_instances
ADD COLUMN IF NOT EXISTS billing_mode TEXT NOT NULL DEFAULT 'UNKNOWN',
ADD COLUMN IF NOT EXISTS input_cost_micros BIGINT,
ADD COLUMN IF NOT EXISTS output_cost_micros BIGINT,
ADD COLUMN IF NOT EXISTS request_cost_micros BIGINT;
ALTER TABLE capability.cognitive_provider_instances
ADD COLUMN IF NOT EXISTS auth_mode TEXT NOT NULL DEFAULT 'NONE';
ALTER TABLE capability.cognitive_provider_instances
DROP CONSTRAINT IF EXISTS cognitive_provider_instances_billing_mode_check,
ADD CONSTRAINT cognitive_provider_instances_billing_mode_check CHECK (billing_mode IN ('UNKNOWN','FREE','PER_INPUT_UNIT','PER_OUTPUT_UNIT','PER_REQUEST')),
DROP CONSTRAINT IF EXISTS cognitive_provider_instances_auth_mode_check,
ADD CONSTRAINT cognitive_provider_instances_auth_mode_check CHECK (auth_mode IN ('NONE','API_KEY','BEARER','USERNAME_PASSWORD','OAUTH','SECRET_FILE')),
DROP CONSTRAINT IF EXISTS cognitive_provider_instances_cost_check,
ADD CONSTRAINT cognitive_provider_instances_cost_check CHECK ((input_cost_micros IS NULL OR input_cost_micros >= 0) AND (output_cost_micros IS NULL OR output_cost_micros >= 0) AND (request_cost_micros IS NULL OR request_cost_micros >= 0));
ALTER TABLE capability.cognitive_model_descriptors
ADD COLUMN IF NOT EXISTS intelligence_score_millis BIGINT;
ALTER TABLE capability.cognitive_model_descriptors
DROP CONSTRAINT IF EXISTS cognitive_model_descriptors_intelligence_score_check,
ADD CONSTRAINT cognitive_model_descriptors_intelligence_score_check CHECK (intelligence_score_millis IS NULL OR (intelligence_score_millis >= 0 AND intelligence_score_millis <= 1000));
RESET ROLE;
COMMIT;
@@ -0,0 +1,43 @@
BEGIN;
SET ROLE work_role;
CREATE TABLE IF NOT EXISTS work.agent_dispatch_intents (
dispatch_id TEXT PRIMARY KEY,
idempotency_key TEXT NOT NULL UNIQUE,
delegation_id TEXT NOT NULL,
run_id TEXT NOT NULL,
implementation_id TEXT NOT NULL,
implementation_revision BIGINT NOT NULL,
source_revision TEXT NOT NULL,
worktree_id TEXT NOT NULL,
verification_plan_ref TEXT NOT NULL,
created_at_unix_millis BIGINT NOT NULL,
CHECK (dispatch_id <> ''), CHECK (idempotency_key <> ''), CHECK (delegation_id <> ''),
CHECK (run_id <> ''), CHECK (implementation_id <> ''), CHECK (implementation_revision > 0),
CHECK (source_revision <> ''), CHECK (worktree_id <> ''), CHECK (verification_plan_ref <> ''),
CHECK (created_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS work.agent_leases (
lease_id TEXT PRIMARY KEY,
run_id TEXT NOT NULL UNIQUE,
worker_id TEXT NOT NULL,
fencing_token BIGINT NOT NULL,
state TEXT NOT NULL,
acquired_at_unix_millis BIGINT NOT NULL,
expires_at_unix_millis BIGINT NOT NULL,
CHECK (lease_id <> ''), CHECK (run_id <> ''), CHECK (worker_id <> ''),
CHECK (fencing_token > 0), CHECK (state IN ('ACTIVE','EXPIRED')),
CHECK (acquired_at_unix_millis > 0), CHECK (expires_at_unix_millis > acquired_at_unix_millis)
);
CREATE TABLE IF NOT EXISTS work.agent_callback_heads (
run_id TEXT PRIMARY KEY,
last_sequence BIGINT NOT NULL DEFAULT 0,
updated_at_unix_millis BIGINT NOT NULL,
CHECK (run_id <> ''), CHECK (last_sequence >= 0), CHECK (updated_at_unix_millis > 0)
);
RESET ROLE;
COMMIT;
@@ -0,0 +1,22 @@
BEGIN;
SET ROLE work_role;
CREATE TABLE IF NOT EXISTS work.agent_dispatch_queue (
dispatch_id TEXT PRIMARY KEY REFERENCES work.agent_dispatch_intents(dispatch_id),
state TEXT NOT NULL,
available_at_unix_millis BIGINT NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
claimed_by TEXT NOT NULL DEFAULT '',
updated_at_unix_millis BIGINT NOT NULL,
CHECK (state IN ('QUEUED','CLAIMED','COMPLETED','FAILED')),
CHECK (available_at_unix_millis > 0),
CHECK (attempts >= 0),
CHECK (updated_at_unix_millis > 0)
);
CREATE INDEX IF NOT EXISTS agent_dispatch_queue_claim_idx
ON work.agent_dispatch_queue(state, available_at_unix_millis, dispatch_id);
RESET ROLE;
COMMIT;
@@ -0,0 +1,41 @@
BEGIN;
SET ROLE work_role;
CREATE TABLE IF NOT EXISTS work.graphs (
graph_id TEXT NOT NULL,
revision BIGINT NOT NULL,
state TEXT NOT NULL,
max_parallel INTEGER NOT NULL,
max_attempts INTEGER NOT NULL,
PRIMARY KEY (graph_id, revision),
CHECK (graph_id <> ''), CHECK (revision > 0),
CHECK (state IN ('DRAFT','ADMITTED','RUNNING','PAUSED','CANCELLING','PARTIAL','VERIFYING','COMPLETED','FAILED','UNKNOWN','RECOVERING')),
CHECK (max_parallel > 0), CHECK (max_attempts > 0)
);
CREATE TABLE IF NOT EXISTS work.graph_nodes (
graph_id TEXT NOT NULL,
revision BIGINT NOT NULL,
node_id TEXT NOT NULL,
idempotency_key TEXT NOT NULL,
state TEXT NOT NULL,
attempts INTEGER NOT NULL,
PRIMARY KEY (graph_id, revision, node_id),
FOREIGN KEY (graph_id, revision) REFERENCES work.graphs(graph_id, revision),
CHECK (node_id <> ''), CHECK (idempotency_key <> ''), CHECK (attempts >= 0),
CHECK (state IN ('BLOCKED_PREREQUISITE','READY','LEASED','RUNNING','SUCCEEDED','FAILED','CANCELLED','UNKNOWN','RECOVERING'))
);
CREATE TABLE IF NOT EXISTS work.graph_edges (
graph_id TEXT NOT NULL,
revision BIGINT NOT NULL,
from_node_id TEXT NOT NULL,
to_node_id TEXT NOT NULL,
PRIMARY KEY (graph_id, revision, from_node_id, to_node_id),
FOREIGN KEY (graph_id, revision) REFERENCES work.graphs(graph_id, revision),
CHECK (from_node_id <> ''), CHECK (to_node_id <> ''), CHECK (from_node_id <> to_node_id)
);
RESET ROLE;
COMMIT;
@@ -0,0 +1,19 @@
BEGIN;
SET ROLE epistemics_role;
ALTER TABLE epistemics.reflection_candidates
ADD COLUMN IF NOT EXISTS claimed_by TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS claim_expires_at_unix_millis BIGINT NOT NULL DEFAULT 0;
ALTER TABLE epistemics.reflection_candidates
DROP CONSTRAINT IF EXISTS reflection_candidates_claim_expiry_check;
ALTER TABLE epistemics.reflection_candidates
ADD CONSTRAINT reflection_candidates_claim_expiry_check
CHECK (claim_expires_at_unix_millis >= 0);
CREATE INDEX IF NOT EXISTS reflection_candidates_claimable_idx
ON epistemics.reflection_candidates (state, claim_expires_at_unix_millis, recorded_at_unix_millis, reflection_candidate_id);
RESET ROLE;
COMMIT;
+1
View File
@@ -102,6 +102,7 @@ services:
LISTEN_ADDR: ":8083"
CORE_RUNTIME_URL: http://core-runtime:8080
LOCAL_CPU_PROVIDER_URL: http://docker.cin.su:18082/v1
LOCAL_LIFEBOAT_PROVIDER_URL: ${LOCAL_LIFEBOAT_PROVIDER_URL:-}
LOCAL_GPU_PROVIDER_URL: http://192.168.220.91:11434
COGNITIVE_OWNER_PRINCIPAL_ID: domovoy-creator-principal
ports: ["19083:8083"]
+82 -1
View File
@@ -84,6 +84,51 @@ services:
volumes:
- assistant_core_secrets:/run/secrets:ro
agent-canary:
profiles: ["agent-canary"]
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: agent-canary, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: "no"
command: ["-enable-canary", "-postgres"]
environment:
POSTGRES_HOST: postgres
POSTGRES_PORT: "5432"
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
WORK_POSTGRES_USER: work_role
WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD}
depends_on: { postgres: { condition: service_healthy } }
agent-worker:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: agent-worker, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
command: ["-worker-id", "agent-worker-production"]
environment:
POSTGRES_HOST: postgres
POSTGRES_PORT: "5432"
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
WORK_POSTGRES_USER: work_role
WORK_POSTGRES_CREDENTIAL_REF: credential://domovoy/work/db-password/v1
WORK_POSTGRES_PASSWORD_FILE: /run/secrets/work_db_password
SECRET_BACKEND_ROOT: /run/secrets
depends_on: { postgres: { condition: service_healthy } }
volumes:
- assistant_core_secrets:/run/secrets:ro
reflection-worker:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: reflection-worker, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
command: ["-worker-id", "reflection-worker-production"]
environment:
POSTGRES_HOST: postgres
POSTGRES_PORT: "5432"
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
EPISTEMICS_POSTGRES_USER: epistemics_role
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
EPISTEMICS_POSTGRES_PASSWORD_FILE: /run/secrets/epistemics_db_password
SECRET_BACKEND_ROOT: /run/secrets
depends_on: { postgres: { condition: service_healthy } }
volumes:
- assistant_core_secrets:/run/secrets:ro
continuity-runtime:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: continuity-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
@@ -126,10 +171,42 @@ services:
LISTEN_ADDR: ":8083"
CORE_RUNTIME_URL: http://core-runtime:8080
LOCAL_CPU_PROVIDER_URL: http://docker.cin.su:18082/v1
LOCAL_LIFEBOAT_PROVIDER_URL: http://cognitive-lifeboat:8086/v1
LOCAL_GPU_PROVIDER_URL: http://192.168.220.91:11434
COGNITIVE_OWNER_PRINCIPAL_ID: domovoy-creator-principal
COGNITIVE_ALLOW_CANARY: "true"
depends_on: [core-runtime]
depends_on: [core-runtime, cognitive-lifeboat]
cognitive-lifeboat:
image: ghcr.io/ggml-org/llama.cpp:server
restart: unless-stopped
command:
- --host
- 0.0.0.0
- --port
- "8086"
- --model
- /models/Qwen3-8B-Q4_K_M.gguf
- --alias
- domovoy-lifeboat
- --jinja
- --ctx-size
- "8192"
- --n-gpu-layers
- "0"
- --threads
- "32"
- --parallel
- "1"
- --reasoning
- "off"
volumes:
- llm_local_models:/models:ro
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:8086/health >/dev/null"]
interval: 10s
timeout: 3s
retries: 10
creator-web:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: creator-web, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
@@ -140,6 +217,7 @@ services:
CONTINUITY_RUNTIME_URL: http://continuity-runtime:8081
COMMIT_RUNTIME_URL: http://commit-runtime:8082
COGNITIVE_RUNTIME_URL: http://cognitive-worker:8083
ASSISTANT_BUILD_REVISION: ${ASSISTANT_SOURCE_REVISION:-unknown}
ports: ["${CREATOR_WEB_PORT:-18080}:8084"]
depends_on: [core-runtime, continuity-runtime, commit-runtime, cognitive-worker]
healthcheck:
@@ -158,3 +236,6 @@ volumes:
external: true
assistant_notification_reader_secret:
external: true
llm_local_models:
external: true
name: llm-local-models
+6 -1
View File
@@ -4,10 +4,15 @@ set -eu
backup_root="${ASSISTANT_BACKUP_DIR:-/home/m/assistant-backups/postgres}"
verify_script="${ASSISTANT_RESTORE_VERIFY_SCRIPT:-/home/m/assistant-ops/verify-postgres-restore.sh}"
latest="$(find "$backup_root" -mindepth 1 -maxdepth 1 -type d ! -name '.*' -printf '%f\n' | sort | tail -n 1)"
latest="$(find "$backup_root" -mindepth 1 -maxdepth 1 -type d -regextype posix-extended -regex '.*/[0-9]{8}T[0-9]{6}Z$' -printf '%f\n' | sort | tail -n 1)"
if [ -z "$latest" ]; then
echo "No completed PostgreSQL backup found in $backup_root" >&2
exit 1
fi
case "$latest" in
[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]T[0-9][0-9][0-9][0-9][0-9][0-9]Z) ;;
*) echo "Selected backup directory has invalid timestamp: $latest" >&2; exit 1 ;;
esac
exec "$verify_script" "$backup_root/$latest"
+1 -1
View File
@@ -45,7 +45,7 @@ $cipherPartial = $null
$manifestPartial = $null
try {
$latest = ((& ssh.exe $SshHost "find '$RemoteBackupRoot' -mindepth 1 -maxdepth 1 -type d ! -name '.*' -printf '%f\n' | sort | tail -n1") -join '').Trim()
$latest = ((& ssh.exe $SshHost "find '$RemoteBackupRoot' -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | grep -E '^[0-9]{8}T[0-9]{6}Z$' | sort | tail -n1") -join '').Trim()
if ($LASTEXITCODE -ne 0 -or $latest -notmatch '^\d{8}T\d{6}Z$') { throw 'No valid completed production backup found' }
New-Item -ItemType Directory -Force -Path $NasBackupRoot | Out-Null
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000074-creator-cabinet-operations.json",
"task_id": "DEV-000074",
"task_file": "dev/tasks/DEV-000139-production-worker-runtime.json",
"task_id": "DEV-000139",
"task_revision": 2
}
@@ -0,0 +1,41 @@
{
"task_id": "ARCH-000011",
"task_revision": 1,
"architecture_revision": "generation-1",
"base_repo_revision": "6f8c4e2bc7d4f86cad3fc0e773c8c580b19a29ee",
"risk_tier": "E",
"intent": "Produce a proposal-only architecture decision for a test Exploration profile with independent external audit, preserving useful local research and evidence while moving review visibility outside Domovoy without creating a direct model-to-world path.",
"requirement_ids": ["FD-009", "FD-015", "FD-017", "FD-018", "FD-019", "FD-025", "INV-AUTH-001", "INV-EPI-001", "INV-PROJ-001", "USER-EXTERNAL-AUDIT-WITH-EXPLORATION-FREEDOM"],
"allowed_change_scope": {
"modules": [],
"paths": [
"docs/EXTERNAL_AUDIT_EXPLORATION_PROPOSAL.md",
"dev/ACTIVE_TASK.json",
"dev/tasks/ARCH-000011-external-audit-exploration-proposal.json"
]
},
"forbidden_change_scope": {
"paths": ["architecture/**", "ci/**", ".gitea/**", "contracts/**", "modules/**", "adapters/**", "cmd/**", "database/**", "deploy/**", "security/**", "tests/**"]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_database_owner_roles": 0
},
"contracts": {
"consume": ["FD-009 model output boundary", "existing Work/Capability/Epistemics/Commit ownership", "existing transactional outbox direction"],
"may_change": []
},
"semantic_owner": "proposal-only; no architectural decision, runtime behavior, audit deployment, or execution privilege is accepted by this task",
"canonical_state": [],
"state_transitions": [],
"side_effects_and_privileges": ["select this proposal in the active task pointer", "no runtime action, provider call, credential handling, deployment, canonical mutation, or external action"],
"persistence_and_migrations": [],
"failure_retry_restart": ["proposal specifies reviewer loss as a non-failure for read-only exploration and a pending state for gated proposals", "no automatic execution or retry is introduced"],
"assumptions": ["the user's explicit instruction authorizes analysis and proposal only, not removal of secret isolation, canonical history, authority validation, or commit/executor ownership", "external audit is independent observation rather than a competing executor", "docker-test remains prohibited"],
"acceptance_scenarios": ["proposal defines useful local behavior during provider loss", "proposal separates canonical owners, audit projection, reviewer evidence and executor", "proposal defines audit outage and unknown-outcome semantics", "proposal identifies staged implementation and explicit decisions required", "no production behavior or protected architecture artifact changes"],
"required_tests": ["task schema", "architecture preflight", "exact diff scope", "documentation review", "CI"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,40 @@
{
"task_id": "ARCH-000012",
"task_revision": 1,
"architecture_revision": "generation-1",
"base_repo_revision": "948896c",
"risk_tier": "E",
"intent": "Accept the staged external-audit and Domovoy Exploration profile proposed by ARCH-000011, while retaining semantic ownership, redacted evidence, and the Commit/Executor boundary.",
"requirement_ids": ["ARCH-000011", "FD-009", "FD-015", "FD-017", "FD-025", "INV-AUTH-001", "INV-EPI-001", "INV-PROJ-001", "USER-CONTINUE-AFTER-ARCH-000011"],
"allowed_change_scope": {
"modules": [],
"paths": [
"architecture/adrs/ADR-0018-external-audit-exploration-mode.md",
"architecture/adrs/README.md",
"docs/EXTERNAL_AUDIT_EXPLORATION_PROPOSAL.md",
"dev/ACTIVE_TASK.json",
"dev/tasks/ARCH-000012-accept-external-audit-exploration.json"
]
},
"forbidden_change_scope": {
"paths": ["architecture/ARCHITECTURE_CONSTITUTION.md", "architecture/architecture-registry.yaml", "architecture/architecture-registry.json", "architecture/CORE_SEMANTIC_MODEL_V1.md", "ci/**", ".gitea/**", "contracts/**", "modules/**", "adapters/**", "cmd/**", "database/**", "deploy/**", "security/**", "tests/**"]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_database_owner_roles": 0
},
"contracts": {"consume": ["ARCH-000011 proposal", "existing Work/Epistemics/Capability/Commit ownership"], "may_change": []},
"semantic_owner": "architecture decision only; no runtime, audit deployment, external action or canonical-state behavior is changed",
"canonical_state": [],
"state_transitions": [],
"side_effects_and_privileges": ["accept ADR-0018", "no provider call, credential handling, deployment, canonical mutation, or external action"],
"persistence_and_migrations": [],
"failure_retry_restart": ["reviewer loss keeps review-governed proposals pending", "no automatic execution or retry is accepted"],
"assumptions": ["the user's instruction 'делай' after ARCH-000011 accepts the recommended staged decision", "initial delivery has no review-governed external action class", "docker-test remains prohibited"],
"acceptance_scenarios": ["ADR defines Exploration without new authority", "audit is a redacted rebuildable projection and not an executor", "reviewer outage semantics are explicit", "implementation is staged and no runtime behavior changes in this task"],
"required_tests": ["task schema", "architecture preflight", "exact diff scope", "independent protected-path review", "CI"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -1,8 +1,8 @@
{
"task_id": "DEV-000074",
"task_revision": 2,
"task_revision": 6,
"architecture_revision": "DEV-000073-baseline",
"base_repo_revision": "11a6a23f6e4f44dd88265a04846419a27f54df98",
"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": [
@@ -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"]
}
@@ -0,0 +1,47 @@
{
"task_id": "DEV-000076",
"task_revision": 1,
"architecture_revision": "DEV-000073-baseline",
"base_repo_revision": "26cf85108a63dfbd67866d9df0445dbcb54983be",
"risk_tier": "E",
"intent": "Show the human user whether Domovoy's primary cognitive channel is available without exposing provider internals or mixing technical events into the conversation.",
"requirement_ids": [
"Chat shows a human-readable cognitive channel status",
"Status is read-only and refreshed periodically",
"Chat does not expose provider, model or binding identifiers",
"Chat remains usable while status observation fails"
],
"allowed_change_scope": {
"modules": ["cmd/creator-web"],
"paths": [
"cmd/creator-web/chat_test.go",
"cmd/creator-web/static/chat.html",
"cmd/creator-web/static/assets/chat.css",
"cmd/creator-web/static/assets/chat.js",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000076-creator-chat-cognitive-status.json"
]
},
"forbidden_change_scope": {
"paths": ["architecture/**", "security/**", "database/**", "modules/**", "contracts/**", "adapters/**", "deploy/**"]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_provider_adapters": 0,
"new_database_owner_roles": 0
},
"contracts": {"consume": ["existing authenticated /api/capabilities/cognitive-proposal/health endpoint"], "may_change": []},
"semantic_owner": "none-interface-adapter",
"canonical_state": [],
"state_transitions": ["Browser polls existing cognitive capability health observation", "Browser renders coarse human-readable status"],
"side_effects_and_privileges": ["Read-only observation", "No new privilege", "No provider/model routing change"],
"persistence_and_migrations": ["No persistence or migration"],
"failure_retry_restart": ["Failed observation renders an unavailable/degraded notice; chat remains available", "Polling retries on the next interval"],
"assumptions": ["The existing health endpoint returns State or state with AVAILABLE, DEGRADED or UNAVAILABLE semantics", "The chat token authorizes the existing health endpoint"],
"acceptance_scenarios": ["Authenticated chat displays a status indicator", "Health polling does not reveal internal identifiers", "Mobile header keeps the status visible", "A failed health request does not disable the composer"],
"required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "devctl check-architecture", "git diff --check", "node --check cmd/creator-web/static/assets/chat.js"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,47 @@
{
"task_id": "DEV-000077",
"task_revision": 1,
"architecture_revision": "DEV-000073-baseline",
"base_repo_revision": "75abfb204bb427a0500a8c37440c374576c2153c",
"risk_tier": "E",
"intent": "Make model configuration changes understandable from the human chat while keeping configuration mutation in the separate operator console.",
"requirement_ids": [
"User can manually re-check the cognitive channel from chat",
"Chat clearly directs configuration changes to the operator console",
"No configuration mutation is performed by the browser chat",
"The conversation remains free of technical runtime identifiers"
],
"allowed_change_scope": {
"modules": ["cmd/creator-web"],
"paths": [
"cmd/creator-web/chat_test.go",
"cmd/creator-web/static/chat.html",
"cmd/creator-web/static/assets/chat.css",
"cmd/creator-web/static/assets/chat.js",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000077-creator-chat-cognitive-bridge.json"
]
},
"forbidden_change_scope": {
"paths": ["architecture/**", "security/**", "database/**", "modules/**", "contracts/**", "adapters/**", "deploy/**"]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_provider_adapters": 0,
"new_database_owner_roles": 0
},
"contracts": {"consume": ["existing authenticated /api/capabilities/cognitive-proposal/health endpoint", "existing /admin operator surface"], "may_change": []},
"semantic_owner": "none-interface-adapter",
"canonical_state": [],
"state_transitions": ["User requests a fresh read-only health observation", "Browser presents console handoff when the channel is not ready"],
"side_effects_and_privileges": ["Read-only observation only", "No new privilege", "No provider/model routing change", "No chat mutation from status controls"],
"persistence_and_migrations": ["No persistence or migration"],
"failure_retry_restart": ["Manual check uses the same safe health route", "Polling remains the retry mechanism"],
"assumptions": ["The operator console remains the authorized place for runtime configuration changes", "The existing health route is sufficient to observe readiness"],
"acceptance_scenarios": ["Clicking status requests a fresh health observation", "Unavailable status exposes an operator-console handoff", "Available status hides the handoff", "Chat HTML remains free of internal identifiers"],
"required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "devctl check-architecture", "git diff --check", "node --check cmd/creator-web/static/assets/chat.js"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,45 @@
{
"task_id": "DEV-000078",
"task_revision": 1,
"architecture_revision": "DEV-000073-baseline",
"base_repo_revision": "0d10e6fd82a7c374bebbc8d3cee9c1ed0b420745",
"risk_tier": "E",
"intent": "Let Domovoy answer explicit questions about the local cognitive channel from the Capability-owned health observation and route configuration changes to the operator console without mutating runtime configuration.",
"requirement_ids": [
"Explicit model/channel questions use observed capability health",
"Model configuration requests never call a provider directly from Core",
"Configuration changes are explained as a separate operator-console action",
"Provider and model identifiers remain hidden from human chat"
],
"allowed_change_scope": {
"modules": ["cmd/core-runtime"],
"paths": [
"cmd/core-runtime/main.go",
"cmd/core-runtime/main_test.go",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000078-core-conversational-cognitive-health.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": ["Capability repository LoadHealth", "existing cognitive-proposal health semantics", "existing /admin operator surface"], "may_change": []},
"semantic_owner": "capability-observation-consumer",
"canonical_state": [],
"state_transitions": ["Human asks about local cognitive channel", "Core reads Capability-owned health observation", "Core returns observation or safe unavailable explanation"],
"side_effects_and_privileges": ["No provider call for explicit status/configuration questions", "No runtime configuration mutation", "No new privilege"],
"persistence_and_migrations": ["Existing utterance and semantic persistence only", "No migration"],
"failure_retry_restart": ["Missing health observation produces an honest unavailable reply", "Future status polling remains available in chat UI"],
"assumptions": ["The operator console is the authorized configuration surface", "Capability health is a sufficient provider-neutral observation for the first conversational response"],
"acceptance_scenarios": ["Проверь локальную модель returns a human-readable health observation", "Переключи модель does not mutate configuration and directs to console", "Replies contain no provider/model/binding identifiers", "Unit tests cover available, unavailable and repository failure states"],
"required_tests": ["go test ./cmd/core-runtime", "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"]
}
@@ -0,0 +1,38 @@
{
"task_id": "DEV-000079",
"task_revision": 1,
"architecture_revision": "DEV-000073-baseline",
"base_repo_revision": "b0d31012dac1526e721e57c691555933eb501a5a",
"risk_tier": "E",
"intent": "Make the primary Domovoy chat self-explanatory with concise conversation starters for cognitive-channel checks and configuration handoff.",
"requirement_ids": [
"A new user can discover the main cognitive-channel conversation paths",
"Conversation starters submit ordinary human text",
"No starter exposes internal identifiers",
"The chat remains usable on mobile"
],
"allowed_change_scope": {
"modules": ["cmd/creator-web"],
"paths": [
"cmd/creator-web/chat_test.go",
"cmd/creator-web/static/chat.html",
"cmd/creator-web/static/assets/chat.css",
"cmd/creator-web/static/assets/chat.js",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000079-chat-conversation-starters.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 message endpoint"], "may_change": []},
"semantic_owner": "none-interface-adapter",
"canonical_state": [],
"state_transitions": ["User selects a conversation starter", "Browser submits its ordinary text through the existing chat flow"],
"side_effects_and_privileges": ["Existing chat semantics only", "No new privilege", "No direct model or configuration mutation"],
"persistence_and_migrations": ["Existing conversation persistence only", "No migration"],
"failure_retry_restart": ["Starter uses existing chat error handling"],
"assumptions": ["The user can still type any other request in the composer"],
"acceptance_scenarios": ["Three human-readable starter actions are visible", "Clicking a starter sends its text through the normal chat flow", "No internal IDs appear in the page", "Starter layout remains usable at mobile width"],
"required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "devctl check-architecture", "git diff --check", "node --check cmd/creator-web/static/assets/chat.js"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,41 @@
{
"task_id": "DEV-000080",
"task_revision": 1,
"architecture_revision": "DEV-000073-baseline",
"base_repo_revision": "47fd0c4b027ff5ab2781b26c5bc4f7ddc45d961f",
"risk_tier": "E",
"intent": "Replace the unstable single-column chat presentation with a fixed three-column layout, explicit console return, and a safe human path for passing AI information into the current conversation.",
"requirement_ids": [
"Header controls keep stable dimensions",
"Desktop chat has independently scrolling left, center and right regions",
"Console has an explicit return-to-chat action",
"User can pass non-secret AI notes as an ordinary chat message",
"Mobile layout remains usable"
],
"allowed_change_scope": {
"modules": ["cmd/creator-web"],
"paths": [
"cmd/creator-web/chat_test.go",
"cmd/creator-web/static/index.html",
"cmd/creator-web/static/chat.html",
"cmd/creator-web/static/assets/chat.css",
"cmd/creator-web/static/assets/chat.js",
"cmd/creator-web/static/assets/console.css",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000080-chat-three-column-layout.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 message endpoint", "existing /admin route"], "may_change": []},
"semantic_owner": "none-interface-adapter",
"canonical_state": ["AI notes are submitted through the existing conversation; no new AI registry state is created"],
"state_transitions": ["User submits non-secret AI notes as a normal chat message", "User navigates between chat and console using explicit links"],
"side_effects_and_privileges": ["Existing chat persistence only", "No direct model/provider mutation", "No new privilege"],
"persistence_and_migrations": ["No migration"],
"failure_retry_restart": ["AI notes use existing chat failure handling", "Layout has no new runtime dependency"],
"assumptions": ["The current conversation is the correct first destination for AI notes", "Sensitive credentials must never be pasted into chat"],
"acceptance_scenarios": ["Desktop regions scroll independently", "Header status and actions do not resize when text changes", "Console visibly links back to chat", "AI notes form sends a clear message into chat", "Chat remains usable below 900px viewport width"],
"required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "devctl check-architecture", "git diff --check", "node --check cmd/creator-web/static/assets/chat.js"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000081",
"task_revision": 1,
"architecture_revision": "DEV-000073-baseline",
"base_repo_revision": "6e0cf3cd1df864e8be8a9b61fe8462181516bab9",
"risk_tier": "E",
"intent": "Polish the stable chat header after the three-column rollout so the cognitive status cannot push navigation controls around.",
"requirement_ids": ["Header status is right-aligned and bounded", "Console exposes an explicit chat return link"],
"allowed_change_scope": {"modules": ["cmd/creator-web"], "paths": ["cmd/creator-web/chat_test.go", "cmd/creator-web/static/assets/chat.css", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000081-chat-header-final-polish.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 admin surfaces"], "may_change": []},
"semantic_owner": "none-interface-adapter",
"canonical_state": [],
"state_transitions": ["No semantic state transition"],
"side_effects_and_privileges": ["No new privilege", "No runtime behavior change"],
"persistence_and_migrations": ["No persistence or migration"],
"failure_retry_restart": ["No new failure mode"],
"assumptions": ["The previous three-column layout is the intended public chat surface"],
"acceptance_scenarios": ["Header status does not alter the positions of console and logout controls", "Admin HTML includes a visible chat return action"],
"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"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000082",
"task_revision": 1,
"architecture_revision": "DEV-000073-baseline",
"base_repo_revision": "ae9933ee846254f970e4ef3f3e46d63e7a01db5a",
"risk_tier": "E",
"intent": "Prevent accidental transmission of obvious credentials through the AI information form while preserving ordinary model notes as chat messages.",
"requirement_ids": ["AI notes form warns about secrets", "Obvious secret-shaped input is blocked before chat submission", "Safe model descriptions continue through the normal chat flow"],
"allowed_change_scope": {"modules": ["cmd/creator-web"], "paths": ["cmd/creator-web/chat_test.go", "cmd/creator-web/static/chat.html", "cmd/creator-web/static/assets/chat.css", "cmd/creator-web/static/assets/chat.js", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000082-chat-ai-data-safety-guard.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 message endpoint"], "may_change": []},
"semantic_owner": "none-interface-adapter",
"canonical_state": [],
"state_transitions": ["Secret-shaped text is rejected locally", "Non-secret AI notes continue as a normal conversation message"],
"side_effects_and_privileges": ["No new privilege", "No transmission when the guard detects a secret-shaped value"],
"persistence_and_migrations": ["No persistence or migration"],
"failure_retry_restart": ["User can edit and retry after removing the sensitive value"],
"assumptions": ["The guard is a convenience barrier, not a complete secret scanner", "Users must still avoid pasting secrets"],
"acceptance_scenarios": ["API-key and private-key-shaped input is blocked", "Password/token labels are blocked", "Ordinary model notes are submitted", "The warning is visible and accessible"],
"required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "devctl check-architecture", "git diff --check", "node --check cmd/creator-web/static/assets/chat.js"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,72 @@
{
"task_id": "DEV-000083",
"task_revision": 3,
"architecture_revision": "DEV-000073-baseline",
"base_repo_revision": "0a2599cfa6255f1a98856d2fd09dc51a2119f525",
"risk_tier": "C",
"intent": "Keep Domovoy alive and conversational when the primary cognitive channel is unavailable by running a small local Lifeboat provider and retaining an honest deterministic degraded mode.",
"requirement_ids": [
"Primary cognitive settings remain confirmation-controlled while healthy",
"Unavailable primary cognition exposes a human-readable degraded mode instead of killing chat",
"A small local-only provider can serve as the Lifeboat through the existing Cognitive Gateway",
"The core can still accept and explain configuration requests when all model providers are unavailable"
],
"allowed_change_scope": {
"modules": ["cmd/core-runtime", "cmd/cognitive-worker", "cmd/creator-web", "modules/capability", "adapters/postgres", "deployment"],
"paths": [
"cmd/core-runtime/main.go",
"cmd/core-runtime/main_test.go",
"cmd/cognitive-worker/main.go",
"cmd/cognitive-worker/main_test.go",
"modules/capability/domain/cognitive_registry.go",
"modules/capability/domain/cognitive_registry_test.go",
"modules/capability/ports/repository.go",
"contracts/capability/cognitive_registry.go",
"cmd/creator-web/static/assets/chat.js",
"cmd/creator-web/static/chat.html",
"cmd/creator-web/chat_test.go",
"adapters/postgres/capability_repository.go",
"tests/cognitive_canary/promotion_test.go",
"deploy/docker-compose.prod.yml",
"deploy/docker-compose.dev.yml",
"dev/tasks/DEV-000083-lifeboat-benchmark-manifest.json",
"docs/evidence/domovoy-lifeboat-qualification-2026-08-21-v1.json",
"dev/tasks/DEV-000083-domovoy-lifeboat-mode.json"
]
},
"forbidden_change_scope": {
"paths": ["architecture/**", "security/**", "database/**", "contracts/**", "adapters/**", "modules/**"]
},
"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 Cognitive Gateway and Capability Registry", "existing chat response fields", "existing cognitive health endpoint"],
"may_change": ["authorized cognitive activation transition"]
},
"semantic_owner": "capability-observation-consumer",
"canonical_state": [],
"state_transitions": [
"Healthy primary channel keeps normal configuration handoff and confirmation behavior",
"Unavailable primary channel renders Domovoy alive in local degraded mode",
"Optional Lifeboat provider participates only through the existing gateway resolution/failover path"
],
"side_effects_and_privileges": ["No new privilege", "No external egress", "No direct provider call from Core", "No configuration mutation from browser chat"],
"persistence_and_migrations": ["No migration", "Existing conversation and capability observations only"],
"failure_retry_restart": ["Provider absence falls back to the deterministic core reply", "The Lifeboat container restarts independently from the primary model runtime", "Restart reconstructs the provider registration from environment"],
"assumptions": ["The Lifeboat endpoint implements the existing OpenAI-compatible local adapter contract", "Capability discovery/promotion remains the existing authorized path", "No real test secrets are in scope for this task"],
"acceptance_scenarios": [
"Primary unavailable message says Domovoy remains alive and can help recover",
"Configuration request in degraded mode no longer falsely requires the primary model",
"Production Lifeboat container exposes the configured small local model through the existing gateway adapter",
"Chat status distinguishes primary unavailable from Domovoy unavailable",
"Healthy mode keeps confirmation/console wording"
],
"required_tests": ["go test ./cmd/core-runtime", "go test ./cmd/cognitive-worker", "go test ./cmd/creator-web", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "devctl check-architecture", "git diff --check", "node --check cmd/creator-web/static/assets/chat.js"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,19 @@
{
"artifact_id": "domovoy-lifeboat-qualification-2026-08-21-v1",
"providers": [
{
"provider_instance_id": "local-lifeboat",
"provider_kind": "openai-compatible-chat-v1",
"endpoint": "http://cognitive-lifeboat:8086/v1",
"candidates": [
{
"provider_model_id": "domovoy-lifeboat",
"model_descriptor_id": "model-7dd2e1dd5adb40af7ad7985f",
"model_revision": 464323937472454403,
"binding_id": "binding-98b26ae572f3534ecb7d32c6",
"binding_revision": 594087847110549365
}
]
}
]
}
@@ -0,0 +1,70 @@
{
"task_id": "DEV-000084",
"task_revision": 1,
"architecture_revision": "DEV-000073-baseline",
"base_repo_revision": "0482e6ac14cd7625598a203dbdec9298b1bac04c",
"risk_tier": "C",
"intent": "Evolve the single-catalog cognitive orchestrator with an always-available built-in provider and task-aware selection across connected providers using capability, quality, latency, cost, and reliability evidence.",
"requirement_ids": [
"Built-in Domovoy cognition remains available without an external provider",
"Connected providers expose typed connection references without credential material",
"Provider selection considers task capability, quality, speed, cost, and reliability",
"Provider and model revisions remain replaceable as the catalog evolves"
],
"allowed_change_scope": {
"modules": ["capability", "cognitive-worker"],
"paths": [
"contracts/capability/cognitive_registry.go",
"contracts/cognitive/types.go",
"contracts/interaction/types.go",
"modules/capability/domain/cognitive_registry.go",
"modules/capability/domain/cognitive_registry_test.go",
"adapters/postgres/capability_repository.go",
"database/migrations/028_capability_cognitive_provider_profile.sql",
"cmd/cognitive-worker/main.go",
"cmd/cognitive-worker/gateway.go",
"cmd/cognitive-worker/gateway_registry_test.go",
"cmd/cognitive-worker/interpretation.go",
"cmd/cognitive-worker/interpretation_live_trace_test.go",
"cmd/cognitive-worker/discovery.go",
"cmd/creator-web/server.go",
"cmd/core-runtime/main.go",
"dev/tasks/DEV-000084-evolving-cognitive-provider-catalog.json"
]
},
"forbidden_change_scope": {
"paths": ["architecture/**", "security/**", "cmd/core-runtime/**", "cmd/creator-web/**", "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 capability registry", "existing cognitive gateway"],
"may_change": ["versioned provider profile and task-aware selection fields"]
},
"semantic_owner": "capability",
"canonical_state": ["provider connection profile", "model suitability and performance evidence"],
"state_transitions": [
"discovered provider profile is validated before invocation",
"selection ranks eligible candidates without granting new authority",
"provider revision supersedes prior metadata without rewriting history"
],
"side_effects_and_privileges": ["No new privilege", "Credential material remains outside canonical registry"],
"persistence_and_migrations": ["One additive capability migration", "No cross-owner writes"],
"failure_retry_restart": ["Built-in provider remains available when registry or remote providers fail", "Provider failures are observed and excluded for the current attempt"],
"assumptions": ["V1 has one shared catalog and no user/group visibility", "Existing CredentialRef contract is retained", "Selection is advisory routing and does not authorize external actions"],
"acceptance_scenarios": [
"Built-in provider answers when no connected provider is invocable",
"Secret values never appear in provider registry responses or logs",
"Task requiring a capability excludes models without it",
"Quality, reliability, latency, and cost policy affect deterministic ranking",
"A failed primary provider falls back to another eligible provider"
],
"required_tests": ["go test ./modules/capability/domain", "go test ./cmd/cognitive-worker", "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_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000085",
"task_revision": 1,
"architecture_revision": "DEV-000073-baseline",
"base_repo_revision": "cdc8eea1a747657ce0534b99ff35933ab3385877",
"risk_tier": "C",
"intent": "Keep Domovoy useful when external or active cognitive bindings are unavailable by invoking the built-in local lifeboat through the Cognitive Gateway and presenting an actionable recovery focus when all cognition fails.",
"requirement_ids": ["Built-in local cognition remains available", "Provider loss does not collapse conversation", "Recovery state is explained and actionable"],
"allowed_change_scope": {"modules": ["cognitive-worker"], "paths": ["cmd/cognitive-worker/gateway.go", "cmd/cognitive-worker/interpretation.go", "cmd/cognitive-worker/main.go", "cmd/cognitive-worker/gateway_registry_test.go", "cmd/cognitive-worker/interpretation_test.go", "dev/tasks/DEV-000085-builtin-cognitive-recovery.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "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 Cognitive Gateway and local provider adapter contracts"], "may_change": []},
"semantic_owner": "capability",
"canonical_state": [],
"state_transitions": ["No active binding -> built-in local invocation", "Built-in invocation failure -> recovery-focused degraded response"],
"side_effects_and_privileges": ["No new privilege", "Built-in path is local-only and cannot egress externally"],
"persistence_and_migrations": ["No persistence or migration"],
"failure_retry_restart": ["Provider failure remains observable; next request retries resolution and built-in local path"],
"assumptions": ["The configured local lifeboat endpoint is part of Domovoy and may be used without external egress acknowledgement"],
"acceptance_scenarios": ["Inactive registry with healthy local lifeboat invokes the local adapter", "Interpretation also uses the built-in local path", "All cognition failure returns an actionable recovery response"],
"required_tests": ["go test ./cmd/cognitive-worker", "go test ./...", "go vet ./...", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000086",
"task_revision": 1,
"architecture_revision": "DEV-000073-baseline",
"base_repo_revision": "cdc8eea1a747657ce0534b99ff35933ab3385877",
"risk_tier": "C",
"intent": "Replace the unusable tiny lifeboat model with a capable, locally hosted Qwen3 8B quantized baseline while keeping Domovoy runtime on docker.cin.su.",
"requirement_ids": ["Built-in local cognition is useful for ordinary dialogue", "Local model remains available without external egress", "Model choice is explicit and replaceable"],
"allowed_change_scope": {"modules": ["deployment", "cognitive-worker"], "paths": ["deploy/docker-compose.prod.yml", "dev/tasks/DEV-000086-local-model-baseline.json", "dev/tasks/DEV-000085-builtin-cognitive-recovery.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**"]},
"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 llama.cpp OpenAI-compatible local endpoint", "existing Cognitive Gateway"], "may_change": []},
"semantic_owner": "capability",
"canonical_state": [],
"state_transitions": ["Configured model file -> loaded local baseline", "Local model replacement -> restart and health verification"],
"side_effects_and_privileges": ["No new privilege", "Model is local-only and mounted read-only into the runtime"],
"persistence_and_migrations": ["No persistence or migration"],
"failure_retry_restart": ["Missing or invalid model keeps the container unhealthy and does not silently fall back to the tiny model"],
"assumptions": ["Qwen3-8B-Q4_K_M.gguf is present in the protected Docker model volume on docker.cin.su before rollout"],
"acceptance_scenarios": ["The configured lifeboat starts with the named Qwen3 8B model", "The OpenAI-compatible health endpoint is healthy", "No other host is used for Domovoy runtime"],
"required_tests": ["go test ./cmd/cognitive-worker", "go test ./...", "go vet ./...", "git diff --check", "production health and model endpoint verification"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT", "REQUIRES_MODEL_ARTIFACT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000087",
"task_revision": 1,
"architecture_revision": "DEV-000073-baseline",
"base_repo_revision": "cdc8eea1a747657ce0534b99ff35933ab3385877",
"risk_tier": "D",
"intent": "When the selected cognitive route is unavailable, keep Domovoy conversational, diagnose the failure, use the built-in local model, and probe explicitly configured external GPU/HTTP providers through the Cognitive Gateway without exposing credentials or bypassing egress policy.",
"requirement_ids": ["Provider loss becomes a recovery objective", "Meaning and task objective guide provider choice", "External egress requires explicit acknowledgement", "Credentials remain opaque secret references"],
"allowed_change_scope": {"modules": ["cognitive-worker"], "paths": ["cmd/cognitive-worker/gateway.go", "cmd/cognitive-worker/interpretation.go", "cmd/cognitive-worker/main.go", "cmd/cognitive-worker/recovery.go", "cmd/cognitive-worker/gateway_registry_test.go", "cmd/cognitive-worker/interpretation_test.go", "cmd/cognitive-worker/recovery_test.go", "dev/tasks/DEV-000087-cognitive-recovery-orchestrator.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "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 typed Cognitive Gateway", "existing local and Ollama adapters", "existing discovery and binding resolver"], "may_change": []},
"semantic_owner": "capability",
"canonical_state": [],
"state_transitions": ["Selected provider failure -> local built-in attempt", "Local failure -> recovery diagnosis and configured-provider probe", "External probe -> registry discovery only; invocation requires policy acknowledgement"],
"side_effects_and_privileges": ["No new privilege", "No arbitrary network scanning", "No credential material in logs or messages"],
"persistence_and_migrations": ["Existing discovery registration only; no new persistence"],
"failure_retry_restart": ["Bounded sequential probes per request", "No unmanaged background retry loop", "Each new request re-resolves and re-probes"],
"assumptions": ["External GPU/HTTP endpoints are explicitly configured by deployment operators", "Core Runtime remains the owner of activation and selection"],
"acceptance_scenarios": ["Inactive registry with healthy local model still produces an intelligent local response", "Interpretation uses the same local recovery path", "External endpoints are discovered but never invoked without egress acknowledgement", "All failures yield actionable recovery text instead of a generic unavailable error"],
"required_tests": ["go test ./cmd/cognitive-worker", "go test ./...", "go vet ./...", "git diff --check", "production recovery request and provider health verification"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT", "BLOCKED_BY_MISSING_CAPABILITY"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000088",
"task_revision": 1,
"architecture_revision": "DEV-000073-baseline",
"base_repo_revision": "cdc8eea1a747657ce0534b99ff35933ab3385877",
"risk_tier": "B",
"intent": "Expose self-describing build revision and runtime update time in the Domovoy chat without requiring a human status query.",
"requirement_ids": ["Human can identify the running build", "Displayed update time reflects the running container", "No internal identifiers are exposed in the chat UI"],
"allowed_change_scope": {"modules": ["creator-web", "deployment"], "paths": ["cmd/creator-web/server.go", "cmd/creator-web/chat_test.go", "cmd/creator-web/static/chat.html", "deploy/docker-compose.prod.yml", "dev/tasks/DEV-000088-runtime-build-status.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**"]},
"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 source revision build argument"], "may_change": []},
"semantic_owner": "creator-web",
"canonical_state": [],
"state_transitions": ["Container startup -> build metadata visible in chat"],
"side_effects_and_privileges": ["No new privilege", "No user or provider secrets displayed"],
"persistence_and_migrations": ["No persistence or migration"],
"failure_retry_restart": ["Missing revision is displayed as development/unknown; startup time remains observable"],
"assumptions": ["The production compose runner supplies ASSISTANT_SOURCE_REVISION for the running revision"],
"acceptance_scenarios": ["Chat shows revision and update time without a manual question", "HTML contains no internal provider or session identifiers", "Tests remain deterministic outside production"],
"required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "git diff --check", "production page verification"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000089",
"task_revision": 1,
"architecture_revision": "DEV-000073-baseline",
"base_repo_revision": "9f717d720e217afd4cc87a4fbf851050d466d8cd",
"risk_tier": "E",
"intent": "Integrate the Domovoy local-model baseline, cognitive recovery orchestrator, and self-describing runtime status into one reviewed production change.",
"requirement_ids": ["Built-in cognition remains useful", "Provider loss triggers recovery", "Production runtime identifies its revision and update time"],
"allowed_change_scope": {"modules": ["cognitive-worker", "creator-web", "deployment", "development-governance"], "paths": ["cmd/cognitive-worker/discovery.go", "cmd/cognitive-worker/gateway.go", "cmd/cognitive-worker/gateway_registry_test.go", "cmd/cognitive-worker/interpretation.go", "cmd/cognitive-worker/main.go", "cmd/creator-web/chat_test.go", "cmd/creator-web/server.go", "cmd/creator-web/static/chat.html", "deploy/docker-compose.prod.yml", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000085-builtin-cognitive-recovery.json", "dev/tasks/DEV-000086-local-model-baseline.json", "dev/tasks/DEV-000087-cognitive-recovery-orchestrator.json", "dev/tasks/DEV-000088-runtime-build-status.json", "dev/tasks/DEV-000089-domovoy-cognitive-upgrade-integration.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["existing cognitive gateway", "existing creator-web runtime", "existing production compose runner"], "may_change": []},
"semantic_owner": "capability",
"canonical_state": [],
"state_transitions": ["Old active task -> integrated reviewed task", "Old local baseline -> Qwen3-8B baseline", "Unavailable active binding -> built-in and configured external recovery"],
"side_effects_and_privileges": ["No new privilege", "External invocation remains acknowledgement-gated", "No secrets in source or UI"],
"persistence_and_migrations": ["No persistence or migration"],
"failure_retry_restart": ["Existing auto-update runner deploys the merged main revision", "Missing model artifact leaves the lifeboat unhealthy rather than silently selecting the tiny model"],
"assumptions": ["Qwen3-8B-Q4_K_M.gguf is present in the protected Docker model volume on docker.cin.su"],
"acceptance_scenarios": ["CI accepts exact task scope", "Production chat shows running revision and update time", "Domovoy can answer through built-in local cognition when registry activation is unavailable"],
"required_tests": ["go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "git diff --check", "production health and revision verification"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT", "REQUIRES_MODEL_ARTIFACT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000090",
"task_revision": 1,
"architecture_revision": "DEV-000073-baseline",
"base_repo_revision": "57fb572d5b7dadff9fd82e995448c1fea30e1b5f",
"risk_tier": "E",
"intent": "Make Domovoy chat failures understandable and recoverable by distinguishing expired sessions, runtime outages, and invalid requests in the human UI.",
"requirement_ids": ["Human sees the actual recovery state", "Expired sessions return to login", "Runtime failure does not pretend to be a model answer"],
"allowed_change_scope": {"modules": ["creator-web", "development-governance"], "paths": ["cmd/creator-web/chat_test.go", "cmd/creator-web/static/assets/chat.js", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000090-chat-runtime-error-recovery.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["existing creator-web HTTP status contract"], "may_change": ["human-facing chat error handling"]},
"semantic_owner": "creator-web",
"canonical_state": [],
"state_transitions": ["401/403 -> clear session and show login", "5xx -> explain runtime outage and allow retry", "other client error -> explain invalid request"],
"side_effects_and_privileges": ["No new privilege", "No internal identifiers or raw upstream error text shown"],
"persistence_and_migrations": ["No persistence or migration"],
"failure_retry_restart": ["Session recovery is local and deterministic", "Runtime outage remains retryable"],
"assumptions": ["Creator Web status codes remain the source of truth for session and runtime failure classes"],
"acceptance_scenarios": ["Expired browser token returns to login with an explanation", "Runtime outage shows a useful retry message", "Valid responses remain unchanged", "No generic 'message not accepted runtime' text remains"],
"required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,33 @@
{
"task_id": "DEV-000091",
"task_revision": 1,
"architecture_revision": "DEV-000073-baseline",
"base_repo_revision": "a6059cc9c6a391152f0981ee2ae8eccee620794d",
"risk_tier": "E",
"intent": "Keep ordinary conversation and simple informational questions available through Domovoy's built-in local channel without waiting for strict cognitive interpretation, while preserving deterministic semantics when local structured interpretation times out.",
"requirement_ids": ["Built-in local channel is always usable", "Simple questions do not depend on external bindings", "Interpretation timeout does not become runtime outage"],
"allowed_change_scope": {
"modules": ["core-runtime", "cognitive-worker", "development-governance"],
"paths": [
"cmd/core-runtime/cognitive_interpretation.go",
"cmd/core-runtime/cognitive_interpretation_test.go",
"cmd/cognitive-worker/interpretation.go",
"cmd/cognitive-worker/interpretation_test.go",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000091-builtin-local-chat-routing.json"
]
},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["existing cognitive interpretation HTTP contract", "existing built-in advisory gateway"], "may_change": []},
"semantic_owner": "interaction",
"canonical_state": [],
"state_transitions": ["simple informational question -> built-in advisory path", "local structured interpretation timeout -> deterministic candidate retained"],
"side_effects_and_privileges": ["No new privilege", "No direct provider calls outside cognitive gateway"],
"persistence_and_migrations": ["No persistence or migration"],
"failure_retry_restart": ["Interpretation remains best-effort and bounded", "Built-in advisory remains the local fallback"],
"assumptions": ["Creator Web invokes advisory for a question whose core response retains NeedsCognition"],
"acceptance_scenarios": ["A simple Russian question reaches the built-in advisory without /v1/interpretations", "A timed-out built-in interpretation returns a completed deterministic candidate", "Investigative or ambiguous requests still use strict interpretation"],
"required_tests": ["go test ./cmd/core-runtime", "go test ./cmd/cognitive-worker", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,26 @@
{
"task_id": "DEV-000092",
"task_revision": 1,
"architecture_revision": "DEV-000073-baseline",
"base_repo_revision": "2306c8d6b31fbf5ac0933ecc72b4f76bec7218f2",
"risk_tier": "E",
"intent": "Answer semantically identified model and external-AI inventory requests from the capability registry through a deterministic read-only chat fast path instead of treating them as unavailable cognition.",
"requirement_ids": ["Model inventory is readable from chat", "No secret material is shown", "Inventory does not require model reasoning"],
"allowed_change_scope": {
"modules": ["core-runtime", "interaction", "development-governance"],
"paths": ["cmd/core-runtime/main.go", "cmd/core-runtime/main_test.go", "modules/interaction/domain/chat.go", "modules/interaction/domain/chat_test.go", "modules/interaction/domain/semantics.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000092-cognitive-model-inventory-fast-path.json"]
},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["capability owner read-only LoadCognitiveCandidates port"], "may_change": ["human-facing chat fast-path text"]},
"semantic_owner": "capability",
"canonical_state": [],
"state_transitions": ["model inventory question -> read-only capability registry query"],
"side_effects_and_privileges": ["No mutation", "CredentialRef and endpoint secret material are never rendered"],
"persistence_and_migrations": ["No persistence or migration"],
"failure_retry_restart": ["Registry read failure produces an honest unavailable-settings explanation", "No cognitive retry is required"],
"assumptions": ["CapabilityRepository.LoadCognitiveCandidates returns provider-neutral metadata and opaque secret references"],
"acceptance_scenarios": ["Какие модели доступны? returns the registered inventory", "Напиши список настроек ИИ returns safe settings", "Empty or unavailable registry does not expose credentials or internal identifiers"],
"required_tests": ["go test ./cmd/core-runtime", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,35 @@
{
"task_id": "DEV-000093",
"task_revision": 1,
"architecture_revision": "DEV-000073-baseline",
"base_repo_revision": "cf66cd472cb791125347922c9e74ded534c9a3a7",
"risk_tier": "E",
"intent": "Repair learning-evidence integrity so reflection candidates preserve normalized source evidence, corrections do not degrade unrelated skills, and unknown or skipped outcomes remain distinct from verified failures.",
"requirement_ids": ["Reflection references valid experience", "Skill evidence stays capability-specific", "Unknown is not failure"],
"allowed_change_scope": {
"modules": ["core-runtime", "epistemics", "capability", "development-governance"],
"paths": [
"cmd/core-runtime/learning.go",
"cmd/core-runtime/learning_test.go",
"modules/epistemics/domain/experience.go",
"modules/epistemics/domain/experience_test.go",
"modules/capability/domain/skill.go",
"modules/capability/domain/skill_test.go",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000093-learning-evidence-integrity.json"
]
},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["existing epistemics experience/reflection contract", "existing capability skill contract"], "may_change": []},
"semantic_owner": "epistemics",
"canonical_state": ["epistemics ExperienceRecord and ReflectionCandidate", "capability SkillProfile"],
"state_transitions": ["failure/correction/low-confidence evidence -> reflection candidate requiring cognition", "unknown/skipped skill observation -> retained as non-failure evidence"],
"side_effects_and_privileges": ["No external mutation", "No new privilege", "No model/provider invocation"],
"persistence_and_migrations": ["No schema migration"],
"failure_retry_restart": ["Learning write failure remains non-authoritative and cannot alter external state", "No retry is inferred as a success"],
"assumptions": ["A generic user correction is not evidence against the reminder-resolution skill unless it is explicitly linked to that skill"],
"acceptance_scenarios": ["Reflection accepts an otherwise valid experience without a caller-supplied ID", "Correction outside reminder flow does not mutate reminder skill", "Unknown and skipped outcomes do not populate known-failure evidence"],
"required_tests": ["go test ./cmd/core-runtime", "go test ./modules/epistemics/domain", "go test ./modules/capability/domain", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,41 @@
{
"task_id": "DEV-000094",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "39f9081",
"risk_tier": "E",
"intent": "Preserve the centrally derived informational intent from Interaction through Core and the local cognitive gateway, so local conversation and provider-recovery responses are selected by meaning rather than duplicated raw-text checks.",
"requirement_ids": ["ADR-0018", "FD-001", "FD-009", "FD-015", "INV-EPI-001", "Local conversation remains useful during provider loss", "Semantic intent is not reparsed by each runtime"],
"allowed_change_scope": {
"modules": ["interaction", "core-runtime", "creator-web", "cognitive-worker", "development-governance"],
"paths": [
"contracts/interaction/types.go",
"contracts/cognitive/types.go",
"modules/interaction/domain/chat.go",
"modules/interaction/domain/chat_test.go",
"modules/interaction/domain/semantics.go",
"cmd/core-runtime/main.go",
"cmd/core-runtime/main_test.go",
"cmd/core-runtime/cognitive_interpretation.go",
"cmd/creator-web/server.go",
"cmd/creator-web/chat_test.go",
"cmd/cognitive-worker/gateway.go",
"cmd/cognitive-worker/gateway_registry_test.go",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000094-semantic-local-recovery-routing.json"
]
},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 1, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["ADR-0018 Exploration profile", "existing Creator chat and gateway contracts"], "may_change": ["interaction ChatMessageResponse informational intent field", "cognitive TaskMeaning informational intent field"]},
"semantic_owner": "interaction for informational intent; Cognitive Gateway only consumes the typed value for an ephemeral local answer",
"canonical_state": ["existing Interaction utterance interpretation only"],
"state_transitions": ["utterance -> one informational intent classification", "typed intent -> Core read-only answer or local advisory response", "provider loss -> recovery-oriented local response without an external action"],
"side_effects_and_privileges": ["No external mutation", "No new privilege", "No provider invocation outside the Cognitive Gateway", "No credential or secret rendering"],
"persistence_and_migrations": ["No schema migration; informational intent is an ephemeral response field derived from existing utterance semantics"],
"failure_retry_restart": ["Gateway loss preserves Core's typed read-only replies", "local provider loss returns a typed built-in response", "no automatic external-provider retry is introduced"],
"assumptions": ["A bounded deterministic classifier is allowed as the Interaction-owned fallback when richer cognition is absent", "the first delivery only provides read-only conversation and recovery guidance; it does not create a Work task or action proposal automatically", "docker-test remains prohibited"],
"acceptance_scenarios": ["Model inventory, capability inventory and cognitive-channel/recovery questions receive a single typed classification", "Core no longer reparses those informational requests from raw text", "Creator passes the typed intent to the local gateway", "gateway built-in advisory does not branch on raw message keywords", "ordinary local fallback remains meaningful when a local provider fails"],
"required_tests": ["go test ./modules/interaction/domain", "go test ./cmd/core-runtime", "go test ./cmd/creator-web", "go test ./cmd/cognitive-worker", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,26 @@
{
"task_id": "DEV-000095",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "8c2989f",
"risk_tier": "E",
"intent": "Render actual Domovoy build revision and update time on every public chat route instead of exposing unresolved template markers.",
"requirement_ids": ["Chat displays version and update time", "Public build metadata is truthful"],
"allowed_change_scope": {
"modules": ["creator-web", "development-governance"],
"paths": ["cmd/creator-web/server.go", "cmd/creator-web/chat_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000095-chat-build-metadata-rendering.json"]
},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["existing Creator Web HTTP surface"], "may_change": []},
"semantic_owner": "none; presentation-only build metadata",
"canonical_state": [],
"state_transitions": [],
"side_effects_and_privileges": ["No mutation, provider call, credential access, or new privilege"],
"persistence_and_migrations": ["None"],
"failure_retry_restart": ["When build metadata environment values are absent, a safe development revision and startup timestamp remain renderable; no raw markers are sent"],
"assumptions": ["The existing container revision environment value is the deployment revision", "docker-test remains prohibited"],
"acceptance_scenarios": ["/chat renders revision and time instead of placeholders", "/chat.html cannot bypass metadata substitution", "public page does not expose internal IDs or unresolved build placeholders"],
"required_tests": ["go test ./cmd/creator-web", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,26 @@
{
"task_id": "DEV-000096",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "305a55a",
"risk_tier": "E",
"intent": "Record usage observations for the built-in Domovoy cognitive provider so routing and learning can account for local fallback success, latency, usage, and failure.",
"requirement_ids": ["Built-in cognitive calls are observable", "Telemetry failure does not block local chat"],
"allowed_change_scope": {
"modules": ["cognitive-worker", "development-governance"],
"paths": ["cmd/cognitive-worker/gateway.go", "cmd/cognitive-worker/interpretation.go", "cmd/cognitive-worker/gateway_registry_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000096-builtin-cognitive-usage-telemetry.json"]
},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "cmd/cognitive-worker/interpretation_live_trace_test.go"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["existing CognitiveUsageObservation and bindingResolver.Observe"], "may_change": []},
"semantic_owner": "capability registry owns cognitive usage observations; cognitive-worker is an observer",
"canonical_state": [],
"state_transitions": [],
"side_effects_and_privileges": ["Adds only an existing usage-observation side effect for built-in provider calls; no external mutation or credential access"],
"persistence_and_migrations": ["No schema or migration changes; uses existing observation endpoint and repository"],
"failure_retry_restart": ["Observation errors remain non-fatal; provider response and deterministic fallback behavior remain unchanged"],
"assumptions": ["The built-in provider identity remains the existing stable local lifeboat identity", "docker-test remains prohibited"],
"acceptance_scenarios": ["Successful built-in advisory call produces an observation", "Failed built-in advisory call produces a non-success observation", "Built-in interpretation call is observed", "Chat behavior remains available when observation persistence fails", "No secrets are included in observations"],
"required_tests": ["go test ./cmd/cognitive-worker", "go test ./...", "go vet ./...", "devctl preflight", "devctl check-diff", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,34 @@
{
"task_id": "DEV-000098",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "b439a813e5932b1774cb3b6c2364c6cac3891314",
"risk_tier": "E",
"intent": "Implement Governance Sentinel V2 so task packets are fully validated and required governance evidence is enforced before promotion.",
"requirement_ids": ["WP-000", "CONSTITUTION-1.3.27", "CONSTITUTION-1.3.28"],
"allowed_change_scope": {
"modules": [],
"paths": ["cmd/devctl/**", ".gitea/workflows/ci.yml", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000098-governance-sentinel-v2.json"]
},
"forbidden_change_scope": {
"paths": ["architecture/ARCHITECTURE_CONSTITUTION.md", "architecture/CORE_SEMANTIC_MODEL_V1.md", "security/**", "database/**", "deploy/**", "modules/**", "contracts/**", "adapters/**", "platform/**"]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_database_owner_roles": 0
},
"contracts": {"consume": ["Architecture registry", "Accepted ADR files", "Codex Task Packet JSON", "Git repository state"], "may_change": []},
"semantic_owner": "none-architecture-guardrail",
"canonical_state": [],
"state_transitions": ["Task packet validation returns pass or typed gap"],
"side_effects_and_privileges": ["Reads repository metadata and required test commands; no external mutation or secret access"],
"persistence_and_migrations": ["No database schema or migration changes"],
"failure_retry_restart": ["Malformed, incomplete, conflicting or unverifiable packets fail closed; checks are deterministic and rerunnable"],
"assumptions": ["ADR-0018 is the current accepted architecture revision", "Independent verification remains a CI/review responsibility", "docker-test remains prohibited"],
"acceptance_scenarios": ["Full packet fields are strictly parsed", "Allowed and forbidden scope overlap is rejected", "Architecture revision existence and base ancestry are checked", "Architecture deltas, dependencies, contracts, privileges and required tests are checked", "Tier E does not bypass validation", "CI executes the required governance checks"],
"required_tests": ["go test ./cmd/devctl", "go run ./cmd/devctl/independent", "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", "go vet ./...", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT", "BLOCKED_BY_MISSING_CAPABILITY"]
}
@@ -0,0 +1,26 @@
{
"task_id": "DEV-000099",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "ff33a62",
"risk_tier": "E",
"intent": "Consolidate active task lineage, project status and architecture registry mirrors after the audit without changing runtime behavior or canonical ownership.",
"requirement_ids": ["WP-001", "ADR-0017", "Cold-agent governance reproducibility"],
"allowed_change_scope": {
"modules": [],
"paths": ["architecture/architecture-registry.yaml", "architecture/architecture-registry.json", "architecture/architecture-registry.md", "docs/PROJECT_STATUS.md", "docs/GOVERNANCE_LINEAGE_DECISION_2026-08-22.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000099-governance-consolidation.json", "cmd/devctl/**"]
},
"forbidden_change_scope": {"paths": ["architecture/ARCHITECTURE_CONSTITUTION.md", "architecture/adrs/**", "security/**", "database/**", "deploy/**", "modules/**", "contracts/**", "adapters/**", ".gitea/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["ADR-0017 ownership decision", "Architecture registry YAML/JSON", "Active task pointer", "Project status evidence"], "may_change": []},
"semantic_owner": "none-governance-projection",
"canonical_state": [],
"state_transitions": ["DEV-000098 completed -> DEV-000099 active -> DEV-000099 completed"],
"side_effects_and_privileges": ["Repository documentation and governance metadata only; no runtime or production mutation"],
"persistence_and_migrations": ["No database schema or migration changes"],
"failure_retry_restart": ["Historical checkpoints remain append-only; ambiguous lineage is recorded as a decision rather than silently rewritten"],
"assumptions": ["ADR-0017 remains accepted", "YAML and JSON registry are semantically equivalent authority mirrors", "docker-test remains prohibited"],
"acceptance_scenarios": ["Active pointer names the current packet and revision", "DEV-000073 stale lineage is explicitly resolved", "PROJECT_STATUS starts with current audit/WP-001 evidence while preserving historical checkpoints", "YAML/JSON/human registry agree on ADR-0017 ownership", "Cold agent can reconstruct current ownership and task ancestry from repository files"],
"required_tests": ["go test ./cmd/devctl", "go run ./cmd/devctl/independent", "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", "go vet ./...", "git diff --check", "registry YAML/JSON parity"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT", "BLOCKED_BY_MISSING_CAPABILITY"]
}
@@ -0,0 +1,22 @@
{
"task_id": "DEV-000100",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "8687c9b",
"risk_tier": "E",
"intent": "Repair production backup and restore selection so only complete timestamped backups are eligible, and make provenance drift degrade readiness.",
"requirement_ids": ["WP-002", "INV-CONT-001", "Production recovery must be reversible"],
"allowed_change_scope": {"modules": [], "paths": ["deploy/verify-latest-postgres-backup.sh", "deploy/windows/Sync-PostgresBackupToNas.ps1", "deploy/windows/Test-DomovoyProductionHealth.ps1", "tests/production_recovery/**", "docs/DEPLOYMENT.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000100-production-recovery-repair.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "contracts/**", "modules/**", "cmd/**", "adapters/**", "platform/**", ".gitea/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["Production backup directory contract", "Deployment manifest evidence", "Production health projection"], "may_change": []},
"semantic_owner": "none-operational-recovery", "canonical_state": [],
"state_transitions": ["ineligible backup -> ignored", "verified backup -> restore candidate", "provenance/backup drift -> degraded health"],
"side_effects_and_privileges": ["Local script changes are read-only until explicit production verification; no secret bytes are logged"],
"persistence_and_migrations": ["No database schema or migration changes"],
"failure_retry_restart": ["Partial/temp/pre-* directories remain untouched and are ignored; restore checks use isolated temporary PostgreSQL only"],
"assumptions": ["Production host is docker.cin.su via docker-prod/docker SSH configuration", "docker-test remains prohibited"],
"acceptance_scenarios": ["pre-*, partial and temp directories are ignored", "latest timestamp directory is selected deterministically", "missing/hash-invalid backup is rejected", "production health is degraded on backup or deployment provenance drift", "restore verification remains isolated and cleanup-safe"],
"required_tests": ["shell fixture tests", "PowerShell parser", "go run ./cmd/devctl/independent", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go test ./...", "go vet ./...", "git diff --check", "production read-only health evidence"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,22 @@
{
"task_id": "DEV-000101",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "15c9517",
"risk_tier": "E",
"intent": "Add dependency-aware readiness projections with typed degraded reasons for core, continuity, commit, cognitive and lifeboat runtimes without changing canonical ownership.",
"requirement_ids": ["WP-003", "Typed degraded dependency reasons", "Frontend health distinct from product readiness"],
"allowed_change_scope": {"modules": ["development-governance"], "paths": ["cmd/core-runtime/**", "cmd/continuity-runtime/**", "cmd/commit-runtime/**", "cmd/cognitive-worker/**", "runtime/health/**", "tests/readiness/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000101-dependency-aware-readiness.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "modules/**", "adapters/**", ".gitea/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["Existing runtime health handlers", "Existing owner-local repository health", "Existing cognitive capability health"], "may_change": []},
"semantic_owner": "none-readiness-projection", "canonical_state": [],
"state_transitions": ["dependency available -> ready", "dependency unavailable/unknown -> typed degraded readiness"],
"side_effects_and_privileges": ["Read-only dependency probes; no canonical mutation, external mutation or credential expansion"],
"persistence_and_migrations": ["No database schema or migration changes"],
"failure_retry_restart": ["Probe timeout is UNKNOWN/DEGRADED, not false healthy; recovery returns ready on next probe"],
"assumptions": ["Existing /healthz endpoints remain backward compatible", "Frontend health and product readiness are separate projections", "docker-test remains prohibited"],
"acceptance_scenarios": ["Stopping each dependency yields the correct typed degraded reason", "DB owner connectivity is represented without cross-owner writes", "cognitive binding/model freshness and lifeboat availability are visible", "recovery returns readiness without restart", "health endpoint does not claim product readiness"],
"required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl/independent", "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", "readiness dependency-stop integration tests"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000102",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "3f2b008",
"risk_tier": "E",
"intent": "Promote the dependency-aware readiness implementation to a controlled production canary with explicit rollback evidence, without changing deployment topology or source contracts.",
"requirement_ids": ["WP-003", "Controlled canary promotion", "Production rollback evidence"],
"allowed_change_scope": {"modules": ["deployment-governance"], "paths": ["dev/ACTIVE_TASK.json", "dev/tasks/DEV-000102-readiness-canary-promotion.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "modules/**", "adapters/**", ".gitea/**", "cmd/**", "runtime/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["Existing production compose topology", "Existing runtime health endpoints", "Existing production backup and deployment verification"], "may_change": []},
"semantic_owner": "none-deployment-canary",
"canonical_state": [],
"state_transitions": ["built candidate -> canary verified", "canary failure -> rollback to recorded production revision"],
"side_effects_and_privileges": ["Controlled production container replacement only after build/config verification; no schema, credential, or external data mutation"],
"persistence_and_migrations": ["No database schema or migration changes"],
"failure_retry_restart": ["Rollback target is production revision b439a813e5932b1774cb3b6c2364c6cac3891314 and recorded core image digest sha256:4fb09995df8efbbb02359e30171f593f9cf9b90489d4ac0bf85a62ed2504ac92"],
"assumptions": ["Production control plane is docker-prod/docker.cin.su only", "Canary is limited to readiness-capable runtime services", "Existing secrets and persistent volumes remain unchanged"],
"acceptance_scenarios": ["Candidate images build from 3f2b008", "Compose configuration validates without secret disclosure", "Canary readiness returns HTTP 200 with dependency checks", "Existing health and HTTPS checks remain healthy", "Rollback command and pre-canary image identity are recorded"],
"required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl independent", "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", "docker compose config on docker-prod", "production canary readiness and rollback verification"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000103",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "6fb9184",
"risk_tier": "E",
"intent": "Prepare the proposed ADR-0019 Truth V1 boundary for review without implementing runtime, schema, connector, source or authority changes.",
"requirement_ids": ["WP-100", "Observation/Claim/BeliefAssessment", "Source provenance freshness conflict"],
"allowed_change_scope": {"modules": ["epistemics-governance"], "paths": ["docs/proposals/ADR-0019-epistemics-truth-v1.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000103-epistemics-truth-v1-proposal.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "contracts/**", "database/**", "migrations/**", "modules/**", "cmd/**", "runtime/**", "adapters/**", "deploy/**", ".gitea/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["ADR-0016 Epistemics experience/reflection ownership", "ADR-0017 typed provenance references", "Core semantic model ownership boundaries"], "may_change": ["Proposed ADR-0019 only; no accepted contract changes"]},
"semantic_owner": "epistemics-proposal-only",
"canonical_state": [],
"state_transitions": ["observation/source evidence -> claim assessment", "assessment -> supported/corroborated/conflicted/stale/retracted/unknown", "correction -> superseding assessment"],
"side_effects_and_privileges": ["No runtime side effects, external mutation, connector access, credential access or authority expansion"],
"persistence_and_migrations": ["No schema, migration or retention job changes"],
"failure_retry_restart": ["Proposal failure leaves no canonical state; accepted implementation must preserve append-only evidence and rebuildable projections"],
"assumptions": ["Proposal is reviewed before acceptance", "Epistemics remains the only proposed truth-evidence owner", "Secrets and unapproved personal data are fail-closed"],
"acceptance_scenarios": ["Owner boundaries and typed references are explicit", "Unknown, assumed, stale and conflicted states cannot silently become facts", "Correction and supersession preserve history", "Freshness and coverage are separate from confidence", "No implementation or architecture registry delta is introduced"],
"required_tests": ["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", "go run ./cmd/devctl/independent", "git diff --check", "proposal scope and required-section review"],
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}

Some files were not shown because too many files have changed in this diff Show More