50  Fleet Management Agent: .deb Packaging, CLI Tool & CI/CD — Spec Template

What this document is: A reusable spec template for any system that follows the centralized cloud console + distributed local agent pattern. Replace the {PLACEHOLDER} tokens with your project’s actual names, then hand the spec to an implementer (human or AI).


50.1 Placeholder Reference

Fill in this table first. Every {TOKEN} in the document resolves from here.

Token Description Example
{PRODUCT_NAME} Human-readable product name Fleet Manager
{SOLUTION_NAME} .NET solution file name (no extension) FleetManager
{AGENT_PKG} Linux package name (lowercase, hyphens) fleet-agent
{AGENT_PROJECT} .NET project name for the agent Agent.Worker
{CLI_NAME} CLI binary name (lowercase, hyphens) fleet-ctl
{CLI_PROJECT} .NET project name for the CLI Fleet.Cli
{CONSOLE_PROJECT} .NET project name for the cloud console CloudConsole.Api
{SHARED_PROJECT} .NET project name for shared contracts Shared.Contracts
{SERVICE_USER} Linux system user the agent runs as fleet-agent
{HUB_ENDPOINT} SignalR hub path /hub/fleet
{GITHUB_ORG} GitHub organization or username your-org
{GITHUB_REPO} GitHub repository name FleetManager
{MAINTAINER_EMAIL} Package maintainer email infra@example.com

50.2 Objective

Set up the {AGENT_PROJECT} as a .deb-installable system daemon distributed via GitHub Releases, and provide a {CLI_NAME} command-line tool for admins to interact with the cloud console. A GitHub Actions workflow orchestrates test, build, package, and release on every version tag.

50.3 Target Outcome

Developer pushes tag          GitHub Actions                  Target Server
─────────────────────         ───────────────────────         ─────────────────────
git tag v1.2.0          ───►  test → build .deb        ───►  curl -LO ...deb
git push --tags               test → build CLI (matrix)       sudo dpkg -i {{AGENT_PKG}}_1.2.0_amd64.deb
                              create GitHub Release           ✓ Agent running as systemd service
                              attach all artifacts

Admin workstation (macOS / Linux)
─────────────────────────────────
{{CLI_NAME}} login --url https://console.example.com
{{CLI_NAME}} agents list
{{CLI_NAME}} agents status <agent-id>
{{CLI_NAME}} apps restart <agent-id> <app-id>
{{CLI_NAME}} audit --last 20

50.4 Part A: Architecture Context

This spec assumes a system with three components and a shared contract library:

┌────────────────────────────────────────────────────────┐
│              {{CONSOLE_PROJECT}}                        │
│              (Cloud Console — ASP.NET Core)             │
│                                                        │
│   REST API          SignalR Hub ({{HUB_ENDPOINT}})     │
│   /api/*            bidirectional real-time             │
└───────┬──────────────────────┬─────────────────────────┘
        │                      │
        │ HTTP                 │ WebSocket
        │                      │
┌───────▼──────────┐   ┌──────▼──────────────────┐
│  {{CLI_NAME}}    │   │  {{AGENT_PROJECT}}       │
│  (Admin CLI)     │   │  (Worker Service daemon) │
│  per-user tool   │   │  per-server daemon       │
└──────────────────┘   └─────────────────────────┘

Both reference:
  {{SHARED_PROJECT}} — DTOs, hub interfaces, enums, command types

50.4.1 Dependency Rule

{{CONSOLE_PROJECT}}  ──►  {{SHARED_PROJECT}}  ◄──  {{AGENT_PROJECT}}
                                  ▲
                                  │
                          {{CLI_PROJECT}}

No horizontal references between executable projects.

50.5 Part B: Repository Structure (New Files Only)

Do not modify existing source code unless explicitly stated.

{{SOLUTION_NAME}}/
├── (existing src/, docs/, {{SOLUTION_NAME}}.sln, README.md, CLAUDE.md)
│
├── src/
│   ├── (existing {{SHARED_PROJECT}}/, {{CONSOLE_PROJECT}}/, {{AGENT_PROJECT}}/)
│   │
│   └── {{CLI_PROJECT}}/                    # NEW
│       ├── {{CLI_PROJECT}}.csproj
│       ├── Program.cs
│       ├── Commands/
│       │   ├── LoginCommand.cs
│       │   ├── AgentsCommand.cs
│       │   ├── AppsCommand.cs
│       │   └── AuditCommand.cs
│       ├── Services/
│       │   ├── CloudApiClient.cs
│       │   └── CliConfigManager.cs
│       └── Models/
│           └── CliConfig.cs
│
├── packaging/                              # NEW
│   ├── build-deb.sh
│   ├── DEBIAN/
│   │   ├── control.template
│   │   ├── conffiles
│   │   ├── postinst
│   │   ├── prerm
│   │   └── postrm
│   └── etc/
│       └── {{AGENT_PKG}}/
│           └── agent.env.default
│
├── .github/
│   └── workflows/
│       └── release.yml                     # NEW
│
└── scripts/
    └── install-from-github.sh              # NEW

Update {SOLUTION_NAME}.sln to include {CLI_PROJECT}.csproj.


50.6 Part C: CLI Tool (src/{{CLI_PROJECT}}/)

50.6.1 C.1 Overview

{CLI_NAME} is a cross-platform command-line tool for admins to interact with the cloud console REST API. It follows standard CLI conventions: per-user config in ~/.config/, human-readable table output by default, JSON output with --json.

{{CLI_NAME}} (CLI, per-user)                 {{AGENT_PKG}} (daemon, system-wide)
────────────────────────────                 ──────────────────────────────────
Config: ~/.config/{{CLI_NAME}}/              Config: /etc/{{AGENT_PKG}}/
Runs as: the human admin                     Runs as: {{SERVICE_USER}} (system user)
Lifecycle: invoked, runs, exits              Lifecycle: always running (systemd)
Talks to: Cloud Console REST API             Talks to: Cloud Console SignalR hub
Installed: manual download / extract         Installed: .deb package

50.6.2 C.2 Project File

Target framework: net8.0
Output type: Exe
Assembly name: {{CLI_NAME}}
NuGet dependencies:
  - System.CommandLine (2.0.0-beta4.*)
Project reference:
  - {{SHARED_PROJECT}} (reuse DTOs, enums, command types)
Do NOT reference {{CONSOLE_PROJECT}} or {{AGENT_PROJECT}}.

50.6.3 C.3 Config Location & Format

Path: ~/.config/{{CLI_NAME}}/config.json

Follows XDG Base Directory Specification. On Linux, use $XDG_CONFIG_HOME if set, otherwise ~/.config. On macOS, use ~/.config as well (not ~/Library — CLI tools conventionally use XDG even on macOS).

{
  "consoleUrl": "https://console.example.com",
  "apiKey": "sk-...",
  "defaultOutput": "table"
}

CliConfigManager service responsibilities: - First-run: create ~/.config/{{CLI_NAME}}/ directory and default config - Read: deserialize from JSON - Write: serialize to JSON, set file permissions to 600 on Unix (protect API key) - Validate: check that consoleUrl is set before any API command; if not, print clear error and exit

50.6.4 C.4 Command Tree

{{CLI_NAME}}
│
├── login                                    Set console URL and credentials
│   Options:
│     --url <url>            Console URL (required)
│     --api-key <key>        API key (optional; prompt interactively if omitted)
│
├── agents                                   Agent fleet management
│   ├── list                                 List all registered agents
│   │   Options:
│   │     --json             Machine-readable JSON output
│   │     --online-only      Show only connected agents
│   │
│   └── status <agentId>                     Detailed status of one agent
│       Options:
│         --json             Machine-readable JSON output
│
├── apps                                     Managed application commands
│   ├── restart <agentId> <appId>            Restart a managed app
│   │
│   ├── config <agentId> <appId>             Push configuration
│   │   Options:
│   │     --set <key=value>  Repeatable; one or more key=value pairs
│   │
│   └── update <agentId> <appId>             Trigger app update
│       Options:
│         --version <ver>    Target version (required)
│         --artifact <url>   Download URL for the new package (required)
│
└── audit                                    View audit log
    Options:
      --last <n>             Number of entries (default: 20)
      --agent <agentId>      Filter by agent
      --json                 Machine-readable JSON output

50.6.5 C.5 Command Implementations

Each command class constructs a System.CommandLine.Command with arguments, options, and an async handler. All commands that call the API should check config validity first and exit with code 1 + message if not configured.

50.6.5.1 login

  1. Accept --url (required) and --api-key (optional).
  2. If --api-key omitted → prompt interactively with masked input (* per keystroke).
  3. Validate URL format.
  4. Test connectivity: GET {url}/api/agents. On success → save config. On failure → show error, do NOT save.
  5. Print: Logged in to {url}. Config saved to ~/.config/{{CLI_NAME}}/config.json

50.6.5.2 agents list

  1. GET /api/agents

  2. Table output (default):

    AGENT ID              HOSTNAME              STATUS   CPU     MEM (MB)  APPS
    agent-001             srv-alpha             online   23.4%   128       2
    agent-002             srv-beta              offline  -       -         3
  3. --json → raw JSON array from API.

  4. --online-only → client-side filter.

50.6.5.3 agents status <agentId>

  1. GET /api/agents/{agentId}

  2. On 404 → Agent '{agentId}' not found. + exit code 1.

  3. Detailed output:

    Agent:     agent-001
    Hostname:  srv-alpha
    OS:        Ubuntu 24.04 LTS
    Status:    online
    Last Seen: 2026-03-07 10:30:15 UTC
    
    Metrics:
      CPU:   23.4%
      MEM:   128.3 MB
      Disk:  52.1%
    
    Managed Apps:
      NAME              VERSION    STATUS
      app-alpha         1.2.0      Running
      app-beta          3.0.1      Running

50.6.5.4 apps restart <agentId> <appId>

  1. POST /api/agents/{agentId}/restart-app with {"appId": "<appId>"}
  2. 202Restart command sent to {appId} on {agentId}.
  3. 404Agent '{agentId}' not connected.

50.6.5.5 apps config <agentId> <appId>

  1. Parse --set key=value into dictionary. Error if none provided.
  2. POST /api/agents/{agentId}/push-config with {"appId": "...", "settings": {...}}
  3. 202Config pushed to {appId} on {agentId}: key1, key2, ...

50.6.5.6 apps update <agentId> <appId>

  1. --version and --artifact both required.
  2. POST /api/agents/{agentId}/update-app with {"appId":"...","targetVersion":"...","artifactUrl":"..."}
  3. 202Update command sent: {appId} → v{version} on {agentId}.

50.6.5.7 audit

  1. GET /api/audit

  2. Client-side: take last N, optionally filter by agent.

  3. Table output:

    TIMESTAMP                AGENT        CATEGORY       MESSAGE
    2026-03-07 10:30:15      agent-001    AppLifecycle   App app-alpha restarted
    2026-03-07 10:28:03      agent-001    Config         Config pushed to app-beta
  4. --json → raw JSON array.

50.6.6 C.6 CloudApiClient Service

Thin wrapper around HttpClient: - Base URL from CliConfigManager - Sets Authorization: Bearer {apiKey} if apiKey is non-empty - Sets Accept: application/json - One method per API endpoint (mirrors REST API surface) - Non-success status codes → descriptive error or result type for the command to format

50.6.7 C.7 Program.cs

Use System.CommandLine to build the command tree. Manual construction — do NOT use Microsoft.Extensions.DependencyInjection. Instantiate CliConfigManager and CloudApiClient in Main, pass to command builders.


50.7 Part D: .deb Packaging (packaging/)

50.7.1 D.1 DEBIAN/control.template

Package: {{AGENT_PKG}}
Version: {{VERSION}}
Section: admin
Priority: optional
Architecture: amd64
Depends: libicu74 | libicu72 | libicu70, libssl3 | libssl1.1
Maintainer: {{PRODUCT_NAME}} Team <{{MAINTAINER_EMAIL}}>
Homepage: https://github.com/{{GITHUB_ORG}}/{{GITHUB_REPO}}
Description: {{PRODUCT_NAME}} Agent
 <one-line description of what the agent does>

{VERSION} is substituted by build-deb.sh at build time.

50.7.2 D.2 DEBIAN/conffiles

/etc/{{AGENT_PKG}}/agent.env

Tells dpkg to preserve admin’s config on upgrade.

50.7.3 D.3 DEBIAN/postinst

#!/bin/bash, set -e. Must handle both fresh install and upgrade.

1. Create {{SERVICE_USER}} system user if not exists
     useradd --system --no-create-home --shell /usr/sbin/nologin {{SERVICE_USER}}
2. mkdir -p /var/lib/{{AGENT_PKG}}
3. chown -R {{SERVICE_USER}}:{{SERVICE_USER}} /opt/{{AGENT_PKG}}
4. chown -R {{SERVICE_USER}}:{{SERVICE_USER}} /var/lib/{{AGENT_PKG}}
5. chown {{SERVICE_USER}}:{{SERVICE_USER}} /etc/{{AGENT_PKG}}/agent.env
6. chmod 600 /etc/{{AGENT_PKG}}/agent.env
7. chmod +x /opt/{{AGENT_PKG}}/{{AGENT_BINARY}}
8. systemctl daemon-reload
9. systemctl enable {{AGENT_PKG}}
10. If already active → systemctl restart. Else → systemctl start.

({AGENT_BINARY} = the published executable name, e.g., Agent.Worker)

50.7.4 D.4 DEBIAN/prerm

1. systemctl stop {{AGENT_PKG}} (if active)
2. systemctl disable {{AGENT_PKG}} (ignore errors)

50.7.5 D.5 DEBIAN/postrm

1. If "$1" = "purge":
     userdel {{SERVICE_USER}}
     rm -rf /var/lib/{{AGENT_PKG}}
     rm -rf /etc/{{AGENT_PKG}}
2. Always: systemctl daemon-reload

50.7.6 D.6 etc/{{AGENT_PKG}}/agent.env.default

Template shipped in package. Copied to agent.env at build time. Example:

DOTNET_ENVIRONMENT=Production
# {{CONSOLE_PROJECT}}__HubUrl=https://console.example.com{{HUB_ENDPOINT}}
# Agent__Id=agent-CHANGEME
# Agent__HeartbeatIntervalSec=30

Adapt the environment variable names to match your project’s IConfiguration binding.


50.8 Part E: Build Script (packaging/build-deb.sh)

50.8.1 Interface

./packaging/build-deb.sh <version>
# Output: ./dist/{{AGENT_PKG}}_<version>_amd64.deb

50.8.2 Steps

1. Validate version argument matches N.N.N pattern.

2. dotnet publish src/{{AGENT_PROJECT}} -c Release \
     --self-contained true \
     -r linux-x64 \
     -p:PublishSingleFile=true \
     -p:IncludeNativeLibrariesForSelfExtract=true \
     -o ./tmp-publish

3. Create staging directory mirroring filesystem:
     ./tmp-deb/{{AGENT_PKG}}_<version>_amd64/
       ├── DEBIAN/
       ├── opt/{{AGENT_PKG}}/
       ├── etc/{{AGENT_PKG}}/
       ├── etc/systemd/system/
       └── var/lib/{{AGENT_PKG}}/

4. Copy files:
     Published binary + appsettings.json  →  opt/{{AGENT_PKG}}/
     systemd unit file                    →  etc/systemd/system/
     agent.env.default                    →  etc/{{AGENT_PKG}}/agent.env
     DEBIAN/*                             →  DEBIAN/

5. Substitute {{VERSION}} in DEBIAN/control.

6. Set permissions: scripts 755, metadata 644.

7. dpkg-deb --build --root-owner-group <staging> ./dist/

8. dpkg-deb --info on the output for verification.

9. Clean up temp directories.

Requirements: set -euo pipefail, runnable from repo root, output to ./dist/.


50.9 Part F: systemd Service File

Place at docs/{{AGENT_PKG}}.service. This is the canonical version included in the .deb.

[Unit]
Description={{PRODUCT_NAME}} Agent
After=network-online.target
Wants=network-online.target

[Service]
Type=notify
WorkingDirectory=/opt/{{AGENT_PKG}}
ExecStart=/opt/{{AGENT_PKG}}/{{AGENT_BINARY}}
User={{SERVICE_USER}}
Group={{SERVICE_USER}}
EnvironmentFile=/etc/{{AGENT_PKG}}/agent.env

Restart=always
RestartSec=10
StartLimitIntervalSec=60
StartLimitBurst=5

# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ReadWritePaths=/var/lib/{{AGENT_PKG}}
ReadOnlyPaths=/opt/{{AGENT_PKG}}

# Resource limits (adjust to your workload)
MemoryMax=512M
CPUQuota=50%

SyslogIdentifier={{AGENT_PKG}}
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

50.10 Part G: GitHub Actions Workflow (.github/workflows/release.yml)

50.10.1 Trigger

on:
  push:
    tags:
      - 'v*.*.*'

50.10.2 Job Graph

push tag v1.2.0
    │
    ▼
┌──────────┐     ┌──────────────┐
│   test   │────►│  build-deb   │────┐
│          │     │              │    │
│ restore  │     │ publish      │    │
│ build    │     │ build .deb   │    │     ┌──────────────┐
│ test     │     │ upload       │    ├────►│   release    │
│          │     └──────────────┘    │     │              │
│          │                         │     │ download all │
│          │     ┌──────────────┐    │     │ create GH    │
│          │────►│  build-cli   │────┘     │ release      │
│          │     │              │          │ attach:      │
└──────────┘     │ matrix:      │          │  .deb        │
                 │  linux-x64   │          │  cli tarballs│
                 │  macos-x64   │          └──────────────┘
                 │  macos-arm64 │
                 └──────────────┘

50.10.3 Job 1: test

Ubuntu-latest. Steps: 1. Checkout 2. Setup .NET SDK (actions/setup-dotnet@v4) 3. dotnet restore 4. dotnet build --no-restore 5. dotnet test --no-build --verbosity normal

50.10.4 Job 2: build-deb (needs: test)

Ubuntu-latest. Steps: 1. Checkout 2. Setup .NET SDK 3. Extract version: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT 4. Run: ./packaging/build-deb.sh $VERSION 5. Upload artifact: ./dist/{{AGENT_PKG}}_*.deb

50.10.5 Job 3: build-cli (needs: test)

Ubuntu-latest. Strategy matrix:

runtime os-label
linux-x64 linux-x64
osx-x64 macos-x64
osx-arm64 macos-arm64

Steps per matrix entry: 1. Checkout 2. Setup .NET SDK 3. dotnet publish src/{{CLI_PROJECT}} -c Release --self-contained -r $RUNTIME -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o ./dist/cli/$OS_LABEL 4. tar -czf {{CLI_NAME}}-$OS_LABEL.tar.gz -C ./dist/cli/$OS_LABEL . 5. Upload artifact

50.10.6 Job 4: release (needs: [build-deb, build-cli])

Ubuntu-latest. Steps: 1. Download all artifacts 2. softprops/action-gh-release@v2: - files: .deb + all CLI tarballs - generate_release_notes: true - env: GITHUB_TOKEN

Permissions: contents: write


50.11 Part H: Convenience Installer (scripts/install-from-github.sh)

50.11.1 Usage

# Latest
curl -fsSL https://raw.githubusercontent.com/{{GITHUB_ORG}}/{{GITHUB_REPO}}/main/scripts/install-from-github.sh | sudo bash

# Specific version
curl -fsSL ... | sudo bash -s -- 1.2.0

50.11.2 Logic

1. Accept optional version arg. If omitted → query GitHub API for latest release tag.
2. Download: https://github.com/{{GITHUB_ORG}}/{{GITHUB_REPO}}/releases/download/v${VERSION}/{{AGENT_PKG}}_${VERSION}_amd64.deb
3. dpkg -i /tmp/{{AGENT_PKG}}.deb
4. Print systemctl status.
5. Remind admin to edit /etc/{{AGENT_PKG}}/agent.env.

50.12 Part I: File Placement Summary

50.12.1 Server (after dpkg -i):

/opt/{{AGENT_PKG}}/                  # Binaries (read-only at runtime)
├── {{AGENT_BINARY}}                 #   Self-contained executable
└── appsettings.json                 #   Default app config

/etc/{{AGENT_PKG}}/                  # Admin config (conffile, preserved on upgrade)
└── agent.env                        #   Secrets + overrides (mode 600)

/etc/systemd/system/                 # Service definition
└── {{AGENT_PKG}}.service

/var/lib/{{AGENT_PKG}}/              # Writable data (service-owned)

systemd: {{AGENT_PKG}}.service → enabled + active
User: {{SERVICE_USER}} (system, no login, no home)

50.12.2 Admin workstation (after CLI extract):

~/bin/{{CLI_NAME}}                   # Or /usr/local/bin/

~/.config/{{CLI_NAME}}/             # Created on first login
└── config.json                      #   Console URL + API key (mode 600)

50.12.3 Comparison

                      Agent (daemon)                CLI (interactive)
                      ──────────────────            ──────────────────
Installed by          dpkg / apt                    Manual extract
Runs as               {{SERVICE_USER}} (system)     Human admin
Lifecycle             Always running (systemd)      On-demand, exits
Config path           /etc/{{AGENT_PKG}}/           ~/.config/{{CLI_NAME}}/
Data path             /var/lib/{{AGENT_PKG}}/       (stateless)
Talks to              Console (SignalR/WebSocket)   Console (REST API)
Installed on          Target servers                Admin's workstation

50.13 Part J: Acceptance Criteria

50.13.1 Agent (.deb)

  1. ./packaging/build-deb.sh 1.0.0 produces ./dist/{{AGENT_PKG}}_1.0.0_amd64.deb
  2. dpkg-deb --info shows correct metadata
  3. dpkg-deb --contents shows files at correct FHS paths
  4. Fresh install on Ubuntu 22.04/24.04 → systemctl status {{AGENT_PKG}} shows active
  5. Upgrade with new .deb → service restarts, agent.env preserved
  6. dpkg -P {{AGENT_PKG}} → service stopped, user removed, all files cleaned

50.13.2 CLI

  1. dotnet run --project src/{{CLI_PROJECT}} -- --help shows full command tree
  2. {CLI_NAME} login --url http://localhost:5000 creates config file
  3. {CLI_NAME} agents list returns formatted table (console running)
  4. {CLI_NAME} agents list --json returns valid JSON
  5. {CLI_NAME} apps restart <agentId> <appId> sends command, prints confirmation
  6. {CLI_NAME} audit --last 5 shows recent entries
  7. Any command without prior login → clear error: Not configured. Run '{{CLI_NAME}} login' first.

50.13.3 CI/CD

  1. Pushing v1.0.0 tag triggers: test → build-deb + build-cli → release
  2. GitHub Release shows: .deb + CLI tarballs (linux-x64, macos-x64, macos-arm64)
  3. install-from-github.sh installs latest release

50.14 Part K: Out of Scope (Future Work)

These items are explicitly NOT part of this spec. List them to set clear boundaries for the implementer.

  • Private APT repository (Aptly, S3, Cloudsmith)
  • ARM64 .deb builds
  • GPG signing of .deb
  • Windows CLI build (win-x64)
  • {CLI_NAME} as a dotnet tool (NuGet global tool)
  • Homebrew formula for CLI
  • Authentication middleware on the Console API
  • Interactive TUI mode (e.g., Spectre.Console live dashboard)
  • Automated rollback on failed health check
  • Console-initiated updates (SignalR push path)
  • Integration tests (install .deb in VM/container)

50.15 Appendix: How to Use This Template

50.15.1 Step 1: Fill in the Placeholder Table (Part A header)

50.15.2 Step 2: Search-and-replace all {TOKENS}

sed -i \
  -e 's/{{PRODUCT_NAME}}/Fleet Manager/g' \
  -e 's/{{SOLUTION_NAME}}/FleetManager/g' \
  -e 's/{{AGENT_PKG}}/fleet-agent/g' \
  -e 's/{{AGENT_PROJECT}}/Agent.Worker/g' \
  -e 's/{{AGENT_BINARY}}/Agent.Worker/g' \
  -e 's/{{CLI_NAME}}/fleet-ctl/g' \
  -e 's/{{CLI_PROJECT}}/Fleet.Cli/g' \
  -e 's/{{CONSOLE_PROJECT}}/CloudConsole.Api/g' \
  -e 's/{{SHARED_PROJECT}}/Shared.Contracts/g' \
  -e 's/{{SERVICE_USER}}/fleet-agent/g' \
  -e 's|{{HUB_ENDPOINT}}|/hub/fleet|g' \
  -e 's/{{GITHUB_ORG}}/your-org/g' \
  -e 's/{{GITHUB_REPO}}/FleetManager/g' \
  -e 's/{{MAINTAINER_EMAIL}}/infra@example.com/g' \
  spec.md

50.15.3 Step 3: Hand to implementer

The resulting document is a concrete, unambiguous spec ready for implementation — whether by a human developer or by Claude Code:

claude "Read the spec at ./spec.md and implement all files described in it."