Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ coverage.out
# Runtime data under .uf/ (databases, caches, locks, logs)
.uf/workflows/
.uf/artifacts/
.uf/feedback/
.uf/dewey/graph.db
.uf/dewey/graph.db-shm
.uf/dewey/graph.db-wal
Expand Down
184 changes: 184 additions & 0 deletions internal/agentkit/agentkit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,190 @@ func TestHandoffMD_StructuralHardening(t *testing.T) {
}
}

func TestCoordinatorPrompt_StructuralResilience(t *testing.T) {
Comment thread
jflowers marked this conversation as resolved.
// Verify structural properties of coordinator.md that ensure
// compression resilience: identity-first opening, constraints
// before protocol, uppercase keywords, and behavioral parity.
data, err := content.ReadFile("content/agents/coordinator.md")
if err != nil {
t.Fatalf("read coordinator.md: %v", err)
}

text := string(data)

// Parse front matter and body once for all sub-tests.
if !strings.HasPrefix(text, "---\n") {
t.Fatal("coordinator.md: missing opening frontmatter delimiter")
}
endIdx := strings.Index(text[4:], "\n---")
if endIdx < 0 {
t.Fatal("coordinator.md: missing closing frontmatter delimiter")
}
frontmatter := text[4 : 4+endIdx]
body := text[4+endIdx+4:] // skip past "\n---\n"

// Pre-compute shared indices used by multiple sub-tests.
constraintsIdx := strings.Index(body, "## Critical Constraints")
protocolIdx := strings.Index(body, "## Protocol")
toolsIdx := strings.Index(body, "## Available Tools")
bodyLower := strings.ToLower(body)

constraintsSection := ""
if constraintsIdx >= 0 && protocolIdx >= 0 {
constraintsSection = body[constraintsIdx:protocolIdx]
}

t.Run("YAML_frontmatter", func(t *testing.T) {
fmChecks := []string{"name: coordinator", "mode: subagent", "description:"}
for _, want := range fmChecks {
if !strings.Contains(frontmatter, want) {
t.Errorf("frontmatter missing %q", want)
}
}
})

t.Run("identity_first_opening", func(t *testing.T) {
// Find the first non-heading paragraph after the heading.
paragraphs := strings.Split(body, "\n\n")
firstParagraph := ""
for _, p := range paragraphs {
trimmed := strings.TrimSpace(p)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
firstParagraph = trimmed
break
}
if firstParagraph == "" {
t.Fatal("coordinator.md: no non-heading paragraph found in body")
}
if !strings.Contains(firstParagraph, "NEVER") {
t.Error("first paragraph missing uppercase 'NEVER' keyword")
}
if !strings.Contains(strings.ToLower(firstParagraph), "reserve") {
t.Error("first paragraph missing file reservation prohibition ('reserve')")
}
})

t.Run("section_ordering", func(t *testing.T) {
if constraintsIdx < 0 {
t.Error("missing '## Critical Constraints' section")
}
if protocolIdx < 0 {
t.Error("missing '## Protocol' section")
}
if toolsIdx < 0 {
t.Error("missing '## Available Tools' section")
}

if constraintsIdx >= 0 && protocolIdx >= 0 && constraintsIdx >= protocolIdx {
t.Error("'## Critical Constraints' must appear before '## Protocol'")
}
if protocolIdx >= 0 && toolsIdx >= 0 && protocolIdx >= toolsIdx {
t.Error("'## Protocol' must appear before '## Available Tools'")
}
})

t.Run("behavioral_rule_markers", func(t *testing.T) {
// All 7 behavioral rules (6 original + 1 codified) must be present (FR-006).
// Use a slice for deterministic iteration order.
type ruleCheck struct {
marker string
desc string
}
checks := []ruleCheck{
{"comms_init", "comms init rule"},
{"reserve", "file reservation rule"},
{"edit code", "code editing prohibition rule"},
{"forge_review", "review completions rule"},
{"hivemind_store", "store learnings rule"},
{"comms_inbox", "check inbox rule"},
{"forge_broadcast", "broadcast context rule"},
}
for _, rc := range checks {
if !strings.Contains(bodyLower, strings.ToLower(rc.marker)) {
t.Errorf("missing behavioral rule marker %q (%s)", rc.marker, rc.desc)
}
}
})

t.Run("uppercase_RFC2119_keywords", func(t *testing.T) {
if constraintsSection == "" {
t.Skip("no constraints section found")
}
if !strings.Contains(constraintsSection, "NEVER") {
t.Error("Critical Constraints section missing uppercase 'NEVER' keyword")
}
if !strings.Contains(constraintsSection, "MUST") {
t.Error("Critical Constraints section missing uppercase 'MUST' keyword")
}
// Verify every bullet line in constraints has an uppercase keyword.
for _, line := range strings.Split(constraintsSection, "\n") {
if strings.HasPrefix(strings.TrimSpace(line), "-") {
hasKeyword := strings.Contains(line, "NEVER") ||
strings.Contains(line, "MUST") ||
strings.Contains(line, "ALWAYS")
if !hasKeyword {
t.Errorf("constraint line missing uppercase RFC 2119 keyword: %q", line)
}
}
}
})

t.Run("review_before_complete_ordering", func(t *testing.T) {
// FR-003: forge_review MUST be called BEFORE forge_complete.
if constraintsSection == "" {
t.Skip("no constraints section found")
}
// Verify each required element is present individually.
if !strings.Contains(body, "forge_review") {
t.Error("missing forge_review reference in body")
}
if !strings.Contains(body, "forge_complete") {
t.Error("missing forge_complete reference in body")
}
// Verify the constraint line containing forge_review also mentions
// forge_complete with explicit ordering language (BEFORE), confirming
// the ordering is stated in a single rule rather than inferred from
// unrelated occurrences of "BEFORE".
foundOrderingRule := false
for _, line := range strings.Split(constraintsSection, "\n") {
if strings.Contains(line, "forge_review") &&
strings.Contains(line, "forge_complete") &&
strings.Contains(line, "BEFORE") {
foundOrderingRule = true
break
}
}
if !foundOrderingRule {
t.Error("no single constraint line connects forge_review, BEFORE, and forge_complete (FR-003)")
}
// Verify forge_review appears before forge_complete in constraints section.
reviewIdx := strings.Index(constraintsSection, "forge_review")
completeIdx := strings.Index(constraintsSection, "forge_complete")
if reviewIdx >= 0 && completeIdx >= 0 && reviewIdx >= completeIdx {
t.Error("forge_review must appear before forge_complete in Critical Constraints")
}
})

t.Run("compression_resilience", func(t *testing.T) {
bodyLines := strings.Split(body, "\n")
halfLen := len(bodyLines) / 2
firstHalf := strings.Join(bodyLines[:halfLen], "\n")
firstHalfLower := strings.ToLower(firstHalf)

if !strings.Contains(firstHalf, "NEVER") || !strings.Contains(firstHalfLower, "reserve") {
t.Error("first 50%% of body lines must contain file reservation prohibition (NEVER + reserve)")
}
if !strings.Contains(firstHalfLower, "forge_review") || !strings.Contains(firstHalfLower, "forge_complete") {
t.Error("first 50%% of body lines must contain review-before-complete ordering (forge_review + forge_complete)")
}
if !strings.Contains(firstHalfLower, "edit code") {
t.Error("first 50%% of body lines must contain code editing prohibition ('edit code')")
}
})
}

