mandatory pre-plan flow: goal → research → plan → APPROVE → execute
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

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:
2026-07-14 13:33:54 +02:00
parent 24cc3b1f4e
commit 5caf49bf48
5 changed files with 61 additions and 6 deletions

View File

@@ -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
}

View File

@@ -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."
}