Compare commits

...
Author SHA1 Message Date
m 7978b495d1 feat(reflection): expose worker runtime status
CI / test (push) Successful in 48s
CI / test (pull_request) Successful in 46s
2026-08-23 19:42:07 +03:00
m d73d4fc118 Merge pull request 'feat(capability): consume qualification evidence in selection' (#220) from codex/dev-000243-qualification-selection-evidence into main
CI / test (push) Successful in 1m8s
feat(capability): consume qualification evidence in selection
2026-08-23 16:30:23 +00:00
m c81e2269e1 feat(capability): consume qualification evidence in selection
CI / test (push) Successful in 1m16s
CI / test (pull_request) Successful in 1m26s
2026-08-23 19:28:13 +03:00
m 47327cc6d8 Merge pull request 'feat(capability): persist model qualification observations' (#219) from codex/dev-000242-durable-qualification-observations into main
CI / test (push) Successful in 1m16s
feat(capability): persist model qualification observations
2026-08-23 16:22:58 +00:00
m d0ede46352 feat(capability): persist model qualification observations
CI / test (push) Successful in 1m14s
CI / test (pull_request) Successful in 1m15s
2026-08-23 19:21:04 +03:00
m e4ba1cb5fa Merge pull request 'docs(fleet): record medium and gpu qualification' (#218) from codex/dev-000241-fleet-qualification-evidence into main
CI / test (push) Successful in 1m15s
docs(fleet): record medium and gpu qualification
2026-08-23 16:13:58 +00:00
m 90104d2553 docs(fleet): record medium and gpu qualification
CI / test (push) Successful in 1m12s
CI / test (pull_request) Successful in 1m7s
2026-08-23 19:11:41 +03:00
m ca0d002073 Merge pull request 'docs(fleet): record CPU to lifeboat failover' (#217) from codex/dev-000240-cpu-lifeboat-failover-evidence into main
CI / test (push) Successful in 52s
docs(fleet): record CPU to lifeboat failover
2026-08-23 16:07:31 +00:00
m 20db36f52e docs(fleet): record CPU to lifeboat failover
CI / test (push) Successful in 51s
CI / test (pull_request) Successful in 1m11s
2026-08-23 19:05:49 +03:00
m 383d4c6e4f Merge pull request 'fix(cognitive): resolve providers by stable instance identity' (#216) from codex/dev-000239-stable-provider-resolution into main
CI / test (push) Successful in 50s
fix(cognitive): resolve providers by stable instance identity
2026-08-23 16:03:07 +00:00
m f3ddb45fe5 fix(cognitive): resolve providers by stable instance identity
CI / test (push) Successful in 1m12s
CI / test (pull_request) Successful in 1m12s
2026-08-23 19:01:18 +03:00
m 6061eb5c13 Merge pull request 'docs(runtime): record local fallback gateway evidence' (#215) from codex/dev-000238-local-fallback-evidence into main
CI / test (push) Successful in 1m16s
docs(runtime): record local fallback gateway evidence
2026-08-23 15:54:47 +00:00
m 0e46a64702 docs(runtime): record local fallback gateway evidence
CI / test (push) Successful in 1m15s
CI / test (pull_request) Successful in 47s
2026-08-23 18:52:27 +03:00
m 0609561dfe Merge pull request 'docs(ui): record authenticated console section smoke' (#214) from codex/dev-000237-ui-authenticated-section-smoke into main
CI / test (push) Successful in 1m0s
docs(ui): record authenticated console section smoke
2026-08-23 15:46:29 +00:00
m 1b88a35b82 docs(ui): record authenticated console section smoke
CI / test (push) Successful in 1m13s
CI / test (pull_request) Successful in 1m14s
2026-08-23 18:44:36 +03:00
m be01b6b883 Merge pull request 'fix(cognitive): satisfy qualification advisory contract' (#213) from codex/dev-000236-qualification-locale-fix into main
CI / test (push) Successful in 1m13s
fix(cognitive): satisfy qualification advisory contract
2026-08-23 15:37:10 +00:00
m deaddbca12 fix(cognitive): satisfy qualification advisory contract
CI / test (push) Successful in 1m19s
CI / test (pull_request) Successful in 1m8s
2026-08-23 18:34:45 +03:00
m b20611590a Merge pull request 'feat(cognitive): add bounded model qualification probe' (#212) from codex/dev-000235-model-qualification-probe into main
CI / test (push) Successful in 51s
feat(cognitive): add bounded model qualification probe
2026-08-23 15:30:02 +00:00
m d4972afd11 feat(cognitive): add bounded model qualification probe
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 45s
2026-08-23 18:27:22 +03:00
m 60e8292c1d Merge pull request 'feat(work): persist orchestration snapshot for replan' (#211) from codex/dev-000234-orchestration-snapshot-replan into main
CI / test (push) Successful in 1m15s
feat(work): persist orchestration snapshot for replan
2026-08-23 15:16:43 +00:00
m da251e1aa8 feat(work): persist orchestration snapshot for replan
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m11s
2026-08-23 18:13:07 +03:00
m eac39a3fd4 Merge pull request 'feat(work): require verified graph result fan-in' (#210) from codex/dev-000233-graph-result-fanin into main
CI / test (push) Successful in 1m0s
feat(work): require verified graph result fan-in
2026-08-23 15:07:44 +00:00
m 1f8af98f31 feat(work): require verified graph result fan-in
CI / test (push) Successful in 50s
CI / test (pull_request) Successful in 44s
2026-08-23 18:05:23 +03:00
m ee16103e03 Merge pull request 'feat(work): materialize durable solution waves' (#209) from codex/dev-000231-solution-orchestrator into main
CI / test (push) Successful in 1m11s
feat(work): materialize durable solution waves
2026-08-23 13:59:42 +00:00
m 09d53adfd1 feat(work): materialize durable solution waves
CI / test (push) Successful in 1m7s
CI / test (pull_request) Successful in 44s
2026-08-23 16:56:58 +03:00
m 89694047b1 Merge pull request 'feat(work): add persistent solution search orchestration policy' (#208) from codex/dev-000231-solution-orchestrator into main
CI / test (push) Successful in 1m20s
feat(work): add persistent solution search orchestration policy
2026-08-23 13:48:58 +00:00
m 23e0525c08 feat(work): add persistent solution search orchestration policy
CI / test (push) Successful in 1m9s
CI / test (pull_request) Successful in 58s
2026-08-23 16:45:58 +03:00
m 45874f2186 Merge pull request 'feat: expose governed development gate controller' (#207) from codex/dev-000230-development-gate-controller into main
CI / test (push) Successful in 1m16s
feat: expose governed development gate controller
2026-08-23 13:40:04 +00:00
m ae5848d799 feat: expose governed development gate controller
CI / test (push) Successful in 1m10s
CI / test (pull_request) Successful in 1m10s
2026-08-23 16:36:48 +03:00
m ec79f19a79 Merge pull request 'feat: connect creator research to truth runtime' (#206) from codex/dev-000229-research-bridge into main
CI / test (push) Successful in 1m53s
feat: connect creator research to truth runtime
2026-08-23 13:30:35 +00:00
50 changed files with 1555 additions and 31 deletions
@@ -505,6 +505,14 @@ func (r *CapabilityRepository) loadCognitiveEvidence(ctx context.Context, value
if err == nil {
value.Performance = &performance
}
var qualification capability.ModelQualificationObservation
err = r.pool.QueryRow(ctx, `SELECT observation_id,provider_instance_id,provider_model_id,status,finish_reason,latency_millis,output_observed,quality_assessed,category,observed_at_unix_millis FROM capability.cognitive_model_qualification_observations WHERE provider_instance_id=$1 AND provider_model_id=$2 ORDER BY observed_at_unix_millis DESC LIMIT 1`, value.Provider.ID, value.Model.ProviderModelID).Scan(&qualification.ID, &qualification.ProviderInstanceID, &qualification.ProviderModelID, &qualification.Status, &qualification.FinishReason, &qualification.LatencyMillis, &qualification.OutputObserved, &qualification.QualityAssessed, &qualification.Category, &qualification.ObservedAtUnixMillis)
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return err
}
if err == nil {
value.Qualification = &qualification
}
if profileID != "" {
var taskEvaluation capability.TaskEvaluation
err = r.pool.QueryRow(ctx, `SELECT evaluation_id,task_profile_id,binding_id,binding_revision,capability_id,result,quality_score_millis,reliability_score_millis,latency_score_millis,overall_score_millis,critical_failure_millis,benchmark_version,artifact_id,observed_at_unix_millis FROM capability.cognitive_task_evaluations WHERE task_profile_id=$1 AND binding_id=$2 AND binding_revision=$3 ORDER BY observed_at_unix_millis DESC LIMIT 1`, profileID, value.Binding.ID, value.Binding.Revision).Scan(&taskEvaluation.ID, &taskEvaluation.ProfileID, &taskEvaluation.BindingID, &taskEvaluation.BindingRevision, &taskEvaluation.CapabilityID, &taskEvaluation.Result, &taskEvaluation.QualityScoreMillis, &taskEvaluation.ReliabilityScoreMillis, &taskEvaluation.LatencyScoreMillis, &taskEvaluation.OverallScoreMillis, &taskEvaluation.CriticalFailureMillis, &taskEvaluation.BenchmarkVersion, &taskEvaluation.ArtifactID, &taskEvaluation.ObservedAtUnixMillis)
@@ -633,3 +641,8 @@ func (r *CapabilityRepository) SaveTaskEvaluation(ctx context.Context, value cap
_, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_task_evaluations (evaluation_id,task_profile_id,binding_id,binding_revision,capability_id,result,quality_score_millis,reliability_score_millis,latency_score_millis,overall_score_millis,critical_failure_millis,benchmark_version,artifact_id,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT DO NOTHING`, value.ID, value.ProfileID, value.BindingID, value.BindingRevision, value.CapabilityID, value.Result, value.QualityScoreMillis, value.ReliabilityScoreMillis, value.LatencyScoreMillis, value.OverallScoreMillis, value.CriticalFailureMillis, value.BenchmarkVersion, value.ArtifactID, value.ObservedAtUnixMillis)
return err
}
func (r *CapabilityRepository) SaveModelQualificationObservation(ctx context.Context, value capability.ModelQualificationObservation) error {
_, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_model_qualification_observations (observation_id,provider_instance_id,provider_model_id,status,finish_reason,latency_millis,output_observed,quality_assessed,category,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) ON CONFLICT DO NOTHING`, value.ID, value.ProviderInstanceID, value.ProviderModelID, value.Status, value.FinishReason, value.LatencyMillis, value.OutputObserved, value.QualityAssessed, value.Category, value.ObservedAtUnixMillis)
return err
}
+10 -2
View File
@@ -263,7 +263,7 @@ func (r *WorkRepository) SaveWorkGraph(ctx context.Context, graph work.WorkGraph
return err
}
defer tx.Rollback(ctx)
if _, err := tx.Exec(ctx, `INSERT INTO work.graphs(graph_id,revision,state,max_parallel,max_attempts,parent_graph_id,parent_graph_revision,trigger_evidence_ref) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(graph_id,revision) DO NOTHING`, graph.ID, graph.Revision, graph.State, graph.Budget.MaxParallel, graph.Budget.MaxAttempts, graph.ParentGraphID, graph.ParentGraphRevision, graph.TriggerEvidenceRef); err != nil {
if _, err := tx.Exec(ctx, `INSERT INTO work.graphs(graph_id,revision,state,max_parallel,max_attempts,parent_graph_id,parent_graph_revision,trigger_evidence_ref,orchestration_bound,orchestration_goal_id,orchestration_goal_state,orchestration_search_round,orchestration_max_round,orchestration_cancel_requested,orchestration_verification_ready,orchestration_unresolved) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) ON CONFLICT(graph_id,revision) DO NOTHING`, graph.ID, graph.Revision, graph.State, graph.Budget.MaxParallel, graph.Budget.MaxAttempts, graph.ParentGraphID, graph.ParentGraphRevision, graph.TriggerEvidenceRef, graph.SearchInputBound, graph.SearchInput.GoalID, graph.SearchInput.GoalState, graph.SearchInput.SearchRound, graph.SearchInput.MaxRound, graph.SearchInput.CancelRequested, graph.SearchInput.VerificationReady, graph.SearchInput.Unresolved); err != nil {
return err
}
for _, node := range graph.Nodes {
@@ -285,7 +285,7 @@ func (r *WorkRepository) SaveWorkGraph(ctx context.Context, graph work.WorkGraph
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,parent_graph_id,parent_graph_revision,trigger_evidence_ref 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, &graph.ParentGraphID, &graph.ParentGraphRevision, &graph.TriggerEvidenceRef)
err := r.pool.QueryRow(ctx, `SELECT graph_id,revision,state,max_parallel,max_attempts,parent_graph_id,parent_graph_revision,trigger_evidence_ref,orchestration_bound,orchestration_goal_id,orchestration_goal_state,orchestration_search_round,orchestration_max_round,orchestration_cancel_requested,orchestration_verification_ready,orchestration_unresolved 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, &graph.ParentGraphID, &graph.ParentGraphRevision, &graph.TriggerEvidenceRef, &graph.SearchInputBound, &graph.SearchInput.GoalID, &graph.SearchInput.GoalState, &graph.SearchInput.SearchRound, &graph.SearchInput.MaxRound, &graph.SearchInput.CancelRequested, &graph.SearchInput.VerificationReady, &graph.SearchInput.Unresolved)
if err != nil {
return work.WorkGraph{}, fmt.Errorf("load graph: %w", err)
}
@@ -351,6 +351,14 @@ func (r *WorkRepository) SaveGraphSynthesis(ctx context.Context, conflict work.C
return tx.Commit(ctx)
}
func (r *WorkRepository) SaveGraphResultCandidate(ctx context.Context, candidate work.ResultCandidate) error {
if candidate.ID == "" || candidate.GraphID == "" || candidate.GraphRevision < 1 || candidate.NodeID == "" || candidate.AuthorRunID == "" || candidate.EvidenceSummary == "" || candidate.Summary == "" {
return errors.New("invalid graph result candidate")
}
_, err := r.pool.Exec(ctx, `INSERT INTO work.graph_result_candidates(result_candidate_id,graph_id,graph_revision,node_id,author_run_id,artifact_refs,evidence_summary,summary) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(result_candidate_id) DO NOTHING`, candidate.ID, candidate.GraphID, candidate.GraphRevision, candidate.NodeID, candidate.AuthorRunID, candidate.ArtifactRefs, candidate.EvidenceSummary, candidate.Summary)
return err
}
func (r *WorkRepository) RecoverStaleClaims(ctx context.Context, nowUnixMillis, staleAfterMillis int64) (int64, error) {
tx, err := r.pool.Begin(ctx)
if err != nil {
+10 -1
View File
@@ -298,12 +298,21 @@ func fallbackEligible(reason cognitive.FinishReason) bool {
}
func (g cognitiveGateway) provider(selected capability.ResolvedCognitiveBinding) advisoryProvider {
var byStableID advisoryProvider
for _, registration := range g.providers[selected.Provider.Kind] {
if registration.EndpointRef == selected.Provider.EndpointRef && registration.CredentialRef == selected.Provider.CredentialRef {
return registration.Provider
}
// Capability revisions can legitimately retain an older credential
// reference while the protected runtime has rotated or reloaded the
// current credential. ProviderInstanceID is the stable owner identity;
// use it after exact metadata matching so stale non-secret metadata does
// not force an unnecessary builtin fallback.
if byStableID == nil && registration.ProviderInstanceID == selected.Provider.ID && registration.Kind == selected.Provider.Kind {
byStableID = registration.Provider
}
}
return nil
return byStableID
}
func writeGatewayResponse(w http.ResponseWriter, response cognitive.GatewayAdvisoryResponse) {
@@ -88,6 +88,19 @@ func TestActiveRegistryBindingReachesProviderAdapter(t *testing.T) {
}
}
func TestProviderUsesStableInstanceWhenCredentialMetadataIsStale(t *testing.T) {
provider := &stubAdvisoryProvider{}
gateway := cognitiveGateway{providers: map[capability.ProviderKind][]providerRegistration{
"provider-kind-1": {{ProviderInstanceID: "provider-1", Kind: "provider-kind-1", EndpointRef: "current-endpoint", CredentialRef: "current-credential", Provider: provider}},
}}
selected := gatewaySelection()
selected.Provider.EndpointRef = "old-endpoint"
selected.Provider.CredentialRef = ""
if got := gateway.provider(selected); got != provider {
t.Fatalf("stable provider identity was not used for stale metadata")
}
}
func TestCanaryFlagIsForwardedToCapabilityResolver(t *testing.T) {
registry := &registryStub{err: errors.New("stop after resolve")}
gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{}, now: time.Now, allowCanary: true}
+1
View File
@@ -169,6 +169,7 @@ func main() {
mux.HandleFunc("POST /v1/task-evaluations/execute", gateway.taskEvaluationHandler)
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)
mux.Handle("POST /v1/provider-discovery/{provider}/qualify", modelQualificationHandler(discoveries, string(envOr("COGNITIVE_OWNER_PRINCIPAL_ID", "domovoy-creator-principal")), httpQualificationObserver{resolver: resolver}))
mux.HandleFunc("GET /v1/provider-discovery/status", discovery.statusHandler)
refreshProviderDiscoveries(resolver, discovery)
log.Printf("cognitive-worker listening on %s", addr)
+167
View File
@@ -0,0 +1,167 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"slices"
"sync"
"time"
"git.cin.su/m/Assistant/adapters/models"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/platform/ids"
)
type qualificationObserver interface {
ObserveQualification(context.Context, capability.ModelQualificationObservation) error
}
type httpQualificationObserver struct{ resolver *httpBindingResolver }
func (o httpQualificationObserver) ObserveQualification(ctx context.Context, value capability.ModelQualificationObservation) error {
if o.resolver == nil {
return fmt.Errorf("qualification observer resolver is nil")
}
return o.resolver.post(ctx, "/v1/cognitive-registry/model-qualification-observations", value, nil)
}
type modelQualificationRequest struct {
Prompt string `json:"prompt"`
MaxOutputBytes int `json:"max_output_bytes"`
ModelIDs []capability.ProviderModelID `json:"model_ids,omitempty"`
}
type modelQualificationResult struct {
ProviderModelID capability.ProviderModelID `json:"provider_model_id"`
Status string `json:"status"`
FinishReason cognitive.FinishReason `json:"finish_reason,omitempty"`
LatencyMillis int64 `json:"latency_millis"`
OutputObserved bool `json:"output_observed"`
QualityAssessed bool `json:"quality_assessed"`
Category capability.ModelRatingCategory `json:"category"`
Error string `json:"error,omitempty"`
}
type modelQualificationResponse struct {
Provider capability.ProviderInstanceID `json:"provider"`
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
Results []modelQualificationResult `json:"results"`
}
func modelQualificationHandler(providers map[string]providerRegistration, owner string, observers ...qualificationObserver) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("provider")
registration, ok := providers[name]
if !ok || registration.Discovery == nil || registration.Provider == nil {
http.Error(w, "provider qualification not found", http.StatusNotFound)
return
}
var request modelQualificationRequest
if !decodeJSON(w, r, 16<<10, &request) || request.Prompt == "" {
return
}
if request.MaxOutputBytes < 1 {
request.MaxOutputBytes = 512
}
if request.MaxOutputBytes > 4096 {
http.Error(w, "max_output_bytes exceeds qualification bound", http.StatusBadRequest)
return
}
modelsFound, probe := registration.Discovery.Discover(r.Context())
if probe.Health != capability.HealthAvailable || len(modelsFound) == 0 {
http.Error(w, "provider unavailable for qualification", http.StatusServiceUnavailable)
return
}
if len(request.ModelIDs) > 0 {
modelsFound = slices.DeleteFunc(modelsFound, func(model models.DiscoveredModel) bool {
return !slices.Contains(request.ModelIDs, model.ProviderModelID)
})
}
if len(modelsFound) == 0 {
http.Error(w, "requested model not discovered", http.StatusNotFound)
return
}
if len(modelsFound) > 16 {
modelsFound = modelsFound[:16]
}
results := qualifyModels(r.Context(), registration, owner, request.Prompt, request.MaxOutputBytes, modelsFound)
if len(observers) > 0 && observers[0] != nil {
if err := persistQualificationResults(r.Context(), observers[0], registration, results); err != nil {
http.Error(w, "persist model qualification observation", http.StatusBadGateway)
return
}
}
w.Header().Set("Content-Type", "application/json")
_ = jsonEncode(w, modelQualificationResponse{Provider: registration.ProviderInstanceID, ObservedAtUnixMillis: time.Now().UnixMilli(), Results: results})
})
}
func qualifyModels(ctx context.Context, registration providerRegistration, owner, prompt string, maxOutputBytes int, discovered []models.DiscoveredModel) []modelQualificationResult {
results := make([]modelQualificationResult, len(discovered))
var wg sync.WaitGroup
for index, model := range discovered {
wg.Add(1)
go func(index int, model models.DiscoveredModel) {
defer wg.Done()
started := time.Now()
request := cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(fmt.Sprintf("qualification:%s:%d", model.ProviderModelID, started.UnixNano())), ActorID: identity.ActorID("qualification-runtime"), PrincipalID: identity.PrincipalID(owner), ScopeID: authority.ScopeID("qualification-scope"), Message: prompt, Locale: "en-US", MaxOutputBytes: maxOutputBytes, ProviderBindingID: "qualification-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: registration.ProviderInstanceID, ModelDescriptorID: capability.ModelDescriptorID("qualification-" + string(model.ProviderModelID)), ProviderModelID: model.ProviderModelID}
response, err := registration.Provider.Advise(ctx, request)
result := modelQualificationResult{ProviderModelID: model.ProviderModelID, LatencyMillis: time.Since(started).Milliseconds(), QualityAssessed: false, Category: capability.ModelRatingUnrated}
if err != nil {
result.Status = "UNAVAILABLE"
result.FinishReason = cognitive.FinishProviderUnavailable
result.Error = err.Error()
results[index] = result
return
}
result.FinishReason = response.FinishReason
result.OutputObserved = response.AdvisoryText != ""
if response.FinishReason == cognitive.FinishCompleted && result.OutputObserved {
result.Status = "REACHABLE"
} else {
result.Status = "RESPONDED_WITHOUT_QUALIFICATION"
}
results[index] = result
}(index, model)
}
wg.Wait()
return results
}
func persistQualificationResults(ctx context.Context, observer qualificationObserver, registration providerRegistration, results []modelQualificationResult) error {
for _, result := range results {
observationID, err := ids.New()
if err != nil {
return err
}
status := capability.ModelQualificationRespondedWithoutRating
switch result.Status {
case "REACHABLE":
status = capability.ModelQualificationReachable
case "UNAVAILABLE":
status = capability.ModelQualificationUnavailable
}
observation := capability.ModelQualificationObservation{
ID: capability.ModelQualificationObservationID(observationID), ProviderInstanceID: registration.ProviderInstanceID, ProviderModelID: result.ProviderModelID,
Status: status, FinishReason: string(result.FinishReason), LatencyMillis: result.LatencyMillis,
OutputObserved: result.OutputObserved, QualityAssessed: false, Category: capability.ModelRatingUnrated,
ObservedAtUnixMillis: time.Now().UnixMilli(),
}
if err := observation.Validate(); err != nil {
return err
}
if err := observer.ObserveQualification(ctx, observation); err != nil {
return err
}
}
return nil
}
func jsonEncode(w http.ResponseWriter, value any) error {
return json.NewEncoder(w).Encode(value)
}
@@ -0,0 +1,85 @@
package main
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.cin.su/m/Assistant/adapters/models"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
)
type qualificationProvider struct{}
func (qualificationProvider) Probe(context.Context) models.ProviderProbe {
return models.ProviderProbe{Health: capability.HealthAvailable}
}
func (qualificationProvider) Discover(context.Context) ([]models.DiscoveredModel, models.ProviderProbe) {
return []models.DiscoveredModel{{ProviderModelID: "model-a"}, {ProviderModelID: "model-b"}}, models.ProviderProbe{Health: capability.HealthAvailable}
}
func (qualificationProvider) Advise(_ context.Context, request cognitive.AdvisoryRequest) (cognitive.AdvisoryResponse, error) {
if err := models.ValidateAdvisoryRequest(request); err != nil {
return cognitive.AdvisoryResponse{}, err
}
return cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: "reachable", FinishReason: cognitive.FinishCompleted}, nil
}
func TestModelQualificationUsesDiscoveredModelsAndLeavesQualityUnrated(t *testing.T) {
provider := qualificationProvider{}
mux := http.NewServeMux()
mux.Handle("POST /v1/provider-discovery/{provider}/qualify", modelQualificationHandler(map[string]providerRegistration{"gpu": {ProviderInstanceID: "gpu", Provider: provider, Discovery: provider}}, "owner"))
server := httptest.NewServer(mux)
defer server.Close()
request := httptest.NewRequest(http.MethodPost, server.URL+"/v1/provider-discovery/gpu/qualify", strings.NewReader(`{"prompt":"ready","max_output_bytes":128}`))
request.Header.Set("Content-Type", "application/json")
response := httptest.NewRecorder()
mux.ServeHTTP(response, request)
if response.Code != http.StatusOK {
t.Fatalf("qualification status=%d body=%s", response.Code, response.Body.String())
}
if !strings.Contains(response.Body.String(), `"status":"REACHABLE"`) || !strings.Contains(response.Body.String(), `"category":"UNRATED"`) || strings.Contains(response.Body.String(), `"quality_assessed":true`) {
t.Fatalf("qualification overclaimed quality: %s", response.Body.String())
}
}
func TestModelQualificationRejectsUnboundedOutput(t *testing.T) {
provider := qualificationProvider{}
mux := http.NewServeMux()
mux.Handle("POST /v1/provider-discovery/{provider}/qualify", modelQualificationHandler(map[string]providerRegistration{"cpu": {ProviderInstanceID: "cpu", Provider: provider, Discovery: provider}}, "owner"))
request := httptest.NewRequest(http.MethodPost, "/v1/provider-discovery/cpu/qualify", strings.NewReader(`{"prompt":"ready","max_output_bytes":8192}`))
request.Header.Set("Content-Type", "application/json")
response := httptest.NewRecorder()
mux.ServeHTTP(response, request)
if response.Code != http.StatusBadRequest {
t.Fatalf("unbounded qualification status=%d", response.Code)
}
}
type qualificationObserverStub struct {
observations []capability.ModelQualificationObservation
}
func (s *qualificationObserverStub) ObserveQualification(_ context.Context, value capability.ModelQualificationObservation) error {
s.observations = append(s.observations, value)
return nil
}
func TestModelQualificationPersistsReachabilityWithoutQuality(t *testing.T) {
provider := qualificationProvider{}
observer := &qualificationObserverStub{}
mux := http.NewServeMux()
mux.Handle("POST /v1/provider-discovery/{provider}/qualify", modelQualificationHandler(map[string]providerRegistration{"cpu": {ProviderInstanceID: "cpu", Provider: provider, Discovery: provider}}, "owner", observer))
request := httptest.NewRequest(http.MethodPost, "/v1/provider-discovery/cpu/qualify", strings.NewReader(`{"prompt":"ready","max_output_bytes":128,"model_ids":["model-a"]}`))
request.Header.Set("Content-Type", "application/json")
response := httptest.NewRecorder()
mux.ServeHTTP(response, request)
if response.Code != http.StatusOK || len(observer.observations) != 1 {
t.Fatalf("qualification persistence status=%d observations=%d body=%s", response.Code, len(observer.observations), response.Body.String())
}
if err := observer.observations[0].Validate(); err != nil || observer.observations[0].Status != capability.ModelQualificationReachable {
t.Fatalf("invalid qualification observation: %+v err=%v", observer.observations[0], err)
}
}
+32
View File
@@ -0,0 +1,32 @@
package main
import (
"errors"
"time"
"git.cin.su/m/Assistant/contracts/work"
workdomain "git.cin.su/m/Assistant/modules/work/domain"
)
var errDevelopmentGateCreate = errors.New("development gate must start at evidence collected with revision one")
func prepareDevelopmentGate(value work.DevelopmentGate, now time.Time) (work.DevelopmentGate, error) {
if value.Stage != work.DevelopmentEvidenceCollected || value.Revision != 1 {
return work.DevelopmentGate{}, errDevelopmentGateCreate
}
value.UpdatedAtUnixMillis = now.UnixMilli()
if err := workdomain.ValidateDevelopmentGate(value); err != nil {
return work.DevelopmentGate{}, err
}
return value, nil
}
func advanceDevelopmentGate(value work.DevelopmentGate, next work.DevelopmentStage, now time.Time) (work.DevelopmentGate, error) {
advanced, err := workdomain.AdvanceDevelopmentGate(value, next)
if err != nil {
return work.DevelopmentGate{}, err
}
advanced.Revision = value.Revision + 1
advanced.UpdatedAtUnixMillis = now.UnixMilli()
return advanced, nil
}
+41
View File
@@ -0,0 +1,41 @@
package main
import (
"testing"
"time"
"git.cin.su/m/Assistant/contracts/work"
)
func TestDevelopmentGateControllerRequiresSequentialVerifiedEvidence(t *testing.T) {
now := time.UnixMilli(10)
gate, err := prepareDevelopmentGate(work.DevelopmentGate{ID: "gate-1", Revision: 1, Stage: work.DevelopmentEvidenceCollected, EvidenceRefs: []string{"evidence-1"}}, now)
if err != nil {
t.Fatal(err)
}
gate.ProposalRef = "proposal-1"
gate, err = advanceDevelopmentGate(gate, work.DevelopmentProposalCreated, now)
if err != nil || gate.Revision != 2 {
t.Fatalf("proposal transition failed: %#v %v", gate, err)
}
gate.TaskPacketRef = "task-1"
gate, err = advanceDevelopmentGate(gate, work.DevelopmentTaskAdmitted, now)
if err != nil {
t.Fatal(err)
}
gate.WorktreeRef = "isolated-worktree"
gate, err = advanceDevelopmentGate(gate, work.DevelopmentIsolated, now)
if err != nil {
t.Fatal(err)
}
gate.ProducerRunRefs = []work.AgentRunID{"producer"}
gate.VerifierRunRefs = []work.AgentRunID{"verifier"}
gate.VerificationEvidence = "verification-1"
gate, err = advanceDevelopmentGate(gate, work.DevelopmentVerified, now)
if err != nil {
t.Fatal(err)
}
if _, err := advanceDevelopmentGate(gate, work.DevelopmentPromoted, now); err == nil {
t.Fatal("promotion bypassed canary")
}
}
+69
View File
@@ -256,6 +256,61 @@ func main() {
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(scaffold)
})
mux.HandleFunc("POST /v1/creator/development/gates", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
var gate work.DevelopmentGate
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&gate); err != nil {
http.Error(w, "invalid development gate", http.StatusBadRequest)
return
}
gate, err := prepareDevelopmentGate(gate, time.Now())
if err != nil {
http.Error(w, "development gate rejected", http.StatusUnprocessableEntity)
return
}
if err := workRepository.SaveDevelopmentGate(r.Context(), gate, 0); err != nil {
http.Error(w, "development gate conflict", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(gate)
})
mux.HandleFunc("POST /v1/creator/development/gates/{id}/advance", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
var request struct {
ExpectedRevision int64 `json:"expected_revision"`
NextStage work.DevelopmentStage `json:"next_stage"`
}
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil || request.ExpectedRevision < 1 || request.NextStage == "" {
http.Error(w, "invalid development gate transition", http.StatusBadRequest)
return
}
gate, err := workRepository.LoadDevelopmentGate(r.Context(), r.PathValue("id"))
if err != nil || gate.Revision != request.ExpectedRevision {
http.Error(w, "development gate revision conflict", http.StatusConflict)
return
}
gate, err = advanceDevelopmentGate(gate, request.NextStage, time.Now())
if err != nil {
http.Error(w, "development gate transition rejected", http.StatusUnprocessableEntity)
return
}
if err := workRepository.SaveDevelopmentGate(r.Context(), gate, request.ExpectedRevision); err != nil {
http.Error(w, "development gate conflict", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(gate)
})
mux.HandleFunc("GET /v1/creator/focus", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
@@ -1003,6 +1058,20 @@ func main() {
}
w.WriteHeader(http.StatusCreated)
})
mux.HandleFunc("POST /v1/cognitive-registry/model-qualification-observations", func(w http.ResponseWriter, r *http.Request) {
var value capability.ModelQualificationObservation
decoder := json.NewDecoder(io.LimitReader(r.Body, 16<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&value); err != nil || value.Validate() != nil {
http.Error(w, "invalid model qualification observation", http.StatusBadRequest)
return
}
if err := capabilityRepository.SaveModelQualificationObservation(r.Context(), value); err != nil {
http.Error(w, "save model qualification observation", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
})
mux.HandleFunc("POST /v1/creator/cognitive-canary", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
+17
View File
@@ -4,6 +4,8 @@ import (
"context"
"flag"
"fmt"
"log"
"net/http"
"os"
"time"
@@ -29,9 +31,21 @@ func main() {
panic(err)
}
defer repository.Close()
status := newWorkerStatus(*workerID, *once)
listenAddr := os.Getenv("REFLECTION_LISTEN_ADDR")
if listenAddr == "" {
listenAddr = ":8092"
}
go func() {
if err := http.ListenAndServe(listenAddr, status.handler()); err != nil {
log.Printf("reflection status server stopped: %v", err)
}
}()
for {
now := time.Now().UnixMilli()
status.cycleStarted(time.Now())
if _, err := repository.RecoverReflectionClaims(ctx, now); err != nil {
status.cycleFailed(time.Now(), err)
panic(err)
}
candidate, claimErr := repository.ClaimReflectionCandidate(ctx, *workerID, now, lease.Milliseconds())
@@ -39,16 +53,19 @@ func main() {
// 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 {
status.cycleFailed(time.Now(), err)
panic(err)
}
fmt.Printf("resolved reflection candidate %s\n", candidate.ID)
} else if *once {
fmt.Println("no reflection candidate")
}
status.cycleFinished(time.Now(), map[bool]string{true: "candidate-resolved", false: "idle"}[claimErr == nil])
if *once {
return
}
if err := repository.WaitForReflectionCandidate(ctx, *pollInterval); err != nil {
status.cycleFailed(time.Now(), err)
panic(err)
}
}
+74
View File
@@ -0,0 +1,74 @@
package main
import (
"encoding/json"
"net/http"
"sync"
"time"
"git.cin.su/m/Assistant/runtime/health"
)
type workerStatus struct {
mu sync.RWMutex
workerID string
once bool
startedAt time.Time
lastCycleAt time.Time
lastCompleted time.Time
lastResult string
lastError string
}
func newWorkerStatus(workerID string, once bool) *workerStatus {
return &workerStatus{workerID: workerID, once: once, startedAt: time.Now().UTC()}
}
func (s *workerStatus) cycleStarted(at time.Time) {
s.mu.Lock()
defer s.mu.Unlock()
s.lastCycleAt = at.UTC()
s.lastResult = "running"
s.lastError = ""
}
func (s *workerStatus) cycleFinished(at time.Time, result string) {
s.mu.Lock()
defer s.mu.Unlock()
s.lastCompleted = at.UTC()
s.lastResult = result
s.lastError = ""
}
func (s *workerStatus) cycleFailed(at time.Time, err error) {
s.mu.Lock()
defer s.mu.Unlock()
s.lastCompleted = at.UTC()
s.lastResult = "failed"
s.lastError = err.Error()
}
func (s *workerStatus) handler() http.Handler {
mux := http.NewServeMux()
mux.Handle("/healthz", health.Handler{Runtime: "reflection-worker", SemanticGeneration: 1, ArchitectureGeneration: 1})
mux.HandleFunc("/status", func(w http.ResponseWriter, _ *http.Request) {
s.mu.RLock()
response := struct {
Runtime string `json:"runtime"`
WorkerID string `json:"worker_id"`
Mode string `json:"mode"`
StartedAt time.Time `json:"started_at"`
LastCycleAt time.Time `json:"last_cycle_at,omitempty"`
LastCompleted time.Time `json:"last_completed_at,omitempty"`
LastResult string `json:"last_result"`
LastError string `json:"last_error,omitempty"`
}{Runtime: "reflection-worker", WorkerID: s.workerID, Mode: "durable-reflection-loop", StartedAt: s.startedAt, LastCycleAt: s.lastCycleAt, LastCompleted: s.lastCompleted, LastResult: s.lastResult, LastError: s.lastError}
if s.once {
response.Mode = "durable-reflection-once"
}
s.mu.RUnlock()
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(response)
})
return mux
}
+35
View File
@@ -0,0 +1,35 @@
package main
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestWorkerStatusReportsCycle(t *testing.T) {
status := newWorkerStatus("test-worker", false)
status.cycleStarted(time.Date(2026, 8, 23, 10, 0, 0, 0, time.UTC))
status.cycleFinished(time.Date(2026, 8, 23, 10, 0, 1, 0, time.UTC), "idle")
recorder := httptest.NewRecorder()
status.handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/status", nil))
if recorder.Code != http.StatusOK {
t.Fatalf("status code = %d, want 200", recorder.Code)
}
body := recorder.Body.String()
for _, expected := range []string{"\"runtime\":\"reflection-worker\"", "\"worker_id\":\"test-worker\"", "\"last_result\":\"idle\""} {
if !strings.Contains(body, expected) {
t.Fatalf("status body missing %q: %s", expected, body)
}
}
}
func TestWorkerHealthIsAvailable(t *testing.T) {
recorder := httptest.NewRecorder()
newWorkerStatus("test-worker", true).handler().ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/healthz", nil))
if recorder.Code != http.StatusOK {
t.Fatalf("health status code = %d, want 200", recorder.Code)
}
}
+43 -4
View File
@@ -61,18 +61,57 @@ func main() {
}
writeJSON(w, map[string]any{"graph": graph, "scheduled_nodes": ready})
})
mux.HandleFunc("POST /v1/work-graphs/orchestration/next", func(w http.ResponseWriter, r *http.Request) {
var request work.SolutionSearchInput
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10)).Decode(&request); err != nil {
http.Error(w, "invalid orchestration input", http.StatusBadRequest)
return
}
decision, err := application.DecideSolutionSearch(request)
if err != nil {
http.Error(w, "invalid orchestration input", http.StatusUnprocessableEntity)
return
}
writeJSON(w, decision)
})
mux.HandleFunc("POST /v1/work-graphs/orchestration/wave", func(w http.ResponseWriter, r *http.Request) {
var request work.SolutionSearchWaveRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&request); err != nil {
http.Error(w, "invalid orchestration wave request", http.StatusBadRequest)
return
}
result, err := application.OrchestrateSolutionSearch(r.Context(), repository, request)
if err != nil {
http.Error(w, "invalid orchestration wave request", http.StatusUnprocessableEntity)
return
}
writeJSON(w, result)
})
mux.HandleFunc("POST /v1/work-graphs/result-callback", func(w http.ResponseWriter, r *http.Request) {
var request work.GraphResultCallback
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&request); err != nil {
http.Error(w, "invalid graph result callback", http.StatusBadRequest)
return
}
graph, err := application.RecordGraphResultCandidate(r.Context(), repository, repository, request)
if err != nil {
http.Error(w, "invalid graph result callback", http.StatusUnprocessableEntity)
return
}
writeJSON(w, graph)
})
mux.HandleFunc("POST /v1/work-graphs/complete", func(w http.ResponseWriter, r *http.Request) {
var request application.GraphCompleteRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&request); err != nil {
http.Error(w, "invalid graph completion request", http.StatusBadRequest)
return
}
graph, err := application.CompleteGraphNode(r.Context(), repository, request.GraphID, request.Revision, request.NodeID, request.Succeeded)
result, err := application.CompleteGraphNodeWithReplan(r.Context(), repository, request)
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
writeJSON(w, graph)
writeJSON(w, result)
})
mux.HandleFunc("POST /v1/work-graphs/task-evaluations/callback", func(w http.ResponseWriter, r *http.Request) {
var request work.TaskEvaluationEvidenceCallback
@@ -93,12 +132,12 @@ func main() {
http.Error(w, "invalid graph synthesis request", http.StatusBadRequest)
return
}
conflict, synthesis, err := application.SynthesizeGraph(r.Context(), repository, request)
conflict, synthesis, graph, err := application.SynthesizeAndCompleteGraph(r.Context(), repository, repository, request)
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
writeJSON(w, map[string]any{"conflict": conflict, "synthesis": synthesis})
writeJSON(w, map[string]any{"conflict": conflict, "synthesis": synthesis, "graph": graph})
})
addr := os.Getenv("LISTEN_ADDR")
if addr == "" {
@@ -172,6 +172,7 @@ type ResolvedCognitiveBinding struct {
Evaluation *EvaluatedCapability
Performance *PerformanceEvidence
TaskEvaluation *TaskEvaluation
Qualification *ModelQualificationObservation
Rating *ModelRating
}
@@ -0,0 +1,47 @@
package capability
import (
"errors"
"git.cin.su/m/Assistant/platform/ids"
)
type ModelQualificationObservationID ids.ID
type ModelQualificationStatus string
const (
ModelQualificationReachable ModelQualificationStatus = "REACHABLE"
ModelQualificationRespondedWithoutRating ModelQualificationStatus = "RESPONDED_WITHOUT_QUALIFICATION"
ModelQualificationUnavailable ModelQualificationStatus = "UNAVAILABLE"
)
// ModelQualificationObservation is operational evidence only. It records
// reachability and latency, never quality, activation, routing priority or
// provider credentials.
type ModelQualificationObservation struct {
ID ModelQualificationObservationID
ProviderInstanceID ProviderInstanceID
ProviderModelID ProviderModelID
Status ModelQualificationStatus
FinishReason string
LatencyMillis int64
OutputObserved bool
QualityAssessed bool
Category ModelRatingCategory
ObservedAtUnixMillis int64
}
func (o ModelQualificationObservation) Validate() error {
if o.ID == "" || o.ProviderInstanceID == "" || o.ProviderModelID == "" || o.FinishReason == "" || o.LatencyMillis < 0 || o.ObservedAtUnixMillis < 1 {
return errors.New("model qualification observation identity and timing are incomplete")
}
switch o.Status {
case ModelQualificationReachable, ModelQualificationRespondedWithoutRating, ModelQualificationUnavailable:
default:
return errors.New("model qualification observation status is unsupported")
}
if o.Category != ModelRatingUnrated || o.QualityAssessed {
return errors.New("model qualification observation cannot assert quality")
}
return nil
}
+29 -17
View File
@@ -21,14 +21,15 @@ const (
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"
GraphNodeBlocked GraphNodeState = "BLOCKED_PREREQUISITE"
GraphNodeReady GraphNodeState = "READY"
GraphNodeLeased GraphNodeState = "LEASED"
GraphNodeRunning GraphNodeState = "RUNNING"
GraphNodeVerifying GraphNodeState = "VERIFYING"
GraphNodeSuccess GraphNodeState = "SUCCEEDED"
GraphNodeFailed GraphNodeState = "FAILED"
GraphNodeCancel GraphNodeState = "CANCELLED"
GraphNodeUnknown GraphNodeState = "UNKNOWN"
)
type GraphBudget struct {
@@ -56,15 +57,17 @@ type GraphEdge struct {
}
type WorkGraph struct {
ID string `json:"id"`
Revision int64 `json:"revision"`
State GraphState `json:"state"`
Nodes []GraphNode `json:"nodes"`
Edges []GraphEdge `json:"edges"`
Budget GraphBudget `json:"budget"`
ParentGraphID string `json:"parent_graph_id,omitempty"`
ParentGraphRevision int64 `json:"parent_graph_revision,omitempty"`
TriggerEvidenceRef string `json:"trigger_evidence_ref,omitempty"`
ID string `json:"id"`
Revision int64 `json:"revision"`
State GraphState `json:"state"`
Nodes []GraphNode `json:"nodes"`
Edges []GraphEdge `json:"edges"`
Budget GraphBudget `json:"budget"`
ParentGraphID string `json:"parent_graph_id,omitempty"`
ParentGraphRevision int64 `json:"parent_graph_revision,omitempty"`
TriggerEvidenceRef string `json:"trigger_evidence_ref,omitempty"`
SearchInputBound bool `json:"search_input_bound,omitempty"`
SearchInput SolutionSearchInput `json:"search_input,omitempty"`
}
// TaskEvaluationGraphRequest describes a durable fan-out. The graph contains
@@ -110,6 +113,15 @@ type ResultCandidate struct {
Summary string `json:"summary"`
}
// GraphResultCallback carries one agent's attributable candidate to Work.
// Recording a candidate never makes the node successful; the node enters
// VERIFYING until an independent synthesis/verifier accepts it.
type GraphResultCallback struct {
GraphID string `json:"graph_id"`
Revision int64 `json:"revision"`
Candidate ResultCandidate `json:"candidate"`
}
type ConflictSet struct {
ID string `json:"id"`
GraphID string `json:"graph_id"`
+66
View File
@@ -0,0 +1,66 @@
package work
type OrchestrationState string
type SearchOptionKind string
const (
OrchestrationSearching OrchestrationState = "SEARCHING"
OrchestrationExecuting OrchestrationState = "EXECUTING"
OrchestrationVerifying OrchestrationState = "VERIFYING"
OrchestrationWaiting OrchestrationState = "WAITING"
OrchestrationCompleted OrchestrationState = "COMPLETED"
OrchestrationCancelled OrchestrationState = "CANCELLED"
SearchOptionAgents SearchOptionKind = "AGENT_TEAM"
SearchOptionCapability SearchOptionKind = "CAPABILITY_DISCOVERY"
SearchOptionResearch SearchOptionKind = "TRUTH_RESEARCH"
SearchOptionReflection SearchOptionKind = "REFLECTION_MEMORY"
SearchOptionHuman SearchOptionKind = "HUMAN_ATTENTION"
)
// SolutionSearchInput is the durable orchestration snapshot. The scheduler
// stores the graph/goal separately; this value only decides the next bounded
// search wave and never grants authority by itself.
type SolutionSearchInput struct {
GoalID GoalID
GoalState GoalState
SearchRound int
MaxRound int
CancelRequested bool
ActiveOptionCount int
VerificationReady bool
Unresolved bool
}
type SolutionSearchDecision struct {
GoalID GoalID
State OrchestrationState
ContinueSearching bool
NextSearchRound int
Options []SearchOptionKind
Reason string
}
type SolutionSearchWaveRequest struct {
Input SolutionSearchInput `json:"input"`
GraphID string `json:"graph_id"`
IdempotencyPrefix string `json:"idempotency_prefix"`
Dispatches []SolutionSearchDispatch `json:"dispatches"`
Budget GraphBudget `json:"budget"`
ParentGraphID string `json:"parent_graph_id,omitempty"`
ParentGraphRevision int64 `json:"parent_graph_revision,omitempty"`
TriggerEvidenceRef string `json:"trigger_evidence_ref,omitempty"`
NowUnixMillis int64 `json:"now_unix_millis"`
}
type SolutionSearchDispatch struct {
Option SearchOptionKind `json:"option"`
Intent AgentDispatchIntent `json:"intent"`
}
type SolutionSearchWaveResult struct {
Decision SolutionSearchDecision `json:"decision"`
Graph WorkGraph `json:"graph"`
ScheduledNodeIDs []GraphNodeID `json:"scheduled_node_ids,omitempty"`
GraphMaterialized bool `json:"graph_materialized"`
}
@@ -0,0 +1,21 @@
BEGIN;
SET ROLE work_role;
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_bound BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_goal_id TEXT NOT NULL DEFAULT '';
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_goal_state TEXT NOT NULL DEFAULT '';
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_search_round INTEGER NOT NULL DEFAULT 0;
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_max_round INTEGER NOT NULL DEFAULT 0;
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_cancel_requested BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_verification_ready BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE work.graphs ADD COLUMN IF NOT EXISTS orchestration_unresolved BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE work.graphs DROP CONSTRAINT IF EXISTS work_graphs_orchestration_snapshot_check;
ALTER TABLE work.graphs ADD CONSTRAINT work_graphs_orchestration_snapshot_check CHECK (
(NOT orchestration_bound AND orchestration_goal_id = '' AND orchestration_search_round = 0 AND orchestration_max_round = 0)
OR (orchestration_bound AND orchestration_goal_id <> '' AND orchestration_search_round >= 1 AND orchestration_max_round >= 1)
);
RESET ROLE;
COMMIT;
@@ -0,0 +1,24 @@
BEGIN;
SET ROLE capability_role;
CREATE TABLE IF NOT EXISTS capability.cognitive_model_qualification_observations (
observation_id TEXT PRIMARY KEY,
provider_instance_id TEXT NOT NULL,
provider_model_id TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('REACHABLE','RESPONDED_WITHOUT_QUALIFICATION','UNAVAILABLE')),
finish_reason TEXT NOT NULL,
latency_millis BIGINT NOT NULL CHECK (latency_millis >= 0),
output_observed BOOLEAN NOT NULL,
quality_assessed BOOLEAN NOT NULL DEFAULT FALSE CHECK (quality_assessed = FALSE),
category TEXT NOT NULL CHECK (category = 'UNRATED'),
observed_at_unix_millis BIGINT NOT NULL CHECK (observed_at_unix_millis > 0)
);
CREATE INDEX IF NOT EXISTS cognitive_model_qualification_lookup
ON capability.cognitive_model_qualification_observations(provider_instance_id, provider_model_id, observed_at_unix_millis DESC);
GRANT SELECT, INSERT ON capability.cognitive_model_qualification_observations TO capability_role;
RESET ROLE;
COMMIT;
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000229-research-runtime-bridge.json",
"task_id": "DEV-000229",
"task_file": "dev/tasks/DEV-000244-reflection-worker-observability.json",
"task_id": "DEV-000244",
"task_revision": 1
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000230",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "ec79f19a7935e1eb8f0f59c4c9ce2940b97c4855",
"risk_tier": "E",
"intent": "Expose the existing Work-owned governed development state machine through authenticated optimistic-concurrency routes without executing code, changing protected source or bypassing promotion gates.",
"requirement_ids": ["governed self-development", "durable gate progression", "canary rollback control"],
"allowed_change_scope": {"modules": ["core-runtime", "governed-development"], "paths": ["cmd/core-runtime/main.go", "cmd/core-runtime/development_gate.go", "cmd/core-runtime/development_gate_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000230-development-gate-controller.json", "docs/PROJECT_STATUS.md"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "contracts/**", "database/**", "deploy/**", "LLM-local/**"]},
"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": ["Work DevelopmentGate state machine", "authenticated Creator session", "optimistic revision persistence"], "may_change": ["authenticated Core gate controller routes"]},
"semantic_owner": "Work governed development gate",
"canonical_state": ["Work-owned development gate and existing proposal/scaffold records"],
"state_transitions": ["EVIDENCE_COLLECTED -> PROPOSAL_CREATED -> TASK_ADMITTED -> ISOLATED -> VERIFIED -> CANARY -> PROMOTED", "CANARY -> ROLLED_BACK"],
"side_effects_and_privileges": ["authenticated durable gate creation/transition only; no checkout, code execution, secret access, production mutation or automatic promotion"],
"persistence_and_migrations": ["none; existing development_gates table and repository are reused"],
"failure_retry_restart": ["optimistic revision conflict is explicit; invalid or skipped transitions are rejected; rollback remains available only from canary"],
"assumptions": ["Creator authentication is the explicit governance boundary for these operator routes", "actual worktree execution and deployment remain separate controlled systems"],
"acceptance_scenarios": ["gate creation requires evidence-collected revision one", "sequential transitions require their typed references", "verified cannot jump directly to promoted", "stale revision is rejected", "canary rollback is allowed", "unauthenticated requests are denied"],
"required_tests": ["go test ./cmd/core-runtime/...", "go vet ./cmd/core-runtime/...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production health smoke"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000231",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "45874f218646d7879dfcbc814782fe56bf0aa06a",
"risk_tier": "E",
"intent": "Provide a durable, parallel-first solution-search decision policy for Work Graph goals so unresolved work continues through independent agent, capability, research and reflection avenues instead of dead-ending after one bounded wave.",
"requirement_ids": ["solution orchestration", "parallel agent team", "durable retry waves", "independent verification"],
"allowed_change_scope": {"modules": ["work-graph", "work-domain"], "paths": ["contracts/work/orchestration.go", "modules/work/domain/orchestration.go", "modules/work/domain/orchestration_test.go", "modules/work/application/orchestration.go", "cmd/work-graph-runtime/main.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000231-solution-orchestrator.json", "docs/PROJECT_STATUS.md"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "LLM-local/**", "modules/work/secrets/**"]},
"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": ["Work Goal state", "durable Work Graph scheduling", "independent verification result"], "may_change": ["typed solution-search decision contract", "Work Graph orchestration decision endpoint"]},
"semantic_owner": "Work solution orchestration policy",
"canonical_state": ["existing Work Goal and Work Graph records; caller persists the returned wave decision"],
"state_transitions": ["SEARCHING -> EXECUTING -> VERIFYING -> COMPLETED", "SEARCHING -> WAITING -> SEARCHING", "any unresolved state -> next durable search wave", "any active state -> CANCELLED"],
"side_effects_and_privileges": ["pure policy decision and unauthenticated internal runtime route only; no provider call, secret access, code mutation, promotion or deployment"],
"persistence_and_migrations": ["none; this stage returns a typed decision for the existing durable scheduler"],
"failure_retry_restart": ["bounded wave exhaustion becomes WAITING with a new wave number and human-attention avenue; it is never represented as failure; cancellation and independently verified completion are terminal"],
"assumptions": ["the existing Work Graph dispatcher or next integration stage will persist the decision and fan out options", "model quality and provider routing remain separate capability decisions"],
"acceptance_scenarios": ["first unresolved goal selects all independent search avenues", "active wave remains EXECUTING", "exhausted wave continues with a higher durable round and human attention", "only cancellation or independently verified completion stops search", "invalid input is rejected", "the decision endpoint does not mutate source, secrets or production"],
"required_tests": ["go test ./modules/work/domain ./modules/work/application ./cmd/work-graph-runtime/...", "go vet ./modules/work/domain ./modules/work/application ./cmd/work-graph-runtime/...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production health smoke"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000232",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "89694047b1e6c7729b00c20a52acb5a2487e6dd6",
"risk_tier": "E",
"intent": "Connect the solution-search policy to the existing durable Work Graph scheduler so each unresolved wave is materialized as parallel dispatch intents and queued nodes.",
"requirement_ids": ["durable solution waves", "parallel fan-out", "agent/reflex orchestration", "restartable scheduling"],
"allowed_change_scope": {"modules": ["work-graph", "work-domain"], "paths": ["contracts/work/orchestration.go", "modules/work/domain/orchestration.go", "modules/work/domain/orchestration_test.go", "modules/work/application/orchestration.go", "cmd/work-graph-runtime/main.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000232-durable-solution-wave-dispatch.json", "docs/PROJECT_STATUS.md"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "LLM-local/**", "modules/work/secrets/**"]},
"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": ["typed solution-search decision", "existing Work Graph and dispatch queue ports"], "may_change": ["solution-search wave materialization request/result", "Work Graph orchestration wave route"]},
"semantic_owner": "Work solution orchestration and Work Graph",
"canonical_state": ["existing work.graphs, graph_nodes, dispatch_intents and dispatch_queue stores"],
"state_transitions": ["policy SEARCHING/WAITING -> graph ADMITTED/RUNNING", "each option -> one queued dispatch node", "human attention remains decision metadata"],
"side_effects_and_privileges": ["durable Work-owned graph and queue writes only; no provider call, secret access, external mutation, promotion or deployment"],
"persistence_and_migrations": ["none; existing graph and queue tables are reused"],
"failure_retry_restart": ["existing queue idempotency and graph revision persistence govern restart; invalid wave shape is rejected; no unresolved goal is marked failed by wave exhaustion"],
"assumptions": ["dispatch intents are supplied by the upstream authority-aware planner", "provider/model selection and execution remain worker responsibilities"],
"acceptance_scenarios": ["four independent solution avenues materialize as four graph nodes", "all ready nodes are queued up to graph MaxParallel", "human attention is not silently executed as an agent", "terminal policy decisions do not create a graph", "invalid or duplicate options are rejected"],
"required_tests": ["go test ./modules/work/domain ./modules/work/application ./cmd/work-graph-runtime/...", "go vet ./modules/work/domain ./modules/work/application ./cmd/work-graph-runtime/...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production health smoke"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000233",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "ee16103e03a5fd911119e2bc45e5904255ab7752",
"risk_tier": "E",
"intent": "Close the multi-agent Work Graph result lifecycle: persist attributable agent candidates, prevent candidate text from completing a node, and complete only after independently verified synthesis.",
"requirement_ids": ["fan-in", "independent verifier", "result provenance", "no false completion"],
"allowed_change_scope": {"modules": ["work-graph", "agent-execution", "postgres-work-adapter"], "paths": ["contracts/work/graph.go", "modules/work/ports/graph.go", "modules/work/application/graph_runtime.go", "adapters/postgres/work_repository.go", "cmd/work-graph-runtime/main.go", "tests/agent_execution/graph_runtime_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000233-graph-result-fanin-verification.json", "docs/PROJECT_STATUS.md"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "LLM-local/**", "modules/work/secrets/**"]},
"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 Work Graph revision store", "existing graph result candidate/synthesis tables", "agent callback evidence"], "may_change": ["typed graph result callback", "Work result callback route", "verified synthesis completion path"]},
"semantic_owner": "Work Graph result and verification lifecycle",
"canonical_state": ["work.graphs, work.graph_nodes, work.graph_result_candidates, work.graph_conflicts and work.graph_synthesis_candidates"],
"state_transitions": ["agent result -> durable ResultCandidate", "candidate -> no completion", "independent verified synthesis -> graph node SUCCEEDED", "all verified nodes -> graph COMPLETED"],
"side_effects_and_privileges": ["Work-owned candidate/graph persistence only; no provider call, secret access, external mutation, promotion or deployment"],
"persistence_and_migrations": ["none; existing result candidate and synthesis tables are reused"],
"failure_retry_restart": ["immutable graph revisions remain attributable; duplicate candidate IDs are idempotent at storage; missing or unverified synthesis cannot mark success"],
"assumptions": ["the verifier supplies all competing candidates and evidence in the synthesis request", "automatic replan selection remains the next stage because a graph does not yet own a persisted goal-search snapshot"],
"acceptance_scenarios": ["result callback persists candidate", "result callback leaves graph running and not completed", "verified independent synthesis advances the node", "verifier cannot be a producer", "fan-in completion requires all graph nodes successful"],
"required_tests": ["go test ./modules/work/domain ./modules/work/application ./tests/agent_execution ./cmd/work-graph-runtime/...", "go vet ./modules/work/domain ./modules/work/application ./tests/agent_execution ./cmd/work-graph-runtime/...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production health smoke"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000234",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "eac39a3fd40e9f8d0752af6f9e7893cb0b03055b",
"risk_tier": "E",
"intent": "Persist the solution-search snapshot with every orchestration Work Graph revision and return the next durable search decision after verified graph completion.",
"requirement_ids": ["durable goal continuity", "automatic replan signal", "restart recovery", "no invented execution authority"],
"allowed_change_scope": {"modules": ["work-graph", "postgres-work-adapter"], "paths": ["contracts/work/graph.go", "modules/work/domain/orchestration.go", "modules/work/application/graph_runtime.go", "adapters/postgres/work_repository.go", "database/migrations/044_work_graph_orchestration_snapshot.sql", "cmd/work-graph-runtime/main.go", "tests/agent_execution/graph_runtime_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000234-orchestration-snapshot-replan.json", "docs/PROJECT_STATUS.md"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "LLM-local/**", "modules/work/secrets/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
"contracts": {"consume": ["Work Graph immutable revision store", "SolutionSearchDecision policy"], "may_change": ["orchestration snapshot fields on WorkGraph", "graph completion result with next search decision"]},
"semantic_owner": "Work Graph orchestration continuity",
"canonical_state": ["work.graphs revision snapshot including GoalID and search round"],
"state_transitions": ["wave materialization persists search snapshot", "verified graph completion -> next search decision", "next decision remains nonterminal when unresolved"],
"side_effects_and_privileges": ["Work-owned graph persistence and queue orchestration only; no model/provider call, secret access, external mutation or promotion"],
"persistence_and_migrations": ["migration 044 adds nullable-by-contract orchestration snapshot columns with backward-compatible defaults"],
"failure_retry_restart": ["restarting from any immutable graph revision reconstructs the same search input; missing snapshot does not invent a replan"],
"assumptions": ["dispatch-intent discovery/materialization is supplied by the next wave planner; this stage returns the durable hand-off rather than fabricating agent authority"],
"acceptance_scenarios": ["new solution wave persists goal and round", "verified completion returns next round decision", "cancelled or verified terminal goal does not replan", "legacy graph without snapshot completes without invented replan"],
"required_tests": ["go test ./modules/work/domain ./modules/work/application ./tests/agent_execution ./cmd/work-graph-runtime/...", "go vet ./modules/work/domain ./modules/work/application ./tests/agent_execution ./cmd/work-graph-runtime/...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production health smoke"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000235",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "60e8292c1df947b887079413b3b35ce8b4212086",
"risk_tier": "E",
"intent": "Add a bounded provider-neutral model qualification probe that discovers actual model IDs, invokes them in parallel, records reachability and latency, and never overclaims quality or hardware superiority.",
"requirement_ids": ["model fleet/failover", "dynamic model discovery", "quality separated from reachability", "parallel qualification"],
"allowed_change_scope": {"modules": ["cognitive-worker", "capability-observation"], "paths": ["cmd/cognitive-worker/main.go", "cmd/cognitive-worker/model_qualification.go", "cmd/cognitive-worker/model_qualification_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000235-model-qualification-probe.json", "docs/PROJECT_STATUS.md"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "LLM-local/**", "modules/work/secrets/**"]},
"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": ["OpenAI-compatible LocalProvider discovery/advisory adapter", "Capability model rating categories"], "may_change": ["internal bounded provider qualification route"]},
"semantic_owner": "Capability cognitive runtime observation",
"canonical_state": ["none; probe response is an operational observation and does not promote a binding"],
"state_transitions": ["DISCOVERED model -> bounded probe -> REACHABLE/UNAVAILABLE/RESPONDED_WITHOUT_QUALIFICATION", "quality remains UNRATED until independent task tests"],
"side_effects_and_privileges": ["read-only provider model discovery and bounded chat invocation; no external mutation, secret exposure, activation, promotion or routing priority"],
"persistence_and_migrations": ["none; existing discovery/observation and promotion flows remain unchanged"],
"failure_retry_restart": ["each probe has provider adapter timeout; one model failure does not cancel sibling probes; rerun is bounded and idempotent in effect"],
"assumptions": ["provider credentials are resolved inside cognitive-worker protected runtime", "additional task-specific tests can be added later without changing the primary probe"],
"acceptance_scenarios": ["actual discovered model IDs are used instead of static placeholders", "models are probed in parallel", "successful reachability reports latency", "quality is explicitly UNRATED", "GPU presence does not alter category or promotion", "output bound and model selection are constrained"],
"required_tests": ["go test ./cmd/cognitive-worker/... ./modules/capability/domain ./adapters/models/...", "go vet ./cmd/cognitive-worker/... ./modules/capability/domain ./adapters/models/...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production health smoke"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000236",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"risk_tier": "E",
"base_repo_revision": "b20611590a075e3574efddbdba6e1ace312d11a8",
"intent": "Make the bounded model qualification invocation satisfy the canonical advisory contract and keep a regression test for the required locale.",
"requirement_ids": ["model qualification smoke", "advisory contract"],
"allowed_change_scope": {"modules": ["cognitive-worker"], "paths": ["cmd/cognitive-worker/model_qualification.go", "cmd/cognitive-worker/model_qualification_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000236-qualification-locale-fix.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "LLM-local/**", "modules/work/secrets/**", "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": ["Creator advisory request validation"], "may_change": []},
"semantic_owner": "Cognitive worker qualification observation",
"canonical_state": ["none; qualification remains an operational observation"],
"state_transitions": ["invalid advisory invocation -> valid bounded invocation"],
"side_effects_and_privileges": ["read-only bounded provider invocation; no promotion or routing mutation"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["one bounded probe failure remains isolated"],
"assumptions": ["provider credentials remain protected inside cognitive-worker"],
"acceptance_scenarios": ["live qualification invocation passes advisory validation", "quality remains UNRATED"],
"required_tests": ["go test ./cmd/cognitive-worker/...", "go vet ./cmd/cognitive-worker/...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production health smoke"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000237",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "be01b6b883c2ce6e9bb47bc851505fe222c46d14",
"risk_tier": "E",
"intent": "Record current authenticated quick acceptance for the five creator-console sections without mutating production state.",
"requirement_ids": ["WP-800", "WP-804", "authenticated UI quick acceptance"],
"allowed_change_scope": {"modules": ["creator-web", "evidence"], "paths": ["docs/evidence/authenticated-ui-section-smoke-2026-08-23-v1.json", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000237-ui-authenticated-section-smoke.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "LLM-local/**", "modules/work/secrets/**", "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": ["authenticated creator-console UI"], "may_change": []},
"semantic_owner": "Creator-web acceptance evidence",
"canonical_state": ["evidence only; no runtime state mutation"],
"state_transitions": ["unverified current section -> observed authenticated quick acceptance"],
"side_effects_and_privileges": ["read-only browser inspection; no mutation"],
"persistence_and_migrations": ["evidence artifact only"],
"failure_retry_restart": ["recheck is idempotent and does not change product state"],
"assumptions": ["the authenticated browser session belongs to the authorized creator"],
"acceptance_scenarios": ["five admin sections load authenticated", "no visible section error is present", "full mobile/accessibility/chaos acceptance remains explicitly unclaimed"],
"required_tests": ["go test ./cmd/creator-web/...", "go vet ./cmd/creator-web/...", "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", "independent", "CI", "production health smoke"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000238",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "0609561dfefb6dce302f56d50bb56bd5a222fb64",
"risk_tier": "E",
"intent": "Record a bounded live proof that the advisory gateway keeps a local lifeboat path when the normal model route cannot be used.",
"requirement_ids": ["Agent Execution local recovery", "model fleet fallback", "false completion prevention"],
"allowed_change_scope": {"modules": ["evidence"], "paths": ["docs/evidence/local-fallback-gateway-live-2026-08-23-v1.json", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000238-local-fallback-evidence.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "LLM-local/**", "modules/work/secrets/**", "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": ["creator advisory gateway", "durable cognitive usage observation"], "may_change": []},
"semantic_owner": "Evidence only",
"canonical_state": ["no runtime state mutation; read-only evidence capture"],
"state_transitions": ["normal route unavailable or not eligible -> builtin local lifeboat response"],
"side_effects_and_privileges": ["bounded local advisory request; no external egress and no secret exposure"],
"persistence_and_migrations": ["existing cognitive usage observation only"],
"failure_retry_restart": ["live request is bounded and idempotent in effect"],
"assumptions": ["the production database observation is the authoritative route attribution"],
"acceptance_scenarios": ["local response is COMPLETED", "usage evidence identifies builtin-local-lifeboat-v1", "no claim of external bridge or multi-model failover"],
"required_tests": ["go test ./cmd/core-runtime/...", "go vet ./cmd/core-runtime/...", "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", "independent", "CI", "production health smoke"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000239",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "6061eb5c13e46cec1e60fc104b022394f0d94dee",
"risk_tier": "E",
"intent": "Resolve a registered provider by its stable instance identity when a persisted Capability revision contains stale non-secret endpoint credential metadata, while preserving exact metadata matching first.",
"requirement_ids": ["model fleet/failover", "credential rotation compatibility", "local fallback attribution"],
"allowed_change_scope": {"modules": ["cognitive-worker"], "paths": ["cmd/cognitive-worker/gateway.go", "cmd/cognitive-worker/gateway_registry_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000239-stable-provider-resolution.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "LLM-local/**", "modules/work/secrets/**", "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": ["Capability ResolvedCognitiveBinding", "protected provider registration"], "may_change": ["internal provider lookup fallback order"]},
"semantic_owner": "Cognitive worker provider adapter boundary",
"canonical_state": ["no new durable state"],
"state_transitions": ["exact provider metadata match -> stable provider identity match -> no provider"],
"side_effects_and_privileges": ["provider lookup only; secret values remain in protected runtime"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["missing provider remains explicit and uses existing builtin fallback"],
"assumptions": ["ProviderInstanceID is the stable Capability-owned provider identity"],
"acceptance_scenarios": ["stale credential metadata still reaches current registered adapter", "exact metadata match retains priority", "no provider remains nil"],
"required_tests": ["go test ./cmd/cognitive-worker/...", "go vet ./cmd/cognitive-worker/...", "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", "independent", "CI", "production health smoke"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000240",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "383d4c6e4fd53bb0c34ed358e0d1be6a416da6cc",
"risk_tier": "E",
"intent": "Record live durable evidence of CPU model failure followed by successful local Lifeboat failover after stable provider resolution was deployed.",
"requirement_ids": ["model fleet/failover", "durable usage evidence", "false completion prevention"],
"allowed_change_scope": {"modules": ["evidence"], "paths": ["docs/evidence/cpu-lifeboat-failover-2026-08-23-v1.json", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000240-cpu-lifeboat-failover-evidence.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "LLM-local/**", "modules/work/secrets/**", "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": ["cognitive advisory gateway", "cognitive usage observations"], "may_change": []},
"semantic_owner": "Evidence only",
"canonical_state": ["no runtime state mutation; evidence captures existing durable observations"],
"state_transitions": ["CPU MODEL_UNAVAILABLE -> builtin local Lifeboat COMPLETED"],
"side_effects_and_privileges": ["bounded LOCAL_ONLY advisory request; no external egress"],
"persistence_and_migrations": ["existing usage observation persistence only"],
"failure_retry_restart": ["the fallback is selected within the bounded gateway request"],
"assumptions": ["provider_instance_id and result_class in capability usage observations are authoritative route attribution"],
"acceptance_scenarios": ["primary failure is durably observed", "fallback completion is durably observed", "HTTP gateway returns COMPLETED", "public health remains OK"],
"required_tests": ["go test ./cmd/cognitive-worker/...", "go vet ./cmd/cognitive-worker/...", "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", "independent", "CI", "production health smoke"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000241",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "ca0d0020739d8f70e2a3ac04820e32794072ab30",
"risk_tier": "E",
"intent": "Record bounded medium and GPU qualification observations without treating hardware presence or reachability as quality or promotion evidence.",
"requirement_ids": ["model fleet inventory", "provider-neutral qualification", "hardware-neutral routing"],
"allowed_change_scope": {"modules": ["evidence"], "paths": ["docs/evidence/fleet-qualification-2026-08-23-v1.json", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000241-fleet-qualification-evidence.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "LLM-local/**", "modules/work/secrets/**", "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": ["provider discovery status", "bounded qualification response"], "may_change": []},
"semantic_owner": "Evidence only",
"canonical_state": ["no runtime state mutation"],
"state_transitions": ["discovered model -> bounded reachability observation -> UNRATED result"],
"side_effects_and_privileges": ["bounded provider invocation; no promotion, activation or routing mutation"],
"persistence_and_migrations": ["evidence artifact only"],
"failure_retry_restart": ["one model timeout does not cancel or misclassify other tier evidence"],
"assumptions": ["provider discovery IDs are the exact IDs returned by the live worker"],
"acceptance_scenarios": ["medium reachability records latency", "GPU timeout is explicit", "both results remain UNRATED", "GPU does not receive an implicit priority"],
"required_tests": ["go test ./cmd/cognitive-worker/...", "go vet ./cmd/cognitive-worker/...", "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", "independent", "CI", "production health smoke"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000242",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "e4ba1cb5fabf930396b2aa9f95d3b4b205e6319d",
"risk_tier": "E",
"intent": "Persist provider-neutral model qualification observations through the Capability owner while keeping quality unrated until independent task evaluation.",
"requirement_ids": ["durable qualification evidence", "model fleet/failover", "quality separated from reachability"],
"allowed_change_scope": {"modules": ["capability", "core-runtime", "cognitive-worker", "database"], "paths": ["adapters/postgres/capability_repository.go", "cmd/cognitive-worker/main.go", "cmd/cognitive-worker/model_qualification.go", "cmd/cognitive-worker/model_qualification_test.go", "cmd/core-runtime/main.go", "contracts/capability/model_qualification.go", "database/migrations/045_capability_model_qualification_observations.sql", "modules/capability/ports/repository.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000242-durable-qualification-observations.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "LLM-local/**", "modules/work/secrets/**", "contracts/work/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
"contracts": {"consume": ["bounded qualification result", "Capability repository boundary"], "may_change": ["typed model qualification observation contract", "internal core persistence route"]},
"semantic_owner": "Capability owns qualification evidence; cognitive-worker produces observations",
"canonical_state": ["cognitive_model_qualification_observations"],
"state_transitions": ["probe -> REACHABLE/RESPONDED_WITHOUT_QUALIFICATION/UNAVAILABLE; category remains UNRATED"],
"side_effects_and_privileges": ["bounded provider call and capability-owned INSERT; no promotion, activation or secret persistence"],
"persistence_and_migrations": ["migration 045 creates capability-owned observation table and index"],
"failure_retry_restart": ["observation persistence failure returns bounded gateway error; duplicate IDs are idempotent"],
"assumptions": ["independent task evaluation remains the only quality/promotion evidence"],
"acceptance_scenarios": ["qualification route persists one observation per probed model", "invalid quality/category is rejected", "prompt/output/error text is not persisted", "existing qualification tests remain valid"],
"required_tests": ["go test ./cmd/cognitive-worker/... ./cmd/core-runtime/... ./modules/capability/... ./adapters/postgres/...", "go vet ./cmd/cognitive-worker/... ./cmd/core-runtime/... ./modules/capability/... ./adapters/postgres/...", "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", "independent", "CI", "production health smoke"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP", "REQUIRES_MIGRATION_DECISION"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000243",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "47327cc6d853da9084c96e3bf48502266706ca57",
"risk_tier": "E",
"intent": "Make Capability selection consume the latest durable qualification observation for fresh unavailability exclusion and transparent latency rating, without granting quality or activation.",
"requirement_ids": ["qualification selection loop", "hardware-neutral routing", "quality separated from reachability"],
"allowed_change_scope": {"modules": ["capability", "adapters/postgres"], "paths": ["adapters/postgres/capability_repository.go", "contracts/capability/cognitive_registry.go", "modules/capability/domain/cognitive_registry.go", "modules/capability/domain/model_rating_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000243-qualification-selection-evidence.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "LLM-local/**", "modules/work/secrets/**", "contracts/work/**"]},
"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": ["durable ModelQualificationObservation"], "may_change": ["resolved binding evidence projection"]},
"semantic_owner": "Capability selection and rating",
"canonical_state": ["qualification observation remains evidence; no activation mutation"],
"state_transitions": ["fresh UNAVAILABLE observation -> candidate excluded", "stale UNAVAILABLE observation -> discovery state remains authoritative", "REACHABLE observation -> latency evidence only"],
"side_effects_and_privileges": ["read-only projection and deterministic selection"],
"persistence_and_migrations": ["uses migration 045 from DEV-000242"],
"failure_retry_restart": ["missing observation preserves existing behavior"],
"assumptions": ["provider/model observed timestamps define freshness boundary"],
"acceptance_scenarios": ["fresh unavailable candidate is not selected", "stale unavailable candidate is not permanently poisoned", "quality remains task-evaluation controlled"],
"required_tests": ["go test ./modules/capability/... ./adapters/postgres/...", "go vet ./modules/capability/... ./adapters/postgres/...", "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", "independent", "CI", "production health smoke"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
@@ -0,0 +1,26 @@
{
"task_id": "DEV-000244",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "c81e2269e157c1a41b567d79d93bbbe49dd4b134",
"risk_tier": "E",
"intent": "Expose bounded health and status evidence for the durable reflection worker without changing reflection semantics or authority.",
"requirement_ids": ["learning reflection loop", "runtime observability", "fast operational verification"],
"allowed_change_scope": {
"modules": ["epistemics"],
"paths": ["cmd/reflection-worker/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000244-reflection-worker-observability.json"]
},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "LLM-local/**", "modules/work/secrets/**", "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 reflection repository port", "runtime health conventions"], "may_change": ["worker operational HTTP surface"]},
"semantic_owner": "Epistemics reflection worker runtime",
"canonical_state": ["none; status is derived operational evidence"],
"state_transitions": ["cycle start/complete/failure are reported without changing candidate lifecycle rules"],
"side_effects_and_privileges": ["read-only health/status endpoint; existing repository writes unchanged"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["status resets on process restart; durable candidate state remains authoritative"],
"assumptions": ["internal production network is sufficient for worker status checks"],
"acceptance_scenarios": ["health endpoint returns 200", "status exposes loop mode and latest cycle", "reflection loop still resolves candidates"],
"required_tests": ["go test ./cmd/reflection-worker", "go vet ./cmd/reflection-worker", "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", "independent", "CI", "production health smoke"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
+2 -2
View File
@@ -24,8 +24,8 @@
- `COMPLETION_MATRIX = docs/evidence/audit-completion-matrix-2026-08-23-v3.json`; all seven objective areas are mapped to current repository/production proof, with external bridge, GPU promotion/quality and deep-acceptance gaps explicit.
- `ACTIVE_TASK = dev/tasks/DEV-000229-research-runtime-bridge.json revision 1`; authenticated Core now has a bounded adapter route to the existing Truth/Research runtime. The epistemics owner remains responsible for persistence, freshness, citations and conflicts.
- `ACTIVE_TASK = dev/tasks/DEV-000235-model-qualification-probe.json revision 1`; cognitive-worker now exposes a bounded provider-neutral qualification probe that discovers actual model IDs, invokes them in parallel, reports reachability/latency and keeps quality `UNRATED` until independent task tests. Hardware presence does not grant promotion or priority.
- `LINEAGE_DECISION = docs/GOVERNANCE_LINEAGE_DECISION_2026-08-22.md`; historical packets remain provenance and are not current authority.
- `REGISTRY_STATUS = YAML and JSON semantic mirrors agree`
- `PRODUCTION_DOMAIN = docker.cin.su`; shared docker-test is not used for this project.
- `NEXT_WORK = qualify an independently operated Agent Execution bridge when available and complete deferred deep browser acceptance with a current signed-in session; retain local fallback and general-first task evaluation when either capability is unavailable.`
- `NEXT_WORK = use qualification evidence in the provider-neutral Capability rating/selection loop, then continue authenticated UI acceptance, reflection recovery and governed self-development gaps.`
@@ -0,0 +1,18 @@
{
"evidence_id": "authenticated-ui-section-smoke-2026-08-23-v1",
"observed_at": "2026-08-23",
"source": "current authenticated in-app browser session against https://assistant.cin.su/admin",
"production_revision_checked": "be01b6b883c2ce6e9bb47bc851505fe222c46d14",
"authenticated": true,
"sections": {
"overview": {"loaded": true, "expected_regions": ["Сводка состояния", "Подтвердить исполнение"], "visible_error_count": 0},
"diagnostics": {"loaded": true, "expected_regions": ["Диагностика кабинета", "Спросить Домового"], "visible_error_count": 0},
"operations": {"loaded": true, "expected_regions": ["Реестр ожиданий", "Модели и fallback", "Агентская работа", "Журнал действий"], "visible_error_count": 0},
"learning": {"loaded": true, "expected_regions": ["Опыт и reflection", "Experience", "Reflection", "Skills"], "visible_error_count": 0},
"settings": {"loaded": true, "expected_regions": ["Настройки рабочего кабинета"], "visible_error_count": 0}
},
"mutations_performed": false,
"secrets_exposed": false,
"status": "AUTHENTICATED_QUICK_SECTION_SMOKE_PASS",
"not_proven": ["full mobile and accessibility acceptance", "offline/session-expiry acceptance", "delegation-specific mutation", "external bridge availability"]
}
@@ -0,0 +1,15 @@
{
"evidence_id": "cpu-lifeboat-failover-2026-08-23-v1",
"observed_at": "2026-08-23",
"production_revision": "383d4c6e4fd53bb0c34ed358e0d1be6a416da6cc",
"request": {"surface": "internal cognitive-worker advisory gateway", "data_policy": "LOCAL_ONLY", "max_output_bytes": 128, "prompt_class": "bounded readiness response"},
"http_result": {"status": 200, "finish_reason": "COMPLETED", "output_observed": true},
"durable_route_observations": [
{"provider_instance_id": "local-cpu-primary", "result_class": "MODEL_UNAVAILABLE", "failure_type": "MODEL_UNAVAILABLE", "latency_millis": 580},
{"provider_instance_id": "builtin-local-lifeboat-v1", "result_class": "COMPLETED", "failure_type": null, "latency_millis": 455}
],
"public_health": {"url": "https://assistant.cin.su/healthz", "status": 200, "runtime": "creator-web"},
"status": "CPU_TO_LIFEBOAT_FAILOVER_LIVE_PASS",
"secrets_exposed": false,
"remaining": ["medium/GPU validated multi-model failover", "quality-based promotion", "long-running failover and restart chaos"]
}
@@ -0,0 +1,14 @@
{
"evidence_id": "fleet-qualification-2026-08-23-v1",
"observed_at": "2026-08-23",
"production_revision": "ca0d0020739d8f70e2a3ac04820e32794072ab30",
"discovery": {"cpu": "AVAILABLE", "medium": "AVAILABLE", "gpu": "AVAILABLE", "lifeboat": "AVAILABLE"},
"probes": [
{"provider": "local-medium-primary", "provider_model_id": "Qwen3.6-27B-UD-Q4_K_XL", "status": "REACHABLE", "finish_reason": "COMPLETED", "latency_millis": 8541, "output_observed": true, "quality_assessed": false, "category": "UNRATED"},
{"provider": "local-gpu-opportunistic", "provider_model_id": "Qwen3.8-27B-UD-Q4_K_XL", "status": "RESPONDED_WITHOUT_QUALIFICATION", "finish_reason": "TIMEOUT", "latency_millis": 10006, "output_observed": false, "quality_assessed": false, "category": "UNRATED"}
],
"status": "FLEET_REACHABILITY_OBSERVED_QUALITY_UNRATED",
"interpretation": "The medium provider is reachable for this bounded probe; the selected GPU model timed out. Hardware presence does not grant priority or promotion.",
"remaining": ["durable qualification result persistence", "independent task-quality evaluation for medium/GPU", "GPU retry/failover under sustained load"],
"secrets_exposed": false
}
@@ -0,0 +1,12 @@
{
"evidence_id": "local-fallback-gateway-live-2026-08-23-v1",
"observed_at": "2026-08-23",
"production_revision": "0609561dfefb6dce302f56d50bb56bd5a222fb64",
"request": {"surface": "internal cognitive-worker advisory gateway", "data_policy": "LOCAL_ONLY", "max_output_bytes": 128, "prompt_class": "bounded readiness response"},
"http_result": {"status": 200, "finish_reason": "COMPLETED", "output_observed": true, "output_content": "готов"},
"durable_usage_observation": {"provider_instance_id": "builtin-local-lifeboat-v1", "model_descriptor_id": "builtin-local-model-v1", "result_class": "COMPLETED", "failure_type": null, "latency_millis": 665},
"status": "LOCAL_FALLBACK_LIVE_PASS",
"proves": ["local lifeboat path can answer when normal route is not eligible", "completion is durably attributed to the local fallback"],
"does_not_prove": ["external Agent Execution bridge", "multi-model failover between validated provider bindings", "GPU quality or promotion", "long-running recovery"],
"secrets_exposed": false
}
@@ -148,6 +148,9 @@ func SelectCognitiveBinding(requirement capability.CognitiveRequirement, candida
if candidate.Evaluation != nil && candidate.Evaluation.Result == capability.EvaluationUnsupported {
continue
}
if candidate.Qualification != nil && candidate.Qualification.Status == capability.ModelQualificationUnavailable && candidate.Qualification.ObservedAtUnixMillis >= maxObservedAt(candidate.Provider.ObservedAtUnixMillis, candidate.Model.ObservedAtUnixMillis) {
continue
}
if requirement.TaskProfileID != "" && (candidate.TaskEvaluation == nil || candidate.TaskEvaluation.ProfileID != requirement.TaskProfileID || candidate.TaskEvaluation.Result != capability.EvaluationSupported) {
continue
}
@@ -195,10 +198,14 @@ func RateCognitiveBinding(candidate capability.ResolvedCognitiveBinding) capabil
latency := int64(500)
if candidate.Performance != nil && candidate.Performance.LatencyMicros != nil {
latency = latencyScore(*candidate.Performance.LatencyMicros)
} else if candidate.Qualification != nil && candidate.Qualification.Status == capability.ModelQualificationReachable {
latency = latencyScore(candidate.Qualification.LatencyMillis * 1000)
}
evidence := int64(700)
if candidate.Performance != nil {
evidence = 1000
} else if candidate.Qualification != nil && candidate.Qualification.Status == capability.ModelQualificationReachable {
evidence = 750
}
overall := (quality*55 + reliability*25 + latency*15 + evidence*5) / 100
category := capability.ModelRatingBalanced
@@ -210,6 +217,13 @@ func RateCognitiveBinding(candidate capability.ResolvedCognitiveBinding) capabil
return capability.ModelRating{Category: category, OverallScoreMillis: overall, QualityScoreMillis: quality, ReliabilityScoreMillis: reliability, LatencyScoreMillis: latency, EvidenceScoreMillis: evidence}
}
func maxObservedAt(left, right int64) int64 {
if left > right {
return left
}
return right
}
func clampMillis(value int64) int64 {
if value < 0 {
return 0
@@ -38,3 +38,22 @@ func TestRateCognitiveBindingClassifiesSlowEvidenceAsBalanced(t *testing.T) {
t.Fatalf("rating=%+v", rating)
}
}
func TestSelectionExcludesFreshUnavailableQualificationButNotStaleEvidence(t *testing.T) {
quality, critical := int64(900), int64(0)
base := capability.ResolvedCognitiveBinding{
Binding: capability.CognitiveBinding{ID: "binding-qualification", Revision: 1, CapabilityID: "creator-advisory", OwnerPrincipalID: "owner", ProviderInstanceID: "provider", ProviderRevision: 1, ModelDescriptorID: "model", ModelRevision: 1, DataPolicy: capability.DataPolicyLocalOnly, Qualification: capability.QualificationValidated, Activation: capability.ActivationActive, Health: capability.HealthAvailable, LimitPolicyRef: "limits"},
Provider: capability.ProviderInstance{ID: "provider", Revision: 1, Kind: "local", EndpointRef: "endpoint", Enabled: true, Qualification: capability.QualificationValidated, Health: capability.HealthAvailable, ObservedAtUnixMillis: 20},
Model: capability.ModelDescriptor{ID: "model", Revision: 1, ProviderInstanceID: "provider", ProviderModelID: "model", MaxContextBytes: 1000, Modalities: []capability.ModelModality{capability.ModalityText}, Capabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, Qualification: capability.QualificationValidated, Health: capability.HealthAvailable, ObservedAtUnixMillis: 20},
Evaluation: &capability.EvaluatedCapability{ID: "evaluation", BindingID: "binding-qualification", BindingRevision: 1, CapabilityID: "creator-advisory", Result: capability.EvaluationSupported, QualityScoreMillis: &quality, CriticalFailureMillis: &critical},
Qualification: &capability.ModelQualificationObservation{ID: "observation", ProviderInstanceID: "provider", ProviderModelID: "model", Status: capability.ModelQualificationUnavailable, FinishReason: "TIMEOUT", ObservedAtUnixMillis: 20, Category: capability.ModelRatingUnrated},
}
requirement := capability.CognitiveRequirement{CapabilityID: "creator-advisory", OwnerPrincipalID: "owner", DataPolicy: capability.DataPolicyLocalOnly, RequiredModalities: []capability.ModelModality{capability.ModalityText}, RequiredCapabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, MaxOutputBytes: 10}
if _, err := SelectCognitiveBinding(requirement, []capability.ResolvedCognitiveBinding{base}); err == nil {
t.Fatal("fresh unavailable qualification should exclude candidate")
}
base.Qualification.ObservedAtUnixMillis = 19
if _, err := SelectCognitiveBinding(requirement, []capability.ResolvedCognitiveBinding{base}); err != nil {
t.Fatalf("stale unavailable qualification should not exclude candidate: %v", err)
}
}
+1
View File
@@ -27,4 +27,5 @@ type Repository interface {
SaveEvaluatedCapability(context.Context, capability.EvaluatedCapability) error
SavePerformanceEvidence(context.Context, capability.PerformanceEvidence) error
SaveTaskEvaluation(context.Context, capability.TaskEvaluation) error
SaveModelQualificationObservation(context.Context, capability.ModelQualificationObservation) error
}
+76
View File
@@ -15,6 +15,7 @@ import (
var ErrGraphDispatchBinding = errors.New("graph node dispatch binding is missing")
var ErrTaskEvaluationCallbackMismatch = errors.New("task evaluation callback does not match graph node")
var ErrGraphResultCallbackMismatch = errors.New("graph result callback does not match graph node")
type GraphScheduleRequest struct {
Graph work.WorkGraph `json:"graph"`
@@ -35,6 +36,12 @@ type GraphCompleteRequest struct {
Succeeded bool `json:"succeeded"`
}
type GraphCompletionResult struct {
Graph work.WorkGraph `json:"graph"`
NextSearchDecision work.SolutionSearchDecision `json:"next_search_decision"`
ReplanRequired bool `json:"replan_required"`
}
type GraphSynthesisRequest struct {
Conflict work.ConflictSet `json:"conflict"`
Candidates []work.ResultCandidate `json:"candidates"`
@@ -82,6 +89,51 @@ func SynthesizeGraph(ctx context.Context, store ports.GraphResultStore, request
return conflict, synthesis, nil
}
// RecordGraphResultCandidate persists attributable agent output. The graph is
// immutable by revision, so candidate recording does not rewrite the node;
// only verified synthesis may create the next successful graph revision.
func RecordGraphResultCandidate(ctx context.Context, candidateStore ports.GraphCandidateStore, graphStore ports.GraphStore, request work.GraphResultCallback) (work.WorkGraph, error) {
candidate := request.Candidate
if candidateStore == nil || request.GraphID == "" || request.Revision < 1 || candidate.ID == "" || candidate.GraphID != request.GraphID || candidate.GraphRevision != request.Revision || candidate.NodeID == "" || candidate.AuthorRunID == "" || candidate.EvidenceSummary == "" || candidate.Summary == "" {
return work.WorkGraph{}, ErrGraphResultCallbackMismatch
}
graph, err := graphStore.LoadWorkGraph(ctx, request.GraphID, request.Revision)
if err != nil {
return work.WorkGraph{}, err
}
found := false
for i := range graph.Nodes {
if graph.Nodes[i].ID != candidate.NodeID {
continue
}
found = true
if graph.Nodes[i].State == work.GraphNodeSuccess || graph.Nodes[i].State == work.GraphNodeCancel {
return work.WorkGraph{}, ErrGraphResultCallbackMismatch
}
}
if !found {
return work.WorkGraph{}, ErrGraphResultCallbackMismatch
}
if err := candidateStore.SaveGraphResultCandidate(ctx, candidate); err != nil {
return work.WorkGraph{}, err
}
return graph, nil
}
// SynthesizeAndCompleteGraph is the only path that turns a verifying node
// into success: synthesis must be independently verified first.
func SynthesizeAndCompleteGraph(ctx context.Context, resultStore ports.GraphResultStore, graphStore ports.GraphStore, request GraphSynthesisRequest) (work.ConflictSet, work.GraphSynthesisCandidate, work.WorkGraph, error) {
conflict, synthesis, err := SynthesizeGraph(ctx, resultStore, request)
if err != nil {
return work.ConflictSet{}, work.GraphSynthesisCandidate{}, work.WorkGraph{}, err
}
graph, err := CompleteGraphNode(ctx, graphStore, conflict.GraphID, conflict.GraphRevision, conflict.NodeID, true)
if err != nil {
return work.ConflictSet{}, work.GraphSynthesisCandidate{}, work.WorkGraph{}, err
}
return conflict, synthesis, graph, nil
}
func AdmitAndSchedule(ctx context.Context, store ports.GraphStore, request GraphScheduleRequest) (work.WorkGraph, []work.GraphNodeID, error) {
if request.Now < 1 {
request.Now = time.Now().UnixMilli()
@@ -231,3 +283,27 @@ func CompleteGraphNode(ctx context.Context, store ports.GraphStore, graphID stri
}
return graph, nil
}
// CompleteGraphNodeWithReplan evaluates the persisted search snapshot after a
// verified graph completion. It does not silently invent dispatch intents;
// the returned decision is the durable hand-off to the next wave planner.
func CompleteGraphNodeWithReplan(ctx context.Context, store ports.GraphStore, request GraphCompleteRequest) (GraphCompletionResult, error) {
graph, err := CompleteGraphNode(ctx, store, request.GraphID, request.Revision, request.NodeID, request.Succeeded)
if err != nil {
return GraphCompletionResult{}, err
}
result := GraphCompletionResult{Graph: graph}
if !graph.SearchInputBound || graph.State != work.GraphCompleted {
return result, nil
}
input := graph.SearchInput
input.ActiveOptionCount = 0
input.Unresolved = true
decision, err := DecideSolutionSearch(input)
if err != nil {
return GraphCompletionResult{}, err
}
result.NextSearchDecision = decision
result.ReplanRequired = decision.ContinueSearching
return result, nil
}
+39
View File
@@ -0,0 +1,39 @@
package application
import (
"context"
"git.cin.su/m/Assistant/contracts/work"
"git.cin.su/m/Assistant/modules/work/domain"
"git.cin.su/m/Assistant/modules/work/ports"
"time"
)
func DecideSolutionSearch(input work.SolutionSearchInput) (work.SolutionSearchDecision, error) {
return domain.DecideSolutionSearch(input)
}
func OrchestrateSolutionSearch(ctx context.Context, store ports.GraphStore, request work.SolutionSearchWaveRequest) (work.SolutionSearchWaveResult, error) {
decision, err := domain.DecideSolutionSearch(request.Input)
if err != nil {
return work.SolutionSearchWaveResult{}, err
}
result := work.SolutionSearchWaveResult{Decision: decision}
if !decision.ContinueSearching || decision.State == work.OrchestrationExecuting {
return result, nil
}
graph, intents, err := domain.BuildSolutionSearchWave(decision, request)
if err != nil {
return work.SolutionSearchWaveResult{}, err
}
if request.NowUnixMillis < 1 {
request.NowUnixMillis = time.Now().UnixMilli()
}
graph, ready, err := AdmitAndSchedule(ctx, store, GraphScheduleRequest{Graph: graph, Intents: intents, Now: request.NowUnixMillis})
if err != nil {
return work.SolutionSearchWaveResult{}, err
}
result.Graph = graph
result.ScheduledNodeIDs = ready
result.GraphMaterialized = true
return result, nil
}
+93
View File
@@ -0,0 +1,93 @@
package domain
import (
"errors"
"fmt"
"git.cin.su/m/Assistant/contracts/work"
)
var ErrInvalidSolutionSearch = errors.New("invalid solution search input")
// DecideSolutionSearch keeps an unresolved goal alive across bounded waves.
// A wave has limits, but an unresolved goal is never converted into FAILED
// merely because one wave is exhausted. The caller persists the decision and
// schedules the next wave through the durable Work Graph.
func DecideSolutionSearch(input work.SolutionSearchInput) (work.SolutionSearchDecision, error) {
if input.GoalID == "" || input.SearchRound < 0 || input.MaxRound < 1 || input.ActiveOptionCount < 0 {
return work.SolutionSearchDecision{}, ErrInvalidSolutionSearch
}
decision := work.SolutionSearchDecision{GoalID: input.GoalID, NextSearchRound: input.SearchRound}
if input.CancelRequested || input.GoalState == work.GoalCancelled {
decision.State = work.OrchestrationCancelled
decision.Reason = "goal cancellation is durable and stops further search waves"
return decision, nil
}
if input.GoalState == work.GoalCompleted && input.VerificationReady {
decision.State = work.OrchestrationCompleted
decision.Reason = "goal has an independently verified accepted result"
return decision, nil
}
if input.ActiveOptionCount > 0 {
decision.State = work.OrchestrationExecuting
decision.ContinueSearching = true
decision.Options = []work.SearchOptionKind{work.SearchOptionAgents}
decision.Reason = "current solution wave is still running"
return decision, nil
}
decision.State = work.OrchestrationSearching
decision.ContinueSearching = true
decision.NextSearchRound++
decision.Options = []work.SearchOptionKind{work.SearchOptionAgents, work.SearchOptionCapability, work.SearchOptionResearch, work.SearchOptionReflection}
if input.Unresolved {
decision.Reason = "previous wave did not establish a verified solution; start all independent search avenues"
} else {
decision.Reason = "no verified result exists; start the first parallel solution wave"
}
if input.SearchRound >= input.MaxRound {
decision.State = work.OrchestrationWaiting
decision.Options = append(decision.Options, work.SearchOptionHuman)
decision.Reason = "bounded wave budget exhausted; continue with a new durable wave and human attention, never claim failure"
}
return decision, nil
}
var ErrInvalidSolutionSearchWave = errors.New("invalid solution search wave")
// BuildSolutionSearchWave turns a policy decision into a durable Work Graph
// fan-out. Human attention remains in the decision but is not an agent dispatch.
func BuildSolutionSearchWave(decision work.SolutionSearchDecision, request work.SolutionSearchWaveRequest) (work.WorkGraph, []work.AgentDispatchIntent, error) {
if !decision.ContinueSearching || (decision.State != work.OrchestrationSearching && decision.State != work.OrchestrationWaiting) || request.GraphID == "" || request.IdempotencyPrefix == "" || request.Budget.MaxParallel < 1 || request.Budget.MaxAttempts < 1 {
return work.WorkGraph{}, nil, ErrInvalidSolutionSearchWave
}
seen := make(map[work.SearchOptionKind]struct{}, len(request.Dispatches))
nodes := make([]work.GraphNode, 0, len(request.Dispatches))
intents := make([]work.AgentDispatchIntent, 0, len(request.Dispatches))
for index, dispatch := range request.Dispatches {
if dispatch.Option == work.SearchOptionHuman || dispatch.Option == "" || dispatch.Intent.DispatchID == "" || dispatch.Intent.IdempotencyKey == "" {
return work.WorkGraph{}, nil, ErrInvalidSolutionSearchWave
}
if _, exists := seen[dispatch.Option]; exists || !containsSearchOption(decision.Options, dispatch.Option) {
return work.WorkGraph{}, nil, ErrInvalidSolutionSearchWave
}
seen[dispatch.Option] = struct{}{}
intent := dispatch.Intent
intent.SpecialistPurpose = string(dispatch.Option)
intents = append(intents, intent)
nodes = append(nodes, work.GraphNode{ID: work.GraphNodeID(fmt.Sprintf("%s-node-%d", request.GraphID, index+1)), DispatchID: intent.DispatchID, IdempotencyKey: fmt.Sprintf("%s:%s:%d", request.IdempotencyPrefix, dispatch.Option, decision.NextSearchRound), State: work.GraphNodeBlocked, Purpose: work.GraphNodePurposeGeneric})
}
if len(nodes) == 0 {
return work.WorkGraph{}, nil, ErrInvalidSolutionSearchWave
}
searchInput := request.Input
searchInput.SearchRound = decision.NextSearchRound
return work.WorkGraph{ID: request.GraphID, Revision: 1, State: work.GraphDraft, Nodes: nodes, Budget: request.Budget, ParentGraphID: request.ParentGraphID, ParentGraphRevision: request.ParentGraphRevision, TriggerEvidenceRef: request.TriggerEvidenceRef, SearchInputBound: true, SearchInput: searchInput}, intents, nil
}
func containsSearchOption(options []work.SearchOptionKind, target work.SearchOptionKind) bool {
for _, option := range options {
if option == target {
return true
}
}
return false
}
+59
View File
@@ -0,0 +1,59 @@
package domain
import (
"fmt"
"git.cin.su/m/Assistant/contracts/work"
"testing"
)
func TestSolutionSearchDoesNotDeadEndAfterBudgetedWave(t *testing.T) {
decision, err := DecideSolutionSearch(work.SolutionSearchInput{GoalID: "goal-1", GoalState: work.GoalInProgress, SearchRound: 3, MaxRound: 3, Unresolved: true})
if err != nil {
t.Fatal(err)
}
if !decision.ContinueSearching || decision.State != work.OrchestrationWaiting || decision.NextSearchRound != 4 {
t.Fatalf("unexpected exhausted-wave decision: %#v", decision)
}
if len(decision.Options) != 5 || decision.Options[4] != work.SearchOptionHuman {
t.Fatalf("all search avenues were not retained: %#v", decision.Options)
}
}
func TestSolutionSearchStopsOnlyForCancelOrVerifiedCompletion(t *testing.T) {
completed, err := DecideSolutionSearch(work.SolutionSearchInput{GoalID: "goal-1", GoalState: work.GoalCompleted, VerificationReady: true, MaxRound: 1})
if err != nil || completed.ContinueSearching || completed.State != work.OrchestrationCompleted {
t.Fatalf("verified completion was not terminal: %#v %v", completed, err)
}
cancelled, err := DecideSolutionSearch(work.SolutionSearchInput{GoalID: "goal-1", GoalState: work.GoalInProgress, CancelRequested: true, MaxRound: 1})
if err != nil || cancelled.ContinueSearching || cancelled.State != work.OrchestrationCancelled {
t.Fatalf("cancel was not terminal: %#v %v", cancelled, err)
}
}
func TestBuildSolutionSearchWaveMaterializesParallelAgentDirections(t *testing.T) {
decision, err := DecideSolutionSearch(work.SolutionSearchInput{GoalID: "goal-1", GoalState: work.GoalInProgress, MaxRound: 1})
if err != nil {
t.Fatal(err)
}
options := []work.SearchOptionKind{work.SearchOptionAgents, work.SearchOptionCapability, work.SearchOptionResearch, work.SearchOptionReflection}
dispatches := make([]work.SolutionSearchDispatch, 0, len(options))
for i, option := range options {
dispatches = append(dispatches, work.SolutionSearchDispatch{Option: option, Intent: work.AgentDispatchIntent{DispatchID: workDispatchID(i), IdempotencyKey: workDispatchID(i) + ":idem"}})
}
graph, intents, err := BuildSolutionSearchWave(decision, work.SolutionSearchWaveRequest{GraphID: "graph-1", IdempotencyPrefix: "goal-1", Dispatches: dispatches, Budget: work.GraphBudget{MaxParallel: 4, MaxAttempts: 2}})
if err != nil {
t.Fatal(err)
}
if len(graph.Nodes) != 4 || len(intents) != 4 || graph.State != work.GraphDraft {
t.Fatalf("unexpected materialized wave: %#v %#v", graph, intents)
}
for i, node := range graph.Nodes {
if node.State != work.GraphNodeBlocked || intents[i].SpecialistPurpose != string(options[i]) {
t.Fatalf("wave direction not preserved: node=%#v intent=%#v", node, intents[i])
}
}
}
func workDispatchID(index int) string {
return fmt.Sprintf("dispatch-%d", index)
}
+4
View File
@@ -18,6 +18,10 @@ type GraphResultStore interface {
SaveGraphSynthesis(context.Context, work.ConflictSet, []work.ResultCandidate, work.GraphSynthesisCandidate) error
}
type GraphCandidateStore interface {
SaveGraphResultCandidate(context.Context, work.ResultCandidate) error
}
// TaskEvaluationSink is a remote-capable boundary to Capability's owner.
// Work never writes the capability schema directly.
type TaskEvaluationSink interface {
+41 -3
View File
@@ -12,9 +12,11 @@ import (
)
type graphStore struct {
graphs map[string]work.WorkGraph
queued []string
intents []string
graphs map[string]work.WorkGraph
queued []string
intents []string
candidates []work.ResultCandidate
syntheses int
}
func (s *graphStore) SaveWorkGraph(_ context.Context, graph work.WorkGraph) error {
@@ -35,6 +37,14 @@ func (s *graphStore) EnqueueDispatch(_ context.Context, item work.AgentDispatchQ
s.queued = append(s.queued, item.DispatchID)
return nil
}
func (s *graphStore) SaveGraphResultCandidate(_ context.Context, candidate work.ResultCandidate) error {
s.candidates = append(s.candidates, candidate)
return nil
}
func (s *graphStore) SaveGraphSynthesis(_ context.Context, _ work.ConflictSet, _ []work.ResultCandidate, _ work.GraphSynthesisCandidate) error {
s.syntheses++
return nil
}
func itoa(value int64) string {
return strconv.FormatInt(value, 10)
@@ -84,6 +94,34 @@ func TestSynthesizeGraphPersistsVerifiedDissent(t *testing.T) {
}
}
func TestGraphResultCallbackRequiresIndependentSynthesisBeforeSuccess(t *testing.T) {
store := &graphStore{}
graph, _, err := application.AdmitAndSchedule(context.Background(), store, application.GraphScheduleRequest{Graph: work.WorkGraph{ID: "result-graph", Revision: 1, Budget: work.GraphBudget{MaxParallel: 1, MaxAttempts: 1}, Nodes: []work.GraphNode{{ID: "node-1", DispatchID: "dispatch-1", IdempotencyKey: "node-1", State: work.GraphNodeReady}}}, Intents: []work.AgentDispatchIntent{{DispatchID: "dispatch-1"}}, Now: 1})
if err != nil {
t.Fatal(err)
}
returned, err := application.RecordGraphResultCandidate(context.Background(), store, store, work.GraphResultCallback{GraphID: graph.ID, Revision: graph.Revision, Candidate: work.ResultCandidate{ID: "candidate-1", GraphID: graph.ID, GraphRevision: graph.Revision, NodeID: "node-1", AuthorRunID: "producer-1", EvidenceSummary: "evidence", Summary: "candidate"}})
if err != nil || len(store.candidates) != 1 || returned.State != work.GraphRunning {
t.Fatalf("candidate callback changed completion state: returned=%+v candidates=%d err=%v", returned, len(store.candidates), err)
}
conflict, synthesis, completed, err := application.SynthesizeAndCompleteGraph(context.Background(), store, store, application.GraphSynthesisRequest{Conflict: work.ConflictSet{ID: "conflict-1", GraphID: graph.ID, GraphRevision: graph.Revision, NodeID: "node-1", CandidateIDs: []string{"candidate-1", "candidate-2"}}, Candidates: []work.ResultCandidate{{ID: "candidate-1", GraphID: graph.ID, GraphRevision: graph.Revision, NodeID: "node-1", AuthorRunID: "producer-1", EvidenceSummary: "evidence", Summary: "candidate"}, {ID: "candidate-2", GraphID: graph.ID, GraphRevision: graph.Revision, NodeID: "node-1", AuthorRunID: "producer-2", EvidenceSummary: "evidence-2", Summary: "candidate-2"}}, SelectedCandidate: "candidate-1", VerifierRunID: "verifier-1", VerifierIdentity: "independent", EvidenceRefs: []work.EvidenceRef{"test:evidence"}, Summary: "verified", Verified: true})
if err != nil || !conflict.Resolved || synthesis.State != work.GraphSynthesisVerified || completed.State != work.GraphCompleted {
t.Fatalf("verified fan-in did not complete graph: conflict=%+v synthesis=%+v graph=%+v err=%v", conflict, synthesis, completed, err)
}
}
func TestCompletedOrchestrationGraphReturnsDurableNextWaveDecision(t *testing.T) {
store := &graphStore{}
graph, _, err := application.AdmitAndSchedule(context.Background(), store, application.GraphScheduleRequest{Graph: work.WorkGraph{ID: "replan-graph", Revision: 1, Budget: work.GraphBudget{MaxParallel: 1, MaxAttempts: 1}, SearchInputBound: true, SearchInput: work.SolutionSearchInput{GoalID: "goal-1", GoalState: work.GoalInProgress, SearchRound: 1, MaxRound: 3}, Nodes: []work.GraphNode{{ID: "node-1", DispatchID: "dispatch-1", IdempotencyKey: "node-1", State: work.GraphNodeReady}}}, Intents: []work.AgentDispatchIntent{{DispatchID: "dispatch-1"}}, Now: 1})
if err != nil {
t.Fatal(err)
}
result, err := application.CompleteGraphNodeWithReplan(context.Background(), store, application.GraphCompleteRequest{GraphID: graph.ID, Revision: graph.Revision, NodeID: "node-1", Succeeded: true})
if err != nil || !result.ReplanRequired || !result.NextSearchDecision.ContinueSearching || result.NextSearchDecision.NextSearchRound != 2 {
t.Fatalf("missing next-wave decision: result=%+v err=%v", result, err)
}
}
func TestGraphSynthesisJSONUsesStableSnakeCaseContract(t *testing.T) {
payload, err := json.Marshal(application.GraphSynthesisRequest{Conflict: work.ConflictSet{ID: "c", GraphID: "g", GraphRevision: 1, NodeID: "n", CandidateIDs: []string{"a", "b"}}, SelectedCandidate: "a", VerifierRunID: "v", VerifierIdentity: "independent", EvidenceRefs: []work.EvidenceRef{"e"}, Summary: "s", Verified: true})
if err != nil || !strings.Contains(string(payload), `"graph_id":"g"`) || !strings.Contains(string(payload), `"selected_candidate_id":"a"`) {