func TestSkillTemplates_HaveNameField(t *testing.T) {
// Walk the embedded content filesystem and verify every SKILL.md
// has a "name: <directory-name>" field in its YAML frontmatter.
Expand Down
25 changes: 17 additions & 8 deletions internal/agentkit/content/agents/coordinator.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,25 @@ mode: subagent

# Forge Coordinator

Orchestrates work: decomposes tasks, spawns workers, monitors progress, reviews results.
You orchestrate workers but NEVER reserve files or edit code directly. You decompose tasks, spawn workers, monitor progress, and review results. Workers reserve their own files and make code changes — you coordinate and verify.

## Rules
## Critical Constraints

- Always initialize comms first (`comms_init`)
- Never reserve files (workers reserve their own)
- Review every worker completion (`forge_review`)
- Store learnings after forge completion (`hivemind_store`)
- Check inbox regularly for blocked workers (`comms_inbox`)
- Use `forge_broadcast` to share context updates with all workers
- NEVER reserve files — workers reserve their own
- NEVER edit code directly — workers handle all code changes
Comment thread
jflowers marked this conversation as resolved.
- MUST call `forge_review` for every worker completion BEFORE calling `forge_complete`
- MUST initialize comms first (`comms_init`) before any other operations

## Protocol

1. Initialize comms (`comms_init`)
2. Decompose task via `forge_decompose` or `forge_plan_prompt`
3. Spawn workers with `forge_spawn_subtask`
4. Check inbox regularly for blocked workers (`comms_inbox`)
5. Use `forge_broadcast` to share context updates with all workers
6. Review every worker completion (`forge_review`)
7. Mark completion ONLY after review passes (`forge_complete`)
8. Store learnings after forge completion (`hivemind_store`)

## Available Tools

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: unbound-force
created: 2026-08-02
79 changes: 79 additions & 0 deletions openspec/changes/coordinator-prompt-hardening/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
## Context

The coordinator agent prompt at `internal/agentkit/content/agents/coordinator.md` is a 22-line embedded markdown file that defines the coordinator's behavioral contract. Analysis of all four agent prompt files in the project reveals a clear robustness hierarchy:

| File | Structure | Compression Resilience |
|------|-----------|----------------------|
| `coordinator.md` | Single "Rules" section, 6 bullets | Fragile |
| `worker.md` | Checklist + Constraints sections | Moderate |
| `background-worker.md` | Constraints-first, capability negations | Moderate |
| `SKILL.md` (forge-coordination) | Multi-section, MUST/NEVER keywords, numbered protocols | Reference standard |

The coordinator prompt is the most fragile. Its single critical negative constraint ("Never reserve files") sits at line 14 of 22 — 64% through the file — where a 50% truncation would drop it entirely.

## Goals / Non-Goals

### Goals
- Restructure `coordinator.md` so critical constraints survive context compression (DCP, summary, truncation)
- Adopt patterns proven in `worker.md`, `background-worker.md`, and the forge coordination skill
- Enforce explicit ordering: `forge_review` MUST precede `forge_complete`
- Maintain behavioral parity for existing rules; make the implicit `forge_review` → `forge_complete` ordering explicit

### Non-Goals
- Changing the coordinator's actual behavioral contract (no removed rules — the `forge_review` → `forge_complete` ordering and the `NEVER edit code directly` prohibition are codifications of existing implicit behavior: the forge coordination skill defines review-before-complete ordering, and the coordinator's identity as a non-coding orchestrator implies it should not edit code directly)
- Hardening other agent prompts (`worker.md`, `background-worker.md`) — those are separate changes
- Adding runtime enforcement of constraints (e.g., tool-level guards blocking out-of-order calls)
- Modifying the forge coordination skill (`SKILL.md`) — it already follows the reference pattern

