Compare commits

..
Author SHA1 Message Date
m 4fc85511c4 DEV-000063 intent work planning foundation
CI / test (push) Successful in 16s
CI / test (pull_request) Successful in 14s
2026-08-17 15:10:43 +03:00
m 18750c2ae6 Merge DEV-000062 final production status
CI / test (push) Successful in 13s
Status-only completion record for DEV-000062.
2026-08-17 11:53:06 +00:00
19 changed files with 1128 additions and 10 deletions
+36
View File
@@ -39,3 +39,39 @@ func (r *WorkRepository) Load(ctx context.Context, id work.TaskID) (work.TaskTar
}
return target, nil
}
func (r *WorkRepository) SavePlanningResult(ctx context.Context, result work.PlanningResult) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
goal := result.Goal
if _, err = tx.Exec(ctx, `INSERT INTO work.goals(goal_id,source_intent_id,source_utterance_id,actor_id,principal_id,scope_id,project_id,project_display_name,desired_outcome,constraints_text,state,provenance,revision,created_at_unix_millis,updated_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) ON CONFLICT(goal_id) DO NOTHING`, goal.ID, goal.SourceIntentID, goal.SourceUtteranceID, goal.ActorID, goal.PrincipalID, goal.ScopeID, goal.ProjectID, goal.ProjectDisplayName, goal.DesiredOutcome, goal.Constraints, goal.State, goal.Provenance, goal.Revision, goal.CreatedAtUnixMillis, goal.UpdatedAtUnixMillis); err != nil {
return err
}
candidate := result.Candidate
if _, err = tx.Exec(ctx, `INSERT INTO work.plan_candidates(plan_candidate_id,goal_id,source,state,summary,validation_summary,requires_clarification,accepted_at_unix_millis,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9) ON CONFLICT(plan_candidate_id) DO NOTHING`, candidate.ID, candidate.GoalID, candidate.Source, candidate.State, candidate.Summary, candidate.ValidationSummary, candidate.RequiresClarification, candidate.AcceptedAtUnixMillis, candidate.RecordedAtUnixMillis); err != nil {
return err
}
plan := result.Plan
if _, err = tx.Exec(ctx, `INSERT INTO work.work_plans(work_plan_id,goal_id,accepted_candidate_id,revision,state,summary,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(work_plan_id) DO NOTHING`, plan.ID, plan.GoalID, plan.AcceptedCandidateID, plan.Revision, plan.State, plan.Summary, plan.RecordedAtUnixMillis); err != nil {
return err
}
for _, requirement := range candidate.Requirements {
if _, err = tx.Exec(ctx, `INSERT INTO work.capability_requirements(capability_requirement_id,plan_candidate_id,capability_kind,capability_id,purpose,state,epistemic_precondition,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(capability_requirement_id) DO NOTHING`, requirement.ID, requirement.PlanCandidateID, requirement.CapabilityKind, requirement.CapabilityID, requirement.Purpose, requirement.State, requirement.EpistemicPrecondition, requirement.RecordedAtUnixMillis); err != nil {
return err
}
}
for _, step := range candidate.Steps {
if _, err = tx.Exec(ctx, `INSERT INTO work.plan_steps(plan_step_id,plan_candidate_id,ordinal,kind,description,requires_capability_id,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7) ON CONFLICT(plan_step_id) DO NOTHING`, step.ID, step.PlanCandidateID, step.Ordinal, step.Kind, step.Description, step.RequiresCapabilityID, step.RecordedAtUnixMillis); err != nil {
return err
}
}
for _, delegation := range candidate.Delegations {
if _, err = tx.Exec(ctx, `INSERT INTO work.delegation_candidates(delegation_candidate_id,plan_candidate_id,capability_requirement_id,kind,executor_hint,state,reason,recorded_at_unix_millis) VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(delegation_candidate_id) DO NOTHING`, delegation.ID, delegation.PlanCandidateID, delegation.CapabilityRequirementID, delegation.Kind, delegation.ExecutorHint, delegation.State, delegation.Reason, delegation.RecordedAtUnixMillis); err != nil {
return err
}
}
return tx.Commit(ctx)
}
@@ -0,0 +1,43 @@
# ADR-0012 - Intent To Work Planning Foundation
Status: Accepted
Date: 2026-08-17
Accepted under: DEV-000063 milestone decision
## Decision
`work` remains the canonical owner for Goal, Task, Decision and the new V1 planning foundation. No new canonical owner is introduced.
Help is the central user-facing concept. Goal, Work and Plan are optional durable coordination primitives, not mandatory records for every conversation. A simple explanation, ordinary answer, short cognitive response, direct reminder capability, safe deterministic operation, or capability-gap response may bypass durable Work entirely.
Human chat must not reduce Domovoy to `chat -> prompt -> model` or to a Codex-specific task generator. The canonical pipeline is:
When durable coordination is actually needed, the canonical path is:
`HumanUtterance -> OriginalHumanIntent -> Goal -> PlanCandidate -> accepted Work/Plan state -> CapabilityRequirement -> DelegationCandidate/Action/Investigation/Observation/Outcome`.
Prompts, Codex instructions, mail payloads, reminders and provider requests are executor-adapter representations. They are not canonical human intent, Goal or Plan.
## Ownership
- `interaction` continues to own append-only human utterances, original intent provenance and typed utterance interpretations.
- `work` owns durable Goals, lifecycle, planning state, plan candidates, accepted plan summaries, capability requirements and general delegation candidates.
- `capability` owns available capability/binding registry and health. Work may reference capability requirements by typed IDs/kinds but does not select a concrete provider as canonical truth.
- `epistemics` owns verified/unverified/unknown evidence semantics. Work planning must preserve epistemic state and may add verification requirements before risky work.
- `authority` and `commit` remain required for any real mutation or external side effect. Planning creates no permission and no execution claim.
## Boundaries
Work PlanCandidates are structured candidates that must be validated before persistence or acceptance. Cognitive planning may later propose candidates only through the Cognitive Gateway/Router; model output never directly mutates canonical Work state. DEV-000063 may implement deterministic V1 candidate planning and keep cognitive candidate generation as a typed extension point.
CapabilityRequirement is executor-neutral. Examples include `READ_EMAIL`, `SEND_EMAIL`, `READ_DATABASE`, `QUERY_LOGS`, `INSPECT_PROJECT`, `MODIFY_CODE`, `RUN_TESTS`, `WEB_RESEARCH`, `COGNITIVE_REASONING`, and `SCHEDULE_REMINDER`. This is an extensible vocabulary, not a closed ontology and not an executor list.
DelegationCandidate is general. `Codex` is only a possible future implementation for `MODIFY_CODE`; mail, research, scheduler, device and database adapters can use the same canonical Work model without changing OriginalHumanIntent or Goal.
## Consequences
`/chat` can choose a HelpPath first. Only the durable-work path creates a planning-only Goal and Plan. Other paths can answer, use cognition, report a missing capability, or describe a direct capability requirement without materializing a Goal/Plan. It must not pretend work was completed. Completion remains tied to outcome semantics, not to a model response.
Project is only one possible context reference. The Work foundation stores project context as optional opaque references and must later permit people, emails, documents, images, servers, organizations, research subjects and other context types without redesigning Goal/Plan.
Capability gaps are first-class planning information. "I cannot do this yet" is not necessarily terminal in future milestones: the system may later discover, connect, configure or build a missing capability and return to the original need. DEV-000063 does not implement self-extension; it preserves the extension path.
+11 -1
View File
@@ -57,14 +57,24 @@
"id": "mod.work",
"owns": [
"Goal",
"HelpPath",
"GoalLifecycle",
"Task",
"Decision"
"Decision",
"PlanCandidate",
"WorkPlan",
"PlanStep",
"CapabilityRequirement",
"DelegationCandidate"
],
"database_schema": "work",
"boundary_class": "remote_capable",
"may_depend_on": [
"contracts.identity",
"contracts.authority",
"contracts.epistemics",
"contracts.capability",
"platform.ids",
"platform.events"
]
},
+2 -2
View File
@@ -27,10 +27,10 @@ modules:
work:
id: mod.work
owns: [Goal, Task, Decision]
owns: [Goal, HelpPath, GoalLifecycle, Task, Decision, PlanCandidate, WorkPlan, PlanStep, CapabilityRequirement, DelegationCandidate]
database_schema: work
boundary_class: remote_capable
may_depend_on: [contracts.identity, contracts.authority, platform.events]
may_depend_on: [contracts.identity, contracts.authority, contracts.epistemics, contracts.capability, platform.ids, platform.events]
epistemics:
id: mod.epistemics
+49 -1
View File
@@ -6,7 +6,9 @@ import (
"errors"
"fmt"
"git.cin.su/m/Assistant/adapters/postgres"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
secretcontracts "git.cin.su/m/Assistant/contracts/secrets"
@@ -16,6 +18,7 @@ import (
identitydomain "git.cin.su/m/Assistant/modules/identity/domain"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
situationdomain "git.cin.su/m/Assistant/modules/situation/domain"
workdomain "git.cin.su/m/Assistant/modules/work/domain"
platformsecrets "git.cin.su/m/Assistant/platform/secrets"
"git.cin.su/m/Assistant/runtime/health"
"github.com/jackc/pgx/v5"
@@ -240,16 +243,61 @@ func main() {
activeFocusName = previousFocus.ProjectDisplayName
activeFocusProvenance = previousFocus.Provenance
}
var planningReply string
workPlanned := false
needsCognition := semantic.NeedsCognition
projectID := ""
if semantic.ActiveFocus != nil {
projectID = string(semantic.ActiveFocus.ProjectID)
} else if previousFocus != nil {
projectID = string(previousFocus.ProjectID)
}
planningInput := work.PlanningInput{
ConversationID: work.ConversationRef(conversation.ID),
UtteranceID: work.SourceUtteranceRef(utterance.ID),
IntentID: work.SourceIntentRef(intent.ID),
ActorID: session.ActorID,
PrincipalID: session.PrincipalID,
ScopeID: authority.ScopeID("domovoy-global-scope"),
ProjectID: work.ProjectRef(projectID),
ProjectDisplayName: activeFocusName,
ProjectProvenance: work.ProjectProvenanceRef(activeFocusProvenance),
Message: utterance.Content,
SpeechAct: work.SpeechActRef(semantic.Interpretation.SpeechAct),
AssertionVerification: epistemics.VerificationState(semantic.Interpretation.AssertionVerification),
ReceivedAtUnixMillis: utterance.ReceivedAtUnixMillis,
}
helpPath := workdomain.DetermineHelpPath(planningInput)
if helpPath.Reply != "" {
planningReply = helpPath.Reply
needsCognition = false
}
if helpPath.NeedsDurableWork {
planning, err := workdomain.BuildPlanningResult(planningInput, now)
if err != nil {
http.Error(w, "plan chat work", http.StatusServiceUnavailable)
return
}
if err := workRepository.SavePlanningResult(r.Context(), planning); err != nil {
http.Error(w, "save work plan", http.StatusInternalServerError)
return
}
planningReply = planning.Reply
workPlanned = true
needsCognition = false
}
response := interaction.ChatMessageResponse{
ConversationID: conversation.ID,
UtteranceID: utterance.ID,
SpeechAct: semantic.Interpretation.SpeechAct,
InterpretationState: utterance.InterpretationState,
NeedsCognition: semantic.NeedsCognition,
NeedsCognition: needsCognition,
FallbackText: semantic.Reply,
ActiveProjectName: activeFocusName,
ProjectProvenance: activeFocusProvenance,
Clarification: semantic.Interpretation.Clarification,
WorkPlanned: workPlanned,
PlanningReply: planningReply,
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
+6 -2
View File
@@ -150,6 +150,7 @@ type browserChatResponse struct {
ActiveFocus string `json:"active_focus"`
ProjectProvenance string `json:"project_provenance"`
Clarification string `json:"clarification"`
WorkPlanned bool `json:"work_planned"`
}
func (g *gateway) handleAdvisoryPolicy(w http.ResponseWriter, r *http.Request) {
@@ -316,7 +317,10 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
}
reply := saved.FallbackText
cognition := "not_requested"
if saved.NeedsCognition && saved.Clarification != interaction.ClarificationRequired {
if saved.PlanningReply != "" {
reply = saved.PlanningReply
}
if saved.NeedsCognition && !saved.WorkPlanned && saved.Clarification != interaction.ClarificationRequired {
cognition = "unavailable"
requestID, idErr := ids.New()
if idErr == nil {
@@ -336,7 +340,7 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(browserChatResponse{ConversationID: string(saved.ConversationID), UtteranceID: string(saved.UtteranceID), SpeechAct: string(saved.SpeechAct), ReplyText: reply, Cognition: cognition, ActiveFocus: saved.ActiveProjectName, ProjectProvenance: string(saved.ProjectProvenance), Clarification: string(saved.Clarification)})
_ = json.NewEncoder(w).Encode(browserChatResponse{ConversationID: string(saved.ConversationID), UtteranceID: string(saved.UtteranceID), SpeechAct: string(saved.SpeechAct), ReplyText: reply, Cognition: cognition, ActiveFocus: saved.ActiveProjectName, ProjectProvenance: string(saved.ProjectProvenance), Clarification: string(saved.Clarification), WorkPlanned: saved.WorkPlanned})
}
func validAdvisoryResponse(response cognitive.GatewayAdvisoryResponse, request cognitive.GatewayAdvisoryRequest) bool {
+1
View File
@@ -80,6 +80,7 @@ chatForm.addEventListener("submit", async (event) => {
sessionStorage.setItem("domovoy.chat.conversation", conversationID);
updateActiveFocus(result.active_focus, result.project_provenance);
const semanticMeta = [result.speech_act, `cognition=${result.cognition}`];
if (result.work_planned) semanticMeta.push("work=planned");
if (result.clarification) semanticMeta.push(`clarification=${result.clarification}`);
appendMessage("assistant", result.reply_text || "Сообщение записано.", semanticMeta.join("; "));
} catch {
+13
View File
@@ -7,6 +7,7 @@ import (
type CapabilityID ids.ID
type CapabilityBindingID ids.ID
type CapabilityKind string
type BindingState string
type BindingProtocol string
@@ -16,6 +17,18 @@ const (
BindingRetired BindingState = "RETIRED"
ProtocolIdempotentReceiptV1 BindingProtocol = "IDEMPOTENT_RECEIPT_V1"
CapabilityKindReadEmail CapabilityKind = "READ_EMAIL"
CapabilityKindSendEmail CapabilityKind = "SEND_EMAIL"
CapabilityKindReadDatabase CapabilityKind = "READ_DATABASE"
CapabilityKindQueryLogs CapabilityKind = "QUERY_LOGS"
CapabilityKindInspectProject CapabilityKind = "INSPECT_PROJECT"
CapabilityKindModifyCode CapabilityKind = "MODIFY_CODE"
CapabilityKindRunTests CapabilityKind = "RUN_TESTS"
CapabilityKindWebResearch CapabilityKind = "WEB_RESEARCH"
CapabilityKindCognitiveReasoning CapabilityKind = "COGNITIVE_REASONING"
CapabilityKindScheduleReminder CapabilityKind = "SCHEDULE_REMINDER"
CapabilityKindVerifyFact CapabilityKind = "VERIFY_FACT"
)
// Binding is Capability-owned correctness state. SecretRef is an opaque
+1
View File
@@ -12,6 +12,7 @@ const (
VerificationUnverified VerificationState = "UNVERIFIED"
VerificationVerified VerificationState = "VERIFIED"
VerificationRejected VerificationState = "REJECTED"
VerificationUnknown VerificationState = "UNKNOWN"
VerificationNotApplicable VerificationState = "NOT_APPLICABLE"
ClaimSourceAuthenticatedHuman ClaimSourceKind = "AUTHENTICATED_HUMAN"
+2
View File
@@ -128,4 +128,6 @@ type ChatMessageResponse struct {
ActiveProjectName string
ProjectProvenance situation.ProjectResolutionProvenance
Clarification ClarificationState
WorkPlanned bool
PlanningReply string
}
+184
View File
@@ -2,13 +2,88 @@ package work
import (
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/platform/ids"
)
type GoalID ids.ID
type TaskID ids.ID
type DecisionID ids.ID
type PlanCandidateID ids.ID
type WorkPlanID ids.ID
type PlanStepID ids.ID
type CapabilityRequirementID ids.ID
type DelegationCandidateID ids.ID
type TaskRevision uint64
type GoalRevision uint64
type PlanRevision uint64
type GoalState string
type PlanCandidateState string
type PlanStepKind string
type CapabilityRequirementState string
type DelegationCandidateState string
type DelegationKind string
type SourceIntentRef string
type SourceUtteranceRef string
type ConversationRef string
type ProjectRef string
type ProjectProvenanceRef string
type SpeechActRef string
type HelpPathKind string
const (
GoalProposed GoalState = "PROPOSED"
GoalUnderstood GoalState = "UNDERSTOOD"
GoalPlanning GoalState = "PLANNING"
GoalReady GoalState = "READY"
GoalInProgress GoalState = "IN_PROGRESS"
GoalBlocked GoalState = "BLOCKED"
GoalCompleted GoalState = "COMPLETED"
GoalFailed GoalState = "FAILED"
GoalCancelled GoalState = "CANCELLED"
GoalSuperseded GoalState = "SUPERSEDED"
PlanCandidateProposed PlanCandidateState = "PROPOSED"
PlanCandidateValidated PlanCandidateState = "VALIDATED"
PlanCandidateAccepted PlanCandidateState = "ACCEPTED"
PlanCandidateRejected PlanCandidateState = "REJECTED"
PlanStepInvestigate PlanStepKind = "INVESTIGATE"
PlanStepObserve PlanStepKind = "OBSERVE"
PlanStepVerify PlanStepKind = "VERIFY"
PlanStepReason PlanStepKind = "REASON"
PlanStepReport PlanStepKind = "REPORT"
PlanStepPrepare PlanStepKind = "PREPARE"
PlanStepCommunicate PlanStepKind = "COMMUNICATE"
PlanStepSchedule PlanStepKind = "SCHEDULE"
PlanStepModify PlanStepKind = "MODIFY"
PlanStepRunChecks PlanStepKind = "RUN_CHECKS"
PlanStepClarify PlanStepKind = "CLARIFY"
PlanStepNoExecution PlanStepKind = "NO_EXECUTION"
CapabilityRequirementRequired CapabilityRequirementState = "REQUIRED"
CapabilityRequirementAvailable CapabilityRequirementState = "AVAILABLE"
CapabilityRequirementMissing CapabilityRequirementState = "MISSING"
CapabilityRequirementBlocked CapabilityRequirementState = "BLOCKED"
DelegationCandidateProposed DelegationCandidateState = "PROPOSED"
DelegationCandidateSelected DelegationCandidateState = "SELECTED"
DelegationCandidateBlocked DelegationCandidateState = "BLOCKED"
DelegationKindAgent DelegationKind = "AGENT"
DelegationKindTool DelegationKind = "TOOL"
DelegationKindAdapter DelegationKind = "ADAPTER"
DelegationKindHuman DelegationKind = "HUMAN"
DelegationKindUnavailable DelegationKind = "UNAVAILABLE"
HelpPathAnswer HelpPathKind = "ANSWER"
HelpPathCognition HelpPathKind = "COGNITION"
HelpPathDirectCapability HelpPathKind = "DIRECT_CAPABILITY"
HelpPathDurableWork HelpPathKind = "DURABLE_WORK"
HelpPathCapabilityGap HelpPathKind = "CAPABILITY_GAP"
)
// TaskTarget carries the integrity context that a work-owned mutation must match.
type TaskTarget struct {
@@ -17,3 +92,112 @@ type TaskTarget struct {
ScopeID authority.ScopeID
Revision TaskRevision
}
type Goal struct {
ID GoalID
SourceIntentID SourceIntentRef
SourceUtteranceID SourceUtteranceRef
ActorID identity.ActorID
PrincipalID identity.PrincipalID
ScopeID authority.ScopeID
ProjectID ProjectRef
ProjectDisplayName string
DesiredOutcome string
Constraints string
State GoalState
Provenance ProjectProvenanceRef
Revision GoalRevision
CreatedAtUnixMillis int64
UpdatedAtUnixMillis int64
}
type CapabilityRequirement struct {
ID CapabilityRequirementID
PlanCandidateID PlanCandidateID
CapabilityKind capability.CapabilityKind
CapabilityID capability.CapabilityID
Purpose string
State CapabilityRequirementState
EpistemicPrecondition epistemics.VerificationState
RecordedAtUnixMillis int64
}
type PlanStep struct {
ID PlanStepID
PlanCandidateID PlanCandidateID
Ordinal int
Kind PlanStepKind
Description string
RequiresCapabilityID CapabilityRequirementID
RecordedAtUnixMillis int64
}
type DelegationCandidate struct {
ID DelegationCandidateID
PlanCandidateID PlanCandidateID
CapabilityRequirementID CapabilityRequirementID
Kind DelegationKind
ExecutorHint string
State DelegationCandidateState
Reason string
RecordedAtUnixMillis int64
}
type PlanCandidate struct {
ID PlanCandidateID
GoalID GoalID
Source string
State PlanCandidateState
Summary string
ValidationSummary string
RequiresClarification bool
AcceptedAtUnixMillis int64
RecordedAtUnixMillis int64
Steps []PlanStep
Requirements []CapabilityRequirement
Delegations []DelegationCandidate
}
type WorkPlan struct {
ID WorkPlanID
GoalID GoalID
AcceptedCandidateID PlanCandidateID
Revision PlanRevision
State PlanCandidateState
Summary string
RecordedAtUnixMillis int64
}
type PlanningInput struct {
ConversationID ConversationRef
UtteranceID SourceUtteranceRef
IntentID SourceIntentRef
ActorID identity.ActorID
PrincipalID identity.PrincipalID
ScopeID authority.ScopeID
ProjectID ProjectRef
ProjectDisplayName string
ProjectProvenance ProjectProvenanceRef
Message string
SpeechAct SpeechActRef
AssertionVerification epistemics.VerificationState
ReceivedAtUnixMillis int64
}
type PlanningResult struct {
Goal Goal
Candidate PlanCandidate
Plan WorkPlan
Reply string
}
type HelpPath struct {
Kind HelpPathKind
NeedsDurableGoal bool
NeedsDurableWork bool
NeedsDurablePlan bool
SuggestedCapabilityKinds []capability.CapabilityKind
CapabilityGap bool
Reason string
Reply string
}
@@ -135,6 +135,97 @@ CREATE TABLE IF NOT EXISTS work.tasks (
task_id TEXT PRIMARY KEY, goal_id TEXT NOT NULL, scope_id TEXT NOT NULL,
revision BIGINT NOT NULL, CHECK (revision > 0)
);
CREATE TABLE IF NOT EXISTS work.goals (
goal_id TEXT PRIMARY KEY,
source_intent_id TEXT NOT NULL,
source_utterance_id TEXT NOT NULL,
actor_id TEXT NOT NULL,
principal_id TEXT NOT NULL,
scope_id TEXT NOT NULL,
project_id TEXT NOT NULL DEFAULT '',
project_display_name TEXT NOT NULL DEFAULT '',
desired_outcome TEXT NOT NULL,
constraints_text TEXT NOT NULL,
state TEXT NOT NULL,
provenance TEXT NOT NULL DEFAULT '',
revision BIGINT NOT NULL,
created_at_unix_millis BIGINT NOT NULL,
updated_at_unix_millis BIGINT NOT NULL,
CHECK (desired_outcome <> ''),
CHECK (state IN ('PROPOSED','UNDERSTOOD','PLANNING','READY','IN_PROGRESS','BLOCKED','COMPLETED','FAILED','CANCELLED','SUPERSEDED')),
CHECK (revision > 0)
);
CREATE TABLE IF NOT EXISTS work.plan_candidates (
plan_candidate_id TEXT PRIMARY KEY,
goal_id TEXT NOT NULL,
source TEXT NOT NULL,
state TEXT NOT NULL,
summary TEXT NOT NULL,
validation_summary TEXT NOT NULL,
requires_clarification BOOLEAN NOT NULL,
accepted_at_unix_millis BIGINT NOT NULL DEFAULT 0,
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (goal_id <> ''),
CHECK (source <> ''),
CHECK (summary <> ''),
CHECK (state IN ('PROPOSED','VALIDATED','ACCEPTED','REJECTED'))
);
CREATE TABLE IF NOT EXISTS work.work_plans (
work_plan_id TEXT PRIMARY KEY,
goal_id TEXT NOT NULL,
accepted_candidate_id TEXT NOT NULL,
revision BIGINT NOT NULL,
state TEXT NOT NULL,
summary TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (goal_id <> ''),
CHECK (accepted_candidate_id <> ''),
CHECK (revision > 0),
CHECK (state IN ('PROPOSED','VALIDATED','ACCEPTED','REJECTED'))
);
CREATE TABLE IF NOT EXISTS work.capability_requirements (
capability_requirement_id TEXT PRIMARY KEY,
plan_candidate_id TEXT NOT NULL,
capability_kind TEXT NOT NULL,
capability_id TEXT NOT NULL DEFAULT '',
purpose TEXT NOT NULL,
state TEXT NOT NULL,
epistemic_precondition TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (plan_candidate_id <> ''),
CHECK (capability_kind <> ''),
CHECK (purpose <> ''),
CHECK (state IN ('REQUIRED','AVAILABLE','MISSING','BLOCKED')),
CHECK (epistemic_precondition IN ('UNVERIFIED','VERIFIED','REJECTED','UNKNOWN','NOT_APPLICABLE'))
);
CREATE TABLE IF NOT EXISTS work.plan_steps (
plan_step_id TEXT PRIMARY KEY,
plan_candidate_id TEXT NOT NULL,
ordinal INTEGER NOT NULL,
kind TEXT NOT NULL,
description TEXT NOT NULL,
requires_capability_id TEXT NOT NULL DEFAULT '',
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (plan_candidate_id <> ''),
CHECK (ordinal > 0),
CHECK (kind IN ('INVESTIGATE','OBSERVE','VERIFY','REASON','REPORT','PREPARE','COMMUNICATE','SCHEDULE','MODIFY','RUN_CHECKS','CLARIFY','NO_EXECUTION')),
CHECK (description <> '')
);
CREATE TABLE IF NOT EXISTS work.delegation_candidates (
delegation_candidate_id TEXT PRIMARY KEY,
plan_candidate_id TEXT NOT NULL,
capability_requirement_id TEXT NOT NULL,
kind TEXT NOT NULL,
executor_hint TEXT NOT NULL DEFAULT '',
state TEXT NOT NULL,
reason TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (plan_candidate_id <> ''),
CHECK (capability_requirement_id <> ''),
CHECK (kind IN ('AGENT','TOOL','ADAPTER','HUMAN','UNAVAILABLE')),
CHECK (state IN ('PROPOSED','SELECTED','BLOCKED')),
CHECK (reason <> '')
);
RESET ROLE;
SET ROLE situation_role;
@@ -0,0 +1,104 @@
BEGIN;
SET ROLE work_role;
CREATE TABLE IF NOT EXISTS work.goals (
goal_id TEXT PRIMARY KEY,
source_intent_id TEXT NOT NULL,
source_utterance_id TEXT NOT NULL,
actor_id TEXT NOT NULL,
principal_id TEXT NOT NULL,
scope_id TEXT NOT NULL,
project_id TEXT NOT NULL DEFAULT '',
project_display_name TEXT NOT NULL DEFAULT '',
desired_outcome TEXT NOT NULL,
constraints_text TEXT NOT NULL,
state TEXT NOT NULL,
provenance TEXT NOT NULL DEFAULT '',
revision BIGINT NOT NULL,
created_at_unix_millis BIGINT NOT NULL,
updated_at_unix_millis BIGINT NOT NULL,
CHECK (desired_outcome <> ''),
CHECK (state IN ('PROPOSED','UNDERSTOOD','PLANNING','READY','IN_PROGRESS','BLOCKED','COMPLETED','FAILED','CANCELLED','SUPERSEDED')),
CHECK (revision > 0)
);
CREATE TABLE IF NOT EXISTS work.plan_candidates (
plan_candidate_id TEXT PRIMARY KEY,
goal_id TEXT NOT NULL,
source TEXT NOT NULL,
state TEXT NOT NULL,
summary TEXT NOT NULL,
validation_summary TEXT NOT NULL,
requires_clarification BOOLEAN NOT NULL,
accepted_at_unix_millis BIGINT NOT NULL DEFAULT 0,
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (goal_id <> ''),
CHECK (source <> ''),
CHECK (summary <> ''),
CHECK (state IN ('PROPOSED','VALIDATED','ACCEPTED','REJECTED'))
);
CREATE TABLE IF NOT EXISTS work.work_plans (
work_plan_id TEXT PRIMARY KEY,
goal_id TEXT NOT NULL,
accepted_candidate_id TEXT NOT NULL,
revision BIGINT NOT NULL,
state TEXT NOT NULL,
summary TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (goal_id <> ''),
CHECK (accepted_candidate_id <> ''),
CHECK (revision > 0),
CHECK (state IN ('PROPOSED','VALIDATED','ACCEPTED','REJECTED'))
);
CREATE TABLE IF NOT EXISTS work.capability_requirements (
capability_requirement_id TEXT PRIMARY KEY,
plan_candidate_id TEXT NOT NULL,
capability_kind TEXT NOT NULL,
capability_id TEXT NOT NULL DEFAULT '',
purpose TEXT NOT NULL,
state TEXT NOT NULL,
epistemic_precondition TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (plan_candidate_id <> ''),
CHECK (capability_kind <> ''),
CHECK (purpose <> ''),
CHECK (state IN ('REQUIRED','AVAILABLE','MISSING','BLOCKED')),
CHECK (epistemic_precondition IN ('UNVERIFIED','VERIFIED','REJECTED','UNKNOWN','NOT_APPLICABLE'))
);
CREATE TABLE IF NOT EXISTS work.plan_steps (
plan_step_id TEXT PRIMARY KEY,
plan_candidate_id TEXT NOT NULL,
ordinal INTEGER NOT NULL,
kind TEXT NOT NULL,
description TEXT NOT NULL,
requires_capability_id TEXT NOT NULL DEFAULT '',
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (plan_candidate_id <> ''),
CHECK (ordinal > 0),
CHECK (kind IN ('INVESTIGATE','OBSERVE','VERIFY','REASON','REPORT','PREPARE','COMMUNICATE','SCHEDULE','MODIFY','RUN_CHECKS','CLARIFY','NO_EXECUTION')),
CHECK (description <> '')
);
CREATE TABLE IF NOT EXISTS work.delegation_candidates (
delegation_candidate_id TEXT PRIMARY KEY,
plan_candidate_id TEXT NOT NULL,
capability_requirement_id TEXT NOT NULL,
kind TEXT NOT NULL,
executor_hint TEXT NOT NULL DEFAULT '',
state TEXT NOT NULL,
reason TEXT NOT NULL,
recorded_at_unix_millis BIGINT NOT NULL,
CHECK (plan_candidate_id <> ''),
CHECK (capability_requirement_id <> ''),
CHECK (kind IN ('AGENT','TOOL','ADAPTER','HUMAN','UNAVAILABLE')),
CHECK (state IN ('PROPOSED','SELECTED','BLOCKED')),
CHECK (reason <> '')
);
RESET ROLE;
COMMIT;
+2 -2
View File
@@ -1,5 +1,5 @@
{
"task_file": "dev/tasks/DEV-000062-human-intent-evidence-project-context.json",
"task_id": "DEV-000062",
"task_file": "dev/tasks/DEV-000063-intent-work-planning-foundation.json",
"task_id": "DEV-000063",
"task_revision": 1
}
@@ -0,0 +1,151 @@
{
"task_id": "DEV-000063",
"task_revision": 1,
"architecture_revision": "ADR-0012",
"base_repo_revision": "18750c2ae69c90336900b333821cfbcf68769c16",
"risk_tier": "E",
"intent": "Add general HelpPath and optional Intent -> Goal -> Work Planning foundation without making Domovoy a Codex prompt generator or granting execution authority.",
"requirement_ids": [
"ADR-0012",
"INV-AUTH-001",
"INV-EPI-001",
"INV-OWN-001"
],
"allowed_change_scope": {
"modules": [
"work",
"interaction",
"situation",
"epistemics",
"capability"
],
"paths": [
"architecture/adrs/ADR-0012-intent-work-planning-foundation.md",
"architecture/architecture-registry.yaml",
"architecture/architecture-registry.json",
"contracts/work/**",
"contracts/interaction/**",
"contracts/capability/**",
"contracts/epistemics/**",
"modules/work/**",
"modules/interaction/**",
"adapters/postgres/**",
"database/bootstrap/**",
"database/migrations/**",
"cmd/core-runtime/**",
"cmd/creator-web/**",
"tests/work_planning/**",
"tests/architecture/**",
"docs/PROJECT_STATUS.md",
"dev/ACTIVE_TASK.json",
"dev/tasks/DEV-000063-intent-work-planning-foundation.json"
]
},
"forbidden_change_scope": {
"paths": [
"architecture/ARCHITECTURE_CONSTITUTION.md",
"modules/authority/**",
"modules/commit/**",
"cmd/commit-runtime/**",
"cmd/cognitive-worker/**",
"security/**",
"ci/**",
".gitea/**"
]
},
"allowed_architecture_delta": {
"new_cross_module_dependencies": 3,
"new_canonical_owners": 0,
"new_privileges": 0,
"new_third_party_dependencies": 0,
"new_provider_adapters": 0
},
"contracts": {
"consume": [
"Interaction OriginalHumanIntent",
"Interaction UtteranceInterpretation",
"Situation ActiveProjectFocus",
"Epistemics VerificationState",
"Capability IDs/health/registry concepts",
"Authority boundary concepts"
],
"may_change": [
"contracts/work@v1",
"contracts/interaction@v1",
"contracts/capability@v1"
]
},
"semantic_owner": "work",
"canonical_state": [
"Goal",
"HelpPath",
"GoalLifecycle",
"PlanCandidate",
"WorkPlan",
"PlanStep",
"CapabilityRequirement",
"DelegationCandidate"
],
"state_transitions": [
"help path ANSWER/COGNITION/DIRECT_CAPABILITY/DURABLE_WORK/CAPABILITY_GAP selected before durable planning",
"goal PROPOSED->UNDERSTOOD->PLANNING->READY when durable work is actually needed",
"plan candidate PROPOSED->VALIDATED->ACCEPTED",
"no transition to IN_PROGRESS without future execution authority"
],
"side_effects_and_privileges": [
"planning-only canonical Work persistence",
"ordinary answer/direct capability/capability gap paths without durable Work persistence",
"no external mutation",
"no Commit/Executor invocation",
"no ACTIVE binding promotion",
"cognitive routing only through existing gateway when later enabled"
],
"persistence_and_migrations": [
"work owner-local goals",
"work owner-local plan candidates",
"work owner-local plan steps",
"work owner-local capability requirements",
"work owner-local delegation candidates",
"no cross-owner FK",
"no cross-owner SQL join dependency"
],
"failure_retry_restart": [
"intent persists even if planning falls back deterministically",
"Goal/Work/Plan are optional and not created for simple help",
"invalid plan candidate fails closed",
"cognition unavailable keeps deterministic planning available",
"planning never claims execution/completion"
],
"assumptions": [
"DEV-000062 is production baseline",
"Work owner is canonical Goal/Task/Decision owner per Core Semantic Model",
"existing CPU CANARY remains not ACTIVE",
"docker-test prohibited"
],
"acceptance_scenarios": [
"investigate UPO performance creates investigation plan, not Codex prompt",
"add support X in Assistant creates MODIFY_CODE requirement without hardcoded executor",
"review new email creates READ_EMAIL and COGNITIVE_REASONING requirements without Codex",
"reminder creates SCHEDULE_REMINDER requirement",
"unverified disk assertion creates verification requirement before risky model install",
"send email creates SEND_EMAIL planning without execution or authority claim",
"cognition unavailable still persists intent/goal/plan when durable work is needed",
"simple explanation can use answer/cognition path without durable Goal/Work/Plan",
"capability gap remains executor-neutral and future self-extension-ready"
],
"required_tests": [
"go test ./...",
"go vet ./...",
"architecture",
"owner ACL",
"LLM-free",
"work planning",
"production live acceptance"
],
"terminal_gap_states": [
"BLOCKED_BY_ARCHITECTURE_GAP",
"BLOCKED_BY_SPEC_GAP",
"BLOCKED_BY_MISSING_CAPABILITY",
"BLOCKED_BY_CONFLICT"
]
}
+13 -1
View File
@@ -1,6 +1,18 @@
# Assistant project status
Audit date: 2026-08-17. Active implementation: DEV-000062 revision 1.
Audit date: 2026-08-17. Active implementation: DEV-000063 revision 1.
## DEV-000063 checkpoint
- `DEV_000063_STATUS = IN_PROGRESS`
- `LAST_COMPLETED = ADR-0012, HelpPath optionality foundation, Work-owned Goal/PlanCandidate/WorkPlan/PlanStep/CapabilityRequirement/DelegationCandidate contracts, deterministic planning V1, optional durable Work creation from /chat, capability-gap/self-extension readiness tests`
- `HELP_PATH_OPTIONALITY = enabled; simple answer/cognition/direct capability/capability gap can bypass durable Goal/Work/Plan`
- `GOAL_WORK_PLAN_OPTIONALITY = enabled; durable records are created only for complex work/planning cases`
- `PROJECT_NEUTRALITY = project context is optional opaque Work reference; Project is not mandatory context`
- `CODEX_NEUTRALITY = no Codex-specific canonical entity or executor selection`
- `TESTS_PASS = 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`
- `NEXT_STEP = commit, push, PR/promotion, migration 017, production deploy, live /chat acceptance`
- `REAL_BLOCKERS = none known`
## DEV-000062 checkpoint
+1 -1
View File
@@ -101,7 +101,7 @@ func ClassifySpeechAct(message string, corrects interaction.HumanUtteranceID) in
switch {
case corrects != "" || strings.Contains(lower, "исправ") || strings.Contains(lower, "не так") || strings.HasPrefix(lower, "нет,"):
return interaction.SpeechActCorrection
case strings.Contains(lower, "?") || strings.HasPrefix(lower, "что ") || strings.HasPrefix(lower, "как ") || strings.HasPrefix(lower, "почему ") || strings.HasPrefix(lower, "кто "):
case strings.Contains(lower, "?") || strings.HasPrefix(lower, "что ") || strings.HasPrefix(lower, "как ") || strings.HasPrefix(lower, "почему ") || strings.HasPrefix(lower, "кто ") || strings.Contains(lower, "объясни"):
return interaction.SpeechActQuestion
case strings.Contains(lower, "проверь") || strings.Contains(lower, "исслед") || strings.Contains(lower, "разбер"):
return interaction.SpeechActRequestToInvestigate
+257
View File
@@ -0,0 +1,257 @@
package domain
import (
"errors"
"strings"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/work"
"git.cin.su/m/Assistant/platform/ids"
)
var ErrInvalidPlanningInput = errors.New("invalid planning input")
var ErrInvalidPlanCandidate = errors.New("invalid plan candidate")
func BuildPlanningResult(input work.PlanningInput, now time.Time) (work.PlanningResult, error) {
if input.UtteranceID == "" || input.IntentID == "" || input.ActorID == "" || input.PrincipalID == "" || strings.TrimSpace(input.Message) == "" {
return work.PlanningResult{}, ErrInvalidPlanningInput
}
millis := now.UnixMilli()
goalID, err := ids.New()
if err != nil {
return work.PlanningResult{}, err
}
candidateID, err := ids.New()
if err != nil {
return work.PlanningResult{}, err
}
planID, err := ids.New()
if err != nil {
return work.PlanningResult{}, err
}
goal := work.Goal{
ID: work.GoalID(goalID),
SourceIntentID: input.IntentID,
SourceUtteranceID: input.UtteranceID,
ActorID: input.ActorID,
PrincipalID: input.PrincipalID,
ScopeID: input.ScopeID,
ProjectID: input.ProjectID,
ProjectDisplayName: input.ProjectDisplayName,
DesiredOutcome: desiredOutcome(input),
Constraints: "planning-only; no execution authority; no external mutation",
State: work.GoalReady,
Provenance: input.ProjectProvenance,
Revision: 1,
CreatedAtUnixMillis: millis,
UpdatedAtUnixMillis: millis,
}
candidate := work.PlanCandidate{
ID: work.PlanCandidateID(candidateID),
GoalID: goal.ID,
Source: "deterministic-planner-v1",
State: work.PlanCandidateAccepted,
Summary: planSummary(input),
ValidationSummary: "validated: structured deterministic candidate; executor-neutral; authority not granted",
RequiresClarification: false,
AcceptedAtUnixMillis: millis,
RecordedAtUnixMillis: millis,
}
addScenarioPlan(input, millis, &candidate)
if hasRiskyUnverifiedAssertion(input) {
addRequirement(&candidate, capability.CapabilityKindVerifyFact, "verify the user-provided resource assertion before risky work", epistemics.VerificationUnverified, millis)
addStep(&candidate, work.PlanStepVerify, "Verify the reported server free-space/capacity using trusted observations before any large model installation.", len(candidate.Steps)+1, millis)
}
if err := ValidatePlanCandidate(candidate); err != nil {
return work.PlanningResult{}, err
}
plan := work.WorkPlan{ID: work.WorkPlanID(planID), GoalID: goal.ID, AcceptedCandidateID: candidate.ID, Revision: 1, State: candidate.State, Summary: candidate.Summary, RecordedAtUnixMillis: millis}
return work.PlanningResult{Goal: goal, Candidate: candidate, Plan: plan, Reply: planningReply(goal, candidate)}, nil
}
func DetermineHelpPath(input work.PlanningInput) work.HelpPath {
lower := strings.ToLower(input.Message)
switch {
case containsAny(lower, "объясни", "расскажи", "что делает"):
return work.HelpPath{Kind: work.HelpPathCognition, Reason: "simple help can be answered without durable Goal/Work/Plan"}
case containsAny(lower, "напомни", "напомин"):
return work.HelpPath{Kind: work.HelpPathDirectCapability, SuggestedCapabilityKinds: []capability.CapabilityKind{capability.CapabilityKindScheduleReminder}, Reason: "reminder is a direct scheduling-capability path, not a coding/project workflow", Reply: "Понял: нужен reminder/scheduler capability. В DEV-000063 я фиксирую это как безопасный путь помощи; само напоминание ещё не зарегистрировано, потому что scheduling adapter не подключён."}
case containsAny(lower, "не умеешь", "подключи интеграцию", "интеграц", "неизвестн", "нет capability", "создай adapter", "создай адаптер"):
return work.HelpPath{Kind: work.HelpPathCapabilityGap, SuggestedCapabilityKinds: []capability.CapabilityKind{}, CapabilityGap: true, Reason: "requested help needs a currently missing capability and may later trigger acquisition/self-extension", Reply: "Похоже, для этой просьбы нужна capability, которой сейчас нет. Это не задача конкретного executor автоматически: в будущем Домовой сможет найти, подключить или создать подходящую capability и вернуться к исходной потребности."}
case requiresDurableWork(lower):
return work.HelpPath{Kind: work.HelpPathDurableWork, NeedsDurableGoal: true, NeedsDurableWork: true, NeedsDurablePlan: true, Reason: "request needs durable coordination, planning, investigation, delegation, or outcome tracking"}
default:
return work.HelpPath{Kind: work.HelpPathAnswer, Reason: "ordinary interaction does not require durable Work"}
}
}
func ValidatePlanCandidate(candidate work.PlanCandidate) error {
if candidate.ID == "" || candidate.GoalID == "" || candidate.Source == "" || candidate.Summary == "" || len(candidate.Steps) == 0 {
return ErrInvalidPlanCandidate
}
switch candidate.State {
case work.PlanCandidateProposed, work.PlanCandidateValidated, work.PlanCandidateAccepted, work.PlanCandidateRejected:
default:
return ErrInvalidPlanCandidate
}
ordinals := map[int]bool{}
for _, step := range candidate.Steps {
if step.ID == "" || step.PlanCandidateID != candidate.ID || step.Ordinal < 1 || step.Kind == "" || step.Description == "" || ordinals[step.Ordinal] {
return ErrInvalidPlanCandidate
}
ordinals[step.Ordinal] = true
}
seenRequirements := map[work.CapabilityRequirementID]bool{}
for _, requirement := range candidate.Requirements {
if requirement.ID == "" || requirement.PlanCandidateID != candidate.ID || requirement.CapabilityKind == "" || requirement.Purpose == "" || requirement.State == "" || requirement.EpistemicPrecondition == "" {
return ErrInvalidPlanCandidate
}
seenRequirements[requirement.ID] = true
}
for _, delegation := range candidate.Delegations {
if delegation.ID == "" || delegation.PlanCandidateID != candidate.ID || delegation.CapabilityRequirementID == "" || delegation.Kind == "" || delegation.State == "" || !seenRequirements[delegation.CapabilityRequirementID] {
return ErrInvalidPlanCandidate
}
}
return nil
}
func addScenarioPlan(input work.PlanningInput, millis int64, candidate *work.PlanCandidate) {
lower := strings.ToLower(input.Message)
switch {
case containsAny(lower, "тормозит", "медлен", "производительн", "разберись", "почему"):
addRequirement(candidate, capability.CapabilityKindInspectProject, "inspect project context", epistemics.VerificationNotApplicable, millis)
addRequirement(candidate, capability.CapabilityKindReadDatabase, "collect database performance observations", epistemics.VerificationUnknown, millis)
addRequirement(candidate, capability.CapabilityKindQueryLogs, "collect runtime/log observations", epistemics.VerificationUnknown, millis)
addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "compare evidence and identify likely causes", epistemics.VerificationUnknown, millis)
addStep(candidate, work.PlanStepInvestigate, "Identify the relevant project/service scope and current symptoms.", 1, millis)
addStep(candidate, work.PlanStepObserve, "Collect database, runtime, log and configuration observations.", 2, millis)
addStep(candidate, work.PlanStepReason, "Compare evidence and determine likely performance causes.", 3, millis)
addStep(candidate, work.PlanStepReport, "Report findings and what remains unverified.", 4, millis)
case containsAny(lower, "добавь", "поддержк", "возможность", "реализ"):
addRequirement(candidate, capability.CapabilityKindModifyCode, "modify software project", epistemics.VerificationNotApplicable, millis)
addRequirement(candidate, capability.CapabilityKindRunTests, "validate software change", epistemics.VerificationNotApplicable, millis)
addStep(candidate, work.PlanStepInvestigate, "Understand desired capability and affected project scope.", 1, millis)
addStep(candidate, work.PlanStepModify, "Prepare a code-change work item for a capable coding executor.", 2, millis)
addStep(candidate, work.PlanStepRunChecks, "Run relevant tests and architecture checks before outcome claim.", 3, millis)
case containsAny(lower, "письм", "почт"):
addRequirement(candidate, capability.CapabilityKindReadEmail, "read incoming communications", epistemics.VerificationUnknown, millis)
if containsAny(lower, "ответ", "ответить", "скажи") {
addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "classify messages needing response", epistemics.VerificationUnknown, millis)
}
if containsAny(lower, "отправь", "напиши") {
addRequirement(candidate, capability.CapabilityKindSendEmail, "send email only after authority", epistemics.VerificationNotApplicable, millis)
}
addStep(candidate, work.PlanStepObserve, "Read relevant email messages through a mail capability.", 1, millis)
addStep(candidate, work.PlanStepReason, "Classify which messages need a response and why.", 2, millis)
addStep(candidate, work.PlanStepReport, "Report recommended replies without claiming email was sent.", 3, millis)
case containsAny(lower, "напомни", "завтра", "напомин"):
addRequirement(candidate, capability.CapabilityKindScheduleReminder, "schedule a future reminder after temporal interpretation", epistemics.VerificationNotApplicable, millis)
addStep(candidate, work.PlanStepSchedule, "Interpret the requested reminder time and reminder subject.", 1, millis)
addStep(candidate, work.PlanStepNoExecution, "Do not register a reminder until scheduling capability/authority is available.", 2, millis)
case containsAny(lower, "отправь", "ивану", "сообщи"):
addRequirement(candidate, capability.CapabilityKindSendEmail, "communicate with the named recipient after channel/authority resolution", epistemics.VerificationNotApplicable, millis)
addStep(candidate, work.PlanStepPrepare, "Resolve recipient and appropriate communication channel.", 1, millis)
addStep(candidate, work.PlanStepCommunicate, "Prepare the message for a communication adapter; do not send without authority.", 2, millis)
default:
addRequirement(candidate, capability.CapabilityKindCognitiveReasoning, "clarify/decompose ambiguous human intent", epistemics.VerificationUnknown, millis)
addStep(candidate, work.PlanStepClarify, "Clarify desired outcome and required capabilities.", 1, millis)
candidate.RequiresClarification = true
}
for _, requirement := range candidate.Requirements {
addDelegation(candidate, requirement, millis)
}
}
func requiresDurableWork(lower string) bool {
return containsAny(lower, "тормозит", "медлен", "производительн", "разберись", "почему", "добавь", "поддержк", "возможность", "реализ", "письм", "почт", "отправь", "ивану", "сообщи", "установи", "большую модель")
}
func addRequirement(candidate *work.PlanCandidate, kind capability.CapabilityKind, purpose string, precondition epistemics.VerificationState, millis int64) {
id, err := ids.New()
if err != nil {
return
}
candidate.Requirements = append(candidate.Requirements, work.CapabilityRequirement{ID: work.CapabilityRequirementID(id), PlanCandidateID: candidate.ID, CapabilityKind: kind, Purpose: purpose, State: work.CapabilityRequirementMissing, EpistemicPrecondition: precondition, RecordedAtUnixMillis: millis})
}
func addStep(candidate *work.PlanCandidate, kind work.PlanStepKind, description string, ordinal int, millis int64) {
id, err := ids.New()
if err != nil {
return
}
var requirementID work.CapabilityRequirementID
if len(candidate.Requirements) > 0 {
requirementID = candidate.Requirements[len(candidate.Requirements)-1].ID
}
candidate.Steps = append(candidate.Steps, work.PlanStep{ID: work.PlanStepID(id), PlanCandidateID: candidate.ID, Ordinal: ordinal, Kind: kind, Description: description, RequiresCapabilityID: requirementID, RecordedAtUnixMillis: millis})
}
func addDelegation(candidate *work.PlanCandidate, requirement work.CapabilityRequirement, millis int64) {
id, err := ids.New()
if err != nil {
return
}
candidate.Delegations = append(candidate.Delegations, work.DelegationCandidate{ID: work.DelegationCandidateID(id), PlanCandidateID: candidate.ID, CapabilityRequirementID: requirement.ID, Kind: delegationKind(requirement.CapabilityKind), ExecutorHint: "", State: work.DelegationCandidateBlocked, Reason: "no concrete executor selected in DEV-000063 planning-only mode", RecordedAtUnixMillis: millis})
}
func delegationKind(kind capability.CapabilityKind) work.DelegationKind {
switch kind {
case capability.CapabilityKindModifyCode, capability.CapabilityKindCognitiveReasoning, capability.CapabilityKindWebResearch:
return work.DelegationKindAgent
case capability.CapabilityKindReadEmail, capability.CapabilityKindSendEmail, capability.CapabilityKindReadDatabase, capability.CapabilityKindQueryLogs, capability.CapabilityKindScheduleReminder, capability.CapabilityKindInspectProject, capability.CapabilityKindRunTests, capability.CapabilityKindVerifyFact:
return work.DelegationKindAdapter
default:
return work.DelegationKindUnavailable
}
}
func desiredOutcome(input work.PlanningInput) string {
lower := strings.ToLower(input.Message)
switch {
case containsAny(lower, "тормозит", "производительн"):
return "identify likely performance cause with sufficient evidence"
case containsAny(lower, "письм", "почт"):
return "review communications and identify what needs response"
case containsAny(lower, "напомни", "напомин"):
return "create a future reminder when scheduling capability is available"
case containsAny(lower, "отправь", "сообщи"):
return "communicate requested information to the recipient after authority"
case containsAny(lower, "добавь", "поддержк", "возможность"):
return "software project gains the requested capability after validated implementation"
default:
return strings.TrimSpace(input.Message)
}
}
func planSummary(input work.PlanningInput) string {
return "Planning foundation for: " + desiredOutcome(input)
}
func planningReply(goal work.Goal, candidate work.PlanCandidate) string {
requirements := make([]string, 0, len(candidate.Requirements))
for _, requirement := range candidate.Requirements {
requirements = append(requirements, string(requirement.CapabilityKind))
}
project := goal.ProjectDisplayName
if project == "" {
project = "текущий контекст"
}
return "Понял цель: " + goal.DesiredOutcome + ". Контекст: " + project + ". План сохранён в безопасном режиме: нужны capability " + strings.Join(requirements, ", ") + ". Исполнение этих инструментов пока не выполнено и требует отдельной capability/authority."
}
func hasRiskyUnverifiedAssertion(input work.PlanningInput) bool {
lower := strings.ToLower(input.Message)
return input.AssertionVerification == epistemics.VerificationUnverified && containsAny(lower, "300 гб", "свободно", "большую модель", "установи", "модель")
}
func containsAny(value string, needles ...string) bool {
for _, needle := range needles {
if strings.Contains(value, needle) {
return true
}
}
return false
}
+161
View File
@@ -0,0 +1,161 @@
package domain
import (
"strings"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/work"
)
func TestPlanningInvestigationIsNotCodexPrompt(t *testing.T) {
result := planForTest(t, "Разберись, почему УПО тормозит.", "REQUEST_TO_INVESTIGATE", epistemics.VerificationNotApplicable)
requireKinds(t, result, capability.CapabilityKindInspectProject, capability.CapabilityKindReadDatabase, capability.CapabilityKindQueryLogs, capability.CapabilityKindCognitiveReasoning)
for _, delegation := range result.Candidate.Delegations {
if strings.Contains(strings.ToLower(delegation.ExecutorHint), "codex") {
t.Fatalf("planning must not hardcode Codex executor: %#v", delegation)
}
}
if strings.Contains(strings.ToLower(result.Candidate.Summary), "prompt") {
t.Fatalf("canonical plan must not be prompt-centric: %q", result.Candidate.Summary)
}
}
func TestPlanningCodeChangeRequiresModifyCodeWithoutExecutor(t *testing.T) {
result := planForTest(t, "В Assistant добавь поддержку X.", "DIRECTIVE", epistemics.VerificationNotApplicable)
requireKinds(t, result, capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests)
for _, delegation := range result.Candidate.Delegations {
if delegation.ExecutorHint != "" {
t.Fatalf("executor must remain unselected in canonical planning: %#v", delegation)
}
}
}
func TestPlanningEmailReviewIsNotCodexSpecific(t *testing.T) {
result := planForTest(t, "Посмотри новые письма и скажи, на какие нужно ответить.", "DIRECTIVE", epistemics.VerificationNotApplicable)
requireKinds(t, result, capability.CapabilityKindReadEmail, capability.CapabilityKindCognitiveReasoning)
requireNoKind(t, result, capability.CapabilityKindModifyCode)
}
func TestPlanningReminderUsesSchedulingRequirement(t *testing.T) {
result := planForTest(t, "Напомни завтра позвонить врачу.", "DIRECTIVE", epistemics.VerificationNotApplicable)
requireKinds(t, result, capability.CapabilityKindScheduleReminder)
requireNoKind(t, result, capability.CapabilityKindModifyCode)
}
func TestPlanningRiskyUnverifiedAssertionRequiresVerification(t *testing.T) {
result := planForTest(t, "На сервере 300 ГБ свободно, установи большую модель.", "ASSERTION", epistemics.VerificationUnverified)
requireKinds(t, result, capability.CapabilityKindVerifyFact)
found := false
for _, requirement := range result.Candidate.Requirements {
if requirement.CapabilityKind == capability.CapabilityKindVerifyFact && requirement.EpistemicPrecondition == epistemics.VerificationUnverified {
found = true
}
}
if !found {
t.Fatalf("expected unverified verification precondition: %#v", result.Candidate.Requirements)
}
}
func TestPlanningSendEmailPreservesAuthorityBoundary(t *testing.T) {
result := planForTest(t, "Отправь Ивану письмо.", "DIRECTIVE", epistemics.VerificationNotApplicable)
requireKinds(t, result, capability.CapabilityKindSendEmail)
if result.Goal.State == work.GoalCompleted || !strings.Contains(result.Reply, "требует отдельной capability/authority") {
t.Fatalf("planning must not claim execution/completion: goal=%#v reply=%q", result.Goal, result.Reply)
}
}
func TestPlanningFallbackPersistsWithoutCognition(t *testing.T) {
result := planForTest(t, "Сделай что-нибудь полезное.", "DIRECTIVE", epistemics.VerificationUnknown)
if result.Goal.ID == "" || result.Candidate.ID == "" || result.Plan.ID == "" {
t.Fatalf("expected persisted planning identities: %#v", result)
}
if result.Candidate.Source != "deterministic-planner-v1" {
t.Fatalf("expected deterministic fallback source: %s", result.Candidate.Source)
}
}
func TestHelpPathSimpleAnswerDoesNotRequireDurableGoalWorkPlan(t *testing.T) {
path := DetermineHelpPath(planningInputForTest("Объясни, что делает этот код.", "QUESTION", epistemics.VerificationNotApplicable))
if path.Kind != work.HelpPathCognition || path.NeedsDurableGoal || path.NeedsDurableWork || path.NeedsDurablePlan {
t.Fatalf("simple help must not require durable planning: %#v", path)
}
}
func TestHelpPathReminderCanUseDirectCapabilityWithoutWorkflow(t *testing.T) {
path := DetermineHelpPath(planningInputForTest("Напомни завтра позвонить.", "DIRECTIVE", epistemics.VerificationNotApplicable))
if path.Kind != work.HelpPathDirectCapability || path.NeedsDurableWork || len(path.SuggestedCapabilityKinds) != 1 || path.SuggestedCapabilityKinds[0] != capability.CapabilityKindScheduleReminder {
t.Fatalf("reminder should be direct scheduling capability path: %#v", path)
}
}
func TestHelpPathComplexWorkMaterializesDurablePlanning(t *testing.T) {
path := DetermineHelpPath(planningInputForTest("Разберись, почему УПО тормозит.", "REQUEST_TO_INVESTIGATE", epistemics.VerificationNotApplicable))
if path.Kind != work.HelpPathDurableWork || !path.NeedsDurableGoal || !path.NeedsDurableWork || !path.NeedsDurablePlan {
t.Fatalf("complex investigation should need durable planning: %#v", path)
}
}
func TestHelpPathCapabilityGapDoesNotBecomeCodexTask(t *testing.T) {
path := DetermineHelpPath(planningInputForTest("Подключи интеграцию с неизвестным сервисом, которого сейчас нет.", "DIRECTIVE", epistemics.VerificationNotApplicable))
if path.Kind != work.HelpPathCapabilityGap || !path.CapabilityGap {
t.Fatalf("expected capability gap path: %#v", path)
}
if strings.Contains(strings.ToLower(path.Reply), "codex") {
t.Fatalf("capability gap must not be represented as Codex task: %#v", path)
}
}
func planForTest(t *testing.T, message string, act string, verification epistemics.VerificationState) work.PlanningResult {
t.Helper()
result, err := BuildPlanningResult(planningInputForTest(message, act, verification), time.Unix(100, 0))
if err != nil {
t.Fatal(err)
}
if err := ValidatePlanCandidate(result.Candidate); err != nil {
t.Fatal(err)
}
return result
}
func planningInputForTest(message string, act string, verification epistemics.VerificationState) work.PlanningInput {
return work.PlanningInput{
ConversationID: "conversation-1",
UtteranceID: "utterance-1",
IntentID: "intent-1",
ActorID: identity.ActorID("actor-1"),
PrincipalID: identity.PrincipalID("principal-1"),
ProjectID: work.ProjectRef("upo"),
ProjectDisplayName: "УПО",
ProjectProvenance: work.ProjectProvenanceRef("EXPLICIT"),
Message: message,
SpeechAct: work.SpeechActRef(act),
AssertionVerification: verification,
ReceivedAtUnixMillis: time.Unix(100, 0).UnixMilli(),
}
}
func requireKinds(t *testing.T, result work.PlanningResult, kinds ...capability.CapabilityKind) {
t.Helper()
actual := map[capability.CapabilityKind]bool{}
for _, requirement := range result.Candidate.Requirements {
actual[requirement.CapabilityKind] = true
}
for _, kind := range kinds {
if !actual[kind] {
t.Fatalf("missing capability kind %s in %#v", kind, result.Candidate.Requirements)
}
}
}
func requireNoKind(t *testing.T, result work.PlanningResult, kind capability.CapabilityKind) {
t.Helper()
for _, requirement := range result.Candidate.Requirements {
if requirement.CapabilityKind == kind {
t.Fatalf("unexpected capability kind %s in %#v", kind, result.Candidate.Requirements)
}
}
}