feat(work): orchestrate dry-run dispatch persistence

This commit is contained in:
m
2026-08-22 12:13:35 +03:00
parent fa62322ed2
commit 7f82240a8f
5 changed files with 107 additions and 2 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000122-agent-dry-run-dispatch.json",
"task_id": "DEV-000122",
"task_file": "dev/tasks/DEV-000123-agent-dry-run-orchestration.json",
"task_id": "DEV-000123",
"task_revision": 1
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000123",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "fa62322",
"risk_tier": "E",
"intent": "Connect the opt-in deterministic dry-run dispatcher to a narrow Work-owned execution persistence port and application coordinator.",
"requirement_ids": ["WP-301", "Work execution port", "Dry-run orchestration"],
"allowed_change_scope": {"modules": ["work-agent-execution"], "paths": ["modules/work/ports/execution.go", "modules/work/application/dry_run_dispatch.go", "tests/agent_execution/dry_run_orchestration_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000123-agent-dry-run-orchestration.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "cmd/**", "runtime/**", "adapters/**", "database/**", ".gitea/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["DEV-000121 persistence methods", "DEV-000122 dry-run dispatch"], "may_change": ["Work execution port and application coordinator"]},
"semantic_owner": "work-agent-execution",
"canonical_state": ["Persistence remains owned by Work repository"],
"state_transitions": ["dispatch intent -> lease -> ordered callbacks"],
"side_effects_and_privileges": ["Explicit Work-owned port only; no runtime entrypoint or production enablement"],
"persistence_and_migrations": ["Reuses DEV-000121 schema; no migration"],
"failure_retry_restart": ["Idempotent repository methods make retries safe; partial failure is returned to caller"],
"assumptions": ["Production dispatcher remains disabled until a separate runtime canary task"],
"acceptance_scenarios": ["All persistence operations are invoked in deterministic order", "Invalid dispatch never reaches the store"],
"required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl/independent", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,29 @@
package application
import (
"context"
"time"
"git.cin.su/m/Assistant/contracts/work"
"git.cin.su/m/Assistant/modules/work/domain"
"git.cin.su/m/Assistant/modules/work/ports"
)
func ExecuteDryRunDispatch(ctx context.Context, store ports.ExecutionStore, intent work.AgentDispatchIntent, workerID string, token work.AgentFencingToken, now time.Time, ttl time.Duration) (work.DryRunDispatch, error) {
dispatch, err := domain.BuildDryRunDispatch(intent, workerID, token, now, ttl)
if err != nil {
return work.DryRunDispatch{}, err
}
if err := store.SaveDispatchIntent(ctx, dispatch.Intent); err != nil {
return work.DryRunDispatch{}, err
}
if err := store.SaveAgentLease(ctx, dispatch.Lease); err != nil {
return work.DryRunDispatch{}, err
}
for _, callback := range dispatch.Callbacks {
if err := store.AcceptAgentCallback(ctx, callback); err != nil {
return work.DryRunDispatch{}, err
}
}
return dispatch, nil
}
+13
View File
@@ -0,0 +1,13 @@
package ports
import (
"context"
"git.cin.su/m/Assistant/contracts/work"
)
type ExecutionStore interface {
SaveDispatchIntent(context.Context, work.AgentDispatchIntent) error
SaveAgentLease(context.Context, work.AgentLease) error
AcceptAgentCallback(context.Context, work.AgentCallback) error
}
@@ -0,0 +1,40 @@
package agent_execution_test
import (
"context"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/work"
"git.cin.su/m/Assistant/modules/work/application"
)
type recordingExecutionStore struct {
operations []string
}
func (s *recordingExecutionStore) SaveDispatchIntent(context.Context, work.AgentDispatchIntent) error {
s.operations = append(s.operations, "intent")
return nil
}
func (s *recordingExecutionStore) SaveAgentLease(context.Context, work.AgentLease) error {
s.operations = append(s.operations, "lease")
return nil
}
func (s *recordingExecutionStore) AcceptAgentCallback(context.Context, work.AgentCallback) error {
s.operations = append(s.operations, "callback")
return nil
}
func TestExecuteDryRunDispatchPersistsInOrder(t *testing.T) {
now := time.UnixMilli(1_700_000_000_000)
intent := work.AgentDispatchIntent{DispatchID: "dispatch-1", IdempotencyKey: "idem-1", DelegationID: "delegation-1", RunID: "run-1", ImplementationID: "fake-worker", ImplementationRevision: 1, SourceRevision: "source-1", WorktreeID: "worktree-1", VerificationPlanRef: "verify-1", CreatedAtUnixMillis: now.UnixMilli()}
store := &recordingExecutionStore{}
result, err := application.ExecuteDryRunDispatch(context.Background(), store, intent, "worker-1", 7, now, 15*time.Minute)
if err != nil {
t.Fatal(err)
}
if result.CallbackCount() != 2 || len(store.operations) != 4 || store.operations[0] != "intent" || store.operations[1] != "lease" || store.operations[2] != "callback" || store.operations[3] != "callback" {
t.Fatalf("unexpected persistence sequence: %#v", store.operations)
}
}