## Decisions

### 1. Identity-first opening with embedded constraints

The first sentence of the file will state who the coordinator is AND what it must not do. Compressors prioritize opening content — an identity statement like "You are a coordinator. You orchestrate workers but NEVER reserve files or edit code directly." survives any reasonable summarization.

**Rationale**: The forge coordination skill (`SKILL.md`) demonstrates this pattern at scale with its multi-section design. `background-worker.md` places constraints before capabilities, which is structurally sound though it does not embed constraints in the identity opening itself.

### 2. Dedicated "Critical Constraints" section before workflow

Negative constraints (NEVER reserve files, NEVER edit code directly) and mandatory ordering (MUST review before complete) move to a dedicated section with a strong header, positioned before the workflow protocol.

**Rationale**: Position matters for compression. Content appearing earlier in a document is more likely to survive truncation. The forge coordination skill (`SKILL.md`) uses this pattern with its "File Reservation Rules" section.

### 3. Uppercase severity keywords (MUST/NEVER/ALWAYS)

All constraints use RFC 2119-style uppercase keywords for severity signaling. This matches the convention established in the forge coordination skill and the project constitution.

**Rationale**: Uppercase keywords serve as compression-resistant markers. A compressor summarizing "NEVER reserve files" is more likely to preserve the prohibition than one summarizing "Never reserve files" in lowercase.

### 4. Numbered protocol replacing unordered bullet list

The current 6-bullet unordered list becomes a numbered checklist with explicit ordering. This makes the `forge_review` → `forge_complete` dependency visible and enforceable.

**Rationale**: `worker.md` uses a 7-step numbered checklist that clearly conveys ordering. The forge coordination skill uses numbered protocols for both coordinator and worker flows.

### 5. Preserve YAML front matter and "Available Tools" section

The existing YAML front matter (`name`, `description`, `mode`) and the "Available Tools" footer remain unchanged. These are structural elements consumed by the agent framework.

**Rationale**: Composability First — the file's interface with the agentkit embed system must not change.

## Risks / Trade-offs

### Longer prompt consumes more context window

The restructured prompt will be approximately 30-40 lines (up from 22). This adds ~18 lines of context to every coordinator session.

