Skip to content

feat!: enforce delegation contract on forge_spawn_subtask - #144

Merged
jflowers merged 2 commits into
unbound-force:mainfrom
jflowers:opsx/spawn-subtask-skill-enforcement
Oct 26, 2026
Merged

jflowers merged 2 commits into
unbound-force:mainfrom
jflowers:opsx/spawn-subtask-skill-enforcement

Conversation

@jflowers

Copy link
Copy Markdown
Contributor

Summary

forge_spawn_subtask now enforces the delegation contract established by the parent change uf_delegate_command (unbound-force/unbound-force#787), closing the silent-omission loophole where subtasks could be spawned with improvised procedures.

  • Breaking: task_classification is required (deterministic | judgment) on every spawn; callers omitting it fail closed with a self-describing error naming the parameter and accepted values.
  • Deterministic subtasks must supply an existence-verified skill reference (1-256 chars) or a recorded skill_absent_reason (1-512 chars) — never both. Judgment subtasks must omit both.
  • Unmet reference requirements fail with SKILL_REFERENCE_REQUIRED; unresolvable references fail with 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 embedding the same fixed directive.
  • Skill resolution verifies .opencode/skills/<name>/SKILL.md then .opencode/skill/<name>/SKILL.md (first root wins); /-containing refs resolve repo-relative with a post-clean traversal guard; absolute and ..-escaping paths are rejected.

Agent kit (coordinator agent, /forge command, forge-coordination skill), repo-level mirrors, docs/tools.md, and the CHANGELOG BREAKING entry all ship in the same change. Cross-repo follow-ups: unbound-force/unbound-force#830 (uf.unleash template update), unbound-force/website#329 (website docs).

Spec: openspec/changes/spawn-subtask-skill-enforcement/

Fixes #143

How to Test

make check
go test ./internal/forge/... ./internal/tools/forge/... -count=1 -race -v

Acceptance scenarios live in openspec/changes/spawn-subtask-skill-enforcement/specs/spawn-delegation-contract/spec.md; every scenario maps to a test. Spot-checks at the MCP boundary (make serve):

  1. Call forge_spawn_subtask without task_classification → error task_classification is required (accepted values: deterministic, judgment).
  2. task_classification: "deterministic" with neither skill nor skill_absent_reason → SKILL_REFERENCE_REQUIRED.
  3. task_classification: "deterministic", skill: "forge-coordination" → ready_to_spawn envelope with resolved path + skill_directive.
  4. task_classification: "judgment" with skill → input-validation error ("valid only for deterministic").
  5. skill: "../secret.sh" → rejected as traversal before any filesystem access.

How to Demo

Run the MCP server (make serve), then as a coordinator agent: attempt a spawn with the old 4-argument shape and observe the self-describing failure; retry with task_classification + skill: "forge-coordination" and show the envelope's audit fields.

Key Files Changed

File Change
internal/forge/spawn.go SpawnParams, contract validation, ResolveSkillRef (dual roots + traversal guard), SkillDirective, sentinel errors, stat-error taxonomy
internal/forge/spawn_test.go Ported and deepened: prompt goldens, boundary values, traversal fixtures, error-message assertions
internal/tools/forge/tools.go Tool schemas + descriptions; pointer-shadow omitted-vs-empty detection
internal/tools/forge/tools_test.go New tool-layer schema/validation/envelope tests
internal/agentkit/content/ + agentkit_test.go Agent-kit contract docs + marker test
.opencode/commands/forge.md, .opencode/skills/forge-coordination/SKILL.md Repo-level mirrors synced byte-identical
docs/tools.md Regenerated
CHANGELOG.md BREAKING entry under Unreleased
openspec/changes/spawn-subtask-skill-enforcement/ Spec artifacts

Known Issues

The review council (9 reviewers, 2 iterations) returned APPROVE with these acknowledged-but-deferred advisories:

  • LOW: "Violations fail closed with SKILL_REFERENCE_REQUIRED or SKILL_NOT_FOUND" wording in tool description/CHANGELOG is over-broad — structural violations fail with plain input-validation errors
  • LOW: forge-coordination SKILL.md fuses the skill_directive forwarding rule into the judgment bullet (field only exists for deterministic spawns)
  • LOW: SpawnSubtask GoDoc says stat errors are "returned unwrapped" — they are context-wrapped (non-sentinel)
  • LOW: no drift guard for .opencode/commands/*.md mirrors (pre-existing; fixed here by manual sync)
  • LOW: test polish: field-specific assertions, ProjectPath anchor in boundary test, root-run skip guard, schema type pins
  • LOW: accepted hardening follow-ups: symlink containment, Windows backslash rejection, whitespace-only reason, forge_subtask_prompt skill verification

This PR was generated by /uf.finale (AI-assisted).

@jflowers
jflowers requested a review from a team as a code owner October 25, 2026 22:42
@jflowers jflowers self-assigned this Oct 25, 2026
- Require task_classification (deterministic or judgment) on every
  spawn; deterministic subtasks must supply an existence-verified
  skill reference or a recorded skill_absent_reason (never both);
  judgment subtasks must omit both
- Fail closed with SKILL_REFERENCE_REQUIRED or SKILL_NOT_FOUND on
  missing references and unresolvable skills
- Envelope gains task_classification, resolved skill path,
  skill_absent_reason, and the tool-rendered skill_directive;
  forge_subtask_prompt gains an optional skill parameter
- Update embedded agent kit, repo-level mirrors, docs/tools.md,
  CHANGELOG, and OpenSpec change artifacts

Fixes unbound-force#143

Assisted-by: glm
Generated with AI assistance (glm)
CI MegaLinter enables gosec, which rejects os.WriteFile permissions
above 0600 (G306). Both test helpers write fixtures into t.TempDir,
so 0600 is equivalent.

Assisted-by: glm
Generated with AI assistance (glm)
@jflowers
jflowers force-pushed the opsx/spawn-subtask-skill-enforcement branch from 22f08c2 to 35e71d3 Compare October 25, 2026 22:48
@jflowers
jflowers merged commit 2cc4d6b into unbound-force:main Oct 26, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

forge spawn-subtask: parameter-level task classification and skill/script reference validation (delegation contract parity)

2 participants