Dzaky Zakiyal Fawwaz

Agent Harness: How I Built This Portfolio Using Autonomous AI Agents

Published on
8 min read
aiagentssdlckiro

The Problem with Vibe-Coding Large Projects

When I decided to rebuild my portfolio from scratch, I faced a familiar problem: large, multi-component projects tend to fall apart when handed to an AI agent without structure. The agent hallucinates dependencies, forgets constraints set three conversations ago, and produces code that works in isolation but breaks the moment it touches anything adjacent.

I had seen this pattern too many times. The answer is not a smarter model — it is a smarter process.

What I Call the Agent Harness

The agent harness is a spec-driven SDLC loop I built around Kiro, a set of VS Code skills I ported into Claude Code. The loop has three phases:

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│   1. DISCOVERY          2. SPECIFICATION        3. IMPL         │
│                                                                 │
│   raw idea              requirements.md          sub-agent      │
│      ↓                  design.md                per task       │
│   brief.md              tasks.md                    ↓           │
│   roadmap.md               ↑                    reviewer        │
│                        human review             checks design   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
  1. Discovery — kiro-discovery takes a raw idea and writes a brief.md plus a roadmap.md that maps features to dependencies.
  2. Specification — per feature, kiro-spec-init → kiro-spec-requirements → kiro-spec-design → kiro-spec-tasks produces four structured documents: requirements in EARS notation, an architecture design with explicit boundary commitments, and a task breakdown with observable completion criteria.
  3. Implementation — kiro-impl dispatches sub-agents, one per task. Each sub-agent reads only its task plus the design doc. It cannot drift into adjacent scope because the boundary is written down.

The harness does not replace human judgment. It enforces that judgment happens upfront — at spec time — rather than being improvised mid-implementation.

Discovery: Turning One Sentence Into a Dependency Graph

The portfolio started as: "rebuild my Publii static site as a TanStack Start app with bilingual support, an AI chatbot, and a blog."

kiro-discovery expanded this into nine feature specs organized into three dependency waves:

WaveFeature specsConstraint
Wave 0design-systemNo dependencies — builds first
Wave 1site-foundationDepends on Wave 0 only
Wave 2 (parallel)home-about, experience-education, achievements-publications, projects-skillset, blog-articles, services-showcase, contact, ai-chatbotAll depend on Wave 1; can build concurrently

The wave structure matters. Spec agents in wave 2 know they can consume the foundation's APIs without redefining them. That constraint is not enforced by code — it is enforced by the boundary commitment section in every design doc.

Specification: Writing the Contract Before the Code

Each spec produces four files:

requirements.md — Written in EARS (Easy Approach to Requirements Syntax): "The system shall X when Y so that Z." The format forces specificity. "Add validation" becomes:

REQ-012: When a required field is empty on form submit, the system
         shall display a localized error message adjacent to the
         field so that the user knows exactly what to fix without
         reading a global error banner.

design.md — The most important document. It specifies: what this spec owns, what it does not own, what it can depend on. It lists every file the spec will create, every component with its props contract, every data model. It includes explicit non-goals. When a sub-agent wanders outside its boundary, the design doc is the artifact that surfaces the error.

A boundary section looks like this:

## Owns
- app/src/components/blog/post-list.tsx
- app/src/components/blog/post-header.tsx
- app/src/routes/$locale/blog/
 
## Does Not Own
- The MDX processing pipeline (owned by site-foundation)
- The i18n catalog (owned by site-foundation)
- Any route outside /blog/
 
## Depends On
- @/mdx/frontmatter (Frontmatter type)
- @/i18n/context (useTranslation hook)
- @design-system/primitives (Card, Badge)

tasks.md — Numbered task list derived from the design, each with an observable completion criterion. "The listing page renders posts newest-first" is an observable criterion. "Implement the blog" is not.

This work is done by agents, but the output is reviewed by me before implementation begins. That review is the single most leveraged hour in the entire process.

Implementation: Sub-Agents Reading a Single Task

kiro-impl runs each task as a separate sub-agent. The sub-agent gets:

  • Its task description and observable completion criterion
  • The feature's design.md (for boundary constraints)
  • The foundation files it is allowed to read

It does not get the full conversation history, the previous tasks, or any other feature's spec. That isolation is intentional. A sub-agent that can see everything will try to "help" by touching things it was not asked to touch.

When a task is complete, kiro-review runs: an adversarial reviewer checks the implementation against the design doc. If the implementation drifts — say, a component reimplements a utility already in the foundation — the reviewer flags it. The implementer fixes it. Only then does the task close.

The conceptual loop looks like this:

for task in tasks:
    implementation = implement_task(
        task=task,
        design_doc=feature.design_md,
        allowed_reads=feature.foundation_files,
        # does NOT receive: other tasks, other features, conversation history
    )
 
    review = review_task(
        task=task,
        implementation=implementation,
        design_doc=feature.design_md,
    )
 
    if review.drift_detected:
        task.status = "blocked"
        task.blocking_reason = review.finding
    else:
        task.status = "done"

What Actually Broke (And How the Spec Caught It)

The site-foundation spec's task 6.3 failed. The observable criterion was: "The language toggle switches the locale without a full-page reload." The implementation worked locally, but TanStack Start's createServerFn + cookie mechanism and the version of @tanstack/start in the lockfile had an incompatibility that caused SSR hydration to diverge.

Because the criterion was explicit, failure was unambiguous. The agent did not ship a broken implementation and mark the task done — the criterion evaluation caught it. Task 6.3 is marked blocked in the spec, not complete, and is scoped to the next dependency update.

That is the harness working correctly: honest, specific, verifiable.

Lessons

Specs are cheap; rework is expensive. The four spec documents for blog-articles took about two hours of agent time and one hour of my review. The implementation took less than a day and produced zero rework on any adjacent feature. Without the spec, I would have spent three days building and a week untangling.

Observable criteria are the unit of trust. "The task is done" means nothing. "The listing page SSR HTML contains post titles and links" is checkable. Every task in this portfolio has a criterion I could verify manually.

Isolation is a feature. Sub-agents that cannot see the whole project make fewer creative decisions. That sounds like a limitation. It is actually how you prevent scope creep from accumulating across thirty tasks.

The harness does not make agents smarter. It makes their mistakes smaller and more visible. The spec constrains the blast radius of any one bad decision. That is the correct mental model.

If you are building something larger than a weekend project with AI agents, write the spec first. The harness is the difference between a project that ships and one that collapses under its own improvised structure.


Template repo — ready-to-use kiro spec templates (requirements.md, design.md, tasks.md) with a worked example and the conceptual harness loop in Python: github.com/dzakybd/agent-harness-template