
Harness Creator
- 1.6k installs
- 11k repo stars
- Updated August 4, 2026
- walkinglabs/learn-harness-engineering
harness-creator is an agent skill for build lightweight agent harnesses with agents.md, state files, verification, and handoff.
About
The harness-creator skill is designed for build lightweight agent harnesses with AGENTS.md, state files, verification, and handoff. Harness Creator Use this skill to make a repository easier for coding agents to start, stay in scope, verify work, and resume across sessions. Keep the harness small enough that agents actually follow it. Invoke when the user creates or audits AGENTS.md, feature state, verification commands, or handoff docs.
- --agent-file CLAUDE.md for Claude-oriented projects.
- --package-manager npm|pnpm|yarn|bun when detection is wrong.
- --commands "cmd one,cmd two" for custom verification.
- --force only after confirming overwrites are acceptable.
- Memory across sessions: Memory Persistence.
Harness Creator by the numbers
- 1,618 all-time installs (skills.sh)
- +64 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #352 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
harness-creator capabilities & compatibility
- Capabilities
- agent file claude.md for claude oriented proje · package manager npm|pnpm|yarn|bun when detecti · commands "cmd one,cmd two" for custom verifica · force only after confirming overwrites are acc
What harness-creator says it does
Harness Creator Use this skill to make a repository easier for coding agents to start, stay in scope, verify work, and resume across sessions.
>-
npx skills add https://github.com/walkinglabs/learn-harness-engineering --skill harness-creatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.6k |
|---|---|
| repo stars | ★ 11k |
| Security audit | 2 / 3 scanners passed |
| Last updated | August 4, 2026 |
| Repository | walkinglabs/learn-harness-engineering ↗ |
How do I build lightweight agent harnesses with agents.md, state files, verification, and handoff?
Build lightweight agent harnesses with AGENTS.md, state files, verification, and handoff.
Who is it for?
Teams making repos agent-friendly with instructions, state, and verification loops.
Skip if: Skip for model selection or chat UI design without repo harness artifacts.
When should I use this skill?
User creates or audits AGENTS.md, feature state, verification commands, or handoff docs.
What you get
Completed harness-creator workflow with documented commands, files, and expected deliverables.
- AGENTS.md or CLAUDE.md
- Feature state files
- Verification workflow definitions
By the numbers
- Models five core harness subsystems for coding agent repositories
- Licensed MIT under walkinglabs/learn-harness-engineering
Files
Harness Creator
Use this skill to make a repository easier for coding agents to start, stay in scope, verify work, and resume across sessions. Keep the harness small enough that agents actually follow it.
Not for model selection, prompt tuning in isolation, chat UI design, or general app architecture.
Core Model
Every useful coding-agent harness has five subsystems:
| Subsystem | Minimal artifact | Purpose |
|---|---|---|
| Instructions | AGENTS.md or CLAUDE.md | Startup path, working rules, definition of done |
| State | feature_list.json, progress.md | Current feature, status, evidence, next step |
| Verification | init.sh or documented commands | Tests/checks the agent must run before claiming done |
| Scope | Feature dependencies and done criteria | Prevents overreach and half-finished work |
| Lifecycle | session-handoff.md, end-of-session routine | Makes the next session restartable |
First Move
1. Inspect what already exists: instruction files, feature/state files, verification commands, docs, package manifests. 2. Ask only for missing context that cannot be inferred safely: target agent, desired file name, tolerance for structure, and whether overwriting is allowed. 3. Prefer a minimal harness first. Add memory, tool safety, multi-agent, or benchmark details only when the user's problem calls for them.
Common Tasks
Create a harness
Use the bundled script when working on a local repository:
node skills/harness-creator/scripts/create-harness.mjs --target /path/to/projectOptions:
--agent-file CLAUDE.mdfor Claude-oriented projects.--package-manager npm|pnpm|yarn|bunwhen detection is wrong.--commands "cmd one,cmd two"for custom verification.--forceonly after confirming overwrites are acceptable.
Then explain what was created and how the user should replace placeholder feature entries.
Audit an existing harness
Run:
node skills/harness-creator/scripts/validate-harness.mjs --target /path/to/projectReport the five subsystem scores, the lowest-scoring area, and the first 2-3 changes that would improve reliability. Treat the lowest score as a candidate bottleneck; confirm with failures, logs, or task outcomes before claiming causality.
Produce a report
Use when the user wants a shareable assessment:
node skills/harness-creator/scripts/render-assessment-html.mjs --target /path/to/project
node skills/harness-creator/scripts/run-benchmark.mjs --target /path/to/project --html /path/to/report.htmlBe clear that this is a structural benchmark. Real effectiveness still needs before/after agent sessions on representative tasks.
When to Read References
Load only the reference needed for the user's problem:
- Memory across sessions: Memory Persistence
- Reusable workflows as skills: Skill Runtime
- Permissions, tools, concurrency: Tool Registry & Safety
- Context budget and progressive disclosure: Context Engineering
- Delegation and parallel agents: Multi-Agent Coordination
- Hooks, startup, long-running work: Lifecycle & Bootstrap
- Non-obvious failure modes: Gotchas
Design Rules
- Keep the root instruction file short: routing and invariants, not a full manual.
- Put project facts in project docs, not in the skill.
- Make verification commands explicit and runnable.
- Require evidence before marking a feature done.
- Use one active feature unless the harness has explicit multi-agent ownership boundaries.
- Prefer append/update state files over relying on chat history.
- Never hide destructive behavior in scripts; overwrites require explicit user approval.
Deliverable Checklist
For a usable minimal harness, leave the target project with:
- [ ]
AGENTS.mdorCLAUDE.md - [ ]
feature_list.json - [ ]
progress.md - [ ]
init.sh - [ ] Optional
session-handoff.mdfor multi-session work - [ ] Documented verification evidence or next action
If you cannot create files, provide exact file contents and commands instead.
interface:
display_name: "Harness Creator"
short_description: "Scaffold and audit coding-agent harnesses"
default_prompt: "Use $harness-creator to create, validate, and benchmark a production-ready coding-agent harness for this repository."
policy:
allow_implicit_invocation: true
{
"skill_name": "harness-creator",
"evals": [
{
"id": 1,
"name": "Minimal Harness Creation",
"prompt": "I have a new TypeScript + React project with no agent setup. Create a minimal harness that makes my agent reliable for single-feature development.",
"expected_output": "AGENTS.md (~50-100 lines), feature_list.json with 3-5 placeholder features, init.sh with verification commands",
"files": [],
"expectations": [
"AGENTS.md includes startup workflow (read files, run init, check feature list)",
"AGENTS.md includes one-feature-at-a-time policy",
"feature_list.json has valid JSON with id, name, description, status fields",
"init.sh runs install, type/static check, test, and build commands when available",
"All files are in project root directory"
]
},
{
"id": 2,
"name": "Session Continuity Setup",
"prompt": "My agent forgets everything between sessions. I need persistent memory and session handoff so it can work on multi-day features.",
"expected_output": "progress.md template, session-handoff.md structure, memory directory setup instructions",
"files": [],
"expectations": [
"progress.md includes sections: current state, what's done, what's in progress, blockers, next session should",
"session-handoff.md includes: what was accomplished, what remains, blockers/decisions, files modified",
"Instructions for memory directory creation (.claude/memory/ or similar)",
"Two-step save invariant explained (topic file then index)"
]
},
{
"id": 3,
"name": "Harness Assessment",
"prompt": "I have an existing AGENTS.md file but my agent still breaks things. Assess my harness and tell me what to improve first.",
"expected_output": "Five-subsystem assessment with scores 1-5 for each, bottleneck identified, prioritized improvement plan",
"files": ["existing-AGENTS.md"],
"expectations": [
"Assessment covers all 5 subsystems: Instructions, State, Verification, Scope, Lifecycle",
"Each subsystem scored 1-5 with justification",
"Lowest-scoring subsystem identified as bottleneck or candidate bottleneck",
"Prioritized improvement plan with 2-3 concrete next steps"
]
},
{
"id": 4,
"name": "Verification Workflow Design",
"prompt": "My agent says 'done' but the tests fail. Design a verification workflow that forces the agent to actually verify before claiming completion.",
"expected_output": "Verification commands list, AGENTS.md section updates, optional quality score tracking",
"files": [],
"expectations": [
"Explicit verification commands listed (tests, lint, type-check, build)",
"AGENTS.md includes Definition of Done section with verification requirement",
"End-of-session checklist includes verification evidence recording",
"Failure path says not to claim done when verification fails"
]
},
{
"id": 5,
"name": "Memory Taxonomy Design",
"prompt": "I want my agent to remember project conventions and user preferences across sessions. Design a memory taxonomy and tell me what belongs where.",
"expected_output": "Memory layer definitions, type taxonomy for auto-memory, what to save vs what to skip",
"files": [],
"expectations": [
"Instruction memory defined (human-curated, version-controlled)",
"Auto-memory defined (agent-written, persistent)",
"Type taxonomy with 3-4 types (e.g., user/feedback/project/reference)",
"Clear guidance on what NOT to save (derivable content)"
]
},
{
"id": 6,
"name": "Tool Safety Design",
"prompt": "My custom coding agent can run shell commands and edit files. Design the tool registry and permission harness so it is powerful but does not silently do dangerous things.",
"expected_output": "Tool registry policy with per-call concurrency classification, default permissions, denial handling, and audit trail",
"files": [],
"expectations": [
"Sensitive tools default to ask or deny instead of allow",
"Concurrency safety is classified per call rather than per tool",
"Permission evaluation side effects are acknowledged and not cached unsafely",
"Audit trail records command, decision, and reason"
]
},
{
"id": 7,
"name": "Context Budget Plan",
"prompt": "My agent loads too many docs at startup and gets slow. Design a context strategy that keeps important context while controlling token cost.",
"expected_output": "Progressive disclosure plan using select/write/compress/isolate operations and explicit context budgets",
"files": [],
"expectations": [
"Defines always-loaded metadata versus on-demand references",
"Uses SELECT, WRITE, COMPRESS, and ISOLATE operations",
"Includes hard caps or budget thresholds",
"Explains invalidation for memoized context builders"
]
},
{
"id": 8,
"name": "Multi-Agent Coordination",
"prompt": "I want multiple agents to work on a large refactor without stepping on each other. Design a multi-agent harness and rules for delegation.",
"expected_output": "Coordinator/delegation design with ownership boundaries, context sharing policy, and merge/review gates",
"files": [],
"expectations": [
"Defines coordinator, worker, and optional reviewer responsibilities",
"Assigns disjoint file or module ownership",
"Prevents recursive fork children from forking again",
"Includes integration and verification gate before claiming done"
]
},
{
"id": 9,
"name": "Lifecycle Bootstrap",
"prompt": "Every new session starts inconsistently. Design startup, hook, and handoff lifecycle rules so a new agent can resume safely.",
"expected_output": "Lifecycle bootstrap design with init.sh, clean-state checks, handoff reads, and hook trust boundaries",
"files": [],
"expectations": [
"init.sh is the standard startup and verification entrypoint",
"Startup reads AGENTS.md, feature_list.json, progress.md, and handoff when present",
"End-of-session procedure records evidence, blockers, and next step",
"Hooks are gated by trust and failure behavior is explicit"
]
},
{
"id": 10,
"name": "Scripted Harness Validation",
"prompt": "Use the harness-creator scripts to scaffold a harness, validate it, and produce an HTML assessment report for a repository.",
"expected_output": "Commands using create-harness.mjs, validate-harness.mjs, and render-assessment-html.mjs plus interpretation of score",
"files": [],
"expectations": [
"Uses create-harness.mjs with a target directory",
"Uses validate-harness.mjs and explains five-subsystem score output",
"Uses render-assessment-html.mjs or run-benchmark.mjs with --html",
"Explains that structural benchmark complements but does not replace real before/after agent sessions"
]
}
]
}
{
"name": "harness-creator",
"version": "1.0.0",
"description": "Harness engineering for AI coding agents — five subsystems, memory persistence, session continuity, verification workflows, scope control, lifecycle management.",
"license": "MIT",
"author": "Learn Harness Engineering",
"repository": "https://github.com/walkinglabs/learn-harness-engineering",
"languages": ["en", "zh"],
"compatibility": {
"minCliVersion": "0.1.0",
"agents": [
"claude-code",
"codex-cli",
"cursor",
"windsurf",
"generic"
]
},
"entryPoint": "SKILL.md",
"references": [
"references/memory-persistence-pattern.md",
"references/context-engineering-pattern.md",
"references/skill-runtime-pattern.md",
"references/tool-registry-pattern.md",
"references/multi-agent-pattern.md",
"references/lifecycle-bootstrap-pattern.md",
"references/gotchas.md"
],
"triggers": [
"harness engineering",
"agent reliability",
"session continuity",
"memory persistence",
"verification workflow",
"scope control",
"lifecycle management",
"AGENTS.md",
"CLAUDE.md",
"feature tracking",
"session handoff",
"multi-agent coordination",
"context engineering",
"tool safety",
"permission pipeline"
],
"bundled": {
"scripts": [
"scripts/create-harness.mjs",
"scripts/validate-harness.mjs",
"scripts/render-assessment-html.mjs",
"scripts/run-benchmark.mjs",
"scripts/lib/harness-utils.mjs"
],
"assets": [],
"templates": [
"templates/agents.md",
"templates/feature-list.json",
"templates/feature-list.schema.json",
"templates/init.sh",
"templates/progress.md",
"templates/session-handoff.md"
]
}
}
harness-creator
A compact skill for building and auditing harnesses around AI coding agents.
It helps a repository provide five things agents need: instructions, state, verification, scope boundaries, and lifecycle handoff.
Install
npx skills add walkinglabs/learn-harness-engineering --skill harness-creatorOr copy skills/harness-creator/ into your skill path.
Use
node skills/harness-creator/scripts/create-harness.mjs --target /path/to/project
node skills/harness-creator/scripts/validate-harness.mjs --target /path/to/project
node skills/harness-creator/scripts/run-benchmark.mjs --target /path/to/project --html /path/to/report.htmlThe scripts use only Node.js built-in modules. They can be run after copying the skill directory into another repository.
What It Creates
AGENTS.mdorCLAUDE.mdfeature_list.jsonprogress.mdinit.shsession-handoff.md
create-harness.mjs detects common project types and package managers. It supports Node/npm/pnpm/yarn/bun, Python, Go, Rust, Maven, Gradle, and .NET at a basic verification-command level.
What It Checks
validate-harness.mjs scores the five harness subsystems:
1. Instructions 2. State 3. Verification 4. Scope 5. Lifecycle
The score is structural. It tells you whether the harness is present and coherent; it does not replace real before/after agent-session testing.
Status
- [x] Minimal harness scaffolding
- [x] Five-subsystem validation
- [x] HTML assessment report
- [x] Structural benchmark report
- [x] 10 eval cases
- [x] Generic verification detection for common stacks
- [ ] Optional real before/after agent-session replay
Files
harness-creator/
├── SKILL.md
├── metadata.json
├── agents/openai.yaml
├── scripts/
│ ├── create-harness.mjs
│ ├── validate-harness.mjs
│ ├── render-assessment-html.mjs
│ ├── run-benchmark.mjs
│ └── lib/harness-utils.mjs
├── templates/
│ ├── agents.md
│ ├── feature-list.json
│ ├── feature-list.schema.json
│ ├── init.sh
│ ├── progress.md
│ └── session-handoff.md
├── references/
└── evals/evals.jsonBoundaries
This skill is for harness engineering, not model selection, prompt tuning alone, or app architecture. Keep project-specific facts in the target repository.
Context Engineering Pattern
Problem
Agents fail when context is managed poorly:
- Too much context → Session startup is slow, token costs explode, model gets lost in details
- Too little context → Agent makes wrong assumptions, reinvents wheels, violates conventions
- Wrong context → Agent focuses on low-level details, misses architectural constraints
Context is not a dump. It's a budget that must be managed with explicit operations.
Golden Rules
Four Context Operations
Every token in the window should earn its place through one of four operations:
1. SELECT — Load context just-in-time, not all-at-once 2. WRITE — Agent writes back to persistent storage (memory, state, rules) 3. COMPRESS — Reactive compaction of older turns mid-session 4. ISOLATE — Delegated work must not pollute parent context
Progressive Disclosure
Three-tier loading:
Tier 1: Metadata (always present, cheap)
→ Feature list, memory index, session status
Tier 2: Instructions (loaded on activation)
→ AGENTS.md, skill bodies, style guides
Tier 3: Resources (loaded on demand)
→ Architecture docs, API references, examplesMemoize Expensive Builders, Invalidate Explicitly
Context builders (e.g., "load all recent git commits") should be memoized to avoid redundant work, but must be invalidated at known mutation points — not reactively. Every mutation point must clear its corresponding cache.
When To Use
- Agent performance degrades in long sessions
- Startup is slow due to eager context loading
- Delegated work pollutes the parent context
- Token costs are unpredictable
Tradeoffs
| Decision | Benefit | Cost |
|---|---|---|
| JIT loading | Fast startup, low idle cost | Agent can't reason about skills until activated |
| Hard caps per block | Predictable token budget | May truncate useful context |
| Manual cache invalidation | No reactive staleness | Developer must add invalidation at each mutation |
| Isolation for delegation | Clean parent context | Child can't see parent's accumulated context |
Implementation Patterns
Select Pattern
## Startup Context (Loaded Immediately)
- Repository root path
- Tech stack (one line)
- Active feature ID from feature_list.json
## On-Demand Context (Loaded When Triggered)
- Skill: Read when skill activates
- Architecture docs: Read when implementing new feature
- API reference: Read when calling external servicesKey moves:
- Audit current context cost per turn
- Apply hard caps to every variable-length block
- Add truncation recovery pointers ("call list_files for full output")
Compress Pattern
Long sessions exhaust the window. Reactive compaction:
1. Trigger: Context usage exceeds threshold (e.g., 80%) 2. Summarize: Older turns (first 50% by token count) 3. Preserve: Recent context (last 20% of turns) 4. Label: Mark snapshot as "compacted at turn N"
## Session Summary (Turns 1-15, compacted)
**Goal**: Implement Q&A feature with citations
**Decisions made**:
- Use streaming response for UX
- Citation format: [doc:chunk] inline references
**Key files created**:
- src/services/QaService.ts
- src/shared/types.ts (extended with QaResult)Isolate Pattern
Delegated work must not pollute parent context:
| Pattern | Context Sharing | Best For |
|---|---|---|
| Coordinator (zero inheritance) | None — workers start fresh | Complex multi-phase tasks |
| Fork (full inheritance) | Full — single-level only | Quick parallel splits |
| Swarm (peer-to-peer) | Shared task list | Long-running independent work |
Key constraint: Fork is single-level only — recursive forks multiply context cost exponentially.
Gotchas
1. Most async work skips "pending" state — work units register directly as "running" 2. Context builders are memoized but manually invalidated — add invalidation or face staleness 3. Truncation is silent until it fires — hard caps enforced at read time 4. Isolation boundary must be enforced at call time — don't just remove tools from prompt
Related Patterns
- Memory Persistence — How memory layers interact with context
- Multi-agent Coordination — Context sharing across agents
Template: Context Budget
## Context Budget (Session)
| Category | Budget | Current | Status |
|----------|--------|---------|--------|
| System prompt | 2,000 | 1,850 | ✓ |
| Instruction files | 3,000 | 2,400 | ✓ |
| Memory index | 1,000 | 600 | ✓ |
| Session history | 10,000 | 4,200 | ✓ |
| Working context | 15,000 | 3,100 | ✓ |
| **Total** | **31,000** | **12,150** | 39% used |
**Compaction trigger**: 80% (24,800 tokens)
**Next action**: Trigger compaction at 24,800 tokensEvidence
Context engineering patterns are observed in production agent runtimes where:
- Context budgets are explicit, not implicit
- Progressive disclosure reduces startup latency by 60-80%
- Manual cache invalidation prevents subtle staleness bugs
- Isolation patterns enable reliable multi-agent coordination
Gotchas — Harness Engineering Failure Modes
Non-obvious principles that will cause bugs if you violate them.
---
1. Memory Index Caps Fire Silently
Symptom: Recent memories "disappear" without error.
Cause: Index has hard caps (e.g., 200 lines / 25KB) enforced at read time. Long entries (multi-sentence summaries) hit byte cap while staying under line cap.
Fix: Keep index entries to one-line hooks. Put detail in topic files.
✓ Good: "Use bun, not npm - user preference 2024-01-15"
✗ Bad: "The user prefers bun over npm because it's faster. This was discussed on 2024-01-15 when the user said 'use bun not npm' and I updated the package.json accordingly..."---
2. Priority Ordering is Counterintuitive
Symptom: Global rule silently overridden by local file.
Cause: Local overrides beat project rules, which beat user rules, which beat org rules. If you inject at user level expecting it to dominate, a local override file in project root wins.
Fix: Test with full instruction-file stack present:
# Test priority ordering
cat ~/.claude/CLAUDE.md # User level
cat ./CLAUDE.md # Project level
cat ./CLAUDE.local.md # Local override (WINS)---
3. Extraction Timing Creates Race Window
Symptom: Background extractor writes memory, but user starts next turn before extraction completes.
Cause: Extraction fires at end of response. User can send message before extraction finishes.
Fix: Coalesce concurrent extraction requests. Advance cursor only after successful run. Failed extraction means those messages reconsidered next time.
---
4. Derivable Content Doesn't Belong in Memory
Symptom: Memory index fills with architecture details that stale quickly.
Cause: Agent saves what's derivable from codebase (architecture, code patterns, version history).
Fix: Exclude derivable content by design. Type taxonomy should forbid saving what's in the repo already.
---
5. Concurrent Classification is Per-Call, Not Per-Tool
Symptom: Tool marked "concurrent-safe" causes race conditions.
Cause: Same tool can be safe for some inputs and unsafe for others. Don't assume tool's concurrency behavior is static.
Fix: Classify each call at runtime:
// Don't do this:
toolRegistry.register('shell', { concurrentSafe: false });
// Do this:
function isCallConcurrentSafe(call: ToolCall): boolean {
if (call.args.command.startsWith('rm -rf')) return false;
if (call.args.command.startsWith('cat')) return true;
// ...runtime classification
}---
6. Permission Evaluation Has Side Effects
Symptom: Permission check changes behavior on subsequent calls.
Cause: Permission evaluator tracks denials, transforms modes, updates state as side effect. Not a pure lookup function.
Fix: Don't cache permission results across calls. Re-evaluate each call fresh.
---
7. Most Async Work Skips "Pending" State
Symptom: UI shows "pending" but work unit never enters that state.
Cause: Work units register directly as "running" in practice. "Pending" exists in state machine but rarely used.
Fix: Don't build UI that assumes every work unit starts pending.
---
8. Fork Children Must Not Fork
Symptom: Context cost explodes exponentially.
Cause: Recursive forks multiply context: parent + child1 + child2 + grandchildren...
Fix: Enforce single-level invariant. Keep fork tool in child's pool (for prompt cache sharing) but block at call time.
---
9. Context Builders are Memoized but Manually Invalidated
Symptom: Model sees stale data for entire session.
Cause: Context builder cached at startup, but mutation doesn't clear cache.
Fix: Every mutation point must explicitly clear its corresponding cache:
// Example: Cache invalidation at mutation point
async function editFile(path: string, content: string) {
await writeFile(path, content);
context.cache.invalidate(`file:${path}`); // MUST invalidate
}---
10. Hook Trust is All-or-Nothing
Symptom: Entire extension system disabled because one hook untrusted.
Cause: If workspace untrusted, all hooks skip — not just suspicious ones.
Fix: Design hooks with trust gate at dispatch point. Don't attempt per-hook trust evaluation.
---
11. Eviction Requires Notification
Symptom: Parent can never read work unit result.
Cause: Work unit evicted before parent notified of completion. Race condition: parent tries to read result that's already GC'd.
Fix: Two-phase eviction: 1. Clean disk output at terminal state (eager) 2. Clean in-memory record after parent notified (lazy)
---
12. Skill Listing Budgets Are Tight
Symptom: Skill description truncated, can't trigger properly.
Cause: Skill descriptions concatenated and capped per entry (~150 chars). Front-loaded trigger language gets priority.
Fix: Front-load distinctive trigger language:
✓ Good: "harness-patterns: Memory, permissions, context engineering, multi-agent"
✗ Bad: "A comprehensive skill for understanding and implementing various patterns related to AI agent harnesses and runtime systems..."---
13. Default Tool Permission is "Allow"
Symptom: Tool bypasses expected gate.
Cause: Tools without custom permission logic delegate entirely to rule-based system. Default is "allow" unless configured otherwise.
Fix: Override default for sensitive tools:
registry.register('shell', {
defaultPermission: 'ask', // NOT 'allow'
// ...
});---
14. Team Memory Requires Auto-Memory Enabled
Symptom: Team-shared memory doesn't work even when configured.
Cause: Team memory builds on same directory/index infrastructure as auto-memory. Disabling auto-memory (via env var or settings) also disables team memory.
Fix: Ensure auto-memory enabled before enabling team memory. Check both feature gate and enablement check.
---
15. Orphaned Topic Files Accumulate
Symptom: Disk space fills with .claude/memory/topics/ files.
Cause: Two-step save (topic file then index). Crash between steps leaves orphaned topic file.
Fix: Periodic sweep deletes topic files not referenced by index. Orphans don't corrupt index but consume disk space.
---
Related Reading
- Memory Persistence Pattern — Gotchas #1, #3, #4, #15
- Tool Registry Pattern — Gotchas #5, #6, #13
- Multi-agent Pattern — Gotchas #8, #11
- Context Engineering Pattern — Gotchas #9
- Lifecycle Pattern — Gotchas #10, #14
Lifecycle and Bootstrap Pattern
Problem
Agent runtimes need extensibility without compromising safety:
- Hooks — Extend behavior at lifecycle moments (pre/post tool execution, session start/end)
- Background tasks — Track long-running work without blocking the main agent
- Bootstrap — Structure initialization across multiple entry modes (CLI, server, SDK)
But uncontrolled extensibility creates:
- Security holes from untrusted hooks
- Resource leaks from tasks that never complete
- Race conditions in initialization
Golden Rules
Hook Trust is All-or-Nothing
If the workspace is untrusted, all hooks skip — not just suspicious ones. Session-scoped hooks are ephemeral and cleaned on session end.
// Example: Hook dispatch with trust gate
async function dispatchHook(
hookType: HookType,
context: HookContext
): Promise<HookResult[]> {
// Trust gate: if workspace untrusted, skip ALL hooks
if (!context.trustBoundary.crossed) {
logger.warn('Untrusted workspace, skipping hooks');
return [];
}
// Session-scoped hooks ephemeral — cleanup on session end
const sessionHooks = context.hooks.getByScope('session');
const projectHooks = context.hooks.getByScope('project');
return await Promise.all([
...sessionHooks.map(h => h.execute(context)),
...projectHooks.map(h => h.execute(context)),
]);
}Long-Running Work: Typed State Machines with Two-Phase Eviction
Each work unit gets: 1. Typed, prefixed ID (e.g., extractor-001, benchmark-002) 2. Strict lifecycle (running → completed | failed | killed) 3. Disk-backed output (not just in-memory)
Eviction is two-phase: 1. Disk output cleaned eagerly at terminal state 2. In-memory records cleaned lazily after parent notified
Bootstrap: Dependency-Ordered, Memoized Stages
Multiple entry modes (CLI, server, SDK) share the same bootstrap path:
Stage 1: Create minimal context (no trust required)
↓
Stage 2: Load tools (read-only safe)
↓
Stage 3: Trust boundary crossed (user grants consent)
↓
Stage 4: Load security-sensitive subsystems (telemetry, secret env vars)Critical inflection: Security-sensitive subsystems must not activate before trust is established.
When To Use
- You need to extend agent behavior without modifying core code
- You need to track long-running background work
- You need structured initialization across multiple entry modes
- You need hooks at lifecycle moments (pre/post tool, session start/end)
Tradeoffs
| Decision | Benefit | Cost |
|---|---|---|
| All-or-nothing hook trust | Simple security boundary | One untrusted hook disables entire extension system |
| Disk-backed task output | Memory constant regardless of concurrent work | I/O latency proportional to work units |
| Dependency-ordered bootstrap | Multiple entry modes share path | Initial startup sequential (can't parallelize stages) |
| Memoized stages | Re-init is fast | Must carefully invalidate memoization on config change |
Implementation Patterns
Hook Lifecycle
Six hook types dispatched at defined moments:
interface HookRegistry {
// Session lifecycle
onSessionStart: (context: SessionContext) => Promise<void>;
onSessionEnd: (context: SessionContext) => Promise<void>;
// Tool execution
preToolExecute: (context: ToolContext) => Promise<ToolContext>;
postToolExecute: (context: ToolResult) => Promise<ToolResult>;
// Prompt submission
prePromptSubmit: (context: PromptContext) => Promise<PromptContext>;
postPromptSubmit: (context: ResponseContext) => Promise<ResponseContext>;
}
// Usage: Register hooks via config
// /update-config hooks.preToolExecute = "scripts/audit-tool-call.js"Long-Running Task Tracking
interface TaskRegistry {
// Typed prefixed IDs
registerWork(
type: 'extraction' | 'benchmark' | 'indexing',
outputType: 'json' | 'text' | 'file'
): string; // Returns typed ID: `extraction-001`
// Strict state machine
updateState(
taskId: string,
state: 'running' | 'completed' | 'failed' | 'killed',
output?: any
): void;
// Two-phase eviction
evictTask(taskId: string): void;
// 1. Clean disk output (eager, at terminal state)
// 2. Clean in-memory record (lazy, after parent notified)
}Bootstrap Sequence
// Example: Dependency-ordered initialization
class AgentBootstrap {
private stages = new Map<string, Stage>();
private memoizedCallers = new Map<string, any>();
async bootstrap(entryMode: 'cli' | 'server' | 'sdk'): Promise<AgentContext> {
// Stage 1: Minimal context (no trust required)
await this.runStage('minimal-context', async () => {
return {
cwd: process.cwd(),
entryMode,
trustBoundary: { crossed: false },
};
});
// Stage 2: Load tools (read-only safe)
await this.runStage('load-tools', async (context) => {
context.tools = await this.loadSafeTools();
return context;
});
// Stage 3: Trust boundary (user grants consent)
await this.runStage('trust-boundary', async (context) => {
const consent = await this.requestConsent();
context.trustBoundary = { crossed: consent };
return context;
});
// Stage 4: Security-sensitive subsystems (requires trust)
if (context.trustBoundary.crossed) {
await this.runStage('load-sensitive', async (context) => {
context.telemetry = await this.loadTelemetry();
context.secretEnvVars = await this.loadSecrets();
return context;
});
}
return context;
}
private async runStage(
name: string,
fn: (context: AgentContext) => Promise<AgentContext>
): Promise<void> {
// Memoized: skip if already run
if (this.stages.has(name) && this.stages.get(name).complete) {
return;
}
// Run stage
const stage = { name, complete: false, running: true };
this.stages.set(name, stage);
try {
await fn(this.context);
stage.complete = true;
} finally {
stage.running = false;
}
}
}Gotchas
1. Hook trust is all-or-nothing — One untrusted hook disables entire extension system 2. Most async work skips "pending" state — Work units register directly as "running" 3. Eviction requires notification — Terminal work unit only GC-eligible after parent notified 4. Fast-path dispatch — Memoized callers must handle concurrent calls without re-running stages 5. Hook types must be disjoint — Don't create overlapping hook scopes
Related Patterns
- Tool Registry — How tools are registered at bootstrap
- Memory Persistence — How memory is loaded at init
Template: Bootstrap Checklist
Before declaring bootstrap complete:
## Bootstrap Verification
### Stage 1: Minimal Context
- [ ] Working directory confirmed
- [ ] Entry mode determined (cli / server / sdk)
- [ ] Trust boundary NOT crossed (no secrets loaded)
### Stage 2: Tools Loaded
- [ ] Read-only tools registered (read, search, glob)
- [ ] Write tools NOT yet registered (edit, shell)
- [ ] Tool permissions set to default (ask / deny)
### Stage 3: Trust Boundary
- [ ] User consent requested (interactive or config flag)
- [ ] Consent recorded in session state
- [ ] Security audit logged
### Stage 4: Sensitive Subsystems
- [ ] Telemetry initialized (if consent given)
- [ ] Secret env vars loaded (if consent given)
- [ ] Write tools registered (edit, shell, exec)
- [ ] Hook system enabled (if workspace trusted)
### Stage 5: Background Tasks
- [ ] Task registry initialized
- [ ] Cleanup handlers registered
- [ ] Drain-on-shutdown configured
## If Any Stage Fails
- Bootstrap halts immediately
- Session remains in safe mode (read-only)
- Error logged with stage name and failure reasonEvidence
Lifecycle and bootstrap patterns are observed in production runtimes where:
- Hook dispatch is all-or-nothing based on workspace trust
- Long-running tasks use typed prefixed IDs and disk-backed output
- Bootstrap is dependency-ordered with memoized stages
- Trust boundary is explicit inflection point for security-sensitive subsystems
Memory and Persistence Pattern
Problem
Without persistent memory, an agent loses all user preferences, project context, and behavioral feedback the moment a session ends. Users must repeat corrections every session ("use bun, not npm"), and the agent cannot accumulate the working knowledge that makes it genuinely useful over time.
Golden Rules
Separate layers by scope and durability
- Instruction memory (human-curated, version-controlled): AGENTS.md, CLAUDE.md, project conventions
- Auto-memory (agent-written, persistent): Progress logs, session handoffs, discovered patterns
- Session extraction (background-derived): Automatic transcript analysis at session end
Two-step save invariant
Every memory write is a two-step operation: 1. Write the full content to a dedicated topic file 2. Append a one-line pointer to the index
If the process crashes between steps, the worst outcome is an orphaned topic file — the index remains consistent.
Local overrides win — always
When the same topic is addressed at multiple scopes, the most-local instruction takes priority:
Organization-wide → User-level → Project-level → Local override
↓ ↓ ↓ ↓
sets floor narrows it narrows further final sayThe index is bounded always-on context; topic files are on-demand detail
- Index: Hard-capped at ~200 lines / 25KB, one line per entry
- Topic files: Unlimited detail, loaded on demand
When To Use
- Your agent persists across sessions and must recall user preferences or project context
- Multiple scopes of instruction coexist and need clear priority ordering
- The agent should learn from sessions without manual curation
- You need background extraction that doesn't block the user
Tradeoffs
| Decision | Benefit | Cost |
|---|---|---|
| Layered memory | Each scope can be shared, audited, overridden independently | More files to discover at startup |
| Local-wins priority | Users can override without touching shared files | Global rule can be silently overridden |
| Bounded index with on-demand topics | Constant context cost regardless of memory volume | Agent must perform extra retrieval step |
| Background extraction | No latency added to user responses | Race window between extraction and next turn |
Implementation Patterns
1. Define memory directory idempotently at startup (e.g., .claude/memory/) 2. Create index file with hard caps enforced at read time 3. Implement two-step save: topic file first, then index update 4. Fire background extraction only after final response with no pending tool calls 5. Enforce mutual exclusion: if main agent wrote to memory, skip extraction that turn 6. Build review mechanism for cross-layer promotion proposals
Gotchas
1. Index truncation is silent until it fires — keep entries short 2. Priority ordering is counterintuitive — local beats project beats user beats org 3. Extraction timing creates a race window — user can start next turn before extraction completes 4. Derivable content doesn't belong in memory — architecture and code patterns are re-derivable from the codebase 5. Orphaned topic files accumulate — periodic cleanup recommended
Related Patterns
- Context Engineering — How to manage context budget across layers
- Lifecycle & Bootstrap — How initialization loads memory
Template: Progress Log Structure
# Session Progress Log
## Current State (Last Updated: YYYY-MM-DD HH:MM)
**Active Feature:** feat-003 - Q&A with Citations
**Status:** In Progress (60% complete)
### What's Done
- [x] Document chunking pipeline
- [x] Index data structure
- [ ] Q&A handler (in progress)
### What's In Progress
- Implementing Q&A IPC handler
- Need to decide: streaming vs batch response
### Blockers
- Waiting on decision: citation format (footnotes vs inline)
### Next Session Should
1. Complete Q&A handler
2. Add citation formatting
3. Test end-to-end flowEvidence
This pattern is grounded in production agent runtimes including Claude Code's memory system, which implements:
- Four-level instruction hierarchy (org/user/project/local)
- Four-type auto-memory taxonomy (user/feedback/project/reference)
- Background session extraction with mutual exclusion
- Team-shared memory as an extension layer
Multi-Agent Coordination Pattern
Problem
Single agents hit limits:
- Context limits — Can't hold full research + implementation in one session
- Specialization — Need separate researchers, implementers, reviewers
- Parallelism — Want to explore multiple approaches simultaneously
But multi-agent systems introduce chaos:
- Workers duplicate each other's research
- Coordinators delegate understanding instead of synthesizing
- Context inheritance explodes exponentially
Golden Rules
The Coordinator Must Synthesize, Not Delegate Understanding
Anti-pattern:
"Based on your findings, fix the authentication system."
Pattern:
"Research identified 3 auth flows: login, logout, token refresh. Implement ONLY the token refresh handler using the JWT strategy documented in [research output]. Return: implementation diff + test results."
The coordinator (orchestrator) adds value by digesting worker results into precise specs before dispatching implementation.
Three Delegation Patterns
| Pattern | Context Sharing | Best For | Constraints |
|---|---|---|---|
| Coordinator | None — workers start fresh | Complex multi-phase tasks (research → synthesize → implement → verify) | Slowest but safest |
| Fork | Full — child inherits parent history | Quick parallel splits sharing loaded context | Single-level only — recursive forks multiply context cost |
| Swarm | Peer-to-peer via shared task list | Long-running independent workstreams | Flat roster — teammates can't spawn other teammates |
Results Arrive Asynchronously; Fire-and-Forget Registration Returns ID Immediately
// Example: Spawn worker, get ID back immediately
const taskId = await coordinator.spawn({
type: 'research',
prompt: 'Analyze auth flows...',
toolFilter: ['read', 'search'], // Restrict tools
});
// Parent can continue working while worker runs
// Results arrive via callback or pollingWhen To Use
- Task too large for single agent session
- Need parallel exploration (e.g., prototype multiple approaches)
- Want persistent specialized teammates (researcher, implementer, reviewer)
- Complex multi-phase workflows
Tradeoffs
| Pattern | Speed | Safety | Context Cost |
|---|---|---|---|
| Coordinator | Slowest | Safest | Lowest (zero inheritance) |
| Fork | Fastest | Medium | Highest (full inheritance) |
| Swarm | Medium | Medium | Medium (shared state only) |
Implementation Patterns
Coordinator Pattern (Recommended for Complex Tasks)
Phased workflow:
Phase 1: Research
↓ (synthesize findings)
Phase 2: Plan
↓ (precise specs)
Phase 3: Implement
↓ (verify)
Phase 4: Review// Example: Coordinator workflow
const research = await coordinator.spawn({
role: 'researcher',
prompt: `Analyze existing authentication in ${authDir}.
Find: login flow, logout flow, token handling.
Return: structured findings only. NO implementation suggestions.`,
toolFilter: ['read', 'search', 'glob'], // Can't write
});
await coordinator.synthesize(research.results);
const implement = await coordinator.spawn({
role: 'implementer',
prompt: `Implement token refresh handler using the JWT strategy
from [Phase 2 findings].
Constraints: Use existing AuthService patterns, add tests.`,
toolFilter: ['read', 'search', 'edit', 'test'], // Can write
});Fork Pattern (Single-Level Only)
// Parent spawns children for parallel work
const forks = await Promise.all([
coordinator.fork({
prompt: 'Implement login handler',
inheritContext: true, // Full parent history
}),
coordinator.fork({
prompt: 'Implement logout handler',
inheritContext: true,
}),
]);
// CRITICAL: Children must not fork recursively
// If allowed, context cost multiplies: parent + child1 + child2 + ...Swarm Pattern (Flat Roster)
// Swarm: persistent team with shared task list
const swarm = new Swarm([
{ id: 'researcher', specialty: 'research' },
{ id: 'implementer', specialty: 'implementation' },
{ id: 'reviewer', specialty: 'verification' },
]);
// Agents pick tasks from shared queue
// Results posted back to shared state
await swarm.dispatch({
taskId: 'feat-001',
pickedBy: 'implementer',
});Gotchas
1. Fork children must not fork — Recursive guard preserves single-level invariant. Keep fork tool in child's pool (for prompt cache sharing) but block at call time. 2. Coordinator workers start with zero context — Only explicit prompt is passed. Don't assume child sees parent's accumulated research. 3. Swarm teammates cannot spawn other teammates — Roster is flat to prevent uncontrolled growth. 4. Write self-contained prompts — "Based on your findings" is an anti-pattern. Coordinator must digest first. 5. Filter each worker's tool set — Researcher doesn't need write; implementer doesn't need broad search.
Related Patterns
- Context Engineering — Isolation patterns for delegation
- Lifecycle & Bootstrap — How agents are spawned at init
Template: Worker Prompt Structure
# Self-Contained Worker Prompt
## Context (Copied from Coordinator Synthesis)
**Task**: Implement token refresh handler
**Background**: Research identified JWT-based auth with 24h access tokens.
**Decision**: Use refresh token rotation (new refresh token on each refresh).
## Your Role
You are an **implementer**. Your job is to write production code following the specs above.
## Constraints
- Use existing patterns from `${authServicePath}`
- Add tests for success and failure cases
- Do NOT modify login/logout handlers (separate task)
## Your Tools
- read, search, edit, test
- Shell: npm test, npm run check only
## Deliverable
Return:
1. Implementation diff (files changed)
2. Test results (pass/fail)
3. Any blockers or clarifications needed
**Do NOT return**: Research findings, architectural debates, alternative designs.Evidence
Multi-agent coordination patterns are observed in production systems where:
- Coordinator workers start with zero context inheritance
- Fork is restricted to single-level to control context explosion
- Swarm agents communicate through shared task lists, not direct prompts
- Results arrive asynchronously with fire-and-forget registration
Skill Runtime Pattern
Use this pattern when you want to package reusable agent behavior as a skill instead of repeating long instructions in every repository.
What Belongs in a Skill
- Reusable workflows that apply across projects.
- Domain-specific decision procedures.
- Templates, checklists, and reference material the agent should load on demand.
- Small helper scripts when they are stable and safe to run.
What Does Not Belong in a Skill
- Project-specific architecture facts that should live in the target repository.
- Secrets, tokens, private URLs, or user-specific credentials.
- Large manuals that the agent must always read before acting.
- Commands with destructive side effects unless they are clearly documented and require explicit user approval.
Runtime Shape
A production skill should use progressive disclosure:
1. SKILL.md frontmatter explains when the skill should trigger. 2. The body gives the shortest reliable workflow. 3. references/ contains deeper material loaded only when relevant. 4. templates/ contains copyable artifacts. 5. evals/ captures representative quality checks.
Design Rules
- Keep the entry file concise enough to scan quickly.
- Prefer concrete checklists over abstract advice.
- Link every referenced bundled file and verify it exists.
- Make installation instructions explicit about the repository, skill name, and target agent.
- Treat scripts as optional helpers, not hidden behavior.
Validation Checklist
- [ ]
SKILL.mdexists and has valid frontmatter. - [ ] Every referenced file exists inside the skill directory.
- [ ] Templates are safe to copy into a target repository.
- [ ] Installation command has been tested with
skills add --listor equivalent. - [ ] The skill does not depend on private local paths.
Tool Registry and Safety Pattern
Problem
Agents need tools (shell, file edit, search, etc.) to be productive. But unbounded tool access creates risks:
- Destructive operations (rm -rf, DROP TABLE, etc.)
- Race conditions from concurrent tool calls
- Silent policy violations from misconfigured permissions
The solution is a fail-closed registry with explicit concurrency classification and a multi-source permission pipeline.
Golden Rules
Default to Fail-Closed
Tools are non-concurrent and non-read-only unless explicitly marked safe. This prevents:
- Accidental parallel execution of state-mutating operations
- Silent data corruption from concurrent writes
Concurrency is Per-Call, Not Per-Tool
The same tool can be safe for some inputs and unsafe for others:
✓ Safe (can run in parallel):
- cat file1.txt
- grep "pattern" src/
- ls -la
✗ Unsafe (must run serially):
- rm -rf build/
- npm install (network, filesystem mutation)
- sed -i 's/old/new/g' *.tsThe runtime partitions a batch of tool calls into consecutive groups: safe calls run in parallel; any unsafe call starts a serial segment.
Permission Pipeline has Side Effects
The permission evaluator is stateful — it:
- Tracks denials (for audit and rate limiting)
- Transforms modes (e.g., auto → ask after denial)
- Updates session state as a side effect
Strict priority order:
Policy (org-wide) → User settings → Project rules → Local overrides → Session grantsWhen To Use
- Your agent runtime needs tool registration
- You need concurrency control for parallel tool calls
- You need permission gating (auto-approve, ask-first, deny)
- You need to track tool usage for audit
Tradeoffs
| Decision | Benefit | Cost |
|---|---|---|
| Fail-closed defaults | New tools are safe out of the box | Developers must actively opt into concurrency |
| Per-call classification | Fine-grained control over parallelism | Requires analyzing each call, not just tool registration |
| Multi-source permission layering | Flexible policy composition | Hard to debug when rules conflict |
| Stateful evaluator | Can adapt behavior based on history | Not a pure function — harder to test |
Implementation Patterns
Tool Registration
// Example: Tool registry entry
interface ToolDefinition {
name: string;
description: string;
handler: (args: any) => Promise<any>;
// Safety classification
isReadOnly: boolean; // Default: false
isConcurrentSafe: boolean; // Default: false
// Optional custom permission logic
permissionCheck?: (args: any, context: ToolContext) => PermissionResult;
}
// Register tools
registry.register('read_file', {
name: 'read_file',
description: 'Read contents of a file',
handler: readFile,
isReadOnly: true,
isConcurrentSafe: true, // Safe to read multiple files in parallel
});
registry.register('write_file', {
name: 'write_file',
description: 'Write or overwrite a file',
handler: writeFile,
isReadOnly: false,
isConcurrentSafe: false, // Must run serially to prevent race conditions
});Permission Pipeline
// Permission evaluation order
async function evaluatePermission(
toolCall: ToolCall,
context: PermissionContext
): Promise<PermissionResult> {
// 1. Policy rules (highest priority, org-wide)
const policyResult = await policyEngine.check(toolCall, context);
if (policyResult !== 'defer') return policyResult;
// 2. User settings
const userResult = await userSettings.check(toolCall, context);
if (userResult !== 'defer') return userResult;
// 3. Project rules
const projectResult = await projectRules.check(toolCall, context);
if (projectResult !== 'defer') return projectResult;
// 4. Local overrides
const localResult = await localOverrides.check(toolCall, context);
if (localResult !== 'defer') return localResult;
// 5. Session grants (lowest priority)
return sessionGrants.check(toolCall, context);
}Bypass-Immune Rules
Certain paths or operations should never be auto-approved:
# Protected paths (never auto-approve)
protected_paths:
- /etc/**
- /usr/**
- node_modules/**
- .git/**
# Protected commands (always ask)
protected_commands:
- "rm -rf*"
- "DROP TABLE*"
- "DELETE FROM*"
- "mkfs*"Gotchas
1. Most async work skips "pending" state — work units register directly as "running" 2. Permission evaluation has side effects — don't cache results across calls 3. Concurrency classification requires analyzing inputs, not just tool name 4. The default permission for tools is "allow" — tools without custom logic delegate to rule-based system 5. Eviction requires notification — terminal work units only GC-eligible after parent notified
Related Patterns
- Lifecycle & Bootstrap — How tools are registered at init
- Hook Lifecycle (
hook-lifecycle-pattern.md) — Pre/post tool execution hooks
Template: Tool Safety Checklist
Before enabling a new tool:
## Tool Safety Review
**Tool name**: [e.g., execute_shell]
### Classification
- [ ] Determined if read-only (true / false / depends on args)
- [ ] Determined if concurrent-safe (true / false / depends on args)
- [ ] Documented unsafe input patterns
### Permission Requirements
- [ ] Default mode set to "ask" or "deny"
- [ ] Bypass-immune paths/commands defined
- [ ] Custom permission logic implemented (if needed)
- [ ] Audit logging enabled
### Testing
- [ ] Tested with safe inputs (should auto-approve)
- [ ] Tested with unsafe inputs (should ask/deny)
- [ ] Tested concurrent execution (should serialize if unsafe)
- [ ] Tested error handling (failures logged, state consistent)Evidence
Tool registry and safety patterns are observed in production agent runtimes including:
- Claude Code's tool registry with explicit concurrency flags
- Multi-source permission evaluation (settings → project → session)
- Protected path/command lists that bypass auto-approve modes
- Per-call concurrency classification that partitions tool batches
#!/usr/bin/env node
import { chmod, mkdir } from 'node:fs/promises';
import path from 'node:path';
import {
copyTemplate,
detectPackageManager,
detectProject,
exists,
initScriptFromCommands,
parseArgs,
verificationCommands,
writeText
} from './lib/harness-utils.mjs';
const args = parseArgs(process.argv.slice(2));
if (args.help) {
console.log(`Usage: node scripts/create-harness.mjs [--target DIR] [--agent-file AGENTS.md|CLAUDE.md] [--package-manager npm|pnpm|yarn|bun] [--force]
Creates a minimal production harness:
AGENTS.md or CLAUDE.md
feature_list.json
progress.md
session-handoff.md
init.sh
Existing files are skipped unless --force is set.`);
process.exit(0);
}
const target = path.resolve(args.target || args._[0] || process.cwd());
const agentFile = args.agentFile || 'AGENTS.md';
const force = Boolean(args.force);
const project = await detectProject(target);
project.packageManager = detectPackageManager(target, args.packageManager);
const commands = args.commands
? String(args.commands).split(',').map((command) => command.trim()).filter(Boolean)
: verificationCommands(project, args.packageManager);
await mkdir(target, { recursive: true });
const replacements = {
AGENT_FILE_NAME: agentFile,
PROJECT_PURPOSE: project.stack === 'generic'
? 'Project harness for reliable agent-assisted development.'
: `Project harness for reliable agent-assisted development in a ${project.stack} codebase.`,
VERIFICATION_COMMANDS: commands.map((command) => `- \`${command}\``).join('\n'),
PRIMARY_VERIFICATION_COMMAND: './init.sh'
};
const results = [];
results.push(await copyTemplate('agents.md', path.join(target, agentFile), replacements, { force }));
results.push(await copyTemplate('feature-list.json', path.join(target, 'feature_list.json'), {}, { force }));
results.push(await copyTemplate('progress.md', path.join(target, 'progress.md'), {}, { force }));
results.push(await copyTemplate('session-handoff.md', path.join(target, 'session-handoff.md'), {}, { force }));
const initPath = path.join(target, 'init.sh');
if (force || !await exists(initPath)) {
await writeText(initPath, initScriptFromCommands(commands));
await chmod(initPath, 0o755);
results.push({ path: initPath, status: 'written' });
} else {
results.push({ path: initPath, status: 'skipped', reason: 'exists' });
}
console.log(`Created harness for ${target}`);
console.log(`Detected stack: ${project.stack}`);
console.log(`Verification commands:`);
for (const command of commands) {
console.log(` - ${command}`);
}
console.log('');
for (const result of results) {
console.log(`${result.status.toUpperCase()} ${path.relative(target, result.path)}${result.reason ? ` (${result.reason})` : ''}`);
}
import { existsSync } from 'node:fs';
import { access, chmod, copyFile, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
export const SKILL_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
export const TEMPLATE_DIR = path.join(SKILL_ROOT, 'templates');
export const SUBSYSTEMS = ['instructions', 'state', 'verification', 'scope', 'lifecycle'];
export function parseArgs(argv) {
const args = { _: [] };
for (let i = 0; i < argv.length; i += 1) {
const token = argv[i];
if (!token.startsWith('--')) {
args._.push(token);
continue;
}
const [rawKey, inlineValue] = token.slice(2).split('=', 2);
const key = rawKey.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
if (inlineValue !== undefined) {
args[key] = inlineValue;
} else if (argv[i + 1] && !argv[i + 1].startsWith('--')) {
args[key] = argv[i + 1];
i += 1;
} else {
args[key] = true;
}
}
return args;
}
export async function exists(filePath) {
try {
await access(filePath);
return true;
} catch {
return false;
}
}
export async function readText(filePath) {
return readFile(filePath, 'utf8');
}
export async function readJson(filePath) {
return JSON.parse(await readText(filePath));
}
export async function writeText(filePath, contents) {
await mkdir(path.dirname(filePath), { recursive: true });
await writeFile(filePath, contents, 'utf8');
}
export async function copyTemplate(templateName, targetPath, replacements = {}, { force = false } = {}) {
if (!force && await exists(targetPath)) {
return { path: targetPath, status: 'skipped', reason: 'exists' };
}
let contents = await readText(path.join(TEMPLATE_DIR, templateName));
for (const [key, value] of Object.entries(replacements)) {
contents = contents.split(`{{${key}}}`).join(value);
}
await writeText(targetPath, contents);
if (templateName.endsWith('.sh')) {
await chmod(targetPath, 0o755);
}
return { path: targetPath, status: 'written' };
}
export function detectPackageManager(root, explicit) {
if (explicit) return explicit;
if (existsSync(path.join(root, 'bun.lockb')) || existsSync(path.join(root, 'bun.lock'))) return 'bun';
if (existsSync(path.join(root, 'pnpm-lock.yaml'))) return 'pnpm';
if (existsSync(path.join(root, 'yarn.lock'))) return 'yarn';
return 'npm';
}
export async function detectProject(root) {
const files = await listFiles(root, { maxFiles: 800 });
const has = (name) => files.some((file) => file === name || file.endsWith(`/${name}`));
const hasPrefix = (prefix) => files.some((file) => file.startsWith(prefix));
const packageJsonPath = path.join(root, 'package.json');
const packageJson = await exists(packageJsonPath).then((ok) => ok ? readJson(packageJsonPath) : null);
let stack = 'generic';
if (packageJson) {
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
if (deps.react || hasPrefix('src/renderer')) stack = 'typescript-react';
else if (deps.typescript || has('tsconfig.json')) stack = 'typescript';
else stack = 'node';
} else if (has('pyproject.toml') || has('requirements.txt')) {
stack = 'python';
} else if (has('go.mod')) {
stack = 'go';
} else if (has('Cargo.toml')) {
stack = 'rust';
} else if (has('pom.xml')) {
stack = 'java-maven';
} else if (has('build.gradle') || has('build.gradle.kts')) {
stack = 'java-gradle';
} else if (files.some((file) => file.endsWith('.csproj') || file.endsWith('.sln'))) {
stack = 'dotnet';
}
return {
root,
stack,
packageJson,
files,
packageManager: detectPackageManager(root)
};
}
export async function listFiles(root, { maxFiles = 1000 } = {}) {
const ignored = new Set(['.git', 'node_modules', 'dist', 'build', '.next', '.venv', 'venv', '__pycache__']);
const results = [];
async function walk(current, relative) {
if (results.length >= maxFiles) return;
let entries = [];
try {
entries = await readdir(current, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
if (results.length >= maxFiles) return;
if (ignored.has(entry.name)) continue;
const rel = relative ? `${relative}/${entry.name}` : entry.name;
const full = path.join(current, entry.name);
if (entry.isDirectory()) {
await walk(full, rel);
} else if (entry.isFile()) {
results.push(rel);
}
}
}
await walk(root, '');
return results.sort();
}
export function verificationCommands(project, explicitPackageManager) {
const pm = explicitPackageManager || project.packageManager || 'npm';
const scripts = project.packageJson?.scripts ?? {};
const run = (script) => {
if (pm === 'npm') return `npm run ${script}`;
if (pm === 'yarn') return `yarn ${script}`;
return `${pm} run ${script}`;
};
if (project.stack === 'python') {
return [
'python -m pytest',
'python -m compileall .'
];
}
if (project.stack === 'go') return ['go test ./...'];
if (project.stack === 'rust') return ['cargo test'];
if (project.stack === 'java-maven') return ['mvn test'];
if (project.stack === 'java-gradle') return ['./gradlew test'];
if (project.stack === 'dotnet') return ['dotnet test'];
if (!project.packageJson) {
return [
'echo "No package manifest detected; replace this line with your project verification command."'
];
}
const install = pm === 'npm'
? 'npm install'
: pm === 'yarn'
? 'yarn install'
: `${pm} install`;
const candidates = [
scripts.check ? run('check') : null,
scripts.typecheck ? run('typecheck') : null,
scripts['type-check'] ? run('type-check') : null,
scripts.lint ? run('lint') : null,
scripts.test ? (pm === 'npm' ? 'npm test' : `${pm} test`) : null,
scripts.build ? run('build') : null
].filter(Boolean);
return [install, ...dedupe(candidates)];
}
export function initScriptFromCommands(commands) {
const body = commands.map((command) => `echo "=== ${escapeForEcho(command)} ==="\n${command}`).join('\n\n');
return `#!/bin/bash
set -e
echo "=== Harness Initialization ==="
${body}
echo "=== Verification Complete ==="
echo ""
echo "Next steps:"
echo "1. Read feature_list.json to see current feature state"
echo "2. Pick ONE unfinished feature to work on"
echo "3. Implement only that feature"
echo "4. Re-run verification before claiming done"
`;
}
function escapeForEcho(value) {
return value.replaceAll('"', '\\"');
}
export function dedupe(values) {
return [...new Set(values)];
}
export function scoreHarness(files) {
const byPath = new Map(files.map((file) => [file.path, file.content]));
const allText = files.map((file) => `${file.path}\n${file.content}`).join('\n\n');
const agents = byPath.get('AGENTS.md') || byPath.get('CLAUDE.md') || '';
const featureList = byPath.get('feature_list.json') || byPath.get('feature-list.json') || '';
const progress = byPath.get('progress.md') || '';
const init = byPath.get('init.sh') || '';
const handoff = byPath.get('session-handoff.md') || '';
const checks = {
instructions: [
hasFile(byPath, ['AGENTS.md', 'CLAUDE.md'], 'Agent instruction file exists'),
textHas(agents, ['Startup Workflow', 'Before writing code'], 'Startup workflow documented'),
textHas(agents, ['Definition of Done', 'done only when'], 'Definition of done documented'),
textHas(agents, ['Verification Commands', './init.sh', 'test', 'verify'], 'Verification commands discoverable'),
textHas(agents, ['feature_list.json', 'progress.md'], 'State artifacts routed from instructions')
],
state: [
hasFile(byPath, ['feature_list.json', 'feature-list.json'], 'Feature tracker exists'),
jsonFeatureList(featureList, 'Feature tracker is valid and has feature fields'),
hasFile(byPath, ['progress.md'], 'Progress log exists'),
textHas(progress, ['Current State', 'What', 'Next'], 'Progress log supports restart'),
textHas(handoff || progress, ['Blockers', 'Files', 'Next Session'], 'Handoff captures blockers/files/next step')
],
verification: [
hasFile(byPath, ['init.sh'], 'Verification entrypoint exists'),
textHas(init, ['set -e'], 'Verification fails fast'),
textHas(init + agents, ['test', 'pytest', 'vitest', 'cargo test', 'go test', 'dotnet test'], 'Test command documented'),
textHas(init + agents, ['build', 'type', 'lint', 'compile'], 'Static/build check documented'),
textHas(allText, ['Evidence', 'Verification Evidence', 'command and output'], 'Verification evidence is recorded')
],
scope: [
textHas(agents, ['One feature at a time', 'one-feature-at-a-time'], 'One-feature-at-a-time rule exists'),
textHas(featureList, ['dependencies'], 'Feature dependencies are tracked'),
textHas(agents + featureList, ['status'], 'Feature status is explicit'),
textHas(agents, ['Stay in scope', 'scope'], 'Scope boundary documented'),
textHas(agents, ['Definition of Done'], 'Completion gate limits scope closure')
],
lifecycle: [
hasFile(byPath, ['init.sh'], 'Startup script exists'),
textHas(agents, ['End of Session', 'Before ending'], 'End-of-session procedure exists'),
hasFile(byPath, ['session-handoff.md'], 'Session handoff template exists'),
textHas(progress + handoff, ['Last Updated', 'Current Objective', 'Recommended Next Step'], 'Session restart markers exist'),
textHas(agents + init, ['restartable', 'clean', 'Next steps'], 'Clean restart path documented')
]
};
const subsystems = Object.fromEntries(Object.entries(checks).map(([name, subsystemChecks]) => {
const passed = subsystemChecks.filter((check) => check.pass).length;
const score = Math.max(1, Math.round((passed / subsystemChecks.length) * 5));
return [name, {
score,
passed,
total: subsystemChecks.length,
checks: subsystemChecks
}];
}));
const total = Object.values(subsystems).reduce((sum, item) => sum + item.score, 0);
const overall = Math.round((total / (SUBSYSTEMS.length * 5)) * 100);
const bottleneck = Object.entries(subsystems).sort((a, b) => a[1].score - b[1].score)[0][0];
return { overall, bottleneck, subsystems };
}
function hasFile(byPath, names, message) {
return { pass: names.some((name) => byPath.has(name)), message };
}
function textHas(text, needles, message) {
const lower = text.toLowerCase();
return { pass: needles.some((needle) => lower.includes(needle.toLowerCase())), message };
}
function jsonFeatureList(text, message) {
try {
const parsed = JSON.parse(text);
const valid = Array.isArray(parsed.features) && parsed.features.every((feature) =>
typeof feature.id === 'string'
&& typeof feature.name === 'string'
&& typeof feature.description === 'string'
&& typeof feature.status === 'string'
);
return { pass: valid, message };
} catch {
return { pass: false, message };
}
}
export async function loadHarnessFiles(root) {
const candidates = [
'AGENTS.md',
'CLAUDE.md',
'feature_list.json',
'feature-list.json',
'progress.md',
'session-handoff.md',
'init.sh'
];
const files = [];
for (const candidate of candidates) {
const fullPath = path.join(root, candidate);
if (await exists(fullPath)) {
files.push({ path: candidate, content: await readText(fullPath) });
}
}
return files;
}
export function formatScoreReport(result, root = '.') {
const lines = [
`Harness validation for ${root}`,
`Overall: ${result.overall}/100`,
`Bottleneck: ${result.bottleneck}`,
''
];
for (const [name, subsystem] of Object.entries(result.subsystems)) {
lines.push(`${name}: ${subsystem.score}/5 (${subsystem.passed}/${subsystem.total})`);
for (const check of subsystem.checks) {
lines.push(` ${check.pass ? 'PASS' : 'FAIL'} ${check.message}`);
}
lines.push('');
}
return lines.join('\n');
}
export function htmlReport(result, title = 'Harness Assessment') {
const rows = Object.entries(result.subsystems).map(([name, subsystem]) => {
const checks = subsystem.checks.map((check) =>
`<li class="${check.pass ? 'pass' : 'fail'}">${check.pass ? 'PASS' : 'FAIL'} ${escapeHtml(check.message)}</li>`
).join('');
return `<section>
<h2>${escapeHtml(name)} <span>${subsystem.score}/5</span></h2>
<ul>${checks}</ul>
</section>`;
}).join('\n');
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${escapeHtml(title)}</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 32px; color: #172026; background: #f7f8fa; }
main { max-width: 960px; margin: 0 auto; }
header { margin-bottom: 24px; }
h1 { margin: 0 0 8px; font-size: 32px; }
.summary { display: flex; gap: 16px; flex-wrap: wrap; margin: 20px 0; }
.metric { background: white; border: 1px solid #d9dee5; border-radius: 8px; padding: 16px 18px; min-width: 180px; }
.metric strong { display: block; font-size: 28px; margin-top: 4px; }
section { background: white; border: 1px solid #d9dee5; border-radius: 8px; margin: 14px 0; padding: 16px 18px; }
h2 { margin: 0 0 10px; font-size: 20px; display: flex; justify-content: space-between; }
ul { margin: 0; padding-left: 20px; }
li { margin: 6px 0; }
.pass { color: #126c43; }
.fail { color: #a23020; }
</style>
</head>
<body>
<main>
<header>
<h1>${escapeHtml(title)}</h1>
<p>Five-subsystem harness validation report.</p>
<div class="summary">
<div class="metric">Overall<strong>${result.overall}/100</strong></div>
<div class="metric">Bottleneck<strong>${escapeHtml(result.bottleneck)}</strong></div>
</div>
</header>
${rows}
</main>
</body>
</html>
`;
}
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
export async function copyFileSafe(source, target, { force = false } = {}) {
if (!force && await exists(target)) {
return { path: target, status: 'skipped', reason: 'exists' };
}
await mkdir(path.dirname(target), { recursive: true });
await copyFile(source, target);
return { path: target, status: 'written' };
}
#!/usr/bin/env node
import path from 'node:path';
import {
htmlReport,
loadHarnessFiles,
parseArgs,
scoreHarness,
writeText
} from './lib/harness-utils.mjs';
const args = parseArgs(process.argv.slice(2));
if (args.help) {
console.log(`Usage: node scripts/render-assessment-html.mjs [--target DIR] [--output FILE]
Renders the five-subsystem harness assessment as a standalone HTML file.`);
process.exit(0);
}
const target = path.resolve(args.target || args._[0] || process.cwd());
const output = path.resolve(args.output || path.join(target, 'harness-assessment.html'));
const result = scoreHarness(await loadHarnessFiles(target));
await writeText(output, htmlReport(result, `Harness Assessment: ${path.basename(target)}`));
console.log(`HTML report written to ${output}`);
console.log(`Overall: ${result.overall}/100`);
#!/usr/bin/env node
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
formatScoreReport,
htmlReport,
loadHarnessFiles,
parseArgs,
readJson,
scoreHarness,
writeText
} from './lib/harness-utils.mjs';
const args = parseArgs(process.argv.slice(2));
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const skillRoot = path.resolve(scriptDir, '..');
if (args.help) {
console.log(`Usage: node scripts/run-benchmark.mjs [--target DIR] [--output FILE] [--html FILE]
Runs a lightweight harness benchmark:
1. Scores the current target harness.
2. Checks eval coverage in evals/evals.json.
3. Produces a JSON report and optional HTML report.
This is a structural benchmark, not an LLM judge. Use it before/after real agent sessions.`);
process.exit(0);
}
const target = path.resolve(args.target || args._[0] || process.cwd());
const output = path.resolve(args.output || path.join(target, 'harness-benchmark.json'));
const evalPath = path.resolve(args.evals || path.join(skillRoot, 'evals', 'evals.json'));
const harnessResult = scoreHarness(await loadHarnessFiles(target));
const evals = await readJson(evalPath);
const evalResult = scoreEvals(evals);
const report = {
generatedAt: new Date().toISOString(),
target,
harness: harnessResult,
evals: evalResult,
recommendation: recommend(harnessResult, evalResult)
};
await writeText(output, `${JSON.stringify(report, null, 2)}\n`);
console.log(`Benchmark report written to ${output}`);
console.log('');
console.log(formatScoreReport(harnessResult, target));
console.log(`Eval coverage: ${evalResult.score}/100 (${evalResult.passed}/${evalResult.total})`);
console.log(`Recommendation: ${report.recommendation}`);
if (args.html) {
const htmlPath = path.resolve(args.html);
await writeText(htmlPath, renderBenchmarkHtml(report));
console.log(`HTML benchmark report written to ${htmlPath}`);
}
if (harnessResult.overall < Number(args.minScore || 70) || evalResult.score < Number(args.minEvalScore || 80)) {
process.exitCode = 1;
}
function scoreEvals(evalsJson) {
const cases = Array.isArray(evalsJson.evals) ? evalsJson.evals : [];
const checks = [];
checks.push({ pass: cases.length >= 10, message: 'At least 10 eval cases' });
checks.push({ pass: cases.some((item) => /minimal|creation/i.test(item.name)), message: 'Covers minimal harness creation' });
checks.push({ pass: cases.some((item) => /session|continuity/i.test(item.name)), message: 'Covers session continuity' });
checks.push({ pass: cases.some((item) => /assessment|score/i.test(item.name)), message: 'Covers harness assessment' });
checks.push({ pass: cases.some((item) => /verification/i.test(item.name)), message: 'Covers verification workflow' });
checks.push({ pass: cases.some((item) => /memory/i.test(item.name)), message: 'Covers memory taxonomy' });
checks.push({ pass: cases.some((item) => /tool|permission|safety/i.test(item.name)), message: 'Covers tool safety' });
checks.push({ pass: cases.some((item) => /multi-agent|delegation|coordination/i.test(item.name)), message: 'Covers multi-agent coordination' });
checks.push({ pass: cases.every((item) => item.prompt && item.expected_output && Array.isArray(item.expectations)), message: 'Each eval has prompt, expected output, expectations' });
checks.push({ pass: cases.every((item) => item.expectations?.length >= 3), message: 'Each eval has at least three expectation checks' });
const passed = checks.filter((check) => check.pass).length;
return {
score: Math.round((passed / checks.length) * 100),
passed,
total: checks.length,
cases: cases.length,
checks
};
}
function recommend(harnessResult, evalResult) {
if (harnessResult.overall >= 85 && evalResult.score >= 90) {
return 'Ready for realistic before/after agent-session benchmarking.';
}
if (harnessResult.overall < 70) {
return `Improve the ${harnessResult.bottleneck} subsystem before benchmarking agent behavior.`;
}
if (evalResult.score < 80) {
return 'Expand eval coverage before treating benchmark results as representative.';
}
return 'Usable, with some gaps worth tightening after first real sessions.';
}
function renderBenchmarkHtml(report) {
const evalHtml = htmlReport(report.harness, `Harness Benchmark: ${path.basename(report.target)}`)
.replace('</main>', `<section>
<h2>Eval Coverage <span>${report.evals.score}/100</span></h2>
<p>${report.evals.passed}/${report.evals.total} benchmark checks passed across ${report.evals.cases} eval cases.</p>
<ul>${report.evals.checks.map((check) => `<li class="${check.pass ? 'pass' : 'fail'}">${check.pass ? 'PASS' : 'FAIL'} ${escapeHtml(check.message)}</li>`).join('')}</ul>
</section>
<section>
<h2>Recommendation</h2>
<p>${escapeHtml(report.recommendation)}</p>
</section>
</main>`);
return evalHtml;
}
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
#!/usr/bin/env node
import path from 'node:path';
import {
formatScoreReport,
htmlReport,
loadHarnessFiles,
parseArgs,
scoreHarness,
writeText
} from './lib/harness-utils.mjs';
const args = parseArgs(process.argv.slice(2));
if (args.help) {
console.log(`Usage: node scripts/validate-harness.mjs [--target DIR] [--json] [--html FILE]
Scores a project harness across five subsystems:
instructions, state, verification, scope, lifecycle
Exit code is 0 when the harness scores at least --min-score (default 70).`);
process.exit(0);
}
const target = path.resolve(args.target || args._[0] || process.cwd());
const minScore = Number(args.minScore || 70);
const files = await loadHarnessFiles(target);
const result = scoreHarness(files);
if (args.html) {
const htmlPath = path.resolve(args.html);
await writeText(htmlPath, htmlReport(result, `Harness Assessment: ${path.basename(target)}`));
console.log(`HTML report written to ${htmlPath}`);
}
if (args.json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(formatScoreReport(result, target));
}
if (result.overall < minScore) {
process.exitCode = 1;
}
---
name: harness-creator
description: >-
Build, audit, and improve lightweight harnesses for AI coding agents: AGENTS.md/CLAUDE.md,
feature state, verification workflows, scope boundaries, lifecycle handoff,
memory persistence, context control, tool safety, and multi-agent coordination.
license: MIT
---
# Harness Creator
Use this skill to make a repository easier for coding agents to start, stay in scope, verify work, and resume across sessions. Keep the harness small enough that agents actually follow it.
Not for model selection, prompt tuning in isolation, chat UI design, or general app architecture.
## Core Model
Every useful coding-agent harness has five subsystems:
| Subsystem | Minimal artifact | Purpose |
|---|---|---|
| Instructions | `AGENTS.md` or `CLAUDE.md` | Startup path, working rules, definition of done |
| State | `feature_list.json`, `progress.md` | Current feature, status, evidence, next step |
| Verification | `init.sh` or documented commands | Tests/checks the agent must run before claiming done |
| Scope | Feature dependencies and done criteria | Prevents overreach and half-finished work |
| Lifecycle | `session-handoff.md`, end-of-session routine | Makes the next session restartable |
## First Move
1. Inspect what already exists: instruction files, feature/state files, verification commands, docs, package manifests.
2. Ask only for missing context that cannot be inferred safely: target agent, desired file name, tolerance for structure, and whether overwriting is allowed.
3. Prefer a minimal harness first. Add memory, tool safety, multi-agent, or benchmark details only when the user's problem calls for them.
## Common Tasks
### Create a harness
Use the bundled script when working on a local repository:
```bash
node skills/harness-creator/scripts/create-harness.mjs --target /path/to/project
```
Options:
- `--agent-file CLAUDE.md` for Claude-oriented projects.
- `--package-manager npm|pnpm|yarn|bun` when detection is wrong.
- `--commands "cmd one,cmd two"` for custom verification.
- `--force` only after confirming overwrites are acceptable.
Then explain what was created and how the user should replace placeholder feature entries.
### Audit an existing harness
Run:
```bash
node skills/harness-creator/scripts/validate-harness.mjs --target /path/to/project
```
Report the five subsystem scores, the lowest-scoring area, and the first 2-3 changes that would improve reliability. Treat the lowest score as a candidate bottleneck; confirm with failures, logs, or task outcomes before claiming causality.
### Produce a report
Use when the user wants a shareable assessment:
```bash
node skills/harness-creator/scripts/render-assessment-html.mjs --target /path/to/project
node skills/harness-creator/scripts/run-benchmark.mjs --target /path/to/project --html /path/to/report.html
```
Be clear that this is a structural benchmark. Real effectiveness still needs before/after agent sessions on representative tasks.
## When to Read References
Load only the reference needed for the user's problem:
- Memory across sessions: [Memory Persistence](references/memory-persistence-pattern.md)
- Reusable workflows as skills: [Skill Runtime](references/skill-runtime-pattern.md)
- Permissions, tools, concurrency: [Tool Registry & Safety](references/tool-registry-pattern.md)
- Context budget and progressive disclosure: [Context Engineering](references/context-engineering-pattern.md)
- Delegation and parallel agents: [Multi-Agent Coordination](references/multi-agent-pattern.md)
- Hooks, startup, long-running work: [Lifecycle & Bootstrap](references/lifecycle-bootstrap-pattern.md)
- Non-obvious failure modes: [Gotchas](references/gotchas.md)
## Design Rules
- Keep the root instruction file short: routing and invariants, not a full manual.
- Put project facts in project docs, not in the skill.
- Make verification commands explicit and runnable.
- Require evidence before marking a feature done.
- Use one active feature unless the harness has explicit multi-agent ownership boundaries.
- Prefer append/update state files over relying on chat history.
- Never hide destructive behavior in scripts; overwrites require explicit user approval.
## Deliverable Checklist
For a usable minimal harness, leave the target project with:
- [ ] `AGENTS.md` or `CLAUDE.md`
- [ ] `feature_list.json`
- [ ] `progress.md`
- [ ] `init.sh`
- [ ] Optional `session-handoff.md` for multi-session work
- [ ] Documented verification evidence or next action
If you cannot create files, provide exact file contents and commands instead.
{{AGENT_FILE_NAME}}
{{PROJECT_PURPOSE}}
Startup Workflow
Before writing code:
1. Confirm working directory with pwd 2. Read this file completely 3. Read project docs if present (docs/ARCHITECTURE.md, docs/PRODUCT.md, README, or equivalent) 4. Run `./init.sh` to verify environment is healthy 5. Read `feature_list.json` to see current feature state 6. Review recent commits with git log --oneline -5
If baseline verification is failing, repair that first before adding new scope.
Working Rules
- One feature at a time: Pick exactly one unfinished feature from
feature_list.json - Verification required: Don't claim done without running verification commands
- Update artifacts: Before ending session, update
progress.mdandfeature_list.json - Stay in scope: Don't modify files unrelated to the current feature
- Leave clean state: Next session must be able to run
./init.shimmediately
Required Artifacts
feature_list.json— Feature state tracker (source of truth)progress.md— Session continuity loginit.sh— Standard startup and verification pathsession-handoff.md— Optional, for larger sessions
Definition of Done
A feature is done only when ALL of the following are true:
- [ ] Target behavior is implemented
- [ ] Required verification actually ran (tests / lint / type-check)
- [ ] Evidence recorded in
feature_list.jsonorprogress.md - [ ] Repository remains restartable from standard startup path
End of Session
Before ending a session:
1. Update progress.md with current state 2. Update feature_list.json with new feature status 3. Record any unresolved risks or blockers 4. Commit with descriptive message once work is in safe state 5. Leave repo clean enough for next session to run ./init.sh immediately
Verification Commands
# Full verification (recommended)
{{PRIMARY_VERIFICATION_COMMAND}}Required checks: {{VERIFICATION_COMMANDS}}
Escalation
If you encounter:
- Architecture decisions: Consult project architecture docs if present, otherwise ask user
- Unclear requirements: Check product/requirements docs if present, otherwise ask user
- Repeated test failures: Update progress, flag for human review
- Scope ambiguity: Re-read
feature_list.jsonfor definition of done
{
"features": [
{
"id": "feat-001",
"name": "Project Setup",
"description": "Confirm the project can install dependencies, run verification, and start from a clean checkout",
"dependencies": [],
"status": "not-started",
"evidence": ""
},
{
"id": "feat-002",
"name": "First User-Facing Feature",
"description": "Replace this placeholder with the first concrete behavior the agent should implement",
"dependencies": ["feat-001"],
"status": "not-started",
"evidence": ""
},
{
"id": "feat-003",
"name": "Verification Coverage",
"description": "Add or confirm tests, type checks, linting, or manual verification for the active feature",
"dependencies": ["feat-002"],
"status": "not-started",
"evidence": ""
},
{
"id": "feat-004",
"name": "Documentation Update",
"description": "Update README, architecture notes, or product docs affected by the implemented feature",
"dependencies": ["feat-003"],
"status": "not-started",
"evidence": ""
},
{
"id": "feat-005",
"name": "Cleanup and Handoff",
"description": "Record verification evidence, update progress.md, and leave a clear next-session path",
"dependencies": ["feat-004"],
"status": "not-started",
"evidence": ""
}
]
}
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Feature List",
"description": "Track feature implementation state for agent-driven development",
"type": "object",
"properties": {
"features": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique feature identifier (e.g., feat-001)",
"pattern": "^feat-\\d+$"
},
"name": {
"type": "string",
"description": "Short feature name"
},
"description": {
"type": "string",
"description": "What this feature does"
},
"dependencies": {
"type": "array",
"items": {
"type": "string"
},
"description": "Feature IDs that must be done before this one"
},
"status": {
"type": "string",
"enum": ["not-started", "in-progress", "blocked", "done"],
"description": "Current implementation state"
},
"evidence": {
"type": "string",
"description": "Verification evidence when status is 'done'"
}
},
"required": ["id", "name", "description", "status"]
}
}
},
"required": ["features"]
}
#!/bin/bash
set -e
echo "=== Harness Initialization ==="
if [ -f package.json ]; then
if [ -f pnpm-lock.yaml ]; then
PM="pnpm"
elif [ -f yarn.lock ]; then
PM="yarn"
elif [ -f bun.lock ] || [ -f bun.lockb ]; then
PM="bun"
else
PM="npm"
fi
echo "=== Installing dependencies with $PM ==="
if [ "$PM" = "npm" ]; then
npm install
else
"$PM" install
fi
node -e "const s=require('./package.json').scripts||{}; process.exit(s.check||s.typecheck||s['type-check']?0:1)" && {
if node -e "const s=require('./package.json').scripts||{}; process.exit(s.check?0:1)"; then
[ "$PM" = "npm" ] && npm run check || "$PM" run check
elif node -e "const s=require('./package.json').scripts||{}; process.exit(s.typecheck?0:1)"; then
[ "$PM" = "npm" ] && npm run typecheck || "$PM" run typecheck
else
[ "$PM" = "npm" ] && npm run type-check || "$PM" run type-check
fi
}
node -e "const s=require('./package.json').scripts||{}; process.exit(s.lint?0:1)" && {
[ "$PM" = "npm" ] && npm run lint || "$PM" run lint
}
node -e "const s=require('./package.json').scripts||{}; process.exit(s.test?0:1)" && {
[ "$PM" = "npm" ] && npm test || "$PM" test
}
node -e "const s=require('./package.json').scripts||{}; process.exit(s.build?0:1)" && {
[ "$PM" = "npm" ] && npm run build || "$PM" run build
}
elif [ -f pyproject.toml ] || [ -f requirements.txt ]; then
echo "=== Running Python verification ==="
python -m pytest
python -m compileall .
elif [ -f go.mod ]; then
echo "=== Running Go verification ==="
go test ./...
elif [ -f Cargo.toml ]; then
echo "=== Running Rust verification ==="
cargo test
elif [ -f pom.xml ]; then
echo "=== Running Maven verification ==="
mvn test
elif [ -f build.gradle ] || [ -f build.gradle.kts ]; then
echo "=== Running Gradle verification ==="
./gradlew test
elif ls *.csproj *.sln >/dev/null 2>&1; then
echo "=== Running .NET verification ==="
dotnet test
else
echo "No recognized package manifest detected."
echo "Replace this section with the project's verification commands."
fi
echo "=== Verification Complete ==="
echo ""
echo "Next steps:"
echo "1. Read feature_list.json to see current feature state"
echo "2. Pick ONE unfinished feature to work on"
echo "3. Implement only that feature"
echo "4. Re-run verification before claiming done"
Session Progress Log
Current State
Last Updated: YYYY-MM-DD HH:MM Session ID: [optional] Active Feature: [feat-XXX - Feature Name]
Status
What's Done
- [x] [Completed item 1]
- [x] [Completed item 2]
What's In Progress
- [ ] [Current work item]
- Details: [specific task]
- Blockers: [if any]
What's Next
1. [Next action item] 2. [Following action item]
Blockers / Risks
- [ ] [Blocker 1]: [description, impact]
- [ ] [Risk 1]: [description, mitigation]
Decisions Made
- [Decision 1]: [description]
- Context: [why this decision was made]
- Alternatives considered: [what else was discussed]
Files Modified This Session
path/to/file1.ts- [brief description of change]path/to/file2.ts- [brief description of change]
Evidence of Completion
- [ ] Tests pass:
[command and output] - [ ] Type check clean:
[command and output] - [ ] Manual verification:
[what was tested]
Notes for Next Session
[Free-form notes that will help the next session pick up context]
Session Handoff
Current Objective
- Goal:
- Current status:
- Branch / commit:
Completed This Session
- [ ]
Verification Evidence
| Check | Command | Result | Notes |
|---|---|---|---|
Files Changed
-
Decisions Made
-
Blockers / Risks
-
Next Session Startup
1. Read AGENTS.md. 2. Read feature_list.json and progress.md. 3. Review this handoff. 4. Run ./init.sh or the documented verification command before editing.
Recommended Next Step
-
Related skills
How it compares
Choose harness-creator when you need a full agent harness audit across AGENTS.md, verification, and handoff rather than a single-session handoff document alone.
FAQ
What does harness-creator do?
Build lightweight agent harnesses with AGENTS.md, state files, verification, and handoff.
When should I use harness-creator?
User creates or audits AGENTS.md, feature state, verification commands, or handoff docs.
Is harness-creator safe to install?
Review the Security Audits panel on this page before installing in production.