Agent Skillstddworks/baguette › app-implement-feature

app-implement-feature

GitHub

指导在baguette项目中实现新功能的流程,涵盖架构设计、领域与基础设施TDD及接线。适用于新增手势、协议扩展或iOS SimulatorKit交互等场景。

.claude/skills/baguette-implement-feature/SKILL.md tddworks/baguette

Trigger Scenarios

添加新功能如手势或按钮 扩展现有协议或CLI命令 用户请求实现特定功能 修改iOS-26 SimulatorKit相关代码

Install

npx skills add tddworks/baguette --skill app-implement-feature -g -y
More Options

Non-standard path

npx skills add https://github.com/tddworks/baguette/tree/main/.claude/skills/baguette-implement-feature -g -y

Use without installing

npx skills use tddworks/baguette@app-implement-feature

指定 Agent (Claude Code)

npx skills add tddworks/baguette --skill app-implement-feature -a claude-code -g -y

安装 repo 全部 skill

npx skills add tddworks/baguette --all -g -y

预览 repo 内 skill

npx skills add tddworks/baguette --list

SKILL.md

Frontmatter
{
    "name": "app-implement-feature",
    "description": "Guide for implementing features in baguette — a Swift CLI + WebSocket server\nthat drives iOS simulators via private SimulatorKit. Use this skill when:\n(1) Adding a new gesture, button, keyboard surface, stream format, or\n    device-chrome behaviour (anything that lands across Domain \/ Infrastructure \/\n    App + Resources\/Web).\n(2) Extending an existing wire-protocol envelope, CLI subcommand, or HTTP route.\n(3) User asks \"add feature X to baguette\", \"implement <gesture>\", \"wire <new\n    verb> through serve \/ input \/ CLI\", or similar.\n(4) Touching the iOS-26 SimulatorKit \/ IndigoHID surface — those edits MUST\n    go through this skill's Architecture phase before code lands.\nAvoid using this skill for refactors that don't add a new feature (keep those\nTDD-driven without the architecture-approval gate)."
}

Implement a feature in baguette

baguette is a CLI + WebSocket server, not a SwiftUI app. There is no ViewModel layer, no async actors in the input path — gestures are synchronous Bool-returning calls into the @Mockable Input abstraction whose only concrete adapter is IndigoHIDInput. The frontend is hand-written vanilla JS IIFEs (no bundler) that talk to the server via one WebSocket per stream.

Naming abstractions: every @Mockable protocol in this codebase is a domain noun for the role it playsInput, Screen, Accessibility, LogStream, DeviceHost, Subprocess, Chromes, Simulators. The pattern-label suffixes "Port" / "Service" / "Manager" never appear. If you reach for XxxPort, the abstraction isn't named yet — keep going until the noun describes what the thing is in the domain.

Repository is allowed — but only for aggregate CRUD, and only named as the collection noun. A protocol that is genuinely a DDD collection-like interface for an aggregate root (load / save / delete by identity) takes the plural of the aggregateSimulators, Chromes, Books, Orders — not BookRepository / OrderRepo. The suffix Repository is still banned; the role (aggregate persistence) is the legitimate case the plural-noun convention already covers. If your protocol isn't aggregate persistence (it's an adapter, an event source, a process boundary, …), the repository carve-out doesn't apply — pick a role-noun like Subprocess / LogStream instead.

Read CLAUDE.md — the "TDD is non-negotiable" gate, the naming rule, and the orchestrator-vs-collaborator split for adapters that wrap 3rd-party I/O are authoritative there. This skill describes the process of adding features that fit those rules.

Workflow

┌──────────────────────────────────────────────────────────────┐
│  0. ARCHITECTURE DESIGN (user approval required)             │
│     wire shape · domain types · adapter split · risks        │
├──────────────────────────────────────────────────────────────┤
│  1. DOMAIN TDD                                                │
│     value types · pure static factories · @Mockable          │
│     abstractions · rich-domain methods                        │
├──────────────────────────────────────────────────────────────┤
│  2. INFRASTRUCTURE TDD                                        │
│     concrete adapter impl. If the adapter wraps 3rd-party I/O │
│     and the call is conversational, introduce a domain-named │
│     `@Mockable` collaborator (`Subprocess`) and split the    │
│     orchestrator from the thin `HostXxx` impl.                │
├──────────────────────────────────────────────────────────────┤
│  3. WIRING                                                    │
│     register on GestureRegistry (gestures only) · CLI         │
│     subcommand · WS route · browser IIFE (when user-facing)  │
├──────────────────────────────────────────────────────────────┤
│  4. DOCS + CHANGELOG (mandatory before reporting "done")      │
│     create or update `docs/features/<feature>.md` ·           │
│     update `CHANGELOG.md` Unreleased section ·                │
│     update `skills/baguette/` references when CLI / wire     │
│     surface changed                                           │
└──────────────────────────────────────────────────────────────┘

