Durability in Wallet Transfer System

In this task, I understood the Durability property of ACID using a wallet system. Durability means that once a transaction is committed, the data will be permanently stored in the database and will not be lost even if the system crashes or restarts.

First, I created the accounts table.

CREATE TABLE accounts (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
balance INT NOT NULL CHECK (balance >= 0),
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Then I inserted sample data.

INSERT INTO accounts (name, balance)
VALUES
(‘Alice’, 1000),
(‘Bob’, 500);
To check the initial data:

SELECT * FROM accounts;
Alice had 1000 and Bob had 500.

Next, I performed a successful transaction to transfer 300 from Alice to Bob.

BEGIN;

UPDATE accounts
SET balance = balance – 300
WHERE id = 1;

UPDATE accounts
SET balance = balance + 300
WHERE id = 2;

COMMIT;
After committing, I checked the data again.

SELECT * FROM accounts;
Now Alice had 700 and Bob had 800.

Then I simulated a system restart by reconnecting to the database and checked the data again.

SELECT * FROM accounts;
The values remained the same, which shows that the committed transaction was permanently saved.

If a failure happens before COMMIT, the changes are not saved.

BEGIN;

UPDATE accounts
SET balance = balance – 200
WHERE id = 1;

— crash before COMMIT
After restart, the balance remains unchanged.

If a failure happens after COMMIT, the data is already stored and will not be lost.

This happens because PostgreSQL uses Write-Ahead Logging (WAL), which ensures that committed data can be recovered even after a crash.

From this task, I understood that Durability ensures that once a transaction is completed, the data remains safe and permanent. This is very important in wallet systems to prevent loss of money or incorrect balances.

CONSISTENCY

Step 1: Check initial data
SELECT * FROM accounts;

Explanation:
Alice has 1000 and Bob has 500.
Both balances are valid (not negative).

Step 2: Try to make balance negative directly
UPDATE accounts
SET balance = -100
WHERE name = ‘Alice’;

Explanation:
Here I am directly trying to set a negative balance.

What happens:
Query fails

Reason:
There is a constraint CHECK (balance >= 0)

So database itself prevents invalid data.

Step 3: Deduct more than available balance
UPDATE accounts
SET balance = balance – 1500
WHERE name = ‘Alice’;

Explanation:
Alice has only 1000 but I tried to deduct 1500.

What happens:
Query fails

Reason:
Result becomes negative, so CHECK constraint blocks it

Step 4: Valid deduction
UPDATE accounts
SET balance = balance – 500
WHERE name = ‘Alice’;

Explanation:
This works because balance remains positive.

Step 5: Using transaction (important case)
BEGIN;

UPDATE accounts
SET balance = balance – 1200
WHERE name = ‘Alice’;

COMMIT;

Explanation:
Here also it fails before commit.

What happens:
Transaction does not complete

Reason:
Constraint is checked during update

Step 6: Application-level check (manual condition)
UPDATE accounts
SET balance = balance – 1200
WHERE name = ‘Alice’
AND balance >= 1200;

Explanation:
Here I added a condition to avoid wrong deduction.

What happens:
No rows updated

Reason:
Condition fails, so query is safe

Final Understanding
Database constraint (CHECK balance >= 0) prevents invalid data
It stops negative values automatically
This is schema-level protection

But,

Checking enough balance before deduction should also be handled in query or application
Otherwise transaction may fail

I tested different updates and observed that database constraints prevent negative balances. I also understood that some validations should be handled in queries or application logic to avoid errors.

🚀 Infinite Monitor Desktop — Turn Your AI Dashboard into a Native App

If you’ve ever used Infinite Monitor, you already know how powerful it is — a dynamic AI-powered dashboard that generates and updates content in real time.

But constantly opening it in the browser? Not ideal.

So I built something better 👇

💻 Introducing: Infinite Monitor Desktop

A cross-platform desktop application for macOS, Windows, and Linux that wraps Infinite Monitor into a clean, native experience.

👉 https://mehdiraized.github.io/infinite-monitor-desktop/

👉 https://github.com/mehdiraized/infinite-monitor-desktop

⸻

✨ Why I Built This

I love Infinite Monitor, but I wanted:
• A dedicated app, not another browser tab
• Better focus and productivity
• Easier access from the desktop
• A smoother, more “native” feel

So I decided to package it into a lightweight desktop app.

⸻

🔥 Features
• 🖥️ Cross-platform support (macOS, Windows, Linux)
• ⚡ Fast and lightweight
• 🔄 Always up-to-date with the latest Infinite Monitor
• 🧠 Full access to AI-powered dashboard features
• 🚀 Simple install and run

⸻

🧩 How It Works

The app is essentially a desktop wrapper around the Infinite Monitor web app.

That means:
• No complex setup
• No need to manage updates manually
• You always get the latest version automatically

⸻

📦 Installation

Just head over to the releases page:

👉 https://github.com/mehdiraized/infinite-monitor-desktop

Download the version for your OS and you’re good to go.

⸻

🎯 Use Cases

This app is perfect for:
• Developers who want a persistent AI dashboard
• Productivity lovers who hate browser clutter
• Anyone using Infinite Monitor daily
• People building workflows around AI-generated widgets

⸻

🛠️ Tech Stack
• Electron (or similar wrapper, depending on your implementation)
• Next.js-based web app embedded inside
• Cross-platform packaging

⸻

💡 What’s Next?

I’m planning to add:
• Native integrations (notifications, shortcuts)
• Offline enhancements
• Better window management
• Custom startup dashboards

If you have ideas, feel free to suggest them!

⸻

🤝 Contributing

PRs and feedback are always welcome 🙌

👉 https://github.com/mehdiraized/infinite-monitor-desktop

⸻

⭐ Support

If you like the project, please consider giving it a star on GitHub — it really helps!

⸻

👋 Final Thoughts

This is just the beginning. My goal is to turn powerful web-based AI tools into seamless desktop experiences.

Give it a try and let me know what you think!

To The Moon Terraform Ep.3

“The Saturn V flew thirteen times. Not thirteen slightly different rockets — thirteen instances of a single masterpiece, each adjusted only by the precise mission parameters it was given. The blueprint was constant. The mission was the variable.”

🌕 Episode 3 — The Blueprint (Variables & Outputs)

There is a species of creature in the engineering world that fascinates and troubles me in equal measure.

It is the hardcoded configuration.

You find it everywhere — lurking deep in .tf files, sometimes in configuration files, occasionally even in shell scripts committed to public repositories with AWS access keys in them (a horror I shall not dwell on here). This creature has one defining characteristic: it cannot adapt. It knows only one environment. One region. One instance size. One mission.

Change any parameter and the whole thing shatters.

The Saturn V engineers understood that missions would vary. The payload changes. The trajectory changes. The launch window changes. But the rocket — the design of the rocket — stays constant. You change the parameters, not the blueprint.

This is the purpose of Terraform Variables.

🔧 Input Variables: The Mission Parameters

An Input Variable is a parameter that makes your Terraform code reusable across missions. Instead of hardcoding "us-east-1" in twenty places, you declare a variable called region and use it everywhere.

When you want to fly to a different launch site, you change one parameter. Not twenty values scattered through three files.

# variables.tf — The Mission Parameter Sheet

# Where are we launching from?
variable "aws_region" {
  description = "The AWS region where mission infrastructure will be deployed"
  type        = string
  default     = "us-east-1"   # Cape Canaveral is the default

  validation {
    condition     = can(regex("^[a-z]{2}-[a-z]+-[0-9]$", var.aws_region))
    error_message = "Region must be a valid AWS region code (e.g., us-east-1, eu-west-1)."
  }
}

# What is this mission called?
variable "mission_name" {
  description = "The name of this mission. Used to tag all resources."
  type        = string

  validation {
    condition     = length(var.mission_name) >= 3 && length(var.mission_name) <= 20
    error_message = "Mission name must be between 3 and 20 characters."
  }
}

# What environment are we operating in?
variable "environment" {
  description = "The operating environment: simulation, staging, or production"
  type        = string
  default     = "simulation"

  validation {
    condition     = contains(["simulation", "staging", "production"], var.environment)
    error_message = "Environment must be 'simulation', 'staging', or 'production'."
  }
}

# What size spacecraft do we need?
variable "instance_type" {
  description = "The EC2 instance type for the lunar module compute"
  type        = string
  default     = "t3.micro"
}

# How many crew members? (How many instances to run)
variable "crew_count" {
  description = "Number of compute instances to launch"
  type        = number
  default     = 1

  validation {
    condition     = var.crew_count >= 1 && var.crew_count <= 10
    error_message = "Crew count must be between 1 and 10. We are not launching an army."
  }
}

# Mission configuration map — structured parameters as a complex type
variable "mission_config" {
  description = "Mission configuration object"
  type = object({
    enable_monitoring = bool
    backup_retention  = number
    allowed_cidrs     = list(string)
  })
  default = {
    enable_monitoring = true
    backup_retention  = 7
    allowed_cidrs     = ["10.0.0.0/8"]
  }
}

🔤 Variable Types: The Classification of Parameters

Variables in Terraform are typed. This is not pedantry — it is the same discipline that kept the Saturn V from exploding because someone mixed up imperial and metric units. (That particular horror belongs to a different programme.)

# The full catalogue of Terraform variable types:

variable "example_string" {
  type    = string
  default = "Houston, we have a configuration"
}

variable "example_number" {
  type    = number
  default = 42
}

variable "example_bool" {
  type    = bool
  default = true
}

variable "example_list" {
  type    = list(string)
  default = ["Armstrong", "Aldrin", "Collins"]
}

variable "example_map" {
  type    = map(string)
  default = {
    commander = "Armstrong"
    pilot     = "Aldrin"
    module    = "Collins"
  }
}

variable "example_set" {
  type    = set(string)
  default = ["us-east-1", "eu-west-1"]
}

variable "example_object" {
  type = object({
    name        = string
    crew_size   = number
    is_crewed   = bool
    destinations = list(string)
  })
}

📁 Variable Values: The Mission Assignment

Declaring a variable is like designing a form with blank fields. Providing the value is like filling in the form for a specific mission.

There are four ways to provide values, in order of increasing precedence (later sources override earlier ones):

Method 1: Default Values (in the variable declaration)

The fallback. Used when nothing else is specified.

Method 2: terraform.tfvars file

# terraform.tfvars — Mission Apollo-Terraform: Production Values

aws_region    = "us-east-1"
mission_name  = "apollo-terraform"
environment   = "production"
instance_type = "t3.small"
crew_count    = 3

mission_config = {
  enable_monitoring = true
  backup_retention  = 30
  allowed_cidrs     = ["10.0.0.0/8", "172.16.0.0/12"]
}

Method 3: Named .tfvars files (for multiple missions)

# For a simulation mission
terraform apply -var-file="simulation.tfvars"

# For a production launch
terraform apply -var-file="production.tfvars"
# simulation.tfvars — Simulation parameters (cheaper, lower stakes)
environment   = "simulation"
instance_type = "t3.micro"
crew_count    = 1

mission_config = {
  enable_monitoring = false
  backup_retention  = 1
  allowed_cidrs     = ["0.0.0.0/0"]
}

Method 4: Environment Variables and CLI flags (for automation)

# Environment variables prefix with TF_VAR_
export TF_VAR_mission_name="apollo-terraform"
export TF_VAR_environment="production"

# Or pass directly at the command line
terraform apply -var="crew_count=5" -var="environment=staging"

📤 Outputs: The Mission Report

If Variables are the inputs to the mission, Outputs are the mission report — the telemetry sent back from space that tells you what was created, where it is, and how to reach it.

After terraform apply, you want to know: What is the IP address of my server? What is the name of the S3 bucket? What is the ARN of the IAM role?

Outputs answer these questions.

# outputs.tf — The Mission Telemetry Report

output "lunar_module_public_ip" {
  description = "Public IP address of the Lunar Module (for SSH access)"
  value       = aws_instance.lunar_module.public_ip
}

output "lunar_module_id" {
  description = "AWS instance ID of the Lunar Module"
  value       = aws_instance.lunar_module.id
}

output "launch_pad_vpc_id" {
  description = "VPC ID of the Launch Pad network"
  value       = aws_vpc.launch_pad.id
}

output "mission_summary" {
  description = "Complete mission deployment summary"
  value = {
    mission     = var.mission_name
    environment = var.environment
    region      = var.aws_region
    server_ip   = aws_instance.lunar_module.public_ip
    server_id   = aws_instance.lunar_module.id
    vpc_id      = aws_vpc.launch_pad.id
  }
}

# Sensitive outputs — marked, so Terraform won't display them in logs
output "database_password" {
  description = "The database password (sensitive — not displayed in logs)"
  value       = random_password.db_password.result
  sensitive   = true   # Redacted in terminal output; still stored in state
}

After terraform apply, you retrieve outputs:

# See all outputs
terraform output

# See one specific output
terraform output lunar_module_public_ip

# Output as JSON (for scripts and pipelines)
terraform output -json

# Example output:
# lunar_module_public_ip = "54.123.45.67"
# launch_pad_vpc_id = "vpc-0a1b2c3d4e5f"

📊 The SIPOC of Episode 3

🔵 Supplier 🟡 Input 🟢 Process 🟠 Output 🔴 Consumer
You, the engineer variables.tf type declarations Terraform validates types & constraints Type-safe variable schema All .tf files using var.* references
terraform.tfvars / env vars Concrete variable values Terraform populates var.* namespace Resolved variable values Resource configurations at apply time
Cloud provider (AWS) Applied resource configuration Resource creation in cloud Real resources with IDs, IPs, ARNs outputs.tf value references
Terraform state Resource attribute values post-apply Output value resolution Named outputs in state CI/CD pipelines, other Terraform modules, your terminal

🔄 Locals: The Derived Calculations

Between inputs and outputs, there is a third category: locals. These are computed values derived from variables and resource attributes — the mission’s internal calculations that don’t need to be exposed as parameters.

# locals.tf — Internal Mission Calculations

locals {
  # Derived naming convention — consistent across all resources
  name_prefix = "${var.mission_name}-${var.environment}"

  # Common tags — applied to everything, computed once
  common_tags = {
    Mission     = var.mission_name
    Environment = var.environment
    ManagedBy   = "terraform"
    LaunchDate  = formatdate("YYYY-MM-DD", timestamp())
  }

  # Conditional logic — which AMI for which environment?
  ami_id = var.environment == "production" ? "ami-prod-0abc123" : "ami-dev-0def456"

  # Computed CIDR blocks
  public_subnet_cidrs  = [for i in range(3) : cidrsubnet(var.vpc_cidr, 8, i)]
  private_subnet_cidrs = [for i in range(3) : cidrsubnet(var.vpc_cidr, 8, i + 10)]
}

# Use in resources:
resource "aws_instance" "lunar_module" {
  ami           = local.ami_id
  instance_type = var.instance_type
  tags          = merge(local.common_tags, { Name = "${local.name_prefix}-lunar-module" })
}

Locals are the mission engineer’s scratch pad. They transform raw parameters into usable values without exposing the intermediate calculations to the outside world.

🌟 The Blueprint Complete

We now have a Terraform configuration that:

  • Accepts parameters — mission name, environment, region, instance type
  • Validates those parameters before any infrastructure is created
  • Computes derived values using locals
  • Creates real infrastructure using the parameterised values
  • Reports back the critical attributes through outputs

This same blueprint can now be used to deploy:

  • A simulation mission in us-east-1 with t3.micro
  • A staging mission in eu-west-1 with t3.small
  • A production mission in us-east-1 with t3.large and 3 instances

One blueprint. Multiple missions. Exactly as NASA intended.

🌕 Next episode: **The Pre-Flight Checklist* — we go deep on terraform plan and learn why you should never, ever apply without planning first.*

It is the same reason you do not fire a rocket engine without checking all 400,000 components first.

Santa Augmentcode Intent Ep.8

The Gifts Are Under the Tree — From Spec to Merged PR 🎄

Accompanying source code repository: Santa Augmentcode Intent

Every year, on the morning of December 25th, I allow myself one quiet moment before the sleigh is unpacked and the Thank-You Letters start arriving. I sit in the empty Workshop, still warm from the night’s work, and look at the bare shelves where the gifts used to be. They are gone because they were delivered. Every one of them. On time, as specified, to the right address. That moment — that quiet confirmation that everything worked — is what we have been building towards in this entire series. Today, we deliver.

The Complete Picture

Over the past seven episodes, we have assembled all the pieces of the Intent workshop:

  • Episode 1: The Workshop and what Intent is.
  • Episode 2: The Living Spec — the source of truth.
  • Episode 3: Santa the Coordinator and the Specialist Elves.
  • Episode 4: Isolated workspaces and resumable sessions.
  • Episode 5: Spec-Driven Development and finishing on time.
  • Episode 6: The Context Engine — the Workshop Library.
  • Episode 7: Multi-agent orchestration in waves.

Now we put them all together and walk through a complete Intent session, from the moment Head Elf Pepper opens the application to the moment the pull request is merged.

The Scenario

Goal: Add a real-time gift delivery progress dashboard to the North Pole’s internal web application. The dashboard should show live sleigh position, remaining delivery count, and an estimated completion time. It must work on tablets and update every 30 seconds.

Constraints:

  • Use the existing Reindeer GPS API (v2)
  • No changes to the Delivery Manifest microservice
  • Safari / iPad compatible
  • Must pass existing end-to-end test suite

Step 1: Open Intent and Set the Goal

Pepper opens Intent on her MacBook. She types into the Coordinator:

We need a real-time gift delivery dashboard. Show sleigh position
on a map, remaining delivery count, and estimated completion time.
Updates every 30 seconds. Must work on iPad Safari.
Use Reindeer GPS API v2. No changes to Delivery Manifest Service.

The Coordinator reads this prompt alongside the Context Engine’s analysis of the codebase. It knows the existing frontend stack (React, TypeScript), the API patterns used elsewhere in the project, and the existing test suite structure.

Step 2: The Coordinator Drafts the Living Spec

Within seconds, the Coordinator produces:

## Gift Delivery Dashboard — Living Spec

### Goal
Real-time dashboard showing sleigh position, delivery count,
and ETA. Updates every 30 seconds. iPad Safari compatible.

### Success Criteria
- [ ] Map renders in < 1s on iPad Safari
- [ ] Position updates within 30s of GPS data change
- [ ] Delivery count matches Manifest API response
- [ ] ETA calculated from current position + remaining stops
- [ ] All existing e2e tests pass
- [ ] Lighthouse accessibility score ≥ 90

### Constraints
- Reindeer GPS API v2 (existing auth token pattern)
- Delivery Manifest Service: read-only, no new endpoints
- Safari / iPad: no Chrome-only APIs

### Architecture
- New React component: `<DeliveryDashboard />`
- GPS polling service (30s interval, exponential backoff on fail)
- Map: Leaflet.js (already in dependencies — see package.json)
- Offline fallback: show last known position with staleness indicator

### Tasks
- [ ] GPS Polling Service — GPS Elf
- [ ] Map Component — Map Elf
- [ ] Delivery Count & ETA Widget — Data Elf
- [ ] Safari/iPad CSS fixes — Responsive Elf
- [ ] Integration tests — Test Elf (starts after GPS + Map done)
- [ ] Accessibility audit — A11y Elf (background)

### Decisions
(none yet)

Pepper reads the spec. She adds one clarification: “ETA should display in local Workshop time, not UTC.” The Coordinator updates the spec and records the decision.

Review time: 4 minutes. No code written yet. One important decision already captured.

Step 3: Intent Creates Isolated Workspaces

The Coordinator identifies the dependency graph:

  • GPS Elf and Map Elf can start in parallel immediately.
  • Data Elf can start immediately (it only reads from the Manifest API).
  • Responsive Elf can start once Map Elf has a working component.
  • Test Elf starts after GPS Elf and Map Elf complete.
  • A11y Elf runs as a background agent throughout.

Intent creates five Git worktrees:

feat/dashboard-gps-service        ← GPS Elf
feat/dashboard-map-component      ← Map Elf
feat/dashboard-data-widgets       ← Data Elf
feat/dashboard-responsive         ← Responsive Elf (waiting)
feat/dashboard-tests              ← Test Elf (waiting)

Wave 1 agents — GPS Elf, Map Elf, Data Elf — start simultaneously. The A11y Elf begins in the background.

Step 4: Wave 1 Runs

Pepper watches the Intent window. Three agent conversations are active. Code appears in the Changes panel. The spec’s task list updates as work progresses.

GPS Elf implements the polling service, using the existing auth token pattern it found via the Context Engine:

// gps-polling.service.ts
export class GpsPollingService {
  private interval = 30_000;
  private backoff = 1;

  async startPolling(onUpdate: (pos: SleighPosition) => void) {
    const poll = async () => {
      try {
        const pos = await reindeerGpsApi.getCurrentPosition();
        onUpdate(pos);
        this.backoff = 1;
      } catch {
        this.backoff = Math.min(this.backoff * 2, 8);
      }
      setTimeout(poll, this.interval * this.backoff);
    };
    poll();
  }
}

The spec updates: ✅ GPS Polling Service — GPS Elf — done.

Map Elf creates the <DeliveryMap /> component using Leaflet.js, which it found already installed in package.json via the Context Engine. It does not install a duplicate library.

Data Elf builds the delivery count and ETA widget, correctly calling the Manifest API’s read-only endpoint and displaying ETA in Workshop local time (it read the spec decision).

Step 5: Handoffs and Wave 2

Both GPS Elf and Map Elf report completion. The spec records the interfaces they produced:

### Decisions
- GPS service exposes: `startPolling(cb: (SleighPosition) => void)`
- Map component props: `{ position: SleighPosition; zoom?: number }`

The Coordinator briefs Responsive Elf with the updated spec and the actual component structure. No guesswork. Test Elf receives both the GPS service interface and the Map component props, plus the existing e2e test patterns from the Context Engine.

Wave 2 begins. Responsive Elf adds Safari/iPad CSS. Test Elf writes integration tests.

Step 6: The Verifier Checks Everything

Wave 2 complete. The Coordinator spawns the Verifier Elf with the full spec and all five implementations.

Verifier’s report:

✅ Map renders in < 1s on iPad Safari — confirmed (Lighthouse: 0.7s)
✅ Position updates within 30s — confirmed
✅ Delivery count matches Manifest API — confirmed
✅ ETA in Workshop local time — confirmed
✅ Existing e2e tests pass — confirmed
✅ Lighthouse accessibility score ≥ 90 — confirmed (score: 94)
⚠️  Minor: GPS service does not log polling errors to existing
    structured logger (see logger.ts — used throughout codebase)

The warning is real and useful. The Context Engine told the Verifier about logger.ts. A generic agent would have missed it.

The Coordinator sends the GPS Elf back for a five-minute fix. It adds one line:

} catch (err) {
  logger.warn('GPS polling failed', { error: err, backoff: this.backoff });

Re-verification: all green.

Step 7: The Pull Request

The Coordinator integrates all five worktrees into a single branch feat/delivery-dashboard and opens a pull request. The PR description is generated from the Living Spec:

## Gift Delivery Dashboard

Implements real-time dashboard (sleigh position, delivery count, ETA).

### Changes
- New: `GpsPollingService` with 30s polling + exponential backoff
- New: `<DeliveryMap />` using existing Leaflet.js dependency
- New: `<DeliveryStats />` widget with ETA in Workshop local time
- New: iPad/Safari responsive CSS
- New: Integration test suite (12 tests, all passing)

### Verified Against Spec
All 6 success criteria confirmed by Verifier Agent.
Existing e2e suite: all passing.

### Decisions Recorded
- ETA displayed in Workshop local time (not UTC) — per Pepper, Dec 3
- Used existing Leaflet.js (not installed new mapping library)
- GPS errors logged via existing structured logger (logger.ts)

Pepper reviews the spec (4 minutes). She reviews the diff (10 minutes, because the spec told her exactly what to look for). She approves.

Total elapsed time: 47 minutes. Zero rework. Christmas safe.

What We Built Together

Over this series, Father Christmas and Head Elf Pepper have explained every layer of Intent:

Concept What It Does Workshop Equivalent
Living Spec Source of truth, auto-updated Master Gift List
Coordinator Agent Plans, delegates, manages handoffs Father Christmas
Specialist Agents Execute focused tasks in parallel Craft Elves
Isolated Workspaces No collisions between parallel agents Private workbenches
Spec-Driven Development Plan first, code follows Write the List before carving begins
Context Engine Deep codebase knowledge for every agent The Workshop Library
Multi-Agent Orchestration Waves, handoffs, background agents Workshop floor choreography
Resumable Sessions State preserved across restarts Workshop never forgets

SIPOC: The Complete Intent Workflow

S — Suppliers I — Inputs P — Process O — Outputs C — Customers
Who/What Developer, Coordinator, Specialists, Context Engine, AI models Goal statement, codebase, constraints, model selection Spec → Review → Wave 1 agents → Handoffs → Wave 2 → Verifier → PR Verified code, living spec, decision log, merged PR Engineering team, product owner, CI/CD, end users
Workshop Pepper, Father Christmas, all Elves, the Library Gift order, Workshop constraints, quality standards Write List → Agree → Parallel building → Handoffs → QC → Sleigh All gifts delivered correctly, on time, to every child Children of the world

Getting Started

Intent is available in public beta for macOS. Download it here. It uses your existing Augment credits. You can also bring Claude Code, Codex, or OpenCode if you already have subscriptions.

Augment’s documentation is at docs.augmentcode.com. Their manifesto — The End of Linear Work — is worth reading before your first session.

A Final Word from Father Christmas

I have been doing this for over a thousand years. Every century, the tools improve. The quill gave way to the telegraph, the telegraph to the computer, and now the computer gives way to the agent. But the fundamental challenge has never changed: how do you coordinate complex, parallel work towards a shared goal, on an unmovable deadline, without chaos?The answer, in 1025 and in 2025, is the same: a clear plan, a good team, and the discipline to keep the plan honest.Augment Intent is the first software I have encountered that truly understands this. It puts the plan first. It keeps the plan honest. It coordinates the team without the Elves colliding. And it finishes on time.I am proud of everything we have built in this Workshop. I hope you will build something wonderful in yours.Merry Christmas, and Happy Coding.Ho ho ho! 🎅

This concludes the Santa Augmentcode Intent series. All eight episodes are available on dev.to under the the-software-s-journey organisation.

Thank you for reading. May your specs be living and your merges be clean.

I Thought I Understood Git. Then I Actually Learned the Difference Between Rebase and Merge.

Merge | Rebase graph

Merge | Rebase graph

I used git rebase long before I actually understood what it does.

I picked it up from a senior developer on my first team. The rule was simple: always rebase before you merge, keep the history clean.

I followed it. I typed the commands. I never questioned it.

Then one day I rebased the wrong branch and rewrote a week of shared history. That was the day I finally sat down and learned what I had been doing.

What merge actually does

git merge takes two branches and joins them. It creates a new commit — a merge commit — that has two parents: the tip of your branch and the tip of the branch you’re merging into.

# You're on feature/my-feature
git merge main

The result is a non-destructive operation. Nothing is rewritten. Your history grows a new node with two parents, and both lines of development are preserved exactly as they happened.

      A---B---C  feature
     /         
D---E---F---G---H  main (merge commit)

The upside — it’s safe.

The downside — if you merge large feature branches frequently, the commit history can become harder to read.

What rebase actually does

git rebase moves your commits. It takes the commits on your branch and replays them on top of another branch, one by one, as if you had started your work from there.

# You're on feature/my-feature
git rebase main
# Before
      A---B---C  feature
     /
D---E---F---G  main

# After
              A'--B'--C'  feature
             /
D---E---F---G  main

Notice the prime marks — A', B', C' are new commits. Same changes, new hashes. Rebase rewrites history.

The upside — your history looks linear and clean.

The downside — if anyone else has those original commits (A, B, C), their history now diverges from yours. This is what I did wrong.

The golden rule of rebase

Never rebase commits that exist outside your local repository.

If you’ve pushed a branch and someone else has pulled it, rebasing rewrites commits they already have. Their history diverges. Pulling becomes painful. You’ll see duplicate commits, conflicts that make no sense, and a very confused colleague.

The safe version — rebase only your local, unpushed work. Once commits are shared, merge.

# ✅ Safe — rebasing local work before pushing
git rebase main
git push origin feature/my-feature

# ❌ Dangerous — rebasing already-pushed commits
git push origin feature/my-feature  # already done
git rebase main                      # rewrites shared history
git push --force                     # now you've broken everyone else

Interactive rebase

Once you understand what rebase does, git rebase -i becomes one of the most useful tools in Git.

It lets you rewrite your local commit history before you share it — squash messy WIP commits into clean ones, reorder commits, edit commit messages, or split a large commit into smaller ones.

# Rewrite the last 3 commits interactively
git rebase -i HEAD~3

This opens an editor where you choose what to do with each commit:

pick a1b2c3 Add order validation
pick d4e5f6 fix typo
pick g7h8i9 WIP: still debugging

# Change to:
pick a1b2c3 Add order validation
squash d4e5f6 fix typo
drop g7h8i9 WIP: still debugging

The result — one clean commit instead of three messy ones.

This is what “clean history” actually means in practice — not avoiding merge commits, but being intentional about what you leave behind.

When to use which

Use merge when:

  • Integrating a completed feature branch into main or develop
  • Working on a shared branch where others have the same commits
  • You want to preserve the full context of when and how branches diverged
  • Merging release branches or hotfixes where an audit trail matters

Use rebase when:

  • Updating your local feature branch with changes from main before pushing
  • Cleaning up local commit history before opening a PR (with -i)
  • Keeping a long-lived feature branch up to date with the base branch
  • Working solo on a branch nobody else has touched

Never rebase:

  • Commits that have already been pushed and shared
  • main, develop, or any branch others are working from

The workflow that actually works

In practice, most teams land somewhere between “always merge” and “always rebase.” Here’s a workflow that’s served me well:

  1. Work locally on a feature branch
  2. Before opening a PR, git rebase -i to clean up commits
  3. git rebase main to update your branch with latest changes
  4. Push and open the PR
  5. Merge the PR into main (with a merge commit or squash merge — team’s call)

This gives you a clean PR history while preserving the merge commit that marks when a feature landed in main. Best of both approaches.

What I wish I had understood earlier

Rebase and merge aren’t competing philosophies. They’re different tools for different moments in a workflow.

Merge preserves history. Rebase rewrites it. Both are useful — but rewriting shared history is always a mistake.

I learned that the hard way. The senior developer who taught me to “always rebase” wasn’t wrong — he just assumed I understood what rebase does. I didn’t. I copied the command without understanding the consequence.

Now I understand both. And I still rebase — just never the wrong branch.

How does your team handle this? Do you have a strict rebase-only policy, or do you let developers decide? I’m curious whether the “clean history” argument is still winning in 2026. 👇

Why My Best Article Gets Zero ChatGPT Citations (And What I Changed)

`
Three thousand visitors a month. A handful of newsletter signups from it. Decent comments. By most measures, it’s performing well.

Then a few weeks ago I was testing something. I opened Perplexity and typed in the exact phrase my article targets. Five sources came back. Mine was not one of them. I tried ChatGPT with Browse enabled. Same thing. Gemini. Same.

I sat with that for a minute. A post I’ve spent more time on than anything else I’ve written is essentially invisible to the tools that a growing chunk of developers now use as their first stop for answers.

So I started digging.

What I assumed the problem was (I was wrong)

My first instinct was that the content was too long. AI tools prefer short, dense answers, right? So maybe 2,800 words was working against me.

Wrong. Some of the most-cited technical content I found when researching this runs 3,000+ words. Length is not the variable.

Then I thought maybe it was the topic. Too niche. Not enough people asking about it.

Also wrong. The keyword gets decent search volume and I confirmed other pages on the same topic were being cited regularly.

The actual problem was something I had not even thought to look at.

The thing I had never checked

I ran my article URL through GoForTool’s AI SEO Analyzer mostly out of curiosity. It does a full on-page audit specifically for AI search visibility, not traditional SEO.

The score came back: 27 out of 100.

I expected maybe 50, 60 tops given how much I’d thought about the writing. 27 was a genuine surprise. The fix list had five items and the top three explained everything.

Problem one: My answer appeared at word 380.

The tool flags something called answer position — how many words appear before your first direct response to the implied question in your title. Mine was at 380. The benchmark for regularly-cited content is under 120.

When I read back through my opening, I could see it immediately. I open with a story about when I first ran into the problem the article addresses. Good for human readers building context. Terrible for an AI trying to extract a clean answer quickly. LLMs weight earlier content more heavily. If your answer is buried, they will find someone else’s answer that isn’t.

Problem two: I was using vague language throughout.

I had phrases like “modern JavaScript frameworks,” “popular bundlers,” “leading cloud providers.” None of those are entities an AI can pin to anything specific. ChatGPT-4o, Perplexity, and Gemini all understand named things. They understand “Vite 5.0,” “esbuild,” “Cloudflare Workers.” They do not understand “popular bundlers.” That phrase could mean anything.

Problem three: PerplexityBot was blocked on my site.

This one stung. A robots.txt rule I had added over a year ago was blocking PerplexityBot from crawling my content. I had not thought about it since. Every article I have written in the last 14 months has been invisible to Perplexity not because of anything wrong with the writing but because of a single line in a config file.

The fixes and how long they took

Fix one: Rewrote the opening paragraph. I moved the story to a later section and put a direct, specific answer up front. Something like: “Optimising Vite build performance for large React apps requires four configuration changes: chunk splitting with manualChunks, enabling build.minify with esbuild, configuring rollupOptions for tree shaking, and using vite-plugin-inspect to identify bottlenecks.” That is now the first thing anyone reads. 20 minutes.

Fix two: Named everything specifically. Went through the article and replaced every vague reference. “Popular bundlers” became “Vite 5, Rollup 4, and esbuild.” “Modern browsers” became “Chrome 120, Firefox 121, and Safari 17.” This took about 15 minutes and made the article substantially more useful for human readers too, not just AI.

Fix three: Fixed robots.txt. Added explicit Allow rules for GPTBot, PerplexityBot, ClaudeBot, and Google-Extended. Five minutes.

Fix four: Added FAQPage schema. Three question-answer pairs embedded in a JSON-LD script tag in the page head. This one I did through GoForTool because it generates the schema for you based on your content — I did not have to write JSON by hand. 10 minutes.

Total time: about 50 minutes across an evening.

What happened after

I gave it two weeks before checking anything.

The article now shows up in Perplexity responses for two related queries. Not my exact target phrase yet, but two adjacent ones. ChatGPT Browse cited it once in a test I ran last week — showed up as a source in an answer about Vite configuration.

My GEO score went from 27 to 71 after the changes. GoForTool lets you re-scan after edits so you can confirm things landed before moving on.

The Google traffic has not changed. Same 3,000 visitors, same position. But now the content is doing a second job it wasn’t doing before.

The robots.txt thing is probably affecting you too

I want to highlight this specifically because I think it is the most commonly missed issue and the fastest to fix.

A lot of developers added security-focused robots.txt rules over the past few years in response to scrapers and bots. Totally reasonable. But AI crawlers use user agents that many of those rules accidentally caught.

Check yours right now. Go to yourdomain.com/robots.txt. Look for any Disallow rules that apply to wildcards or to specific bots you may not have evaluated recently. Then add explicit Allow entries for the crawlers you actually want:

`robot_framework
User-agent: GPTBot
Allow: /

User-agent: PerplexityBot
Allow: /

User-agent: ClaudeBot
Allow: /

User-agent: Google-Extended
Allow: /
`

Seriously, do this before anything else. If you are blocked, nothing else you do matters.

Running this audit on your own content

The manual version of what I described takes 15 to 20 minutes per URL: read through your opening for answer position, count your named entities, inspect your schema, check robots.txt. Doable, but slow if you have a lot of content.

The automated version through GoForTool’s AI SEO Analyzer takes about 90 seconds per URL and catches things I was missing manually. It also generates the schema for you rather than making you write it from scratch.

I’m now running it on every article before publishing. The pre-publish check has become part of my workflow the same way running a linter before committing has become automatic — you just do it.

What does your GEO score come back as? Drop it in the comments with a rough description of your content type. Happy to suggest what to fix first based on the number.

seo #ai #webdev #javascript`

Multi-Agent Systems on AWS Lambda with Durable Functions

In my previous post on multi-agent systems, I built a purchasing coordinator where a coordinator agent routes requests to specialist agents based on RISEN prompt contracts. RISEN structures system prompts into five components: Role, Instructions, Steps, Expectation, and Narrowing. In a multi-agent system, the Steps section encodes the routing logic and the Narrowing section prevents agents from doing each other’s work. A laptop triggers Price Research and Delivery. A used car triggers all five specialists. The routing logic lives in the prompts, not in code. It works, but it runs in a single process. No fault isolation, no independent scaling, no durability. If the process crashes halfway through specialist consultations, you start over.

The durable functions post solved the durability problem for a support ticket triage workflow: checkpoint each step, suspend for human review, resume where you left off. But that was a single-agent workflow.

This post combines the two. The same purchasing coordinator, deployed to AWS Lambda, with each specialist as its own Lambda function. The coordinator is a durable function that checkpoints every specialist call. If it’s interrupted after consulting three of five specialists, it resumes from the fourth. When a high-value purchase needs human approval, the function suspends, compute charges stop, and it picks up exactly where it left off when the approver responds.

The two SDKs have distinct roles. The Strands Agents SDK handles the AI reasoning: the planning agent decides which specialists to call, and the synthesis agent produces the recommendation. The durable execution SDK handles the infrastructure: checkpointing, parallel dispatch, suspension, and replay. The coordinator uses both. The specialists only use Strands.

The complete source code is on GitHub: github.com/gunnargrosch/durable-multi-agent-purchasing.

What Changes from the In-Process Demo

The multi-agent post used tool() callbacks for specialist dispatch: each specialist was defined as a Strands tool, and the coordinator agent called them as functions within the same process. That’s the simplest possible architecture, and it’s fine for development. Here’s what changes when you deploy:

In-Process (previous post) Lambda + Durable (this post)
Specialist invocation tool() callback, in-process context.invoke(), separate Lambda function
Execution model Sequential (one specialist at a time) Parallel via context.parallel()
Fault tolerance None. Process crash = restart Checkpointed. Resume from last completed step
Scaling Single process Each specialist scales independently
Human-in-the-loop Not supported waitForCallback() with zero-cost suspension
IAM isolation Shared process permissions Per-specialist least-privilege policies
Routing visibility Real-time hook as each tool is called Checkpointed plan step with routing summary
Infrastructure npm start SAM template, 6 Lambda functions

The RISEN prompts carry over with only minor adjustments. The coordinator’s prompt splits into two phases (plan and synthesis) instead of a single invocation, because the durable function needs to checkpoint the plan before dispatching specialists. The specialist prompts are unchanged.

How It Works

If you haven’t read the durable functions post, here’s the key mental model: durable functions use checkpoint and replay. Your handler re-executes from the top on every resume, but completed steps return their cached results instantly without re-executing. New work picks up from where it left off. The SDK manages this transparently. You write sequential code and the infrastructure handles the rest.

[CoordinatorFunction] — Lambda Durable Function (Sonnet, 1536MB)
  ├→ context.step('plan')            → Planning agent selects specialists
  ├→ context.parallel('specialists') → Runs selected specialists concurrently:
  │     ├─ context.invoke(PriceResearchFunction)  (Haiku)   ← always
  │     ├─ context.invoke(FinancingFunction)       (Haiku)   ← if value > $5K
  │     ├─ context.invoke(DeliveryFunction)        (Haiku)   ← if physical product
  │     ├─ context.invoke(RiskAssessmentFunction)  (Sonnet)  ← if value > $10K / used
  │     └─ context.invoke(ContractReviewFunction)  (Haiku)   ← if subscription/lease
  ├→ context.step('synthesize')      → Synthesis agent combines findings
  └→ context.waitForCallback()       → Human approval (when requireApproval=true)

Three things to notice:

  1. context.invoke() is a new primitive not covered in the durable functions post. It’s the SDK’s built-in method for calling other Lambda functions. It checkpoints the result automatically and suspends the coordinator while waiting, so you don’t pay for compute during specialist execution. Despite the SDK’s API reference describing it as invoking “another durable function,” context.invoke() works with any Lambda function. The specialists here are standard functions without DurableConfig.
  2. context.step() wraps the planning and synthesis phases with retry strategies, just like the Bedrock calls in the support triage demo. Each step checkpoints its result. On replay, it returns the cached result without re-executing.
  3. context.parallel() wraps the specialist invocations so they run concurrently, each independently checkpointed. If specialist 3 of 5 fails, the other four results are preserved.

The SAM Template

Here are the key parts of the coordinator’s definition in the SAM template. The template defines 6 functions total: the coordinator plus 5 specialists that follow the same pattern.

CoordinatorFunction:
  Type: AWS::Serverless::Function
  Properties:
    Handler: coordinator.handler
    MemorySize: 1536
    Timeout: 300
    Environment:
      Variables:
        COORDINATOR_MODEL_ID: !Sub global.${CoordinatorModelId}
        PRICE_RESEARCH_FUNCTION: !Ref PriceResearchFunction
        # ... remaining specialist function references
    Policies:
      - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicDurableExecutionRolePolicy
      - Statement:
          - Effect: Allow
            Action:
              - bedrock:InvokeModel
              - bedrock:InvokeModelWithResponseStream
            Resource: # Bedrock model + inference profile ARNs
          - Effect: Allow
            Action: lambda:InvokeFunction
            Resource:
              - !GetAtt PriceResearchFunction.Arn
              - !GetAtt FinancingFunction.Arn
              - !GetAtt DeliveryFunction.Arn
              - !GetAtt RiskAssessmentFunction.Arn
              - !GetAtt ContractReviewFunction.Arn
    DurableConfig:
      ExecutionTimeout: 86400
      RetentionPeriodInDays: 7
    AutoPublishAlias: live

A few things to note:

  • No API Gateway, no function URLs, no public endpoints. The multi-agent post previewed two deployment options: Lambda with HTTP endpoints, or AgentCore containers. This implementation goes a simpler route. context.invoke() calls specialists directly via the Lambda API. The coordinator’s IAM policy grants lambda:InvokeFunction on each specialist ARN. Specialists are unreachable from outside the coordinator’s execution role.
  • Shared handler, different prompts. All five specialists use the same specialist.ts handler. The PROMPT_NAME environment variable selects which RISEN prompt to load. This keeps the template repetitive but predictable: each specialist block differs only in its name, PROMPT_NAME, and model ID.
  • Per-specialist model selection. Risk Assessment gets AdvancedSpecialistModelId (Sonnet) for stronger reasoning. The other four get SpecialistModelId (Haiku). Same pattern as the in-process demo, now enforced at the infrastructure level.
  • DurableConfig with 24-hour execution timeout. The used car scenario with human approval can sit overnight. Each individual invocation is bounded by Timeout: 300 (the coordinator’s per-replay limit). ExecutionTimeout: 86400 is the outer wall-clock limit across all replays and suspensions.
  • InvokeModelWithResponseStream is included because the Strands SDK’s BedrockModel may use streaming internally for token generation. Without it, the coordinator would get AccessDeniedException on agent invocations.
  • ESM build. The coordinator uses ESM (Format: esm) with esbuild. The full template includes a Banner that injects a createRequire shim because some dependencies expect CommonJS require. This is a known pattern for ESM Lambda functions with mixed dependencies. See the repo’s template.yaml for the complete Metadata block.

The Coordinator Handler

The coordinator is the only durable function. Here’s the handler skeleton showing the four durable phases. The full source includes types, the specialist registry, retry strategies, and routing summary logic.

export const handler = withDurableExecution(async (
  event: CoordinatorEvent,
  context: DurableContext
): Promise<Record<string, unknown>> => {

  // Phase 1: Plan — agent decides which specialists to consult
  const plan = await context.step<AnalysisPlan>('plan', async () => {
    let capturedPlan: AnalysisPlan | null = null
    const planTool = tool({
      name: 'create_analysis_plan',
      inputSchema: z.object({
        specialists: z.array(z.object({
          name: z.enum(['price-research', 'financing', 'delivery',
                        'risk-assessment', 'contract-review']),
          prompt: z.string(),
        })),
      }),
      callback: async (input) => { capturedPlan = input; return 'Plan created.' },
    })
    const agent = new Agent({ model, systemPrompt: planPrompt, tools: [planTool], printer: false })
    await agent.invoke(event.request)
    if (!capturedPlan) throw new Error('Planning agent did not call create_analysis_plan')
    return capturedPlan
  }, { retryStrategy: bedrockRetry })

  // Phase 2: Consult specialists in parallel via context.invoke()
  const results = await context.parallel<{ name: string; response: string }>('specialists',
    plan.specialists.map((spec) => ({
      name: spec.name,
      func: async (ctx) => {
        try {
          const specialist = SPECIALISTS[spec.name]
          if (!specialist) {
            throw new Error(`Unknown specialist: ${spec.name}`)
          }
          const result = await ctx.invoke<{ prompt: string }, { response: string }>(
            spec.name, specialist.functionName, { prompt: spec.prompt }
          )
          return { name: spec.name, response: result.response }
        } catch (err) {
          const display = SPECIALISTS[spec.name]?.display ?? spec.name
          return { name: spec.name, response: `[${display} unavailable: ${err instanceof Error ? err.message : 'unknown error'}]` }
        }
      },
    }))
  )

  // Phase 3: Synthesize specialist findings into a recommendation
  const response = await context.step('synthesize', async () => {
    // Build findings from checkpointed parallel results, invoke synthesis agent
    // (see full source for details)
  }, { retryStrategy: bedrockRetry })

  // Phase 4: Human approval (optional)
  if (event.requireApproval) {
    const approval = await context.waitForCallback<ApprovalPayload>(
      'approval',
      async (callbackId) => { context.logger.info('approval_callback_created', { callbackId }) },
      { timeout: { hours: 24 }, serdes: defaultSerdes }
    )
    if (!approval.approved) return { status: 'rejected', recommendation: response }
  }

  return { status: event.requireApproval ? 'approved' : 'completed', recommendation: response }
})

Let’s walk through what’s happening in each phase.

Phase 1: Plan

The planning phase is the biggest change from the in-process demo. In the previous post, the coordinator was a single agent that both decided which specialists to call and synthesized their findings. Here, planning and synthesis are separate agents wrapped in separate context.step() calls.

Why split them? Checkpointing. In the in-process demo, if the coordinator crashes after calling three specialists, you lose the routing decision and all three results. With durable functions, the plan is checkpointed as a single unit. If the function replays after the plan step, it returns the cached plan instantly without calling Bedrock again.

The planning agent uses tool() with a Zod schema to produce structured output. The create_analysis_plan tool captures the plan into a closure variable, and the step returns it as its checkpointed result. If the agent doesn’t call the tool, the step throws an error. The retry strategy will attempt it three times, but this is a non-transient failure: if the model didn’t call the tool on the first attempt, retries won’t help. After retries are exhausted, the execution fails.

Phase 2: Parallel specialists

This is where context.invoke() replaces tool() callbacks. Each specialist invocation is a branch inside context.parallel():

const result = await ctx.invoke<{ prompt: string }, { response: string }>(
  spec.name,             // step name for checkpoint history
  specialist.functionName, // Lambda function name from environment
  { prompt: spec.prompt }  // payload
)

context.invoke() calls the specialist Lambda function directly, checkpoints the result, and suspends the coordinator while waiting. You don’t pay for coordinator compute while specialists are executing. On replay, completed invocations return their cached results without re-invoking the specialist.

Each branch has a try/catch for graceful degradation. If the Delivery specialist times out, the coordinator still gets results from Price Research, Financing, Risk Assessment, and Contract Review. The synthesis agent notes the gap and advises the buyer to investigate delivery independently. This is a meaningful improvement over the in-process demo, where a failed specialist tool call could derail the entire coordinator.

Phase 3: Synthesize

The synthesis agent receives all specialist findings and produces a structured recommendation. Like the plan step, the entire synthesis is one checkpointed unit. The synthesis prompt’s Narrowing section prevents it from overriding specialist findings or filling in gaps from failed specialists.

Phase 4: Human approval

When requireApproval is true (the used car scenario sets this), the function suspends at waitForCallback. Compute charges stop. The approver reviews the recommendation and sends a callback via the Lambda API or the demo’s interactive prompt. The function resumes and returns the final status.

Note the serdes: defaultSerdes option on the callback. As covered in the durable functions post’s gotchas, waitForCallback defaults to passthrough serialization (not JSON.parse). Without defaultSerdes, approval.approved would be undefined at runtime even though TypeScript thinks it’s a boolean.

The Specialist Handler

All five specialists share one handler. The PROMPT_NAME environment variable selects the behavior:

const promptName = process.env.PROMPT_NAME!
const systemPrompt = loadPrompt(promptName)
const model = createModel(SPECIALIST_MODEL)

export const handler = async (
  event: SpecialistEvent,
  context: Context
): Promise<{ response: string }> => {
  const logger = makeLogger({ handler: promptName, requestId: context.awsRequestId })
  const tools = loadSpecialistTools(promptName, logger)
  const agent = new Agent({ model, systemPrompt, tools, printer: false })
  const result = await agent.invoke(event.prompt)
  return { response: result.toString() }
}

Specialists are standard Lambda functions, not durable. They don’t need checkpointing because each one completes in a single invocation (a Bedrock call and response processing). The coordinator’s context.invoke() handles the durability: if a specialist invocation times out, the coordinator can retry from the checkpoint without re-running specialists that already succeeded.

loadSpecialistTools returns specialist-specific tools (see src/lib/specialist-tools.ts). Most specialists are pure reasoning (no tools). The Price Research specialist has a save_price_snapshot tool that logs structured price data. In production, that tool could write to DynamoDB or call a pricing API. The coordinator never sees these tools. They’re scoped to each specialist’s domain.

The Prompt Split

The in-process demo had one coordinator prompt with routing in the Steps section. The durable version splits this into two prompts:

coordinator-plan handles routing. Here’s the Steps and Expectation sections:

# Steps
1. Read the purchase request and identify what is being purchased, its likely category
   (vehicle, electronics, real estate, software, etc.), the approximate value range,
   and any special circumstances mentioned or implied (used/secondhand, financing needed,
   physical delivery, contract or subscription, high value).
2. Select specialists using these routing rules:
   - Always include price-research to compare options and assess market value.
   - Include financing if the estimated value exceeds $5,000, or if financing or a loan
     is mentioned or implied.
   - Include delivery if the item is a tangible physical product that must be physically
     received — electronics, appliances, vehicles, furniture, machinery. Vehicles always
     need delivery planning (transport, pickup, or test drive logistics). Do not include
     for purely digital purchases (software, SaaS subscriptions, downloadable content).
   - Include contract-review if the purchase involves a subscription, lease, warranty
     agreement, service contract, purchase agreement, or any multi-year financial
     commitment. Vehicle and real estate purchases always involve contracts.
   - Include risk-assessment if the estimated value exceeds $10,000, the item is used or
     secondhand, or the category carries known risk (vehicles, real estate, machinery).
     Do not include for new electronics, appliances, or standard retail under $10,000.
3. For each selected specialist, write a focused prompt describing what to analyze about
   this specific purchase. Include relevant details from the request (item, price,
   condition, location, urgency). The specialist's own system prompt defines its role —
   do not repeat the role description in your prompt.
4. Call create_analysis_plan exactly once with the selected specialists and their prompts.

# Expectation
A single call to create_analysis_plan containing:
- An array of specialists, each with a name (from the allowed set) and a prompt string.
- Only specialists whose routing criteria are met.
- Prompts that are specific to this purchase, not generic templates.

These are the same routing rules from the original coordinator prompt in the multi-agent post. The difference: instead of calling specialist tools directly (Steps 2-6 in the original said “invoke the research_prices tool,” “invoke the evaluate_financing tool”), the plan agent calls create_analysis_plan once with all selected specialists. The actual dispatch happens via context.invoke() in Phase 2.

coordinator-synthesis handles the final recommendation. It receives specialist findings and produces the buyer-facing output. Its Narrowing prevents it from contradicting specialists or filling in missing analysis.

This split means the coordinator makes two agent invocations (plan + synthesize) instead of one. Each agent invocation may involve multiple Bedrock round-trips internally as the Strands SDK handles reasoning. The trade-off is worth it: the plan is checkpointed before any specialist is called, and the synthesis is checkpointed after all specialists complete. On replay, both return cached results instantly.

Testing with the Local Runner

The durable functions post showed LocalDurableTestRunner for the support triage workflow. The multi-agent demo adds a new pattern: registerFunction for mocking context.invoke() targets.

// Register mock handlers for each specialist Lambda function
runner = new LocalDurableTestRunner({ handlerFunction: handler });
runner
  .registerFunction("PriceResearchFunction", specialistHandler)
  .registerFunction("FinancingFunction", specialistHandler)
  .registerFunction("DeliveryFunction", specialistHandler)
  .registerFunction("RiskAssessmentFunction", specialistHandler)
  .registerFunction("ContractReviewFunction", specialistHandler);

// Run the full workflow and verify the result
const result = runner.run({ payload: laptopPayload });
const output = await result;
expect(output.getStatus()).toBe("SUCCEEDED");
expect(output.getResult().routing.called).toContain("Price Research");

registerFunction maps function names to local handlers. When the coordinator calls context.invoke("PriceResearchFunction", payload), the test runner routes it to the registered mock instead of calling Lambda. This lets you test the full checkpoint/replay lifecycle without deploying or calling Bedrock. The full test suite also tests callback suspension and resumption using runner.getOperation() and sendCallbackSuccess(), the same pattern from the durable functions post.

The test suite covers seven scenarios: standard flow, all-5-specialists flow, approval, rejection, specialist failure with graceful degradation, callback failure, and planning agent failure.

Try the Demo

The repo includes an interactive demo with three purchase scenarios:

Scenario Value Specialists Approval
Laptop $1,500 Price Research + Delivery No
Used car $18,000 All 5 specialists Yes (waitForCallback)
SaaS subscription $200/mo Price Research + Contract Review Yes (waitForCallback)

You’ll need:

  • Node.js 24+ and npm
  • For cloud mode: AWS SAM CLI 1.153.1+ and Bedrock access to Claude Sonnet 4.6 and Claude Haiku 4.5

Local mode (no AWS credentials needed)

git clone https://github.com/gunnargrosch/durable-multi-agent-purchasing.git
cd durable-multi-agent-purchasing
npm install
npm run demo:local -- --ticket=used-car

Local mode uses mocked Bedrock responses. The used car scenario exercises all four durable primitives: step (plan and synthesis), invoke (specialist calls), parallel (concurrent dispatch), and waitForCallback (human approval where you play the purchase approver).

Cloud mode (real Bedrock responses)

sam build
sam deploy --guided
npm run demo:cloud -- --ticket=used-car --region=us-east-1

Cloud mode invokes the deployed coordinator with real Bedrock calls. You’ll see actual AI-generated specialist analyses and a synthesized recommendation. The demo polls execution history and prompts you when the approval callback is created.

The demo uses direct aws lambda invoke with --invocation-type Event for simplicity. In production, the coordinator would typically sit behind an upstream service: an API Gateway endpoint receiving purchase requests, an EventBridge rule triggered by order events, or an SQS queue processing a backlog. The coordinator itself doesn’t care how it’s invoked. It receives the event payload and the durable SDK handles the rest.

Inspecting execution history

After a cloud run, you can inspect the execution in the Lambda console under the Durable executions tab, or via the CLI:

aws lambda list-durable-executions-by-function 
  --function-name durable-multi-agent-purchasing-CoordinatorFunction

aws lambda get-durable-execution 
  --durable-execution-arn "<arn-from-list>"

The execution history shows each step’s status and timing: when the plan completed, how long each specialist took, and whether the callback is pending or resolved.

Design Decisions

context.invoke() over HTTP

The multi-agent post previewed two deployment approaches: Lambda with API Gateway/Function URLs (HTTP), or AgentCore containers. This demo takes a third path: context.invoke() for direct Lambda-to-Lambda invocation.

The result is simpler than either preview option. No API Gateway resources, no function URLs, no SigV4 signing, no HTTP client configuration. The coordinator calls specialists via the Lambda API, and the durable SDK handles checkpointing and retry. Specialists are unreachable from outside the coordinator’s execution role, which is better isolation than an HTTP endpoint with IAM auth.

The trade-off: specialists are only callable from within a durable function. If you later need specialists accessible from other services (a REST API, a Step Functions state machine, another team’s coordinator), you’d need to add API Gateway or function URLs at that point. For this use case, where one coordinator owns all specialist dispatch, direct invocation is the right call.

Splitting plan from synthesis

The in-process coordinator was a single agent invocation: read the request, call specialist tools, synthesize findings. With durable functions, that becomes two separate agents wrapped in separate context.step() calls.

This introduces an extra Bedrock call, but the checkpointing benefits are significant. The plan is preserved before any specialist runs. If the function replays after three of five specialists complete, the plan doesn’t need to be regenerated. The synthesis is preserved after all specialists complete. If the function replays during the approval callback, the recommendation doesn’t need to be regenerated.

The alternative would be wrapping the entire coordinator in a single step. That would mean one Bedrock conversation (cheaper) but no intermediate checkpoints. A failure during synthesis would replay from the beginning, including all specialist invocations. With the split, a synthesis failure only retries the synthesis.

Planning agent with tool capture

The planning agent uses tool() with a Zod schema to produce structured output. This is the same pattern from the in-process demo, but used differently. In the previous post, tools were the dispatch mechanism (calling tools = calling specialists). Here, the tool is purely for structured output capture. The plan step returns the captured plan as its checkpointed result, and context.invoke() handles the actual specialist dispatch.

Why not just have the planning agent return JSON directly? Tool calling with a schema gives you validation at the SDK level. If the agent returns a plan with an invalid specialist name, Zod catches it before the plan is checkpointed. Without the tool, you’d parse and validate the JSON yourself, and an invalid plan could be checkpointed and break on every subsequent replay.

Graceful degradation in parallel

Each specialist branch in context.parallel() has a try/catch that returns an error message instead of throwing:

catch (err) {
  return { name: spec.name, response: `[unavailable: ${err instanceof Error ? err.message : 'unknown error'}]` }
}

This means a failed specialist doesn’t fail the entire parallel block. The synthesis agent receives partial results and notes the gap. Without the catch, a failed specialist would fail its branch. Whether that fails the entire parallel block depends on the completion config, but either way, synthesis would never run with partial results. With the catch, every branch succeeds (some with error messages), the parallel block always completes, and the synthesis agent can work with whatever it has.

Cost

The main cost is Bedrock token usage, not Lambda compute.

Scenario Specialists Agent invocations Estimated cost
Laptop (2 specialists) 2 ~4 (plan + 2 specialists + synthesize) ~$0.02-0.05
Used car (5 specialists) 5 ~7 (plan + 5 specialists + synthesize) ~$0.05-0.15

The coordinator uses Sonnet (~$3/$15 per million input/output tokens). Most specialists use Haiku (~$1/$5 per million input/output tokens). Risk Assessment uses Sonnet. Lambda compute for the active execution periods (plan, specialist dispatch, synthesis, replay) totals ~$0.0001. During the waitForCallback suspension, compute charges are zero.

Things to Watch For

Checkpoint size. Each step result is serialized and stored as a checkpoint. The durable functions post covered the 256KB limit per checkpoint. With 5 specialists returning Bedrock responses, the parallel result could get large if the model is verbose. Monitor response sizes. If you hit the limit, truncate or summarize specialist responses before returning them from the branch, or store full responses in S3 and return a reference.

Debugging failures. If the plan step fails after exhausting retries, or a specialist consistently times out, the execution moves to FAILED status. Use get-durable-execution to see which step failed and the error message. The coordinator uses context.logger (replay-aware), so CloudWatch Logs show each phase’s progress without duplicate lines from replays. Specialist failures are easier to debug since each specialist has its own log group (sam logs --name PriceResearchFunction --tail).

Replay safety. Durable functions re-execute your handler from the top on every resume. Completed context.step() and context.invoke() calls return cached results, but any code outside those primitives runs again on every replay. If you add a side effect (writing to DynamoDB, sending a notification, calling an external API), wrap it in a context.step() so it executes exactly once. The coordinator’s comment on the save-recommendation step shows this pattern. Without the step wrapper, you’d send duplicate notifications every time the function replays.

Cold starts. The used car scenario invokes 5 specialist Lambda functions in parallel. If all 5 are cold, that’s 5 concurrent cold starts: arm64 Node.js 24, the Strands SDK, and the Bedrock client. This can add several seconds to the first specialist round. The in-process demo had no cold start penalty for specialists since everything ran in one process. In practice, the cold start overhead is small relative to the Bedrock inference time that follows. For workflows that already include multi-second model calls and human approval, a few extra seconds on the first invocation is rarely the bottleneck.

Wrapping Up

This post showed how to take a multi-agent system from a single-process demo to a deployed, fault-tolerant system on Lambda with durable functions. The RISEN prompts carry over with minimal changes. The architectural shift is from in-process tool calls to checkpointed Lambda-to-Lambda invocations with independent scaling, failure isolation, and human-in-the-loop approval.

Additional Resources

What multi-agent workflow would you deploy with durable functions? Let me know in the comments!

Santa Augmentcode Intent Ep.5

Finishing Before Christmas — Spec-Driven Development 📜

Accompanying source code repository: Santa Augmentcode Intent

Do you know why Christmas always arrives on time? Not because I am superhuman. Not because the reindeer are faster than physics should allow. Christmas arrives on time because of one inviolable rule in the North Pole: nothing gets built until we agree, in writing, on what done looks like. We call it the Master Gift List. The world calls it Spec-Driven Development. The result is the same: no surprises on Christmas morning.

The Old Way: Code First, Regret Later

There is a seductive pattern in software development that I call Build First, Discover Later. It goes like this:

  1. Someone has a rough idea.
  2. A developer (or, increasingly, an agent) starts building immediately.
  3. Halfway through, the stakeholder sees a demo and says: “That is not what I meant at all.”
  4. Everything is reworked. The deadline slips. Someone’s Christmas is ruined.

I have witnessed this pattern in the Workshop. A zealous Elf, eager to begin, starts carving a rocking horse before Father Christmas has decided whether it should be red or blue, large or small, with or without a mane. Three hours later, the horse is red, large, and maned — and the child’s letter clearly specified blue, small, no mane.

The problem is not the Elf’s carving ability. The problem is that work began before intent was explicit.

What Is Spec-Driven Development?

Spec-Driven Development (SDD) is a workflow practice, not a programming paradigm. It has one central principle:

The spec is the primary artifact. Code is the result of executing it.

In SDD, you do not write code and then document it. You write a spec — a precise, executable description of what you intend to build — and then agents (or humans) implement it. The spec comes first. The spec stays current. The spec is what gets reviewed.

Augment Intent was built around this principle. Their product manifesto states it plainly: at that point, the plan becomes the product.

The Three Pillars of SDD in Intent

Pillar 1: Write the Spec Before Any Code

The Coordinator Agent will not spawn Specialists until a spec exists. This is not bureaucracy — it is discipline. A spec written before implementation captures the human’s intent (what success looks like, what constraints exist, what decisions have been made) rather than narrating code that has already been written.

A good pre-implementation spec answers:

  • What is the goal, measured how?
  • What must not change (frozen modules, existing contracts, budget)?
  • What does “done” look like in a test?
  • Which parts depend on which others?

Pillar 2: The Spec Updates as Work Progresses

A spec that does not evolve becomes fiction. Intent’s living spec updates automatically as agents complete tasks, record decisions, and encounter new constraints. When you return to the spec after a day of agent work, it reflects what was actually built, not what was originally planned.

This closes the gap that traditionally exists between the plan and the product. In most organisations, that gap is enormous and filled with undocumented decisions, tribal knowledge, and polite disagreements about what the requirements “really meant.”

Pillar 3: Review the Spec, Not Just the Code

Code review is expensive. Reviewing a 2,000-line pull request is difficult, slow, and error-prone. Reviewing a spec is cheap. A well-structured spec can be reviewed in minutes, catching fundamental misalignments before a single line of code is written.

SDD moves the review point earlier in the pipeline. You review intent first. Implementation follows. This is why Intent has the Verifier Agent checking output against the spec — because the spec is the ground truth, and the code is measured against it.

The Christmas Deadline as a Forcing Function

Father Christmas has one advantage over most product teams: an unmovable deadline. Christmas is December 25th. Always. Everywhere. It does not slip. It does not move to accommodate a stakeholder who changed their mind in November.

This deadline is a gift (pun intended). It forces the Workshop to be explicit about scope from the very beginning. We cannot add “one more toy” on December 20th without removing something else. Every addition requires a trade-off. The Master Gift List enforces this.

SDD imposes the same discipline on software teams. When the spec defines the scope, additions are visible. You can see exactly what is in and what is out. Scope creep does not happen silently — it requires a spec change, which requires a decision, which requires a conversation. That conversation, had early, is cheap. Had at 11pm on December 24th, it is catastrophic.

SDD vs Traditional Approaches

Aspect Traditional Spec-Driven
Source of truth Code Living Spec
When specs are written After or alongside code Before code
Spec accuracy over time Drifts (document rot) Stays current (auto-updated)
Agent alignment Re-explain each session Automatic via spec
Review point Code review only Spec review + code review
Scope control Implicit, often invisible Explicit, requires spec change
Refactoring cost Break and fix Spec-guided

SIPOC: Spec-Driven Development Workflow

S — Suppliers I — Inputs P — Process O — Outputs C — Customers
Who/What Product owner, developer, Coordinator Agent Business goal, constraints, success criteria Write spec → Review spec → Spawn agents → Agents implement → Spec updates → Verifier checks Working software that matches intent, accurate documentation, decision log Engineering team, product owner, QA, end users
Workshop Management, Head Elf Pepper, Father Christmas Child’s wish list, Workshop constraints, quality standards Write the Master List → Agree on scope → Elves build → List updates → QC checks Gifts that match the wish list, accurate gift manifest, no surprises Children, parents, Father Christmas himself

A Worked Example: The Christmas Eve Delivery App

Let us say Head Elf Pepper wants to build a real-time delivery tracker for the sleigh. The old way:

Pepper: "Build a delivery tracker."
Agent: [builds something]
Pepper: "That is not what I wanted."
[Repeat until December 24th.]

The SDD way:

## Christmas Eve Delivery Tracker — Spec

### Goal
Real-time map showing sleigh position and remaining deliveries,
updating every 30 seconds, accessible on elves' tablets.

### Success Criteria
- [ ] Map renders in < 1 second on standard tablet browser
- [ ] Position updates within 30s of sleigh movement
- [ ] Remaining house count is accurate (matches manifest)
- [ ] Works offline (last known position shown if signal lost)

### Constraints
- Must use existing Reindeer GPS API (v2)
- No server-side changes to Delivery Manifest Service
- Must work in Safari (elves use iPads)

### Out of Scope (v1)
- Historical route replay
- Per-gift tracking
- Weather overlay

### Tasks
- [ ] Implement GPS polling service — Tracker Elf
- [ ] Build map component — Map Elf
- [ ] Integrate manifest API — Data Elf
- [ ] Offline fallback — Resilience Elf
- [ ] Integration tests — Test Elf

This spec takes ten minutes to write and saves days of rework. The Elves know exactly what to build. Pepper reviews the spec and agrees it is correct before a line of code is written. The Verifier knows exactly what to check.

This is SDD. This is how Christmas gets delivered on time.

What Comes Next

In Episode 6, Father Christmas will explain the Context Engine — the magical knowledge base that ensures every Elf in the Workshop understands every toy design, every material property, and every past decision. Without it, SDD is impossible at scale.

The plan is the product. Everything else is wrapping paper.Ho ho ho! 🎅

Part of the Santa Augmentcode Intent series. Published on dev.to under the the-software-s-journey organisation.

Santa Augmentcode Intent Ep.2

The Master Gift List That Writes Itself 🎄

Accompanying source code repository: Santa Augmentcode Intent

Every year, on the first of December, I sit at my great oak desk and open the Master Gift List. In the old days, I wrote it once and hoped for the best. By the fifteenth, it bore little resemblance to reality. An Elf had improvised. A supplier had changed a toy’s colour. Three children had written amended letters. The List lied to me — and I only found out on Christmas Eve.No more.

The Problem With Static Specifications

In software, as in Christmas, requirements arrive in waves. The product owner changes her mind. The designer refines the mockup. The security review adds three new constraints.

Traditional specifications — whether a Confluence page, a Notion doc, or a PDF handed over at the start of a sprint — share one fatal flaw: they are written once and then abandoned. The moment the first line of code is committed, the spec begins to drift. By the time you ship, nobody is quite sure what the original intention was.

Fred Brooks called this “document rot.” Father Christmas calls it “the Lying List,” and it has ruined more Christmas Eves than I care to admit.

The Living Spec: What Intent Does Differently

In Augment Intent, the specification is not a document you write and forget. It is a living spec — a Markdown file that lives inside your workspace and evolves as work progresses.

Here is how it works:

1. You seed the spec. Head Elf Pepper (that is you) types a goal into the Coordinator. The Coordinator drafts an initial spec: requirements, architecture decisions, task breakdown, success criteria.

2. The spec is the source of truth. Every Specialist Agent receives the spec as its briefing. Not a copy of a chat history. Not a vague summary. The actual, versioned spec.

3. As Elves complete tasks, the spec updates. When the Auth Agent finishes the token service, the spec marks that task complete. When the API Agent discovers a complication, the spec records the decision. The spec stays accurate because it is being written by the same agents that are doing the work.

4. When you change your mind, the update propagates. If Pepper changes a constraint mid-flight, the Coordinator updates the spec and re-briefs any Elves whose work is affected. No more Elves building to an outdated design.

A Tale of Two Lists

Let me show you the difference in concrete terms.

The Old Way (Static List):

Ticket XMAS-1234
Title: Improve gift delivery speed
Description: Make it faster. Children are waiting.
Assigned to: @delivery-elf

This ticket is, forgive my language, useless to an agent. What does “faster” mean? Faster than last year? Faster than a competitor? Faster per route, or faster in aggregate? An Elf handed this will make a reasonable guess — and the guess may be entirely wrong.

The New Way (Living Spec):

## Gift Delivery Optimisation — Living Spec

### Goal
Reduce average per-route delivery time by 20% vs Christmas 2024.

### Success Criteria
- [ ] Route calculation completes in < 200ms
- [ ] Zero missed deliveries in smoke test (1,000 houses)
- [ ] Sleigh fuel consumption unchanged or reduced

### Constraints
- Must work with existing reindeer harness API (v3.2)
- No changes to the chimney-descent module (frozen until Jan)

### Tasks
- [x] Profile current route-calculation bottleneck — Dasher Agent
- [ ] Implement A* pathfinding — Prancer Agent (in progress)
- [ ] Integration test against 2024 route dataset — Blitzen Agent
- [ ] Update delivery manifest schema — Rudolph Agent

### Decisions
- 2025-12-03: Chose A* over Dijkstra for weighted graph support
- 2025-12-05: Excluded Antarctic routes from v1 scope

This spec is machine-executable. Every Elf knows what “done” means. Every constraint is explicit. Every decision is recorded. And when Prancer finishes A*, the spec will tick that box and Blitzen will know it is time to start testing.

SIPOC: The Living Spec Process

S — Suppliers I — Inputs P — Process O — Outputs C — Customers
Who/What Developer (Head Elf Pepper), Coordinator Agent, Specialist Agents Initial goal statement, codebase context, model selection Goal → Coordinator drafts spec → Agents execute & update → Spec reflects reality Always-accurate living spec, completed task list, decision log All Specialist Agents, human reviewers, future contributors
Workshop Pepper, Father Christmas, the Elves “Deliver all gifts 20% faster” Santa writes the List → Elves build → List updates as gifts are wrapped A gift list that is always true Every Elf, every Quality Inspector, the Head of Logistics

Why This Matters for Parallel Work

Here is the key insight. When three Elves work simultaneously on different parts of the toy, each one needs to know:

  • What the other two are building.
  • Which interfaces they must respect.
  • What has already been decided.

Without a living spec, each Elf improvises. The train set’s power connector uses a different voltage than the controller the second Elf built. Integration collapses at the last minute.

With a living spec, those interfaces are declared upfront. When the first Elf changes something, the spec records it, and the second Elf’s briefing updates. The spec is the coordination mechanism.

As Augment’s manifesto puts it: once you are running multiple agents in parallel, the spec stops being process and starts being infrastructure.

Practical Notes: Writing a Good Spec Seed

Father Christmas has learned, through decades of painful experience, that the quality of the output depends entirely on the quality of the intent put in. Here are my rules for seeding a Living Spec:

  1. Name the goal, not the solution. Write “reduce onboarding drop-off by 20%”, not “add a progress bar to the onboarding screen”.
  2. State the constraints explicitly. Frozen modules, existing APIs, budget limits — write them down before any Elf starts.
  3. Define done. A measurable success criterion is worth a hundred vague requirements.
  4. Record decisions as they are made. An undocumented decision is an ambiguity waiting to cause a merge conflict.

What Is Coming Next

In Episode 3, Father Christmas will introduce the most important relationship in the Workshop: Santa the Coordinator and the Elves as Specialist Agents. We will examine who does what, how handoffs work, and why the Verifier Elf might be the most important Elf in the building.

The Master Gift List is only as good as the team that follows it. Fortunately, my team is excellent.Ho ho ho! 🎅

Part of the Santa Augmentcode Intent series. Published on dev.to under the the-software-s-journey organisation.