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
60 changes: 60 additions & 0 deletions internal/agentkit/agentkit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,66 @@ func TestScaffold_FileCount(t *testing.T) {
}
}

func TestHandoffMD_StructuralHardening(t *testing.T) {
data, err := content.ReadFile("content/commands/handoff.md")
if err != nil {
t.Fatalf("read embedded handoff.md: %v", err)
}
text := string(data)

// (1) Ordering constraint text appears before the first numbered step.
const orderingConstraint = "Steps MUST execute in this exact order"
constraintIdx := strings.Index(text, orderingConstraint)
if constraintIdx < 0 {
t.Error("handoff.md: missing ordering constraint text")
}
// Find first numbered step (line starting with "0." or "1.").
firstStepIdx := strings.Index(text, "\n0.")
if firstStepIdx < 0 {
firstStepIdx = strings.Index(text, "\n1.")
}
if firstStepIdx < 0 {
t.Error("handoff.md: no numbered workflow steps found")
} else if constraintIdx >= firstStepIdx {
t.Error("handoff.md: ordering constraint must appear before the first numbered step")
}

// (2) Handoff note categories appear within the org_session_end step section.
sessionEndIdx := strings.Index(text, "org_session_end")
if sessionEndIdx < 0 {
t.Fatal("handoff.md: missing org_session_end reference")
}
afterSessionEnd := text[sessionEndIdx:]

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] Category co-location check could be stronger

afterSessionEnd is everything from org_session_end to the end of the file. If the categories appeared in a completely different section that happens to follow org_session_end, this would still pass. The spec requires co-location "within the same markdown section."

A stronger assertion would find the next ## heading after org_session_end and verify categories appear before it. Not blocking — the test is directionally correct.

categories := []string{"Completed", "In Progress", "Blocked", "Next Steps", "Gotchas"}
for _, cat := range categories {
if !strings.Contains(afterSessionEnd, cat) {
t.Errorf("handoff.md: handoff note category %q not found after org_session_end", cat)
}
}

// (3) Separate "## Handoff Note Template" section is absent.
if strings.Contains(text, "## Handoff Note Template") {
t.Error("handoff.md: separate '## Handoff Note Template' section should be removed")
}

// (4) Forge precondition check text is present.
if !strings.Contains(text, "Forge precondition check") {
t.Error("handoff.md: missing forge precondition check step")
}

// (5) Step dependency rationale is present for each step (2-5).
for _, dep := range []string{
"Depends on step 1",
"Depends on step 2",
"Depends on step 3",
"Depends on step 4",
} {
if !strings.Contains(text, dep) {
t.Errorf("handoff.md: missing dependency rationale %q", dep)
}
}
}

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
51 changes: 39 additions & 12 deletions internal/agentkit/content/commands/handoff.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,45 @@ Wrap up a session cleanly.

## Workflow

1. Summarize completed work and open blockers
2. `comms_release_all()` — free all file reservations
3. `org_update()` / `org_close()` — update cell statuses
4. `org_sync()` — persist state to git
5. `org_session_end(handoff_notes="...")` — save handoff for next session
Steps MUST execute in this exact order -- do not reorder
or parallelize.

## Handoff Note Template
0. **Forge precondition check** -- SHOULD check for
active forge workers before proceeding. If workers
are active, warn the user and request confirmation
before releasing reservations. If forge tools are
unavailable or you have no active forge context
(no known epic_id/project_key), skip this check.

Include in your handoff notes:
1. **Summarize completed work and open blockers** --
do this first so you have full awareness of session
state before releasing anything.

- **Completed**: What tasks were finished
- **In Progress**: What was started but not finished
- **Blocked**: What is waiting on external input
- **Next Steps**: What the next agent should do first
- **Gotchas**: Any surprises or edge cases discovered
2. `comms_release_all()` -- free all file reservations.
Depends on step 1: you need the summary to know what
was reserved and whether any reservations are still
needed.

3. `org_update()` / `org_close()` -- update cell
statuses. Depends on step 2: reservations must be
released before updating cell state to avoid stale
lock references.

4. `org_sync()` -- persist state to git.
Depends on step 3: cell statuses must be final
before syncing to avoid persisting intermediate
state.

5. `org_session_end(handoff_notes="...")` -- save
handoff for next session. Depends on step 4: sync
must complete before ending the session so the
handoff reflects the final persisted state. Structure
your handoff notes as follows:

