Skip to content

Commit 8e66ead

Browse files
clkaoSpike Testclaude
authored
Exception paths split out of claude-fo-dispatch.md into the failure-triggered fo-dispatch-recovery skill (#466)
* Split Degraded Mode / Break-Glass / Context-Budget exception bodies out of claude-fo-dispatch.md into a failure-triggered skill claude-fo-dispatch.md carried the full Degraded Mode contract, the Break-Glass manual-dispatch template, and the dead-ensign/budget-failure detail on every dispatching session's happy path even though they fire only on failure (~7.8KB / ~1.9k tok). Move the three bodies verbatim into a new non-user-invocable skills/fo-dispatch-recovery/SKILL.md (the #457 pattern), leaving a resident trigger line per exception that carries the first action and loads the skill only when the failure actually happens. Wires the new skill into the AC-4 contractlint closure (deferredSkillCores, lazyLoadSkills, and a deferredModuleBodies walk that also retroactively closure-checks the pre-existing using-legacy-claude-team token in claude-fo-dispatch.md) and the ensigncycle greet guard, plus the four re-points and the docs/runtime-support.md discriminator update. Adds offline oracles + RED-control unit tests and a //go:build live scenario harness (degraded-bare, break-glass-shim) for the two behavioral-equivalence ACs; the live baseline-proof itself is not complete (see the implementation stage report) — the sandbox's benchmark credential authenticated once, long enough to catch and fix a real CLI slash-command-interception bug in the degraded-bare prompt, then returned 401 on every later call. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * contractlint: pin the degraded-mode captain-report constant and using-legacy-claude-team's two Degraded Mode pointers Feedback cycle 1 (pre-merge adversarial audit, no material refutation, two offline-coverage pins): T1 — the offline oracle's degradedModeCaptainReportPrefix constant (dispatch_recovery_assert_test.go) was a free-floating value with nothing binding it to fo-dispatch-recovery/SKILL.md's actual Captain Report Template text. TestDegradedModeCaptainReportPrefixBindsSkillBlockquote extracts the constant from source and asserts it appears verbatim in the skill, so a paraphrase of either side reds instead of silently drifting apart. T2 — using-legacy-claude-team/SKILL.md's two re-pointed Degraded-Mode lines were unwatched by any closure check. Walking the whole file through the existing generic prose-pointer scanner was tried first and rejected: it also reds on two untouched, deliberately-prose lines ("Degraded Mode cannot be entered" / "Degraded Mode is entered") that name Degraded Mode as a STATE, not as a pointer — the scanner can't tell the two apart. Instead, TestUsingLegacyClaudeTeamDegradedModePointersNameRealAnchor locates the two known pointer lines by a stable marker phrase (present in both the old bare form and the current Skill-token form) and asserts each still carries the recovery-skill token and names a real anchor — narrower than the audit's suggested mechanism but closure-checking exactly the two pointers it cared about, without generalizing the scanner or touching the untouched prose. Both pins verified RED against their respective mutants locally (paraphrase / restore-old-bare-pointer), then reverted before commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Spike Test <spike@example.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 7cb5555 commit 8e66ead

13 files changed

Lines changed: 758 additions & 72 deletions

docs/runtime-support.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ Prefer pseudo-code contracts over narrative instructions. Shared and runtime con
2020

2121
A `` line states where the capability is realized: `→ shipped` names the binary that already implements it (invoke it directly); `→ runtime-binding` defers to the host adapter (and, for a deferred-module section, names the core file the boot core loads at that load point); `→ prose` marks an obligation with no binary backing — judgment-owned when it names no verb, or a deterministic mechanism still hand-followed when it names a `becomes` verb a binary will later ship (e.g. `«dispatch.next-action»``spacedock dispatch next-action`, descoped to roadmap 0222). A deferred-module section (the boot core's dispatch/merge load points) may itself take this compact shape — a `→ runtime-binding` line naming the core file plus `done-when`/`guard` lines — rather than a narrative paragraph.
2222

23-
A deferred module realizes as one of two shapes keyed on WHERE its host binding lives, not on whether it loads an adapter section alongside at the trigger. A module whose host binding lives in a deferred reference file stays a core file the boot core names by path via `→ runtime-binding`: dispatch and merge both bind through `references/claude-fo-dispatch.md` (the merge teardown loads there at first dispatch — there is no separate Claude merge reference), so both are references. A module whose host binding is boot-resident, or that has no host binding, realizes as a non-user-invocable skill the boot core invokes via `Skill(skill="spacedock:…")`: the status-viewer surface has no host binding, and the write/id-style surface's `spacedock new` binding lives boot-resident in the runtime adapter's new-entity section, so both are skills even though `spacedock new` is per-host.
23+
A deferred module realizes as a core reference file or a non-user-invocable skill, keyed on WHERE its host binding lives, not on whether it loads an adapter section alongside at the trigger. A module whose host binding lives in a deferred reference file stays a core file the boot core names by path via `→ runtime-binding`: dispatch and merge both bind through `references/claude-fo-dispatch.md` (the merge teardown loads there at first dispatch — there is no separate Claude merge reference), so both are references. A module whose host binding is boot-resident, or that has no host binding, realizes as a non-user-invocable skill the boot core invokes via `Skill(skill="spacedock:…")`: the status-viewer surface has no host binding, and the write/id-style surface's `spacedock new` binding lives boot-resident in the runtime adapter's new-entity section, so both are skills even though `spacedock new` is per-host.
24+
25+
A failure-triggered exception body whose trigger lives inside a host adapter's deferred reference realizes as a non-user-invocable skill too: the adapter keeps a resident trigger line that carries the first action and names the skill (`spacedock:fo-dispatch-recovery` — Degraded Mode, break-glass manual dispatch, budget-fail/dead-ensign handling), so the body loads only when its failure actually fires; the trigger line, not the boot core, is the namer.
2426

2527
### Runtime binding-block shape
2628

internal/contractlint/boot_resident_closure_test.go

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,22 @@ var deferredSkillCores = map[string][]string{
5353
filepath.Join("skills", "fo-write-core", "SKILL.md"): {
5454
"## FO Write Scope", "## ID Styles",
5555
},
56+
filepath.Join("skills", "fo-dispatch-recovery", "SKILL.md"): {
57+
"## Degraded Mode", "## Break-Glass Manual Dispatch", "## Context Budget Failure and Dead Ensign Handling",
58+
},
59+
}
60+
61+
// deferredModuleBodies are contract bodies that are NOT boot-resident (the FO loader
62+
// does not read them at boot) but that themselves carry in-module `spacedock:` lazy
63+
// skill triggers whose targets must resolve on disk, and whose prose pointers at a
64+
// moved section must not dangle. claude-fo-dispatch.md is read at first dispatch
65+
// (not boot) and carries both the legacy-team probe token and the three
66+
// fo-dispatch-recovery failure-trigger lines (Degraded Mode, Break-Glass, Context
67+
// Budget). Walking it with the same extraction+os.Stat oracle retroactively
68+
// closure-checks the existing using-legacy-claude-team token there, uncovered by
69+
// bootResidentBodies today because this file is not itself boot-resident.
70+
var deferredModuleBodies = []string{
71+
filepath.Join("skills", "first-officer", "references", "claude-fo-dispatch.md"),
5672
}
5773

5874
// bodyReferenceRe matches a sibling reference read-path named in a contract body
@@ -84,6 +100,7 @@ var lazyLoadSkills = map[string]bool{
84100
"feedback-rejection-flow": true,
85101
"fo-status-viewer": true,
86102
"fo-write-core": true,
103+
"fo-dispatch-recovery": true,
87104
}
88105

89106
// deferredLoadPoint is one extracted load-point: the on-disk path the body names
@@ -143,7 +160,8 @@ func extractDeferredLoadPoints(body string) []deferredLoadPoint {
143160
func TestBootResidentDeferredLoadPointsResolve(t *testing.T) {
144161
root := repoRoot(t)
145162
total := 0
146-
for _, rel := range bootResidentBodies {
163+
walked := append(append([]string{}, bootResidentBodies...), deferredModuleBodies...)
164+
for _, rel := range walked {
147165
data, err := os.ReadFile(filepath.Join(root, rel))
148166
if err != nil {
149167
t.Fatalf("read boot-resident body %s: %v", rel, err)
@@ -360,10 +378,21 @@ func TestDeferredSkillProsePointersResolve(t *testing.T) {
360378
t.Fatal("no section owners derived from deferredSkillCores — the prose-pointer check would pass vacuously")
361379
}
362380
walked := 0
381+
targets := make([]string, 0, len(deferredSkillCores)+len(deferredModuleBodies))
363382
for ref := range deferredSkillCores {
383+
targets = append(targets, ref)
384+
}
385+
// deferredModuleBodies joins the walk so a resident trigger line inside a
386+
// non-boot-resident contract body (e.g. claude-fo-dispatch.md's Degraded
387+
// Mode/Break-Glass/Context Budget trigger lines) is scanned for a dangling
388+
// bare-name pointer at a moved section too — today it walks only
389+
// deferredSkillCores files, so without this extension those trigger lines
390+
// would go unscanned.
391+
targets = append(targets, deferredModuleBodies...)
392+
for _, ref := range targets {
364393
data, err := os.ReadFile(filepath.Join(root, ref))
365394
if err != nil {
366-
t.Errorf("read deferred skill file %s: %v", ref, err)
395+
t.Errorf("read deferred module body %s: %v", ref, err)
367396
continue
368397
}
369398
walked++
@@ -372,7 +401,7 @@ func TestDeferredSkillProsePointersResolve(t *testing.T) {
372401
}
373402
}
374403
if walked == 0 {
375-
t.Fatal("walked zero deferred skill files — the prose-pointer check would pass vacuously")
404+
t.Fatal("walked zero deferred module bodies — the prose-pointer check would pass vacuously")
376405
}
377406
}
378407

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// ABOUTME: Value-binding checks for the fo-dispatch-recovery skill: the
2+
// ABOUTME: live-scenario oracle's captain-report constant, and its two cross-file pointers.
3+
package contractlint
4+
5+
import (
6+
"os"
7+
"path/filepath"
8+
"regexp"
9+
"strings"
10+
"testing"
11+
)
12+
13+
// degradedModeCaptainReportConstRe extracts the Go string-literal value of
14+
// degradedModeCaptainReportPrefix from its declaration in
15+
// internal/ensigncycle/dispatch_recovery_assert_test.go. It is a single-line
16+
// `const NAME = "..."` string literal, so a regex is sufficient — no AST needed
17+
// for one literal.
18+
var degradedModeCaptainReportConstRe = regexp.MustCompile(`degradedModeCaptainReportPrefix\s*=\s*"((?:[^"\\]|\\.)*)"`)
19+
20+
// TestDegradedModeCaptainReportPrefixBindsSkillBlockquote is a VALUE-BINDING
21+
// check, not a prose-grep and not the banned CODE-BOUND-AS-BEHAVIOR-SUBSTITUTE
22+
// (see the boundary-guard package doc): it never claims the skill's prose PROVES
23+
// AC-2's behavior — the `degraded-bare` live scenario and its offline oracle
24+
// (`assertDegradedBareObservables`) are what prove that, by running against a
25+
// real captured stream. What this binds is narrower: two INDEPENDENTLY EDITABLE
26+
// values — the Go constant the oracle matches stream text against, and the
27+
// actual opening of `fo-dispatch-recovery/SKILL.md`'s `## Degraded Mode` Captain
28+
// Report Template blockquote — must not silently drift apart. If either is
29+
// paraphrased without the other, the oracle stops matching real output and every
30+
// future live run either false-fails (a correct FO, paraphrased skill, stale
31+
// constant) or, worse, the drift goes undetected because no offline test
32+
// exercises the pairing. That is the same shape as a ref-closure or dedup
33+
// check: a machine-checkable fact about two on-disk sources, not an
34+
// interpretation of what the prose means.
35+
func TestDegradedModeCaptainReportPrefixBindsSkillBlockquote(t *testing.T) {
36+
root := repoRoot(t)
37+
38+
assertPath := filepath.Join(root, "internal", "ensigncycle", "dispatch_recovery_assert_test.go")
39+
assertSrc, err := os.ReadFile(assertPath)
40+
if err != nil {
41+
t.Fatalf("read %s: %v", assertPath, err)
42+
}
43+
m := degradedModeCaptainReportConstRe.FindSubmatch(assertSrc)
44+
if m == nil {
45+
t.Fatalf("degradedModeCaptainReportPrefix constant not found in %s — the value-binding check has nothing to bind", assertPath)
46+
}
47+
prefix := string(m[1])
48+
if prefix == "" {
49+
t.Fatal("extracted an empty degradedModeCaptainReportPrefix — extraction bug, the check would pass vacuously")
50+
}
51+
52+
skillPath := filepath.Join(root, "skills", "fo-dispatch-recovery", "SKILL.md")
53+
skillBody, err := os.ReadFile(skillPath)
54+
if err != nil {
55+
t.Fatalf("read %s: %v", skillPath, err)
56+
}
57+
if !strings.Contains(string(skillBody), prefix) {
58+
t.Errorf("the oracle's degradedModeCaptainReportPrefix (%q) does not appear verbatim in %s's Captain Report Template — the skill's report sentence and the offline oracle's match constant have diverged", prefix, skillPath)
59+
}
60+
}
61+
62+
// usingLegacyClaudeTeamDegradedModePointerMarkers are the two re-pointed
63+
// Degraded-Mode pointer lines in using-legacy-claude-team/SKILL.md, identified by
64+
// a stable lead phrase that survived the re-point (present in both the OLD bare
65+
// pointer text and the CURRENT Skill-token text), so this pin locates the same
66+
// two lines regardless of unrelated prose edits elsewhere in the file.
67+
var usingLegacyClaudeTeamDegradedModePointerMarkers = []string{
68+
"**Failure handling:**",
69+
"**Fall back to Degraded Mode**",
70+
}
71+
72+
// TestUsingLegacyClaudeTeamDegradedModePointersNameRealAnchor is a narrower,
73+
// token-scoped alternative to walking using-legacy-claude-team/SKILL.md through
74+
// the generic referenceProsePointerDanglers scanner (TestDeferredSkillProsePointersResolve).
75+
// That generic walk was tried first and rejected: it also reds on :50/:52
76+
// ("Degraded Mode cannot be entered" / "Degraded Mode is entered"), which name
77+
// Degraded Mode as a STATE, not as a pointer at the moved section — the scanner
78+
// has no way to tell the two apart, and those two lines are deliberately
79+
// untouched prose the entity's design already reviewed as "checked and valid
80+
// as-is." Generalizing the scanner to make that distinction is out of scope here
81+
// (see the implementation cycle-1 amendment's residual observation).
82+
//
83+
// Instead, this pin locates the file's two KNOWN pointer lines by a stable
84+
// marker phrase (not by token presence, so a regression to the OLD bare-pointer
85+
// form — which drops the token entirely — is still caught) and asserts each one
86+
// (1) still carries the Skill(skill="spacedock:fo-dispatch-recovery") token, and
87+
// (2) names a section that is a real anchor in fo-dispatch-recovery/SKILL.md.
88+
// The two marker-mentioned lines are exactly what the audit cared about; :50/:52
89+
// carry neither marker, so they never enter this check.
90+
func TestUsingLegacyClaudeTeamDegradedModePointersNameRealAnchor(t *testing.T) {
91+
root := repoRoot(t)
92+
93+
recoverySkillRel := filepath.Join("skills", "fo-dispatch-recovery", "SKILL.md")
94+
anchors, ok := deferredSkillCores[recoverySkillRel]
95+
if !ok || len(anchors) == 0 {
96+
t.Fatalf("deferredSkillCores has no anchors for %s — the pin has nothing to bind against", recoverySkillRel)
97+
}
98+
recoveryBody, err := os.ReadFile(filepath.Join(root, recoverySkillRel))
99+
if err != nil {
100+
t.Fatalf("read %s: %v", recoverySkillRel, err)
101+
}
102+
103+
legacyRel := filepath.Join("skills", "using-legacy-claude-team", "SKILL.md")
104+
legacyData, err := os.ReadFile(filepath.Join(root, legacyRel))
105+
if err != nil {
106+
t.Fatalf("read %s: %v", legacyRel, err)
107+
}
108+
lines := strings.Split(string(legacyData), "\n")
109+
110+
for _, marker := range usingLegacyClaudeTeamDegradedModePointerMarkers {
111+
lineNo, line, found := findLineContaining(lines, marker)
112+
if !found {
113+
t.Errorf("%s: no line carries the marker %q — the pointer line this pin tracks has moved or been deleted", legacyRel, marker)
114+
continue
115+
}
116+
if !strings.Contains(line, "spacedock:fo-dispatch-recovery") {
117+
t.Errorf("%s:%d (marker %q) no longer carries Skill(skill=\"spacedock:fo-dispatch-recovery\") — it has regressed to a bare pointer with no resolving token: %q", legacyRel, lineNo, marker, strings.TrimSpace(line))
118+
continue
119+
}
120+
var named string
121+
for _, anchor := range anchors {
122+
name := strings.TrimLeft(anchor, "# ")
123+
if strings.Contains(line, name) {
124+
named = name
125+
break
126+
}
127+
}
128+
if named == "" {
129+
t.Errorf("%s:%d (marker %q) carries the recovery-skill token but names none of its section anchors %v: %q", legacyRel, lineNo, marker, anchors, strings.TrimSpace(line))
130+
continue
131+
}
132+
if !strings.Contains(string(recoveryBody), "## "+named) {
133+
t.Errorf("%s:%d (marker %q) names section %q, but %s carries no such heading — the pointer target has drifted", legacyRel, lineNo, marker, named, recoverySkillRel)
134+
}
135+
}
136+
}
137+
138+
// findLineContaining returns the 1-indexed line number and text of the first
139+
// line in lines containing marker.
140+
func findLineContaining(lines []string, marker string) (lineNo int, line string, found bool) {
141+
for i, l := range lines {
142+
if strings.Contains(l, marker) {
143+
return i + 1, l, true
144+
}
145+
}
146+
return 0, "", false
147+
}

0 commit comments

Comments
 (0)