Phase 0: Architecture Design (mandatory)

For any feature that crosses a layer boundary or touches the SimulatorKit / IndigoHID / AXPTranslator / spawn surface, stop and design before coding. Briefly produce:

  1. Wire shape — the JSON envelope on baguette serve WS / baguette input stdin. Field names, optional vs required, default values.
  2. CLI surface — subcommand + flag names. Match existing patterns (--udid, --width, --height).
  3. Domain types — value types (struct/enum) and which @Mockable abstraction is added/changed. Rich domain: behaviour lives on the value, not in a service (DeviceButton.press, KeyboardKey.press are the templates). Name new abstractions for their domain roleSubprocess, DeviceHost, Accessibility. Never XxxPort / XxxService / XxxManager, and never the suffix XxxRepository. If the abstraction is aggregate CRUD (load / save / delete by identity for an aggregate root), name it as the plural collection nounSimulators, Chromes, Books. If the noun isn't obvious, the abstraction probably shouldn't exist yet.
  4. Adapter changes — how the production class (IndigoHIDInput, AXPTranslatorAccessibility, SimDeviceLogStream, …) handles it. Which private-API symbol, which arg shape. Flag iOS-26-specific gotchas explicitly (signature drift between idb/AXe and Xcode 26 has burned us before — see buttons.md for the canonical example).
  5. I/O split (only when the adapter wraps 3rd-party I/O) — read CLAUDE.md's "Splitting an adapter that wraps 3rd-party I/O" section and pick a pattern:
    • One-shot fetch (single private-API call → operate on the value): lift the post-fetch logic into a pure static factory in Domain/ (AXNode.walk(from:transform:), AXFrameTransform, LineBuffer). The adapter shrinks to "make the call, hand the result to the static factory." No new abstraction needed.
    • Conversational I/O (start / stream / signal-exit / terminate): introduce one small @Mockable collaborator named like a domain noun (Subprocess, never LogProcessPort). The orchestrator depends on any Subprocess; tests inject MockSubprocess. The concrete impl (HostSubprocess ~30–50 LOC) is integration-only and should be excluded from coverage.
  6. Frontend — does the browser need to send / receive this? If yes, which IIFE(s) change.

Present a short ASCII diagram + a "files to touch" list, then ask the user to approve before writing code. The cost of a wrong SimulatorKit signature is a backboardd crash, not just a failing test.

Architecture diagram templates

Gesture-style feature (single private-API call, no conversation):

Wire JSON                Domain                       Infrastructure
{type:"<verb>",          ┌─────────────┐             ┌────────────────┐
 …}                ─────▶│ <Verb>      │             │ IndigoHIDInput │
                         │ Gesture +   │ Input       │ adapter        │
                         │ value types │ ───────────▶│ (SimulatorKit) │
                         └─────────────┘             └────────────────┘
                              ▲                              │
                              │                              ▼
                         GestureRegistry                  iOS sim
                              ▲
       CLI ─── ArgumentParser ┤
       WS  ─── Server.streamWS┤
       JS  ─── sim-input{,-bridge}.js

Conversational-I/O feature (start / stream / terminate — split the orchestrator from the host-process collaborator):

Wire JSON              Domain                   Infrastructure
{type:"<verb>",        ┌─────────────────┐      ┌──────────────────────┐
 …}              ─────▶│ <Feature>       │      │ <Feature>Orchestrator│
                       │ value types,    │ ───▶ │ (state machine,      │
                       │ pure factories  │      │  error mapping)      │
                       │                 │      └──────────────────────┘
                       │ <Collaborator>  │              │ depends on
                       │ @Mockable       │ ◀─────       ▼
                       │ (e.g. Subprocess)│      ┌──────────────────────┐
                       └─────────────────┘      │ Host<Collaborator>   │
                              ▲                 │ (~30–50 LOC, the     │
                              │                 │  irreducible private │
                              │                 │  / OS call)          │
       CLI ─── ArgumentParser ┤                 └──────────────────────┘
       WS  ─── /…/<feature> route

