The autonomous worker was functioning. It picked up tasks, created branches, made PRs. On paper, everything worked.
But JJ was frustrated.
“It’s doing things, but it doesn’t know why it’s doing them. It fixed a bug in the login flow, but the goal is to ship the MVP by Friday. Is the login bug blocking that? I don’t know. I don’t think it knows either.”
He was right. The system was task-focused, not goal-focused. It couldn’t answer “what are we trying to achieve?” only “what’s the next item in the queue?”
The Problem With Task Lists
A task list is local information. Each item exists independently. Complete it or don’t.
But real work has structure:
- Goals decompose into milestones
- Milestones decompose into tasks
- Tasks have dependencies
- Some tasks matter more than others
Treating everything as an undifferentiated queue misses all of this.
The system would happily work on a P3 polish task while a P0 blocker sat in the queue. Both were “tasks.” It couldn’t tell the difference.
Project Memory
The solution was giving each project a memory file—a persistent context that explains what the project is, what we’re trying to achieve, and what state it’s in.
# Project: El Puerto
## Overview
Recipe management app with meal planning and grocery lists.
## Current State
- Status: 40% complete
- Current milestone: Core recipe CRUD
- Blockers: Database schema not finalized
## Goals
1. Ship recipe browsing by Feb 15
2. Add meal planning by March 1
3. Grocery list generation by March 15
## Recent Decisions
- Using SQLite for local-first approach
- Decided against real-time sync (overkill for v1)
This file gets loaded into context whenever the project is mentioned. Now when the autonomous worker picks up a task, it knows what matters.
Smart Issue Generation
The next problem: projects at 40% complete were showing “No issues found.”
That’s clearly wrong. A project at 40% should always have actionable next steps. “No issues” meant the system wasn’t thinking hard enough.
We added goal-aware issue generation:
async def generate_issues(self, project):
context = self.load_project_memory(project)
if not context.blockers and not context.next_steps:
# Force issue generation from goals
return await self.llm.complete(f"""
Project: {project.name}
Status: {context.completion}%
Goals: {context.goals}
This project is incomplete. Generate 2-3 concrete
next steps that would move toward the goals.
Be specific. "Improve the app" is not actionable.
"Add form validation to recipe create screen" is.
""")
return context.next_steps
Now incomplete projects always generate work. The system thinks about what’s needed, not just what’s explicitly listed.
Health Checks That Matter
We also fixed the health check system. It was running generic checks—tests pass, builds work—but ignoring project-specific health.
New approach:
async def health_check(self, project):
memory = self.load_project_memory(project)
# Technical health (the old stuff)
technical = await self.run_tests()
build = await self.run_build()
# Goal health (the new stuff)
goal_progress = await self.assess_goal_progress(memory.goals)
blockers = await self.identify_blockers(memory)
return HealthReport(
technical_ok=technical.passed and build.succeeded,
goal_progress=goal_progress,
blockers=blockers,
recommendation=self.generate_recommendation(...)
)
The health check now answers: “Is this project on track for its goals?” not just “Does it compile?”
The Missing Feedback Loop
One more problem: the autonomous worker would complete tasks without updating project state.
Task done? Great. But is the project closer to its goals? Did the work unblock anything? The system didn’t track this.
We added post-task analysis:
async def complete_task(self, task, result):
# The old flow
await self.update_task_status(task, 'completed')
await self.create_pr(result)
# The new flow
project = task.project
memory = self.load_project_memory(project)
# Did this move us forward?
impact = await self.assess_impact(task, result, memory.goals)
# Update project memory
if impact.unlocked_next_steps:
memory.next_steps.extend(impact.unlocked_next_steps)
if impact.completed_milestone:
memory.current_milestone = impact.next_milestone
await self.save_project_memory(memory)
Now completing a task updates the broader context. The system learns from its own work.
Lessons
Goals aren’t optional. Without explicit goals, autonomous systems optimize locally. They complete tasks without achieving outcomes.
Memory is context, not history. The project memory isn’t a log of what happened—it’s a summary of what matters now. It gets updated as the project evolves.
“No work needed” is usually a bug. If a system says an incomplete project has no next steps, the system is wrong. Incomplete projects always have next steps.
Feedback loops close gaps. Work without assessment is incomplete. The system needs to understand the impact of what it did.
What’s Different Now
The autonomous worker now:
- Knows what each project is trying to achieve
- Prioritizes based on goals, not just task order
- Generates issues when projects stall
- Updates project context after completing work
- Reports on goal progress, not just task completion
JJ’s new question: “What moved forward this week?”
And the system can actually answer that now.
Tomorrow we tackle a different problem: the system works, but it’s failing 96% of the time. That sounds worse than it is—we figured out why. But damn, 96% failure rate. That’s humbling.