DEV-000261 durable additional task evaluation scheduling #239

Merged
m merged 3 commits from codex/dev-000248-self-development-runtime-status into main 2026-08-23 20:42:20 +00:00
4 changed files with 90 additions and 2 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000260-deep-authenticated-acceptance.json",
"task_id": "DEV-000260",
"task_file": "dev/tasks/DEV-000261-durable-additional-task-evaluation-scheduling.json",
"task_id": "DEV-000261",
"task_revision": 1
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000261",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "d2dd38b",
"risk_tier": "E",
"intent": "Connect unresolved model-evaluation evidence to a bounded additional Work Graph and durable dispatch schedule.",
"requirement_ids": ["model fleet/failover", "general-first qualification", "adaptive additional tests", "durable parallel dispatch"],
"allowed_change_scope": {"modules": ["work", "capability-evaluation"], "paths": ["modules/work/application/graph_runtime.go", "modules/work/application/task_evaluation_dispatch_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000261-durable-additional-task-evaluation-scheduling.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "deploy/**", "contracts/**", "LLM-local/**", "cmd/**", "adapters/**"]},
"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": ["TaskEvaluationExpansionRequest", "UnresolvedTaskEvidence", "Work Graph admission and dispatch ports"], "may_change": ["application orchestration function only"]},
"semantic_owner": "Work application schedules; Capability domain decides qualification expansion",
"canonical_state": ["work graph and dispatch queue remain durable owners"],
"state_transitions": ["completed unresolved general evaluation -> bounded additional graph -> leased/queued specialist dispatch"],
"side_effects_and_privileges": ["durable graph/dispatch persistence through existing ports; no provider or external mutation"],
"persistence_and_migrations": ["none; existing graph and dispatch persistence is reused"],
"failure_retry_restart": ["existing graph admission and worker lease/recovery semantics remain authoritative; qualified candidate suppresses expansion"],
"assumptions": ["caller supplies fresh dispatch identities and bounded additional benchmark version", "independent verifier remains required before qualification promotion"],
"acceptance_scenarios": ["unresolved general result creates exactly two bounded additional cases", "additional graph preserves parent evidence lineage", "fresh dispatch intent is queued through existing scheduler", "qualified candidate creates no additional graph"],
"required_tests": ["go test ./modules/work/application ./modules/work/domain ./modules/capability/domain", "go vet ./modules/work/application ./modules/work/domain ./modules/capability/domain", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "independent evidence review", "git diff --check", "CI", "production health smoke"],
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_SPEC_GAP"]
}
+33
View File
@@ -29,6 +29,19 @@ type TaskEvaluationScheduleRequest struct {
Now int64 `json:"now_unix_millis"`
}
// TaskEvaluationExpansionScheduleRequest carries a typed expansion decision
// from Capability to Work. Capability owns the decision and test-case
// construction; Work owns durable graph lineage, persistence and dispatch.
type TaskEvaluationExpansionScheduleRequest struct {
Profile capability.TaskEvaluationProfile
Cases []capability.TaskEvaluationCase
ParentGraphState work.GraphState
TriggerEvidenceRef string
Plan work.TaskEvaluationGraphRequest
Intents []work.AgentDispatchIntent
Now int64
}
type GraphCompleteRequest struct {
GraphID string `json:"graph_id"`
Revision int64 `json:"revision"`
@@ -229,6 +242,26 @@ func AdmitAndScheduleTaskEvaluation(ctx context.Context, store ports.GraphStore,
return AdmitAndSchedule(ctx, store, GraphScheduleRequest{Graph: graph, Intents: intents, Now: request.Now})
}
// ScheduleAdditionalTaskEvaluation is the runtime hand-off for the
// "general-first, additional-when-unresolved" policy. Capability decides
// whether to expand and supplies the bounded cases; this function never
// invents cases or chooses a model, and only persists/enqueues the new graph.
func ScheduleAdditionalTaskEvaluation(ctx context.Context, store ports.GraphStore, request TaskEvaluationExpansionScheduleRequest) (work.WorkGraph, []work.GraphNodeID, error) {
if request.Profile.Stage != capability.TaskEvaluationAdditional || request.Profile.ParentProfileID == "" || request.TriggerEvidenceRef == "" || len(request.Cases) == 0 {
return work.WorkGraph{}, nil, errors.New("additional task evaluation lineage is incomplete")
}
plan := request.Plan
plan.Profile = request.Profile
plan.Cases = append([]capability.TaskEvaluationCase(nil), request.Cases...)
plan.ParentGraphState = request.ParentGraphState
plan.TriggerEvidenceRef = request.TriggerEvidenceRef
graph, ready, err := AdmitAndScheduleTaskEvaluation(ctx, store, TaskEvaluationScheduleRequest{Plan: plan, Intents: request.Intents, Now: request.Now})
if err != nil {
return work.WorkGraph{}, nil, err
}
return graph, ready, nil
}
func CompleteGraphNode(ctx context.Context, store ports.GraphStore, graphID string, revision int64, nodeID work.GraphNodeID, success bool) (work.WorkGraph, error) {
graph, err := store.LoadWorkGraph(ctx, graphID, revision)
if err != nil {
@@ -45,3 +45,35 @@ func TestAdmitAndScheduleTaskEvaluationCarriesSpecialistContext(t *testing.T) {
t.Fatalf("specialist context was not carried into dispatch: %+v", store.intents)
}
}
func TestExpandAndScheduleTaskEvaluationCreatesAdditionalDurableGraph(t *testing.T) {
store := &taskGraphStore{}
additional := capability.TaskEvaluationProfile{ID: "profile-additional", Revision: 1, CapabilityID: "capability-chat", Stage: capability.TaskEvaluationAdditional, ParentProfileID: "profile-general", TriggerEvidenceRef: "artifact-unresolved", BenchmarkVersion: "additional-v1", RequiredCaseIDs: []string{"case-a:reproduce", "case-a:address-failure"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100}
request := TaskEvaluationExpansionScheduleRequest{
Profile: additional, Cases: []capability.TaskEvaluationCase{{CaseID: "case-a:reproduce", Prompt: "проверь доступность", EvidenceArtifactRef: "artifact-unresolved"}, {CaseID: "case-a:address-failure", Prompt: "проверь доступность; обработай неизвестный результат", EvidenceArtifactRef: "artifact-unresolved"}},
ParentGraphState: work.GraphCompleted, TriggerEvidenceRef: "artifact-unresolved",
Plan: work.TaskEvaluationGraphRequest{
GraphID: "graph-additional", BindingIDs: []capability.CapabilityBindingID{"binding-a"}, DispatchIDs: []string{"dispatch-additional"}, IdempotencyPrefix: "additional", Budget: work.GraphBudget{MaxParallel: 1, MaxAttempts: 2}, ParentGraphID: "graph-general", ParentGraphRevision: 2,
},
Intents: []work.AgentDispatchIntent{{DispatchID: "dispatch-additional", IdempotencyKey: "idem-additional", DelegationID: "delegation-a", RunID: "run-a", ImplementationID: "impl", ImplementationRevision: 1, SourceRevision: "source", WorktreeID: "tree", VerificationPlanRef: "verify", CreatedAtUnixMillis: 1}},
Now: 20,
}
graph, ready, err := ScheduleAdditionalTaskEvaluation(context.Background(), store, request)
if err != nil || len(ready) != 1 || graph.State != work.GraphRunning {
t.Fatalf("graph=%+v ready=%v err=%v", graph, ready, err)
}
if len(graph.Nodes) != 1 || graph.Nodes[0].TaskProfileID != "profile-additional" || len(graph.Nodes[0].CaseIDs) != 2 || graph.TriggerEvidenceRef != "artifact-unresolved" {
t.Fatalf("additional graph did not preserve bounded expansion lineage: %+v", graph)
}
if len(store.intents) != 1 || store.intents[0].TaskEvaluationProfileID != "profile-additional" {
t.Fatalf("additional dispatch context missing: %+v", store.intents)
}
}
func TestScheduleAdditionalTaskEvaluationRejectsIncompleteLineage(t *testing.T) {
store := &taskGraphStore{}
_, _, err := ScheduleAdditionalTaskEvaluation(context.Background(), store, TaskEvaluationExpansionScheduleRequest{Profile: capability.TaskEvaluationProfile{Stage: capability.TaskEvaluationGeneral}})
if err == nil || len(store.intents) != 0 {
t.Fatalf("incomplete expansion lineage was accepted: intents=%v err=%v", store.intents, err)
}
}