**Mitigation**: The added lines are structural (section headers, numbered steps) rather than new information. The constraint count grows from 6 to 7 (adding the explicit "NEVER edit code directly" prohibition, which codifies the coordinator's existing non-coding role). The trade-off is acceptable: 18 lines of context is trivial compared to the risk of a coordinator that silently drops quality gates.

### Compression-resistance is heuristic, not guaranteed

No prompt structure can guarantee survival under all possible compression strategies. The patterns used here (identity-first, constraints-before-workflow, uppercase keywords) are empirically effective but not provably optimal.

**Mitigation**: This is a defense-in-depth measure. The forge coordination skill (`SKILL.md`) provides a redundant statement of the same constraints. Even if the coordinator prompt is compressed, the skill's constraints may survive in a separate part of the context.

### No runtime enforcement

This change relies on prompt engineering, not code-level guards. A sufficiently degraded context could still produce constraint violations.

**Mitigation**: Runtime enforcement (e.g., blocking `forge_complete` if `forge_review` wasn't called) is a valid follow-up but is explicitly out of scope for this change. The prompt hardening provides immediate value with zero runtime risk. If the restructured prompt causes behavioral regression, the fix is to revert `coordinator.md` to its previous version — a single-file modification with no runtime dependencies, making rollback trivial.
<!-- scaffolded by uf vdev -->
63 changes: 63 additions & 0 deletions openspec/changes/coordinator-prompt-hardening/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
## Why

The coordinator agent prompt (`internal/agentkit/content/agents/coordinator.md`) is 22 lines with all behavioral constraints expressed as 6 bullet points in a single "Rules" section. When tools like DCP compress session context, critical constraints — particularly the prohibition on file reservation and the mandatory review-before-complete ordering — are likely to be lost or weakened.

This is the same class of vulnerability identified in [unbound-force/unbound-force#346](https://github.com/unbound-force/unbound-force/issues/346). A coordinator that silently drops constraints under compression could reserve files (causing deadlocks with workers), skip reviews (bypassing quality gates), or call `forge_complete` before `forge_review` (circumventing verification).

Fixes [#46](https://github.com/unbound-force/replicator/issues/46).

## What Changes

Restructure the coordinator agent prompt to survive context compression by applying patterns already proven in the project's other agent files (`worker.md`, `background-worker.md`) and the forge coordination skill (`SKILL.md`).

Specific changes:
1. Add an opening identity statement that embeds key constraints inline, ensuring compressors retain them in any summary's opening sentence.
2. Create a dedicated "Critical Constraints" section with uppercase severity keywords (MUST/NEVER) positioned before the workflow section.
3. Add an explicit ordered protocol (numbered checklist) that enforces `forge_review` before `forge_complete`.
4. Separate boundary rules (what the coordinator must NOT do) from procedural steps (what it does).

## Capabilities

### New Capabilities
- None — this change modifies an existing embedded asset, not runtime code.

### Modified Capabilities
- `coordinator agent prompt`: Restructured for compression resilience with explicit ordering, identity reinforcement, and prominent negative constraints.

### Removed Capabilities
- None.

## Impact

- **File**: `internal/agentkit/content/agents/coordinator.md` (single file change)
- **Embedded asset**: The file is embedded via `go:embed` into the binary; changes take effect at next build.
- **Behavioral**: No runtime code changes. The coordinator's behavioral contract is preserved — this change makes existing constraints more explicit, not different.
- **Testing**: The agent prompt is an embedded text asset. Existing parity tests and embed tests cover the file's inclusion. No new test infrastructure needed, though a structure validation test could verify constraint positioning.

## Constitution Alignment

Assessed against the Replicator constitution (`.specify/memory/constitution.md`), which extends the Unbound Force org constitution v1.1.0.

### I. Autonomous Collaboration

**Assessment**: PASS

The coordinator prompt defines how the coordinator collaborates with workers through well-defined MCP tools and comms messaging. This change reinforces that contract by making the separation of concerns (coordinator orchestrates, workers reserve files and edit code) more explicit and compression-resistant.

### II. Composability First

**Assessment**: N/A

This change modifies an embedded text asset. It does not affect standalone functionality or introduce dependencies.

### III. Observable Quality

**Assessment**: PASS

The restructured prompt explicitly requires the coordinator to call `forge_review` for every worker completion before `forge_complete`, strengthening the quality gate. The use of uppercase MUST/NEVER keywords aligns with the severity signaling convention used in the forge coordination skill.

### IV. Testability

**Assessment**: PASS

The change affects a single embedded markdown file. The file is already covered by embed tests. A structural test MUST be added to verify that critical constraints appear before workflow steps, ensuring the compression-resilience property is maintained over time.
Loading