mandatory pre-plan flow: goal → research → plan → APPROVE → execute
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.
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user