8.6 KiB
name, description
| name | description |
|---|---|
| spf-update-behavior | Update an existing SPF behavior whose purpose is changing or expanding. Distinct from /refactor-behavior, which preserves purpose — this skill handles cases where the behavior gains new responsibility (new state slot to react to, new lifecycle phase, new constraint, new code path). Carries /refactor-behavior's purpose-first discipline applied to the *purpose change*. Triggers: "update behavior", "extend behavior", "modify behavior", "change behavior purpose", "expand behavior responsibility". |
Update an SPF Behavior
Modify an existing SPF behavior whose purpose is changing or expanding.
The canonical failure mode without this discipline is treating
purpose-changes as refactors — applying /refactor-behavior's
preserve-purpose lens to a change that's actually adding responsibility. The
discipline distinction matters because:
- Refactor: behavior X stays X, but improved (cleaner code, better patterns, smaller surface).
- Update: behavior X gains new responsibility — reacts to a new slot, owns a new lifecycle phase, applies a new constraint. The behavior's contract changes.
This skill is a stub scoped for use by /spf-implement-feature. Failure-
mode catalog grows from real use.
Usage
/spf-update-behavior <behavior-name>
Typically invoked from /spf-implement-feature's Step 6 when a feature
implementation requires extending an existing behavior. Can be invoked
directly when the user has identified the behavior to update.
Reference docs
- The existing behavior file and its tests (required reading)
internal/design/spf/conventions/behaviors.md— convention catalog the update must continue to satisfyinternal/design/spf/conventions/signals.md— multi-writer characterization when adding writers to a slot another behavior writes.claude/skills/refactor-behavior/SKILL.md— the purpose-first discipline shape this skill mirrors (applied to purpose change instead of preserved purpose)- The feature doc driving the update (if invoked from
/spf-implement-feature) — Step 1 grounds the update in the doc's phase row or "What's not implemented" entry
Failure-mode catalog (seeded; grows with use)
-
Purpose-change articulation skipped. The most common failure mode: the user invokes the skill saying "add bandwidth sampling to setupAudioBufferActors" without naming what's actually changing about the behavior's contract. Articulating the change forces clarity: "setupAudioBufferActors gains responsibility for bandwidth-sampling on audio fetches, which it didn't have before — same composition position, same lifecycle, but now writes
bandwidthStatefrom audio samples in addition to creating buffer actors." -
Slot map evolution without multi-writer characterization. If the update adds a writer to a slot another behavior already writes, the multi-writer characterization from
conventions/signals.mdmust be done explicitly. Default-merge or silent-overwrite is a bug — typically surfaces as race conditions or last-write-wins ordering bugs. -
Cleanup pattern preservation/migration mishandled. Existing cleanup contracts (what gets torn down, when, in what order) must be honored or explicitly migrated. Adding a new resource without adding cleanup is the canonical leak shape; reorganizing cleanup without preserving order is the canonical lifecycle bug.
-
Conflating with refactor-behavior territory. If the purpose isn't actually changing — the behavior's contract stays the same, just the implementation improves — route to
/refactor-behavior. The discipline for purpose-preservation vs purpose-evolution differs; using the wrong skill produces drift in either direction (refactor-as-update bloats the behavior; update-as-refactor silently changes contracts).
Steps (do these in order)
Step 1 — Articulate the purpose change
The load-bearing setup step. Before any code:
- What is the behavior's current purpose? Read the existing behavior,
read
conventions/behaviors.mdfor context. Articulate in plain language. - What's changing? New responsibility, new state slot to react to, new lifecycle phase, new constraint? Name the specific change.
- What's not changing? Surface the parts of the contract that are preserved — slot positions, composition placement, cleanup ordering, observable interface.
- Why is this an update, not a refactor? If the answer is "the
behavior does the same thing, just differently," stop and route to
/refactor-behavior.
Stop and report to user with the purpose-change articulation. The user confirms before proceeding.
Step 2 — Identify slot map / interface changes
- New slots read? Add to
stateKeys/contextKeys. Perconventions/signals.md, narrow is better. - New slots written? Multi-writer characterization required. Three-axis check: decision domain, trigger, cost. Document the coordination strategy with the existing writer(s).
- Slots removed? If the update removes a read or write, verify no downstream behavior depends on it.
- Interface change? If the behavior's external contract changes (e.g., it now emits an event it didn't before), document the contract change explicitly.
Step 3 — Apply conventions to the change
- Cleanup pattern. Per project convention (named-cleanup-collection + wrapper, not AbortController for SPF). If adding a new resource that needs cleanup, slot into the existing cleanup pattern.
- Per-type behavior? If the update touches a per-type behavior, apply
per-type discipline (sibling behaviors + shared helper) per
conventions/behaviors.md. - Composition-variant logic. If the new responsibility is variant- specific (live-only, audio-only-only), the answer is not to add a runtime conditional inside the always-on behavior. Either split the behavior into per-variant siblings or compose a new behavior into the variant factory.
Step 4 — Implement (TDD)
- Update the test first. Add an assertion for the new behavior, or write a new test case for the new responsibility.
- Run the test failing.
- Update the behavior to satisfy the new test while preserving existing tests' assertions.
- Run all tests passing (the existing tests + the new one).
- Run composition tests to verify no regression on the behavior's downstream consumers.
Step 5 — Final-shape audit + commit
Per parent skill (/spf-implement-feature), commits are typically batched
at the feature-implementation level. If invoked standalone, propose a
per-update commit shape.
Audit checklist:
- Purpose change reflected — does the implementation match the Step 1 articulation?
- Multi-writer coordination clean — if a new writer was added, is the coordination documented and tested?
- Cleanup preserved or migrated — no leaks introduced; cleanup order preserved or explicitly changed?
- Existing tests still passing — preserved-contract tests must still hold
- Conventions adherence
When this is the wrong skill
- Behavior's purpose stays the same, just code improves →
/refactor-behavior - Creating a new behavior →
/spf-create-behavior - Major restructuring (split or merge) →
/refactor-behavior(which may route to/split-behavioror/merge-behaviors) - Pure config-driven change with no behavior code change → handle in the feature implementation directly; no behavior-update needed
How the failure-mode catalog grows
Same pattern as other SPF skills: when a new failure mode surfaces, add an entry with a worked-example citation. This skill is a stub; the catalog will likely expand significantly as the first real implementations exercise it.
Open framing question
The boundary between /spf-update-behavior and /refactor-behavior-with-
extension is genuinely open. Per project_spf_implementation_skills_next
memory: "spf-update-behavior OR non-trivial updates to spf-refactor- behavior — when an existing behavior needs a feature-implementation change
that isn't a pure refactor. Open which framing — extend refactor-behavior
or add a new skill."
This skill ships as a separate skill (rather than a refactor-behavior extension) because the purposes differ — refactor preserves; update changes. If usage reveals the discipline is mostly shared, the skills may later merge. For now, the separation is intentional: route by purpose-preserved vs purpose-changed, and let the failure-mode catalogs diverge based on what each skill actually catches.