Phase 1: Domain TDD

Write the failing test FIRST, in Tests/BaguetteTests/<Context>/.

Patterns that already exist — match them:

  • Pure value typesstruct Foo: Equatable, Sendable { … }. Add Hashable only if a Set / Dictionary key actually needs it.
  • Rich domain methods — verbs live on the value:
    extension DeviceButton {
        func press(duration: Double = 0, on input: any Input) -> Bool {
            input.button(self, duration: duration)
        }
    }
    
  • Gesture protocol — wire DTO with static let wireType + static func parse + func execute(on input: any Input). The body of execute should be one line that delegates to a rich-domain method.
  • Field extractors — use Field.requiredString / requiredDouble / optionalDouble. Don't open-code dict["…"] as? Double.

Example test rhythm (from KeyboardTests.swift):

@Suite("KeyboardKey")
struct KeyboardKeyTests {
    @Test func `parses lowercase letter wire codes onto HID page 7`() {
        #expect(KeyboardKey.from(wireCode: "KeyA")?.hidUsage
            == HIDUsage(page: 7, usage: 0x04))
    }
}

Run swift test --filter <Suite> after each red→green cycle.

Phase 2: Infrastructure TDD

@Mockable abstractions in this layer get auto-generated MockXxx companions; tests substitute the mock and assert on returned state:

let input = MockInput()
given(input).key(.any, modifiers: .any, duration: .any).willReturn(true)

KeyboardKey.from(wireCode: "KeyA")!.press(modifiers: [.shift], on: input)
verify(input).key(.value(_), modifiers: .value([.shift]), duration: .value(0))
    .called(1)

If the feature needs a new method on an existing abstraction, add it to the protocol with a doc comment, then update the production adapter. Do not introduce a parallel hierarchy just to enable testing — IndigoHIDInput is allowed to be the only Input impl.

Adapter wraps 3rd-party I/O? Pick the split.

If your adapter touches a private framework, Foundation.Process, dlopen, Pipe, kill(pid), or any external XPC, do not pile all of that into one class. Use one of the two splits.

(a) One-shot fetch — pure factory in Domain. When the private-API call is "make one call, get a value back, operate on the value" (AXPTranslator.frontmostApplicationWithDisplayId: returns one AXPMacPlatformElement), lift the post-fetch logic into a pure static factory in Domain and drive it with Fake… NSObjects that override KVC / selectors. The adapter shrinks to "make the call, hand the result to the factory." Examples:

  • AXNode.walk(from rootElement: NSObject, transform: AXFrameTransform, depthCap:, deadline:) -> AXNode
  • AXFrameTransform.map(_ macFrame: CGRect) -> CGRect
  • LineBuffer.append(_ bytes: Data) -> [String]
  • AXElementReader.string/bool/frame/children(...)

These all live in Domain/<Context>/, are exercised by direct unit tests, and the Infrastructure adapter calls them after the irreducible private-API call.

(b) Conversational I/O — domain-named collaborator. When the adapter has a real conversation with the outside (start → many onBytesonExit, plus a terminate poke), pure helpers don't capture the state machine cleanly. Introduce one small @Mockable collaborator in Domain, named like a noun for what it is (Subprocess, never LogProcessPort):

@Mockable
protocol Subprocess: AnyObject, Sendable {
    func run(executable: URL, arguments: [String],
             onBytes: @escaping @Sendable (Data) -> Void,
             onExit:  @escaping @Sendable (Int32) -> Void) throws
    func terminate()
}

The orchestrator depends on any Subprocess. Tests inject MockSubprocess and drive every byte/exit/terminate path. The concrete impl (HostSubprocess ~30–50 LOC, wraps Foundation.Process) is the only integration-only file — exclude it from coverage targets and rely on a manual smoke test.

Coverage expectation per file

After the split:

  • Domain pure factories / value types: 100% unit-tested.
  • Orchestrator (<Feature>Orchestrator or Sim<Feature>Stream): ≥ 90% unit-tested via the collaborator's MockXxx.
  • Host<Collaborator> (the irreducible call): integration-only, excluded from coverage.

