- Create src/magical-objects/magical-object-types.ts with DamageDealer and Healer interfaces - MagicalWeapon implements DamageDealer - HealingObject implements Healer - Character depends on interfaces instead of concrete classes
13 KiB
name, description, disable-model-invocation, license, metadata
| name | description | disable-model-invocation | license | metadata | ||
|---|---|---|---|---|---|---|
| problem-breakdown | Break down a problem into small, independently executable steps using yx. Use after user-story-conversation to create a test execution list, or during horizontal refactoring to plan file moves and transformations. Each step becomes a yak with yx add and includes execution context so another agent can execute it independently. | true | MIT |
|
Problem Breakdown
Break a problem into small, independently executable steps using the yx CLI. Each step becomes a yak with execution context, enabling another agent (or future you) to execute it independently.
When to Use
- After user-story-conversation — Convert the output (Allium spec + fast-check properties) into a concrete test execution list: which files to create/change, what tests to write, where to place them.
- Horizontal refactoring — Plan cross-cutting changes like moving value objects into a
value-objects/directory, extracting interfaces, or restructuring modules. - Feature scaffolding — Break a feature into file creation, implementation, wiring, and test steps.
The Method
Step 1: Identify the work items
From the problem description, extract discrete, independently executable units of work. Each work item should satisfy:
- Self-contained — can be executed without waiting for another yak to finish
- Small — one file, one method, one move, one test
- Verifiable — has a clear pass/fail condition (compiles, tests green, linter clean)
- Ordered — parents block children (use
yx add --under)
Step 2: Create yaks with yx add
For each work item:
yx add "create src/domain/health.value-objects.ts"
yx add "implement Health.create() with invariant n >= 0" --under "create src/domain/health.value-objects.ts"
yx add "write fast-check property: Health.create rejects negative numbers" --under "implement Health.create() with invariant n >= 0"
Use --under to express dependency hierarchy. Children block their parent.
Step 3: Add execution context with yx context
For each yak, add enough detail for another agent to execute it independently:
echo "Create src/domain/health.value-objects.ts with a Health value object class.
- Private constructor taking number
- Static create(n: number): Health — throws if n < 0
- get value(): number
- sub(amount: number): Health — returns Health.create(max(0, this.value - amount))
- add(amount: number): Health — returns Health.create(this.value + amount)
- No dependency on other domain entities yet" | yx context "implement Health.create() with invariant n >= 0"
Step 4: Execute
The executor agent reads each yak's context, executes the step, and marks it done:
yx start "implement Health.create() with invariant n >= 0"
# ... execute ...
yx done "implement Health.create() with invariant n >= 0"
Output Patterns
Pattern A: Test Execution List (after user-story-conversation)
After a user-story-conversation produces an Allium spec and fast-check properties, break them into file-level execution steps. Before writing any test yak, run the Test Strategy Decision below.
Test Strategy Decision
For each rule/invariant from the spec, decide whether to write a property-based test or an example-based test. Discuss with the user:
| Signal | Choose | Rationale |
|---|---|---|
fc.property would be trivially short (< 5 lines) |
Example-based | Property overhead not worth it |
| Invariant is a simple arithmetic relationship | Example-based | One or two examples cover all cases |
| State transition has a small, finite input space | Example-based | Exhaustive examples are feasible |
| Invariant involves collections, sequences, or math | Property-based | Need random inputs to find edge cases |
| Rule has complex guards (requires + ensures chains) | Property-based | Random inputs surface hidden preconditions |
| User says "just show it works" | Example-based | Confidence test, not a robustness guarantee |
| User says "prove it always holds" | Property-based | That's what properties are for |
Default: start with example-based tests. Escalate to property-based only when the user or the spec demands broader coverage. This keeps the yak list smaller and faster to execute.
After deciding, create the test yaks with the chosen approach in context.
Feature: Characters Deal Damage
├── create src/domain/status.value-objects.ts ← ADT for alive/dead
│ └── write Status discriminated union ← {kind: 'alive'} | {kind: 'dead'}
├── create src/domain/health.value-objects.ts ← Health value object
│ ├── implement Health.create() with invariant ← throws if n < 0
│ └── implement Health.sub() ← capped at 0
├── create src/domain/character.entity.ts ← Character entity
│ ├── implement Character constructor ← name, health, status
│ └── implement Character.dealDamage() ← with self-damage guard
├── write tests/health.spec.ts ← example + PBT tests
│ ├── example: 500 - 200 = 300 ← Health.sub arithmetic (simple, example-based)
│ ├── property: Health.sub never goes below zero ← fc.property (invariant, property-based)
│ └── example: 100 - 200 = 0 ← Health.sub boundary (simple, example-based)
├── write tests/character.spec.ts ← example + PBT tests
│ ├── example: 1000 health, 200 damage → 800 ← dealDamage happy path (example-based)
│ ├── property: dealDamage reduces target health ← fc.property (invariant, property-based)
│ └── example: self-damage is forbidden ← dealDamage guard (example-based)
└── run npm test ← verify all pass
Pattern B: Horizontal Refactoring
For cross-cutting structural changes:
Refactor: Move value objects to value-objects/
├── create src/domain/value-objects/ ← new directory
│ └── write barrel index.ts ← re-export all value objects
├── move src/domain/health.ts → src/domain/value-objects/health.ts
│ └── update all imports to point to value-objects/health
├── move src/domain/damage.ts → src/domain/value-objects/damage.ts
│ └── update all imports to point to value-objects/damage
├── move src/domain/level.ts → src/domain/value-objects/level.ts
│ └── update all imports to point to value-objects/level
├── update src/domain/index.ts ← update barrel exports
└── run npm run checks ← format, lint, typecheck, test
Context Template
Each yak's context should contain:
### Location
File: src/path/to/file.ts
Line: ~line numbers (if modifying existing)
### What to create/modify
Clear description of the change.
### Implementation details
- Key signatures
- Invariants to enforce
- Dependencies (what already exists)
- What NOT to implement (scope guard)
### Verification
- npm run typecheck passes
- npm test passes (specific test file)
- npm run lint:fix clean
### References
- Allium spec: .allium/path/allium-file.allium
- Related yak: "name of parent yak"
Rules
- One yak per file operation — create, move, or modify a single file
- Context is king — if another agent can't execute it from the context alone, add more detail
- Scope guards — explicitly state what NOT to implement in each yak's context (prevents scope creep)
- Dependencies via
--under— use the hierarchy, not just flat list - Verification yak — always add a final yak to run the full check suite
- No implementation details in yak names — yak names should be action-oriented summaries; details go in context
- Keep yaks small — if a yak's context is more than 30 lines, split it
Integration with Other Skills
| Skill | When to run problem-breakdown after |
|---|---|
| user-story-conversation | After Allium spec + properties are produced |
| distill | After spec extraction, before test generation |
| tend | After spec changes, to plan implementation updates |
| propagate | When propagate produces obligations, to break them into file-level steps |
| weed | After divergence is found, to plan alignment fixes |
Example: Full Breakdown
After a user-story-conversation on "Characters can Deal Damage":
# Phase 1: Create value objects
yx add "create src/domain/status.value-objects.ts"
echo "Create src/domain/status.value-objects.ts
- Discriminated union: type Status = { kind: 'alive' } | { kind: 'dead' }
- No methods, just the type
- Export as default" | yx context "create src/domain/status.value-objects.ts"
# Phase 2: Health value object
yx add "create src/domain/health.value-objects.ts" --under "create src/domain/status.value-objects.ts"
echo "Create src/domain/health.value-objects.ts
- class Health with private constructor
- static create(n: number): Health — throw if n < 0
- get value(): number
- sub(amount: number): Health — Health.create(max(0, this.value - amount))
- add(amount: number): Health — Health.create(this.value + amount)
- NO: maxForLevel, NO: isMax, NO: isZero — those belong to later stories
- NO: dependency on Character or Level" | yx context "create src/domain/health.value-objects.ts"
# Phase 3: Character entity
yx add "create src/domain/character.entity.ts" --under "create src/domain/health.value-objects.ts"
echo "Create src/domain/character.entity.ts
- class Character with readonly name, health, status
- constructor(name: string, health: Health, status: Status)
- dealDamage(target: Character, damage: number): void — pure logic, no mutation
- self-damage guard (this.name === target.name → return)
- health reduced by damage amount (calls target.health.sub)
- NO: factions, NO: level, NO: magicalObjects — those belong to later stories
- NO: isAllyOf, NO: isDead — those belong to later stories" | yx context "create src/domain/character.entity.ts"
# Phase 4: Test Strategy Decision
Before writing test yaks, decide property vs example for each test item:
| Spec item | Decision | Why |
| ---------------------------- | --------------- | -------------------------------------- |
| Health.create rejects neg. | Example-based | One negative input is sufficient |
| Health.sub never below zero | Property-based | Invariant over arbitrary input range |
| Health.sub correct arithmetic| Example-based | Simple arithmetic, examples cover all |
| dealDamage reduces health | Property-based | Invariant: result = max(0, h - d) |
| Self-damage forbidden | Example-based | One case (same name) proves the rule |
# Phase 4: Tests
yx add "write tests/health.spec.ts" --under "create src/domain/health.value-objects.ts"
echo "Write tests/health.spec.ts
- Import Health from src/domain/health.value-objects.ts
- Example: Health.create rejects negative (it('rejects -1', () => { expect(() => Health.create(-1)).toThrow() }))
- Property: Health.sub never below zero (fc.property(fc.integer({min:0,max:10000}), fc.integer({min:0,max:10000}), (h, d) => { const c = new Character({ health: Health.create(h) }); c.takeDamage(d); return c.health >= 0; }))
- Example: Health.sub 500 - 200 = 300
- Example: Health.sub 100 - 200 = 0
- Use vitest describe/it blocks with fc.assert()" | yx context "write tests/health.spec.ts"
yx add "write tests/character.spec.ts" --under "create src/domain/character.entity.ts"
echo "Write tests/character.spec.ts
- Import Character, Health, Status from domain
- Example: dealDamage 1000 health, 200 damage → 800 (it('reduces health', () => { ... }))
- Property: dealDamage reduces target health (fc.property(fc.integer({min:0,max:10000}), fc.integer({min:0,max:10000}), (h, d) => { ... return target.health === Math.max(0, h - d) }))
- Example: self-damage forbidden (it('does nothing when target is self', () => { ... }))
- Use vitest describe/it blocks with fc.assert()" | yx context "write tests/character.spec.ts"
# Phase 5: Verify
yx add "run npm test and npm run typecheck" --under "write tests/health.spec.ts"
yx add "run npm run checks" --under "run npm test and npm run typecheck"
Tips
- Start from the spec — the Allium spec's entities and rules map directly to yak groups
- Group by file — create yaks for file creation first, then implementation, then tests
- Add scope guards — explicitly state what NOT to implement to prevent scope creep
- Use yx tags — tag yaks with
test,domain,refactorfor filtering:yx tag "write tests/health.spec.ts" test - Review before executing — run
yx listto verify the hierarchy makes sense before starting work