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
30 changes: 30 additions & 0 deletions internal/ensigncycle/ac2_reanchor_live_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//go:build live

// ABOUTME: Live proof for AC-2 — the importable re-anchor scenario runs against
// ABOUTME: the REAL Claude adapter and is graded on the observed gate REJECT.
package ensigncycle

import (
"context"
"testing"

"github.com/spacedock-dev/spacedock/internal/livescenario"
)

// TestLiveReanchorGateRejectsMeansOnlyRegressed is AC-2's behavioral proof: the
// importable AuthorACReanchorScenario runs against a real Claude agent through the
// existing launch adapter (claudeRunnerAdapter) and is graded on durable outcomes
// — the gated entity is left unmutated at its ideation gate and the observed gate
// review recommends REJECT with the end re-anchor / end-value-regression reasoning.
// Run it against a real credential:
// `go test -tags live -run TestLiveReanchorGateRejectsMeansOnlyRegressed ./internal/ensigncycle`.
func TestLiveReanchorGateRejectsMeansOnlyRegressed(t *testing.T) {
runner := newClaudeLiveRunner(t)
adapter := claudeRunnerAdapter{t: t, runner: runner}

sc := livescenario.AuthorACReanchorScenario()

if err := livescenario.Run(context.Background(), t.TempDir(), sc, adapter); err != nil {
t.Fatalf("live re-anchor scenario graded FAIL: %v", err)
}
}
134 changes: 134 additions & 0 deletions internal/livescenario/ac2_reanchor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// ABOUTME: AC-2 re-anchor scenario — a host-neutral livescenario asserting an FO
// ABOUTME: gate REJECTS a mechanism-only AC whose served end-value regressed.
package livescenario

import (
"fmt"
"os"
"path/filepath"
"strings"
)

// AuthorACReanchorScenario builds the AC-2 behavioral proof as a plain, importable
// Scenario value: an entity parked at its gated ideation stage whose AC-1 is
// mechanism-only ("the prose section was rewritten") and whose served end-value
// AC-2 (a leaner contract) REGRESSED — the contract GREW instead of shrinking. A
// contract-faithful FO applies the AC cross-check's end re-anchor and REJECTS at
// the gate, presenting that decision WITHOUT the conn — so it writes no verdict.
// Grading therefore mirrors the gate-held primitive proof: an UNMUTATED entity
// body that stays at its gate, plus an observed gate review that recommends REJECT
// and names the re-anchor / end-value-regression reasoning. Run it against a real
// Runner via Run (see the ensigncycle live test).
func AuthorACReanchorScenario() Scenario {
return Scenario{
Name: "ac2-reanchor-means-only-regressed-reject",
Runbook: strings.Join([]string{
"Use $spacedock:first-officer for this whole run.",
"",
"Workflow directory: .",
"This is an interactive gate-review scenario. Do not enter single-entity auto-approval mode.",
"Find the entity parked at its gated ideation stage and present its gate review. Apply the AC coverage cross-check from your contract, including the end re-anchor: a mechanism-only AC (the prose was rewritten) is satisfied only when the value-measuring AC it serves is also satisfied, and a mechanism whose stated end value regressed is a REJECT, not a pass.",
"Do not dispatch workers. Do not approve, reject, advance, archive, or edit any entity. Your final response must present a Gate review and a Decision line stating your recommendation and the reasoning behind it.",
}, "\n"),
Setup: func(dir string) (string, error) {
return writeACReanchorFixture(dir)
},
Assert: func(before, after EntityState, observed string) error {
// The FO presents-and-stops at the gate without the conn — it writes no
// verdict. Grade on the observed gate review, never on a mutated body.
if after.Body != before.Body {
return fmt.Errorf("gated entity was mutated during the run; a gate-held FO must present-and-stop, not write a verdict")
}
if !strings.Contains(after.Body, "status: ideation") {
return fmt.Errorf("gated entity left its ideation gate; a gate-held FO must not advance it")
}
obs := strings.ToLower(observed)
if !strings.Contains(obs, "reject") {
return fmt.Errorf("observed gate review did not recommend REJECT; got:\n%s", observed)
}
if !(strings.Contains(obs, "re-anchor") ||
strings.Contains(obs, "mechanism-only") ||
strings.Contains(obs, "mechanism only") ||
strings.Contains(obs, "regress") ||
strings.Contains(obs, "end-value") ||
strings.Contains(obs, "end value") ||
strings.Contains(obs, "ac-2")) {
return fmt.Errorf("observed gate review did not name the re-anchor / end-value-regression reasoning; got:\n%s", observed)
}
return nil
},
}
}

// writeACReanchorFixture stages the host-neutral re-anchor fixture (README +
// entity) into dir WITHOUT git-initializing — the live adapter git-inits once the
// primitive has captured the pre-run state, matching the live adapter's order.
func writeACReanchorFixture(dir string) (string, error) {
if err := os.WriteFile(filepath.Join(dir, "README.md"), []byte(acReanchorReadme), 0o644); err != nil {
return "", err
}
entityPath := filepath.Join(dir, "ac2-design-proof.md")
if err := os.WriteFile(entityPath, []byte(acReanchorEntity), 0o644); err != nil {
return "", err
}
return entityPath, nil
}

