Compare commits

...
Author SHA1 Message Date
m 924d8c3767 DEV-000068 activate runtime experience loop
CI / test (push) Successful in 18s
CI / test (pull_request) Successful in 16s
2026-08-17 18:35:46 +03:00
m 67ff1f73cd Merge DEV-000067 final production status
CI / test (push) Successful in 17s
DEV-000067 status promoted
2026-08-17 15:17:19 +00:00
13 changed files with 789 additions and 10 deletions
@@ -246,6 +246,53 @@ func (r *CapabilityRepository) SaveSkillProfile(ctx context.Context, value capab
return tx.Commit(ctx)
}
func (r *CapabilityRepository) LoadSkillProfile(ctx context.Context, id capability.SkillProfileID) (capability.SkillProfile, error) {
var value capability.SkillProfile
err := r.pool.QueryRow(ctx, `SELECT
p.skill_profile_id,p.revision,p.name,p.description,p.applicable_context,p.implementation_ref,p.implementation_version,p.status,
p.supporting_experience_refs,p.known_failure_experience_refs,p.confidence_millis,p.last_validated_unix_millis,
p.observed_failure_count,p.observed_correction_count,p.recorded_at_unix_millis
FROM capability.skill_profile_heads h
JOIN capability.skill_profiles p ON p.skill_profile_id=h.skill_profile_id AND p.revision=h.revision
WHERE p.skill_profile_id=$1`, id).Scan(&value.ID, &value.Revision, &value.Name, &value.Description, &value.ApplicableContext,
&value.ImplementationRef, &value.ImplementationVersion, &value.Status, &value.SupportingExperienceRefs,
&value.KnownFailureExperienceRefs, &value.ConfidenceMillis, &value.LastValidatedUnixMillis,
&value.ObservedFailureCount, &value.ObservedCorrectionCount, &value.RecordedAtUnixMillis)
if err != nil {
return capability.SkillProfile{}, fmt.Errorf("load skill profile: %w", err)
}
return value, nil
}
func (r *CapabilityRepository) RecentSkillProfiles(ctx context.Context, limit int) ([]capability.SkillProfile, error) {
if limit < 1 || limit > 100 {
return nil, fmt.Errorf("invalid skill profile limit")
}
rows, err := r.pool.Query(ctx, `SELECT
p.skill_profile_id,p.revision,p.name,p.description,p.applicable_context,p.implementation_ref,p.implementation_version,p.status,
p.supporting_experience_refs,p.known_failure_experience_refs,p.confidence_millis,p.last_validated_unix_millis,
p.observed_failure_count,p.observed_correction_count,p.recorded_at_unix_millis
FROM capability.skill_profile_heads h
JOIN capability.skill_profiles p ON p.skill_profile_id=h.skill_profile_id AND p.revision=h.revision
ORDER BY h.updated_at DESC, p.skill_profile_id LIMIT $1`, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var result []capability.SkillProfile
for rows.Next() {
var value capability.SkillProfile
if err := rows.Scan(&value.ID, &value.Revision, &value.Name, &value.Description, &value.ApplicableContext,
&value.ImplementationRef, &value.ImplementationVersion, &value.Status, &value.SupportingExperienceRefs,
&value.KnownFailureExperienceRefs, &value.ConfidenceMillis, &value.LastValidatedUnixMillis,
&value.ObservedFailureCount, &value.ObservedCorrectionCount, &value.RecordedAtUnixMillis); err != nil {
return nil, err
}
result = append(result, value)
}
return result, rows.Err()
}
func (r *CapabilityRepository) LoadBinding(ctx context.Context, id capability.CapabilityBindingID, revision int64) (capability.Binding, error) {
var binding capability.Binding
err := r.pool.QueryRow(ctx, `SELECT binding_id,capability_id,owner_principal_id,revision,state,https_origin,delivery_path,receipt_path,
@@ -31,6 +31,58 @@ func (r *EpistemicsRepository) SaveSystemExpectation(ctx context.Context, value
return err
}
func (r *EpistemicsRepository) RecentExperienceRecords(ctx context.Context, limit int) ([]epistemics.ExperienceRecord, error) {
if limit < 1 || limit > 100 {
return nil, fmt.Errorf("invalid experience limit")
}
rows, err := r.pool.Query(ctx, `SELECT
experience_id,source_kind,original_intent_id,source_utterance_id,context_refs,help_method,help_method_ref,
capability_implementation_ref,capability_usage_observation_ref,capability_gap_ref,skill_ref,expectation_id,
expected_outcome,actual_outcome,outcome_ref,outcome_detail,human_response_ref,correction_utterance_id,
verification_evidence,confidence_millis,recorded_at_unix_millis
FROM epistemics.experience_records ORDER BY recorded_at_unix_millis DESC, experience_id DESC LIMIT $1`, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var result []epistemics.ExperienceRecord
for rows.Next() {
var value epistemics.ExperienceRecord
if err := rows.Scan(&value.ID, &value.SourceKind, &value.OriginalIntentID, &value.SourceUtteranceID, &value.ContextRefs, &value.HelpMethod, &value.HelpMethodRef,
&value.CapabilityImplementationRef, &value.CapabilityUsageObservationRef, &value.CapabilityGapRef, &value.SkillRef, &value.ExpectationID,
&value.ExpectedOutcome, &value.ActualOutcome, &value.OutcomeRef, &value.OutcomeDetail, &value.HumanResponseRef, &value.CorrectionUtteranceID,
&value.VerificationEvidence, &value.ConfidenceMillis, &value.RecordedAtUnixMillis); err != nil {
return nil, err
}
result = append(result, value)
}
return result, rows.Err()
}
func (r *EpistemicsRepository) RecentReflectionCandidates(ctx context.Context, limit int) ([]epistemics.ReflectionCandidate, error) {
if limit < 1 || limit > 100 {
return nil, fmt.Errorf("invalid reflection limit")
}
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
FROM epistemics.reflection_candidates ORDER BY recorded_at_unix_millis DESC, reflection_candidate_id DESC LIMIT $1`, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var result []epistemics.ReflectionCandidate
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 {
return nil, err
}
result = append(result, value)
}
return result, rows.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,
+6 -3
View File
@@ -144,10 +144,10 @@ func resolveChatCapability(ctx context.Context, repository *postgres.CapabilityR
return resolution, err
}
func observeCapabilityUsage(ctx context.Context, repository *postgres.CapabilityRepository, selected capability.ResolvedCapabilityImplementation, requirement capability.CapabilityResolutionRequirement, outcome capability.UsageOutcomeClass, outcomeRef string, started time.Time, now time.Time) {
func observeCapabilityUsage(ctx context.Context, repository *postgres.CapabilityRepository, selected capability.ResolvedCapabilityImplementation, requirement capability.CapabilityResolutionRequirement, outcome capability.UsageOutcomeClass, outcomeRef string, started time.Time, now time.Time) (capability.CapabilityUsageObservationID, error) {
id, err := ids.New()
if err != nil {
return
return "", err
}
latency := now.Sub(started).Milliseconds()
observation := capability.CapabilityUsageObservation{
@@ -164,7 +164,10 @@ func observeCapabilityUsage(ctx context.Context, repository *postgres.Capability
LatencyMillis: &latency,
ObservedAtUnixMillis: now.UnixMilli(),
}
_ = repository.SaveCapabilityUsageObservation(ctx, observation)
if err := repository.SaveCapabilityUsageObservation(ctx, observation); err != nil {
return "", err
}
return observation.ID, nil
}
func buildReminderRequest(message string, session identity.CreatorSession, intent interaction.OriginalHumanIntent, utterance interaction.HumanUtterance, now time.Time) (continuity.ScheduleReminderRequest, error) {
+314
View File
@@ -0,0 +1,314 @@
package main
import (
"context"
"strings"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/interaction"
capabilitydomain "git.cin.su/m/Assistant/modules/capability/domain"
epistemicsdomain "git.cin.su/m/Assistant/modules/epistemics/domain"
)
const reminderResponseSkillID = capability.SkillProfileID("skill-obvious-reminder-response-v1")
type learningEpistemicsStore interface {
SaveSystemExpectation(context.Context, epistemics.SystemExpectation) error
SaveExperienceRecord(context.Context, epistemics.ExperienceRecord) error
SaveReflectionCandidate(context.Context, epistemics.ReflectionCandidate) error
}
type learningCapabilityStore interface {
LoadSkillProfile(context.Context, capability.SkillProfileID) (capability.SkillProfile, error)
SaveSkillProfile(context.Context, capability.SkillProfile) error
}
type learningRecorder struct {
epistemics learningEpistemicsStore
capability capabilityLearningStore
}
type capabilityLearningStore interface {
learningCapabilityStore
}
type reminderResponseLearningInput struct {
Utterance interaction.HumanUtterance
Intent interaction.OriginalHumanIntent
Result continuity.HumanResponseResult
At time.Time
}
type correctionLearningInput struct {
Utterance interaction.HumanUtterance
Intent interaction.OriginalHumanIntent
At time.Time
}
type fastPathDeclineLearningInput struct {
Utterance interaction.HumanUtterance
Intent interaction.OriginalHumanIntent
Result continuity.HumanResponseResult
Reason capability.FastPathDeclineReason
At time.Time
}
type capabilityLearningInput struct {
Utterance interaction.HumanUtterance
Intent interaction.OriginalHumanIntent
ImplementationID capability.CapabilityImplementationID
UsageObservation capability.CapabilityUsageObservationID
GapID capability.CapabilityGapID
Outcome capability.UsageOutcomeClass
OutcomeRef string
At time.Time
}
func (r learningRecorder) recordReminderResponse(ctx context.Context, input reminderResponseLearningInput) {
if r.epistemics == nil || r.capability == nil || input.Intent.ID == "" || input.Utterance.ID == "" || input.Result.Response.ID == "" {
return
}
expectedEffect := "resolve obvious reminder continuation using deterministic target resolver"
if input.Result.Response.TargetProspectiveID != "" {
expectedEffect += " for prospective item " + string(input.Result.Response.TargetProspectiveID)
}
expectation := epistemics.SystemExpectation{
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
ExpectedOutcome: epistemics.OutcomeSuccess,
ExpectedEffect: expectedEffect,
ConfidenceMillis: 850,
EvidenceBasis: []epistemics.EvidenceRef{"continuity:response-target-resolution"},
RecordedAtUnixMillis: input.At.UnixMilli(),
}
expectation, err := epistemicsdomain.NewSystemExpectation(expectation)
if err != nil || r.epistemics.SaveSystemExpectation(ctx, expectation) != nil {
return
}
actual, confidence := outcomeForHumanResponse(input.Result.Response)
experience := epistemics.ExperienceRecord{
SourceKind: epistemics.ExperienceSourceContinuity,
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
ContextRefs: contextRefs(
"continuity:human-response:"+string(input.Result.Response.ID),
"continuity:prospective:"+string(input.Result.Response.TargetProspectiveID),
"continuity:attention:"+string(input.Result.Response.TargetAttentionID),
),
HelpMethod: epistemics.HelpMethodDeterministicFastPath,
HelpMethodRef: "continuity:human-response-target-resolver-v1",
SkillRef: epistemics.SkillRef(reminderResponseSkillID),
ExpectationID: expectation.ID,
ExpectedOutcome: expectation.ExpectedOutcome,
ActualOutcome: actual,
OutcomeRef: "continuity:human-response:" + string(input.Result.Response.ID),
OutcomeDetail: string(input.Result.Response.Operation) + " -> " + string(input.Result.Response.Outcome),
HumanResponseRef: epistemics.HumanResponseRef(input.Result.Response.ID),
VerificationEvidence: []epistemics.EvidenceRef{epistemics.EvidenceRef("human-response:" + string(input.Result.Response.Outcome))},
ConfidenceMillis: confidence,
RecordedAtUnixMillis: input.At.UnixMilli(),
}
r.saveExperienceAndReflection(ctx, experience)
r.updateReminderSkill(ctx, experience, skillOutcomeForExperience(experience), false)
}
func (r learningRecorder) recordCorrection(ctx context.Context, input correctionLearningInput) {
if r.epistemics == nil || input.Intent.ID == "" || input.Utterance.ID == "" {
return
}
experience := epistemics.ExperienceRecord{
SourceKind: epistemics.ExperienceSourceInteraction,
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
HelpMethod: epistemics.HelpMethodClarification,
SkillRef: epistemics.SkillRef(reminderResponseSkillID),
ExpectedOutcome: epistemics.OutcomeSuccess,
ActualOutcome: epistemics.OutcomeUserCorrected,
OutcomeDetail: "human correction indicates prior interpretation or target resolution may be wrong",
CorrectionUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
VerificationEvidence: []epistemics.EvidenceRef{"interaction:human-correction"},
ConfidenceMillis: 1000,
RecordedAtUnixMillis: input.At.UnixMilli(),
}
saved, ok := r.saveExperienceAndReflection(ctx, experience)
if ok {
r.updateReminderSkill(ctx, saved, capability.UsageFailed, true)
}
}
func (r learningRecorder) recordFastPathDecline(ctx context.Context, input fastPathDeclineLearningInput) {
if r.epistemics == nil || input.Intent.ID == "" || input.Utterance.ID == "" {
return
}
experience, err := epistemicsdomain.FastPathDeclineExperience(
epistemics.OriginalIntentRef(input.Intent.ID),
epistemics.SourceUtteranceRef(input.Utterance.ID),
epistemics.SkillRef(reminderResponseSkillID),
string(outcomeForDecline(input.Reason)),
input.At.UnixMilli(),
)
if err != nil {
return
}
if input.Result.Response.ID != "" {
experience.ContextRefs = contextRefs("continuity:response-candidate:" + string(input.Result.Response.Operation))
}
saved, ok := r.saveExperienceAndReflection(ctx, experience)
if ok {
r.updateReminderSkill(ctx, saved, capability.UsageSkipped, false)
}
}
func (r learningRecorder) recordCapabilityUse(ctx context.Context, input capabilityLearningInput) {
if r.epistemics == nil || input.Intent.ID == "" || input.Utterance.ID == "" {
return
}
actual := epistemics.OutcomeSuccess
switch input.Outcome {
case capability.UsageFailed:
actual = epistemics.OutcomeFailed
case capability.UsageSkipped:
actual = epistemics.OutcomeNotApplicable
case capability.UsageUnknown:
actual = epistemics.OutcomeUnknown
}
experience := epistemics.ExperienceRecord{
SourceKind: epistemics.ExperienceSourceCapability,
OriginalIntentID: epistemics.OriginalIntentRef(input.Intent.ID),
SourceUtteranceID: epistemics.SourceUtteranceRef(input.Utterance.ID),
HelpMethod: epistemics.HelpMethodCapabilityExecution,
CapabilityImplementationRef: epistemics.CapabilityImplementationRef(input.ImplementationID),
CapabilityUsageObservationRef: epistemics.CapabilityUsageObservationRef(input.UsageObservation),
CapabilityGapRef: epistemics.CapabilityGapRef(input.GapID),
ExpectedOutcome: epistemics.OutcomeSuccess,
ActualOutcome: actual,
OutcomeRef: input.OutcomeRef,
OutcomeDetail: "capability outcome " + string(input.Outcome),
VerificationEvidence: []epistemics.EvidenceRef{"capability:usage-observation"},
ConfidenceMillis: 900,
RecordedAtUnixMillis: input.At.UnixMilli(),
}
r.saveExperienceAndReflection(ctx, experience)
}
func (r learningRecorder) saveExperienceAndReflection(ctx context.Context, value epistemics.ExperienceRecord) (epistemics.ExperienceRecord, bool) {
value, err := epistemicsdomain.NewExperienceRecord(value)
if err != nil {
return epistemics.ExperienceRecord{}, false
}
if err := r.epistemics.SaveExperienceRecord(ctx, value); err != nil {
return epistemics.ExperienceRecord{}, false
}
if candidate, ok, err := epistemicsdomain.ReflectionForExperience(value); err == nil && ok {
_ = r.epistemics.SaveReflectionCandidate(ctx, candidate)
}
return value, true
}
func (r learningRecorder) updateReminderSkill(ctx context.Context, experience epistemics.ExperienceRecord, outcome capability.UsageOutcomeClass, corrected bool) {
if r.capability == nil || experience.ID == "" {
return
}
profile, err := r.capability.LoadSkillProfile(ctx, reminderResponseSkillID)
if err != nil {
return
}
next, err := capabilitydomain.ObserveSkillOutcome(profile, capability.SkillOutcomeObservation{
ExperienceRef: capability.SkillEvidenceRef("epistemics:experience:" + string(experience.ID)),
Outcome: outcome,
UserCorrected: corrected,
ObservedAtUnixMillis: experience.RecordedAtUnixMillis,
})
if err != nil {
return
}
if err := r.capability.SaveSkillProfile(ctx, next); err != nil {
return
}
}
func outcomeForHumanResponse(response continuity.HumanResponse) (epistemics.OutcomeClass, int64) {
if response.ResolutionState != continuity.ResponseResolved {
switch response.ResolutionState {
case continuity.ResponseAmbiguous:
return epistemics.OutcomeConflict, 850
case continuity.ResponseNoTarget:
return epistemics.OutcomeOutOfDistribution, 850
default:
return epistemics.OutcomeUnexpected, 700
}
}
switch response.Outcome {
case continuity.ResponseOutcomeUserReportedComplete, continuity.ResponseOutcomeSnoozed, continuity.ResponseOutcomeCancelled, continuity.ResponseOutcomeDismissed:
return epistemics.OutcomeSuccess, 950
case continuity.ResponseOutcomeClarificationNeeded:
return epistemics.OutcomeConflict, 850
default:
return epistemics.OutcomeUnknown, 500
}
}
func skillOutcomeForExperience(experience epistemics.ExperienceRecord) capability.UsageOutcomeClass {
switch experience.ActualOutcome {
case epistemics.OutcomeSuccess, epistemics.OutcomePartialSuccess:
return capability.UsageSucceeded
case epistemics.OutcomeFailed, epistemics.OutcomeUnexpected, epistemics.OutcomeUserCorrected:
return capability.UsageFailed
case epistemics.OutcomeUnknown:
return capability.UsageUnknown
default:
return capability.UsageSkipped
}
}
func outcomeForDecline(reason capability.FastPathDeclineReason) epistemics.OutcomeClass {
switch reason {
case capability.FastPathLowConfidence:
return epistemics.OutcomeLowConfidence
case capability.FastPathOutOfDistribution:
return epistemics.OutcomeOutOfDistribution
case capability.FastPathConflict:
return epistemics.OutcomeConflict
case capability.FastPathStale:
return epistemics.OutcomeStale
default:
return epistemics.OutcomeNotApplicable
}
}
func contextRefs(values ...string) []epistemics.ContextRef {
refs := make([]epistemics.ContextRef, 0, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" || strings.HasSuffix(value, ":") {
continue
}
refs = append(refs, epistemics.ContextRef(value))
}
return refs
}
func looksLikeUnsupportedContinuation(message string, result continuity.HumanResponseResult) bool {
lower := strings.ToLower(strings.TrimSpace(message))
if result.Handled {
return false
}
if result.Response.Operation == continuity.ResponseContinue || result.Response.Operation == continuity.ResponseReject {
return true
}
return strings.Contains(lower, "это") && (strings.Contains(lower, "напомин") || strings.Contains(lower, "перенеси") || strings.Contains(lower, "закрой") || strings.Contains(lower, "передвинь"))
}
func declineReasonForResponse(result continuity.HumanResponseResult) capability.FastPathDeclineReason {
switch result.Response.Operation {
case continuity.ResponseReject:
return capability.FastPathConflict
case continuity.ResponseContinue:
return capability.FastPathNotApplicable
default:
return capability.FastPathOutOfDistribution
}
}
+62 -2
View File
@@ -68,6 +68,19 @@ func main() {
log.Fatal(err)
}
defer capabilityRepository.Close()
epistemicsPassword, err := readCredentialRefSecret("EPISTEMICS_POSTGRES_PASSWORD", "EPISTEMICS_POSTGRES_CREDENTIAL_REF")
if err != nil {
log.Fatal(err)
}
epistemicsUser := os.Getenv("EPISTEMICS_POSTGRES_USER")
if epistemicsUser == "" {
epistemicsUser = "epistemics_role"
}
epistemicsRepository, err := postgres.OpenEpistemics(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: epistemicsUser, Password: epistemicsPassword})
if err != nil {
log.Fatal(err)
}
defer epistemicsRepository.Close()
identityRepository, err := postgres.OpenIdentity(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("IDENTITY_POSTGRES_USER"), Password: os.Getenv("IDENTITY_POSTGRES_PASSWORD")})
if err != nil {
log.Fatal(err)
@@ -94,6 +107,7 @@ func main() {
if err != nil {
log.Fatal(err)
}
learning := learningRecorder{epistemics: epistemicsRepository, capability: capabilityRepository}
mux := http.NewServeMux()
authenticateCreator := func(w http.ResponseWriter, r *http.Request) (identity.CreatorSession, bool) {
session, err := identitydomain.ValidateCreatorSession(sessionKey, bearerToken(r), time.Now())
@@ -172,6 +186,42 @@ func main() {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(status)
})
mux.HandleFunc("GET /v1/creator/learning/recent", func(w http.ResponseWriter, r *http.Request) {
if _, ok := authenticateCreator(w, r); !ok {
return
}
limit := 10
if raw := r.URL.Query().Get("limit"); raw != "" {
parsed, err := strconv.Atoi(raw)
if err != nil || parsed < 1 || parsed > 50 {
http.Error(w, "invalid learning limit", http.StatusBadRequest)
return
}
limit = parsed
}
experiences, err := epistemicsRepository.RecentExperienceRecords(r.Context(), limit)
if err != nil {
http.Error(w, "learning experience unavailable", http.StatusServiceUnavailable)
return
}
reflections, err := epistemicsRepository.RecentReflectionCandidates(r.Context(), limit)
if err != nil {
http.Error(w, "learning reflection unavailable", http.StatusServiceUnavailable)
return
}
skills, err := capabilityRepository.RecentSkillProfiles(r.Context(), limit)
if err != nil {
http.Error(w, "learning skills unavailable", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(struct {
Experiences []epistemics.ExperienceRecord
Reflections []epistemics.ReflectionCandidate
Skills []capability.SkillProfile
}{Experiences: experiences, Reflections: reflections, Skills: skills})
})
mux.HandleFunc("POST /v1/creator/chat/messages", func(w http.ResponseWriter, r *http.Request) {
session, ok := authenticateCreator(w, r)
if !ok {
@@ -245,6 +295,9 @@ func main() {
Message: utterance.Content,
RequestedAtUnixMillis: now.UnixMilli(),
})
if semantic.Interpretation.SpeechAct == interaction.SpeechActCorrection {
learning.recordCorrection(r.Context(), correctionLearningInput{Utterance: utterance, Intent: intent, At: now})
}
activeFocusName := ""
activeFocusProvenance := situation.ProjectResolutionProvenance("")
if semantic.ActiveFocus != nil {
@@ -270,6 +323,9 @@ func main() {
if responseErr == nil && responseResult.Handled {
planningReply = responseResult.Reply
needsCognition = false
learning.recordReminderResponse(r.Context(), reminderResponseLearningInput{Utterance: utterance, Intent: intent, Result: responseResult, At: now})
} else if responseErr == nil && looksLikeUnsupportedContinuation(utterance.Content, responseResult) {
learning.recordFastPathDecline(r.Context(), fastPathDeclineLearningInput{Utterance: utterance, Intent: intent, Result: responseResult, Reason: declineReasonForResponse(responseResult), At: now})
}
planningInput := work.PlanningInput{
ConversationID: work.ConversationRef(conversation.ID),
@@ -320,10 +376,12 @@ func main() {
if scheduleErr == nil {
reminder = &result.Item
planningReply = fmt.Sprintf("Готово. Я создал напоминание: %s.", result.Item.Summary)
observeCapabilityUsage(r.Context(), capabilityRepository, resolution.Eligible[0], resolution.Requirement, capability.UsageSucceeded, string(result.Item.ID), started, finished)
usageID, _ := observeCapabilityUsage(r.Context(), capabilityRepository, resolution.Eligible[0], resolution.Requirement, capability.UsageSucceeded, string(result.Item.ID), started, finished)
learning.recordCapabilityUse(r.Context(), capabilityLearningInput{Utterance: utterance, Intent: intent, ImplementationID: resolution.Eligible[0].Implementation.ID, UsageObservation: usageID, Outcome: capability.UsageSucceeded, OutcomeRef: "continuity:prospective:" + string(result.Item.ID), At: finished})
} else {
planningReply = "Scheduling capability сейчас недоступна: напоминание не создано. Я не буду притворяться, что действие выполнено."
observeCapabilityUsage(r.Context(), capabilityRepository, resolution.Eligible[0], resolution.Requirement, capability.UsageFailed, "continuity-reminder-unavailable", started, finished)
usageID, _ := observeCapabilityUsage(r.Context(), capabilityRepository, resolution.Eligible[0], resolution.Requirement, capability.UsageFailed, "continuity-reminder-unavailable", started, finished)
learning.recordCapabilityUse(r.Context(), capabilityLearningInput{Utterance: utterance, Intent: intent, ImplementationID: resolution.Eligible[0].Implementation.ID, UsageObservation: usageID, Outcome: capability.UsageFailed, OutcomeRef: "continuity-reminder-unavailable", At: finished})
}
} else {
planningReply = "Мне нужно уточнить время напоминания. Например: «Напомни завтра в 10 позвонить врачу»."
@@ -332,6 +390,7 @@ func main() {
} else if resolution.Gap != nil {
capabilityGapID = resolution.Gap.ID
planningReply = "Сейчас scheduling capability недоступна, поэтому напоминание не создано. Я зафиксировал capability gap."
learning.recordCapabilityUse(r.Context(), capabilityLearningInput{Utterance: utterance, Intent: intent, GapID: resolution.Gap.ID, Outcome: capability.UsageSkipped, OutcomeRef: "capability:gap:" + string(resolution.Gap.ID), At: now})
}
needsCognition = false
}
@@ -344,6 +403,7 @@ func main() {
} else if resolution.Gap != nil {
capabilityGapID = resolution.Gap.ID
planningReply = "Сейчас у меня ещё не подключена возможность отправлять почту. Я зафиксировал capability gap, без ложного выполнения."
learning.recordCapabilityUse(r.Context(), capabilityLearningInput{Utterance: utterance, Intent: intent, GapID: resolution.Gap.ID, Outcome: capability.UsageSkipped, OutcomeRef: "capability:gap:" + string(resolution.Gap.ID), At: now})
}
needsCognition = false
}
+2
View File
@@ -536,6 +536,8 @@ func creatorRoute(method, path string) (routeTarget, string, bool) {
if validID(id) {
return targetCore, "/v1/creator/capabilities/" + id + "/health", true
}
case method == http.MethodGet && path == "/api/learning/recent":
return targetCore, "/v1/creator/learning/recent", true
case method == http.MethodGet && strings.HasPrefix(path, "/api/prospective-items/"):
id := strings.TrimPrefix(path, "/api/prospective-items/")
if validID(id) {
+25 -2
View File
@@ -141,9 +141,11 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
.register-empty { margin: 0; padding: 22px 18px; color: var(--faint); font-size: 11px; }
.activity-panel { margin-bottom: 18px; border: 1px solid var(--line); background: rgba(13, 20, 17, .9); }
.learning-panel { margin-bottom: 18px; border: 1px solid rgba(120,198,163,.3); background: linear-gradient(135deg, rgba(22,31,27,.92), rgba(10,16,14,.94)); box-shadow: inset 3px 0 0 rgba(120,198,163,.38); }
.activity-panel > header { min-height: 66px; padding: 13px 18px; display: flex; align-items: center; justify-content: space-between; gap: 18px; border-bottom: 1px solid var(--line); }
.activity-panel header p { margin: 0 0 6px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .13em; text-transform: uppercase; }
.activity-panel h2 { margin: 0; font: 400 22px/1 var(--serif); }
.learning-panel > header { min-height: 66px; padding: 13px 18px; display: flex; align-items: center; justify-content: space-between; gap: 18px; border-bottom: 1px solid var(--line); }
.activity-panel header p, .learning-panel header p { margin: 0 0 6px; color: var(--faint); font: 9px/1 var(--mono); letter-spacing: .13em; text-transform: uppercase; }
.activity-panel h2, .learning-panel h2 { margin: 0; font: 400 22px/1 var(--serif); }
.activity-tools { display: flex; align-items: center; gap: 12px; color: var(--muted); font: 9px/1 var(--mono); letter-spacing: .1em; }
.activity-tools .icon-button { color: var(--muted); font-size: 18px; }
.activity-list { max-height: 300px; overflow: auto; scrollbar-color: var(--faint) transparent; }
@@ -167,6 +169,23 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
.activity-reconcile:disabled { opacity: .55; cursor: wait; }
.activity-error { color: var(--bad); }
.learning-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); }
.learning-grid article { min-width: 0; border-right: 1px solid var(--line-soft); }
.learning-grid article:last-child { border-right: 0; }
.learning-grid h3 { margin: 0; padding: 13px 18px 10px; color: var(--signal); font: 600 10px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; border-bottom: 1px solid var(--line-soft); }
.learning-list { max-height: 238px; overflow: auto; scrollbar-color: var(--faint) transparent; }
.learning-list[aria-busy="true"] { opacity: .72; }
.learning-row { min-height: 66px; padding: 12px 18px; display: grid; gap: 6px; border-bottom: 1px solid var(--line-soft); }
.learning-row:last-child { border-bottom: 0; }
.learning-row:hover { background: rgba(120,198,163,.05); }
.learning-row strong { color: var(--ink); font: 600 10px/1.35 var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.learning-row span { color: var(--muted); font: 10px/1.35 var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.learning-row time { color: var(--faint); font: 9px/1.35 var(--mono); }
.learning-state { width: fit-content; padding: 5px 8px; border: 1px solid currentColor; border-radius: 100px; color: var(--faint); font: 600 8px/1 var(--mono); letter-spacing: .06em; }
.learning-state[data-state="SUCCESS"], .learning-state[data-state="VALIDATED"] { color: var(--good); }
.learning-state[data-state="USER_CORRECTED"], .learning-state[data-state="FAILED"], .learning-state[data-state="DEGRADED"], .learning-state[data-state="SUSPENDED"] { color: var(--bad); }
.learning-state[data-state="CONFLICT"], .learning-state[data-state="OUT_OF_DISTRIBUTION"], .learning-state[data-state="OPEN"], .learning-state[data-state="EVALUATING"] { color: var(--warn); }
.data-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }
.data-card { min-width: 0; padding: 24px; border: 1px solid var(--line); background: linear-gradient(145deg, rgba(22, 31, 27, .94), rgba(13, 20, 17, .98)); box-shadow: 0 10px 40px rgba(0,0,0,.12); animation: reveal .55s both; }
.task-card { animation-delay: .08s; }
@@ -281,7 +300,11 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px
.advisor-result { grid-template-columns: 1fr; gap: 10px; }
.register-panel > header { align-items: start; }
.activity-panel > header { align-items: flex-start; }
.learning-panel > header { align-items: flex-start; }
.activity-tools { align-items: flex-end; flex-direction: column-reverse; }
.learning-grid { grid-template-columns: 1fr; }
.learning-grid article { border-right: 0; border-bottom: 1px solid var(--line-soft); }
.learning-grid article:last-child { border-bottom: 0; }
.activity-row { padding: 14px 16px; grid-template-columns: minmax(0, 1fr) auto; gap: 8px 12px; }
.activity-state { grid-column: 2; grid-row: 1; }
.activity-operation { grid-column: 1; grid-row: 1; }
+106 -1
View File
@@ -84,7 +84,10 @@ async function showConsole() {
await loadAdvisoryPolicy();
const context = saveContext();
if (context["session-id"] && context["workspace-id"]) loadDashboard();
else loadActionHistory();
else {
loadActionHistory();
loadLearning();
}
}
function endSession(message = "Сессия завершена.") {
@@ -147,6 +150,7 @@ contextForm.addEventListener("submit", (event) => {
byId("refresh-all").addEventListener("click", loadDashboard);
byId("refresh-actions").addEventListener("click", loadActionHistory);
byId("refresh-learning").addEventListener("click", loadLearning);
byId("logout").addEventListener("click", () => endSession());
advisoryForm.addEventListener("submit", async (event) => {
@@ -234,6 +238,7 @@ async function loadDashboard() {
}
work.push(loadProspectiveRegister(context["prospective-id"]));
work.push(loadActionHistory());
work.push(loadLearning());
if (context["capability-id"]) work.push(loadCapability(context["capability-id"]));
else setSignal("capability", "не выбрана", "idle");
@@ -241,6 +246,106 @@ async function loadDashboard() {
setText("last-update", `Обновлено ${new Intl.DateTimeFormat("ru-RU", {hour: "2-digit", minute: "2-digit", second: "2-digit"}).format(new Date())}`);
}
async function loadLearning() {
const experienceList = byId("experience-list");
const reflectionList = byId("reflection-list");
const skillList = byId("skill-list");
[experienceList, reflectionList, skillList].forEach((list) => {
list.setAttribute("aria-busy", "true");
list.replaceChildren();
const loading = document.createElement("p");
loading.className = "register-empty";
loading.textContent = "Читаю learning evidence…";
list.append(loading);
});
try {
const result = await request("/api/learning/recent?limit=8");
const experiences = Array.isArray(result.Experiences) ? result.Experiences : [];
const reflections = Array.isArray(result.Reflections) ? result.Reflections : [];
const skills = Array.isArray(result.Skills) ? result.Skills : [];
setText("learning-count", `${experiences.length + reflections.length + skills.length} СИГНАЛОВ`);
renderLearningList(experienceList, experiences, createExperienceRow, "Experience пока не записан.");
renderLearningList(reflectionList, reflections, createReflectionRow, "ReflectionCandidate пока нет.");
renderLearningList(skillList, skills, createSkillRow, "SkillProfile пока не загружен.");
} catch (error) {
[experienceList, reflectionList, skillList].forEach((list) => {
list.replaceChildren();
const failed = document.createElement("p");
failed.className = "register-empty activity-error";
failed.textContent = error.message;
list.append(failed);
});
setText("learning-count", "ОШИБКА");
} finally {
[experienceList, reflectionList, skillList].forEach((list) => list.setAttribute("aria-busy", "false"));
}
}
function renderLearningList(list, items, rowFactory, emptyText) {
list.replaceChildren();
if (items.length === 0) {
const empty = document.createElement("p");
empty.className = "register-empty";
empty.textContent = emptyText;
list.append(empty);
return;
}
items.forEach((item) => list.append(rowFactory(item)));
}
function createExperienceRow(item) {
const row = document.createElement("article");
row.className = "learning-row";
const state = document.createElement("em");
state.className = "learning-state";
state.dataset.state = item.ActualOutcome || "UNKNOWN";
state.textContent = item.ActualOutcome || "UNKNOWN";
const title = document.createElement("strong");
title.textContent = item.HelpMethod || "help method";
const detail = document.createElement("span");
detail.textContent = item.OutcomeDetail || item.OutcomeRef || "evidence recorded";
const time = document.createElement("time");
time.textContent = item.RecordedAtUnixMillis ? new Date(item.RecordedAtUnixMillis).toLocaleString("ru-RU") : "время неизвестно";
row.append(state, title, detail, time);
return row;
}
function createReflectionRow(item) {
const row = document.createElement("article");
row.className = "learning-row";
const state = document.createElement("em");
state.className = "learning-state";
state.dataset.state = item.State || "OPEN";
state.textContent = item.Trigger || "REFLECTION";
const title = document.createElement("strong");
title.textContent = item.State || "OPEN";
const detail = document.createElement("span");
detail.textContent = item.Summary || "reflection candidate";
const time = document.createElement("time");
time.textContent = item.RecordedAtUnixMillis ? new Date(item.RecordedAtUnixMillis).toLocaleString("ru-RU") : "время неизвестно";
row.append(state, title, detail, time);
return row;
}
function createSkillRow(item) {
const row = document.createElement("article");
row.className = "learning-row";
const state = document.createElement("em");
state.className = "learning-state";
state.dataset.state = item.Status || "UNKNOWN";
state.textContent = item.Status || "UNKNOWN";
const title = document.createElement("strong");
title.textContent = item.Name || "skill profile";
const detail = document.createElement("span");
const support = Array.isArray(item.SupportingExperienceRefs) ? item.SupportingExperienceRefs.length : 0;
const failures = Array.isArray(item.KnownFailureExperienceRefs) ? item.KnownFailureExperienceRefs.length : 0;
detail.textContent = `confidence ${item.ConfidenceMillis ?? "—"} · +${support} / -${failures}`;
const time = document.createElement("time");
time.textContent = item.RecordedAtUnixMillis ? new Date(item.RecordedAtUnixMillis).toLocaleString("ru-RU") : "время неизвестно";
row.append(state, title, detail, time);
return row;
}
async function loadActionHistory() {
const list = byId("action-history");
list.setAttribute("aria-busy", "true");
+24
View File
@@ -143,6 +143,30 @@
</div>
</section>
<section class="learning-panel" aria-labelledby="learning-title">
<header>
<div><p>Learning / evidence</p><h2 id="learning-title">Опыт и reflection</h2></div>
<div class="activity-tools">
<span id="learning-count">0 СИГНАЛОВ</span>
<button id="refresh-learning" class="icon-button" type="button" aria-label="Обновить опыт" title="Обновить опыт">↻</button>
</div>
</header>
<div class="learning-grid">
<article>
<h3>Experience</h3>
<div id="experience-list" class="learning-list" aria-live="polite" aria-busy="false"><p class="register-empty">Опыт ещё не загружен.</p></div>
</article>
<article>
<h3>Reflection</h3>
<div id="reflection-list" class="learning-list" aria-live="polite" aria-busy="false"><p class="register-empty">Reflection ещё не загружен.</p></div>
</article>
<article>
<h3>Skills</h3>
<div id="skill-list" class="learning-list" aria-live="polite" aria-busy="false"><p class="register-empty">Skill profile ещё не загружен.</p></div>
</article>
</div>
</section>
<section class="data-grid">
<article class="data-card focus-card">
<header><span class="card-number">01</span><div><p>Situation</p><h2>Активный фокус</h2></div><span id="focus-badge" class="status-badge neutral">НЕ ЗАГРУЖЕН</span></header>
+4
View File
@@ -17,6 +17,7 @@ services:
WORK_DB_PASSWORD: ${WORK_DB_PASSWORD}
CAPABILITY_DB_PASSWORD: ${CAPABILITY_DB_PASSWORD}
COMMIT_DB_PASSWORD: ${COMMIT_DB_PASSWORD}
EPISTEMICS_DB_PASSWORD_FILE: /run/secrets/epistemics_db_password
INTERACTION_DB_PASSWORD_FILE: /run/secrets/interaction_db_password
volumes:
- assistant_pg:/var/lib/postgresql
@@ -72,6 +73,9 @@ services:
WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD}
CAPABILITY_POSTGRES_USER: capability_role
CAPABILITY_POSTGRES_PASSWORD: ${CAPABILITY_DB_PASSWORD}
EPISTEMICS_POSTGRES_USER: epistemics_role
EPISTEMICS_POSTGRES_CREDENTIAL_REF: credential://domovoy/epistemics/db-password/v1
EPISTEMICS_POSTGRES_PASSWORD_FILE: /run/secrets/epistemics_db_password
INTERACTION_POSTGRES_USER: interaction_role
INTERACTION_POSTGRES_CREDENTIAL_REF: credential://domovoy/interaction/db-password/v1
INTERACTION_POSTGRES_PASSWORD_FILE: /run/secrets/interaction_db_password
+7
View File
@@ -18,6 +18,13 @@ SQL
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -v ON_ERROR_STOP=1 -v capability_password="$CAPABILITY_DB_PASSWORD" <<'SQL'
ALTER ROLE capability_role PASSWORD :'capability_password';
SQL
if [ "${EPISTEMICS_DB_PASSWORD_FILE:-}" != "" ]; then
EPISTEMICS_DB_PASSWORD="$(tr -d '\r\n' < "$EPISTEMICS_DB_PASSWORD_FILE")"
fi
: "${EPISTEMICS_DB_PASSWORD:?EPISTEMICS_DB_PASSWORD or EPISTEMICS_DB_PASSWORD_FILE is required}"
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -v ON_ERROR_STOP=1 -v epistemics_password="$EPISTEMICS_DB_PASSWORD" <<'SQL'
ALTER ROLE epistemics_role PASSWORD :'epistemics_password';
SQL
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -v ON_ERROR_STOP=1 -v commit_password="$COMMIT_DB_PASSWORD" <<'SQL'
ALTER ROLE commit_role PASSWORD :'commit_password';
SQL
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000067-experience-reflection-skill-evolution.json",
"task_id": "DEV-000067",
"task_file": "dev/tasks/DEV-000068-runtime-experience-reflection-loop.json",
"task_id": "DEV-000068",
"task_revision": 1
}
@@ -0,0 +1,138 @@
{
"task_id": "DEV-000068",
"task_revision": 1,
"architecture_revision": "ADR-0016",
"base_repo_revision": "67ff1f73cddfd94f9f5745acc87f997aed49630a",
"risk_tier": "E",
"intent": "Activate the first production runtime Experience and Reflection loop for meaningful /chat, HumanResponse, capability and fast-path outcomes without creating a LearningManager.",
"requirement_ids": [
"ADR-0016",
"INV-OWN-001",
"INV-EPI-001",
"Runtime Experience capture",
"Runtime ReflectionCandidate triggers",
"SkillEvidence from real reminder response outcomes",
"Fast-path decline returns to reasoning/clarification"
],
"allowed_change_scope": {
"modules": [
"epistemics",
"capability",
"interaction",
"continuity"
],
"paths": [
"contracts/epistemics/**",
"contracts/capability/**",
"modules/epistemics/**",
"modules/capability/**",
"adapters/postgres/**",
"database/migrations/**",
"cmd/core-runtime/**",
"cmd/creator-web/**",
"cmd/continuity-runtime/**",
"deploy/docker-compose.prod.yml",
"deploy/images/002_owner_passwords.sh",
"deploy/windows/**",
"tests/owner_acl/**",
"tests/cognitive_advisory/**",
"docs/PROJECT_STATUS.md",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000068-runtime-experience-reflection-loop.json"
]
},
"forbidden_change_scope": {
"paths": [
"architecture/ARCHITECTURE_CONSTITUTION.md",
"modules/authority/**",
"modules/commit/**",
"cmd/commit-runtime/**",
"security/**",
"ci/**",
".gitea/**"
]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 0,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_provider_adapters": 0,
"new_runtime_secret_file_for_existing_owner": "epistemics_role database password for core-runtime"
},
"contracts": {
"consume": [
"Interaction HumanUtterance / OriginalHumanIntent / Correction refs",
"Continuity HumanResponse / Attention / ProspectiveItem refs",
"Capability CapabilityUsageObservation / CapabilityGap / SkillProfile",
"Epistemics SystemExpectation / ExperienceRecord / ReflectionCandidate"
],
"may_change": [
"contracts/epistemics@v1",
"contracts/capability@v1"
]
},
"semantic_owner": "epistemics",
"canonical_state": [
"SystemExpectation",
"ExperienceRecord",
"ReflectionCandidate",
"SkillProfile"
],
"state_transitions": [
"meaningful chat help path -> SystemExpectation before outcome",
"HumanResponse resolved COMPLETE/SNOOZE/CANCEL -> positive ExperienceRecord and SkillProfile supporting evidence",
"Human correction -> USER_CORRECTED ExperienceRecord and ReflectionCandidate",
"ambiguous/no-target/unsupported fast path -> decline/clarification ExperienceRecord and ReflectionCandidate",
"repeated failure/correction evidence -> SkillProfile DEGRADED/SUSPENDED according to deterministic policy"
],
"side_effects_and_privileges": [
"No external mutation is introduced",
"No model output can directly mutate canonical state, Authority, source code, deployment or skill lifecycle",
"Core runtime receives only the existing Epistemics owner database credential through secret-file/CredentialRef mechanism",
"Creator Web remains a proxy/UI and receives no direct database credential",
"Docker-test remains prohibited"
],
"persistence_and_migrations": [
"no new canonical owner",
"no cross-owner FK",
"no cross-owner SQL join dependency",
"reuse DEV-000067 tables with optional indexes/projections only if needed"
],
"failure_retry_restart": [
"Experience/reflection capture is append-only and idempotent by generated IDs",
"A failed learning write must not pretend success; production acceptance verifies rows",
"Fast-path decline is valid safety behavior and does not mutate target state"
],
"assumptions": [
"DEV-000067 production foundation is active",
"ADR-0016 is sufficient; no new architecture owner is required",
"V1 uses deterministic reflection triggers; cognitive reflection remains optional candidate generation only"
],
"acceptance_scenarios": [
"Create reminder then complete it -> positive ExperienceRecord and SkillProfile supporting evidence",
"Create ambiguous targets and correct Domovoy -> USER_CORRECTED ExperienceRecord and ReflectionCandidate",
"Unsupported/uncertain response phrasing -> fast path declines and asks clarification/falls back without guessed mutation",
"Operator can inspect recent ExperienceRecords, ReflectionCandidates and SkillProfile status",
"Human chat does not expose internal learning IDs",
"LLM-free operation passes"
],
"required_tests": [
"go test ./...",
"go vet ./...",
"go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json",
"go run ./cmd/devctl check-architecture",
"go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json",
"tests/cognitive_advisory/cognitive_worker_env_policy_test.ps1",
"tests/owner_acl/verify.ps1 -DockerContext docker-prod -PostgresContainer assistant-postgres-1",
"production live learning acceptance A/B/C",
"production health",
"production /chat verification"
],
"terminal_gap_states": [
"BLOCKED_BY_ARCHITECTURE_GAP",
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_MISSING_CAPABILITY",
"BLOCKED_BY_CONFLICT"
]
}