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
43 changes: 23 additions & 20 deletions .opencode/skills/forge-coordination/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ tags: [forge, coordination, multi-agent]

Patterns for coordinating parallel agent work.

## Coordinator Protocol
## Coordinator-Only Operations

- Coordinators MUST NOT reserve files
- Coordinators MUST NOT edit files directly
- Only coordinators MAY call `comms_release_all()` for emergency release of all reservations

### Coordinator Protocol

1. **Initialize**: `comms_init(project_path=".", task_description="...")`
2. **Check learnings**: `hivemind_find(query="<task keywords>")`
Expand All @@ -21,23 +27,27 @@ Patterns for coordinating parallel agent work.
9. **Complete**: `forge_complete(bead_id, summary, files_touched)`
10. **Learn**: `hivemind_store(information="...", tags="forge,<topic>")`

## Worker Protocol
## Worker-Only Operations

- Workers MUST reserve files before editing
- Workers MUST use `exclusive=true` when reserving files
- Workers MUST NOT call `comms_release_all()`
- Workers MUST release files when done: `comms_release(paths=[...])`

### Worker Protocol

1. **Initialize**: `comms_init(project_path=".", task_description="...")`
2. **Check learnings**: `hivemind_find(query="<task keywords>")`
3. **Reserve files**: `comms_reserve(paths=[...], reason="...")`
4. **Implement**: Make changes to reserved files
3. **Reserve files**: `comms_reserve(paths=[...], exclusive=true, reason="...")`
If reservation fails:
a. Check who holds the reservation
b. Send a message via `comms_send` to negotiate release
c. Wait for release or escalate to coordinator
4. **Implement**: Make changes to reserved files only
5. **Report progress**: `forge_progress(bead_id, progress_percent, status)`
6. **Store learnings**: `hivemind_store(information="...", tags="...")`
7. **Complete**: `forge_complete(bead_id, summary, files_touched)`

## File Reservation Rules

- Workers MUST reserve files before editing
- Coordinators NEVER reserve files
- Use `comms_reserve(paths=[...], exclusive=true)` for exclusive access
- Release files when done: `comms_release(paths=[...])`
- Emergency release: `comms_release_all()` (coordinator only)
7. **Release files**: `comms_release(paths=[...])`
8. **Complete**: `forge_complete(bead_id, summary, files_touched)`

## Progress Reporting