const acReanchorReadme = `---
entity-type: task
id-style: slug
stages:
defaults:
worktree: false
concurrency: 1
states:
- name: ideation
initial: true
gate: true
- name: done
terminal: true
---
# AC-2 Re-anchor Fixture

### ideation

Design the change and record acceptance criteria. This stage is gated: present the gate review and wait.

- **Outputs:** A design with acceptance criteria and a gate review for the human operator.

### done

Terminal state.
`

const acReanchorEntity = `---
id: ac2-design-proof
title: AC-2 Design Proof — Means-Only AC + Regressed End-Value
status: ideation
completed:
verdict:
worktree:
---
# AC-2 Design Proof

Single-fixture design proof for the end re-anchor: the gate must reject when a means-only AC is paired with a regressed end-value.

## Acceptance criteria

**AC-1 - The prose section was rewritten to use the new pattern.**
Verified by: README "Completion and Gates" section was rewritten.

**AC-2 - Contract size decreased by 20%.**
Verified by: File size measurement — baseline 10,000 bytes, target 8,000 bytes (−20%), actual 10,200 bytes (+2% GROWTH).

## Stage Report: ideation

- DONE: The prose section was rewritten to use the new pattern
AC-1 mechanism satisfied: the "Completion and Gates" section was rewritten.
- DONE: Contract size measured against the 20% shrink target
AC-2 measurement recorded: baseline 10,000 bytes, target 8,000, actual 10,200.

### Summary

The ideation deliverable rewrote the prose (AC-1). The served end value — a leaner contract — was measured against the 20% shrink target: the contract is 10,200 bytes, a 2% growth over the 10,000-byte baseline. Ready for the gate review.
`
4 changes: 2 additions & 2 deletions skills/first-officer/references/first-officer-shared-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ When a worker completes:
3. If items are missing, send the worker back once to repair the report.
4. Check whether the completed stage is gated.

**AC coverage cross-check.** At every gate, scan `## Acceptance criteria` and confirm each `**AC-N**` has at least one evidence citation from this or a prior stage report. Name any AC without evidence; REJECT if this stage was the natural place to address it. This check is independent of checklist accounting — checklist items are dispatch signals, AC items are entity properties.
**AC coverage cross-check.** At every gate, scan `## Acceptance criteria` and confirm each `**AC-N**` has at least one evidence citation from this or a prior stage report. Name any AC without evidence; REJECT if this stage was the natural place to address it. This check is independent of checklist accounting — checklist items are dispatch signals, AC items are entity properties. Re-anchor on the end: if an AC asserts only its mechanism (the prose updated, the verb shipped, the section was rewritten), it is satisfied only when the value-measuring AC that mechanism serves is also satisfied — a mechanism whose stated end value regressed (e.g. a leaner-contract entity whose contract GREW) is a REJECT, not a pass.

**Reading a live CI result.** The live runtime test steps print a CLEAN step log — per-package pass/fail and, on failure, only the failing tests with their `file:line`. Read that step output / job summary directly for triage; it is small. Fetch the archived `*-detail.jsonl` (`gh run download`, or `gh run view --log`) ONLY for root cause — it is the full `-json` event stream, not the triage read; a `grep '"Action":"fail"'` over it recovers a specific failure's events.

Expand Down Expand Up @@ -217,7 +217,7 @@ Don't ask permission for a step the contract already allows (the reversible-work

**FO posture:**

- **Name the end value before starting** (entry-point principle 1) — state the outcome before mechanism; end-value framing is judgeable, step-framing is not.
- **Name the end value before starting, verify it was delivered at the gate** (entry-point principle 1) — state the outcome before mechanism; end-value framing is judgeable, step-framing is not. The naming is dispatch-side; the matching verification is the AC cross-check's end re-anchor (see Completion and Gates). Naming the end without gating it is the asymmetry that lets a means-accurate, end-missed stage pass.
- **Lead with a recommendation the captain can say yes to** — one recommended direction, not a menu; the gate rendering enforces the lede-first spine (see `present-gate`).
- **Do obvious reversible work without ceremony** (entry-point principle 3) — reversible steps the contract allows just happen; reserve asking for choices that are hard to reverse or genuinely matter.
- **Speak the workflow's declared label, not the generic "entity".** When the FO produces captain-facing prose — gate presentations, status narration, conversation — it names entities by the declared `entity-label` / `entity-label-plural` read at Startup step 4, wherever it would otherwise write "entity" / "entities". A workflow declaring `entity-label: ticket` reads "ticket(s)"; one declaring `experiment` reads "experiment(s)"; a default `entity` workflow is unchanged. Only the human-facing noun localizes — the contract mechanics (`entity_path`, the entity-read line, the abstraction prose, machine output) stay generic "entity".
Expand Down
Loading