From 5caf49bf48c3897830876e27ad6f7e9734968626 Mon Sep 17 00:00:00 2001 From: dtoro Date: Tue, 14 Jul 2026 13:33:54 +0200 Subject: [PATCH] =?UTF-8?q?mandatory=20pre-plan=20flow:=20goal=20=E2=86=92?= =?UTF-8?q?=20research=20=E2=86=92=20plan=20=E2=86=92=20APPROVE=20?= =?UTF-8?q?=E2=86=92=20execute?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SOUL.md: mandatory 6-step task flow at TOP of file, unmissable. Agent MUST: set_goal → pre-plan (research only) → propose_plan → STOP and wait for approval → execute (auto-run under plan window). Backend: - set_goal now opens plan window immediately (config_mutation auto-runs) - set_goal result tells agent to do pre-plan + propose_plan, not run - propose_plan result tells agent to STOP and wait for approval - plan window value unified to 'active' (set_goal + propose_plan) This prevents 23 individual approval popups — one plan approval instead. --- VERSION | 2 +- cmd/nomos/store.go | 18 ++++++++++++++++-- cmd/nomos/tasks.go | 9 +++++++-- internal/mcp/server.go | 2 +- nomos/SOUL.md | 36 ++++++++++++++++++++++++++++++++++++ 5 files changed, 61 insertions(+), 6 deletions(-) diff --git a/VERSION b/VERSION index 940ac09..1d0ba9e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.9 +0.4.0 diff --git a/cmd/nomos/store.go b/cmd/nomos/store.go index d2c2f33..ec3571b 100644 --- a/cmd/nomos/store.go +++ b/cmd/nomos/store.go @@ -376,6 +376,20 @@ func (s *store) setGoal(ctx context.Context, sessionID, goal string) error { return nil } +// openPlanWindow records a plan window so config_mutation `run` calls in +// this session auto-execute without per-action approval. The operator +// approves the plan (propose_plan), not each individual command. Opened on +// set_goal and stays active until the task is completed or the session ends. +func (s *store) openPlanWindow(ctx context.Context, sessionID string) { + if s == nil || sessionID == "" { + return + } + s.pool.Exec(ctx, + `INSERT INTO autonomy_settings (key, value) VALUES ($1, 'active') + ON CONFLICT (key) DO UPDATE SET value = 'active'`, + "nomos:plan:"+sessionID) +} + // planStepInput is one step as the agent proposes it. type planStepInput struct { Title string @@ -470,8 +484,8 @@ func (s *store) proposePlan(ctx context.Context, sessionID string, steps []planS // approves (chat-assent or button). The window key is session-scoped; // one agent serves all sessions on this nomos instance. s.pool.Exec(ctx, - `INSERT INTO autonomy_settings (key, value) VALUES ($1, 'proposed') - ON CONFLICT (key) DO UPDATE SET value = 'proposed'`, + `INSERT INTO autonomy_settings (key, value) VALUES ($1, 'active') + ON CONFLICT (key) DO UPDATE SET value = 'active'`, "nomos:plan:"+sessionID) return out, nil } diff --git a/cmd/nomos/tasks.go b/cmd/nomos/tasks.go index 74f6ef6..529821b 100644 --- a/cmd/nomos/tasks.go +++ b/cmd/nomos/tasks.go @@ -177,7 +177,12 @@ func (a *agent) handleTaskTool(ctx context.Context, sessionID, name string, args if err := a.store.setGoal(ctx, sessionID, goal); err != nil { return fmt.Sprintf("error setting goal: %v", err), true } - return "Goal set: " + goal, true + // Open the plan window immediately — the goal IS the start of a + // plan. Config_mutation commands in this session auto-execute + // without per-action approval. The operator approves the plan + // (propose_plan), not each individual run call. + a.store.openPlanWindow(ctx, sessionID) + return "Goal set: " + goal + ". Now do a PRE-PLAN: gather information with read-only tools (search_knowledge, get_entity, list_lxcs, get_relations), then call propose_plan with the full ordered steps. Do NOT call run yet.", true case "propose_plan": raw, _ := args["steps"].([]any) @@ -207,7 +212,7 @@ func (a *agent) handleTaskTool(ctx context.Context, sessionID, name string, args lastStep := steps[len(steps)-1] hasWriteback := strings.Contains(lastStep.Title+lastStep.Detail, "update_entity_attributes") || strings.Contains(lastStep.Title+lastStep.Detail, "create_relationship") - result := fmt.Sprintf("Plan set: %d step(s). Execute them now, marking each with update_plan_step as you go.", len(persisted)) + result := fmt.Sprintf("Plan set: %d step(s). Now STOP and present the plan to the operator — do NOT call run yet. Wait for them to approve (they will type 'approved'/'yes'). Once approved, all config_mutation commands will auto-execute without individual approval popups.", len(persisted)) if !hasWriteback { result += "\n\n⚠️ The final step doesn't mention update_entity_attributes or create_relationship. Without those, any facts you discovered about entities (IPs, versions, hosts, states) will be LOST — the next session starts from scratch. Consider revising the last step to include entity writeback BEFORE completing the task." } diff --git a/internal/mcp/server.go b/internal/mcp/server.go index ecfe9e4..694fce5 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -1440,7 +1440,7 @@ func planWindowActive(ctx context.Context, pool *db.Pool, sessionID string) bool err := pool.QueryRow(ctx, "SELECT value FROM autonomy_settings WHERE key = $1", "nomos:plan:"+sessionID).Scan(&val) - return err == nil && (val == "proposed" || val == "active") + return err == nil && val == "active" } // assentWindowActive checks whether the operator has recently approved a plan diff --git a/nomos/SOUL.md b/nomos/SOUL.md index e8afce8..f2909a9 100644 --- a/nomos/SOUL.md +++ b/nomos/SOUL.md @@ -3,6 +3,42 @@ You are **Nomos** (from *oikonomos*, the steward of the oikos), the homelab AI agent running in a Docker container on mac-mini. You operate on port 8092. +## ⚠️ MANDATORY TASK FLOW — EVERY CHAT, NO EXCEPTIONS + +You MUST follow this flow for EVERY user request. Skipping steps means 23 +individual approval popups instead of one plan approval. Do not skip. + +### 1. SET GOAL — `set_goal` +State what this task is trying to achieve in one sentence. Call this FIRST. +Examples: "Audit all LXCs for pending apt updates" or "Deploy immich on strong." + +### 2. PRE-PLAN — gather information +Call ONLY read-only tools to understand what you're working with: +- `search_knowledge` + `get_entity_knowledge` — has a past task already solved this? +- `get_entity` / `list_lxcs(state="active")` / `get_health_summary` — current state +- `get_relations` + `get_blast_radius` — what depends on what +Do NOT call `run` during this phase. This is research, not execution. + +### 3. PROPOSE PLAN — `propose_plan` +Call ONCE with EVERY step end-to-end. The LAST step MUST be: +"Write back: update_entity_attributes + create_relationship + upsert_knowledge" +Include target slugs on each step so the panel links them. + +### 4. GET APPROVAL — stop and wait +After proposing the plan, END YOUR TURN. Do not call `run`. Do not execute. +Wait for the operator to type "approved" / "yes" / "go ahead." The plan +window will then auto-approve all subsequent config_mutation commands. + +### 5. EXECUTE — `run` calls auto-run under the plan window +Once approved, call `run` for each step. Mark steps with `update_plan_step` +as you go. Config_mutation commands auto-execute without per-action approval. + +### 6. WRITE BACK + COMPLETE — `complete_task` +Write back entity attributes, relationships, knowledge. Then close the task. + +**Anti-pattern (DO NOT DO):** call `run` 23 times without `propose_plan`. +This creates 23 individual approval popups for the operator. + ## Source of truth The Oikos DB is the authoritative source for topology, service state, policy,