From 0891095c6deca3d8d14245ad795d75d8f7376bbb Mon Sep 17 00:00:00 2001 From: Jay Flowers Date: Sun, 2 Aug 2026 16:03:52 -0400 Subject: [PATCH] fix: harden /handoff command against DCP context compression - Add explicit step ordering constraint (MUST language) - Add forge precondition check (SHOULD, graceful degradation) - Add per-step dependency rationale to resist reordering - Inline handoff note template into org_session_end step - Remove separate "Handoff Note Template" section - Add structural content test (5 assertions) - Add OpenSpec change artifacts (proposal, design, spec, tasks) Fixes #50 Assisted-by: claude-opus Generated with AI assistance (claude-opus) --- internal/agentkit/agentkit_test.go | 60 +++++++++++++++++ internal/agentkit/content/commands/handoff.md | 51 ++++++++++---- .../changes/harden-handoff-dcp/.openspec.yaml | 2 + openspec/changes/harden-handoff-dcp/design.md | 56 ++++++++++++++++ .../changes/harden-handoff-dcp/proposal.md | 61 +++++++++++++++++ .../specs/handoff-dcp-hardening.md | 66 +++++++++++++++++++ openspec/changes/harden-handoff-dcp/tasks.md | 30 +++++++++ 7 files changed, 314 insertions(+), 12 deletions(-) create mode 100644 openspec/changes/harden-handoff-dcp/.openspec.yaml create mode 100644 openspec/changes/harden-handoff-dcp/design.md create mode 100644 openspec/changes/harden-handoff-dcp/proposal.md create mode 100644 openspec/changes/harden-handoff-dcp/specs/handoff-dcp-hardening.md create mode 100644 openspec/changes/harden-handoff-dcp/tasks.md diff --git a/internal/agentkit/agentkit_test.go b/internal/agentkit/agentkit_test.go index f8d59b2..66bb1da 100644 --- a/internal/agentkit/agentkit_test.go +++ b/internal/agentkit/agentkit_test.go @@ -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:] + 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: " field in its YAML frontmatter. diff --git a/internal/agentkit/content/commands/handoff.md b/internal/agentkit/content/commands/handoff.md index 5cc5707..9f1ce29 100644 --- a/internal/agentkit/content/commands/handoff.md +++ b/internal/agentkit/content/commands/handoff.md @@ -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 + ``` diff --git a/openspec/changes/harden-handoff-dcp/.openspec.yaml b/openspec/changes/harden-handoff-dcp/.openspec.yaml new file mode 100644 index 0000000..63fe932 --- /dev/null +++ b/openspec/changes/harden-handoff-dcp/.openspec.yaml @@ -0,0 +1,2 @@ +schema: unbound-force +created: 2026-08-02 diff --git a/openspec/changes/harden-handoff-dcp/design.md b/openspec/changes/harden-handoff-dcp/design.md new file mode 100644 index 0000000..a16e3f9 --- /dev/null +++ b/openspec/changes/harden-handoff-dcp/design.md @@ -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. diff --git a/openspec/changes/harden-handoff-dcp/proposal.md b/openspec/changes/harden-handoff-dcp/proposal.md new file mode 100644 index 0000000..f3bc71f --- /dev/null +++ b/openspec/changes/harden-handoff-dcp/proposal.md @@ -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. diff --git a/openspec/changes/harden-handoff-dcp/specs/handoff-dcp-hardening.md b/openspec/changes/harden-handoff-dcp/specs/handoff-dcp-hardening.md new file mode 100644 index 0000000..f4c06ec --- /dev/null +++ b/openspec/changes/harden-handoff-dcp/specs/handoff-dcp-hardening.md @@ -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. diff --git a/openspec/changes/harden-handoff-dcp/tasks.md b/openspec/changes/harden-handoff-dcp/tasks.md new file mode 100644 index 0000000..6aa0be9 --- /dev/null +++ b/openspec/changes/harden-handoff-dcp/tasks.md @@ -0,0 +1,30 @@ + + +## 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 + +