If you can't get the orchestrator into a MockXxx-driven test, it's not split correctly — go back to Phase 0.

When the adapter calls private SimulatorKit symbols

For IndigoHIDInput (and any future direct private-symbol caller):

  • Resolve the symbol in resolveFunctions() and log presence in the [hid] symbols resolved … line.
  • Match the arg signature against a verified open-source bridge (see the kittyfarm typedef approach used for the buttons feature in docs/features/buttons.md) — guessing the signature from older idb / AXe code has burned us before.
  • Add log(...) lines at each branch (symbol resolved, message built non-nil, sent) so users can see exactly where dispatch dies.
  • Bracket multi-step sequences (modifiers, two-finger holds) with matching down/up pairs; never leave a key/modifier latched.

Phase 3: Wiring

Each new gesture / verb flows through the same checklist:

  1. GestureRegistry.standard — one r.register(<Verb>.self) line in Domain/Input/GestureRegistry.swift.
  2. CLI subcommand — new struct …Command: ParsableCommand in App/Commands/GestureCommands.swift (or a sibling). Add to RootCommand.subcommands. Update CommandParsingTests.
  3. Server WS — usually nothing: Server.streamWS already routes wire JSON through GestureDispatcher.dispatch(line:). New behaviour only if you're adding a control verb that bypasses the gesture path.
  4. Browser — when the feature is user-facing:
    • Add the wire field handling to Resources/Web/sim-input-bridge.js (translate plugin dialect → baguette wire).
    • Expose a method on SimInput in sim-input.js.
    • If a new DOM-driven module is needed, write a single-purpose IIFE that hangs one class on window, add a <script> tag in sim.html (and farm/farm.html if the farm path uses it), and mount it from sim-native.js and/or farm/farm-tile.js.
    • The frontend stays a dumb sender: no HID codes, no chrome lookups, no domain logic. The Swift side owns rich domain.

Phase 4: Docs + Changelog (mandatory before "done")

When the code is green and the feature works end-to-end, before reporting completion:

4a. Feature doc

Create or update docs/features/<feature>.md. Match the existing shape (buttons.md, keyboard.md, screenshot.md):

  • One-paragraph what + why intro listing all entry points (CLI, wire JSON, browser).
  • Wire JSON examples (every shape, with required + optional fields explained).
  • Dispatch path — which Input method, which SimulatorKit symbol, which arg shape (with the iOS-26 signature gotcha documented if relevant).
  • Where the magic numbers come from — link to the spec / chrome bundle / Apple HID page so the next maintainer can verify them.
  • Adding a new — five-step recipe matching the Phase-1→3 checklist above.
  • Known limits — be honest about phase-1 scope (no IME, no emoji, no F-keys, etc.).

4b. CHANGELOG

Append a bullet under ## [Unreleased]### Added (or ### Changed for behaviour changes). Match the prose tone of existing entries — explain WHAT shipped, WHY it matters, and any non-obvious gotcha (e.g. the iOS-26 4-arg signature for buttons). Link the feature doc.

- **<Feature name>.** One-sentence summary of what shipped and the
  primary entry point. Mention any iOS-26 / SimulatorKit gotcha worth
  preserving for future maintainers. See [`docs/features/<feature>.md`](docs/features/<feature>.md).

4c. Skill references

If the feature changed the CLI surface or wire-protocol envelope, also update:

  • skills/baguette/SKILL.md — the "What's wired vs what isn't" list.
  • skills/baguette/references/cli.md — new flags / subcommands.
  • skills/baguette/references/wire-protocol.md — new envelope shapes.

These files are what the agent skill loads; if they're stale, the next agent will mis-propose stale invocations.

