Compare commits

...
Author SHA1 Message Date
m f63c4bb40c feat: connect creator research to truth runtime
CI / test (push) Successful in 1m9s
CI / test (pull_request) Successful in 1m8s
2026-08-23 16:27:00 +03:00
m 22f839df99 Merge pull request 'feat: expose controlled provider discovery status' (#205) from codex/dev-000228-controlled-discovery into main
CI / test (push) Successful in 1m9s
feat: expose controlled provider discovery status
2026-08-23 12:57:48 +00:00
m c41634a065 feat: expose controlled provider discovery status
CI / test (push) Successful in 1m5s
CI / test (pull_request) Successful in 1m2s
2026-08-23 15:54:30 +03:00
m 2609f31e59 Merge pull request 'feat: expose bounded local agent executor' (#204) from codex/dev-000227-local-executor into main
CI / test (push) Successful in 1m10s
feat: expose bounded local agent executor
2026-08-23 12:48:40 +00:00
m 7aa41b3be2 feat: expose bounded local agent executor
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m6s
2026-08-23 15:45:00 +03:00
m 1f6ca363ee docs: record authenticated UI acceptance evidence
CI / test (push) Successful in 1m14s
Merge DEV-000226 authenticated UI acceptance evidence
2026-08-23 12:31:16 +00:00
m 02471990ef docs: record authenticated UI acceptance
CI / test (push) Successful in 1m11s
CI / test (pull_request) Successful in 1m13s
2026-08-23 15:29:04 +03:00
m 40b56798e2 fix: serve login shell at direct route
CI / test (push) Successful in 1m9s
Merge DEV-000225 login route fix
2026-08-23 12:21:44 +00:00
m c53d7ded06 fix: serve login shell at direct route
CI / test (push) Successful in 1m13s
CI / test (pull_request) Successful in 1m13s
2026-08-23 15:19:32 +03:00
m e080c34a77 docs: record live fleet routing evidence
CI / test (push) Successful in 1m23s
Merge DEV-000224 live fleet evidence
2026-08-23 11:56:15 +00:00
m 306f53d484 docs: record live fleet routing evidence
CI / test (push) Successful in 1m10s
CI / test (pull_request) Successful in 57s
2026-08-23 14:53:50 +03:00
m c4411afaaf docs: reconcile current audit status
CI / test (push) Successful in 1m4s
Merge DEV-000223 status reconciliation
2026-08-23 11:42:59 +00:00
m c508941a8a docs: reconcile current audit status
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 59s
2026-08-23 14:41:14 +03:00
m ee68e2207e feat: extend task verifier and unresolved case builder
CI / test (push) Successful in 56s
Merge DEV-000222 verifier modes and case builder
2026-08-23 11:32:03 +00:00
m 3d9a5bccba feat: extend task verifier and unresolved case builder
CI / test (push) Successful in 47s
CI / test (pull_request) Successful in 51s
2026-08-23 14:30:45 +03:00
m d736500d98 feat(work): add independent task verifier runtime
CI / test (push) Successful in 1m10s
feat(work): add independent task verifier runtime (#198)
2026-08-23 11:21:10 +00:00
m 33b51a4445 feat(work): add independent task verifier runtime
CI / test (push) Successful in 1m6s
CI / test (pull_request) Successful in 1m7s
2026-08-23 14:19:38 +03:00
m 0aab10863c feat(capability): require verifier evidence for quality
CI / test (push) Successful in 1m5s
feat(capability): require verifier evidence for quality (#197)
2026-08-23 11:10:02 +00:00
m b17caa3ea9 feat(capability): require verifier evidence for quality
CI / test (push) Successful in 58s
CI / test (pull_request) Successful in 1m8s
2026-08-23 14:08:29 +03:00
m 66ed523248 fix(cognitive): separate reachability from quality evidence
CI / test (push) Successful in 1m9s
fix(cognitive): separate reachability from quality evidence (#196)
2026-08-23 11:03:41 +00:00
m c5b5055ff1 fix(cognitive): separate reachability from quality evidence
CI / test (push) Successful in 1m9s
CI / test (pull_request) Successful in 59s
2026-08-23 14:02:02 +03:00
m 7476f43ffa feat(capability): gate additional model evaluations
CI / test (push) Successful in 1m5s
feat(capability): gate additional model evaluations (#195)
2026-08-23 10:57:52 +00:00
m be29ed5776 feat(capability): gate additional model evaluations
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 59s
2026-08-23 13:56:16 +03:00
m ab840878fe docs(status): refresh current runtime evidence
CI / test (push) Successful in 58s
docs(status): refresh current runtime evidence (#194)
2026-08-23 10:52:12 +00:00
m aebc932b97 docs(status): refresh current runtime evidence
CI / test (push) Successful in 1m2s
CI / test (pull_request) Successful in 1m7s
2026-08-23 13:50:44 +03:00
m 05a246ea3f fix(agent-worker): report local fallback truthfully
CI / test (push) Successful in 1m5s
fix(agent-worker): report local fallback truthfully (#193)
2026-08-23 10:42:50 +00:00
m c847988630 fix(agent-worker): report local fallback truthfully
CI / test (push) Successful in 1m3s
CI / test (pull_request) Successful in 1m3s
2026-08-23 13:41:22 +03:00
m 62f0c529b3 feat(work): persist governed development candidates
CI / test (push) Successful in 53s
feat(work): persist governed development candidates (#192)
2026-08-23 10:29:05 +00:00
m 54958d0902 feat(work): persist governed development candidates
CI / test (push) Successful in 54s
CI / test (pull_request) Successful in 58s
2026-08-23 13:26:41 +03:00
m 8d3e7f7cac feat(work): make agent execution recovery durable
CI / test (push) Successful in 1m4s
DEV-000214 durable Agent Execution recovery
2026-08-23 10:15:19 +00:00
m c9eb398857 chore(governance): declare canary scope for recovery
CI / test (push) Successful in 59s
CI / test (pull_request) Successful in 1m8s
2026-08-23 13:12:25 +03:00
m 284ea88553 feat(work): make agent execution recovery durable
CI / test (push) Failing after 33s
CI / test (pull_request) Failing after 42s
2026-08-23 13:10:18 +03:00
m f9b9765100 feat(work): link task evaluation worker execution
CI / test (push) Successful in 1m7s
DEV-000213 worker evaluator link
2026-08-23 09:59:00 +00:00
m 863a379048 feat(work): link task evaluation worker execution
CI / test (push) Successful in 57s
CI / test (pull_request) Successful in 1m2s
2026-08-23 12:56:01 +03:00
m a86d1994c3 Merge pull request 'feat: execute bounded task evaluation cases' (#189) from codex/wp-212-task-evaluator-invocation into main
CI / test (push) Successful in 49s
2026-08-23 09:41:37 +00:00
68 changed files with 1709 additions and 74 deletions
+129 -13
View File
@@ -2,11 +2,14 @@ package postgres
import (
"context"
"encoding/json"
"errors"
"fmt"
"reflect"
"slices"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/work"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
@@ -15,10 +18,11 @@ import (
type WorkRepository struct{ pool *pgxpool.Pool }
var (
ErrDispatchIntentConflict = errors.New("dispatch intent idempotency conflict")
ErrAgentLeaseUnavailable = errors.New("agent lease unavailable")
ErrAgentCallbackRejected = errors.New("agent callback rejected")
ErrDevelopmentGateConflict = errors.New("development gate revision conflict")
ErrDispatchIntentConflict = errors.New("dispatch intent idempotency conflict")
ErrAgentLeaseUnavailable = errors.New("agent lease unavailable")
ErrAgentCallbackRejected = errors.New("agent callback rejected")
ErrDevelopmentGateConflict = errors.New("development gate revision conflict")
ErrDevelopmentCandidateConflict = errors.New("development candidate conflict")
)
func OpenWork(ctx context.Context, config ContinuityConfig) (*WorkRepository, error) {
@@ -101,21 +105,60 @@ func (r *WorkRepository) SaveDevelopmentGate(ctx context.Context, gate work.Deve
return nil
}
func (r *WorkRepository) SaveExtensionProposal(ctx context.Context, value work.ExtensionProposal) error {
result, err := r.pool.Exec(ctx, `INSERT INTO work.extension_proposals(proposal_id,requested_by_principal_id,stage,evidence_refs,deduplicated_signal_keys,desired_outcome,smallest_change_layer,affected_owners,affected_contracts,risk_tier,evaluation_plan,alternatives,rollback_plan,assumptions,created_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) ON CONFLICT(proposal_id) DO NOTHING`, value.ID, value.RequestedByPrincipalID, value.Stage, value.EvidenceRefs, value.DeduplicatedSignalKeys, value.DesiredOutcome, value.SmallestChangeLayer, value.AffectedOwners, value.AffectedContracts, value.RiskTier, value.EvaluationPlan, value.Alternatives, value.RollbackPlan, value.Assumptions, value.CreatedAtUnixMillis)
if err != nil {
return err
}
if result.RowsAffected() != 1 {
return ErrDevelopmentCandidateConflict
}
return nil
}
func (r *WorkRepository) SaveDevelopmentScaffold(ctx context.Context, value work.DevelopmentScaffold) error {
result, err := r.pool.Exec(ctx, `INSERT INTO work.development_scaffolds(scaffold_id,requested_by_principal_id,stage,proposal_ref,base_repo_revision,worktree_ref,task_intent,modules,allowed_paths,forbidden_paths,required_tests,artifact_lineage_refs,protected_main_access,production_access,secret_access,created_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) ON CONFLICT(scaffold_id) DO NOTHING`, value.ID, value.RequestedByPrincipalID, value.Stage, value.ProposalRef, value.BaseRepoRevision, value.WorktreeRef, value.TaskIntent, value.Modules, value.AllowedPaths, value.ForbiddenPaths, value.RequiredTests, value.ArtifactLineageRefs, value.ProtectedMainAccess, value.ProductionAccess, value.SecretAccess, value.CreatedAtUnixMillis)
if err != nil {
return err
}
if result.RowsAffected() != 1 {
return ErrDevelopmentCandidateConflict
}
return nil
}
func (r *WorkRepository) SaveDispatchIntent(ctx context.Context, intent work.AgentDispatchIntent) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
_, err = tx.Exec(ctx, `INSERT INTO work.agent_dispatch_intents(dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis,specialist_purpose,task_evaluation_profile_id,task_evaluation_binding_id,task_evaluation_case_ids) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT(idempotency_key) DO NOTHING`, intent.DispatchID, intent.IdempotencyKey, intent.DelegationID, intent.RunID, intent.ImplementationID, intent.ImplementationRevision, intent.SourceRevision, intent.WorktreeID, intent.VerificationPlanRef, intent.CreatedAtUnixMillis, intent.SpecialistPurpose, intent.TaskEvaluationProfileID, intent.TaskEvaluationBindingID, intent.TaskEvaluationCaseIDs)
profileJSON, casesJSON, err := marshalTaskPayload(intent.TaskEvaluationProfile, intent.TaskEvaluationCases)
if err != nil {
return err
}
executionJSON, err := json.Marshal(intent.TaskEvaluationExecution)
if err != nil {
return err
}
_, err = tx.Exec(ctx, `INSERT INTO work.agent_dispatch_intents(dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis,specialist_purpose,task_evaluation_profile_id,task_evaluation_binding_id,task_evaluation_case_ids,task_evaluation_profile,task_evaluation_cases,task_evaluation_execution) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17) ON CONFLICT(idempotency_key) DO NOTHING`, intent.DispatchID, intent.IdempotencyKey, intent.DelegationID, intent.RunID, intent.ImplementationID, intent.ImplementationRevision, intent.SourceRevision, intent.WorktreeID, intent.VerificationPlanRef, intent.CreatedAtUnixMillis, intent.SpecialistPurpose, intent.TaskEvaluationProfileID, intent.TaskEvaluationBindingID, intent.TaskEvaluationCaseIDs, profileJSON, casesJSON, executionJSON)
if err != nil {
return fmt.Errorf("save dispatch intent: %w", err)
}
var existing work.AgentDispatchIntent
err = tx.QueryRow(ctx, `SELECT dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis,specialist_purpose,task_evaluation_profile_id,task_evaluation_binding_id,task_evaluation_case_ids FROM work.agent_dispatch_intents WHERE idempotency_key=$1`, intent.IdempotencyKey).Scan(&existing.DispatchID, &existing.IdempotencyKey, &existing.DelegationID, &existing.RunID, &existing.ImplementationID, &existing.ImplementationRevision, &existing.SourceRevision, &existing.WorktreeID, &existing.VerificationPlanRef, &existing.CreatedAtUnixMillis, &existing.SpecialistPurpose, &existing.TaskEvaluationProfileID, &existing.TaskEvaluationBindingID, &existing.TaskEvaluationCaseIDs)
var profileJSONExisting, casesJSONExisting, executionJSONExisting []byte
err = tx.QueryRow(ctx, `SELECT dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis,specialist_purpose,task_evaluation_profile_id,task_evaluation_binding_id,task_evaluation_case_ids,task_evaluation_profile,task_evaluation_cases,task_evaluation_execution FROM work.agent_dispatch_intents WHERE idempotency_key=$1`, intent.IdempotencyKey).Scan(&existing.DispatchID, &existing.IdempotencyKey, &existing.DelegationID, &existing.RunID, &existing.ImplementationID, &existing.ImplementationRevision, &existing.SourceRevision, &existing.WorktreeID, &existing.VerificationPlanRef, &existing.CreatedAtUnixMillis, &existing.SpecialistPurpose, &existing.TaskEvaluationProfileID, &existing.TaskEvaluationBindingID, &existing.TaskEvaluationCaseIDs, &profileJSONExisting, &casesJSONExisting, &executionJSONExisting)
if err != nil {
return fmt.Errorf("read dispatch intent: %w", err)
}
if err := unmarshalTaskPayload(profileJSONExisting, casesJSONExisting, &existing.TaskEvaluationProfile, &existing.TaskEvaluationCases); err != nil {
return err
}
if len(executionJSONExisting) > 0 && string(executionJSONExisting) != "null" {
if err := json.Unmarshal(executionJSONExisting, &existing.TaskEvaluationExecution); err != nil {
return err
}
}
if !sameDispatchIntent(existing, intent) {
return ErrDispatchIntentConflict
}
@@ -123,15 +166,24 @@ func (r *WorkRepository) SaveDispatchIntent(ctx context.Context, intent work.Age
}
func sameDispatchIntent(left, right work.AgentDispatchIntent) bool {
return left.DispatchID == right.DispatchID && left.IdempotencyKey == right.IdempotencyKey && left.DelegationID == right.DelegationID && left.RunID == right.RunID && left.ImplementationID == right.ImplementationID && left.ImplementationRevision == right.ImplementationRevision && left.SourceRevision == right.SourceRevision && left.WorktreeID == right.WorktreeID && left.VerificationPlanRef == right.VerificationPlanRef && left.CreatedAtUnixMillis == right.CreatedAtUnixMillis && left.SpecialistPurpose == right.SpecialistPurpose && left.TaskEvaluationProfileID == right.TaskEvaluationProfileID && left.TaskEvaluationBindingID == right.TaskEvaluationBindingID && slices.Equal(left.TaskEvaluationCaseIDs, right.TaskEvaluationCaseIDs)
return left.DispatchID == right.DispatchID && left.IdempotencyKey == right.IdempotencyKey && left.DelegationID == right.DelegationID && left.RunID == right.RunID && left.ImplementationID == right.ImplementationID && left.ImplementationRevision == right.ImplementationRevision && left.SourceRevision == right.SourceRevision && left.WorktreeID == right.WorktreeID && left.VerificationPlanRef == right.VerificationPlanRef && left.CreatedAtUnixMillis == right.CreatedAtUnixMillis && left.SpecialistPurpose == right.SpecialistPurpose && left.TaskEvaluationProfileID == right.TaskEvaluationProfileID && left.TaskEvaluationBindingID == right.TaskEvaluationBindingID && slices.Equal(left.TaskEvaluationCaseIDs, right.TaskEvaluationCaseIDs) && reflect.DeepEqual(left.TaskEvaluationProfile, right.TaskEvaluationProfile) && slices.Equal(left.TaskEvaluationCases, right.TaskEvaluationCases) && reflect.DeepEqual(left.TaskEvaluationExecution, right.TaskEvaluationExecution)
}
func (r *WorkRepository) LoadDispatchIntent(ctx context.Context, dispatchID string) (work.AgentDispatchIntent, error) {
var intent work.AgentDispatchIntent
err := r.pool.QueryRow(ctx, `SELECT dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis,specialist_purpose,task_evaluation_profile_id,task_evaluation_binding_id,task_evaluation_case_ids FROM work.agent_dispatch_intents WHERE dispatch_id=$1`, dispatchID).Scan(&intent.DispatchID, &intent.IdempotencyKey, &intent.DelegationID, &intent.RunID, &intent.ImplementationID, &intent.ImplementationRevision, &intent.SourceRevision, &intent.WorktreeID, &intent.VerificationPlanRef, &intent.CreatedAtUnixMillis, &intent.SpecialistPurpose, &intent.TaskEvaluationProfileID, &intent.TaskEvaluationBindingID, &intent.TaskEvaluationCaseIDs)
var profileJSON, casesJSON, executionJSON []byte
err := r.pool.QueryRow(ctx, `SELECT dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis,specialist_purpose,task_evaluation_profile_id,task_evaluation_binding_id,task_evaluation_case_ids,task_evaluation_profile,task_evaluation_cases,task_evaluation_execution FROM work.agent_dispatch_intents WHERE dispatch_id=$1`, dispatchID).Scan(&intent.DispatchID, &intent.IdempotencyKey, &intent.DelegationID, &intent.RunID, &intent.ImplementationID, &intent.ImplementationRevision, &intent.SourceRevision, &intent.WorktreeID, &intent.VerificationPlanRef, &intent.CreatedAtUnixMillis, &intent.SpecialistPurpose, &intent.TaskEvaluationProfileID, &intent.TaskEvaluationBindingID, &intent.TaskEvaluationCaseIDs, &profileJSON, &casesJSON, &executionJSON)
if err != nil {
return work.AgentDispatchIntent{}, fmt.Errorf("load dispatch intent: %w", err)
}
if err := unmarshalTaskPayload(profileJSON, casesJSON, &intent.TaskEvaluationProfile, &intent.TaskEvaluationCases); err != nil {
return work.AgentDispatchIntent{}, err
}
if len(executionJSON) > 0 && string(executionJSON) != "null" {
if err := json.Unmarshal(executionJSON, &intent.TaskEvaluationExecution); err != nil {
return work.AgentDispatchIntent{}, err
}
}
return intent, nil
}
@@ -147,7 +199,7 @@ func (r *WorkRepository) ClaimNextDispatch(ctx context.Context, workerID string,
}
defer tx.Rollback(ctx)
var item work.AgentDispatchQueueItem
err = tx.QueryRow(ctx, `SELECT dispatch_id,state,available_at_unix_millis,attempts,claimed_by,updated_at_unix_millis FROM work.agent_dispatch_queue WHERE state='QUEUED' AND available_at_unix_millis <= $1 ORDER BY available_at_unix_millis,dispatch_id FOR UPDATE SKIP LOCKED LIMIT 1`, nowUnixMillis).Scan(&item.DispatchID, &item.State, &item.AvailableAtUnixMillis, &item.Attempts, &item.ClaimedBy, &item.UpdatedAtUnixMillis)
err = tx.QueryRow(ctx, `SELECT dispatch_id,state,available_at_unix_millis,attempts,claimed_by,updated_at_unix_millis FROM work.agent_dispatch_queue WHERE state='QUEUED' AND available_at_unix_millis <= $1 AND attempts < retry_limit ORDER BY available_at_unix_millis,dispatch_id FOR UPDATE SKIP LOCKED LIMIT 1`, nowUnixMillis).Scan(&item.DispatchID, &item.State, &item.AvailableAtUnixMillis, &item.Attempts, &item.ClaimedBy, &item.UpdatedAtUnixMillis)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return work.AgentDispatchQueueItem{}, nil
@@ -190,6 +242,21 @@ func (r *WorkRepository) CompleteDispatch(ctx context.Context, dispatchID, worke
return nil
}
func (r *WorkRepository) RetryDispatch(ctx context.Context, dispatchID, workerID string, nowUnixMillis int64, delay time.Duration) error {
if delay < 0 {
return fmt.Errorf("invalid retry delay")
}
availableAt := nowUnixMillis + delay.Milliseconds()
command, err := r.pool.Exec(ctx, `UPDATE work.agent_dispatch_queue SET state=CASE WHEN attempts >= retry_limit THEN 'FAILED' ELSE 'QUEUED' END,available_at_unix_millis=$1,claimed_by='',updated_at_unix_millis=$2 WHERE dispatch_id=$3 AND state='CLAIMED' AND claimed_by=$4`, availableAt, nowUnixMillis, dispatchID, workerID)
if err != nil {
return err
}
if command.RowsAffected() != 1 {
return ErrAgentCallbackRejected
}
return nil
}
func (r *WorkRepository) SaveWorkGraph(ctx context.Context, graph work.WorkGraph) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
@@ -200,7 +267,11 @@ func (r *WorkRepository) SaveWorkGraph(ctx context.Context, graph work.WorkGraph
return err
}
for _, node := range graph.Nodes {
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_nodes(graph_id,revision,node_id,dispatch_id,idempotency_key,state,attempts,purpose,task_profile_id,binding_id,case_ids) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT(graph_id,revision,node_id) DO NOTHING`, graph.ID, graph.Revision, node.ID, node.DispatchID, node.IdempotencyKey, node.State, node.Attempts, node.Purpose, node.TaskProfileID, node.BindingID, node.CaseIDs); err != nil {
profileJSON, casesJSON, err := marshalTaskPayload(node.TaskProfile, node.TaskCases)
if err != nil {
return err
}
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_nodes(graph_id,revision,node_id,dispatch_id,idempotency_key,state,attempts,purpose,task_profile_id,binding_id,case_ids,task_evaluation_profile,task_evaluation_cases) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) ON CONFLICT(graph_id,revision,node_id) DO NOTHING`, graph.ID, graph.Revision, node.ID, node.DispatchID, node.IdempotencyKey, node.State, node.Attempts, node.Purpose, node.TaskProfileID, node.BindingID, node.CaseIDs, profileJSON, casesJSON); err != nil {
return err
}
}
@@ -218,13 +289,18 @@ func (r *WorkRepository) LoadWorkGraph(ctx context.Context, graphID string, revi
if err != nil {
return work.WorkGraph{}, fmt.Errorf("load graph: %w", err)
}
rows, err := r.pool.Query(ctx, `SELECT node_id,dispatch_id,idempotency_key,state,attempts,purpose,task_profile_id,binding_id,case_ids FROM work.graph_nodes WHERE graph_id=$1 AND revision=$2 ORDER BY node_id`, graphID, revision)
rows, err := r.pool.Query(ctx, `SELECT node_id,dispatch_id,idempotency_key,state,attempts,purpose,task_profile_id,binding_id,case_ids,task_evaluation_profile,task_evaluation_cases FROM work.graph_nodes WHERE graph_id=$1 AND revision=$2 ORDER BY node_id`, graphID, revision)
if err != nil {
return work.WorkGraph{}, err
}
for rows.Next() {
var node work.GraphNode
if err := rows.Scan(&node.ID, &node.DispatchID, &node.IdempotencyKey, &node.State, &node.Attempts, &node.Purpose, &node.TaskProfileID, &node.BindingID, &node.CaseIDs); err != nil {
var profileJSON, casesJSON []byte
if err := rows.Scan(&node.ID, &node.DispatchID, &node.IdempotencyKey, &node.State, &node.Attempts, &node.Purpose, &node.TaskProfileID, &node.BindingID, &node.CaseIDs, &profileJSON, &casesJSON); err != nil {
rows.Close()
return work.WorkGraph{}, err
}
if err := unmarshalTaskPayload(profileJSON, casesJSON, &node.TaskProfile, &node.TaskCases); err != nil {
rows.Close()
return work.WorkGraph{}, err
}
@@ -276,10 +352,21 @@ func (r *WorkRepository) SaveGraphSynthesis(ctx context.Context, conflict work.C
}
func (r *WorkRepository) RecoverStaleClaims(ctx context.Context, nowUnixMillis, staleAfterMillis int64) (int64, error) {
command, err := r.pool.Exec(ctx, `UPDATE work.agent_dispatch_queue SET state='QUEUED',claimed_by='',updated_at_unix_millis=$1 WHERE state='CLAIMED' AND updated_at_unix_millis <= $2`, nowUnixMillis, nowUnixMillis-staleAfterMillis)
tx, err := r.pool.Begin(ctx)
if err != nil {
return 0, err
}
defer tx.Rollback(ctx)
if _, err := tx.Exec(ctx, `UPDATE work.agent_leases AS l SET state='EXPIRED' FROM work.agent_dispatch_queue AS q JOIN work.agent_dispatch_intents AS i ON i.dispatch_id=q.dispatch_id WHERE q.state='CLAIMED' AND q.updated_at_unix_millis <= $1 AND l.run_id=i.run_id AND l.state='ACTIVE'`, nowUnixMillis-staleAfterMillis); err != nil {
return 0, err
}
command, err := tx.Exec(ctx, `UPDATE work.agent_dispatch_queue SET state=CASE WHEN attempts >= retry_limit THEN 'FAILED' ELSE 'QUEUED' END,available_at_unix_millis=$1,claimed_by='',updated_at_unix_millis=$2 WHERE state='CLAIMED' AND updated_at_unix_millis <= $3`, nowUnixMillis, nowUnixMillis, nowUnixMillis-staleAfterMillis)
if err != nil {
return 0, err
}
if err := tx.Commit(ctx); err != nil {
return 0, err
}
return command.RowsAffected(), nil
}
@@ -570,3 +657,32 @@ func latestRowErr(err error) error {
}
return err
}
func marshalTaskPayload(profile *capability.TaskEvaluationProfile, cases []capability.TaskEvaluationCase) ([]byte, []byte, error) {
var profileJSON []byte
var err error
if profile != nil {
profileJSON, err = json.Marshal(profile)
if err != nil {
return nil, nil, err
}
}
casesJSON, err := json.Marshal(cases)
return profileJSON, casesJSON, err
}
func unmarshalTaskPayload(profileJSON, casesJSON []byte, profile **capability.TaskEvaluationProfile, cases *[]capability.TaskEvaluationCase) error {
if len(profileJSON) > 0 && string(profileJSON) != "null" {
var value capability.TaskEvaluationProfile
if err := json.Unmarshal(profileJSON, &value); err != nil {
return err
}
*profile = &value
}
if len(casesJSON) > 0 && string(casesJSON) != "null" {
if err := json.Unmarshal(casesJSON, cases); err != nil {
return err
}
}
return nil
}
+2 -1
View File
@@ -24,7 +24,8 @@ func (s *memoryStore) SaveDispatchIntent(context.Context, work.AgentDispatchInte
s.writes++
return nil
}
func (s *memoryStore) SaveAgentLease(context.Context, work.AgentLease) error { s.writes++; return nil }
func (s *memoryStore) SaveAgentLease(context.Context, work.AgentLease) error { s.writes++; return nil }
func (s *memoryStore) RenewAgentLease(context.Context, work.AgentLease, time.Time) error { return nil }
func (s *memoryStore) AcceptAgentCallback(context.Context, work.AgentCallback) error {
s.writes++
return nil
+8 -8
View File
@@ -7,26 +7,26 @@ import (
"git.cin.su/m/Assistant/contracts/work"
)
// localFallbackExecutor keeps the durable work path alive when no external
// executor is reachable. It never claims that an external action happened.
// The work owner records the WAITING callback and can retry after a bridge is
// localExecutor provides the bounded local execution capability. It never
// claims that an unsupported or external action happened. The work owner
// records the WAITING callback and can retry after a suitable executor is
// configured or a human chooses the next step.
type localFallbackExecutor struct {
type localExecutor struct {
now func() time.Time
}
func newLocalFallbackExecutor() localFallbackExecutor {
return localFallbackExecutor{now: time.Now}
func newLocalExecutor() localExecutor {
return localExecutor{now: time.Now}
}
func (e localFallbackExecutor) Execute(_ context.Context, request work.AgentExecutionRequest) (work.AgentExecutionResult, error) {
func (e localExecutor) Execute(_ context.Context, request work.AgentExecutionRequest) (work.AgentExecutionResult, error) {
now := time.Now()
if e.now != nil {
now = e.now()
}
return work.AgentExecutionResult{
State: work.AgentExecutionWaiting,
Summary: "Внешний Agent Execution Bridge недоступен; локальный fallback сохранил durable работу и ожидает исполнителя.",
Summary: "Локальный исполнитель не поддерживает эту операцию; durable работа сохранена и ожидает подходящего исполнителя.",
ObservedAtUnixMillis: now.UnixMilli(),
}, nil
}
+3 -3
View File
@@ -8,9 +8,9 @@ import (
"git.cin.su/m/Assistant/contracts/work"
)
func TestLocalFallbackNeverClaimsExternalExecution(t *testing.T) {
func TestLocalExecutorNeverClaimsUnsupportedExecution(t *testing.T) {
when := time.UnixMilli(1_700_000_000_000)
executor := localFallbackExecutor{now: func() time.Time { return when }}
executor := localExecutor{now: func() time.Time { return when }}
result, err := executor.Execute(context.Background(), work.AgentExecutionRequest{Intent: work.AgentDispatchIntent{DispatchID: "dispatch-1"}})
if err != nil {
t.Fatal(err)
@@ -19,6 +19,6 @@ func TestLocalFallbackNeverClaimsExternalExecution(t *testing.T) {
t.Fatalf("unexpected fallback result: %+v", result)
}
if result.OutcomeRef != "" || result.Summary == "" {
t.Fatalf("fallback must not claim an outcome: %+v", result)
t.Fatalf("local executor must not claim an outcome: %+v", result)
}
}
+12 -2
View File
@@ -49,9 +49,12 @@ func main() {
os.Exit(1)
}
} else {
executor = newLocalFallbackExecutor()
// The local executor keeps supported task-qualification work alive, but
// it is not an external Agent Execution Bridge and must never be reported
// as one. Unsupported work remains durably WAITING.
localFallback = true
fmt.Fprintln(os.Stderr, "protected agent execution bridge URL is not configured; local fallback preserves durable work without external mutation")
executor = newTaskEvaluationExecutor(envOr("COGNITIVE_WORKER_URL", "http://cognitive-worker:8083"), envOr("WORK_GRAPH_RUNTIME_URL", "http://work-graph-runtime:8088"), envOr("TASK_VERIFIER_URL", "http://task-verifier:8091"), &http.Client{Timeout: *leaseTTL})
fmt.Fprintln(os.Stderr, "protected agent execution bridge URL is not configured; task evaluations use the local durable evaluator, other work remains in fallback")
}
go func() {
if serveErr := http.ListenAndServe(*statusAddr, workerStatusHandler(executor != nil && !localFallback, localFallback)); serveErr != nil {
@@ -107,6 +110,13 @@ func loadProtectedBridgeURLFrom(root, path string) (string, bool) {
return value, true
}
func envOr(name, fallback string) string {
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
return value
}
return fallback
}
func resolveDatabasePassword(name, refName string, consumer secretcontracts.RuntimeConsumer) (string, error) {
if _, ok := os.LookupEnv(name); ok {
return "", fmt.Errorf("%s direct environment secret is forbidden", name)
+12 -8
View File
@@ -6,12 +6,14 @@ import (
)
type workerStatusResponse struct {
Runtime string `json:"runtime"`
ExecutorConfigured bool `json:"executor_configured"`
Mode string `json:"mode"`
Ready bool `json:"ready"`
Reason string `json:"reason"`
LocalFallback bool `json:"local_fallback"`
Runtime string `json:"runtime"`
ExecutorConfigured bool `json:"executor_configured"`
Mode string `json:"mode"`
Ready bool `json:"ready"`
Reason string `json:"reason"`
LocalFallback bool `json:"local_fallback"`
LocalExecutor bool `json:"local_executor"`
Capabilities []string `json:"capabilities,omitempty"`
}
func workerStatusHandler(executorConfigured bool, localFallback ...bool) http.Handler {
@@ -23,10 +25,12 @@ func workerStatusHandler(executorConfigured bool, localFallback ...bool) http.Ha
Ready: false,
Reason: "protected agent execution bridge URL is not configured",
LocalFallback: local,
LocalExecutor: local,
}
if local {
status.Mode = "local-fallback"
status.Reason = "external bridge unavailable; durable work is retained without external mutation"
status.Mode = "local-executor"
status.Reason = "bounded local task-evaluation executor is active; unsupported work remains durably waiting"
status.Capabilities = []string{"task-evaluation"}
}
if executorConfigured {
status.Mode = "execution"
+17 -2
View File
@@ -38,16 +38,31 @@ func TestWorkerStatusReportsRecoveryOnlyWithoutExecutor(t *testing.T) {
}
}
func TestWorkerStatusReportsLocalFallback(t *testing.T) {
func TestWorkerStatusReportsLocalExecutor(t *testing.T) {
response := httptest.NewRecorder()
workerStatusHandler(false, true).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/status", nil))
var value workerStatusResponse
if err := json.NewDecoder(response.Body).Decode(&value); err != nil {
t.Fatal(err)
}
if response.Code != http.StatusOK || value.Ready || value.Mode != "local-fallback" || !value.LocalFallback {
if response.Code != http.StatusOK || value.Ready || value.Mode != "local-executor" || !value.LocalFallback || !value.LocalExecutor {
t.Fatalf("unexpected status: code=%d value=%+v", response.Code, value)
}
if len(value.Capabilities) != 1 || value.Capabilities[0] != "task-evaluation" {
t.Fatalf("unexpected local capabilities: %+v", value.Capabilities)
}
}
func TestWorkerStatusDoesNotCallLocalFallbackAnExternalExecutor(t *testing.T) {
response := httptest.NewRecorder()
workerStatusHandler(false, true).ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/status", nil))
var value workerStatusResponse
if err := json.NewDecoder(response.Body).Decode(&value); err != nil {
t.Fatal(err)
}
if value.ExecutorConfigured || value.Ready || !value.LocalFallback || !value.LocalExecutor || value.Mode != "local-executor" {
t.Fatalf("local fallback was presented as external execution: %+v", value)
}
}
func TestWorkerReadyzRequiresExecutor(t *testing.T) {
@@ -0,0 +1,109 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"git.cin.su/m/Assistant/contracts/artifact"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/work"
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
)
type taskEvaluationExecutor struct {
cognitiveURL string
callbackURL string
verifierURL string
client *http.Client
fallback localExecutor
}
func newTaskEvaluationExecutor(cognitiveURL, callbackURL, verifierURL string, client *http.Client) taskEvaluationExecutor {
if client == nil {
client = http.DefaultClient
}
return taskEvaluationExecutor{cognitiveURL: strings.TrimRight(cognitiveURL, "/"), callbackURL: strings.TrimRight(callbackURL, "/"), verifierURL: strings.TrimRight(verifierURL, "/"), client: client, fallback: newLocalExecutor()}
}
func (e taskEvaluationExecutor) Execute(ctx context.Context, request work.AgentExecutionRequest) (work.AgentExecutionResult, error) {
intent := request.Intent
if intent.TaskEvaluationExecution == nil || intent.TaskEvaluationProfile == nil || len(intent.TaskEvaluationCases) == 0 {
return e.fallback.Execute(ctx, request)
}
execution := intent.TaskEvaluationExecution
if execution.GraphID == "" || execution.NodeID == "" || execution.BindingRevision < 1 || execution.ActorID == "" || execution.PrincipalID == "" || execution.ScopeID == "" {
return e.fallback.Execute(ctx, request)
}
if e.cognitiveURL == "" || e.callbackURL == "" || e.verifierURL == "" {
return e.fallback.Execute(ctx, request)
}
payload := capability.TaskEvaluationExecutionRequest{Profile: *intent.TaskEvaluationProfile, CapabilityID: intent.TaskEvaluationProfile.CapabilityID, BindingID: intent.TaskEvaluationBindingID, BindingRevision: execution.BindingRevision, ActorID: execution.ActorID, PrincipalID: execution.PrincipalID, ScopeID: execution.ScopeID, Cases: intent.TaskEvaluationCases, MaxOutputBytes: 4096}
response, err := postJSON[capability.TaskEvaluationExecutionResponse](ctx, e.client, e.cognitiveURL+"/v1/task-evaluations/execute", payload)
if err != nil {
return work.AgentExecutionResult{}, err
}
if len(response.Responses) == 0 || response.ObservedAtUnixMillis < 1 {
return work.AgentExecutionResult{}, fmt.Errorf("task evaluation returned no observations")
}
if len(intent.TaskEvaluationCases) == 0 {
return work.AgentExecutionResult{}, fmt.Errorf("task evaluation cases are missing")
}
caseByID := make(map[string]capability.TaskEvaluationCase, len(intent.TaskEvaluationCases))
for _, item := range intent.TaskEvaluationCases {
caseByID[item.CaseID] = item
}
observations := make([]capability.TaskCaseObservation, 0, len(response.Responses))
for _, item := range response.Responses {
caseValue, ok := caseByID[item.CaseID]
if !ok || caseValue.Verification.Mode == "" {
return e.fallback.Execute(ctx, request)
}
verified, verifyErr := postJSON[capability.TaskCaseVerificationResponse](ctx, e.client, e.verifierURL+"/v1/task-evaluations/verify", capability.TaskCaseVerificationRequest{CaseID: item.CaseID, ResponseText: item.ResponseText, LatencyMicros: item.LatencyMicros, EvidenceArtifactRef: item.EvidenceArtifactRef, Verification: caseValue.Verification})
if verifyErr != nil {
return e.fallback.Execute(ctx, request)
}
observations = append(observations, capability.TaskCaseObservation{CaseID: verified.CaseID, Result: verified.Result, QualityScoreMillis: verified.QualityScoreMillis, LatencyMicros: verified.LatencyMicros, CriticalFailure: verified.CriticalFailure, EvidenceArtifactRef: verified.EvidenceArtifactRef, VerificationEvidenceRef: verified.VerificationEvidenceRef, VerifierIdentity: verified.VerifierIdentity})
}
evaluation, err := capabilitydomain.BuildTaskEvaluation(*intent.TaskEvaluationProfile, capability.TaskEvaluationID("evaluation:"+intent.DispatchID), response.BindingID, response.BindingRevision, intent.TaskEvaluationProfile.CapabilityID, observations, artifact.ArtifactID(intent.TaskEvaluationCases[0].EvidenceArtifactRef), response.ObservedAtUnixMillis)
if err != nil {
return work.AgentExecutionResult{}, err
}
callback := work.TaskEvaluationEvidenceCallback{GraphID: execution.GraphID, Revision: execution.GraphRevision, NodeID: execution.NodeID, Evaluation: evaluation, Succeeded: true}
if _, err := postJSON[json.RawMessage](ctx, e.client, e.callbackURL+"/v1/work-graphs/task-evaluations/callback", callback); err != nil {
return work.AgentExecutionResult{}, err
}
return work.AgentExecutionResult{State: work.AgentExecutionCompleted, Summary: "task evaluation executed, aggregated and accepted by Work Graph callback", OutcomeRef: string(evaluation.ID), ArtifactRefs: []string{string(evaluation.ArtifactID)}, ObservedAtUnixMillis: time.Now().UnixMilli()}, nil
}
func postJSON[T any](ctx context.Context, client *http.Client, url string, payload any) (T, error) {
var result T
body, err := json.Marshal(payload)
if err != nil {
return result, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return result, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return result, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return result, fmt.Errorf("POST %s: status %s", url, resp.Status)
}
if resp.StatusCode == http.StatusNoContent {
return result, nil
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return result, err
}
return result, nil
}
@@ -0,0 +1,43 @@
package main
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/work"
)
func TestTaskEvaluationExecutorInvokesAggregatesAndCallbacks(t *testing.T) {
profile := capability.TaskEvaluationProfile{ID: "profile-1", Revision: 1, CapabilityID: "capability-1", Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "general-v1", RequiredCaseIDs: []string{"case-1"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100}
verification := capability.TaskCaseVerification{Mode: capability.TaskCaseVerificationExactText, ExpectedText: "answer", MinimumQualityMillis: 900}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/v1/task-evaluations/execute" {
_ = json.NewEncoder(w).Encode(capability.TaskEvaluationExecutionResponse{ProfileID: profile.ID, BindingID: "binding-1", BindingRevision: 3, Responses: []capability.TaskCaseResponse{{CaseID: "case-1", ResponseText: "answer", LatencyMicros: 2, EvidenceArtifactRef: "artifact-1"}}, ObservedAtUnixMillis: 10})
return
}
if r.URL.Path == "/v1/task-evaluations/verify" {
_ = json.NewEncoder(w).Encode(capability.TaskCaseVerificationResponse{CaseID: "case-1", Result: capability.EvaluationSupported, QualityScoreMillis: 900, LatencyMicros: 2, EvidenceArtifactRef: "artifact-1", VerificationEvidenceRef: "verify-1", VerifierIdentity: "verifier-1"})
return
}
if r.URL.Path == "/v1/work-graphs/task-evaluations/callback" {
var callback work.TaskEvaluationEvidenceCallback
if err := json.NewDecoder(r.Body).Decode(&callback); err != nil || callback.GraphID != "graph-1" || callback.NodeID != "node-1" || callback.Evaluation.Result != capability.EvaluationSupported {
http.Error(w, "invalid callback", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusNoContent)
return
}
http.NotFound(w, r)
}))
defer server.Close()
executor := newTaskEvaluationExecutor(server.URL, server.URL, server.URL, server.Client())
result, err := executor.Execute(context.Background(), work.AgentExecutionRequest{Intent: work.AgentDispatchIntent{DispatchID: "dispatch-1", TaskEvaluationBindingID: "binding-1", TaskEvaluationProfile: &profile, TaskEvaluationCases: []capability.TaskEvaluationCase{{CaseID: "case-1", Prompt: "answer", EvidenceArtifactRef: "artifact-1", Verification: verification}}, TaskEvaluationExecution: &work.TaskEvaluationDispatchContext{GraphID: "graph-1", GraphRevision: 1, NodeID: "node-1", BindingRevision: 3, ActorID: "actor-1", PrincipalID: "principal-1", ScopeID: "scope-1"}}})
if err != nil || result.State != work.AgentExecutionCompleted || result.OutcomeRef == "" {
t.Fatalf("result=%+v err=%v", result, err)
}
}
+44
View File
@@ -3,6 +3,7 @@ package main
import (
"encoding/json"
"net/http"
"sort"
"time"
"git.cin.su/m/Assistant/contracts/capability"
@@ -17,6 +18,10 @@ type discoveryService struct {
}
func (s discoveryService) handler(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet && r.URL.Path == "/v1/provider-discovery/status" {
s.statusHandler(w, r)
return
}
registration, ok := s.providers[r.PathValue("provider")]
if !ok || registration.Discovery == nil {
http.Error(w, "provider discovery not found", http.StatusNotFound)
@@ -35,3 +40,42 @@ func (s discoveryService) handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(result)
}
type discoveryStatus struct {
Provider string `json:"provider"`
ProviderKind capability.ProviderKind `json:"provider_kind"`
Health capability.HealthState `json:"health"`
FailureClass capability.CognitiveFailureType `json:"failure_class,omitempty"`
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
Models []discoveredModelStatus `json:"models"`
}
type discoveredModelStatus struct {
ProviderModelID capability.ProviderModelID `json:"provider_model_id"`
Fingerprint capability.ModelFingerprint `json:"fingerprint,omitempty"`
Capabilities []capability.ModelCapability `json:"capabilities,omitempty"`
}
func (s discoveryService) statusHandler(w http.ResponseWriter, r *http.Request) {
providers := make([]string, 0, len(s.providers))
for name := range s.providers {
providers = append(providers, name)
}
sort.Strings(providers)
result := make([]discoveryStatus, 0, len(providers))
for _, name := range providers {
registration := s.providers[name]
status := discoveryStatus{Provider: name, ProviderKind: registration.Kind, Health: capability.HealthUnknown, ObservedAtUnixMillis: s.now().UnixMilli(), Models: []discoveredModelStatus{}}
if registration.Discovery != nil {
models, probe := registration.Discovery.Discover(r.Context())
status.Health = probe.Health
status.FailureClass = capability.CognitiveFailureType(probe.Failure)
for _, model := range models {
status.Models = append(status.Models, discoveredModelStatus{ProviderModelID: model.ProviderModelID, Fingerprint: model.Fingerprint, Capabilities: model.Capabilities})
}
}
result = append(result, status)
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(result)
}
+52
View File
@@ -0,0 +1,52 @@
package main
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"git.cin.su/m/Assistant/adapters/models"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/identity"
)
type discoveryTestProvider struct{}
func (discoveryTestProvider) Probe(context.Context) models.ProviderProbe {
return models.ProviderProbe{Health: capability.HealthAvailable}
}
func (discoveryTestProvider) Discover(context.Context) ([]models.DiscoveredModel, models.ProviderProbe) {
return []models.DiscoveredModel{{ProviderModelID: "local-model", Fingerprint: "fingerprint-1", Capabilities: []capability.ModelCapability{capability.CapabilityAdvisory}}}, models.ProviderProbe{Health: capability.HealthAvailable}
}
func (discoveryTestProvider) Advise(context.Context, cognitive.AdvisoryRequest) (cognitive.AdvisoryResponse, error) {
return cognitive.AdvisoryResponse{}, nil
}
func TestDiscoveryStatusIsReadOnlyAndDoesNotExposeCredentialMetadata(t *testing.T) {
service := discoveryService{
providers: map[string]providerRegistration{"cpu": {ProviderInstanceID: "provider-1", Kind: "openai-compatible-chat-v1", CredentialRef: "credential://must-not-be-returned", Discovery: discoveryTestProvider{}}},
owner: identity.PrincipalID("owner-1"),
now: func() time.Time { return time.UnixMilli(1234) },
}
response := httptest.NewRecorder()
service.handler(response, httptest.NewRequest(http.MethodGet, "/v1/provider-discovery/status", nil))
if response.Code != http.StatusOK {
t.Fatalf("status=%d", response.Code)
}
body := response.Body.String()
var result []discoveryStatus
if err := json.NewDecoder(strings.NewReader(body)).Decode(&result); err != nil {
t.Fatal(err)
}
if len(result) != 1 || result[0].Provider != "cpu" || result[0].Health != capability.HealthAvailable || len(result[0].Models) != 1 || result[0].ObservedAtUnixMillis != 1234 {
t.Fatalf("unexpected discovery status: %+v", result)
}
if strings.Contains(body, "must-not-be-returned") || strings.Contains(body, "credential://") {
t.Fatalf("credential metadata leaked in discovery status: %s", body)
}
}
+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.HandleFunc("GET /v1/provider-discovery/status", discovery.statusHandler)
refreshProviderDiscoveries(resolver, discovery)
log.Printf("cognitive-worker listening on %s", addr)
if err := http.ListenAndServe(addr, mux); err != nil {
+7 -4
View File
@@ -44,6 +44,7 @@ func (g cognitiveGateway) taskEvaluationHandler(w http.ResponseWriter, r *http.R
caseSet[id] = struct{}{}
}
observations := make([]capability.TaskCaseObservation, len(request.Cases))
responses := make([]capability.TaskCaseResponse, len(request.Cases))
var wg sync.WaitGroup
seenCases := make(map[string]struct{}, len(request.Cases))
for i, item := range request.Cases {
@@ -67,10 +68,12 @@ func (g cognitiveGateway) taskEvaluationHandler(w http.ResponseWriter, r *http.R
invocation := cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(fmt.Sprintf("task-evaluation:%s:%s", request.Profile.ID, item.CaseID)), ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: item.Prompt, MaxOutputBytes: int(request.MaxOutputBytes), ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, ProviderInstanceID: selected.Provider.ID, ModelDescriptorID: selected.Model.ID, ProviderModelID: selected.Model.ProviderModelID}
response, invokeErr := g.invokeAdmitted(r.Context(), string(invocation.RequestID), provider, invocation, int(request.MaxOutputBytes))
observation := capability.TaskCaseObservation{CaseID: item.CaseID, LatencyMicros: time.Since(started).Microseconds(), EvidenceArtifactRef: item.EvidenceArtifactRef}
responses[index] = capability.TaskCaseResponse{CaseID: item.CaseID, ResponseText: response.AdvisoryText, LatencyMicros: observation.LatencyMicros, EvidenceArtifactRef: item.EvidenceArtifactRef}
if invokeErr == nil && response.FinishReason == cognitive.FinishCompleted && response.AdvisoryText != "" {
observation.Result = capability.EvaluationSupported
// Invocation success proves reachability/support for this case,
// not semantic answer quality. A verifier may score quality later.
// Invocation success proves reachability only. Without an
// independent verifier and answer evidence, semantic support and
// quality must remain UNKNOWN rather than becoming false evidence.
observation.Result = capability.EvaluationUnknown
observation.QualityScoreMillis = 0
} else {
observation.Result = capability.EvaluationUnknown
@@ -84,7 +87,7 @@ func (g cognitiveGateway) taskEvaluationHandler(w http.ResponseWriter, r *http.R
return
}
wg.Wait()
writeTaskEvaluationJSON(w, capability.TaskEvaluationExecutionResponse{ProfileID: request.Profile.ID, BindingID: selected.Binding.ID, BindingRevision: selected.Binding.Revision, Observations: observations, ObservedAtUnixMillis: time.Now().UnixMilli()})
writeTaskEvaluationJSON(w, capability.TaskEvaluationExecutionResponse{ProfileID: request.Profile.ID, BindingID: selected.Binding.ID, BindingRevision: selected.Binding.Revision, Observations: observations, Responses: responses, ObservedAtUnixMillis: time.Now().UnixMilli()})
}
func writeTaskEvaluationJSON(w http.ResponseWriter, value any) {
+6 -1
View File
@@ -47,7 +47,12 @@ func TestTaskEvaluationHandlerInvokesEveryBoundedCase(t *testing.T) {
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
t.Fatal(err)
}
if len(response.Observations) != 2 || response.Observations[0].Result != capability.EvaluationSupported || response.Observations[1].Result != capability.EvaluationSupported {
if len(response.Observations) != 2 || response.Observations[0].Result != capability.EvaluationUnknown || response.Observations[1].Result != capability.EvaluationUnknown {
t.Fatalf("unexpected observations: %+v", response.Observations)
}
for _, observation := range response.Observations {
if observation.QualityScoreMillis != 0 || observation.CriticalFailure {
t.Fatalf("reachability-only evidence must not carry quality or failure claims: %+v", observation)
}
}
}
+78
View File
@@ -116,6 +116,10 @@ func main() {
if err != nil {
log.Fatal(err)
}
researchClient, err := newResearchRuntimeClient(envOr("RESEARCH_RUNTIME_URL", "http://research-runtime:8087"), &http.Client{Timeout: 12 * time.Second})
if err != nil {
log.Fatal(err)
}
learning := learningRecorder{epistemics: epistemicsRepository, capability: capabilityRepository}
mux := http.NewServeMux()
authenticateCreator := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
@@ -178,6 +182,80 @@ func main() {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(session)
})
mux.HandleFunc("POST /v1/creator/research/web", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
var request epistemics.WebResearchRequest
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 128<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid web research request", http.StatusBadRequest)
return
}
result, err := researchClient.EvaluateWeb(r.Context(), request)
if err != nil {
http.Error(w, "web research unavailable", http.StatusBadGateway)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(result)
})
mux.HandleFunc("POST /v1/creator/development/proposals", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
return
}
var request struct {
Evidence []work.ExtensionEvidence `json:"evidence"`
Proposal work.ExtensionProposalRequest `json:"proposal"`
}
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid development proposal", http.StatusBadRequest)
return
}
proposal, err := workdomain.BuildExtensionProposal(request.Evidence, request.Proposal)
if err != nil {
http.Error(w, "development proposal rejected", http.StatusUnprocessableEntity)
return
}
proposal.RequestedByPrincipalID = string(session.PrincipalID)
if err := workRepository.SaveExtensionProposal(r.Context(), proposal); err != nil {
http.Error(w, "development proposal conflict", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(proposal)
})
mux.HandleFunc("POST /v1/creator/development/scaffolds", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
return
}
var request work.DevelopmentScaffoldRequest
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid development scaffold", http.StatusBadRequest)
return
}
scaffold, err := workdomain.BuildDevelopmentScaffold(request)
if err != nil {
http.Error(w, "development scaffold rejected", http.StatusUnprocessableEntity)
return
}
scaffold.RequestedByPrincipalID = string(session.PrincipalID)
if err := workRepository.SaveDevelopmentScaffold(r.Context(), scaffold); err != nil {
http.Error(w, "development scaffold conflict", http.StatusConflict)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(scaffold)
})
mux.HandleFunc("GET /v1/creator/focus", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
+53
View File
@@ -0,0 +1,53 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"git.cin.su/m/Assistant/contracts/epistemics"
)
type researchRuntimeClient struct {
baseURL string
client *http.Client
}
func newResearchRuntimeClient(rawURL string, client *http.Client) (researchRuntimeClient, error) {
base := strings.TrimRight(strings.TrimSpace(rawURL), "/")
if base == "" {
return researchRuntimeClient{}, fmt.Errorf("research runtime URL is empty")
}
if client == nil {
client = http.DefaultClient
}
return researchRuntimeClient{baseURL: base, client: client}, nil
}
func (c researchRuntimeClient) EvaluateWeb(ctx context.Context, request epistemics.WebResearchRequest) (epistemics.ResearchResult, error) {
payload, err := json.Marshal(request)
if err != nil {
return epistemics.ResearchResult{}, err
}
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/research/web/evaluate", bytes.NewReader(payload))
if err != nil {
return epistemics.ResearchResult{}, err
}
httpRequest.Header.Set("Content-Type", "application/json")
response, err := c.client.Do(httpRequest)
if err != nil {
return epistemics.ResearchResult{}, err
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return epistemics.ResearchResult{}, fmt.Errorf("research runtime status %d", response.StatusCode)
}
var result epistemics.ResearchResult
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
return epistemics.ResearchResult{}, err
}
return result, nil
}
+33
View File
@@ -0,0 +1,33 @@
package main
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"git.cin.su/m/Assistant/contracts/epistemics"
)
func TestResearchRuntimeClientForwardsBoundedWebRequest(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/v1/research/web/evaluate" || r.Header.Get("Content-Type") != "application/json" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
var request epistemics.WebResearchRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil || request.URL != "https://assistant.cin.su/healthz" {
t.Fatalf("unexpected request body: %#v %v", request, err)
}
_ = json.NewEncoder(w).Encode(epistemics.ResearchResult{PlanID: request.Plan.ID, Findings: []epistemics.ResearchFinding{{PropositionKey: request.PropositionKey, State: epistemics.FindingSupported}}})
}))
defer server.Close()
client, err := newResearchRuntimeClient(server.URL, server.Client())
if err != nil {
t.Fatal(err)
}
result, err := client.EvaluateWeb(context.Background(), epistemics.WebResearchRequest{Plan: epistemics.ResearchPlan{ID: "plan-1"}, PropositionKey: "health", URL: "https://assistant.cin.su/healthz"})
if err != nil || result.PlanID != "plan-1" || len(result.Findings) != 1 || result.Findings[0].State != epistemics.FindingSupported {
t.Fatalf("unexpected result: %#v %v", result, err)
}
}
+1 -1
View File
@@ -150,7 +150,7 @@ func (g *gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if r.URL.Path == "/" || r.URL.Path == "/chat" || r.URL.Path == "/chat.html" {
if r.URL.Path == "/" || r.URL.Path == "/login" || r.URL.Path == "/chat" || r.URL.Path == "/chat.html" {
g.serveEmbeddedHTML(w, "chat.html")
return
}
+12
View File
@@ -354,6 +354,18 @@ func TestConsoleAndSecurityHeaders(t *testing.T) {
}
}
func TestLoginRouteServesChatShell(t *testing.T) {
handler, err := newHandler("http://core-runtime:8080", "http://continuity-runtime:8081", "http://commit-runtime:8082", http.DefaultClient)
if err != nil {
t.Fatal(err)
}
response := httptest.NewRecorder()
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/login", nil))
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "id=\"username\"") || !strings.Contains(response.Body.String(), "id=\"password\"") {
t.Fatalf("login response = %d", response.Code)
}
}
func TestAdvisoryModeRejectsInvalidConfiguration(t *testing.T) {
_, err := newHandlerWithAdvisoryMode("http://core.invalid", "http://continuity.invalid", "http://commit.invalid", http.DefaultClient, "browser-selected")
if err == nil || !strings.Contains(err.Error(), "invalid advisory binding mode") {
+76
View File
@@ -0,0 +1,76 @@
package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"git.cin.su/m/Assistant/contracts/capability"
)
const verifierIdentity = "deterministic-task-verifier-v1"
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"runtime":"task-verifier","status":"ok"}`))
})
mux.HandleFunc("POST /v1/task-evaluations/verify", verifyHandler)
addr := ":8091"
if value := strings.TrimSpace(os.Getenv("LISTEN_ADDR")); value != "" {
addr = value
}
if err := http.ListenAndServe(addr, mux); err != nil {
panic(err)
}
}
func verifyHandler(w http.ResponseWriter, r *http.Request) {
var request capability.TaskCaseVerificationRequest
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil || request.CaseID == "" || request.ResponseText == "" || request.EvidenceArtifactRef == "" || request.LatencyMicros < 0 || (request.Verification.Mode != capability.TaskCaseVerificationExactText && request.Verification.Mode != capability.TaskCaseVerificationJSONFieldEquals) || (request.Verification.Mode == capability.TaskCaseVerificationJSONFieldEquals && request.Verification.FieldPath == "") || request.Verification.ExpectedText == "" || request.Verification.MinimumQualityMillis < 0 || request.Verification.MinimumQualityMillis > 1000 {
http.Error(w, "invalid task verification request", http.StatusBadRequest)
return
}
matched := matches(request)
quality := int64(0)
result := capability.EvaluationUnsupported
if matched {
quality = request.Verification.MinimumQualityMillis
if quality == 0 {
quality = 1000
}
result = capability.EvaluationSupported
}
evidence := verificationEvidence(request, matched)
writeJSON(w, capability.TaskCaseVerificationResponse{CaseID: request.CaseID, Result: result, QualityScoreMillis: quality, LatencyMicros: request.LatencyMicros, EvidenceArtifactRef: request.EvidenceArtifactRef, VerificationEvidenceRef: evidence, VerifierIdentity: verifierIdentity})
}
func verificationEvidence(request capability.TaskCaseVerificationRequest, matched bool) string {
payload := fmt.Sprintf("%s|%s|%s|%s|%s|%t", verifierIdentity, request.CaseID, request.EvidenceArtifactRef, request.Verification.Mode, request.Verification.FieldPath, matched)
digest := sha256.Sum256([]byte(payload))
return "verification:" + hex.EncodeToString(digest[:])
}
func matches(request capability.TaskCaseVerificationRequest) bool {
if request.Verification.Mode == capability.TaskCaseVerificationExactText {
return strings.TrimSpace(request.ResponseText) == strings.TrimSpace(request.Verification.ExpectedText)
}
var document map[string]json.RawMessage
if json.Unmarshal([]byte(request.ResponseText), &document) != nil {
return false
}
value, ok := document[request.Verification.FieldPath]
return ok && strings.TrimSpace(string(value)) == strings.TrimSpace(request.Verification.ExpectedText)
}
func writeJSON(w http.ResponseWriter, value any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(value)
}
+41
View File
@@ -0,0 +1,41 @@
package main
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"git.cin.su/m/Assistant/contracts/capability"
)
func TestVerifyExactTextProducesIndependentEvidence(t *testing.T) {
payload, _ := json.Marshal(capability.TaskCaseVerificationRequest{CaseID: "case-1", ResponseText: " answer ", EvidenceArtifactRef: "case-artifact", Verification: capability.TaskCaseVerification{Mode: capability.TaskCaseVerificationExactText, ExpectedText: "answer", MinimumQualityMillis: 900}})
recorder := httptest.NewRecorder()
verifyHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/task-evaluations/verify", bytes.NewReader(payload)))
var response capability.TaskCaseVerificationResponse
if recorder.Code != http.StatusOK || json.NewDecoder(recorder.Body).Decode(&response) != nil || response.Result != capability.EvaluationSupported || response.QualityScoreMillis != 900 || response.VerifierIdentity == "" || response.VerificationEvidenceRef == "" {
t.Fatalf("unexpected verification response: code=%d body=%s value=%+v", recorder.Code, recorder.Body.String(), response)
}
}
func TestVerifyMismatchIsUnsupportedWithEvidence(t *testing.T) {
payload, _ := json.Marshal(capability.TaskCaseVerificationRequest{CaseID: "case-1", ResponseText: "wrong", EvidenceArtifactRef: "case-artifact", Verification: capability.TaskCaseVerification{Mode: capability.TaskCaseVerificationExactText, ExpectedText: "answer", MinimumQualityMillis: 900}})
recorder := httptest.NewRecorder()
verifyHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/task-evaluations/verify", bytes.NewReader(payload)))
var response capability.TaskCaseVerificationResponse
if recorder.Code != http.StatusOK || json.NewDecoder(recorder.Body).Decode(&response) != nil || response.Result != capability.EvaluationUnsupported || response.QualityScoreMillis != 0 || response.VerificationEvidenceRef == "" {
t.Fatalf("unexpected mismatch response: code=%d body=%s value=%+v", recorder.Code, recorder.Body.String(), response)
}
}
func TestVerifyJSONFieldEquals(t *testing.T) {
payload, _ := json.Marshal(capability.TaskCaseVerificationRequest{CaseID: "json-1", ResponseText: `{"answer":42}`, EvidenceArtifactRef: "case-artifact", Verification: capability.TaskCaseVerification{Mode: capability.TaskCaseVerificationJSONFieldEquals, FieldPath: "answer", ExpectedText: "42", MinimumQualityMillis: 850}})
recorder := httptest.NewRecorder()
verifyHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/task-evaluations/verify", bytes.NewReader(payload)))
var response capability.TaskCaseVerificationResponse
if recorder.Code != http.StatusOK || json.NewDecoder(recorder.Body).Decode(&response) != nil || response.Result != capability.EvaluationSupported || response.QualityScoreMillis != 850 {
t.Fatalf("unexpected JSON verification response: code=%d body=%s value=%+v", recorder.Code, recorder.Body.String(), response)
}
}
+20
View File
@@ -70,6 +70,22 @@ type TaskEvaluationExpansionRequest struct {
BenchmarkVersion string
}
type TaskCaseVerificationMode string
const (
TaskCaseVerificationExactText TaskCaseVerificationMode = "EXACT_TEXT"
TaskCaseVerificationJSONFieldEquals TaskCaseVerificationMode = "JSON_FIELD_EQUALS"
)
// TaskCaseVerification is a bounded, task-owned rubric. The expected text is
// test data, not model output, and is never sent to the cognitive provider.
type TaskCaseVerification struct {
Mode TaskCaseVerificationMode
FieldPath string
ExpectedText string
MinimumQualityMillis int64
}
// TaskCaseObservation is produced by the bounded evaluator and verifier. A
// score without a case result and evidence reference is not accepted as
// qualification evidence.
@@ -80,6 +96,10 @@ type TaskCaseObservation struct {
LatencyMicros int64
CriticalFailure bool
EvidenceArtifactRef string
// VerificationEvidenceRef and VerifierIdentity are required whenever an
// observation claims semantic support or a non-zero quality score.
VerificationEvidenceRef string
VerifierIdentity string
}
// TaskEvaluation is durable evidence for one model/binding revision under one
@@ -11,6 +11,33 @@ type TaskEvaluationCase struct {
CaseID string
Prompt string
EvidenceArtifactRef string
Verification TaskCaseVerification
}
type TaskCaseResponse struct {
CaseID string
ResponseText string
LatencyMicros int64
EvidenceArtifactRef string
}
type TaskCaseVerificationRequest struct {
CaseID string
ResponseText string
LatencyMicros int64
EvidenceArtifactRef string
Verification TaskCaseVerification
}
type TaskCaseVerificationResponse struct {
CaseID string
Result EvaluationResult
QualityScoreMillis int64
LatencyMicros int64
CriticalFailure bool
EvidenceArtifactRef string
VerificationEvidenceRef string
VerifierIdentity string
}
type TaskEvaluationExecutionRequest struct {
@@ -31,5 +58,6 @@ type TaskEvaluationExecutionResponse struct {
BindingID CapabilityBindingID
BindingRevision int64
Observations []TaskCaseObservation
Responses []TaskCaseResponse
ObservedAtUnixMillis int64
}
+7
View File
@@ -46,6 +46,8 @@ type GraphNode struct {
TaskProfileID capability.TaskEvaluationProfileID `json:"task_profile_id,omitempty"`
BindingID capability.CapabilityBindingID `json:"binding_id,omitempty"`
CaseIDs []string `json:"case_ids,omitempty"`
TaskProfile *capability.TaskEvaluationProfile `json:"task_profile,omitempty"`
TaskCases []capability.TaskEvaluationCase `json:"task_cases,omitempty"`
}
type GraphEdge struct {
@@ -79,6 +81,11 @@ type TaskEvaluationGraphRequest struct {
ParentGraphRevision int64 `json:"parent_graph_revision,omitempty"`
ParentGraphState GraphState `json:"parent_graph_state,omitempty"`
TriggerEvidenceRef string `json:"trigger_evidence_ref,omitempty"`
Cases []capability.TaskEvaluationCase `json:"cases,omitempty"`
BindingRevision int64 `json:"binding_revision,omitempty"`
ActorID string `json:"actor_id,omitempty"`
PrincipalID string `json:"principal_id,omitempty"`
ScopeID string `json:"scope_id,omitempty"`
}
// TaskEvaluationEvidenceCallback is emitted by a specialist after it has
+15
View File
@@ -3,7 +3,9 @@ package work
import (
"time"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/identity"
)
type AgentLeaseID string
@@ -68,4 +70,17 @@ type AgentDispatchIntent struct {
TaskEvaluationProfileID capability.TaskEvaluationProfileID `json:"task_evaluation_profile_id,omitempty"`
TaskEvaluationBindingID capability.CapabilityBindingID `json:"task_evaluation_binding_id,omitempty"`
TaskEvaluationCaseIDs []string `json:"task_evaluation_case_ids,omitempty"`
TaskEvaluationProfile *capability.TaskEvaluationProfile `json:"task_evaluation_profile,omitempty"`
TaskEvaluationCases []capability.TaskEvaluationCase `json:"task_evaluation_cases,omitempty"`
TaskEvaluationExecution *TaskEvaluationDispatchContext `json:"task_evaluation_execution,omitempty"`
}
type TaskEvaluationDispatchContext struct {
GraphID string `json:"graph_id"`
GraphRevision int64 `json:"graph_revision"`
NodeID GraphNodeID `json:"node_id"`
BindingRevision int64 `json:"binding_revision"`
ActorID identity.ActorID `json:"actor_id"`
PrincipalID identity.PrincipalID `json:"principal_id"`
ScopeID authority.ScopeID `json:"scope_id"`
}
+17 -15
View File
@@ -70,6 +70,7 @@ type ExtensionProposalRequest struct {
// authority grant, implementation body or promotion decision.
type ExtensionProposal struct {
ID string
RequestedByPrincipalID string
Stage ExtensionProposalStage
EvidenceRefs []string
DeduplicatedSignalKeys []string
@@ -113,19 +114,20 @@ type DevelopmentScaffoldRequest struct {
// DevelopmentScaffold is a provenance-linked task-packet candidate. It has
// no executable instruction, credential, checkout handle or authority grant.
type DevelopmentScaffold struct {
ID string
Stage DevelopmentScaffoldStage
ProposalRef string
BaseRepoRevision string
WorktreeRef string
TaskIntent string
Modules []string
AllowedPaths []string
ForbiddenPaths []string
RequiredTests []string
ArtifactLineageRefs []string
ProtectedMainAccess string
ProductionAccess string
SecretAccess string
CreatedAtUnixMillis int64
ID string
RequestedByPrincipalID string
Stage DevelopmentScaffoldStage
ProposalRef string
BaseRepoRevision string
WorktreeRef string
TaskIntent string
Modules []string
AllowedPaths []string
ForbiddenPaths []string
RequiredTests []string
ArtifactLineageRefs []string
ProtectedMainAccess string
ProductionAccess string
SecretAccess string
CreatedAtUnixMillis int64
}
@@ -0,0 +1,16 @@
BEGIN;
SET ROLE work_role;
ALTER TABLE work.agent_dispatch_intents
ADD COLUMN IF NOT EXISTS task_evaluation_profile JSONB,
ADD COLUMN IF NOT EXISTS task_evaluation_cases JSONB NOT NULL DEFAULT '[]',
ADD COLUMN IF NOT EXISTS task_evaluation_execution JSONB;
ALTER TABLE work.graph_nodes
ADD COLUMN IF NOT EXISTS task_evaluation_profile JSONB,
ADD COLUMN IF NOT EXISTS task_evaluation_cases JSONB NOT NULL DEFAULT '[]',
ADD COLUMN IF NOT EXISTS task_evaluation_execution JSONB;
RESET ROLE;
COMMIT;
@@ -0,0 +1,15 @@
BEGIN;
SET ROLE work_role;
ALTER TABLE work.agent_dispatch_queue
ADD COLUMN IF NOT EXISTS retry_limit INTEGER NOT NULL DEFAULT 3;
ALTER TABLE work.agent_dispatch_queue
DROP CONSTRAINT IF EXISTS agent_dispatch_queue_retry_limit_check;
ALTER TABLE work.agent_dispatch_queue
ADD CONSTRAINT agent_dispatch_queue_retry_limit_check CHECK (retry_limit > 0);
RESET ROLE;
COMMIT;
@@ -0,0 +1,45 @@
BEGIN;
SET ROLE work_role;
CREATE TABLE IF NOT EXISTS work.extension_proposals (
proposal_id TEXT PRIMARY KEY,
requested_by_principal_id TEXT NOT NULL,
stage TEXT NOT NULL,
evidence_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
deduplicated_signal_keys TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
desired_outcome TEXT NOT NULL,
smallest_change_layer TEXT NOT NULL,
affected_owners TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
affected_contracts TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
risk_tier TEXT NOT NULL,
evaluation_plan TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
alternatives TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
rollback_plan TEXT NOT NULL,
assumptions TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
created_at_unix_millis BIGINT NOT NULL,
CHECK (proposal_id <> ''), CHECK (requested_by_principal_id <> ''), CHECK (stage = 'PROPOSED'), CHECK (desired_outcome <> ''), CHECK (smallest_change_layer <> ''), CHECK (risk_tier <> ''), CHECK (rollback_plan <> ''), CHECK (created_at_unix_millis > 0)
);
CREATE TABLE IF NOT EXISTS work.development_scaffolds (
scaffold_id TEXT PRIMARY KEY,
requested_by_principal_id TEXT NOT NULL,
stage TEXT NOT NULL,
proposal_ref TEXT NOT NULL,
base_repo_revision TEXT NOT NULL,
worktree_ref TEXT NOT NULL,
task_intent TEXT NOT NULL,
modules TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
allowed_paths TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
forbidden_paths TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
required_tests TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
artifact_lineage_refs TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
protected_main_access TEXT NOT NULL,
production_access TEXT NOT NULL,
secret_access TEXT NOT NULL,
created_at_unix_millis BIGINT NOT NULL,
CHECK (scaffold_id <> ''), CHECK (requested_by_principal_id <> ''), CHECK (stage = 'SANDBOXED'), CHECK (proposal_ref <> ''), CHECK (base_repo_revision <> ''), CHECK (worktree_ref <> ''), CHECK (task_intent <> ''), CHECK (protected_main_access = 'DENIED'), CHECK (production_access = 'DENIED'), CHECK (secret_access = 'DENIED'), CHECK (created_at_unix_millis > 0)
);
RESET ROLE;
COMMIT;
+15 -1
View File
@@ -118,10 +118,24 @@ services:
WORK_POSTGRES_CREDENTIAL_REF: credential://domovoy/work/db-password/v1
WORK_POSTGRES_PASSWORD_FILE: /run/secrets/work_db_password
SECRET_BACKEND_ROOT: /run/secrets
depends_on: { postgres: { condition: service_healthy } }
TASK_VERIFIER_URL: http://task-verifier:8091
depends_on:
postgres: { condition: service_healthy }
task-verifier: { condition: service_healthy }
volumes:
- assistant_core_secrets:/run/secrets:ro
task-verifier:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: task-verifier, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
environment:
LISTEN_ADDR: :8091
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8091/healthz >/dev/null"]
interval: 10s
timeout: 3s
retries: 10
work-graph-runtime:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: work-graph-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000212-task-evaluator-invocation.json",
"task_id": "DEV-000212",
"task_file": "dev/tasks/DEV-000229-research-runtime-bridge.json",
"task_id": "DEV-000229",
"task_revision": 1
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000213",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "a86d1994c3f48db86592eb87fb54533297e0671c",
"risk_tier": "E",
"intent": "Carry the complete bounded task-evaluation profile and case payload through durable Work Graph dispatch so agent-worker can invoke Cognitive Worker, aggregate observations, and return the existing Work Graph callback after restart-safe recovery.",
"requirement_ids": ["dispatch-to-evaluator link", "payload durability", "worker recovery", "observation aggregation", "callback integration"],
"allowed_change_scope": {"modules": ["work", "agent-worker"], "paths": ["contracts/work/**", "modules/work/**", "adapters/postgres/work_repository.go", "cmd/agent-worker/**", "cmd/work-graph-runtime/**", "database/migrations/041_task_evaluation_payload.sql", "tests/agent_execution/**", "tests/model_qualification/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000213-worker-evaluator-link.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "cmd/devctl/**", "modules/identity/**", "modules/capability/**"]},
"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": ["TaskEvaluationExecutionRequest/Response", "Work Graph callback", "existing AgentExecutor and recovery contract"], "may_change": ["typed dispatch payload and worker evaluator executor"]},
"semantic_owner": "work owns dispatch lifecycle; capability owns evaluation evidence; cognitive-worker owns provider invocation",
"canonical_state": ["work dispatch payload is durable input; Capability remains evidence owner"],
"state_transitions": ["task graph node -> durable payload dispatch", "worker claim -> evaluator invocation", "response -> aggregate TaskEvaluation", "aggregate -> Work Graph evidence callback"],
"side_effects_and_privileges": ["HTTP calls to internal cognitive/core/work boundaries; no direct foreign DB and no new secret"],
"persistence_and_migrations": ["041 adds JSONB payload columns to work graph nodes and dispatch intents"],
"failure_retry_restart": ["payload is loaded after stale claim recovery; evaluator failure leaves dispatch retryable", "callback only follows a persisted aggregate; unknown remains unknown"],
"assumptions": ["Cognitive Worker endpoint is available as optional local execution path", "external bridge remains preferred for non-evaluation agent tasks"],
"acceptance_scenarios": ["profile/cases survive save-load", "task dispatch invokes evaluator with original prompt payload", "all observations aggregate deterministically", "callback receives matching binding/profile/artifact", "evaluator or callback failure does not complete dispatch", "legacy dispatch remains unchanged"],
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "migration/schema checks", "CI", "production public health check"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000214",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "f9b9765100df02bead16280d2db9c1af478a4589",
"risk_tier": "E",
"intent": "Make Agent Execution recovery restart-safe: renew active leases with managed heartbeats, fence stale workers, cap retries, and requeue transient execution failures without losing durable dispatch state.",
"requirement_ids": ["durable heartbeat", "lease fencing", "bounded retry", "restart recovery", "transient failure requeue"],
"allowed_change_scope": {"modules": ["work", "agent-worker"], "paths": ["contracts/work/**", "modules/work/**", "adapters/postgres/work_repository.go", "cmd/agent-worker/**", "cmd/agent-canary/**", "database/migrations/042_agent_execution_recovery.sql", "tests/agent_execution/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000214-agent-execution-recovery.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "cmd/devctl/**", "modules/identity/**", "modules/capability/**", "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": 1},
"contracts": {"consume": ["existing AgentExecutor", "existing lease/callback contract", "durable dispatch queue"], "may_change": ["WorkerStore retry/recovery port"]},
"semantic_owner": "work owns dispatch, lease, callback and retry lifecycle",
"canonical_state": ["work agent dispatch queue and lease tables remain canonical; heartbeat is durable evidence of liveness"],
"state_transitions": ["CLAIMED -> QUEUED on transient failure or stale recovery", "CLAIMED -> FAILED after retry limit", "ACTIVE lease -> EXPIRED before stale claim requeue", "heartbeat renews lease and callback sequence"],
"side_effects_and_privileges": ["work_role writes only work-owned queue and lease state; no external mutation"],
"persistence_and_migrations": ["042 adds bounded retry limit to dispatch queue"],
"failure_retry_restart": ["stale recovery expires the old lease before requeue", "transient execution failure uses durable delayed retry", "retry limit prevents hot-loop poison dispatch", "heartbeat failure cancels execution and leaves a retryable dispatch"],
"assumptions": ["operational heartbeat ticker is lifecycle-managed by the bounded worker call and durable state remains authoritative"],
"acceptance_scenarios": ["long execution renews lease and emits ordered heartbeats", "stale claim is fenced and requeued", "third failed attempt becomes FAILED", "waiting/error dispatch is not left CLAIMED", "legacy dispatch behavior remains compatible"],
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "migration/schema checks", "CI", "production public health check"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000215",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "8d3e7f7cac236aa50919bd1cd854012dcc8500a5",
"risk_tier": "E",
"intent": "Expose a durable, authenticated control-plane boundary for governed self-development proposals and isolated scaffold candidates without executing source mutation, bypassing verification, or granting promotion authority.",
"requirement_ids": ["durable proposal intake", "isolated scaffold contract", "authenticated operator boundary", "no implicit promotion"],
"allowed_change_scope": {"modules": ["work", "core-runtime"], "paths": ["contracts/work/self_development.go", "modules/work/domain/**", "cmd/core-runtime/main.go", "adapters/postgres/work_repository.go", "database/migrations/043_governed_development_candidates.sql", "tests/semantic_evolution/**", "tests/work/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000215-governed-development-control-plane.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "cmd/devctl/**", "modules/identity/**", "modules/capability/**", "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": 1},
"contracts": {"consume": ["ADR-0022 governed self-development boundary", "creator authentication", "work development gate"], "may_change": ["work-owned proposal/scaffold persistence and authenticated control-plane endpoints"]},
"semantic_owner": "work owns proposal, scaffold and development gate lineage",
"canonical_state": ["work-owned extension proposal and scaffold candidates are durable; source tree and production remain outside this task"],
"state_transitions": ["repeated evidence -> PROPOSED", "proposal -> SANDBOXED scaffold candidate", "gate remains explicit and cannot be promoted by these endpoints"],
"side_effects_and_privileges": ["authenticated creator session required; database writes stay within work owner; no checkout, source mutation, deployment or secret access"],
"persistence_and_migrations": ["043 adds durable work-owned proposal/scaffold candidate records"],
"failure_retry_restart": ["idempotent candidate IDs preserve lineage; duplicate IDs are rejected; no endpoint advances promotion stage"],
"assumptions": ["an independent executor and verifier remain separate future capabilities"],
"acceptance_scenarios": ["unauthenticated requests are rejected", "repeated evidence creates a durable proposal", "protected paths cannot create a scaffold", "valid scaffold is durable and remains SANDBOXED", "no promotion or source mutation occurs"],
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "migration/schema checks", "CI", "production public health check"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000216",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "62f0c529b3f8b3c92e4a69fcc6298d59b3015fde",
"risk_tier": "E",
"intent": "Keep agent-worker status truthful when the protected external execution bridge is absent while retaining local task-evaluation fallback.",
"requirement_ids": ["WP-301", "truthful degraded status", "local fallback"],
"allowed_change_scope": {"modules": ["agent-worker"], "paths": ["cmd/agent-worker/main.go", "cmd/agent-worker/status_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000216-agent-worker-status-truth.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "database/**", "deploy/**", "security/**", "contracts/**", "modules/**", "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": ["protected bridge file", "agent-worker status response", "local task-evaluation fallback"], "may_change": []},
"semantic_owner": "work",
"canonical_state": ["existing worker status projection and durable dispatch lifecycle"],
"state_transitions": ["bridge absent -> local-fallback status", "bridge present -> execution status"],
"side_effects_and_privileges": ["no external mutation; no new credentials; no persistence change"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["bridge absence remains explicit after restart; local evaluator remains available for bounded qualification work"],
"assumptions": ["task-evaluation fallback is not general external agent execution"],
"acceptance_scenarios": ["worker marks local_fallback when protected bridge URL is absent", "status never reports executor_configured=true for local fallback", "configured bridge path remains execution mode", "no secret value is exposed"],
"required_tests": ["go test ./cmd/agent-worker", "go vet ./cmd/agent-worker", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production worker status"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000217",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "05a246ea3f240a77be769969ad62a236fba33bac",
"risk_tier": "E",
"intent": "Refresh the project status projection and redacted live evidence after the agent-worker truthful fallback fix without changing runtime behavior.",
"requirement_ids": ["current audit evidence", "task lineage accuracy", "negative-state transparency"],
"allowed_change_scope": {"modules": ["operations-evidence"], "paths": ["docs/PROJECT_STATUS.md", "docs/evidence/audit-runtime-status-2026-08-23-v2.json", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000217-current-status-evidence.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "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": ["live health/readiness/status responses", "merged task lineage"], "may_change": ["operations status projection and redacted evidence artifact"]},
"semantic_owner": "operations-evidence projection",
"canonical_state": ["none; docs and evidence are rebuildable projections"],
"state_transitions": ["stale status projection -> current evidence projection"],
"side_effects_and_privileges": ["read-only live checks; no secrets, credentials or runtime mutation"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["evidence records unavailable/degraded states explicitly; no success inferred from missing data"],
"assumptions": ["historical evidence remains immutable and is not rewritten"],
"acceptance_scenarios": ["active task points to the current merged packet", "bridge absence is reported as local fallback", "GPU timeout is reported as degraded without changing FAST preference", "authenticated UI availability is not claimed without a current signed-in session", "no secret value appears in the projection"],
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production public health and runtime status"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000218",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "ab840878fe7d21a0a6aeb69193a3d58ea23a56e0",
"risk_tier": "E",
"intent": "Add a conservative deterministic policy gate for creating additional model-evaluation profiles only after general evaluation evidence remains unresolved.",
"requirement_ids": ["general-first evaluation", "bounded additional evaluation", "no unnecessary fleet-wide tests"],
"allowed_change_scope": {"modules": ["capability"], "paths": ["modules/capability/domain/task_evaluation.go", "modules/capability/domain/task_evaluation_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000218-automatic-expansion-policy.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "cmd/**", "contracts/**", "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": ["TaskEvaluationProfile", "TaskEvaluation", "TaskEvaluationExpansionRequest"], "may_change": []},
"semantic_owner": "capability owns model qualification evidence and expansion policy",
"canonical_state": ["none; this packet creates no persistence or activation authority"],
"state_transitions": ["general COMPLETED/PARTIAL unresolved evidence -> bounded additional profile candidate", "qualified candidate -> no expansion"],
"side_effects_and_privileges": ["pure deterministic decision; no provider call, no model invocation, no mutation"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["missing trigger evidence or additional cases fails closed; repeated decision is deterministic"],
"assumptions": ["a later task supplies task-specific additional cases from an unresolved outcome and schedules them through the existing Work Graph"],
"acceptance_scenarios": ["general graph not completed/partial does not expand", "unknown/unsupported/low-scoring candidates expand only with evidence and new cases", "one qualified candidate suppresses expansion", "no provider or hardware label changes the decision"],
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production public health check"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000219",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "7476f43ffa57d55f5a5a9bddc7f1c74e9b87ae0e",
"risk_tier": "E",
"intent": "Prevent task-evaluation invocation success from being recorded as semantic model quality or support without independent verifier evidence.",
"requirement_ids": ["truthful model evidence", "independent verification", "no false qualification"],
"allowed_change_scope": {"modules": ["cognitive-worker"], "paths": ["cmd/cognitive-worker/task_evaluation.go", "cmd/cognitive-worker/task_evaluation_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000219-reachability-not-quality.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "modules/**", "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": ["TaskEvaluationExecutionRequest/Response", "TaskCaseObservation", "Cognitive Gateway"], "may_change": []},
"semantic_owner": "capability evidence remains owner; cognitive-worker only records invocation observation",
"canonical_state": ["existing task-evaluation evidence; no new persistence"],
"state_transitions": ["successful invocation -> UNKNOWN reachability observation", "independent verifier evidence -> future supported/quality observation"],
"side_effects_and_privileges": ["no additional provider calls or privileges"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["unknown evidence remains retryable/eligible for a separately verified evaluation; no promotion follows invocation alone"],
"assumptions": ["a later verifier task will attach answer-quality evidence and score cases"],
"acceptance_scenarios": ["nonempty provider response is not SUPPORTED", "quality score remains zero", "provider invocation failure remains UNKNOWN with critical failure", "no model/hardware label can manufacture quality evidence"],
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production public health check"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000220",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "66ed523248ea6cf0a41383e2206d9a70eeaafda8",
"risk_tier": "E",
"intent": "Require explicit independent verifier identity and evidence before task evaluation can claim semantic support or non-zero quality.",
"requirement_ids": ["independent verifier evidence", "no false quality", "typed model qualification"],
"allowed_change_scope": {"modules": ["capability", "agent-worker"], "paths": ["contracts/capability/task_evaluation.go", "modules/capability/domain/task_evaluation.go", "modules/capability/domain/task_evaluation_test.go", "cmd/agent-worker/task_evaluation_executor_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000220-verifier-evidence-boundary.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "cmd/cognitive-worker/**", "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": ["TaskCaseObservation", "TaskEvaluation", "existing Work Graph callback"], "may_change": ["typed observation fields for verifier evidence"]},
"semantic_owner": "capability owns qualification evidence; verifier remains independent of provider invocation",
"canonical_state": ["existing capability task-evaluation evidence"],
"state_transitions": ["reachability UNKNOWN -> separately verified SUPPORTED only with evidence"],
"side_effects_and_privileges": ["no provider invocation or external mutation"],
"persistence_and_migrations": ["existing task evaluation persistence is unchanged"],
"failure_retry_restart": ["missing verifier fields fail closed; unknown reachability remains non-promotable"],
"assumptions": ["a later verifier runtime will produce VerificationEvidenceRef and VerifierIdentity"],
"acceptance_scenarios": ["supported without verifier is rejected", "unknown with zero quality remains valid", "verified supported observation aggregates normally", "no hardware/provider name bypasses the boundary"],
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production public health check"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000221",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "0aab10863cb23824688029c0bab6734f100b4610",
"risk_tier": "E",
"intent": "Provide an independent deterministic task-verifier runtime and wire model task evaluation through response evidence rather than provider self-assertion.",
"requirement_ids": ["independent verifier runtime", "typed answer evidence", "quality qualification boundary", "degraded verifier fallback"],
"allowed_change_scope": {"modules": ["capability", "cognitive-worker", "agent-worker", "task-verifier", "deployment"], "paths": ["contracts/capability/task_evaluation.go", "contracts/capability/task_evaluation_execution.go", "cmd/cognitive-worker/task_evaluation.go", "cmd/agent-worker/main.go", "cmd/agent-worker/task_evaluation_executor.go", "cmd/agent-worker/task_evaluation_executor_test.go", "cmd/task-verifier/**", "deploy/docker-compose.prod.yml", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000221-task-verifier-runtime.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "modules/**", "LLM-local/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["TaskEvaluationCase", "TaskEvaluationExecutionRequest/Response", "TaskCaseObservation", "existing worker callback"], "may_change": ["typed verification spec, response envelope and verifier result" ]},
"semantic_owner": "capability owns evidence; task-verifier is a replaceable independent verifier adapter and does not own canonical state",
"canonical_state": ["existing capability task-evaluation evidence only; verifier has no database"],
"state_transitions": ["provider response -> bounded response evidence -> verifier result -> TaskCaseObservation -> TaskEvaluation"],
"side_effects_and_privileges": ["local network calls only; no secrets; no external mutation; no authority expansion"],
"persistence_and_migrations": ["none; verifier evidence is carried through existing callback and capability persistence"],
"failure_retry_restart": ["verifier unavailable returns local fallback/waiting; no quality or promotion claim is made"],
"assumptions": ["initial verifier mode is exact-text for bounded deterministic cases; future verifiers may add typed modes under separate task packets"],
"acceptance_scenarios": ["exact response produces supported evidence with verifier identity", "mismatch produces unsupported evidence", "cognitive worker never asserts support from invocation alone", "worker wires verifier result before callback", "verifier process has health endpoint and production compose dependency", "verifier loss does not produce false quality"],
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production public health and verifier health check"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000222",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "d736500d9818a7748c3715c442edb695c5aed1100",
"risk_tier": "E",
"intent": "Extend the independent task verifier with a structured JSON field mode and add a bounded case builder driven by unresolved evidence.",
"requirement_ids": ["task-aware verification", "additional cases during lifetime", "general-first bounded expansion"],
"allowed_change_scope": {"modules": ["capability", "task-verifier"], "paths": ["contracts/capability/task_evaluation.go", "cmd/task-verifier/main.go", "cmd/task-verifier/main_test.go", "modules/capability/domain/task_evaluation.go", "modules/capability/domain/task_evaluation_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000222-verifier-modes-and-case-builder.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "cmd/core-runtime/**", "cmd/creator-web/**", "cmd/agent-worker/**", "cmd/cognitive-worker/**", "modules/identity/**", "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": ["TaskCaseVerification", "TaskEvaluationCase", "unresolved outcome evidence"], "may_change": ["verification modes and bounded case construction" ]},
"semantic_owner": "capability owns evaluation policy; verifier remains stateless adapter",
"canonical_state": ["none; case builder returns candidates and does not persist or activate them"],
"state_transitions": ["unresolved evidence -> two bounded additional case candidates", "structured response -> verifier evidence -> supported/unsupported"],
"side_effects_and_privileges": ["no model calls, no external mutation, no secrets"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["invalid unresolved evidence fails closed; verifier remains stateless and repeatable"],
"assumptions": ["generated cases still require an explicit verification spec before quality can be claimed"],
"acceptance_scenarios": ["JSON field match verifies with evidence", "JSON mismatch is unsupported", "unresolved evidence yields exactly two bounded probes", "generated cases do not bypass verifier or promotion gate"],
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production public health check"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000223",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "ee68e2207eff5535c79cc594637a971cea03d71c",
"risk_tier": "E",
"intent": "Reconcile the status projection with the merged DEV-000222 verifier stage and preserve explicit remaining runtime and UI gaps.",
"requirement_ids": ["current audit evidence", "task lineage accuracy", "negative-state transparency"],
"allowed_change_scope": {"modules": ["operations-evidence"], "paths": ["docs/PROJECT_STATUS.md", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000223-status-reconciliation.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "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": ["merged task lineage", "existing redacted live evidence"], "may_change": ["operations status projection"]},
"semantic_owner": "operations-evidence projection",
"canonical_state": ["none; docs are rebuildable projections"],
"state_transitions": ["stale task pointer -> merged verifier stage pointer"],
"side_effects_and_privileges": ["none; documentation only"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["historical evidence remains immutable"],
"assumptions": ["DEV-000222 main revision ee68e22 is the current deployed baseline"],
"acceptance_scenarios": ["status names DEV-000222 as last completed", "remaining bridge and current authenticated UI gaps remain explicit", "no secret value is added"],
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production public health check"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000224",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "c4411afaaf21886cba9b5ead586afb26d697a355",
"risk_tier": "E",
"intent": "Record the current production model-fleet readiness and dynamic tier selection without claiming quality or promotion.",
"requirement_ids": ["model fleet/failover", "GPU is not intelligence priority", "truthful operational evidence"],
"allowed_change_scope": {"modules": ["operations-evidence"], "paths": ["docs/PROJECT_STATUS.md", "docs/evidence/fleet-readiness-live-2026-08-23-v2.json", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000224-live-fleet-evidence.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "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": ["live cognitive-worker fleet readiness response"], "may_change": ["redacted evidence and status projection"]},
"semantic_owner": "operations-evidence projection",
"canonical_state": ["none; evidence is a rebuildable projection"],
"state_transitions": ["live fleet readiness -> redacted evidence record"],
"side_effects_and_privileges": ["read-only production inspection"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["unavailable providers remain explicit; no missing data is promoted to ready"],
"assumptions": ["fleet endpoint response is authoritative for this observation window"],
"acceptance_scenarios": ["FAST unavailable selects available MEDIUM", "HEAVY availability does not imply priority", "LIFEBOAT remains fallback", "secret values are absent"],
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production public health and fleet live check"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
+23
View File
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000225",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "e080c34a772caa5d256ab9f4ae0d2568bb8a7fb9",
"risk_tier": "E",
"intent": "Serve the existing chat/login shell at the direct /login route so users can reach authentication without a 404.",
"requirement_ids": ["UI login direct route", "authenticated acceptance entrypoint"],
"allowed_change_scope": {"modules": ["creator-web"], "paths": ["cmd/creator-web/server.go", "cmd/creator-web/server_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000225-login-route-fix.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "modules/**", "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": ["existing embedded chat/login shell"], "may_change": ["HTTP route mapping only"]},
"semantic_owner": "creator-web route projection",
"canonical_state": ["none"],
"state_transitions": ["GET /login -> existing chat shell"],
"side_effects_and_privileges": ["none; GET only"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["route remains deterministic and stateless"],
"assumptions": ["authentication behavior remains unchanged and is handled by the existing chat shell"],
"acceptance_scenarios": ["GET /login returns 200", "login inputs are present", "root route remains unchanged", "no credential values are added"],
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production public health and /login check"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000226",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "40b56798e23d852d26156b0bc2abbc518fd6b9b6",
"risk_tier": "E",
"intent": "Record current authenticated desktop/mobile UI and operations acceptance evidence without mutating production state.",
"requirement_ids": ["UI operations acceptance", "authenticated browser evidence", "responsive acceptance"],
"allowed_change_scope": {"modules": ["operations-evidence"], "paths": ["docs/PROJECT_STATUS.md", "docs/evidence/authenticated-ui-acceptance-2026-08-23-v2.json", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000226-ui-acceptance-evidence.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "cmd/**", "modules/**", "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": ["current authenticated browser DOM and visual state", "live production UI responses"], "may_change": ["redacted UI acceptance evidence and status projection"]},
"semantic_owner": "operations-evidence projection",
"canonical_state": ["none; evidence is rebuildable and redacted"],
"state_transitions": ["current browser acceptance -> evidence record"],
"side_effects_and_privileges": ["read-only navigation and section inspection; no production mutation"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["missing authenticated session or unavailable section must remain explicit"],
"assumptions": ["browser evidence reflects the deployed revision shown in the page metadata"],
"acceptance_scenarios": ["root chat authenticated", "diagnostics 7/7", "fleet 4/4 with fallback visible", "learning/reflection visible", "390x844 layout readable", "no secret values retained"],
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production public health and authenticated browser evidence"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
+47
View File
@@ -0,0 +1,47 @@
{
"task_id": "DEV-000227",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "1f6ca363ee241b41a89bff1f1b603f15e9873b00",
"risk_tier": "E",
"intent": "Make the existing bounded local task-evaluation path an explicit local executor capability while preserving truthful durable waiting for unsupported work and keeping the external bridge optional.",
"requirement_ids": ["local executor", "truthful degraded execution", "optional external bridge"],
"allowed_change_scope": {
"modules": ["agent-worker", "operations-status"],
"paths": [
"cmd/agent-worker/local_fallback.go",
"cmd/agent-worker/local_fallback_test.go",
"cmd/agent-worker/task_evaluation_executor.go",
"cmd/agent-worker/status.go",
"cmd/agent-worker/status_test.go",
"cmd/agent-worker/main.go",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000227-local-executor.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.AgentExecutor", "typed task-evaluation runtime endpoints"],
"may_change": ["agent-worker status projection adds local executor capability metadata"]
},
"semantic_owner": "work agent-worker runtime",
"canonical_state": ["existing durable Work dispatch, lease and callback state"],
"state_transitions": ["unsupported local request -> WAITING", "supported task evaluation -> verified callback"],
"side_effects_and_privileges": ["existing bounded internal task-evaluation calls and Work callback only; no external mutation authority"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["unsupported work remains durable WAITING; existing lease recovery remains authoritative"],
"assumptions": ["task-evaluation execution and verification endpoints are already deployed internal capabilities", "external bridge may remain absent"],
"acceptance_scenarios": ["local status names bounded local executor and capability", "unsupported work never claims completion", "task evaluation still executes, verifies and callbacks", "external bridge status remains separate", "no secret or new privilege is introduced"],
"required_tests": ["go test ./cmd/agent-worker/...", "go vet ./cmd/agent-worker/...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production status smoke"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000228",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "2609f31e59d3914cdf4adcf23ec618705277d12a",
"risk_tier": "E",
"intent": "Expose a read-only, provider-neutral discovery status view so Domovoy can inspect available optional providers and models before requesting qualification, without exposing credential metadata or activating anything.",
"requirement_ids": ["controlled discovery", "provider qualification visibility", "optional external tools"],
"allowed_change_scope": {"modules": ["cognitive-worker", "operations-status"], "paths": ["cmd/cognitive-worker/discovery.go", "cmd/cognitive-worker/discovery_test.go", "cmd/cognitive-worker/main.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000228-controlled-discovery.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": ["existing LocalProvider discovery probes", "existing capability model metadata"], "may_change": ["read-only cognitive-worker discovery status projection"]},
"semantic_owner": "cognitive-worker observation projection",
"canonical_state": ["existing Capability discovery registration remains the canonical persisted state"],
"state_transitions": ["provider probe -> observed discovery status", "discovery registration remains DISCOVERED/INACTIVE until qualification and promotion"],
"side_effects_and_privileges": ["read-only provider probe on explicit status request; no activation, mutation or credential disclosure"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["unavailable provider is reported with health/failure state; persisted discovery remains available through existing owner"],
"assumptions": ["provider discovery adapters already enforce bounded requests", "qualification remains a separate task-evaluation and promotion step"],
"acceptance_scenarios": ["status lists registered providers deterministically", "model inventory and probe health are visible", "credential references are not returned", "status does not activate or persist a provider", "unknown provider remains absent or unavailable"],
"required_tests": ["go test ./cmd/cognitive-worker/...", "go vet ./cmd/cognitive-worker/...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production status smoke"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000229",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "22f839df99d8f6abd95f00cf99d34b4b45ace692",
"risk_tier": "E",
"intent": "Connect authenticated Domovoy/Core requests to the existing bounded Truth/Research web runtime while preserving source freshness, exact citations, conflicts and read-only connector policy.",
"requirement_ids": ["Truth/Research runtime", "authenticated internet research", "source evidence"],
"allowed_change_scope": {"modules": ["core-runtime", "research-runtime-bridge"], "paths": ["cmd/core-runtime/main.go", "cmd/core-runtime/research_client.go", "cmd/core-runtime/research_client_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000229-research-runtime-bridge.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": ["existing epistemics.WebResearchRequest and ResearchResult", "existing authenticated Creator session"], "may_change": ["authenticated Core-to-research runtime adapter route"]},
"semantic_owner": "epistemics research runtime; Core is an authenticated transport boundary",
"canonical_state": ["epistemics runtime remains sole owner of persisted truth bundles, claims, beliefs and citations"],
"state_transitions": ["authenticated request -> bounded web connector -> evaluated persisted finding"],
"side_effects_and_privileges": ["read-only outbound source fetches to preconfigured allowlisted hosts; authenticated user request persists evidence in epistemics owner"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["research runtime unavailable returns explicit gateway failure; no claim is emitted by Core"],
"assumptions": ["research runtime allowlist is configured outside code", "web connector retains exact source snapshot and does not invent citations"],
"acceptance_scenarios": ["unauthenticated research request is denied", "authenticated request reaches research runtime", "source evidence and finding are returned without fabricated claims", "research runtime remains bounded by allowlist", "no secret is forwarded or returned"],
"required_tests": ["go test ./cmd/core-runtime/...", "go vet ./cmd/core-runtime/...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production health and research-runtime readiness smoke"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
+8 -2
View File
@@ -2,6 +2,8 @@
## Current audit delta — 2026-08-23
- `LAST_COMPLETED = DEV-000222`; independent task verification now supports exact text and structured top-level JSON-field checks, and unresolved outcomes can produce two bounded additional case candidates after the general evaluation stage. The task packet is merged to `main` and deployed; quality remains unclaimed unless verifier evidence is present.
- `CURRENT_PRODUCTION_STATUS = LIVE_HEALTHY`; public `/healthz` is healthy. The exact deployed revision is authoritative in the deployment log and merge record, not duplicated in this mutable status projection. Evidence: `docs/evidence/audit-runtime-status-2026-08-23-v1.json`.
- `FAST_PROVIDER_STATUS = LIVE_SMOKE_SUPPORTED`: local CPU gateway authentication is configured through a protected file reference; four bounded local-only advisory cases completed through cognitive-worker. Evidence: `docs/evidence/wp-601-fast-gateway-live-smoke-2026-08-22-v1.json`.
- `MEDIUM_PROVIDER_STATUS = LIVE_BOUNDED_SMOKE_SUPPORTED_UNPROMOTED`; four bounded local-provider cases passed with template thinking disabled. This is availability/response-shape evidence only; lifecycle qualification, activation and promotion remain unclaimed. Evidence: `docs/evidence/medium-live-bounded-smoke-2026-08-23-v1.json`.
@@ -13,13 +15,17 @@
- `BACKUP_RESTORE_STATUS = VERIFIED`; latest checksum and isolated restore evidence is retained in `docs/evidence/wp-002-postgres-restore-2026-08-22-v1.json`.
- `GOVERNED_SELF_DEVELOPMENT_STATUS = DURABLE_GATE_PERSISTED`; Work-owned revisioned gate storage and optimistic-concurrency persistence are live. No bridge-backed source mutation, promotion or rollback is claimed. Evidence: `docs/evidence/governed-development-gate-persistence-2026-08-23-v1.json`.
- `UI_AUTHENTICATED_ACCEPTANCE = AUTHENTICATED_QUICK_SMOKE_PASS`; browser-driven smoke completed in the active production session: chat response received, admin login accepted, diagnostics 7/7 passed, operations fleet 4/4 READY, and action history loaded. Deep mutation, delegation-specific and long-running acceptance remain intentionally deferred.
- `CURRENT_UI_SESSION_STATUS = NO_CURRENT_AUTHENTICATED_SESSION`; the historical quick-smoke evidence above is retained, but the current acceptance run has no signed-in browser session, so no new authenticated visual result is claimed. Public cabinet shell and unauthenticated protection were checked separately.
- `AGENT_WORKER_STATUS_CORRECTION = LIVE_TRUTHFUL_LOCAL_EXECUTOR`; absence of the protected bridge is reported as `executor_configured=false`, `mode=local-executor`, `ready=false`, with `task-evaluation` as the only advertised local capability; unsupported work remains durably waiting and is not represented as external agent execution. Evidence: `docs/evidence/audit-runtime-status-2026-08-23-v2.json` (pre-DEV-000227 field shape remains historical).
- `FLEET_READINESS_LIVE = MEDIUM_SELECTED_BY_AVAILABILITY`; the live cognitive worker observed FAST unavailable, MEDIUM available, HEAVY available and LIFEBOAT available; it selected MEDIUM while the configured preference remained FAST. This is routing evidence only, not a quality or promotion claim. Evidence: `docs/evidence/fleet-readiness-live-2026-08-23-v2.json`.
- `UI_AUTHENTICATED_ACCEPTANCE = CURRENT_LIVE_PASS`; current signed-in browser evidence covers root chat session, admin diagnostics 7/7, operations fleet 4/4 READY with FAST selected, truthful local-executor agent status, action history and learning/reflection sections; the 390x844 console layout remained readable without horizontal overflow. Evidence: `docs/evidence/authenticated-ui-acceptance-2026-08-23-v2.json`.
## Current governance checkpoint
- `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-000207-gpu-invocation-qualification.json revision 1`
- `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.
- `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 = resolve the protected GPU request compatibility gap, provision/qualify an independently operated Agent Execution bridge, and complete deferred deep browser acceptance; retain explicit evidence for all remaining negative directions.`
- `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.`
@@ -0,0 +1,33 @@
{
"observed_on": "2026-08-23",
"source": "production docker.cin.su read-only live checks",
"public": {
"healthz": {"status": 200, "runtime": "creator-web"}
},
"agent_worker": {
"runtime": "agent-worker",
"executor_configured": false,
"mode": "local-fallback",
"ready": false,
"local_fallback": true,
"reason": "external bridge unavailable; durable work is retained without external mutation"
},
"fleet": {
"preferred_tier": "FAST",
"selected_tier": "FAST",
"ready_tiers": ["FAST", "MEDIUM", "LIFEBOAT"],
"fallback_tier": "LIFEBOAT",
"heavy": {"ready": false, "reason_class": "provider timeout"}
},
"runtimes": {
"core": "ready",
"work_graph": "ready",
"research": "ready",
"reflection_worker": "running"
},
"ui": {
"public_admin_shell": "available",
"authenticated_browser_session": "not_available_for_current_acceptance"
},
"secret_policy": "values omitted; only typed states and reason classes retained"
}
@@ -0,0 +1,20 @@
{
"observed_on": "2026-08-23",
"source": "current authenticated in-app browser session against assistant.cin.su",
"production_revision_visible": "40b56798e23d",
"chat": {"authenticated": true, "session_state": "active", "message_surface": "visible"},
"admin": {
"authenticated": true,
"diagnostics": {"passed": 7, "total": 7},
"operations": {"fleet_ready": 4, "fleet_total": 4, "selected_tier": "FAST", "fallback_tier": "LIFEBOAT"},
"agent_worker": {"mode": "local-fallback", "executor_configured": false, "external_mutation": false},
"action_history_records_visible": 25,
"learning_signals_visible": 17,
"reflection_state_visible": "RESOLVED",
"skill_state_visible": "SUSPENDED"
},
"responsive": {"viewport": "390x844", "horizontal_overflow_observed": false, "sections_readable": true},
"mutations_performed": false,
"secrets_exposed": false,
"limitations": ["no delegation-specific mutation was performed", "no external bridge was configured"]
}
@@ -0,0 +1,17 @@
{
"observed_on": "2026-08-23",
"source": "production cognitive-worker read-only endpoint on docker.cin.su",
"endpoint": "/v1/fleet/readiness",
"preferred_tier": "FAST",
"selected_tier": "MEDIUM",
"ready_tiers": ["MEDIUM", "HEAVY", "LIFEBOAT"],
"fallback_tier": "LIFEBOAT",
"members": [
{"tier": "FAST", "ready": false, "reason_class": "provider connection refused"},
{"tier": "MEDIUM", "ready": true, "reason_class": "available"},
{"tier": "HEAVY", "ready": true, "reason_class": "available"},
{"tier": "LIFEBOAT", "ready": true, "reason_class": "available"}
],
"interpretation": "Selection follows current readiness and preserves LIFEBOAT fallback; GPU/HEAVY availability does not by itself override the preferred tier or prove superior quality.",
"secret_policy": "values omitted; only typed states and reason classes retained"
}
@@ -2,6 +2,8 @@ package domain
import (
"errors"
"fmt"
"strings"
"git.cin.su/m/Assistant/contracts/artifact"
"git.cin.su/m/Assistant/contracts/capability"
@@ -9,6 +11,57 @@ import (
var ErrInvalidTaskEvaluation = errors.New("invalid task evaluation")
type UnresolvedTaskEvidence struct {
ID string
OriginalPrompt string
FailureSummary string
EvidenceArtifactRef string
}
// BuildAdditionalTaskEvaluationCases creates two bounded probes from one
// unresolved outcome. It creates prompts only; quality still requires a
// verification spec and independent verifier evidence.
func BuildAdditionalTaskEvaluationCases(value UnresolvedTaskEvidence) ([]capability.TaskEvaluationCase, error) {
if strings.TrimSpace(value.ID) == "" || strings.TrimSpace(value.OriginalPrompt) == "" || strings.TrimSpace(value.FailureSummary) == "" || strings.TrimSpace(value.EvidenceArtifactRef) == "" {
return nil, ErrInvalidTaskEvaluation
}
base := strings.TrimSpace(value.OriginalPrompt)
failure := strings.TrimSpace(value.FailureSummary)
return []capability.TaskEvaluationCase{
{CaseID: value.ID + ":reproduce", Prompt: base, EvidenceArtifactRef: value.EvidenceArtifactRef},
{CaseID: value.ID + ":address-failure", Prompt: base + "\n\nAddress this observed unresolved condition without assuming it is true: " + failure, EvidenceArtifactRef: value.EvidenceArtifactRef},
}, nil
}
const automaticExpansionMinimumOverallScore int64 = 700
// AutomaticTaskEvaluationExpansion decides whether a completed general
// evaluation has evidence that warrants a bounded additional profile. It is
// deliberately conservative: one supported candidate above the quality
// threshold suppresses expansion, while unknown/unsupported/low-scoring or
// critically failing candidates keep the unresolved path explicit.
func AutomaticTaskEvaluationExpansion(parent capability.TaskEvaluationProfile, parentGraphState string, evaluations []capability.TaskEvaluation, request capability.TaskEvaluationExpansionRequest, profileID capability.TaskEvaluationProfileID, revision int64) (capability.TaskEvaluationProfile, bool, error) {
if parentGraphState != "COMPLETED" && parentGraphState != "PARTIAL" {
return capability.TaskEvaluationProfile{}, false, nil
}
if len(evaluations) == 0 {
return capability.TaskEvaluationProfile{}, false, nil
}
for _, evaluation := range evaluations {
if evaluation.Result == capability.EvaluationSupported && evaluation.OverallScoreMillis >= automaticExpansionMinimumOverallScore && evaluation.CriticalFailureMillis == 0 {
return capability.TaskEvaluationProfile{}, false, nil
}
}
if request.TriggerEvidenceRef == "" || len(request.NewCaseIDs) == 0 {
return capability.TaskEvaluationProfile{}, false, fmt.Errorf("unresolved task evaluation requires trigger evidence and additional cases")
}
profile, err := BuildAdditionalTaskEvaluationProfile(parent, request, profileID, revision)
if err != nil {
return capability.TaskEvaluationProfile{}, false, err
}
return profile, true, nil
}
func BuildAdditionalTaskEvaluationProfile(parent capability.TaskEvaluationProfile, request capability.TaskEvaluationExpansionRequest, profileID capability.TaskEvaluationProfileID, revision int64) (capability.TaskEvaluationProfile, error) {
if err := parent.Validate(); err != nil || parent.Stage != capability.TaskEvaluationGeneral || request.ParentProfileID != parent.ID || request.CapabilityID != parent.CapabilityID || request.TriggerEvidenceRef == "" || len(request.NewCaseIDs) == 0 || profileID == "" || revision < 1 || request.BenchmarkVersion == "" {
return capability.TaskEvaluationProfile{}, ErrInvalidTaskEvaluation
@@ -55,6 +108,12 @@ func BuildTaskEvaluation(profile capability.TaskEvaluationProfile, evaluationID
if _, ok := seen[observation.CaseID]; ok || observation.EvidenceArtifactRef == "" || observation.QualityScoreMillis < 0 || observation.QualityScoreMillis > 1000 || observation.LatencyMicros < 0 {
return capability.TaskEvaluation{}, ErrInvalidTaskEvaluation
}
if observation.Result == capability.EvaluationSupported && (observation.VerificationEvidenceRef == "" || observation.VerifierIdentity == "") {
return capability.TaskEvaluation{}, ErrInvalidTaskEvaluation
}
if observation.Result == capability.EvaluationUnknown && observation.QualityScoreMillis != 0 {
return capability.TaskEvaluation{}, ErrInvalidTaskEvaluation
}
seen[observation.CaseID] = struct{}{}
qualityTotal += observation.QualityScoreMillis
latencyTotal += observation.LatencyMicros
@@ -1,6 +1,7 @@
package domain
import (
"strings"
"testing"
"git.cin.su/m/Assistant/contracts/artifact"
@@ -10,14 +11,22 @@ import (
func TestBuildTaskEvaluationAggregatesOnlyProfileCases(t *testing.T) {
profile := capability.TaskEvaluationProfile{ID: "research-v1", Revision: 1, CapabilityID: "creator-advisory", Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "research-v1", RequiredCaseIDs: []string{"claim", "uncertainty"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100}
value, err := BuildTaskEvaluation(profile, "evaluation-1", "binding-1", 2, "creator-advisory", []capability.TaskCaseObservation{
{CaseID: "claim", Result: capability.EvaluationSupported, QualityScoreMillis: 900, LatencyMicros: 400_000, EvidenceArtifactRef: "artifact-case-1"},
{CaseID: "uncertainty", Result: capability.EvaluationSupported, QualityScoreMillis: 800, LatencyMicros: 600_000, EvidenceArtifactRef: "artifact-case-2"},
{CaseID: "claim", Result: capability.EvaluationSupported, QualityScoreMillis: 900, LatencyMicros: 400_000, EvidenceArtifactRef: "artifact-case-1", VerificationEvidenceRef: "verify-case-1", VerifierIdentity: "verifier-1"},
{CaseID: "uncertainty", Result: capability.EvaluationSupported, QualityScoreMillis: 800, LatencyMicros: 600_000, EvidenceArtifactRef: "artifact-case-2", VerificationEvidenceRef: "verify-case-2", VerifierIdentity: "verifier-1"},
}, artifact.ArtifactID("artifact-summary"), 10)
if err != nil || value.Result != capability.EvaluationSupported || value.OverallScoreMillis == 0 || value.ProfileID != profile.ID {
t.Fatalf("evaluation=%+v err=%v", value, err)
}
}
func TestBuildTaskEvaluationRejectsSupportedWithoutIndependentVerifier(t *testing.T) {
profile := capability.TaskEvaluationProfile{ID: "quality-v1", Revision: 1, CapabilityID: "creator-advisory", Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "quality-v1", RequiredCaseIDs: []string{"case"}, QualityWeightMillis: 700, ReliabilityWeightMillis: 200, LatencyWeightMillis: 100}
_, err := BuildTaskEvaluation(profile, "evaluation-unverified", "binding-1", 1, profile.CapabilityID, []capability.TaskCaseObservation{{CaseID: "case", Result: capability.EvaluationSupported, QualityScoreMillis: 900, EvidenceArtifactRef: "artifact-case"}}, artifact.ArtifactID("artifact-summary"), 1)
if err == nil {
t.Fatal("supported observation without verifier evidence was accepted")
}
}
func TestBuildTaskEvaluationDoesNotPromoteUnknown(t *testing.T) {
profile := capability.TaskEvaluationProfile{ID: "coding-v1", Revision: 1, CapabilityID: "creator-advisory", Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "coding-v1", RequiredCaseIDs: []string{"compile"}, QualityWeightMillis: 700, ReliabilityWeightMillis: 200, LatencyWeightMillis: 100}
value, err := BuildTaskEvaluation(profile, "evaluation-2", "binding-1", 2, "creator-advisory", []capability.TaskCaseObservation{{CaseID: "compile", Result: capability.EvaluationUnknown, QualityScoreMillis: 0, LatencyMicros: 0, EvidenceArtifactRef: "artifact-case"}}, artifact.ArtifactID("artifact-summary"), 11)
@@ -33,3 +42,28 @@ func TestBuildAdditionalProfileRequiresUnresolvedEvidenceAndPreservesHistory(t *
t.Fatalf("profile=%+v err=%v", profile, err)
}
}
func TestAutomaticExpansionWaitsForGeneralGraphAndUnresolvedCandidates(t *testing.T) {
parent := capability.TaskEvaluationProfile{ID: "general-v1", Revision: 1, CapabilityID: "creator-advisory", Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "general-v1", RequiredCaseIDs: []string{"general"}, QualityWeightMillis: 700, ReliabilityWeightMillis: 200, LatencyWeightMillis: 100}
request := capability.TaskEvaluationExpansionRequest{ParentProfileID: parent.ID, CapabilityID: parent.CapabilityID, TriggerEvidenceRef: "outcome-unsolved", NewCaseIDs: []string{"edge-case"}, BenchmarkVersion: "additional-v1"}
value, expanded, err := AutomaticTaskEvaluationExpansion(parent, "COMPLETED", []capability.TaskEvaluation{{ID: "evaluation-1", ProfileID: parent.ID, BindingID: "binding-1", BindingRevision: 1, CapabilityID: parent.CapabilityID, Result: capability.EvaluationUnknown, OverallScoreMillis: 0, CriticalFailureMillis: 1000, BenchmarkVersion: parent.BenchmarkVersion, ArtifactID: "artifact-1", ObservedAtUnixMillis: 1}}, request, "additional-v1", 1)
if err != nil || !expanded || value.Stage != capability.TaskEvaluationAdditional || value.ParentProfileID != parent.ID {
t.Fatalf("expected bounded expansion, profile=%+v expanded=%t err=%v", value, expanded, err)
}
}
func TestAutomaticExpansionDoesNotRunWhenAQualifiedCandidateExists(t *testing.T) {
parent := capability.TaskEvaluationProfile{ID: "general-v1", Revision: 1, CapabilityID: "creator-advisory", Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "general-v1", RequiredCaseIDs: []string{"general"}, QualityWeightMillis: 700, ReliabilityWeightMillis: 200, LatencyWeightMillis: 100}
request := capability.TaskEvaluationExpansionRequest{ParentProfileID: parent.ID, CapabilityID: parent.CapabilityID, TriggerEvidenceRef: "outcome-unsolved", NewCaseIDs: []string{"edge-case"}, BenchmarkVersion: "additional-v1"}
value, expanded, err := AutomaticTaskEvaluationExpansion(parent, "PARTIAL", []capability.TaskEvaluation{{ID: "evaluation-1", ProfileID: parent.ID, BindingID: "binding-1", BindingRevision: 1, CapabilityID: parent.CapabilityID, Result: capability.EvaluationSupported, OverallScoreMillis: 850, CriticalFailureMillis: 0, BenchmarkVersion: parent.BenchmarkVersion, ArtifactID: "artifact-1", ObservedAtUnixMillis: 1}}, request, "additional-v1", 1)
if err != nil || expanded || value.ID != "" {
t.Fatalf("qualified candidate must suppress expansion: profile=%+v expanded=%t err=%v", value, expanded, err)
}
}
func TestBuildAdditionalTaskEvaluationCasesFromUnresolvedEvidence(t *testing.T) {
cases, err := BuildAdditionalTaskEvaluationCases(UnresolvedTaskEvidence{ID: "outcome-1", OriginalPrompt: "compute answer", FailureSummary: "the answer was not verified", EvidenceArtifactRef: "artifact-outcome-1"})
if err != nil || len(cases) != 2 || cases[0].CaseID != "outcome-1:reproduce" || !strings.Contains(cases[1].Prompt, "not verified") {
t.Fatalf("unexpected generated cases: cases=%+v err=%v", cases, err)
}
}
+64 -3
View File
@@ -2,6 +2,8 @@ package application
import (
"context"
"fmt"
"sync"
"time"
"git.cin.su/m/Assistant/contracts/work"
@@ -33,16 +35,75 @@ func ExecuteAgentDispatch(ctx context.Context, store ports.ExecutionStore, execu
if err := store.AcceptAgentCallback(ctx, heartbeat); err != nil {
return work.AgentExecutionResult{}, err
}
result, err := executor.Execute(ctx, work.AgentExecutionRequest{Intent: intent, WorkerID: workerID, FencingToken: token, LeaseExpiresAt: lease.ExpiresAt})
executionCtx, cancel := context.WithCancel(ctx)
defer cancel()
var stateMu sync.Mutex
currentLease := lease
lastSequence := heartbeat.Sequence
heartbeatDone := make(chan struct{})
heartbeatStopped := make(chan struct{})
heartbeatFailure := make(chan error, 1)
interval := ttl / 3
if interval <= 0 {
return work.AgentExecutionResult{}, fmt.Errorf("invalid heartbeat interval")
}
go func() {
defer close(heartbeatStopped)
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-heartbeatDone:
return
case observedAt := <-ticker.C:
stateMu.Lock()
nextLease, renewErr := domain.RenewAgentLease(currentLease, workerID, token, observedAt, ttl)
if renewErr == nil {
renewErr = store.RenewAgentLease(executionCtx, nextLease, nextLease.ExpiresAt)
}
if renewErr == nil {
lastSequence++
heartbeat = work.AgentCallback{RunID: intent.RunID, WorkerID: workerID, FencingToken: token, Sequence: lastSequence, Kind: work.AgentCallbackHeartbeat, ObservedAt: observedAt}
renewErr = domain.ValidateAgentCallback(heartbeat, nextLease, lastSequence-1)
}
if renewErr == nil {
renewErr = store.AcceptAgentCallback(executionCtx, heartbeat)
}
if renewErr == nil {
currentLease = nextLease
}
stateMu.Unlock()
if renewErr != nil {
select {
case heartbeatFailure <- renewErr:
default:
}
cancel()
return
}
}
}
}()
result, err := executor.Execute(executionCtx, work.AgentExecutionRequest{Intent: intent, WorkerID: workerID, FencingToken: token, LeaseExpiresAt: lease.ExpiresAt})
close(heartbeatDone)
<-heartbeatStopped
select {
case heartbeatErr := <-heartbeatFailure:
return work.AgentExecutionResult{}, heartbeatErr
default:
}
if err != nil {
return work.AgentExecutionResult{}, err
}
stateMu.Lock()
lease = currentLease
stateMu.Unlock()
observedAt := time.UnixMilli(result.ObservedAtUnixMillis)
callback := work.AgentCallback{RunID: intent.RunID, WorkerID: workerID, FencingToken: token, Sequence: 2, Kind: work.AgentCallbackResult, ObservedAt: observedAt}
callback := work.AgentCallback{RunID: intent.RunID, WorkerID: workerID, FencingToken: token, Sequence: lastSequence + 1, Kind: work.AgentCallbackResult, ObservedAt: observedAt}
if result.State == work.AgentExecutionWaiting {
callback.Kind = work.AgentCallbackCheckpoint
}
if err := domain.ValidateAgentCallback(callback, lease, heartbeat.Sequence); err != nil {
if err := domain.ValidateAgentCallback(callback, lease, lastSequence); err != nil {
return work.AgentExecutionResult{}, err
}
if err := store.AcceptAgentCallback(ctx, callback); err != nil {
@@ -18,6 +18,10 @@ func (s *bridgeStore) SaveAgentLease(_ context.Context, lease work.AgentLease) e
s.lease = lease
return nil
}
func (s *bridgeStore) RenewAgentLease(_ context.Context, lease work.AgentLease, _ time.Time) error {
s.lease = lease
return nil
}
func (s *bridgeStore) AcceptAgentCallback(_ context.Context, callback work.AgentCallback) error {
s.callbacks = append(s.callbacks, callback)
return nil
@@ -29,6 +33,17 @@ func (bridgeExecutor) Execute(context.Context, work.AgentExecutionRequest) (work
return work.AgentExecutionResult{State: work.AgentExecutionCompleted, Summary: "done", ObservedAtUnixMillis: 2}, nil
}
type slowBridgeExecutor struct{}
func (slowBridgeExecutor) Execute(ctx context.Context, request work.AgentExecutionRequest) (work.AgentExecutionResult, error) {
select {
case <-time.After(45 * time.Millisecond):
return work.AgentExecutionResult{State: work.AgentExecutionCompleted, Summary: "done", ObservedAtUnixMillis: time.Now().UnixMilli()}, nil
case <-ctx.Done():
return work.AgentExecutionResult{}, ctx.Err()
}
}
func TestExecuteAgentDispatchPersistsLeaseAndBridgeCallbacks(t *testing.T) {
store := &bridgeStore{}
now := time.UnixMilli(1)
@@ -40,3 +55,20 @@ func TestExecuteAgentDispatchPersistsLeaseAndBridgeCallbacks(t *testing.T) {
t.Fatalf("callbacks=%+v", store.callbacks)
}
}
func TestExecuteAgentDispatchRenewsLeaseAndOrdersHeartbeats(t *testing.T) {
store := &bridgeStore{}
now := time.Now().UTC()
_, err := ExecuteAgentDispatch(context.Background(), store, slowBridgeExecutor{}, work.AgentDispatchIntent{DispatchID: "d-slow", IdempotencyKey: "i-slow", DelegationID: "del-slow", RunID: "run-slow", ImplementationID: "impl", ImplementationRevision: 1, SourceRevision: "source", WorktreeID: "tree", VerificationPlanRef: "verify", CreatedAtUnixMillis: now.UnixMilli()}, "worker", 1, now, 30*time.Millisecond)
if err != nil {
t.Fatal(err)
}
if len(store.callbacks) < 3 {
t.Fatalf("expected heartbeat callbacks, got %d", len(store.callbacks))
}
for i, callback := range store.callbacks {
if callback.Sequence != uint64(i+1) {
t.Fatalf("callback sequence is not ordered: %#v", store.callbacks)
}
}
}
+10
View File
@@ -5,6 +5,9 @@ import (
"errors"
"time"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/work"
"git.cin.su/m/Assistant/modules/work/domain"
"git.cin.su/m/Assistant/modules/work/ports"
@@ -157,6 +160,13 @@ func AdmitAndScheduleTaskEvaluation(ctx context.Context, store ports.GraphStore,
intents[i].TaskEvaluationProfileID = node.TaskProfileID
intents[i].TaskEvaluationBindingID = node.BindingID
intents[i].TaskEvaluationCaseIDs = append([]string(nil), node.CaseIDs...)
if node.TaskProfile != nil {
profile := *node.TaskProfile
profile.RequiredCaseIDs = append([]string(nil), node.TaskProfile.RequiredCaseIDs...)
intents[i].TaskEvaluationProfile = &profile
}
intents[i].TaskEvaluationCases = append([]capability.TaskEvaluationCase(nil), node.TaskCases...)
intents[i].TaskEvaluationExecution = &work.TaskEvaluationDispatchContext{GraphID: graph.ID, GraphRevision: graph.Revision, NodeID: node.ID, BindingRevision: request.Plan.BindingRevision, ActorID: identity.ActorID(request.Plan.ActorID), PrincipalID: identity.PrincipalID(request.Plan.PrincipalID), ScopeID: authority.ScopeID(request.Plan.ScopeID)}
break
}
}
@@ -33,6 +33,7 @@ func TestAdmitAndScheduleTaskEvaluationCarriesSpecialistContext(t *testing.T) {
request := TaskEvaluationScheduleRequest{
Plan: work.TaskEvaluationGraphRequest{
GraphID: "graph-task", Profile: capability.TaskEvaluationProfile{ID: "profile-general", Revision: 1, CapabilityID: "capability-chat", Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "general-v1", RequiredCaseIDs: []string{"case-a", "case-b"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100},
Cases: []capability.TaskEvaluationCase{{CaseID: "case-a", Prompt: "one", EvidenceArtifactRef: "artifact-a"}, {CaseID: "case-b", Prompt: "two", EvidenceArtifactRef: "artifact-b"}},
BindingIDs: []capability.CapabilityBindingID{"binding-a"}, DispatchIDs: []string{"dispatch-a"}, IdempotencyPrefix: "eval", Budget: work.GraphBudget{MaxParallel: 1, MaxAttempts: 2},
},
Intents: []work.AgentDispatchIntent{{DispatchID: "dispatch-a", IdempotencyKey: "idem-a", DelegationID: "delegation-a", RunID: "run-a", ImplementationID: "impl", ImplementationRevision: 1, SourceRevision: "source", WorktreeID: "tree", VerificationPlanRef: "verify", CreatedAtUnixMillis: 1}},
+13 -4
View File
@@ -15,6 +15,12 @@ type WorkerCycleResult struct {
Idle bool
}
const transientDispatchRetryDelay = time.Second
func retryClaimedDispatch(ctx context.Context, store ports.WorkerStore, dispatchID, workerID string, now time.Time) error {
return store.RetryDispatch(ctx, dispatchID, workerID, now.UnixMilli(), transientDispatchRetryDelay)
}
// RecoverOnlyWorkerCycle keeps durable recovery alive while the real executor
// capability is absent. It intentionally does not claim a dispatch: a missing
// executor must never be represented as successful work.
@@ -37,11 +43,11 @@ func ExecuteWorkerCycle(ctx context.Context, store ports.WorkerStore, workerID s
}
intent, err := store.LoadDispatchIntent(ctx, item.DispatchID)
if err != nil {
_ = store.CompleteDispatch(ctx, item.DispatchID, workerID, work.AgentDispatchFailed, now.UnixMilli())
_ = retryClaimedDispatch(ctx, store, item.DispatchID, workerID, now)
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID}, err
}
if _, err := ExecuteDryRunDispatch(ctx, store, intent, workerID, token, now, leaseTTL); err != nil {
_ = store.CompleteDispatch(ctx, item.DispatchID, workerID, work.AgentDispatchFailed, now.UnixMilli())
_ = retryClaimedDispatch(ctx, store, item.DispatchID, workerID, now)
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID}, err
}
if err := store.CompleteDispatch(ctx, item.DispatchID, workerID, work.AgentDispatchCompleted, now.UnixMilli()); err != nil {
@@ -64,15 +70,18 @@ func ExecuteWorkerCycleWithExecutor(ctx context.Context, store ports.WorkerStore
}
intent, err := store.LoadDispatchIntent(ctx, item.DispatchID)
if err != nil {
_ = store.CompleteDispatch(ctx, item.DispatchID, workerID, work.AgentDispatchFailed, now.UnixMilli())
_ = retryClaimedDispatch(ctx, store, item.DispatchID, workerID, now)
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID}, err
}
result, err := ExecuteAgentDispatch(ctx, store, executor, intent, workerID, token, now, leaseTTL)
if err != nil {
_ = store.CompleteDispatch(ctx, item.DispatchID, workerID, work.AgentDispatchFailed, now.UnixMilli())
_ = retryClaimedDispatch(ctx, store, item.DispatchID, workerID, now)
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID}, err
}
if result.State != work.AgentExecutionCompleted {
if err := retryClaimedDispatch(ctx, store, item.DispatchID, workerID, now); err != nil {
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID}, err
}
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID}, nil
}
if err := store.CompleteDispatch(ctx, item.DispatchID, workerID, work.AgentDispatchCompleted, now.UnixMilli()); err != nil {
+6
View File
@@ -26,10 +26,16 @@ func (s *recoveryOnlyStore) LoadDispatchIntent(context.Context, string) (work.Ag
func (s *recoveryOnlyStore) CompleteDispatch(context.Context, string, string, work.AgentDispatchQueueState, int64) error {
return nil
}
func (s *recoveryOnlyStore) RetryDispatch(context.Context, string, string, int64, time.Duration) error {
return nil
}
func (s *recoveryOnlyStore) SaveDispatchIntent(context.Context, work.AgentDispatchIntent) error {
return nil
}
func (s *recoveryOnlyStore) SaveAgentLease(context.Context, work.AgentLease) error { return nil }
func (s *recoveryOnlyStore) RenewAgentLease(context.Context, work.AgentLease, time.Time) error {
return nil
}
func (s *recoveryOnlyStore) AcceptAgentCallback(context.Context, work.AgentCallback) error {
return nil
}
@@ -20,6 +20,24 @@ func BuildTaskEvaluationGraph(request work.TaskEvaluationGraphRequest) (work.Wor
if err := request.Profile.Validate(); err != nil {
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
}
if len(request.Cases) != len(request.Profile.RequiredCaseIDs) {
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
}
seenCases := make(map[string]struct{}, len(request.Cases))
for _, item := range request.Cases {
if item.CaseID == "" || item.Prompt == "" || item.EvidenceArtifactRef == "" {
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
}
if _, exists := seenCases[item.CaseID]; exists {
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
}
seenCases[item.CaseID] = struct{}{}
}
for _, requiredID := range request.Profile.RequiredCaseIDs {
if _, exists := seenCases[requiredID]; !exists {
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
}
}
if request.Profile.Stage == capability.TaskEvaluationGeneral {
if request.ParentGraphID != "" || request.ParentGraphRevision != 0 || request.ParentGraphState != "" || request.TriggerEvidenceRef != "" {
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
@@ -56,7 +74,15 @@ func BuildTaskEvaluationGraph(request work.TaskEvaluationGraphRequest) (work.Wor
TaskProfileID: request.Profile.ID,
BindingID: bindingID,
CaseIDs: append([]string(nil), request.Profile.RequiredCaseIDs...),
TaskProfile: cloneTaskEvaluationProfile(request.Profile),
TaskCases: append([]capability.TaskEvaluationCase(nil), request.Cases...),
})
}
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}, nil
}
func cloneTaskEvaluationProfile(value capability.TaskEvaluationProfile) *capability.TaskEvaluationProfile {
copyValue := value
copyValue.RequiredCaseIDs = append([]string(nil), value.RequiredCaseIDs...)
return &copyValue
}
@@ -9,7 +9,7 @@ import (
func evaluationGraphRequest(stage capability.TaskEvaluationProfileStage) work.TaskEvaluationGraphRequest {
profile := capability.TaskEvaluationProfile{ID: "profile-1", Revision: 1, CapabilityID: "capability-1", Stage: stage, BenchmarkVersion: "general-v1", RequiredCaseIDs: []string{"case-a", "case-b"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100}
request := work.TaskEvaluationGraphRequest{GraphID: "graph-1", Profile: profile, BindingIDs: []capability.CapabilityBindingID{"binding-a", "binding-b"}, DispatchIDs: []string{"dispatch-a", "dispatch-b"}, IdempotencyPrefix: "task-eval", Budget: work.GraphBudget{MaxParallel: 2, MaxAttempts: 2}}
request := work.TaskEvaluationGraphRequest{GraphID: "graph-1", Profile: profile, BindingIDs: []capability.CapabilityBindingID{"binding-a", "binding-b"}, DispatchIDs: []string{"dispatch-a", "dispatch-b"}, IdempotencyPrefix: "task-eval", Budget: work.GraphBudget{MaxParallel: 2, MaxAttempts: 2}, Cases: []capability.TaskEvaluationCase{{CaseID: "case-a", Prompt: "short answer", EvidenceArtifactRef: "artifact-a"}, {CaseID: "case-b", Prompt: "long answer", EvidenceArtifactRef: "artifact-b"}}}
if stage == capability.TaskEvaluationAdditional {
request.Profile.ParentProfileID = "profile-general"
request.Profile.TriggerEvidenceRef = "artifact:unresolved-1"
+2
View File
@@ -2,6 +2,7 @@ package ports
import (
"context"
"time"
"git.cin.su/m/Assistant/contracts/work"
)
@@ -13,5 +14,6 @@ type AgentExecutor interface {
type ExecutionStore interface {
SaveDispatchIntent(context.Context, work.AgentDispatchIntent) error
SaveAgentLease(context.Context, work.AgentLease) error
RenewAgentLease(context.Context, work.AgentLease, time.Time) error
AcceptAgentCallback(context.Context, work.AgentCallback) error
}
+2
View File
@@ -3,6 +3,7 @@ package ports
import (
"context"
"errors"
"time"
"git.cin.su/m/Assistant/contracts/work"
)
@@ -14,5 +15,6 @@ type WorkerStore interface {
LoadDispatchIntent(context.Context, string) (work.AgentDispatchIntent, error)
ClaimNextDispatch(context.Context, string, int64) (work.AgentDispatchQueueItem, error)
CompleteDispatch(context.Context, string, string, work.AgentDispatchQueueState, int64) error
RetryDispatch(context.Context, string, string, int64, time.Duration) error
RecoverStaleClaims(context.Context, int64, int64) (int64, error)
}
+2 -1
View File
@@ -15,7 +15,8 @@ func (s *canaryStore) SaveDispatchIntent(context.Context, work.AgentDispatchInte
s.writes++
return nil
}
func (s *canaryStore) SaveAgentLease(context.Context, work.AgentLease) error { s.writes++; return nil }
func (s *canaryStore) SaveAgentLease(context.Context, work.AgentLease) error { s.writes++; return nil }
func (s *canaryStore) RenewAgentLease(context.Context, work.AgentLease, time.Time) error { return nil }
func (s *canaryStore) AcceptAgentCallback(context.Context, work.AgentCallback) error {
s.writes++
return nil
@@ -21,6 +21,9 @@ func (s *recordingExecutionStore) SaveAgentLease(context.Context, work.AgentLeas
s.operations = append(s.operations, "lease")
return nil
}
func (s *recordingExecutionStore) RenewAgentLease(context.Context, work.AgentLease, time.Time) error {
return nil
}
func (s *recordingExecutionStore) AcceptAgentCallback(context.Context, work.AgentCallback) error {
s.operations = append(s.operations, "callback")
return nil
@@ -0,0 +1,20 @@
package agent_execution_test
import (
"os"
"strings"
"testing"
)
func TestRecoveryMigrationDefinesBoundedRetryState(t *testing.T) {
payload, err := os.ReadFile("../../database/migrations/042_agent_execution_recovery.sql")
if err != nil {
t.Fatal(err)
}
source := strings.ToLower(string(payload))
for _, fragment := range []string{"set role work_role", "retry_limit integer not null default 3", "retry_limit > 0"} {
if !strings.Contains(source, fragment) {
t.Fatalf("migration is missing %q", fragment)
}
}
}
@@ -0,0 +1,20 @@
package semantic_evolution_test
import (
"os"
"strings"
"testing"
)
func TestGovernedDevelopmentCandidateMigrationIsWorkOwnedAndSandboxed(t *testing.T) {
payload, err := os.ReadFile("../../database/migrations/043_governed_development_candidates.sql")
if err != nil {
t.Fatal(err)
}
source := strings.ToLower(string(payload))
for _, fragment := range []string{"set role work_role", "create table if not exists work.extension_proposals", "create table if not exists work.development_scaffolds", "stage = 'sandboxed'", "protected_main_access = 'denied'", "production_access = 'denied'", "secret_access = 'denied'"} {
if !strings.Contains(source, fragment) {
t.Fatalf("migration is missing %q", fragment)
}
}
}