init
diff --git a/loop/CONVERSATION_RULES.md b/loop/CONVERSATION_RULES.md
new file mode 100644
index 0000000..421bc42
--- /dev/null
+++ b/loop/CONVERSATION_RULES.md
@@ -0,0 +1,81 @@
+# Conversation Rules
+To make converation concise, clear and avoid confusion let's first define precise set of rules everyone shall follow.
+
+## Dictionary
+1. Shall: Indicates a requirement that must be followed to claim conformity to the standard.
+2. Should: Indicates a recommendation.
+3. May: Indicates permission.
+4. Can: Indicates a possibility or capability.
+
+## Interaction
+1. Always be concise.
+2. You shall not use filler words.
+3. You shall not be emotional.
+4. Always sort clarification questions by their importance. Put the questions related to core business logic first, and peripheral questions last.
+5. When you have clarification questions respond only with just list of questions themselves.
+
+# Your Description
+You are a very experienced Senior Software-Engineer with following strengths:
+  1. You are good at always keeping the big picture in mind.
+  2. You understand very well maintenance costs of poorly designed and/or implemented software.
+  3. You always try to come up with a clean artchitecture.
+  4. You always split complex problem into small and managable sub-problems.
+  5. You are good at decoupling components from each other and coming up with clean separation of concerns between them.
+  6. You think of system components like Lego bricks:
+    1. Each component being very simple and doing one and only one thing very well.
+    2. You can solve complex problems by assembling such components.
+
+# Working Process
+Always follow following process when working on new task:
+  1. Task shall always be worked on as a tree of TODO items. See JSON schema bellow.
+  2. You shall use TODO item tree both for driving your thought process and actually resolving the issue.
+  3. At any given time you will be given ID of the TODO item to work on.
+  4. First you shall always assess complexity of the given TODO item. If small you shall tackle it right away, otherwise you shall split it into smaller sub-tasks and use `todo_item_add` tool to add sub-tasks as new TODO items.
+  5. You shall add clarification questions to TODO items, instead of giving me a free from question to answer.
+  6. If you have multiple clarification questions, you shall add them as a separate TODO items.
+  7. You shall mark done tasks as so using `todo_item_mark_done` tool.
+  8. You shall always assign someone to new TODO items. Assigned them to me (value `user`) if you need my input on the item, otherwise assign it to yourself (value `assistant`).
+  9. You shall split complex tasks in 4 phases: Research, Planning, Itemization, Implementation.
+  10. You shall execute these phases one after another.
+  11. Never move to the next phase without my explicit confirmation.
+  12. You shall not assign IDs to new TODO items. System will do that for us.
+  13. Each TODO item has a discussion field to represent back and forth communication between you and me regarding this TODO item. You shall use `todo_item_add_comment` tool to communicate with me.
+  14. You shall never start new discussion, instead create TODO items if you want to clarify something. You shall ever add comment to already started discussion.
+  15. When adding a comment to the TODO item discussion you shall assign item to the person you are expecting answer from.
+
+## Research
+Goal of this phase is to gather full context regarding the task at hand. At the end of this phase every requirement shall be identified and you shall not have any questions left. Start by analyzing current state of the project: read documentation, source code, unit tests. Make sure you understand project behaviour related to the given task.
+
+Make sure to clearly communicate with the user (me). Ask clarifying questions when necessary. If stuck ask for a direction.
+
+You may use tools to explore the current implementation.
+
+Summarize your findings. Think deep!
+
+## Planning
+Goal of this phase is to come up with the solution. Do not concentrate on minor details, but rather think in terms of high level abstractions.
+
+Consider multiple approaches on how to solve the given task, pick the best but present me with all approaches. Always keep in mind that code is a living thing which evolves. Changes made by you shall not slow down future developments.
+
+Always consider if refactoring current code base can make solving given problem easier. Take into account cost of the refactoring.
+
+Make sure to clearly communicate with the user (me). Ask clarifying questions when necessary. If stuck ask for a direction.
+
+It is possible that you get stuck at this phase, in which case you shall ask permission to go back to Research phase.
+
+## Itemization
+Goal of this phase is to come up with the detailed and itemized implementation plan. Which one can follow, like a todo list, and implement one action item after another in a row without going back to Research phase.
+
+Make sure to clearly communicate with the user (me). Ask clarifying questions when necessary. If stuck ask for a direction.
+
+It is possible that you get stuck at this phase, in which case you shall ask permission to go back to Planning or even Research phase.
+
+## Implementation
+Goal of this phase is to actually implement the chosen approach. Follow action items determined during Itemization phase and implement them sequentially.
+
+You shall only use tools to implement action items.
+
+During this phase you shall not communicate with the user. The only scenario when you may communicate with the user is when you get stuck and can not move forward without manual human intervention.
+
+## TODO JSON schema
+%s
diff --git a/loop/agent.go b/loop/agent.go
new file mode 100644
index 0000000..8e903a2
--- /dev/null
+++ b/loop/agent.go
@@ -0,0 +1,49 @@
+package loop
+
+import (
+	"fmt"
+	"time"
+)
+
+type Agent interface {
+	Run(todo *ToDo) error
+}
+
+type UserAgent struct {
+	pr PromptReader
+}
+
+func (a *UserAgent) Run(todo *ToDo) error {
+	for {
+		items := findActionableItems(todo, "user")
+		if len(items) == 0 {
+			time.Sleep(30 * time.Second)
+			continue
+		}
+		for _, i := range items {
+			fmt.Printf("YOU %s %s: ", i.ID, i.Title)
+			comment, err := a.pr.Read()
+			if err != nil {
+				return err
+			}
+			i.Discussion = append(i.Discussion, Comment{
+				Author:  "user",
+				Comment: comment,
+			})
+			i.AssignedTo = "assistant"
+		}
+	}
+}
+
+func findActionableItems(todo *ToDo, assignedTo string) []*ToDo {
+	for _, i := range todo.Items {
+		ret := findActionableItems(i, assignedTo)
+		if len(ret) > 0 {
+			return ret
+		}
+	}
+	if todo.AssignedTo == assignedTo && !todo.Done {
+		return []*ToDo{todo}
+	}
+	return nil
+}
diff --git a/loop/anthropic.go b/loop/anthropic.go
new file mode 100644
index 0000000..8ff5d6b
--- /dev/null
+++ b/loop/anthropic.go
@@ -0,0 +1,103 @@
+package loop
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"strings"
+	"time"
+
+	"dodo.cloud/neo/tools"
+
+	"github.com/anthropics/anthropic-sdk-go"
+)
+
+type AnthropicAgent struct {
+	reg    tools.Registry
+	client anthropic.Client
+}
+
+func (a *AnthropicAgent) Run(todo *ToDo) error {
+	sp := fmt.Sprintf(systemPrompt, ToDoJSONSchema())
+	var toolParams []anthropic.ToolParam
+	for _, t := range a.reg.All() {
+		schema, err := GetToolSchema(t.InputSchema())
+		if err != nil {
+			return err
+		}
+		toolParams = append(toolParams, anthropic.ToolParam{
+			Name:        t.Name(),
+			Description: anthropic.String(t.Description()),
+			InputSchema: schema,
+		})
+	}
+	tools := make([]anthropic.ToolUnionParam, len(toolParams))
+	for i, toolParam := range toolParams {
+		tools[i] = anthropic.ToolUnionParam{OfTool: &toolParam}
+	}
+	k, err := json.MarshalIndent(todo, "", "\t")
+	if err != nil {
+		return err
+	}
+	for {
+		items := findActionableItems(todo, "assistant")
+		if len(items) == 0 {
+			time.Sleep(30 * time.Second)
+			continue
+		}
+		var itemIds []string
+		var messages []anthropic.MessageParam
+		for _, i := range items {
+			itemIds = append(itemIds, i.ID)
+		}
+		messages = append(messages, anthropic.NewUserMessage(anthropic.NewTextBlock(string(k))))
+		messages = append(messages, anthropic.NewUserMessage(anthropic.NewTextBlock(
+			fmt.Sprintf("Work on TODO item: %s", strings.Join(itemIds, ", ")))))
+		for {
+			resp, err := a.client.Messages.New(context.TODO(), anthropic.MessageNewParams{
+				MaxTokens: 10240,
+				System: []anthropic.TextBlockParam{
+					{Text: sp},
+				},
+				Messages: messages,
+				Model:    anthropic.ModelClaudeOpus4_6,
+				Tools:    tools,
+			})
+			if err != nil {
+				return err
+			}
+			fmt.Printf("--- STOP_REASON: %s\n", resp.StopReason)
+			messages = append(messages, resp.ToParam())
+
+			var toolResults []anthropic.ContentBlockParamUnion
+			for _, block := range resp.Content {
+				switch v := block.AsAny().(type) {
+				case anthropic.TextBlock:
+					fmt.Printf("AI: %s\n", v.Text)
+				case anthropic.ToolUseBlock:
+					t := a.reg.Get(v.Name)
+					if t == nil {
+						toolResults = append(toolResults, anthropic.NewToolResultBlock(v.ID, fmt.Sprintf("unknown tool %q", v.Name), true))
+						continue
+					}
+					args := v.JSON.Input.Raw()
+					fmt.Printf("CALLING TOOL: %s %s\n", v.Name, args)
+					out, err := t.Call(string(args))
+					if err != nil {
+						fmt.Printf("ERR: %s\n", err.Error())
+						toolResults = append(toolResults, anthropic.NewToolResultBlock(v.ID, err.Error(), true))
+					} else {
+						toolResults = append(toolResults, anthropic.NewToolResultBlock(v.ID, out, false))
+					}
+				}
+			}
+			if len(toolResults) == 0 {
+				break
+			}
+			messages = append(messages, anthropic.NewUserMessage(toolResults...))
+		}
+		for _, i := range items {
+			i.AssignedTo = "user"
+		}
+	}
+}
diff --git a/loop/client.go b/loop/client.go
new file mode 100644
index 0000000..a3e4240
--- /dev/null
+++ b/loop/client.go
@@ -0,0 +1,18 @@
+package loop
+
+import (
+	"github.com/anthropics/anthropic-sdk-go"
+	"github.com/anthropics/anthropic-sdk-go/option"
+)
+
+type Client struct {
+	c anthropic.Client
+}
+
+func NewClient(key string) *Client {
+	return &Client{
+		c: anthropic.NewClient(
+			option.WithAPIKey(key),
+		),
+	}
+}
diff --git a/loop/loop.go b/loop/loop.go
new file mode 100644
index 0000000..6cfa111
--- /dev/null
+++ b/loop/loop.go
@@ -0,0 +1,209 @@
+package loop
+
+import (
+	_ "embed"
+	"encoding/json"
+	"fmt"
+	"sync"
+
+	"dodo.cloud/neo/tools"
+
+	"github.com/anthropics/anthropic-sdk-go"
+	"github.com/invopop/jsonschema"
+)
+
+//go:embed CONVERSATION_RULES.md
+var systemPrompt string
+
+type Message struct {
+	Author   string `json:"author"`
+	Contents string `json:"contents"`
+	Done     bool   `json:"done"`
+}
+
+type Conversation struct {
+	Messages []Message `json:"message"`
+}
+
+var todo *ToDo
+
+func Run(pr PromptReader, client *Client, tools tools.Registry) error {
+	RegisterToDoTools(tools)
+	fmt.Printf("YOU: ")
+	prompt, err := pr.Read()
+	if err != nil {
+		return err
+	}
+	todo = &ToDo{}
+	todo.ID = "1"
+	todo.Title = prompt
+	todo.AssignedTo = "assistant"
+	agents := []Agent{
+		&UserAgent{pr},
+		&AnthropicAgent{tools, client.c},
+	}
+	var wg sync.WaitGroup
+	for _, a := range agents {
+		wg.Add(1)
+		go func() {
+			if err := a.Run(todo); err != nil {
+				panic(err)
+			}
+			wg.Done()
+		}()
+	}
+	wg.Wait()
+	return nil
+}
+
+// func Loop(todo *ToDo, item *ToDo, pr PromptReader, client *Client, reg tools.Registry) error {
+// 	messages := []anthropic.MessageParam{
+// 		anthropic.NewUserMessage(anthropic.NewTextBlock(string(k))),
+// 		anthropic.NewUserMessage(anthropic.NewTextBlock(fmt.Sprintf("Work on TODO item: %s", item.ID))),
+// 	}
+// 	for {
+// 		fmt.Println(todo.String())
+// 		if item.AssignedTo == "user" {
+// 			fmt.Printf("YOU %s: ", item.ID)
+// 			prompt, err := pr.Read()
+// 			if err != nil {
+// 				return err
+// 			}
+// 			item.Discussion = append(item.Discussion, Comment{
+// 				Author:  "user",
+// 				Comment: prompt,
+// 			})
+// 			item.AssignedTo = "assistant"
+// 			messages = append(messages, anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)))
+// 		} else {
+// 			if len(messages) == 0 {
+// 				messages = append(messages, anthropic.NewUserMessage(anthropic.NewTextBlock(fmt.Sprintf("Work on TODO item with id"))))
+// 			}
+// 		}
+
+// 	}
+// }
+
+func pickToDoItem(todo *ToDo) *ToDo {
+	if todo.Done {
+		return nil
+	}
+	for _, i := range todo.Items {
+		if ret := pickToDoItem(i); ret != nil {
+			return ret
+		}
+	}
+	return todo
+}
+
+func findItemByID(todo *ToDo, id string) *ToDo {
+	if todo.ID == id {
+		return todo
+	}
+	for _, i := range todo.Items {
+		if ret := findItemByID(i, id); ret != nil {
+			return ret
+		}
+	}
+	return nil
+}
+
+type ToDoItem struct {
+	ParentID    string `json:"parentId"`
+	Title       string `json:"title"`
+	Description string `json:"description"`
+	AssignedTo  string `json:"assignedTo"`
+}
+
+type ToDoAddItemArgs struct {
+	Items []ToDoItem `json:"items"`
+}
+
+func ToDoAddItem(args ToDoAddItemArgs) (string, error) {
+	for _, td := range args.Items {
+		item := findItemByID(todo, td.ParentID)
+		if item == nil {
+			return "error", fmt.Errorf("TODO item with given id not found: %s", td.ParentID)
+		}
+		id := fmt.Sprintf("%s.%d", item.ID, len(item.Items)+1)
+		item.Items = append(item.Items, &ToDo{
+			ID:          id,
+			Title:       td.Title,
+			Description: td.Description,
+			AssignedTo:  td.AssignedTo,
+		})
+	}
+	return "done", nil
+}
+
+type ToDoMarkItemDoneArgs struct {
+	ID string `json:"id"`
+}
+
+func ToDoMarkItemDone(args ToDoMarkItemDoneArgs) (string, error) {
+	item := findItemByID(todo, args.ID)
+	if item == nil {
+		return "error", fmt.Errorf("TODO item with given id not found: %s", args.ID)
+	}
+	item.Done = true
+	return "done", nil
+}
+
+type ToDoItemAddCommentArgs struct {
+	ID       string `json:"id"`
+	Comment  string `json:"comment"`
+	AssignTo string `json:"assignTo"`
+}
+
+func ToDoItemAddComment(args ToDoItemAddCommentArgs) (string, error) {
+	item := findItemByID(todo, args.ID)
+	if item == nil {
+		return "error", fmt.Errorf("TODO item with given id not found: %s", args.ID)
+	}
+	if len(item.Discussion) == 0 {
+		return "error", fmt.Errorf("You shall never initiate a discussion, if you want to clarify something create a TODO item for it.")
+	}
+	item.Discussion = append(item.Discussion, Comment{
+		Author:  "assistant",
+		Comment: args.Comment,
+	})
+	item.AssignedTo = args.AssignTo
+	return "done", nil
+}
+
+func RegisterToDoTools(reg tools.Registry) {
+	reg.Add(tools.NewFuncTool("todo_item_add", ToDoAddItem, "Add new ToDo item."))
+	reg.Add(tools.NewFuncTool("todo_item_mark_done", ToDoMarkItemDone, "Marks ToDo item with given ID as done."))
+	reg.Add(tools.NewFuncTool("todo_item_add_comment", ToDoItemAddComment, "Adds discussion comment to given ToDo item"))
+}
+
+func GetToolSchema(schema *jsonschema.Schema) (anthropic.ToolInputSchemaParam, error) {
+	schemaBytes, err := json.Marshal(schema)
+	if err != nil {
+		return anthropic.ToolInputSchemaParam{}, err
+	}
+	var schemaMap map[string]any
+	if err := json.Unmarshal(schemaBytes, &schemaMap); err != nil {
+		return anthropic.ToolInputSchemaParam{}, err
+	}
+
+	inputSchema, err := parseSchemaMap(schemaMap)
+	if err != nil {
+		return anthropic.ToolInputSchemaParam{}, err
+	}
+	return inputSchema, nil
+}
+
+func parseSchemaMap(s map[string]any) (anthropic.ToolInputSchemaParam, error) {
+	bytes, err := json.Marshal(s)
+	if err != nil {
+		return anthropic.ToolInputSchemaParam{}, fmt.Errorf("failed to marshal schema: %w", err)
+	}
+
+	var schema anthropic.ToolInputSchemaParam
+	if err := json.Unmarshal(bytes, &schema); err != nil {
+		return anthropic.ToolInputSchemaParam{}, fmt.Errorf("failed to unmarshal schema: %w", err)
+	}
+
+	return schema, nil
+}
diff --git a/loop/prompt.go b/loop/prompt.go
new file mode 100644
index 0000000..c889247
--- /dev/null
+++ b/loop/prompt.go
@@ -0,0 +1,28 @@
+package loop
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+)
+
+type PromptReader interface {
+	Read() (string, error)
+}
+
+type IOReaderPromptReader struct {
+	s *bufio.Scanner
+}
+
+func NewIOReaderPromptReader(r io.Reader) *IOReaderPromptReader {
+	return &IOReaderPromptReader{
+		s: bufio.NewScanner(r),
+	}
+}
+
+func (r IOReaderPromptReader) Read() (string, error) {
+	if !r.s.Scan() {
+		return "", fmt.Errorf("done")
+	}
+	return r.s.Text(), nil
+}
diff --git a/loop/todo.go b/loop/todo.go
new file mode 100644
index 0000000..e639857
--- /dev/null
+++ b/loop/todo.go
@@ -0,0 +1,61 @@
+package loop
+
+import (
+	"fmt"
+	"strings"
+	"sync"
+
+	"github.com/invopop/jsonschema"
+)
+
+type Comment struct {
+	Author  string `json:"author"`
+	Comment string `json:"comment"`
+}
+
+func (c Comment) String() string {
+	return fmt.Sprintf("%s: %s", c.Author, c.Comment)
+}
+
+type ToDo struct {
+	ID          string    `json:"id"`
+	Title       string    `json:"title"`
+	Description string    `json:"description"`
+	Items       []*ToDo   `json:"items"`
+	Done        bool      `json:"done"`
+	AssignedTo  string    `json:"assignedTo"`
+	Discussion  []Comment `json:"discussion"`
+	lock        sync.Locker
+}
+
+const tmpl = `%s: %s
+%s
+%s
+%s`
+
+func (t ToDo) String() string {
+	var comments []string
+	for _, c := range t.Discussion {
+		comments = append(comments, fmt.Sprintf("\t - %s", c.String()))
+	}
+	var items []string
+	for _, i := range t.Items {
+		items = append(items, fmt.Sprintf("\t%s", i.String()))
+	}
+	return fmt.Sprintf(tmpl, t.ID, t.Title, t.Description, strings.Join(comments, "\n"), strings.Join(items, "\n"))
+}
+
+func ToDoJSONSchema() string {
+	reflector := jsonschema.Reflector{
+		AllowAdditionalProperties:  false,
+		RequiredFromJSONSchemaTags: true,
+		DoNotReference:             false,
+	}
+	var v ToDo
+	s := reflector.Reflect(v)
+	b, err := s.MarshalJSON()
+	if err != nil {
+		panic(err)
+	}
+	return string(b)
+}