Compare commits

..
Author SHA1 Message Date
m 79c99289e7 feat(epistemics): evaluate read-only research evidence
CI / test (push) Successful in 24s
CI / test (pull_request) Successful in 21s
2026-08-22 14:23:17 +03:00
m 07a8de9cb5 Merge pull request 'test(creator-web): support file-backed acceptance secrets' (#101) from codex/wp-400-graph-harness into main
CI / test (push) Successful in 22s
2026-08-22 11:13:55 +00:00
m fdf38196f0 Merge remote-tracking branch 'origin/main' into codex/wp-400-graph-harness
CI / test (push) Successful in 22s
CI / test (pull_request) Successful in 21s
2026-08-22 14:12:29 +03:00
m b753761e1a chore(governance): include file-backed acceptance in worker task
CI / test (push) Successful in 22s
CI / test (pull_request) Successful in 22s
2026-08-22 14:11:10 +03:00
m 5faa5321dd test(creator-web): support file-backed acceptance secrets
CI / test (push) Failing after 16s
CI / test (pull_request) Failing after 16s
2026-08-22 14:09:46 +03:00
m 7271d7950d Merge pull request 'feat(runtime): production workers and cabinet acceptance' (#100) from codex/wp-400-graph-harness into main
CI / test (push) Successful in 28s
2026-08-22 11:03:21 +00:00
m 533243da25 Merge remote-tracking branch 'origin/main' into codex/wp-400-graph-harness
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 23s
2026-08-22 14:01:48 +03:00
m b1fba3ccef chore(dev): advance active worker task
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 21s
2026-08-22 13:58:39 +03:00
m b0afa2417f feat(runtime): run durable workers continuously
CI / test (push) Failing after 15s
CI / test (pull_request) Failing after 15s
2026-08-22 13:57:08 +03:00
m 6f5f7e1ee3 test(creator-web): cover cabinet acceptance contract
CI / test (push) Failing after 15s
2026-08-22 13:49:03 +03:00
m 94c5bcb017 Merge pull request 'feat(work): add deterministic graph harness' (#99) from codex/wp-400-graph-harness into main
CI / test (push) Failing after 15s
Reviewed-on: #99
2026-08-22 10:47:42 +00:00
m 3f7c4705fe Merge pull request 'feat(work): add durable agent dispatch queue' (#98) from codex/wp-301-agent-worker into main
CI / test (push) Successful in 22s
Reviewed-on: #98
2026-08-22 10:47:26 +00:00
m 1ccd43c552 feat(work): add governed self-development gate
CI / test (push) Failing after 15s
CI / test (pull_request) Failing after 14s
2026-08-22 13:46:01 +03:00
m d38406fbc5 feat(capability): add fleet readiness policy
CI / test (push) Failing after 15s
2026-08-22 13:43:36 +03:00
m f73c69960c feat(epistemics): add durable reflection worker
CI / test (push) Failing after 16s
2026-08-22 13:41:44 +03:00
m 32e33e3050 feat(epistemics): compile truth evidence bundle
CI / test (push) Successful in 24s
2026-08-22 13:37:22 +03:00
m e9bb42cd07 feat(work): persist graph revisions
CI / test (push) Successful in 21s
2026-08-22 13:33:54 +03:00
m 4a800df1e1 feat(work): add graph admission scheduler
CI / test (push) Successful in 22s
2026-08-22 13:31:39 +03:00
m b2f922cbbc feat(work): add deterministic graph harness
CI / test (push) Successful in 22s
2026-08-22 13:29:43 +03:00
m a4450348a1 feat(work): add one-shot agent worker recovery
CI / test (push) Successful in 21s
CI / test (pull_request) Successful in 21s
2026-08-22 13:26:52 +03:00
47 changed files with 1791 additions and 35 deletions
+58 -2
View File
@@ -3,6 +3,7 @@ package postgres
import (
"context"
"fmt"
"time"
"git.cin.su/m/Assistant/contracts/epistemics"
"github.com/jackc/pgx/v5/pgxpool"
@@ -64,7 +65,7 @@ func (r *EpistemicsRepository) RecentReflectionCandidates(ctx context.Context, l
}
rows, err := r.pool.Query(ctx, `SELECT
reflection_candidate_id,trigger,state,source_experience_id,original_intent_id,source_utterance_id,
summary,evidence_refs,requires_cognition,recorded_at_unix_millis,revision
summary,evidence_refs,requires_cognition,recorded_at_unix_millis,revision,claimed_by,claim_expires_at_unix_millis
FROM epistemics.reflection_candidates ORDER BY recorded_at_unix_millis DESC, reflection_candidate_id DESC LIMIT $1`, limit)
if err != nil {
return nil, err
@@ -74,7 +75,7 @@ func (r *EpistemicsRepository) RecentReflectionCandidates(ctx context.Context, l
for rows.Next() {
var value epistemics.ReflectionCandidate
if err := rows.Scan(&value.ID, &value.Trigger, &value.State, &value.SourceExperienceID, &value.OriginalIntentID, &value.SourceUtteranceID,
&value.Summary, &value.EvidenceRefs, &value.RequiresCognition, &value.RecordedAtUnixMillis, &value.Revision); err != nil {
&value.Summary, &value.EvidenceRefs, &value.RequiresCognition, &value.RecordedAtUnixMillis, &value.Revision, &value.ClaimedBy, &value.ClaimExpiresAtUnixMillis); err != nil {
return nil, err
}
result = append(result, value)
@@ -82,6 +83,61 @@ func (r *EpistemicsRepository) RecentReflectionCandidates(ctx context.Context, l
return result, rows.Err()
}
func (r *EpistemicsRepository) ClaimReflectionCandidate(ctx context.Context, workerID string, nowUnixMillis, leaseMillis int64) (epistemics.ReflectionCandidate, error) {
if workerID == "" || leaseMillis < 1 {
return epistemics.ReflectionCandidate{}, fmt.Errorf("invalid reflection claim")
}
tx, err := r.pool.Begin(ctx)
if err != nil {
return epistemics.ReflectionCandidate{}, err
}
defer tx.Rollback(ctx)
var value epistemics.ReflectionCandidate
err = tx.QueryRow(ctx, `SELECT reflection_candidate_id,trigger,state,source_experience_id,original_intent_id,source_utterance_id,summary,evidence_refs,requires_cognition,recorded_at_unix_millis,revision,claimed_by,claim_expires_at_unix_millis
FROM epistemics.reflection_candidates
WHERE state='OPEN' AND (claim_expires_at_unix_millis=0 OR claim_expires_at_unix_millis < $1)
ORDER BY recorded_at_unix_millis,reflection_candidate_id
FOR UPDATE SKIP LOCKED LIMIT 1`, nowUnixMillis).Scan(&value.ID, &value.Trigger, &value.State, &value.SourceExperienceID, &value.OriginalIntentID, &value.SourceUtteranceID, &value.Summary, &value.EvidenceRefs, &value.RequiresCognition, &value.RecordedAtUnixMillis, &value.Revision, &value.ClaimedBy, &value.ClaimExpiresAtUnixMillis)
if err != nil {
return epistemics.ReflectionCandidate{}, err
}
value.State = epistemics.ReflectionCandidateEvaluating
value.ClaimedBy = workerID
value.ClaimExpiresAtUnixMillis = nowUnixMillis + leaseMillis
_, err = tx.Exec(ctx, `UPDATE epistemics.reflection_candidates SET state=$1,claimed_by=$2,claim_expires_at_unix_millis=$3 WHERE reflection_candidate_id=$4 AND state='OPEN'`, value.State, value.ClaimedBy, value.ClaimExpiresAtUnixMillis, value.ID)
if err != nil {
return epistemics.ReflectionCandidate{}, err
}
if err := tx.Commit(ctx); err != nil {
return epistemics.ReflectionCandidate{}, err
}
return value, nil
}
func (r *EpistemicsRepository) RecoverReflectionClaims(ctx context.Context, nowUnixMillis int64) (int64, error) {
result, err := r.pool.Exec(ctx, `UPDATE epistemics.reflection_candidates SET state='OPEN',claimed_by='',claim_expires_at_unix_millis=0 WHERE state='EVALUATING' AND claim_expires_at_unix_millis > 0 AND claim_expires_at_unix_millis < $1`, nowUnixMillis)
return result.RowsAffected(), err
}
func (r *EpistemicsRepository) ResolveReflectionCandidate(ctx context.Context, id epistemics.ReflectionCandidateID, workerID string, nowUnixMillis int64) error {
if id == "" || workerID == "" {
return fmt.Errorf("invalid reflection resolution")
}
result, err := r.pool.Exec(ctx, `UPDATE epistemics.reflection_candidates SET state='RESOLVED',claimed_by='',claim_expires_at_unix_millis=0,revision=revision+1 WHERE reflection_candidate_id=$1 AND state='EVALUATING' AND claimed_by=$2 AND claim_expires_at_unix_millis >= $3`, id, workerID, nowUnixMillis)
if err == nil && result.RowsAffected() != 1 {
return fmt.Errorf("reflection candidate claim lost")
}
return err
}
func (r *EpistemicsRepository) WaitForReflectionCandidate(ctx context.Context, interval time.Duration) error {
if interval <= 0 {
return fmt.Errorf("invalid reflection poll interval")
}
_, err := r.pool.Exec(ctx, `SELECT pg_sleep($1)`, interval.Seconds())
return err
}
func (r *EpistemicsRepository) SaveExperienceRecord(ctx context.Context, value epistemics.ExperienceRecord) error {
_, err := r.pool.Exec(ctx, `INSERT INTO epistemics.experience_records (
experience_id, source_kind, original_intent_id, source_utterance_id, context_refs, help_method, help_method_ref,
+94
View File
@@ -7,6 +7,7 @@ import (
"time"
"git.cin.su/m/Assistant/contracts/work"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
@@ -68,6 +69,15 @@ func (r *WorkRepository) SaveDispatchIntent(ctx context.Context, intent work.Age
return tx.Commit(ctx)
}
func (r *WorkRepository) LoadDispatchIntent(ctx context.Context, dispatchID string) (work.AgentDispatchIntent, error) {
var intent work.AgentDispatchIntent
err := r.pool.QueryRow(ctx, `SELECT dispatch_id,idempotency_key,delegation_id,run_id,implementation_id,implementation_revision,source_revision,worktree_id,verification_plan_ref,created_at_unix_millis FROM work.agent_dispatch_intents WHERE dispatch_id=$1`, dispatchID).Scan(&intent.DispatchID, &intent.IdempotencyKey, &intent.DelegationID, &intent.RunID, &intent.ImplementationID, &intent.ImplementationRevision, &intent.SourceRevision, &intent.WorktreeID, &intent.VerificationPlanRef, &intent.CreatedAtUnixMillis)
if err != nil {
return work.AgentDispatchIntent{}, fmt.Errorf("load dispatch intent: %w", err)
}
return intent, nil
}
func (r *WorkRepository) EnqueueDispatch(ctx context.Context, item work.AgentDispatchQueueItem) error {
_, err := r.pool.Exec(ctx, `INSERT INTO work.agent_dispatch_queue(dispatch_id,state,available_at_unix_millis,attempts,claimed_by,updated_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT(dispatch_id) DO NOTHING`, item.DispatchID, item.State, item.AvailableAtUnixMillis, item.Attempts, item.ClaimedBy, item.UpdatedAtUnixMillis)
return err
@@ -82,6 +92,9 @@ func (r *WorkRepository) ClaimNextDispatch(ctx context.Context, workerID string,
var item work.AgentDispatchQueueItem
err = tx.QueryRow(ctx, `SELECT dispatch_id,state,available_at_unix_millis,attempts,claimed_by,updated_at_unix_millis FROM work.agent_dispatch_queue WHERE state='QUEUED' AND available_at_unix_millis <= $1 ORDER BY available_at_unix_millis,dispatch_id FOR UPDATE SKIP LOCKED LIMIT 1`, nowUnixMillis).Scan(&item.DispatchID, &item.State, &item.AvailableAtUnixMillis, &item.Attempts, &item.ClaimedBy, &item.UpdatedAtUnixMillis)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return work.AgentDispatchQueueItem{}, nil
}
return work.AgentDispatchQueueItem{}, err
}
item.State = work.AgentDispatchClaimed
@@ -98,6 +111,14 @@ func (r *WorkRepository) ClaimNextDispatch(ctx context.Context, workerID string,
return item, nil
}
func (r *WorkRepository) WaitForDispatch(ctx context.Context, interval time.Duration) error {
if interval <= 0 {
return fmt.Errorf("invalid worker poll interval")
}
_, err := r.pool.Exec(ctx, `SELECT pg_sleep($1)`, interval.Seconds())
return err
}
func (r *WorkRepository) CompleteDispatch(ctx context.Context, dispatchID, workerID string, state work.AgentDispatchQueueState, nowUnixMillis int64) error {
if state != work.AgentDispatchCompleted && state != work.AgentDispatchFailed {
return fmt.Errorf("invalid terminal dispatch state: %s", state)
@@ -112,6 +133,79 @@ func (r *WorkRepository) CompleteDispatch(ctx context.Context, dispatchID, worke
return nil
}
func (r *WorkRepository) SaveWorkGraph(ctx context.Context, graph work.WorkGraph) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err := tx.Exec(ctx, `INSERT INTO work.graphs(graph_id,revision,state,max_parallel,max_attempts) VALUES($1,$2,$3,$4,$5) ON CONFLICT(graph_id,revision) DO NOTHING`, graph.ID, graph.Revision, graph.State, graph.Budget.MaxParallel, graph.Budget.MaxAttempts); err != nil {
return err
}
for _, node := range graph.Nodes {
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_nodes(graph_id,revision,node_id,idempotency_key,state,attempts) VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT(graph_id,revision,node_id) DO NOTHING`, graph.ID, graph.Revision, node.ID, node.IdempotencyKey, node.State, node.Attempts); err != nil {
return err
}
}
for _, edge := range graph.Edges {
if _, err := tx.Exec(ctx, `INSERT INTO work.graph_edges(graph_id,revision,from_node_id,to_node_id) VALUES($1,$2,$3,$4) ON CONFLICT(graph_id,revision,from_node_id,to_node_id) DO NOTHING`, graph.ID, graph.Revision, edge.From, edge.To); err != nil {
return err
}
}
return tx.Commit(ctx)
}
func (r *WorkRepository) LoadWorkGraph(ctx context.Context, graphID string, revision int64) (work.WorkGraph, error) {
var graph work.WorkGraph
err := r.pool.QueryRow(ctx, `SELECT graph_id,revision,state,max_parallel,max_attempts FROM work.graphs WHERE graph_id=$1 AND revision=$2`, graphID, revision).Scan(&graph.ID, &graph.Revision, &graph.State, &graph.Budget.MaxParallel, &graph.Budget.MaxAttempts)
if err != nil {
return work.WorkGraph{}, fmt.Errorf("load graph: %w", err)
}
rows, err := r.pool.Query(ctx, `SELECT node_id,idempotency_key,state,attempts FROM work.graph_nodes WHERE graph_id=$1 AND revision=$2 ORDER BY node_id`, graphID, revision)
if err != nil {
return work.WorkGraph{}, err
}
for rows.Next() {
var node work.GraphNode
if err := rows.Scan(&node.ID, &node.IdempotencyKey, &node.State, &node.Attempts); err != nil {
rows.Close()
return work.WorkGraph{}, err
}
graph.Nodes = append(graph.Nodes, node)
}
if err := rows.Err(); err != nil {
rows.Close()
return work.WorkGraph{}, err
}
rows.Close()
edges, err := r.pool.Query(ctx, `SELECT from_node_id,to_node_id FROM work.graph_edges WHERE graph_id=$1 AND revision=$2 ORDER BY from_node_id,to_node_id`, graphID, revision)
if err != nil {
return work.WorkGraph{}, err
}
for edges.Next() {
var edge work.GraphEdge
if err := edges.Scan(&edge.From, &edge.To); err != nil {
edges.Close()
return work.WorkGraph{}, err
}
graph.Edges = append(graph.Edges, edge)
}
if err := edges.Err(); err != nil {
edges.Close()
return work.WorkGraph{}, err
}
edges.Close()
return graph, nil
}
func (r *WorkRepository) RecoverStaleClaims(ctx context.Context, nowUnixMillis, staleAfterMillis int64) (int64, error) {
command, err := r.pool.Exec(ctx, `UPDATE work.agent_dispatch_queue SET state='QUEUED',claimed_by='',updated_at_unix_millis=$1 WHERE state='CLAIMED' AND updated_at_unix_millis <= $2`, nowUnixMillis, nowUnixMillis-staleAfterMillis)
if err != nil {
return 0, err
}
return command.RowsAffected(), nil
}
func (r *WorkRepository) SaveAgentLease(ctx context.Context, lease work.AgentLease) error {
err := r.pool.QueryRow(ctx, `INSERT INTO work.agent_leases(lease_id,run_id,worker_id,fencing_token,state,acquired_at_unix_millis,expires_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(run_id) DO UPDATE SET lease_id=EXCLUDED.lease_id,worker_id=EXCLUDED.worker_id,fencing_token=EXCLUDED.fencing_token,state=EXCLUDED.state,acquired_at_unix_millis=EXCLUDED.acquired_at_unix_millis,expires_at_unix_millis=EXCLUDED.expires_at_unix_millis WHERE work.agent_leases.state='EXPIRED' OR work.agent_leases.expires_at_unix_millis <= EXCLUDED.acquired_at_unix_millis RETURNING run_id`, lease.ID, lease.RunID, lease.WorkerID, lease.FencingToken, lease.State, lease.AcquiredAt.UnixMilli(), lease.ExpiresAt.UnixMilli()).Scan(new(string))
if err != nil {
+76
View File
@@ -0,0 +1,76 @@
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"time"
"git.cin.su/m/Assistant/adapters/postgres"
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
"git.cin.su/m/Assistant/contracts/work"
"git.cin.su/m/Assistant/modules/work/application"
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
)
func main() {
workerID := flag.String("worker-id", "agent-worker-1", "durable worker identity")
staleAfter := flag.Duration("stale-after", 10*time.Minute, "age after which a claimed item is recovered")
leaseTTL := flag.Duration("lease-ttl", 15*time.Minute, "dry-run lease duration")
pollInterval := flag.Duration("poll-interval", 5*time.Second, "database-owned idle wake-up interval")
once := flag.Bool("once", false, "run one cycle and exit")
flag.Parse()
password, err := resolveDatabasePassword("WORK_POSTGRES_PASSWORD", "WORK_POSTGRES_CREDENTIAL_REF", secretcontracts.RuntimeConsumerWorkWorker)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
repository, err := postgres.OpenWork(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("WORK_POSTGRES_USER"), Password: password})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer repository.Close()
ctx := context.Background()
for {
result, cycleErr := application.ExecuteWorkerCycle(ctx, repository, *workerID, work.AgentFencingToken(1), time.Now().UTC(), *staleAfter, *leaseTTL)
if cycleErr != nil {
fmt.Fprintln(os.Stderr, cycleErr)
if *once {
os.Exit(1)
}
}
if cycleErr == nil && (result.Completed || result.RecoveredClaims > 0) {
_ = json.NewEncoder(os.Stdout).Encode(result)
}
if *once {
return
}
if err := repository.WaitForDispatch(ctx, *pollInterval); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
}
func resolveDatabasePassword(name, refName string, consumer secretcontracts.RuntimeConsumer) (string, error) {
if _, ok := os.LookupEnv(name); ok {
return "", fmt.Errorf("%s direct environment secret is forbidden", name)
}
path, ref := os.Getenv(name+"_FILE"), os.Getenv(refName)
if path == "" || ref == "" {
return "", fmt.Errorf("%s_FILE and %s are required", name, refName)
}
root := os.Getenv("SECRET_BACKEND_ROOT")
if root == "" {
root = "/run/secrets"
}
resolver, err := platformsecrets.NewFileResolver(root)
if err != nil {
return "", err
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: consumer, Path: path})
}
+74
View File
@@ -0,0 +1,74 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"time"
"git.cin.su/m/Assistant/adapters/postgres"
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
)
func main() {
workerID := flag.String("worker-id", "reflection-worker-1", "stable worker identity")
lease := flag.Duration("lease-ttl", 5*time.Minute, "candidate claim lease")
pollInterval := flag.Duration("poll-interval", 15*time.Second, "database-owned idle wake-up interval")
once := flag.Bool("once", false, "run one cycle and exit")
flag.Parse()
ctx := context.Background()
password, err := resolveDatabasePassword("EPISTEMICS_POSTGRES_PASSWORD", "EPISTEMICS_POSTGRES_CREDENTIAL_REF")
if err != nil {
panic(err)
}
config := postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("EPISTEMICS_POSTGRES_USER"), Password: password}
repository, err := postgres.OpenEpistemics(ctx, config)
if err != nil {
panic(err)
}
defer repository.Close()
for {
now := time.Now().UnixMilli()
if _, err := repository.RecoverReflectionClaims(ctx, now); err != nil {
panic(err)
}
candidate, claimErr := repository.ClaimReflectionCandidate(ctx, *workerID, now, lease.Milliseconds())
if claimErr == nil {
// This worker only closes the durable evaluation loop. Any cognitive
// proposal remains a candidate and must be admitted by a separate governed flow.
if err := repository.ResolveReflectionCandidate(ctx, candidate.ID, *workerID, time.Now().UnixMilli()); err != nil {
panic(err)
}
fmt.Printf("resolved reflection candidate %s\n", candidate.ID)
} else if *once {
fmt.Println("no reflection candidate")
}
if *once {
return
}
if err := repository.WaitForReflectionCandidate(ctx, *pollInterval); err != nil {
panic(err)
}
}
}
func resolveDatabasePassword(name, refName string) (string, error) {
if _, ok := os.LookupEnv(name); ok {
return "", fmt.Errorf("%s direct environment secret is forbidden", name)
}
path, ref := os.Getenv(name+"_FILE"), os.Getenv(refName)
if path == "" || ref == "" {
return "", fmt.Errorf("%s_FILE and %s are required", name, refName)
}
root := os.Getenv("SECRET_BACKEND_ROOT")
if root == "" {
root = "/run/secrets"
}
resolver, err := platformsecrets.NewFileResolver(root)
if err != nil {
return "", err
}
return resolver.ResolveFile(secretcontracts.SecretFileRef{CredentialRef: secretcontracts.CredentialRef(ref), Purpose: secretcontracts.SecretPurposeDatabasePassword, Consumer: secretcontracts.RuntimeConsumerEpistemicsWorker, Path: path})
}
+25
View File
@@ -0,0 +1,25 @@
package capability
type FleetTier string
const (
FleetTierFast FleetTier = "FAST"
FleetTierMedium FleetTier = "MEDIUM"
FleetTierHeavy FleetTier = "HEAVY"
FleetTierLifeboat FleetTier = "LIFEBOAT"
)
type FleetMember struct {
ProviderInstanceID ProviderInstanceID
ModelDescriptorID ModelDescriptorID
Tier FleetTier
Ready bool
Reason string
}
type FleetReadiness struct {
ObservedAtUnixMillis int64
Members []FleetMember
ReadyTiers []FleetTier
FallbackTier FleetTier
}
+45 -11
View File
@@ -16,19 +16,24 @@ type FreshnessState string
type CoverageState string
type ConflictState string
type EvidenceKind string
type ResearchFindingState string
const (
FreshnessCurrent FreshnessState = "CURRENT"
FreshnessStale FreshnessState = "STALE"
FreshnessUnknown FreshnessState = "UNKNOWN"
CoverageComplete CoverageState = "COMPLETE"
CoveragePartial CoverageState = "PARTIAL"
CoverageUnknown CoverageState = "UNKNOWN"
ConflictNone ConflictState = "NONE"
ConflictPresent ConflictState = "PRESENT"
ConflictUnknown ConflictState = "UNKNOWN"
EvidenceObserved EvidenceKind = "OBSERVED"
EvidenceAssumed EvidenceKind = "ASSUMED"
FreshnessCurrent FreshnessState = "CURRENT"
FreshnessStale FreshnessState = "STALE"
FreshnessUnknown FreshnessState = "UNKNOWN"
CoverageComplete CoverageState = "COMPLETE"
CoveragePartial CoverageState = "PARTIAL"
CoverageUnknown CoverageState = "UNKNOWN"
ConflictNone ConflictState = "NONE"
ConflictPresent ConflictState = "PRESENT"
ConflictUnknown ConflictState = "UNKNOWN"
EvidenceObserved EvidenceKind = "OBSERVED"
EvidenceAssumed EvidenceKind = "ASSUMED"
FindingSupported ResearchFindingState = "SUPPORTED"
FindingStale ResearchFindingState = "STALE"
FindingConflicted ResearchFindingState = "CONFLICTED"
FindingUnknown ResearchFindingState = "UNKNOWN"
)
type ResearchPlan struct {
@@ -72,6 +77,35 @@ type Citation struct {
ResolvedAtUnixMillis int64
}
// ResearchCandidate is read-only evidence returned by a connector. The
// connector supplies the immutable snapshot and exact excerpt; the evaluator
// never fetches, rewrites or invents either one.
type ResearchCandidate struct {
PropositionKey string
Statement string
Source Source
Snapshot SourceSnapshot
Excerpt SourceExcerpt
Evidence EvidenceKind
Coverage CoverageState
Conflict ConflictState
}
type ResearchFinding struct {
PropositionKey string
Statement string
Claim Claim
Citations []Citation
State ResearchFindingState
Reason string
}
type ResearchResult struct {
PlanID ResearchPlanID
Findings []ResearchFinding
EvaluatedAtUnixMillis int64
}
// ResolveCitation creates evidence only when all identifiers form one exact
// source chain. It deliberately does not fetch, infer or rewrite source text.
func ResolveCitation(source Source, snapshot SourceSnapshot, excerpt SourceExcerpt, evidence EvidenceKind, coverage CoverageState, conflict ConflictState, resolvedAtUnixMillis int64) (Citation, error) {
+38
View File
@@ -0,0 +1,38 @@
package epistemics
type BeliefState string
const (
BeliefProvisional BeliefState = "PROVISIONAL"
BeliefConflicted BeliefState = "CONFLICTED"
BeliefVerified BeliefState = "VERIFIED"
)
type Observation struct {
ID ObservationID
SourceSnapshot SourceSnapshotID
Excerpt SourceExcerptID
Text string
ObservedAt int64
}
type Claim struct {
ID ClaimID
Statement string
ObservationID ObservationID
CitationID CitationID
Verification VerificationState
}
type Belief struct {
ID BeliefID
ClaimID ClaimID
State BeliefState
}
type TruthBundle struct {
Observation Observation
Claim Claim
Belief Belief
Citation Citation
}
+13 -11
View File
@@ -120,15 +120,17 @@ type ExperienceRecord struct {
// Cognitive reflection, if used, must go through Cognitive Gateway/Router and
// produce candidates only; it cannot directly rewrite canonical state.
type ReflectionCandidate struct {
ID ReflectionCandidateID
Trigger ReflectionTriggerKind
State ReflectionCandidateState
SourceExperienceID ExperienceRecordID
OriginalIntentID OriginalIntentRef
SourceUtteranceID SourceUtteranceRef
Summary string
EvidenceRefs []EvidenceRef
RequiresCognition bool
RecordedAtUnixMillis int64
Revision int64
ID ReflectionCandidateID
Trigger ReflectionTriggerKind
State ReflectionCandidateState
SourceExperienceID ExperienceRecordID
OriginalIntentID OriginalIntentRef
SourceUtteranceID SourceUtteranceRef
Summary string
EvidenceRefs []EvidenceRef
RequiresCognition bool
RecordedAtUnixMillis int64
Revision int64
ClaimedBy string
ClaimExpiresAtUnixMillis int64
}
+3 -1
View File
@@ -15,7 +15,9 @@ const (
type RuntimeConsumer string
const (
RuntimeConsumerCore RuntimeConsumer = "core-runtime"
RuntimeConsumerCore RuntimeConsumer = "core-runtime"
RuntimeConsumerWorkWorker RuntimeConsumer = "agent-worker"
RuntimeConsumerEpistemicsWorker RuntimeConsumer = "reflection-worker"
)
// SecretFileRef is bootstrap delivery metadata for the V1 protected local
+72
View File
@@ -0,0 +1,72 @@
package work
type GraphNodeID string
type GraphState string
type GraphNodeState string
const (
GraphDraft GraphState = "DRAFT"
GraphAdmitted GraphState = "ADMITTED"
GraphRunning GraphState = "RUNNING"
GraphPartial GraphState = "PARTIAL"
GraphVerifying GraphState = "VERIFYING"
GraphCompleted GraphState = "COMPLETED"
GraphFailed GraphState = "FAILED"
GraphUnknown GraphState = "UNKNOWN"
GraphNodeBlocked GraphNodeState = "BLOCKED_PREREQUISITE"
GraphNodeReady GraphNodeState = "READY"
GraphNodeLeased GraphNodeState = "LEASED"
GraphNodeRunning GraphNodeState = "RUNNING"
GraphNodeSuccess GraphNodeState = "SUCCEEDED"
GraphNodeFailed GraphNodeState = "FAILED"
GraphNodeCancel GraphNodeState = "CANCELLED"
GraphNodeUnknown GraphNodeState = "UNKNOWN"
)
type GraphBudget struct {
MaxParallel int
MaxAttempts int
}
type GraphNode struct {
ID GraphNodeID
IdempotencyKey string
State GraphNodeState
Attempts int
}
type GraphEdge struct {
From GraphNodeID
To GraphNodeID
}
type WorkGraph struct {
ID string
Revision int64
State GraphState
Nodes []GraphNode
Edges []GraphEdge
Budget GraphBudget
}
type ResultCandidate struct {
ID string
GraphID string
GraphRevision int64
NodeID GraphNodeID
AuthorRunID AgentRunID
ArtifactRefs []EvidenceRef
EvidenceSummary string
Summary string
}
type ConflictSet struct {
ID string
GraphID string
GraphRevision int64
NodeID GraphNodeID
CandidateIDs []string
Resolved bool
}
+29
View File
@@ -0,0 +1,29 @@
package work
type DevelopmentStage string
const (
DevelopmentEvidenceCollected DevelopmentStage = "EVIDENCE_COLLECTED"
DevelopmentProposalCreated DevelopmentStage = "PROPOSAL_CREATED"
DevelopmentTaskAdmitted DevelopmentStage = "TASK_ADMITTED"
DevelopmentIsolated DevelopmentStage = "ISOLATED"
DevelopmentVerified DevelopmentStage = "VERIFIED"
DevelopmentCanary DevelopmentStage = "CANARY"
DevelopmentPromoted DevelopmentStage = "PROMOTED"
DevelopmentRolledBack DevelopmentStage = "ROLLED_BACK"
)
type DevelopmentGate struct {
ID string
Stage DevelopmentStage
EvidenceRefs []string
ProposalRef string
TaskPacketRef string
WorktreeRef string
ProducerRunRefs []AgentRunID
VerifierRunRefs []AgentRunID
VerificationEvidence string
CanaryEvidence string
PromotionRef string
RollbackRef string
}
@@ -0,0 +1,41 @@
BEGIN;
SET ROLE work_role;
CREATE TABLE IF NOT EXISTS work.graphs (
graph_id TEXT NOT NULL,
revision BIGINT NOT NULL,
state TEXT NOT NULL,
max_parallel INTEGER NOT NULL,
max_attempts INTEGER NOT NULL,
PRIMARY KEY (graph_id, revision),
CHECK (graph_id <> ''), CHECK (revision > 0),
CHECK (state IN ('DRAFT','ADMITTED','RUNNING','PAUSED','CANCELLING','PARTIAL','VERIFYING','COMPLETED','FAILED','UNKNOWN','RECOVERING')),
CHECK (max_parallel > 0), CHECK (max_attempts > 0)
);
CREATE TABLE IF NOT EXISTS work.graph_nodes (
graph_id TEXT NOT NULL,
revision BIGINT NOT NULL,
node_id TEXT NOT NULL,
idempotency_key TEXT NOT NULL,
state TEXT NOT NULL,
attempts INTEGER NOT NULL,
PRIMARY KEY (graph_id, revision, node_id),
FOREIGN KEY (graph_id, revision) REFERENCES work.graphs(graph_id, revision),
CHECK (node_id <> ''), CHECK (idempotency_key <> ''), CHECK (attempts >= 0),
CHECK (state IN ('BLOCKED_PREREQUISITE','READY','LEASED','RUNNING','SUCCEEDED','FAILED','CANCELLED','UNKNOWN','RECOVERING'))
);
CREATE TABLE IF NOT EXISTS work.graph_edges (
graph_id TEXT NOT NULL,
revision BIGINT NOT NULL,
from_node_id TEXT NOT NULL,
to_node_id TEXT NOT NULL,
PRIMARY KEY (graph_id, revision, from_node_id, to_node_id),
FOREIGN KEY (graph_id, revision) REFERENCES work.graphs(graph_id, revision),
CHECK (from_node_id <> ''), CHECK (to_node_id <> ''), CHECK (from_node_id <> to_node_id)
);
RESET ROLE;
COMMIT;
@@ -0,0 +1,19 @@
BEGIN;
SET ROLE epistemics_role;
ALTER TABLE epistemics.reflection_candidates
ADD COLUMN IF NOT EXISTS claimed_by TEXT NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS claim_expires_at_unix_millis BIGINT NOT NULL DEFAULT 0;
ALTER TABLE epistemics.reflection_candidates
DROP CONSTRAINT IF EXISTS reflection_candidates_claim_expiry_check;
ALTER TABLE epistemics.reflection_candidates
ADD CONSTRAINT reflection_candidates_claim_expiry_check
CHECK (claim_expires_at_unix_millis >= 0);
CREATE INDEX IF NOT EXISTS reflection_candidates_claimable_idx
ON epistemics.reflection_candidates (state, claim_expires_at_unix_millis, recorded_at_unix_millis, reflection_candidate_id);
RESET ROLE;
COMMIT;
+32
View File
@@ -97,6 +97,38 @@ services:
WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD}
depends_on: { postgres: { condition: service_healthy } }
agent-worker:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: agent-worker, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
command: ["-worker-id", "agent-worker-production"]
environment:
POSTGRES_HOST: postgres
POSTGRES_PORT: "5432"
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
WORK_POSTGRES_USER: work_role
WORK_POSTGRES_CREDENTIAL_REF: credential://domovoy/work/db-password/v1
WORK_POSTGRES_PASSWORD_FILE: /run/secrets/work_db_password
SECRET_BACKEND_ROOT: /run/secrets
depends_on: { postgres: { condition: service_healthy } }
volumes:
- assistant_core_secrets:/run/secrets:ro
reflection-worker:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: reflection-worker, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
command: ["-worker-id", "reflection-worker-production"]
environment:
POSTGRES_HOST: postgres
POSTGRES_PORT: "5432"
POSTGRES_DATABASE: ${POSTGRES_DB:-assistant}
EPISTEMICS_POSTGRES_USER: epistemics_role
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
EPISTEMICS_POSTGRES_PASSWORD_FILE: /run/secrets/epistemics_db_password
SECRET_BACKEND_ROOT: /run/secrets
depends_on: { postgres: { condition: service_healthy } }
volumes:
- assistant_core_secrets:/run/secrets:ro
continuity-runtime:
build: { context: .., dockerfile: deploy/images/runtime.Dockerfile, args: { TARGET: continuity-runtime, SOURCE_REVISION: "${ASSISTANT_SOURCE_REVISION:-unknown}" } }
restart: unless-stopped
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000129-agent-dispatch-queue.json",
"task_id": "DEV-000129",
"task_file": "dev/tasks/DEV-000140-research-evaluation-runtime.json",
"task_id": "DEV-000140",
"task_revision": 1
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000130",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "323a67a",
"risk_tier": "E",
"intent": "Implement a one-shot durable agent worker cycle that recovers stale claims, claims one queue item, executes the deterministic worker path and completes it with worker fencing.",
"requirement_ids": ["WP-301", "Agent worker", "Stale-run/claim recovery"],
"allowed_change_scope": {"modules": ["work-agent-execution", "work-storage"], "paths": ["adapters/postgres/work_repository.go", "modules/work/ports/worker.go", "modules/work/application/worker.go", "cmd/agent-worker/main.go", "tests/agent_execution/worker_sql_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000130-agent-worker-recovery.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "database/**", "runtime/**", ".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-000129 durable queue", "DEV-000122 deterministic worker", "DEV-000123 orchestration"], "may_change": ["WorkerStore port and one-shot worker application"]},
"semantic_owner": "work-agent-execution",
"canonical_state": ["Queue state and dispatch intent remain Work-owned"],
"state_transitions": ["stale CLAIMED -> QUEUED -> CLAIMED -> COMPLETED/FAILED"],
"side_effects_and_privileges": ["Explicit one-shot command; no unmanaged goroutine or in-memory queue"],
"persistence_and_migrations": ["Reuses migration 030; production apply follows separate canary task"],
"failure_retry_restart": ["Stale recovery is durable; worker completion requires claimed_by match; failures become FAILED"],
"assumptions": ["Dispatcher enqueue path is supplied by a later integration task", "Deterministic worker remains the only implementation in this canary stage"],
"acceptance_scenarios": ["Worker recovers stale claims", "Worker claim is row-locked", "Wrong worker cannot complete claim", "Worker cycle is one-shot and restartable"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_MIGRATION_DECISION", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000131",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "a445034",
"risk_tier": "E",
"intent": "Implement the first deterministic Multi-Agent Work Graph harness: typed graph revision, DAG validation, budgets and dependency-aware ready-node selection.",
"requirement_ids": ["WP-400", "DAG validation", "Fan-out/fan-in foundation"],
"allowed_change_scope": {"modules": ["work-agent-execution"], "paths": ["contracts/work/graph.go", "modules/work/domain/graph.go", "tests/agent_execution/graph_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000131-work-graph-harness.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "database/**", "adapters/**", "cmd/**", "runtime/**", ".gitea/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["ADR-0021 graph proposal", "DEV-000130 worker/recovery contracts"], "may_change": ["Work graph typed contracts and pure domain harness"]},
"semantic_owner": "work-agent-execution",
"canonical_state": ["Typed WorkGraph value only; no persistence in this harness"],
"state_transitions": ["DRAFT/ADMITTED/RUNNING graph with dependency-aware READY nodes"],
"side_effects_and_privileges": ["Pure deterministic validation; no dispatch or external mutation"],
"persistence_and_migrations": ["Persistence/scheduler implementation follows after harness"],
"failure_retry_restart": ["Cycles, duplicate IDs/keys and invalid budgets reject before scheduling"],
"assumptions": ["Graph scheduler must not bypass Agent Execution leases or verifier policy"],
"acceptance_scenarios": ["Cycle rejected", "Ready nodes deterministic", "Dependent node remains blocked until prerequisite succeeds", "Budget is validated"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000132",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "b2f922c",
"risk_tier": "E",
"intent": "Add deterministic Work Graph admission, bounded ready-node scheduling and explicit conflict resolution to the graph harness.",
"requirement_ids": ["WP-400", "Graph admission", "Bounded scheduler", "Conflict resolution"],
"allowed_change_scope": {"modules": ["work-agent-execution"], "paths": ["modules/work/domain/graph_scheduler.go", "tests/agent_execution/graph_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000132-work-graph-admission-scheduler.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "database/**", "adapters/**", "cmd/**", "runtime/**", ".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-000131 graph harness", "ADR-0021 graph lifecycle"], "may_change": ["Pure graph admission/scheduling/conflict domain functions"]},
"semantic_owner": "work-agent-execution",
"canonical_state": ["No persistence; graph values remain deterministic harness state"],
"state_transitions": ["DRAFT -> ADMITTED; READY nodes bounded by MaxParallel; conflict unresolved -> explicitly resolved"],
"side_effects_and_privileges": ["Pure domain logic only"],
"persistence_and_migrations": ["Durable scheduler follows after the harness"],
"failure_retry_restart": ["Budget overflow and unknown conflict candidate reject without mutation"],
"assumptions": ["Runtime scheduler must use Agent Execution queue/leases in a later task"],
"acceptance_scenarios": ["Admission validates attempt budget", "Scheduler respects active/max parallel", "Conflict cannot resolve without explicit known candidate"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000133",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "4a800df",
"risk_tier": "E",
"intent": "Persist immutable Work Graph revisions with typed nodes and dependency edges for durable scheduler integration.",
"requirement_ids": ["WP-401", "Durable graph revision", "Graph node dependencies"],
"allowed_change_scope": {"modules": ["work-agent-execution", "work-storage"], "paths": ["database/migrations/031_work_graph_persistence.sql", "adapters/postgres/work_repository.go", "tests/agent_execution/graph_persistence_sql_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000133-work-graph-persistence.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "cmd/**", "runtime/**", ".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": 1},
"contracts": {"consume": ["DEV-000131/132 graph contracts and scheduler"], "may_change": ["Work graph persistence schema and repository methods"]},
"semantic_owner": "work-agent-execution",
"canonical_state": ["work.graphs", "work.graph_nodes", "work.graph_edges"],
"state_transitions": ["graph revision persisted once; nodes and edges immutable within revision"],
"side_effects_and_privileges": ["Work schema only; no cross-owner SQL"],
"persistence_and_migrations": ["Append-only migration 031; production apply is later canary"],
"failure_retry_restart": ["Idempotent inserts; load reconstructs deterministic node/edge order"],
"assumptions": ["Durable scheduler and result/conflict persistence follow"],
"acceptance_scenarios": ["Graph revision cannot collide into mutation", "Nodes/edges remain tied to revision", "Load reconstructs graph"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_MIGRATION_DECISION", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000134",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "e9bb42c",
"risk_tier": "E",
"intent": "Implement the first Truth runtime compiler that produces typed Observation, Claim, Belief and Citation values from an exact source chain.",
"requirement_ids": ["WP-100", "Observation/Claim/Belief", "Truth citation provenance"],
"allowed_change_scope": {"modules": ["epistemics"], "paths": ["contracts/epistemics/truth.go", "modules/epistemics/domain/truth.go", "tests/research_contracts/truth_bundle_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000134-truth-bundle-compiler.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "database/**", "adapters/**", "cmd/**", "runtime/**", ".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-000109 research contracts", "DEV-000110 citation resolver"], "may_change": ["Typed Truth bundle contracts and pure compiler"]},
"semantic_owner": "epistemics",
"canonical_state": ["Typed TruthBundle value only; persistence follows"],
"state_transitions": ["source chain -> observed observation -> unverified claim -> provisional/conflicted belief"],
"side_effects_and_privileges": ["Pure compiler; no fetching, external mutation or authority grant"],
"persistence_and_migrations": ["Truth persistence is a later task"],
"failure_retry_restart": ["Broken source chain rejects; conflict remains visible"],
"assumptions": ["Research executor will supply exact source snapshots/excerpts"],
"acceptance_scenarios": ["Observation/claim/citation refs align", "Conflict produces CONFLICTED belief", "Broken source chain is rejected"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000135",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "32e33e3",
"risk_tier": "E",
"intent": "Add durable lease, stale recovery, and one-shot governed closure for reflection candidates.",
"requirement_ids": ["WP-501", "Reflection candidate worker", "Stale claim recovery"],
"allowed_change_scope": {"modules": ["epistemics", "epistemics-storage"], "paths": ["contracts/epistemics/types.go", "database/migrations/032_reflection_candidate_leases.sql", "adapters/postgres/epistemics_repository.go", "cmd/reflection-worker/main.go", "tests/research_contracts/reflection_worker_sql_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000135-reflection-worker.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "contracts/authority/**", "modules/cognitive/**", ".gitea/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 1, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
"contracts": {"consume": ["Existing epistemics ReflectionCandidate contract"], "may_change": ["ReflectionCandidate lease metadata"]},
"semantic_owner": "epistemics",
"canonical_state": ["epistemics.reflection_candidates"],
"state_transitions": ["OPEN to EVALUATING under lease; EVALUATING to OPEN on expiry; EVALUATING to RESOLVED by owning worker"],
"side_effects_and_privileges": ["Epistemics schema only; no cognitive call and no proposal promotion"],
"persistence_and_migrations": ["Append-only migration 032; production apply is later canary"],
"failure_retry_restart": ["FOR UPDATE SKIP LOCKED claim; expired claims are recoverable; one-shot worker is restart-safe"],
"assumptions": ["Cognitive evaluation and skill admission remain separate governed stages"],
"acceptance_scenarios": ["Concurrent workers claim distinct candidates", "expired evaluation returns to OPEN", "only claim owner can resolve"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_MIGRATION_DECISION", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000136",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "f73c699",
"risk_tier": "E",
"intent": "Make model fleet tiers, readiness, and explicit lifeboat fallback durable as typed capability policy.",
"requirement_ids": ["WP-601", "Model fleet policy", "Fallback readiness"],
"allowed_change_scope": {"modules": ["capability"], "paths": ["contracts/capability/fleet.go", "modules/capability/domain/fleet.go", "modules/capability/domain/fleet_test.go", "tests/cognitive_registry/fleet_contract_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000136-fleet-readiness-policy.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "database/**", "cmd/**", ".gitea/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["Existing cognitive registry and health observations"], "may_change": ["Typed fleet readiness policy"]},
"semantic_owner": "capability",
"canonical_state": ["FleetReadiness value object"],
"state_transitions": ["observed member readiness determines selected preferred or fallback tier"],
"side_effects_and_privileges": ["No provider invocation or external mutation"],
"persistence_and_migrations": ["In-memory typed decision; runtime persistence follows fleet registration integration"],
"failure_retry_restart": ["No ready fallback yields explicit error"],
"assumptions": ["Existing Cognitive Gateway remains the only invocation boundary"],
"acceptance_scenarios": ["heavy timeout selects ready lifeboat", "no ready member fails closed"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000137",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "d38406f",
"risk_tier": "E",
"intent": "Add typed governed self-development gate requiring evidence, task packet, isolated worktree, independent verifier, canary, promotion or rollback.",
"requirement_ids": ["WP-701", "Governed self-development", "Independent verification", "Rollback"],
"allowed_change_scope": {"modules": ["work-agent-execution"], "paths": ["contracts/work/self_development.go", "modules/work/domain/self_development.go", "modules/work/domain/self_development_test.go", "tests/semantic_evolution/self_development_contract_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000137-governed-self-development-gate.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "database/**", "cmd/**", "contracts/authority/**", ".gitea/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["Existing AgentRun and work graph contracts"], "may_change": ["Typed self-development stage gate"]},
"semantic_owner": "work-agent-execution",
"canonical_state": ["DevelopmentGate value object"],
"state_transitions": ["evidence to proposal to task to isolation to independent verification to canary to promotion; canary may rollback"],
"side_effects_and_privileges": ["No direct code mutation, provider invocation, or promotion side effect"],
"persistence_and_migrations": ["Value object only; durable gate persistence follows execution-plane integration"],
"failure_retry_restart": ["Invalid stage transitions and missing evidence fail closed"],
"assumptions": ["Task packet and worktree identity are provided by the execution plane"],
"acceptance_scenarios": ["same producer cannot verify output", "promotion is terminal", "canary can rollback"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000138",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "1ccd43c",
"risk_tier": "E",
"intent": "Add executable contract coverage for authenticated cabinet, operations, learning, accessibility, responsive, and reduced-motion surfaces.",
"requirement_ids": ["WP-801", "WP-802", "WP-805", "Authenticated cabinet acceptance"],
"allowed_change_scope": {"modules": ["creator-web"], "paths": ["tests/creator_web/cabinet_acceptance_contract_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000138-ui-operations-acceptance-contract.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "database/**", "cmd/**", "contracts/**", ".gitea/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["Existing creator-web authenticated API and static cabinet"], "may_change": ["Acceptance tests only"]},
"semantic_owner": "creator-web",
"canonical_state": ["None; tests inspect existing UI contract"],
"state_transitions": ["login session gates cabinet; operations and learning request authenticated API data"],
"side_effects_and_privileges": ["No external mutation; live authenticated browser acceptance remains a separate production verification"],
"persistence_and_migrations": ["None"],
"failure_retry_restart": ["Missing auth, learning, or responsive contract fails CI"],
"assumptions": ["Credentials are never embedded in acceptance tests"],
"acceptance_scenarios": ["cabinet exposes operations and reflection surfaces", "session and learning routes are authenticated", "mobile/reduced motion rules are present"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check"],
"terminal_gap_states": ["BLOCKED_BY_AUTHENTICATED_BROWSER_SESSION", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000139",
"task_revision": 2,
"architecture_revision": "ADR-0018",
"base_repo_revision": "6f5f7e1",
"risk_tier": "E",
"intent": "Run agent and reflection workers as restartable production processes with DB-owned idle wakeups and protected credential refs.",
"requirement_ids": ["WP-302", "WP-502", "Automatic worker runtime", "Production recovery"],
"allowed_change_scope": {"modules": ["work-agent-execution", "epistemics", "deployment", "creator-web-acceptance"], "paths": ["contracts/secrets/types.go", "modules/work/application/worker.go", "adapters/postgres/work_repository.go", "cmd/agent-worker/main.go", "adapters/postgres/epistemics_repository.go", "cmd/reflection-worker/main.go", "deploy/docker-compose.prod.yml", "tests/creator_web/verify-remote.ps1", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000139-production-worker-runtime.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "contracts/authority/**", ".gitea/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 2, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["Existing durable dispatch/reflection tables and file secret resolver"], "may_change": ["Worker process lifecycle and production service wiring"]},
"semantic_owner": "work-agent-execution and epistemics",
"canonical_state": ["work.agent_dispatch_queue", "epistemics.reflection_candidates"],
"state_transitions": ["workers recover stale claims, claim durable work, process one cycle, and repeat after DB-owned wakeup"],
"side_effects_and_privileges": ["Work and epistemics database roles only; credentials remain CredentialRef/file-backed"],
"persistence_and_migrations": ["Uses migrations 030 and 032 already in main"],
"failure_retry_restart": ["restartable containers; stale claims recover after restart; -once remains diagnostic mode"],
"assumptions": ["Production secret volume contains work and epistemics refs at declared paths"],
"acceptance_scenarios": ["both workers stay running idle", "worker survives empty queue", "worker uses protected secret files", "stale dispatch/reflection claims recover"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "docker compose config"],
"terminal_gap_states": ["BLOCKED_BY_SECRET_REF_MISMATCH", "BLOCKED_BY_MIGRATION_DECISION", "BLOCKED_BY_CONFLICT"]
}
@@ -0,0 +1,23 @@
{
"task_id": "DEV-000140",
"task_revision": 1,
"architecture_revision": "ADR-0018",
"base_repo_revision": "07a8de9",
"risk_tier": "E",
"intent": "Evaluate read-only research evidence into explicit supported, stale, conflicted or unknown findings without persistence or authority mutation.",
"requirement_ids": ["WP-102", "research evaluation", "citation provenance", "freshness", "conflict visibility"],
"allowed_change_scope": {"modules": ["epistemics-research-runtime"], "paths": ["contracts/epistemics/research.go", "modules/epistemics/domain/research.go", "modules/epistemics/application/research.go", "tests/research_runtime/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000140-research-evaluation-runtime.json"]},
"forbidden_change_scope": {"paths": ["architecture/**", "database/**", "deploy/**", "security/**", "cmd/**", ".gitea/**"]},
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
"contracts": {"consume": ["ADR-0019 typed source, snapshot, excerpt and citation contracts"], "may_change": ["typed research finding and read-only connector boundary"]},
"semantic_owner": "epistemics",
"canonical_state": [],
"state_transitions": ["connector evidence -> supported/stale/conflicted/unknown result", "invalid source chain -> rejected"],
"side_effects_and_privileges": ["read-only connector boundary; no external mutation; no claim persistence"],
"persistence_and_migrations": ["none"],
"failure_retry_restart": ["evaluation is pure and repeatable; connector failures remain errors"],
"assumptions": ["connectors provide immutable exact excerpts and source snapshots", "proposition keys are connector-supplied typed grouping keys"],
"acceptance_scenarios": ["no evidence remains UNKNOWN", "stale evidence is marked STALE", "incompatible statements remain CONFLICTED", "invalid citation chain is rejected", "connector boundary cannot mutate canonical state"],
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "research runtime tests"],
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_CONFLICT"]
}
+60
View File
@@ -0,0 +1,60 @@
package domain
import (
"errors"
"slices"
"sort"
"git.cin.su/m/Assistant/contracts/capability"
)
var ErrInvalidFleetReadiness = errors.New("invalid fleet readiness")
func BuildFleetReadiness(observedAt int64, members []capability.FleetMember) (capability.FleetReadiness, error) {
if observedAt < 1 || len(members) == 0 {
return capability.FleetReadiness{}, ErrInvalidFleetReadiness
}
result := capability.FleetReadiness{ObservedAtUnixMillis: observedAt, Members: append([]capability.FleetMember(nil), members...), FallbackTier: capability.FleetTierLifeboat}
seen := map[capability.FleetTier]bool{}
for i := range result.Members {
member := &result.Members[i]
if member.ProviderInstanceID == "" || member.ModelDescriptorID == "" || !validFleetTier(member.Tier) || seen[member.Tier] && member.Ready {
return capability.FleetReadiness{}, ErrInvalidFleetReadiness
}
if member.Ready {
seen[member.Tier] = true
}
}
for _, tier := range []capability.FleetTier{capability.FleetTierFast, capability.FleetTierMedium, capability.FleetTierHeavy, capability.FleetTierLifeboat} {
if seen[tier] {
result.ReadyTiers = append(result.ReadyTiers, tier)
}
}
return result, nil
}
func SelectFleetTier(readiness capability.FleetReadiness, preferred capability.FleetTier) (capability.FleetTier, error) {
if readiness.ObservedAtUnixMillis < 1 || len(readiness.Members) == 0 || !validFleetTier(readiness.FallbackTier) {
return "", ErrInvalidFleetReadiness
}
if slices.Contains(readiness.ReadyTiers, preferred) {
return preferred, nil
}
if slices.Contains(readiness.ReadyTiers, readiness.FallbackTier) {
return readiness.FallbackTier, nil
}
return "", ErrInvalidFleetReadiness
}
func validFleetTier(value capability.FleetTier) bool {
return value == capability.FleetTierFast || value == capability.FleetTierMedium || value == capability.FleetTierHeavy || value == capability.FleetTierLifeboat
}
func SortFleetMembers(members []capability.FleetMember) {
sort.SliceStable(members, func(i, j int) bool {
if members[i].Tier != members[j].Tier {
return members[i].Tier < members[j].Tier
}
return members[i].ProviderInstanceID < members[j].ProviderInstanceID
})
}
+18
View File
@@ -0,0 +1,18 @@
package domain
import (
"testing"
"git.cin.su/m/Assistant/contracts/capability"
)
func TestFleetReadinessFallsBackToLifeboatWhenPrimaryTiersUnavailable(t *testing.T) {
readiness, err := BuildFleetReadiness(100, []capability.FleetMember{{ProviderInstanceID: "cpu", ModelDescriptorID: "qwen", Tier: capability.FleetTierLifeboat, Ready: true, Reason: "local"}, {ProviderInstanceID: "gpu", ModelDescriptorID: "heavy", Tier: capability.FleetTierHeavy, Ready: false, Reason: "timeout"}})
if err != nil {
t.Fatal(err)
}
selected, err := SelectFleetTier(readiness, capability.FleetTierHeavy)
if err != nil || selected != capability.FleetTierLifeboat {
t.Fatalf("selected=%q err=%v", selected, err)
}
}
@@ -0,0 +1,27 @@
package application
import (
"context"
"git.cin.su/m/Assistant/contracts/epistemics"
domain "git.cin.su/m/Assistant/modules/epistemics/domain"
)
// ReadOnlyConnector is the only integration point for web/repository sources.
// Implementations return immutable evidence; they cannot persist claims.
type ReadOnlyConnector interface {
Collect(context.Context, epistemics.ResearchPlan) ([]epistemics.ResearchCandidate, error)
}
type ResearchService struct{ Connector ReadOnlyConnector }
func (s ResearchService) Evaluate(ctx context.Context, plan epistemics.ResearchPlan, nowUnixMillis int64) (epistemics.ResearchResult, error) {
if s.Connector == nil {
return epistemics.ResearchResult{}, domain.ErrNoResearchEvidence
}
candidates, err := s.Connector.Collect(ctx, plan)
if err != nil {
return epistemics.ResearchResult{}, err
}
return domain.EvaluateResearch(plan, candidates, nowUnixMillis)
}
+110
View File
@@ -0,0 +1,110 @@
package domain
import (
"errors"
"strings"
"git.cin.su/m/Assistant/contracts/epistemics"
)
var (
ErrInvalidResearchPlan = errors.New("invalid research plan")
ErrInvalidResearchCandidate = errors.New("invalid research candidate")
ErrNoResearchEvidence = errors.New("no research evidence")
)
// EvaluateResearch turns connector evidence into an explainable, non-canonical
// result. It is deliberately pure: it does not call a provider, persist a
// claim, or promote an assessment into authority.
func EvaluateResearch(plan epistemics.ResearchPlan, candidates []epistemics.ResearchCandidate, evaluatedAtUnixMillis int64) (epistemics.ResearchResult, error) {
if err := plan.Validate(); err != nil || evaluatedAtUnixMillis < 1 {
return epistemics.ResearchResult{}, ErrInvalidResearchPlan
}
result := epistemics.ResearchResult{PlanID: plan.ID, EvaluatedAtUnixMillis: evaluatedAtUnixMillis}
if len(candidates) == 0 {
result.Findings = []epistemics.ResearchFinding{{State: epistemics.FindingUnknown, Reason: "no connector evidence was returned"}}
return result, nil
}
groups := make(map[string][]epistemics.ResearchCandidate)
order := make([]string, 0, len(candidates))
for _, candidate := range candidates {
if strings.TrimSpace(candidate.PropositionKey) == "" || strings.TrimSpace(candidate.Statement) == "" || candidate.Evidence == "" {
return epistemics.ResearchResult{}, ErrInvalidResearchCandidate
}
if _, exists := groups[candidate.PropositionKey]; !exists {
order = append(order, candidate.PropositionKey)
}
groups[candidate.PropositionKey] = append(groups[candidate.PropositionKey], candidate)
}
for _, key := range order {
finding, err := evaluateGroup(key, groups[key], evaluatedAtUnixMillis)
if err != nil {
return epistemics.ResearchResult{}, err
}
result.Findings = append(result.Findings, finding)
}
return result, nil
}
func evaluateGroup(key string, candidates []epistemics.ResearchCandidate, evaluatedAt int64) (epistemics.ResearchFinding, error) {
if len(candidates) == 0 {
return epistemics.ResearchFinding{}, ErrNoResearchEvidence
}
first := candidates[0]
firstBundle, err := compileResearchCandidate(first)
if err != nil {
return epistemics.ResearchFinding{}, ErrInvalidResearchCandidate
}
finding := epistemics.ResearchFinding{PropositionKey: key, Statement: first.Statement, Claim: firstBundle.Claim, Citations: []epistemics.Citation{firstBundle.Citation}, State: epistemics.FindingSupported}
statements := map[string]struct{}{normalize(first.Statement): {}}
if first.Snapshot.Freshness == epistemics.FreshnessUnknown || first.Evidence == epistemics.EvidenceAssumed || first.Coverage != epistemics.CoverageComplete {
finding.State, finding.Reason = epistemics.FindingUnknown, "evidence is assumed, unknown or incomplete"
} else if first.Snapshot.Freshness == epistemics.FreshnessStale {
finding.State, finding.Reason = epistemics.FindingStale, "source snapshot is stale"
}
for _, candidate := range candidates[1:] {
bundle, resolveErr := compileResearchCandidate(candidate)
if resolveErr != nil {
return epistemics.ResearchFinding{}, ErrInvalidResearchCandidate
}
finding.Citations = append(finding.Citations, bundle.Citation)
statements[normalize(candidate.Statement)] = struct{}{}
if candidate.Snapshot.Freshness == epistemics.FreshnessUnknown || candidate.Evidence == epistemics.EvidenceAssumed || candidate.Coverage != epistemics.CoverageComplete {
finding.State, finding.Reason = epistemics.FindingUnknown, "at least one source has assumed, unknown or incomplete evidence"
} else if candidate.Snapshot.Freshness == epistemics.FreshnessStale {
finding.State, finding.Reason = epistemics.FindingStale, "at least one source snapshot is stale"
}
}
if len(statements) > 1 {
finding.State, finding.Reason = epistemics.FindingConflicted, "connector evidence contains incompatible statements"
}
for _, citation := range finding.Citations {
if citation.Conflict == epistemics.ConflictPresent {
finding.State, finding.Reason = epistemics.FindingConflicted, "connector marked evidence as conflicting"
}
}
if len(finding.Citations) == 0 || evaluatedAt < 1 {
return epistemics.ResearchFinding{}, ErrInvalidResearchCandidate
}
return finding, nil
}
func normalize(value string) string { return strings.ToLower(strings.Join(strings.Fields(value), " ")) }
// candidateConflict keeps unknown source freshness explicit at the candidate
// boundary. A connector may mark evidence as conflicting without changing the
// source; group-level incompatible statements are detected separately.
func compileResearchCandidate(candidate epistemics.ResearchCandidate) (epistemics.TruthBundle, error) {
if candidate.Coverage == "" || candidate.Conflict == "" {
return epistemics.TruthBundle{}, ErrInvalidResearchCandidate
}
citation, err := epistemics.ResolveCitation(candidate.Source, candidate.Snapshot, candidate.Excerpt, candidate.Evidence, candidate.Coverage, candidate.Conflict, candidate.Snapshot.CapturedAtUnixMillis)
if err != nil {
return epistemics.TruthBundle{}, err
}
observation := epistemics.Observation{ID: epistemics.ObservationID("observation:" + string(candidate.Excerpt.ID)), SourceSnapshot: candidate.Snapshot.ID, Excerpt: candidate.Excerpt.ID, Text: candidate.Excerpt.Text, ObservedAt: candidate.Snapshot.CapturedAtUnixMillis}
claim := epistemics.Claim{ID: epistemics.ClaimID("claim:" + string(candidate.Excerpt.ID)), Statement: candidate.Statement, ObservationID: observation.ID, CitationID: citation.ID, Verification: epistemics.VerificationUnverified}
return epistemics.TruthBundle{Observation: observation, Claim: claim, Belief: epistemics.Belief{ID: epistemics.BeliefID("belief:" + string(candidate.Excerpt.ID)), ClaimID: claim.ID, State: epistemics.BeliefProvisional}, Citation: citation}, nil
}
+31
View File
@@ -0,0 +1,31 @@
package domain
import (
"errors"
"strings"
"git.cin.su/m/Assistant/contracts/epistemics"
)
var ErrInvalidTruthBundle = errors.New("invalid truth bundle")
func CompileObservedClaim(source epistemics.Source, snapshot epistemics.SourceSnapshot, excerpt epistemics.SourceExcerpt, statement string, observedAt int64, conflict epistemics.ConflictState) (epistemics.TruthBundle, error) {
if strings.TrimSpace(statement) == "" || observedAt < 1 {
return epistemics.TruthBundle{}, ErrInvalidTruthBundle
}
citation, err := epistemics.ResolveCitation(source, snapshot, excerpt, epistemics.EvidenceObserved, epistemics.CoverageComplete, conflict, observedAt)
if err != nil {
return epistemics.TruthBundle{}, ErrInvalidTruthBundle
}
observation := epistemics.Observation{ID: epistemics.ObservationID("observation:" + string(excerpt.ID)), SourceSnapshot: snapshot.ID, Excerpt: excerpt.ID, Text: excerpt.Text, ObservedAt: observedAt}
claim := epistemics.Claim{ID: epistemics.ClaimID("claim:" + string(excerpt.ID)), Statement: statement, ObservationID: observation.ID, CitationID: citation.ID, Verification: epistemics.VerificationUnverified}
state := epistemics.BeliefProvisional
if conflict == epistemics.ConflictPresent || conflict == epistemics.ConflictUnknown {
state = epistemics.BeliefConflicted
}
bundle := epistemics.TruthBundle{Observation: observation, Claim: claim, Belief: epistemics.Belief{ID: epistemics.BeliefID("belief:" + string(excerpt.ID)), ClaimID: claim.ID, State: state}, Citation: citation}
if bundle.Observation.SourceSnapshot != snapshot.ID || bundle.Claim.ObservationID != bundle.Observation.ID || bundle.Claim.CitationID != bundle.Citation.ID || bundle.Belief.ClaimID != bundle.Claim.ID {
return epistemics.TruthBundle{}, ErrInvalidTruthBundle
}
return bundle, nil
}
+43
View File
@@ -0,0 +1,43 @@
package application
import (
"context"
"time"
"git.cin.su/m/Assistant/contracts/work"
"git.cin.su/m/Assistant/modules/work/ports"
)
type WorkerCycleResult struct {
RecoveredClaims int64
DispatchID string
Completed bool
Idle bool
}
func ExecuteWorkerCycle(ctx context.Context, store ports.WorkerStore, workerID string, token work.AgentFencingToken, now time.Time, staleAfter, leaseTTL time.Duration) (WorkerCycleResult, error) {
recovered, err := store.RecoverStaleClaims(ctx, now.UnixMilli(), staleAfter.Milliseconds())
if err != nil {
return WorkerCycleResult{}, err
}
item, err := store.ClaimNextDispatch(ctx, workerID, now.UnixMilli())
if err != nil {
return WorkerCycleResult{RecoveredClaims: recovered}, err
}
if item.DispatchID == "" {
return WorkerCycleResult{RecoveredClaims: recovered, Idle: true}, nil
}
intent, err := store.LoadDispatchIntent(ctx, item.DispatchID)
if err != nil {
_ = store.CompleteDispatch(ctx, item.DispatchID, workerID, work.AgentDispatchFailed, now.UnixMilli())
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())
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID}, err
}
if err := store.CompleteDispatch(ctx, item.DispatchID, workerID, work.AgentDispatchCompleted, now.UnixMilli()); err != nil {
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID}, err
}
return WorkerCycleResult{RecoveredClaims: recovered, DispatchID: item.DispatchID, Completed: true}, nil
}
+107
View File
@@ -0,0 +1,107 @@
package domain
import (
"errors"
"sort"
"git.cin.su/m/Assistant/contracts/work"
)
var (
ErrInvalidGraph = errors.New("invalid work graph")
ErrGraphCycle = errors.New("work graph contains a cycle")
)
func ValidateWorkGraph(graph work.WorkGraph) error {
if graph.ID == "" || graph.Revision < 1 || graph.Budget.MaxParallel < 1 || graph.Budget.MaxAttempts < 1 || len(graph.Nodes) == 0 {
return ErrInvalidGraph
}
nodes := make(map[work.GraphNodeID]struct{}, len(graph.Nodes))
keys := make(map[string]struct{}, len(graph.Nodes))
for _, node := range graph.Nodes {
if node.ID == "" || node.IdempotencyKey == "" || node.Attempts < 0 {
return ErrInvalidGraph
}
if _, exists := nodes[node.ID]; exists {
return ErrInvalidGraph
}
if _, exists := keys[node.IdempotencyKey]; exists {
return ErrInvalidGraph
}
nodes[node.ID] = struct{}{}
keys[node.IdempotencyKey] = struct{}{}
}
adjacency := make(map[work.GraphNodeID][]work.GraphNodeID, len(nodes))
for _, edge := range graph.Edges {
if edge.From == "" || edge.To == "" || edge.From == edge.To {
return ErrInvalidGraph
}
if _, ok := nodes[edge.From]; !ok {
return ErrInvalidGraph
}
if _, ok := nodes[edge.To]; !ok {
return ErrInvalidGraph
}
adjacency[edge.From] = append(adjacency[edge.From], edge.To)
}
visiting := map[work.GraphNodeID]bool{}
visited := map[work.GraphNodeID]bool{}
var visit func(work.GraphNodeID) error
visit = func(node work.GraphNodeID) error {
if visiting[node] {
return ErrGraphCycle
}
if visited[node] {
return nil
}
visiting[node] = true
for _, next := range adjacency[node] {
if err := visit(next); err != nil {
return err
}
}
delete(visiting, node)
visited[node] = true
return nil
}
for node := range nodes {
if err := visit(node); err != nil {
return err
}
}
return nil
}
func ReadyGraphNodes(graph work.WorkGraph) ([]work.GraphNodeID, error) {
if err := ValidateWorkGraph(graph); err != nil {
return nil, err
}
completed := map[work.GraphNodeID]bool{}
for _, node := range graph.Nodes {
if node.State == work.GraphNodeSuccess {
completed[node.ID] = true
}
}
blockedBy := map[work.GraphNodeID][]work.GraphNodeID{}
for _, edge := range graph.Edges {
blockedBy[edge.To] = append(blockedBy[edge.To], edge.From)
}
ready := make([]work.GraphNodeID, 0)
for _, node := range graph.Nodes {
if node.State != work.GraphNodeReady && node.State != work.GraphNodeBlocked {
continue
}
dependenciesReady := true
for _, dependency := range blockedBy[node.ID] {
if !completed[dependency] {
dependenciesReady = false
break
}
}
if dependenciesReady {
ready = append(ready, node.ID)
}
}
sort.Slice(ready, func(i, j int) bool { return ready[i] < ready[j] })
return ready, nil
}
+60
View File
@@ -0,0 +1,60 @@
package domain
import (
"errors"
"sort"
"git.cin.su/m/Assistant/contracts/work"
)
var (
ErrGraphBudgetExceeded = errors.New("work graph budget exceeded")
ErrConflictUnresolved = errors.New("conflict resolution is not explicit")
)
func ScheduleReadyNodes(graph work.WorkGraph, active int) ([]work.GraphNodeID, error) {
if err := ValidateWorkGraph(graph); err != nil {
return nil, err
}
if active < 0 || active > graph.Budget.MaxParallel {
return nil, ErrGraphBudgetExceeded
}
ready, err := ReadyGraphNodes(graph)
if err != nil {
return nil, err
}
available := graph.Budget.MaxParallel - active
if len(ready) > available {
ready = ready[:available]
}
return ready, nil
}
func AdmitGraph(graph work.WorkGraph) (work.WorkGraph, error) {
if err := ValidateWorkGraph(graph); err != nil {
return work.WorkGraph{}, err
}
for _, node := range graph.Nodes {
if node.Attempts > graph.Budget.MaxAttempts {
return work.WorkGraph{}, ErrGraphBudgetExceeded
}
}
graph.State = work.GraphAdmitted
return graph, nil
}
func ResolveGraphConflict(conflict work.ConflictSet, selectedCandidate string) (work.ConflictSet, error) {
if conflict.ID == "" || conflict.GraphID == "" || conflict.GraphRevision < 1 || conflict.NodeID == "" || conflict.Resolved || selectedCandidate == "" {
return work.ConflictSet{}, ErrConflictUnresolved
}
candidates := append([]string(nil), conflict.CandidateIDs...)
sort.Strings(candidates)
for _, candidate := range candidates {
if candidate == selectedCandidate {
conflict.CandidateIDs = candidates
conflict.Resolved = true
return conflict, nil
}
}
return work.ConflictSet{}, ErrConflictUnresolved
}
+97
View File
@@ -0,0 +1,97 @@
package domain
import (
"errors"
"slices"
"git.cin.su/m/Assistant/contracts/work"
)
var ErrInvalidDevelopmentGate = errors.New("invalid governed self-development gate")
func AdvanceDevelopmentGate(current work.DevelopmentGate, next work.DevelopmentStage) (work.DevelopmentGate, error) {
if err := ValidateDevelopmentGate(current); err != nil {
return work.DevelopmentGate{}, err
}
if !validDevelopmentTransition(current.Stage, next) {
return work.DevelopmentGate{}, ErrInvalidDevelopmentGate
}
current.Stage = next
switch next {
case work.DevelopmentProposalCreated:
if current.ProposalRef == "" {
return work.DevelopmentGate{}, ErrInvalidDevelopmentGate
}
case work.DevelopmentTaskAdmitted:
if current.TaskPacketRef == "" {
return work.DevelopmentGate{}, ErrInvalidDevelopmentGate
}
case work.DevelopmentIsolated:
if current.WorktreeRef == "" {
return work.DevelopmentGate{}, ErrInvalidDevelopmentGate
}
case work.DevelopmentVerified:
if current.VerificationEvidence == "" || len(current.VerifierRunRefs) == 0 || len(current.ProducerRunRefs) == 0 || sameRuns(current.ProducerRunRefs, current.VerifierRunRefs) {
return work.DevelopmentGate{}, ErrInvalidDevelopmentGate
}
case work.DevelopmentCanary:
if current.CanaryEvidence == "" {
return work.DevelopmentGate{}, ErrInvalidDevelopmentGate
}
case work.DevelopmentPromoted:
if current.PromotionRef == "" {
return work.DevelopmentGate{}, ErrInvalidDevelopmentGate
}
case work.DevelopmentRolledBack:
if current.RollbackRef == "" {
return work.DevelopmentGate{}, ErrInvalidDevelopmentGate
}
}
return current, nil
}
func ValidateDevelopmentGate(value work.DevelopmentGate) error {
if value.ID == "" || len(value.EvidenceRefs) == 0 || !validDevelopmentStage(value.Stage) || duplicateStrings(value.EvidenceRefs) {
return ErrInvalidDevelopmentGate
}
return nil
}
func validDevelopmentTransition(current, next work.DevelopmentStage) bool {
if current == work.DevelopmentCanary && next == work.DevelopmentRolledBack {
return true
}
if current == work.DevelopmentPromoted || current == work.DevelopmentRolledBack {
return false
}
order := []work.DevelopmentStage{work.DevelopmentEvidenceCollected, work.DevelopmentProposalCreated, work.DevelopmentTaskAdmitted, work.DevelopmentIsolated, work.DevelopmentVerified, work.DevelopmentCanary, work.DevelopmentPromoted}
index := func(value work.DevelopmentStage) int { return slices.Index(order, value) }
return index(next) == index(current)+1
}
func validDevelopmentStage(value work.DevelopmentStage) bool {
return slices.Contains([]work.DevelopmentStage{work.DevelopmentEvidenceCollected, work.DevelopmentProposalCreated, work.DevelopmentTaskAdmitted, work.DevelopmentIsolated, work.DevelopmentVerified, work.DevelopmentCanary, work.DevelopmentPromoted, work.DevelopmentRolledBack}, value)
}
func duplicateStrings(values []string) bool {
seen := map[string]bool{}
for _, value := range values {
if value == "" || seen[value] {
return true
}
seen[value] = true
}
return false
}
func sameRuns(left, right []work.AgentRunID) bool {
if len(left) != len(right) {
return false
}
for _, value := range left {
if !slices.Contains(right, value) {
return false
}
}
return true
}
@@ -0,0 +1,28 @@
package domain
import (
"testing"
"git.cin.su/m/Assistant/contracts/work"
)
func TestGovernedSelfDevelopmentRequiresIndependentVerifierBeforeCanary(t *testing.T) {
gate := work.DevelopmentGate{ID: "dev-1", Stage: work.DevelopmentEvidenceCollected, EvidenceRefs: []string{"obs-1"}, ProposalRef: "proposal-1", TaskPacketRef: "task-1", WorktreeRef: "worktree-1", ProducerRunRefs: []work.AgentRunID{"producer"}, VerifierRunRefs: []work.AgentRunID{"verifier"}, VerificationEvidence: "verify-1", CanaryEvidence: "canary-1", PromotionRef: "promotion-1"}
for _, stage := range []work.DevelopmentStage{work.DevelopmentProposalCreated, work.DevelopmentTaskAdmitted, work.DevelopmentIsolated, work.DevelopmentVerified, work.DevelopmentCanary, work.DevelopmentPromoted} {
var err error
gate, err = AdvanceDevelopmentGate(gate, stage)
if err != nil {
t.Fatalf("stage %s: %v", stage, err)
}
}
if _, err := AdvanceDevelopmentGate(gate, work.DevelopmentRolledBack); err == nil {
t.Fatal("promoted gate must be terminal")
}
}
func TestGovernedSelfDevelopmentRejectsSameProducerAndVerifier(t *testing.T) {
gate := work.DevelopmentGate{ID: "dev-1", Stage: work.DevelopmentIsolated, EvidenceRefs: []string{"obs-1"}, ProducerRunRefs: []work.AgentRunID{"same"}, VerifierRunRefs: []work.AgentRunID{"same"}, VerificationEvidence: "verify-1"}
if _, err := AdvanceDevelopmentGate(gate, work.DevelopmentVerified); err == nil {
t.Fatal("same run cannot verify its own output")
}
}
+15
View File
@@ -0,0 +1,15 @@
package ports
import (
"context"
"git.cin.su/m/Assistant/contracts/work"
)
type WorkerStore interface {
ExecutionStore
LoadDispatchIntent(context.Context, string) (work.AgentDispatchIntent, error)
ClaimNextDispatch(context.Context, string, int64) (work.AgentDispatchQueueItem, error)
CompleteDispatch(context.Context, string, string, work.AgentDispatchQueueState, int64) error
RecoverStaleClaims(context.Context, int64, int64) (int64, error)
}
@@ -0,0 +1,20 @@
package agent_execution_test
import (
"os"
"strings"
"testing"
)
func TestGraphPersistenceMigrationIsRevisionScoped(t *testing.T) {
data, err := os.ReadFile("../../database/migrations/031_work_graph_persistence.sql")
if err != nil {
t.Fatal(err)
}
sql := string(data)
for _, marker := range []string{"work.graphs", "work.graph_nodes", "work.graph_edges", "PRIMARY KEY (graph_id, revision)", "FOREIGN KEY (graph_id, revision)"} {
if !strings.Contains(sql, marker) {
t.Fatalf("graph persistence marker missing: %s", marker)
}
}
}
+58
View File
@@ -0,0 +1,58 @@
package agent_execution_test
import (
"testing"
"git.cin.su/m/Assistant/contracts/work"
workdomain "git.cin.su/m/Assistant/modules/work/domain"
)
func TestWorkGraphRejectsCycleAndOrdersReadyNodes(t *testing.T) {
graph := work.WorkGraph{ID: "graph-1", Revision: 1, State: work.GraphAdmitted, Budget: work.GraphBudget{MaxParallel: 2, MaxAttempts: 2}, Nodes: []work.GraphNode{
{ID: "node-b", IdempotencyKey: "key-b", State: work.GraphNodeReady},
{ID: "node-a", IdempotencyKey: "key-a", State: work.GraphNodeReady},
{ID: "node-c", IdempotencyKey: "key-c", State: work.GraphNodeBlocked},
}, Edges: []work.GraphEdge{{From: "node-a", To: "node-c"}}}
ready, err := workdomain.ReadyGraphNodes(graph)
if err != nil || len(ready) != 2 || ready[0] != "node-a" || ready[1] != "node-b" {
t.Fatalf("unexpected ready nodes: %#v %v", ready, err)
}
graph.Edges = append(graph.Edges, work.GraphEdge{From: "node-c", To: "node-a"})
if err := workdomain.ValidateWorkGraph(graph); err != workdomain.ErrGraphCycle {
t.Fatalf("expected cycle rejection, got %v", err)
}
}
func TestWorkGraphKeepsDependentNodeBlocked(t *testing.T) {
graph := work.WorkGraph{ID: "graph-1", Revision: 1, State: work.GraphRunning, Budget: work.GraphBudget{MaxParallel: 1, MaxAttempts: 1}, Nodes: []work.GraphNode{
{ID: "source", IdempotencyKey: "source-key", State: work.GraphNodeRunning},
{ID: "dependent", IdempotencyKey: "dependent-key", State: work.GraphNodeBlocked},
}, Edges: []work.GraphEdge{{From: "source", To: "dependent"}}}
ready, err := workdomain.ReadyGraphNodes(graph)
if err != nil || len(ready) != 0 {
t.Fatalf("dependent node became ready early: %#v %v", ready, err)
}
}
func TestGraphSchedulerEnforcesParallelBudgetAndExplicitConflict(t *testing.T) {
graph := work.WorkGraph{ID: "graph-1", Revision: 1, State: work.GraphDraft, Budget: work.GraphBudget{MaxParallel: 2, MaxAttempts: 2}, Nodes: []work.GraphNode{
{ID: "b", IdempotencyKey: "b-key", State: work.GraphNodeReady},
{ID: "a", IdempotencyKey: "a-key", State: work.GraphNodeReady},
}}
admitted, err := workdomain.AdmitGraph(graph)
if err != nil || admitted.State != work.GraphAdmitted {
t.Fatalf("graph was not admitted: %#v %v", admitted, err)
}
ready, err := workdomain.ScheduleReadyNodes(admitted, 1)
if err != nil || len(ready) != 1 || ready[0] != "a" {
t.Fatalf("scheduler ignored parallel budget/order: %#v %v", ready, err)
}
conflict := work.ConflictSet{ID: "conflict-1", GraphID: "graph-1", GraphRevision: 1, NodeID: "a", CandidateIDs: []string{"candidate-b", "candidate-a"}}
resolved, err := workdomain.ResolveGraphConflict(conflict, "candidate-a")
if err != nil || !resolved.Resolved || resolved.CandidateIDs[0] != "candidate-a" {
t.Fatalf("conflict was not explicitly resolved: %#v %v", resolved, err)
}
if _, err := workdomain.ResolveGraphConflict(conflict, "unknown"); err != workdomain.ErrConflictUnresolved {
t.Fatalf("unknown candidate was accepted: %v", err)
}
}
+20
View File
@@ -0,0 +1,20 @@
package agent_execution_test
import (
"os"
"strings"
"testing"
)
func TestWorkerRecoveryAndClaimUseDurableQueueStatements(t *testing.T) {
data, err := os.ReadFile("../../adapters/postgres/work_repository.go")
if err != nil {
t.Fatal(err)
}
source := string(data)
for _, marker := range []string{"FOR UPDATE SKIP LOCKED", "state='CLAIMED'", "state='QUEUED'", "RecoverStaleClaims"} {
if !strings.Contains(source, marker) {
t.Fatalf("worker persistence marker missing: %s", marker)
}
}
}
@@ -0,0 +1,18 @@
package cognitive_registry_test
import (
"testing"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/modules/capability/domain"
)
func TestFleetPolicyRequiresAnExplicitReadyFallback(t *testing.T) {
readiness, err := domain.BuildFleetReadiness(1, []capability.FleetMember{{ProviderInstanceID: "cpu", ModelDescriptorID: "model", Tier: capability.FleetTierFast, Ready: false, Reason: "401"}})
if err != nil {
t.Fatal(err)
}
if _, err := domain.SelectFleetTier(readiness, capability.FleetTierFast); err == nil {
t.Fatal("expected no invocable fallback")
}
}
@@ -0,0 +1,54 @@
package creator_web_test
import (
"os"
"strings"
"testing"
)
func TestCreatorCabinetHasAuthenticatedOperationalAndLearningSurfaces(t *testing.T) {
page, err := os.ReadFile("../../cmd/creator-web/static/index.html")
if err != nil {
t.Fatal(err)
}
markup := string(page)
for _, marker := range []string{
`<form id="login-form"`, `aria-labelledby="diagnostics-title"`, `data-page="operations"`,
`id="learning"`, `id="reflection-list"`, `id="skill-list"`, `id="settings"`,
`id="logout"`, `role="alert"`, `aria-live="polite"`,
} {
if !strings.Contains(markup, marker) {
t.Fatalf("cabinet missing %q", marker)
}
}
}
func TestCreatorCabinetRoutesSessionAndLearningThroughAuthenticatedAPI(t *testing.T) {
script, err := os.ReadFile("../../cmd/creator-web/static/assets/console.js")
if err != nil {
t.Fatal(err)
}
code := string(script)
for _, marker := range []string{
`request("/api/session/current")`, `request("/api/learning/recent?limit=8")`,
`request("/api/advisory-policy")`, `request("/api/capabilities/cognitive-proposal/health")`, `request("/api/learning/recent?limit=1")`, `sessionStorage.getItem("domovoy.creator.session")`,
`if (response.status === 401`,
} {
if !strings.Contains(code, marker) {
t.Fatalf("console missing authenticated flow %q", marker)
}
}
}
func TestCreatorCabinetResponsiveAndReducedMotionRulesExist(t *testing.T) {
style, err := os.ReadFile("../../cmd/creator-web/static/assets/console.css")
if err != nil {
t.Fatal(err)
}
css := string(style)
for _, marker := range []string{"@media (max-width: 680px)", "@media (prefers-reduced-motion: reduce)", ".learning-grid { grid-template-columns: 1fr; }"} {
if !strings.Contains(css, marker) {
t.Fatalf("console missing responsive rule %q", marker)
}
}
}
+16 -8
View File
@@ -37,6 +37,19 @@ function Post-CoreJson([string]$url, [object]$body) {
}
}
function Get-ContainerSecret([object[]]$environment, [string]$name) {
$fileEntry = $environment | Where-Object { $_ -like "${name}_FILE=*" } | Select-Object -First 1
if ($fileEntry) {
$path = $fileEntry.Substring("${name}_FILE=".Length)
$value = docker --context $DockerContext exec $coreContainer sh -c "cat -- '$path'"
if ($LASTEXITCODE -ne 0 -or -not $value) { throw "$name file-backed secret is not readable" }
return ($value -join "`n").TrimEnd("`r", "`n")
}
$entry = $environment | Where-Object { $_ -like "${name}=*" } | Select-Object -First 1
if (-not $entry) { throw "$name is not configured" }
return $entry.Substring("${name}=".Length)
}
$health = Invoke-RestMethod -Uri "$BaseURL/healthz" -Method Get
if ($health.status -ne "ok" -or $health.runtime -ne "creator-web") { throw "Creator web health response is invalid" }
@@ -52,19 +65,14 @@ $invalid = Invoke-WebRequest -Uri "$BaseURL/api/session/current" -Headers @{Auth
if ($invalid.StatusCode -ne 401) { throw "Invalid Creator session was not denied" }
$environment = docker --context $DockerContext inspect --format '{{range .Config.Env}}{{println .}}{{end}}' $coreContainer
$usernameEntry = $environment | Where-Object { $_ -like "CREATOR_USERNAME=*" } | Select-Object -First 1
$passwordEntry = $environment | Where-Object { $_ -like "CREATOR_PASSWORD=*" } | Select-Object -First 1
if (-not $usernameEntry -or -not $passwordEntry) { throw "Creator username/password is not configured" }
$creatorUsername = $usernameEntry.Substring("CREATOR_USERNAME=".Length)
$creatorPassword = $passwordEntry.Substring("CREATOR_PASSWORD=".Length)
$creatorUsername = Get-ContainerSecret $environment "CREATOR_USERNAME"
$creatorPassword = Get-ContainerSecret $environment "CREATOR_PASSWORD"
$invalidCredentials = Invoke-WebRequest -Uri "$BaseURL/api/session" -Method Post -ContentType "application/json" -Body (@{username=$creatorUsername;password="invalid"} | ConvertTo-Json -Compress) -SkipHttpErrorCheck
if ($invalidCredentials.StatusCode -ne 401) { throw "Invalid Creator credentials were not denied" }
$issued = Invoke-RestMethod -Uri "$BaseURL/api/session" -Method Post -ContentType "application/json" -Body (@{username=$creatorUsername;password=$creatorPassword} | ConvertTo-Json -Compress)
if (-not $issued.Token) { throw "Creator username/password session was not issued through the gateway" }
$bootstrapEntry = $environment | Where-Object { $_ -like "CREATOR_BOOTSTRAP_TOKEN=*" } | Select-Object -First 1
if (-not $bootstrapEntry) { throw "Creator bootstrap token is not configured" }
$bootstrapToken = $bootstrapEntry.Substring("CREATOR_BOOTSTRAP_TOKEN=".Length)
$bootstrapToken = Get-ContainerSecret $environment "CREATOR_BOOTSTRAP_TOKEN"
$recoveryIssued = Invoke-RestMethod -Uri "$BaseURL/api/session" -Method Post -Headers @{Authorization="Bearer $bootstrapToken"}
if (-not $recoveryIssued.Token) { throw "Creator recovery session was not issued through the gateway" }
$current = Invoke-RestMethod -Uri "$BaseURL/api/session/current" -Method Get -Headers @{Authorization="Bearer $($issued.Token)"}
@@ -0,0 +1,27 @@
package research_contracts_test
import (
"os"
"strings"
"testing"
)
func TestReflectionCandidateLeaseSQLIsDurableAndRecoverable(t *testing.T) {
checks := map[string][]string{
"../../database/migrations/032_reflection_candidate_leases.sql": {"claim_expires_at_unix_millis", "reflection_candidates_claimable_idx"},
"../../adapters/postgres/epistemics_repository.go": {"claim_expires_at_unix_millis", "FOR UPDATE SKIP LOCKED", "state='OPEN'", "state='RESOLVED'"},
"../../cmd/reflection-worker/main.go": {"RecoverReflectionClaims", "ClaimReflectionCandidate", "ResolveReflectionCandidate"},
}
for path, needles := range checks {
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
text := string(data)
for _, needle := range needles {
if !strings.Contains(text, needle) {
t.Fatalf("%s missing %q", path, needle)
}
}
}
}
@@ -0,0 +1,27 @@
package research_contracts_test
import (
"testing"
"git.cin.su/m/Assistant/contracts/epistemics"
episdomain "git.cin.su/m/Assistant/modules/epistemics/domain"
)
func TestCompileObservedClaimPreservesEvidenceChainAndConflict(t *testing.T) {
source := epistemics.Source{ID: "source-1", Locator: "https://example.test/source"}
snapshot := epistemics.SourceSnapshot{ID: "snapshot-1", SourceID: source.ID, ContentDigest: "sha256:test", CapturedAtUnixMillis: 100, Freshness: epistemics.FreshnessCurrent}
excerpt := epistemics.SourceExcerpt{ID: "excerpt-1", SnapshotID: snapshot.ID, Text: "exact observation", Start: 0, End: 17}
bundle, err := episdomain.CompileObservedClaim(source, snapshot, excerpt, "The observed fact is true", 200, epistemics.ConflictPresent)
if err != nil || bundle.Claim.ObservationID != bundle.Observation.ID || bundle.Belief.State != epistemics.BeliefConflicted || bundle.Citation.Evidence != epistemics.EvidenceObserved {
t.Fatalf("invalid truth bundle: %#v %v", bundle, err)
}
}
func TestCompileObservedClaimRejectsBrokenSourceChain(t *testing.T) {
source := epistemics.Source{ID: "source-1", Locator: "https://example.test/source"}
snapshot := epistemics.SourceSnapshot{ID: "snapshot-1", SourceID: "other-source", ContentDigest: "sha256:test", CapturedAtUnixMillis: 100, Freshness: epistemics.FreshnessCurrent}
excerpt := epistemics.SourceExcerpt{ID: "excerpt-1", SnapshotID: snapshot.ID, Text: "exact observation", Start: 0, End: 17}
if _, err := episdomain.CompileObservedClaim(source, snapshot, excerpt, "statement", 200, epistemics.ConflictNone); err != episdomain.ErrInvalidTruthBundle {
t.Fatalf("broken chain was accepted: %v", err)
}
}
@@ -0,0 +1,66 @@
package research_runtime_test
import (
"context"
"testing"
"git.cin.su/m/Assistant/contracts/epistemics"
app "git.cin.su/m/Assistant/modules/epistemics/application"
domain "git.cin.su/m/Assistant/modules/epistemics/domain"
)
type connector struct {
candidates []epistemics.ResearchCandidate
}
func (c connector) Collect(context.Context, epistemics.ResearchPlan) ([]epistemics.ResearchCandidate, error) {
return c.candidates, nil
}
func candidate(statement string, freshness epistemics.FreshnessState, source, snapshot, excerpt string) epistemics.ResearchCandidate {
return epistemics.ResearchCandidate{PropositionKey: "domovoy", Statement: statement, Evidence: epistemics.EvidenceObserved,
Coverage: epistemics.CoverageComplete, Conflict: epistemics.ConflictNone,
Source: epistemics.Source{ID: epistemics.SourceID(source), Locator: "https://example.test/" + source},
Snapshot: epistemics.SourceSnapshot{ID: epistemics.SourceSnapshotID(snapshot), SourceID: epistemics.SourceID(source), ContentDigest: "sha256:" + snapshot, CapturedAtUnixMillis: 10, Freshness: freshness},
Excerpt: epistemics.SourceExcerpt{ID: epistemics.SourceExcerptID(excerpt), SnapshotID: epistemics.SourceSnapshotID(snapshot), Text: statement, Start: 0, End: int64(len(statement))}}
}
func TestResearchServiceKeepsUnknownStaleAndConflictExplicit(t *testing.T) {
plan := epistemics.ResearchPlan{ID: "plan-1", Question: "question", SourceClasses: []string{"repository"}, FreshnessPolicy: "current"}
unknown, err := (app.ResearchService{Connector: connector{}}).Evaluate(context.Background(), plan, 100)
if err != nil || len(unknown.Findings) != 1 || unknown.Findings[0].State != epistemics.FindingUnknown {
t.Fatalf("unknown evidence was not explicit: %#v %v", unknown, err)
}
result, err := (app.ResearchService{Connector: connector{candidates: []epistemics.ResearchCandidate{
candidate("one answer", epistemics.FreshnessStale, "s1", "ss1", "e1"),
candidate("another answer", epistemics.FreshnessCurrent, "s2", "ss2", "e2"),
}}}).Evaluate(context.Background(), plan, 100)
if err != nil || len(result.Findings) != 1 || result.Findings[0].State != epistemics.FindingConflicted || len(result.Findings[0].Citations) != 2 {
t.Fatalf("conflict was not retained: %#v %v", result, err)
}
}
func TestEvaluateResearchRejectsFabricatedEvidenceChain(t *testing.T) {
value := candidate("answer", epistemics.FreshnessCurrent, "s1", "ss1", "e1")
value.Excerpt.SnapshotID = "other-snapshot"
plan := epistemics.ResearchPlan{ID: "plan-1", Question: "question", SourceClasses: []string{"web"}, FreshnessPolicy: "current"}
if _, err := domain.EvaluateResearch(plan, []epistemics.ResearchCandidate{value}, 100); err != domain.ErrInvalidResearchCandidate {
t.Fatalf("fabricated evidence was accepted: %v", err)
}
}
func TestResearchServiceKeepsAssumptionAndUnknownFreshnessVisible(t *testing.T) {
plan := epistemics.ResearchPlan{ID: "plan-1", Question: "question", SourceClasses: []string{"web"}, FreshnessPolicy: "current"}
value := candidate("assumed answer", epistemics.FreshnessCurrent, "s1", "ss1", "e1")
value.Evidence = epistemics.EvidenceAssumed
result, err := (app.ResearchService{Connector: connector{candidates: []epistemics.ResearchCandidate{value}}}).Evaluate(context.Background(), plan, 100)
if err != nil || result.Findings[0].State != epistemics.FindingUnknown {
t.Fatalf("assumption was promoted: %#v %v", result, err)
}
value = candidate("unknown answer", epistemics.FreshnessUnknown, "s2", "ss2", "e2")
value.Conflict = epistemics.ConflictUnknown
result, err = (app.ResearchService{Connector: connector{candidates: []epistemics.ResearchCandidate{value}}}).Evaluate(context.Background(), plan, 100)
if err != nil || result.Findings[0].State != epistemics.FindingUnknown {
t.Fatalf("unknown freshness was promoted: %#v %v", result, err)
}
}
@@ -0,0 +1,15 @@
package semantic_evolution_test
import (
"testing"
"git.cin.su/m/Assistant/contracts/work"
workdomain "git.cin.su/m/Assistant/modules/work/domain"
)
func TestSelfDevelopmentRollbackIsAvailableOnlyFromCanary(t *testing.T) {
gate := work.DevelopmentGate{ID: "dev-1", Stage: work.DevelopmentCanary, EvidenceRefs: []string{"obs"}, CanaryEvidence: "canary", RollbackRef: "rollback"}
if _, err := workdomain.AdvanceDevelopmentGate(gate, work.DevelopmentRolledBack); err != nil {
t.Fatal(err)
}
}