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
75 changes: 33 additions & 42 deletions .opencode/commands/forge.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,20 @@
description: Decompose task into subtasks and coordinate parallel agents
---

<protect>
# /forge

Decompose a task and spawn parallel workers.

## Critical Invariants

These rules are non-negotiable and MUST NOT be skipped:

- **Review MUST complete before marking work done** — step 7 (review) MUST finish before step 8 (complete). NEVER skip review.
- **The review gate is mandatory** — `forge_complete` MUST NOT be called until `forge_review` has passed for every worker. NEVER bypass the review gate.
- ALWAYS create a forge, even for small tasks.
- Coordinator orchestrates, workers execute — workers MUST NOT call `forge_complete`.

## Task

$ARGUMENTS
Expand All @@ -15,53 +25,34 @@ $ARGUMENTS
1. Initialize comms: `comms_init(project_path=".", task_description="Forge: <task>")`
2. Check prior learnings: `hivemind_find(query="<task keywords>")`
3. Decompose: `forge_decompose(task="<task>", context="<learnings>")`
- Before decomposing, check historical success rates: `forge_get_strategy_insights(task="<task>")`
- Choose from: `file-based`, `feature-based`, `risk-based`, or `auto`
4. Create epic: `org_create_epic(epic_title="<task>", subtasks=[...])`
5. For each subtask: `forge_spawn_subtask(bead_id, epic_id, subtask_title, files)`
5. For each subtask: `forge_spawn_subtask(bead_id, epic_id, subtask_title, files, task_classification, skill | skill_absent_reason)`
- `task_classification` is required: `deterministic` or `judgment`
- `deterministic` subtasks require a `skill` reference (verified skill name or repo-relative script path) OR a `skill_absent_reason` — never both
- `judgment` subtasks must omit both `skill` and `skill_absent_reason` — supplying either fails input validation
- Unverifiable references fail closed (`SKILL_NOT_FOUND`); missing both fails with `SKILL_REFERENCE_REQUIRED`
- Pass `project_path` when spawning against a repo other than the current directory
- Forward the envelope's `skill_directive` verbatim into the worker prompt (or pass the same `skill` to `forge_subtask_prompt`); never write skill instructions yourself
6. Monitor: check `comms_inbox()` every few minutes
7. Review: `forge_review(task_id, files_touched)` for each completed worker
8. Complete: `forge_complete(bead_id, summary, files_touched)`
9. Store learnings: `hivemind_store(information="...", tags="forge,<topic>")`
- `comms_inbox()` — check for messages from workers
- `forge_status(epic_id, project_key)` — check worker progress
- `org_cells(status="in_progress")` — see active cells
- If a worker is blocked: read the message with `comms_read_message(message_id)`, acknowledge with `comms_ack(message_id)`, then either unblock or reassign the subtask
7. Review FIRST (before complete): `forge_review(task_id, files_touched)` for each completed worker — MUST finish before step 8
8. Complete (after ALL reviews pass):
- `forge_complete(bead_id, summary, files_touched)` — mark epic done
- `forge_record_outcome(bead_id, duration_ms, success)` — record for learning
- `hivemind_store(information="...", tags="forge,<topic>")` — store learnings
- `org_sync()` — persist state to git

## Rules

- Always create a forge, even for small tasks
- Coordinator orchestrates, workers execute
- Review every worker's output before marking complete — NEVER skip this step
- ALWAYS create a forge, even for small tasks
- Coordinator orchestrates, workers execute — workers MUST NOT call `forge_complete`
- Workers reserve their own files via `comms_reserve`
- Check inbox regularly for blocked workers
- Review every worker's output before marking complete
- Store learnings after completion

## Strategy Selection

Before decomposing, check historical success rates:

```
forge_get_strategy_insights(task="<task>")
```

Choose from: `file-based`, `feature-based`, `risk-based`, or `auto`.

## Monitoring

While workers are active:

1. `comms_inbox()` — check for messages from workers
2. `forge_status(epic_id, project_key)` — check worker progress
3. `org_cells(status="in_progress")` — see active cells

## Completion

After all workers finish:

1. `forge_complete(bead_id, summary, files_touched)` — mark epic done
2. `forge_record_outcome(bead_id, duration_ms, success)` — record for learning
3. `hivemind_store(information="...", tags="forge,<topic>")` — store learnings
4. `org_sync()` — persist state to git

## Error Recovery

If a worker is blocked:

1. Read the worker's message: `comms_read_message(message_id)`
2. Acknowledge: `comms_ack(message_id)`
3. Either unblock or reassign the subtask
</protect>
5 changes: 4 additions & 1 deletion .opencode/skills/forge-coordination/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ Patterns for coordinating parallel agent work.
3. **Select strategy**: `forge_get_strategy_insights(task="<task>")`
4. **Decompose**: `forge_decompose(task="<task>", context="<learnings>")`
5. **Create epic**: `org_create_epic(epic_title="<task>", subtasks=[...])`
6. **Spawn workers**: `forge_spawn_subtask(bead_id, epic_id, subtask_title, files)`
6. **Spawn workers**: `forge_spawn_subtask(bead_id, epic_id, subtask_title, files, task_classification, skill | skill_absent_reason)`
- `task_classification` is required (`deterministic` | `judgment`)
- `deterministic` subtasks must supply a verified `skill` reference or a `skill_absent_reason` (mutually exclusive); spawn fails closed with `SKILL_REFERENCE_REQUIRED` / `SKILL_NOT_FOUND`
- `judgment` subtasks must omit both `skill` and `skill_absent_reason`; forward the envelope's `skill_directive` verbatim (or pass the same `skill` to `forge_subtask_prompt`) — never write skill instructions yourself
7. **Monitor**: `comms_inbox()` + `forge_status(epic_id, project_key)`
8. **Review**: `forge_review(task_id, files_touched)` for each worker
9. **Complete**: `forge_complete(bead_id, summary, files_touched)`
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,19 @@ this project adheres to [Semantic Versioning](https://semver.org/).
(Part of unbound-force/unbound-force#428)
- Slash commands migrated to `uf.*` namespace (e.g., `/unleash` is
now `/uf.unleash`, `/cobalt-crush` is now `/uf.cobalt-crush`)
- **BREAKING**: `forge_spawn_subtask` now enforces the delegation
contract: `task_classification` is required (`deterministic` or
`judgment`); deterministic-classified subtasks must supply a
`skill` reference (1-256 chars, existence-verified) or a
`skill_absent_reason` (1-512 chars) — never both; judgment-classified
subtasks must omit both. Violations fail closed with
`SKILL_REFERENCE_REQUIRED` or `SKILL_NOT_FOUND`. The success
envelope gains `task_classification`, the resolved skill path,
`skill_absent_reason`, and the tool-rendered `skill_directive`;
`forge_subtask_prompt` gains an optional `skill` parameter that
embeds the same fixed directive.
(Fixes [#143](https://github.com/unbound-force/replicator/issues/143))
- Spec: `openspec/changes/spawn-subtask-skill-enforcement/specs/spawn-delegation-contract/spec.md`

### Security
- Minimum Go toolchain raised to 1.26.9 to resolve reachable standard-library
Expand Down
31 changes: 28 additions & 3 deletions docs/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -1032,7 +1032,7 @@ Analyze task and recommend decomposition strategy.

### `forge_spawn_subtask`

Prepare a subtask for spawning with Task tool.
Prepare a subtask for spawning with Task tool. Delegation contract: task_classification is required (deterministic|judgment); deterministic-classified subtasks must supply a verified skill/script reference (skill) or a recorded skill_absent_reason (mutually exclusive); judgment-classified subtasks must omit both. Violations fail closed with SKILL_REFERENCE_REQUIRED or SKILL_NOT_FOUND.

```json
{
Expand All @@ -1049,21 +1049,43 @@ Prepare a subtask for spawning with Task tool.
},
"type": "array"
},
"project_path": {
"default": ".",
"type": "string"
},
"shared_context": {
"type": "string"
},
"skill": {
"maxLength": 256,
"minLength": 1,
"type": "string"
},
"skill_absent_reason": {
"maxLength": 512,
"minLength": 1,
"type": "string"
},
"subtask_description": {
"type": "string"
},
"subtask_title": {
"type": "string"
},
"task_classification": {
"enum": [
"deterministic",
"judgment"
],
"type": "string"
}
},
"required": [
"bead_id",
"epic_id",
"subtask_title",
"files"
"files",
"task_classification"
],
"type": "object"
}
Expand Down Expand Up @@ -1093,7 +1115,7 @@ Get status of a swarm by epic ID.

### `forge_subtask_prompt`

Generate the prompt for a spawned subtask agent.
Generate the prompt for a spawned subtask agent. When skill is supplied, the fixed skill directive is embedded in the prompt.

```json
{
Expand All @@ -1116,6 +1138,9 @@ Generate the prompt for a spawned subtask agent.
"shared_context": {
"type": "string"
},
"skill": {
"type": "string"
},
"subtask_description": {
"type": "string"
},
Expand Down
35 changes: 35 additions & 0 deletions internal/agentkit/agentkit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,41 @@ func TestForgeMD_StructuralHardening(t *testing.T) {
})
}

// TestAgentKit_DelegationContractDemonstrated verifies the embedded
// kit demonstrates the forge_spawn_subtask delegation contract:
// the coordinator agent, the /forge command, and the
// forge-coordination skill all show task_classification, the
// reference-or-reason rule, the judgment-side prohibition, and the
// tool-rendered skill_directive forwarding rule.
func TestAgentKit_DelegationContractDemonstrated(t *testing.T) {
cases := []struct {
path string
}{
{"content/agents/coordinator.md"},
{"content/commands/forge.md"},
{"content/skills/forge-coordination/SKILL.md"},
}
for _, tc := range cases {
data, err := content.ReadFile(tc.path)
if err != nil {
t.Fatalf("read %s: %v", tc.path, err)
}
text := string(data)
for _, needle := range []string{
"forge_spawn_subtask",
"task_classification",
"skill_absent_reason",
"SKILL_REFERENCE_REQUIRED",
"omit both",
"skill_directive",
} {
if !strings.Contains(text, needle) {
t.Errorf("%s: delegation contract marker %q not found", tc.path, needle)
}
}
}
}

func TestForgeCoordinationSkill_StructuralHardening(t *testing.T) {
data, err := content.ReadFile("content/skills/forge-coordination/SKILL.md")
if err != nil {
Expand Down
11 changes: 10 additions & 1 deletion internal/agentkit/content/agents/coordinator.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,16 @@ You orchestrate workers but NEVER reserve files or edit code directly. You decom

1. Initialize comms (`comms_init`)
2. Decompose task via `forge_decompose` or `forge_plan_prompt`
3. Spawn workers with `forge_spawn_subtask`
3. Spawn workers with `forge_spawn_subtask` — every call MUST pass
`task_classification` (`deterministic` or `judgment`); a
`deterministic` subtask additionally requires either a verified
`skill` reference (skill name or repo-relative script path) or a
recorded `skill_absent_reason` (mutually exclusive). Spawn fails
closed with `SKILL_REFERENCE_REQUIRED` / `SKILL_NOT_FOUND`
otherwise. A `judgment` subtask MUST omit both `skill` and
`skill_absent_reason`. Forward the envelope's `skill_directive`
verbatim into the worker prompt (or pass the same `skill` to
`forge_subtask_prompt`); never write skill instructions yourself.
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`)
Expand Down
8 changes: 7 additions & 1 deletion internal/agentkit/content/commands/forge.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ $ARGUMENTS
- Before decomposing, check historical success rates: `forge_get_strategy_insights(task="<task>")`
- Choose from: `file-based`, `feature-based`, `risk-based`, or `auto`
4. Create epic: `org_create_epic(epic_title="<task>", subtasks=[...])`
5. For each subtask: `forge_spawn_subtask(bead_id, epic_id, subtask_title, files)`
5. For each subtask: `forge_spawn_subtask(bead_id, epic_id, subtask_title, files, task_classification, skill | skill_absent_reason)`
- `task_classification` is required: `deterministic` or `judgment`
- `deterministic` subtasks require a `skill` reference (verified skill name or repo-relative script path) OR a `skill_absent_reason` — never both
- `judgment` subtasks must omit both `skill` and `skill_absent_reason` — supplying either fails input validation
- Unverifiable references fail closed (`SKILL_NOT_FOUND`); missing both fails with `SKILL_REFERENCE_REQUIRED`
- Pass `project_path` when spawning against a repo other than the current directory
- Forward the envelope's `skill_directive` verbatim into the worker prompt (or pass the same `skill` to `forge_subtask_prompt`); never write skill instructions yourself
6. Monitor: check `comms_inbox()` every few minutes
- `comms_inbox()` — check for messages from workers
- `forge_status(epic_id, project_key)` — check worker progress
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ Patterns for coordinating parallel agent work.
3. **Select strategy**: `forge_get_strategy_insights(task="<task>")`
4. **Decompose**: `forge_decompose(task="<task>", context="<learnings>")`
5. **Create epic**: `org_create_epic(epic_title="<task>", subtasks=[...])`
6. **Spawn workers**: `forge_spawn_subtask(bead_id, epic_id, subtask_title, files)`
6. **Spawn workers**: `forge_spawn_subtask(bead_id, epic_id, subtask_title, files, task_classification, skill | skill_absent_reason)`
- `task_classification` is required (`deterministic` | `judgment`)
- `deterministic` subtasks must supply a verified `skill` reference or a `skill_absent_reason` (mutually exclusive); spawn fails closed with `SKILL_REFERENCE_REQUIRED` / `SKILL_NOT_FOUND`
- `judgment` subtasks must omit both `skill` and `skill_absent_reason`; forward the envelope's `skill_directive` verbatim (or pass the same `skill` to `forge_subtask_prompt`) — never write skill instructions yourself
7. **Monitor**: `comms_inbox()` + `forge_status(epic_id, project_key)`
8. **Review**: `forge_review(task_id, files_touched)` for each worker
9. **Complete**: `forge_complete(bead_id, summary, files_touched)`
Expand Down
Loading
Loading