You type a command. Your task is saved. Let's trace exactly what happens in between.
tasks addtasks list / ls--all to see completed ones too.tasks donetasks deleteThere's no button to click — you type a command instead. Under the hood, it works the same way a web app does: you send input, the app processes it, and you get output. The difference is text in, text out.
Let's trace end-to-end what happens the moment you hit Enter on tasks add "Ship the landing page":
tasks binaryThe operating system looks on your PATH for an executable called "tasks" — the compiled Go program.main.go starts and hands off to CobraThe entry point calls cmd.Execute(), which kicks off the command-routing engine.When you ask AI to "add a priority field to tasks," it needs to touch the Store (where tasks are defined and saved) AND the list command (which displays them). Knowing the flow helps you give better instructions.
Every codebase is a story with characters. Here are the four main players in this one.
There's a director (main.go), a routing coordinator (Cobra), specialists who handle each scene (cmd/ files), and a prop master who manages all the physical items (the Store). Everyone knows their job and stays out of each other's way.
Notice how each file does exactly one thing. This is called the Single Responsibility Principle. When you ask AI to add a feature, you can be precise: "update only the Store" or "add a new command file in cmd/".
Let's read real code together. Side-by-side, line by line — no CS degree needed.
You fill in the "Use" field (the command name), the "Short" field (the label on the envelope), and the "RunE" field (the instructions for what to do when this form arrives). Cobra is the postal service — it reads the form and executes the instructions.
var addCmd = &cobra.Command{
Use: "add <description>",
Short: "Add a new task",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
s, err := openStore()
if err != nil {
return err
}
defer s.Close()
t := s.Add(args[0])
fmt.Fprintf(cmd.OutOrStdout(), "Added task %d\n", t.ID)
return nil
},
}
Create a new command object and store it in addCmd.
When someone types "tasks add ..." this is the command name pattern to match.
A one-line description shown in the help menu.
Require exactly one argument — the task description. Error out if they provide zero or two.
RunE is the function that actually runs. It receives the typed arguments as a list of strings.
Open the CSV file (and lock it). Store the result in "s".
If something went wrong opening the file, stop here and report the error.
Close parenthesis of the error check.
"defer" means: run s.Close() when this function exits, no matter what. Saves and unlocks the file.
Add the task (args[0] = the first argument you typed). Get back the new task object "t".
Print "Added task 1" (or whatever ID) to the terminal output.
Return nil = no error, everything worked.
End of the function and the command definition.
defer s.Close() means "close and save the file when this function finishes — even if there's an error." Without it, you'd have to remember to close the file in every possible exit path. Defer handles it automatically. You can tell AI: "use defer for any cleanup at the end of a function."
Every command needs to open the Store. Instead of repeating that code five times, root.go defines a shared helper called openStore() that all commands can call.
func resolveFile() (string, error) {
if dataFile != "" {
return dataFile, nil
}
if env := os.Getenv("TASKS_FILE"); env != "" {
return env, nil
}
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home dir: %w", err)
}
return filepath.Join(home, ".tasks.csv"), nil
}
Function that figures out where the tasks file lives. Returns a file path or an error.
If the user passed --file /some/path.csv, use that path.
Return that path (second value "nil" = no error).
End of that check.
Otherwise, check the TASKS_FILE environment variable. If it's set, use that.
Return the env variable path.
End of that check.
Otherwise, find the user's home directory (e.g. /Users/yourname).
If we can't find it, return an error message.
End of error check.
Default: use ~/.tasks.csv (join home + ".tasks.csv").
End of function.
All task data lives here. This is the most important file in the codebase.
When you open the Store, you grab the cabinet key (flock) and pull all the folders into your arms (load into memory). You make changes in your arms. When you're done, you put everything back in the drawer exactly as arranged (save to CSV), then return the key (unlock). Nobody else can open the drawer while you have the key.
type Task struct {
ID int
Description string
CreatedAt time.Time
IsComplete bool
}
Define a blueprint (called a struct) named Task.
Every task has an ID — a whole number (1, 2, 3...).
A description — any text, like "Ship the landing page".
A timestamp for when it was created, in full date+time format.
A true/false flag: has this task been marked done?
End of the blueprint.
CSV is just a text file with commas. No database needed. You can open ~/.tasks.csv in any text editor or spreadsheet app and see all your tasks.
func loadFile(path string) (*os.File, error) {
f, err := os.OpenFile(path,
os.O_RDWR|os.O_CREATE, os.ModePerm)
if err != nil {
return nil, fmt.Errorf("open %s: %w", path, err)
}
if err := syscall.Flock(int(f.Fd()),
syscall.LOCK_EX); err != nil {
_ = f.Close()
return nil, fmt.Errorf("lock %s: %w", path, err)
}
return f, nil
}
Function: open the tasks file. Takes a path string, returns a file handle or an error.
Open the file at that path. O_RDWR = read+write access. O_CREATE = create if it does not exist.
If we could not open it (wrong permissions?), return an error with the file path for context.
Now grab an exclusive lock on the file. LOCK_EX means: nobody else can read or write until we unlock. This is like putting a "DO NOT DISTURB" sign on the file.
If locking failed, close the file we just opened (clean up) and return an error.
All good — return the locked file handle to the caller.
End of function.
Imagine two terminal windows both running tasks add at the same moment. Without a lock, both would read the file, both add a task, and both write — one overwriting the other. flock makes the second process wait until the first is done. Same pattern you would use with any shared resource in AI-built apps.
func (s *Store) Add(description string) Task {
id := 1
for _, t := range s.tasks {
if t.ID >= id {
id = t.ID + 1
}
}
t := Task{
ID: id,
Description: description,
CreatedAt: time.Now(),
IsComplete: false,
}
s.tasks = append(s.tasks, t)
s.dirty = true
return t
}
Add method on Store. Takes a description string, returns the new Task.
Start with ID = 1 as a candidate.
Loop through all existing tasks.
If any task has an ID equal to or higher than our candidate, bump our candidate up by 1.
End of loop — now "id" is guaranteed to be higher than all existing IDs.
Build a new Task object with all its fields filled in.
Use the ID we just calculated.
Use the description the user typed.
Set the creation time to right now.
New tasks start as not complete.
End of task creation.
Add this new task to our in-memory list.
Set dirty = true: this tells Close() that it needs to write to disk.
Return the new task so the caller can print its ID.
End of Add().
You have seen every part. Now let's step back and see how they form a complete, reliable system.
You can tell AI: "use a layered architecture with a dedicated data store," "add a dirty flag so we only write when necessary," "use defer for file cleanup," "add flock for safe concurrent access." These are precise, professional instructions — not hand-wavy guesses.
This codebase has real engineering decisions in it: file locking, layered architecture, the dirty flag pattern, deferred cleanup. These aren't beginner tricks — they're patterns used in production software. You know what they are, why they exist, and when to ask for them.
Course complete 🎉
You now know this codebase end to end. Go build something great.