Expand All @@ -53,10 +63,3 @@ forge_progress(
message="Implemented core logic, starting tests"
)
```

## Conflict Resolution

If a file reservation fails:
1. Check who holds the reservation
2. Send a message via `comms_send` to negotiate
3. Wait for release or escalate to coordinator
105 changes: 105 additions & 0 deletions internal/agentkit/agentkit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,26 @@ import (
"testing"
)

// findRepoRoot walks up from the current working directory to find the
// directory containing go.mod, which is the repository root.
func findRepoRoot(t *testing.T) string {
t.Helper()
dir, err := os.Getwd()
if err != nil {
t.Fatalf("getwd: %v", err)
}
for {
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
return dir
}
parent := filepath.Dir(dir)
if parent == dir {
t.Fatal("could not find repo root (no go.mod found)")
}
dir = parent
}
}

func TestScaffold_FreshDirectory(t *testing.T) {
dir := t.TempDir()
results, err := Scaffold(dir, false)
Expand Down Expand Up @@ -798,6 +818,91 @@ func TestForgeMD_StructuralHardening(t *testing.T) {
})
}

func TestForgeCoordinationSkill_StructuralHardening(t *testing.T) {
data, err := content.ReadFile("content/skills/forge-coordination/SKILL.md")
if err != nil {
t.Fatalf("read embedded forge-coordination SKILL.md: %v", err)
}
text := string(data)

// (1) Role-scoped section headers exist.
roleSections := []string{
"## Coordinator-Only Operations",
"## Worker-Only Operations",
}
for _, sec := range roleSections {
if !strings.Contains(text, sec) {
t.Errorf("forge-coordination SKILL.md: missing role-scoped section %q", sec)
}
}

// (2) MUST/MUST NOT rules appear before protocol steps in each role section.
coordinatorOpsIdx := strings.Index(text, "## Coordinator-Only Operations")
coordinatorProtocolIdx := strings.Index(text, "### Coordinator Protocol")
workerOpsIdx := strings.Index(text, "## Worker-Only Operations")
workerProtocolIdx := strings.Index(text, "### Worker Protocol")

if coordinatorOpsIdx < 0 || coordinatorProtocolIdx < 0 {
t.Fatal("forge-coordination SKILL.md: missing Coordinator sections")
}
if workerOpsIdx < 0 || workerProtocolIdx < 0 {
t.Fatal("forge-coordination SKILL.md: missing Worker sections")
}

// Coordinator MUST NOT rules must be between the section header and the protocol.
coordSection := text[coordinatorOpsIdx:coordinatorProtocolIdx]
coordMustRules := []string{
"MUST NOT reserve files",
"MUST NOT edit files directly",
}
for _, rule := range coordMustRules {
if !strings.Contains(coordSection, rule) {
t.Errorf("forge-coordination SKILL.md: coordinator MUST rule %q not found before Coordinator Protocol", rule)
}
}

// Worker MUST rules must be between the section header and the protocol.
workerSection := text[workerOpsIdx:workerProtocolIdx]
workerMustRules := []string{
"MUST reserve files before editing",
"MUST NOT call `comms_release_all()`",
}
for _, rule := range workerMustRules {
if !strings.Contains(workerSection, rule) {
t.Errorf("forge-coordination SKILL.md: worker MUST rule %q not found before Worker Protocol", rule)
}
}

// (3) exclusive=true appears in the comms_reserve call within Worker Protocol.
workerProtocolSection := text[workerProtocolIdx:]
if !strings.Contains(workerProtocolSection, "exclusive=true") {
t.Error("forge-coordination SKILL.md: Worker Protocol missing exclusive=true in comms_reserve call")
}

// (4) Removed sections from original document are absent.
removedSections := []string{
"## File Reservation Rules",
"## Conflict Resolution",
}
for _, sec := range removedSections {
if strings.Contains(text, sec) {
t.Errorf("forge-coordination SKILL.md: removed section %q should not be present", sec)
}
}

// (5) Both copies are byte-identical (embedded agentkit vs .opencode at repo root).
// Find repo root by walking up from the working directory to locate go.mod.
repoRoot := findRepoRoot(t)
opencodePath := filepath.Join(repoRoot, ".opencode", "skills", "forge-coordination", "SKILL.md")
opencodeCopy, err := os.ReadFile(opencodePath)
if err != nil {
t.Fatalf("read .opencode forge-coordination SKILL.md: %v", err)
}
if string(data) != string(opencodeCopy) {
t.Error("forge-coordination SKILL.md: embedded copy and .opencode copy are not byte-identical")
}
}

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
43 changes: 23 additions & 20 deletions internal/agentkit/content/skills/forge-coordination/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ tags: [forge, coordination, multi-agent]

Patterns for coordinating parallel agent work.

## Coordinator Protocol
## Coordinator-Only Operations

- Coordinators MUST NOT reserve files
Comment thread
jflowers marked this conversation as resolved.
- Coordinators MUST NOT edit files directly
- Only coordinators MAY call `comms_release_all()` for emergency release of all reservations

### Coordinator Protocol

1. **Initialize**: `comms_init(project_path=".", task_description="...")`
2. **Check learnings**: `hivemind_find(query="<task keywords>")`
Expand All @@ -21,23 +27,27 @@ Patterns for coordinating parallel agent work.
9. **Complete**: `forge_complete(bead_id, summary, files_touched)`
10. **Learn**: `hivemind_store(information="...", tags="forge,<topic>")`

## Worker Protocol
## Worker-Only Operations

- Workers MUST reserve files before editing
- Workers MUST use `exclusive=true` when reserving files
- Workers MUST NOT call `comms_release_all()`
Comment thread
jflowers marked this conversation as resolved.
- Workers MUST release files when done: `comms_release(paths=[...])`

### Worker Protocol

1. **Initialize**: `comms_init(project_path=".", task_description="...")`
2. **Check learnings**: `hivemind_find(query="<task keywords>")`
3. **Reserve files**: `comms_reserve(paths=[...], reason="...")`
4. **Implement**: Make changes to reserved files
3. **Reserve files**: `comms_reserve(paths=[...], exclusive=true, reason="...")`
If reservation fails:
a. Check who holds the reservation
b. Send a message via `comms_send` to negotiate release
c. Wait for release or escalate to coordinator
4. **Implement**: Make changes to reserved files only
5. **Report progress**: `forge_progress(bead_id, progress_percent, status)`
6. **Store learnings**: `hivemind_store(information="...", tags="...")`
7. **Complete**: `forge_complete(bead_id, summary, files_touched)`

## File Reservation Rules

- Workers MUST reserve files before editing
- Coordinators NEVER reserve files
- Use `comms_reserve(paths=[...], exclusive=true)` for exclusive access
- Release files when done: `comms_release(paths=[...])`
- Emergency release: `comms_release_all()` (coordinator only)
7. **Release files**: `comms_release(paths=[...])`
Comment thread
jflowers marked this conversation as resolved.
8. **Complete**: `forge_complete(bead_id, summary, files_touched)`

## Progress Reporting

Expand All @@ -53,10 +63,3 @@ forge_progress(
message="Implemented core logic, starting tests"
)
```