Anti-patterns to avoid

  • Reaching for class to hold rich domain. Use struct (or enum) with extension methods. Swift structs already have methods, computed properties, protocol conformance, and auto-synthesised Equatable / Hashable / Sendable — everything rich domain needs. final class is reserved for boundaries that genuinely need reference semantics (Server, IndigoHIDInput, CoreSimulators, LiveChromes, H264Encoder). See references/domain-models.md for the full rule.
  • Plumbing chrome (or any aggregate) into IndigoHIDInput. SRP violation — the adapter's job is wire-format dispatch, not domain resolution. Resolve overrides at the call site or on the rich domain value.
  • Positional triples on the JS side. simInput.button(name, dur, hidUsage) is wrong shape; either accept an options object or move the resolution back into Swift.
  • type / key reaching the wire as a no-op fallback. If you can't implement a feature, fail loudly with a parse error — silent drops mid-string are worse than an explicit {"ok":false,"error":"…"}.
  • Adding a Mockable abstraction with one concrete impl just for testing. IndigoHIDInput is allowed to be the only Input impl; don't invent a parallel hierarchy. The exception is the orchestrator + collaborator split for 3rd-party I/O — there the collaborator (Subprocess) earns its MockSubprocess because the state machine is real.
  • Naming an abstraction XxxPort / XxxService / XxxManager, or suffixing aggregate persistence with XxxRepository. These are pattern labels, not domain nouns. The codebase uses role-named protocols (Input, Screen, Accessibility, LogStream, DeviceHost, Subprocess). For aggregate CRUD specifically, use the plural collection noun (Simulators, Chromes, Books) — not BookRepository. If the noun isn't obvious, the abstraction probably shouldn't exist yet — keep the logic inline or push it into a pure helper instead.
  • Cramming the irreducible private-API call AND the orchestration logic into one Infrastructure file. That's the original sin that kept AXPTranslatorAccessibility and SimDeviceLogStream at 18% / 33% coverage. Always split per the Phase-2 rule.
  • MOCKING outside the test target. It's .debug-only by design so release builds carry no mock code. Don't reach for MockXxx from production code.

References

Checklist (use TaskCreate for non-trivial features)

Phase 0 — Architecture

  • Wire JSON shape sketched (required vs optional fields)
  • CLI subcommand + flag names follow existing patterns
  • Domain types listed (value types + which @Mockable abstraction is added/changed)
  • New abstraction names are domain nouns (no XxxPort / XxxService / XxxManager; aggregate CRUD uses the plural collection noun like Books, not BookRepository)
  • Adapter private-API symbol + arg signature verified against a known-good source
  • If the adapter wraps 3rd-party I/O, the orchestrator-vs-collaborator split is decided (one-shot factory vs @Mockable collaborator)
  • iOS-26 gotchas flagged (MainActor? new symbol? signature drift?)
  • User has approved the design

Phase 1 — Domain (red → green → refactor)

  • Failing test in Tests/BaguetteTests/<Context>/
  • Value types in Domain/<Context>/
  • Rich-domain method on the value (e.g. .press(...))
  • If the feature introduces a Domain pure factory (AXNode.walk, LineBuffer.append, AXFrameTransform.map), it's covered at 100%
  • If a new collaborator was introduced, it's @Mockable and named for its domain role
  • swift test --filter <Suite> green

Phase 2 — Infrastructure

  • Existing abstraction extended with a doc comment (or new collaborator added)
  • Production adapter impl with [<context>] log lines at branches
  • If 3rd-party I/O: orchestrator depends on any <Collaborator>, and the Host<Collaborator> is a thin (~30–50 LOC) integration-only file
  • Mockable test stubs return values; verify(...).called(N) on the right method
  • Coverage: orchestrator ≥ 90%; pure factories at 100%
  • swift test green

Phase 3 — Wiring

  • GestureRegistry.standard registers the new gesture (gestures only)
  • CLI subcommand registered in RootCommand; CommandParsingTests updated
  • Browser changes (if any) span sim-input.js, sim-input-bridge.js, relevant IIFE, and BOTH sim.html + farm/farm.html script tags
  • Manual smoke test on a booted sim (the irreducible private-API call is integration-only — make sure it actually works end-to-end)

Phase 4 — Docs + Changelog

  • docs/features/<feature>.md created or updated
  • CHANGELOG.md Unreleased entry written in the existing prose tone
  • skills/baguette/SKILL.md "What's wired" list updated (if CLI/wire changed)
  • skills/baguette/references/cli.md updated (if CLI changed)
  • skills/baguette/references/wire-protocol.md updated (if wire changed)

Version History

  • b537fbd Current 2026-07-24 16:32

Same Skill Collection

skills/baguette/SKILL.md

Metadata

Files
0
Version
87c93db
Hash
ba2cf64b
Indexed
2026-07-24 16:32

Главная - Вики-сайт
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-08-20 06:37
浙ICP备14020137号-1 $Гость$