```
- Completed: What tasks were finished
- In Progress: What was started but not finished
- Blocked: What is waiting on external input
- Next Steps: What the next agent should do first
- Gotchas: Any surprises or edge cases discovered
```
2 changes: 2 additions & 0 deletions openspec/changes/harden-handoff-dcp/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: unbound-force
created: 2026-08-02
56 changes: 56 additions & 0 deletions openspec/changes/harden-handoff-dcp/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
## Context

The `/handoff` command (`internal/agentkit/content/commands/handoff.md`) is a 25-line embedded prompt that instructs agents to perform a 5-step session teardown. The current structure uses a numbered list for step ordering and a separate section for the handoff note template. Under DCP context compression, numbered lists can be reordered or merged, and standalone template sections can be dropped entirely.

The related issue [unbound-force/unbound-force#346](https://github.com/unbound-force/unbound-force/issues/346) demonstrated that `/review-pr`'s confirmation gate was bypassed under the same compression conditions. The fix there added explicit "MANDATORY GATE" markers and session-resume guards -- patterns we adapt here.

## Goals / Non-Goals

### Goals
- Make step ordering survive DCP context compression by adding explicit ordering constraints
- Embed the handoff note template directly in the `org_session_end` step so it cannot be separated from the action
- Add a forge precondition check that prevents releasing reservations while workers are active
- Keep the prompt concise -- hardening should add minimal token overhead

### Non-Goals
- Changing the `org_session_end` MCP tool itself (the tool is fine; the prompt is the issue)
- Adding runtime enforcement of step ordering (this is a prompt-level fix, not a code-level fix)
- Restructuring other commands for DCP resilience (that's a separate effort per command)
- Testing DCP compression behavior (compression is non-deterministic and not controllable by tests; structural content assertions verify the hardening properties, while DCP behavior is accepted as a known limitation per R2)

## Decisions

### D1: Inline the template into the tool call step

**Decision**: Move the handoff note template from a separate "Handoff Note Template" section into the `org_session_end` step itself, formatted as a code block showing the expected argument structure.

**Rationale**: When the template is a separate section, compression can drop it while keeping the workflow. When it's part of the step instruction, dropping it means dropping the step -- which is much harder for compression to justify since the step contains a tool call. This aligns with the Autonomous Collaboration principle: the artifact (handoff note) is fully described at the point of creation.

### D2: Add explicit ordering constraint language

**Decision**: Add "Steps MUST execute in this exact order -- do not reorder or parallelize" at the top of the workflow section, and add sequencing rationale to each step.

**Rationale**: Numbered lists are not strong enough ordering signals under compression. Explicit MUST language with rationale for each dependency makes reordering require actively contradicting a stated constraint.

### D3: Forge precondition as SHOULD, not MUST

**Decision**: The forge worker check is a SHOULD ("Check for active forge workers before proceeding") rather than a MUST hard gate.

**Rationale**: Per the Composability First principle, the handoff command must remain functional even when forge tools are unavailable. A MUST gate would break handoff in environments where forge is not configured. The check uses `forge_status` which is an existing MCP tool, maintaining artifact-based coordination.

### D4: Single-file change

**Decision**: All hardening changes are contained within `handoff.md`. No changes to Go source, MCP tools, or other embedded assets.

**Rationale**: This is a prompt-level fix for a prompt-level vulnerability. The underlying tools (`comms_release_all`, `org_session_end`, `forge_status`) function correctly; the issue is that the prompt instructions degrade under compression.

## Risks / Trade-offs

### R1: Prompt length increase
Adding ordering constraints, inline templates, and precondition checks increases the prompt from ~25 lines to ~45-50 lines. This adds token overhead to every session that loads the command. Accepted because the reliability improvement outweighs the marginal token cost.

### R2: Cannot fully prevent compression reordering
No prompt structure can guarantee preservation under aggressive compression. The hardening reduces the probability of step reordering and template loss but does not eliminate it. This is an inherent limitation of prompt-based workflows.

### R3: Forge precondition may produce false positives
If `forge_status` returns stale data (e.g., a worker that crashed without cleanup), the agent may hesitate to proceed with handoff. The SHOULD (not MUST) designation means the agent can proceed with a warning rather than blocking entirely.
61 changes: 61 additions & 0 deletions openspec/changes/harden-handoff-dcp/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
## Why

The `/handoff` command in `internal/agentkit/content/commands/handoff.md` defines a 5-step session teardown workflow where step ordering is critical: summarize before release, release before sync, sync before session end. Under DCP (Dynamic Context Protocol) context compression, three fragilities emerge:

1. **Step ordering lost** -- compression can flatten or reorder the numbered steps, causing an agent to release reservations before summarizing (losing awareness of held files), or sync before closing cells (persisting stale state).
2. **Handoff note template dropped** -- the structured template (Completed, In Progress, Blocked, Next Steps, Gotchas) is exactly the kind of content compression reduces to "write handoff notes," losing critical categories the next session depends on.
3. **No forge precondition check** -- nothing prevents an agent from invoking `/handoff` mid-forge and releasing active worker reservations.

This is the same class of vulnerability as [unbound-force/unbound-force#346](https://github.com/unbound-force/unbound-force/issues/346), where the `/review-pr` command's confirmation gate was bypassed under compressed context.

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

## What Changes

Harden `internal/agentkit/content/commands/handoff.md` against DCP context compression by restructuring the prompt to survive lossy summarization.

## Capabilities

### New Capabilities
- `forge-precondition-guard`: Handoff command checks for active forge workers before releasing reservations

### Modified Capabilities
- `handoff-workflow`: Restructured with DCP-resistant ordering constraints, inline template, and mandatory precondition check

### Removed Capabilities
- None

## Impact

- **File**: `internal/agentkit/content/commands/handoff.md` -- single file change
- **Behavioral**: Agents invoking `/handoff` will now verify no forge workers are active before proceeding, and will produce structured handoff notes even under compressed context
- **Embedded asset**: The file is embedded via `agentkit.go` and distributed with the binary -- a rebuild is required to pick up the change
- **Test**: The agentkit embedding test (`agentkit_test.go`) validates file presence. A new structural content test will verify the hardening properties (ordering constraint placement, template co-location, forge precondition presence) on the embedded file

## Constitution Alignment

Assessed against the Replicator constitution (`.specify/memory/constitution.md`).

### I. Autonomous Collaboration

**Assessment**: PASS

This change improves artifact-based collaboration by ensuring handoff notes remain structured and complete under compression. The precondition check uses the existing `forge_status` tool via MCP, maintaining artifact-based coordination rather than introducing runtime coupling.

### II. Composability First

**Assessment**: N/A

This change modifies an embedded prompt file, not a tool or binary capability. The handoff command remains functional whether forge tools are available or not -- the precondition check is a SHOULD, not a hard gate that blocks session teardown.

### III. Observable Quality

**Assessment**: PASS

The structured handoff note template produces consistently formatted output that downstream consumers (the next session's `org_session_start`) can parse reliably. Moving the template inline with the tool call step eliminates the gap between instruction and execution.

### IV. Testability

**Assessment**: PASS

Structural content assertions verify the hardening properties of the embedded markdown (ordering constraint placement, template co-location, forge precondition presence). DCP compression behavior is inherently untestable and is accepted as a known limitation. No external services or mutable state are involved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
## ADDED Requirements

### Requirement: Handoff step ordering constraint

The `/handoff` command workflow MUST include an explicit ordering constraint statement before the step list. The constraint MUST use RFC 2119 MUST language to prohibit reordering or parallelizing steps.

#### Scenario: Ordering constraint is structurally distinct from step list
- **GIVEN** the hardened `handoff.md` file is read
- **WHEN** the workflow section is examined
- **THEN** the ordering constraint statement MUST appear as a distinct paragraph before the first numbered step in the markdown file

### Requirement: Forge precondition check

The `/handoff` command SHOULD instruct the agent to check for active forge workers before executing `comms_release_all`. If active workers are detected, the agent SHOULD warn the user and request confirmation before proceeding. If the agent has no active forge context (no known `epic_id` or `project_key`), the forge precondition check SHOULD be skipped, same as when forge tools are unavailable.

#### Scenario: Handoff invoked with active forge workers
- **GIVEN** a forge session is active with running workers
- **WHEN** the agent invokes `/handoff`
- **THEN** the agent SHOULD check `forge_status` and warn that active workers will lose their reservations
- **AND** the agent SHOULD request user confirmation before calling `comms_release_all`

#### Scenario: Handoff invoked with no forge activity
- **GIVEN** no forge session is active
- **WHEN** the agent invokes `/handoff`
- **THEN** the agent SHOULD proceed through the workflow without blocking on the forge check

#### Scenario: Forge tools unavailable
- **GIVEN** forge tools are not available in the current MCP session
- **WHEN** the agent invokes `/handoff`
- **THEN** the agent MUST proceed with handoff without the forge check (graceful degradation)

### Requirement: Inline handoff note template

The handoff note template MUST be embedded directly within the `org_session_end` workflow step rather than in a separate section. The template MUST specify all five categories: Completed, In Progress, Blocked, Next Steps, Gotchas.

#### Scenario: Handoff note template is co-located with tool call
- **GIVEN** the hardened `handoff.md` file is read
- **WHEN** the `org_session_end` step is examined
- **THEN** the handoff note template (all five categories) MUST appear within the same markdown section as the `org_session_end` tool call instruction

### Requirement: Step dependency rationale

Each workflow step MUST include a brief rationale explaining why it depends on the previous step completing first.

#### Scenario: Each step contains a dependency rationale
- **GIVEN** the hardened `handoff.md` file is read
- **WHEN** each numbered workflow step is examined
- **THEN** each step MUST contain a rationale clause following the tool call instruction that explains why it depends on the previous step

## MODIFIED Requirements

### Requirement: Workflow section structure

Previously: A numbered list of 5 steps followed by a separate "Handoff Note Template" section.

The workflow section MUST now contain:
1. An ordering constraint header
2. An optional forge precondition check
3. The existing 5 steps with inline rationale for each dependency
4. The handoff note template embedded within step 5 (`org_session_end`)

The separate "Handoff Note Template" section MUST be removed.

## REMOVED Requirements

None.
30 changes: 30 additions & 0 deletions openspec/changes/harden-handoff-dcp/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!--
[P] marks tasks eligible for parallel execution.
Add [P] when a task: (a) touches different files from
other [P] tasks in the group, (b) has no dependency
on prior tasks in the group, (c) can safely execute
without ordering constraints.
Do NOT add [P] when tasks modify the same file --
parallel workers will cause merge conflicts.
Tasks without [P] run sequentially first, then [P]
tasks run in parallel.
-->

## 1. Harden handoff.md

All tasks in this group modify the same file (`internal/agentkit/content/commands/handoff.md`), so none are parallel-eligible.

- [x] 1.1 Add ordering constraint header to the workflow section: insert "Steps MUST execute in this exact order -- do not reorder or parallelize" before the numbered step list
- [x] 1.2 Add forge precondition check before the existing step 1 (renumber existing steps accordingly): "SHOULD check for active forge workers. If workers are active, warn and request confirmation before proceeding. If forge tools are unavailable or the agent has no active forge context, skip this check."
- [x] 1.3 Add step dependency rationale to each workflow step: brief inline explanation of why each step depends on the previous one completing first (e.g., "summarize first so you know what to report in handoff notes")
- [x] 1.4 Inline the handoff note template into the `org_session_end` step: move the 5-category template (Completed, In Progress, Blocked, Next Steps, Gotchas) from the separate "Handoff Note Template" section into the step 5 instruction, formatted as a code block showing the expected `handoff_notes` argument structure
- [x] 1.5 Remove the separate "## Handoff Note Template" section (currently the last section in the file)

## 2. Verify

- [x] 2.1 Run `make build` to verify the embedded asset compiles cleanly
- [x] 2.2 Run `make test` to verify agentkit embedding tests pass (file presence and prefix matching)
- [x] 2.3 Add a structural content test in `internal/agentkit/agentkit_test.go` that reads the embedded `handoff.md` and asserts: (1) ordering constraint text appears before the first numbered step, (2) the handoff note categories (Completed, In Progress, Blocked, Next Steps, Gotchas) appear within the `org_session_end` step section, (3) the separate "## Handoff Note Template" section is absent, (4) the forge precondition check text is present
- [x] 2.4 Verify constitution alignment: confirm the forge check uses SHOULD (not MUST), no new runtime dependencies are introduced, and the handoff note template is co-located with the tool call instruction
<!-- spec-review: passed -->
<!-- code-review: passed -->