## Conflict Resolution

If a file reservation fails:
1. Check who holds the reservation
2. Send a message via `comms_send` to negotiate
3. Wait for release or escalate to coordinator
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: unbound-force
created: 2026-08-02
70 changes: 70 additions & 0 deletions openspec/changes/harden-forge-skill-compression/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
## Context

The `forge-coordination` skill (`internal/agentkit/content/skills/forge-coordination/SKILL.md` and its mirror at `.opencode/skills/forge-coordination/SKILL.md`) encodes critical multi-agent safety constraints. These constraints are currently expressed in low-salience constructs — parenthetical asides, buried bullets, and separate sections — that DCP context compression discards first.

The proposal (constitution-aligned, all principles PASS or N/A) calls for restructuring the document to promote safety-critical constraints to positions that survive compression.

## Goals / Non-Goals

### Goals
- Restructure the skill to use role-scoped sections that clearly separate coordinator-only and worker-only operations
- Inline conflict resolution steps at the point where reservation failure occurs, rather than in a separate section
- Make `exclusive=true` the documented default for `comms_reserve`, not an option to remember
- Ensure the strongest phrasing (MUST/NEVER) appears first and at section-level prominence
- Keep both copies of the file in sync (agentkit embed + opencode skills)

### Non-Goals
- Changing MCP tool behavior or adding new tools
- Modifying Go source code
- Adding tests for MCP tool behavior (structural tests for document shape are in scope per project convention)
- Changing the agentkit embed mechanism
- Restructuring other skills — this change targets forge-coordination only

## Decisions

### 1. Role-scoped sections replace mixed bullet lists

**Decision**: Replace the flat "File Reservation Rules" section with two explicit sections: "Coordinator-Only Operations" and "Worker-Only Operations." Each section states what that role MUST and MUST NOT do.

**Rationale**: Section headers are high-salience constructs that survive compression. A parenthetical like `(coordinator only)` is exactly the kind of qualifier DCP drops. Promoting it to a section header makes it structurally impossible to compress away without losing the entire section.

### 2. Inline conflict resolution at point of use

**Decision**: Move the conflict resolution steps from a separate section into the Worker Protocol, immediately after the "Reserve files" step.

**Rationale**: When compression removes "less important" sections, a separate "Conflict Resolution" section is a candidate for removal. If it's inlined at the step where reservation failure occurs, it's part of the protocol flow and survives as long as the protocol itself does.

### 3. `exclusive=true` as the documented default

**Decision**: Change the `comms_reserve` call in the Worker Protocol to include `exclusive=true` directly: `comms_reserve(paths=[...], exclusive=true, reason="...")`.

**Rationale**: When the parameter appears in the primary protocol step, agents copy it by default. When it's a separate bullet explaining an option, it's an optimization to drop.

### 4. Strongest constraint phrasing in primary position

**Decision**: Each role section opens with its MUST/MUST NOT rules as the first lines, before any protocol steps.

**Rationale**: First-position content in a section is the last thing compression removes. Burying MUST rules after descriptive text makes them candidates for trimming.

### 5. Worker Protocol expanded from 7 to 8 steps

**Decision**: Insert an explicit "Release files" step (step 7) into the Worker Protocol and renumber "Complete" from step 7 to step 8.

**Rationale**: The original 7-step protocol omitted an explicit file release step, relying on agents to infer cleanup. Making release explicit (a) ensures file reservations are freed before the completion signal and (b) positions the `comms_release` call at the point of use rather than as a separate rule to remember. This is an intentional behavioral modification that strengthens the protocol.

### 6. Both files updated identically

**Decision**: Both `internal/agentkit/content/skills/forge-coordination/SKILL.md` and `.opencode/skills/forge-coordination/SKILL.md` receive the same content.

**Rationale**: These files are currently identical. One is embedded into the binary for scaffolding; the other is loaded by opencode at runtime. Divergence would create inconsistent agent behavior.

## Risks / Trade-offs

### Risk: Increased document length
The restructured document will be slightly longer due to inlining and explicit role sections. This is an acceptable trade-off — a longer document with redundant safety constraints is better than a shorter document where safety constraints are compressed away.

### Risk: Divergence between the two file copies
Both files must be updated identically. The implementation should update one, then copy to the other, to minimize divergence risk. Existing agentkit embed tests will catch if the embedded copy is missing or malformed.

### Trade-off: Redundancy vs. DRY
Some constraints will appear in multiple places (e.g., "Workers MUST reserve files" appears in both the Worker Protocol steps and the Worker-Only Operations rules). This intentional redundancy ensures the constraint survives even if one occurrence is compressed.
62 changes: 62 additions & 0 deletions openspec/changes/harden-forge-skill-compression/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
## Why

The `forge-coordination` skill contains critical access-control and operational constraints expressed as parenthetical asides, weak bullet items, and separated sections. DCP context compression drops these low-salience constructs first, causing agents to lose safety-critical rules:

- `(coordinator only)` parenthetical on `comms_release_all()` — compressed away, workers could release all reservations system-wide
- `exclusive=true` as one bullet in a list — omitted, workers call `comms_reserve` without exclusivity, enabling concurrent edits
- Conflict resolution steps separated from the point of failure — compressed to "resolve conflicts," skipping negotiation
- Duplicate constraints at different strength levels across files — compressor may pick the weakest phrasing

References: [unbound-force/replicator#48](https://github.com/unbound-force/replicator/issues/48), [unbound-force/unbound-force#346](https://github.com/unbound-force/unbound-force/issues/346)

## What Changes

Restructure the `forge-coordination` skill to survive DCP context compression by promoting critical constraints to high-salience positions (section headers, MUST rules at section top, inline at point-of-use).

## Capabilities

### New Capabilities
- None

### Modified Capabilities
- `forge-coordination skill`: Restructured to use explicit role-scoped sections ("Coordinator-Only Operations", "Worker-Only Operations"), inline conflict resolution at point of use, strongest constraint phrasing in primary position, and `exclusive=true` as the documented default

### Removed Capabilities
- None

## Impact

- **File**: `internal/agentkit/content/skills/forge-coordination/SKILL.md` (embedded agentkit copy)
- **File**: `.opencode/skills/forge-coordination/SKILL.md` (opencode skills copy)
- Both files must stay in sync — they are currently identical

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: This says "No behavioral changes to MCP tools or Go code" and "No test changes required," but design decision D5 adds a new step 7 ("Release files") to the Worker Protocol, expanding it from 7 to 8 steps. The design doc acknowledges this as "an intentional behavioral modification." Update this section to reflect the protocol step addition — same approach PR #54 used for its 7th rule codification.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — the Impact section was stale after D5 was added. Updated in ff0f9e5:

  • "No behavioral changes to MCP tools or Go code" → replaced with explicit acknowledgment of the Worker Protocol expansion (7→8 steps, design decision D5)
  • "No test changes required" → replaced with reference to the structural test (TestForgeCoordinationSkill_StructuralHardening)

The Impact section now accurately reflects both the protocol step addition and the test addition.

- The Worker Protocol is expanded from 7 to 8 steps (explicit "Release files" step added as step 7; see design decision D5) — this is an intentional behavioral modification to the protocol, not a Go code or MCP tool change
- A structural test (`TestForgeCoordinationSkill_StructuralHardening`) validates compression-critical patterns survive editing
- Agents loading this skill will receive compression-resistant constraints

## Constitution Alignment

Assessed against the Unbound Force org constitution.

### I. Autonomous Collaboration

**Assessment**: PASS

This change strengthens artifact-based communication by ensuring the skill document (an artifact consumed by autonomous agents) retains its critical constraints under compression. The change maintains self-describing outputs and does not alter inter-agent communication protocols.

### II. Composability First

**Assessment**: N/A

No dependencies are introduced or removed. The skill file remains a standalone document. No changes to binary functionality or Dewey integration.

### III. Observable Quality

**Assessment**: N/A

No MCP tool responses or machine-parseable outputs are changed. This is a prompt document restructuring.

### IV. Testability

**Assessment**: N/A

No testable components are added or modified. The change targets prompt content only. Existing agentkit embed tests continue to verify the file is properly embedded.
Loading