Thoughts About Hijinx

Yes — seeing the existing simulation.area and simulation.region shapes helps a lot. The Kubernetes-ish version fits your system very naturally because your areas already expose the exact thing hijinx need: bounded hosts, environmental conditions, affordances, constraints, and region/feature membership.

The area schema is especially important because affordances are already typed, classified, input/output-aware activities with salience. That is basically the scheduler surface for hijinx binding. An area can already say “I support area.activity.music_venue and area.activity.ensemble_session,” with inputs like agent.musician, agent.human, and item.instrument, and outputs like signal.performance and signal.musical_coherence.

So yes: I would not make “incident” primarily Lua. I would make it a declarative resource.

The shape I see is not one kind, but three related kinds:

kind: rally.incident

Defines the pressure primitive.

kind: rally.hijinx

Binds one or more incidents to an area/environment/social frame.

kind: rally.hijinx.instance

Runtime instance: this specific party started this specific nonsense here, with these witnesses, pouch slots, and filled context.

That gives you a clean split between “definition,” “template,” and “running thing.”

The most Kubernetes-like shape is:

apiVersion: net.plantange/v1
kind: rally.incident

meta:
  title: Stuck Note
  name:
    common:
      en: stuck_note
  uuid: 02bd9100-a417-7f00-000000000100
  description:
    common:
      en: "A note is stuck in the room. Keep playing through it until the room releases."

spec:
  class: musical_pressure
  scope: activity
  tags:
    - kurzz
    - music
    - pressure
    - recovery

  requires:
    affordances:
      - type: area.activity.ensemble_session
        min_salience: 0.50
      - type: area.activity.music_venue
        min_salience: 0.25
    signals:
      - signal.jazz_session
      - signal.musical_coherence

  parameters:
    stuck_pitch_midi:
      type: integer
      default: 60
      fill:
        strategy: fixed
    clear_target_count:
      type: integer
      default: 7
    cooldown_steps:
      type: integer
      default: 8

  state:
    fields:
      active:
        type: boolean
        default: false
      clear_progress_count:
        type: integer
        default: 0
      ai_contribution_count:
        type: integer
        default: 0
      participant_contribution_count:
        type: integer
        default: 0
      triggered_at_step:
        type: integer
        nullable: true
      cleared_at_step:
        type: integer
        nullable: true

  inputs:
    - name: ai_matching_hits_this_step
      source: incident.ai_matching_hits_this_step
      type: integer
      default: 0
    - name: participant_matching_hits_this_step
      source: incident.participant_matching_hits_this_step
      type: integer
      default: 0
    - name: step
      source: pop.step
      type: integer
      default: 0

  lifecycle:
    start:
      when:
        all:
          - cooldown_ready: true
          - no_active_incident_in_lane: true
      set:
        active: true
        clear_progress_count: 0
        ai_contribution_count: 0
        participant_contribution_count: 0
        triggered_at_step: "${inputs.step}"
      emit:
        - event: incident.stuck_note.started.v1

    progress:
      when:
        expression: "inputs.ai_matching_hits_this_step + inputs.participant_matching_hits_this_step > 0"
      increment:
        clear_progress_count: "inputs.ai_matching_hits_this_step + inputs.participant_matching_hits_this_step"
        ai_contribution_count: "inputs.ai_matching_hits_this_step"
        participant_contribution_count: "inputs.participant_matching_hits_this_step"
      emit:
        - event: incident.stuck_note.progress.v1

    clear:
      when:
        expression: "state.clear_progress_count >= parameters.clear_target_count"
      set:
        active: false
        cleared_at_step: "${inputs.step}"
      emit:
        - event: incident.stuck_note.cleared.v1

  outcomes:
    cleared_by:
      type: enum
      values:
        - participant
        - ai
        - mixed
      derive:
        participant: "state.participant_contribution_count > 0 and state.ai_contribution_count == 0"
        ai: "state.ai_contribution_count > 0 and state.participant_contribution_count == 0"
        mixed: "state.participant_contribution_count > 0 and state.ai_contribution_count > 0"

  presentation:
    pitch_label:
      from_midi: "${parameters.stuck_pitch_midi}"

    started:
      display_text: "Stuck note ${pitch_label}: a note is stuck in the room."
      strategy_hint_text: "Hammer ${pitch_label} to clear, or build around it."
      barks:
        - speaker: jax
          text: "Answer me, drummer."

    progress:
      display_text: "Stuck note ${pitch_label}: ${state.clear_progress_count}/${parameters.clear_target_count} cleared."
      strategy_hint_text: "Hammer ${pitch_label} to clear, or build around it."

    cleared:
      variants:
        - when: "outcomes.cleared_by == 'participant'"
          display_text: "You forced ${pitch_label} back into the room's pocket."
          strategy_hint_text: "Result: strong recovery. Next: leave one bar of space, then re-enter clean."
          barks:
            - speaker: mira
              text: "Ugly save. Useful save."
        - when: "outcomes.cleared_by == 'mixed'"
          display_text: "You and the band folded ${pitch_label} back into the groove."
          strategy_hint_text: "Result: shared recovery. Next: keep pocket steady and avoid overplaying."
          barks:
            - speaker: jax
              text: "There. Keep that pocket."
        - default: true
          display_text: "The band absorbed ${pitch_label} while you held position."
          strategy_hint_text: "Result: passive save. Next: answer sooner on the next incident window."
          barks:
            - speaker: mira
              text: "Leave it. Let it breathe."

  rally:
    deltas:
      started:
        escalation: 0.06
        trust: 0.0
        survivability: -0.02
        reason: stuck_note_started
      progress:
        escalation: 0.0
        trust: 0.0
        survivability: 0.0
        reason: stuck_note_progress
      cleared:
        escalation: -0.07
        trust: 0.08
        survivability: 0.05
        reason: stuck_note_cleared

That spec replaces most of the Lua you showed. The Lua host becomes an interpreter/executor for a small incident DSL instead of a bespoke incident script.

The only things that still need code are the generic operations:

cooldown_ready
no_active_incident_in_lane
increment counters
evaluate expressions
derive cleared_by
emit events
format presentation strings
apply Rally deltas
convert MIDI to pitch label

That is the right direction. Stuck Note should not need custom Lua to say “add matching hits to progress; clear at seven.” That is schema territory.

Now, the more important move is how this gets instanced in a narrative frame.

That is where rally.hijinx comes in.

apiVersion: net.plantange/v1
kind: rally.hijinx

meta:
  title: Make A Scene
  name:
    common:
      en: make_a_scene
  uuid: 02bd9100-a417-7f00-000000000200
  description:
    common:
      en: "The party deliberately attracts attention to bend the room, buy time, or turn confusion into local memory."

spec:
  activation:
    mode: player_or_party
    offered_when:
      area:
        affordances:
          any:
            - type: area.activity.music_venue
              min_salience: 0.50
            - class: spectacle
              min_salience: 0.50
            - class: social
              min_salience: 0.50
      party:
        requires:
          - agent.human
          - agent.musician

  binding:
    hosts:
      area:
        kind: simulation.area
      region:
        kind: simulation.region
        optional: true

    environmental_frame:
      from:
        - area.environment
        - region.climate
        - current_activity
        - rally.route_state

    affordances:
      stage:
        select:
          from: area.affordances
          prefer:
            - class: spectacle
            - type: area.activity.music_venue
          fallback:
            - class: social

      witness:
        select:
          from: social_frame.present_groups
          prefer:
            - largest
            - highest_attention

      pressure:
        select:
          from: rally.pressures
          prefer:
            - highest

  pouch_slots:
    reservations:
      - scope: party
        type: active_scheme
        count: 1
      - scope: area
        type: public_attention
        count: 1

  questions:
    - id: intended_audience
      prompt: "Who are you trying to distract?"
      choices:
        - id: crowd
          label: "the crowd"
        - id: authority
          label: "the nearest authority"
        - id: rival
          label: "a rival"
        - id: nobody
          label: "nobody, which is worse"

  context_fill:
    motive:
      strategy: from_highest_rally_pressure
    stage:
      strategy: from_binding
      binding: stage
    witness:
      strategy: from_binding
      binding: witness
    authority:
      strategy: nearest_actor_with_role
      role: authority
      fallback: area_owner
    tone:
      strategy: random_enum
      values:
        - useful
        - suspicious
        - accidentally_artistic
        - technically_not_illegal

  incidents:
    mode: pool
    select:
      count: 1
      from:
        - ref: rally.incident/stuck_note
          weight: 1
        - ref: rally.incident/side_door
          weight: 1
        - ref: rally.incident/cover_groove
          weight: 2
        - ref: rally.incident/the_mechanic
          weight: 1

  residue:
    on_resolved:
      - type: rumor
        key: local_rumor
        text: "The town remembers the band as helpful maniacs."
      - type: rally_delta
        trust: 0.04
        escalation: 0.02
      - type: affordance_memory
        target: area
        tag: remembers_public_scene
        duration: local_arc

    on_failed:
      - type: warning
        key: public_warning
        text: "The room decided this was not charming."
      - type: rally_delta
        trust: -0.03
        escalation: 0.08

This is the “instanced in a narrative frame” part.

The area supplies the host. The hijinx binds the affordances. The incident supplies the pressure.

At runtime, you produce an instance:

apiVersion: net.plantange/v1
kind: rally.hijinx.instance

meta:
  title: Make A Scene at Kurzz Performance Stage
  name:
    common:
      en: make_a_scene_kurzz_stage_001
  uuid: 02bd9100-a417-7f00-000000000300

spec:
  template:
    ref: rally.hijinx/make_a_scene

  status:
    phase: active

  activation:
    mode: party
    started_at_step: 144
    started_by:
      party_id: party.novan

  bindings:
    area:
      ref: simulation.area/performance_stage
      uuid: 02bd9100-a417-7f00-000000000001

    selected_affordances:
      stage:
        type: area.activity.music_venue
        class: spectacle
      session:
        type: area.activity.ensemble_session
        class: social

    environmental_frame:
      noise_level: 0.75
      light_level: 0.85
      visibility: 0.90

    narrative:
      intended_audience: crowd
      tone: technically_not_illegal
      motive: make_gas_money
      witness: room
      authority: venue_owner

  pouch_slots:
    reserved:
      - scope: party
        owner: party.novan
        type: active_scheme
        count: 1
      - scope: area
        owner: simulation.area/performance_stage
        type: public_attention
        count: 1

  incidents:
    active:
      - ref: rally.incident/stuck_note
        instance_id: incident.stuck_note.144
        parameters:
          stuck_pitch_midi: 60
          clear_target_count: 7

  residue:
    pending:
      - local_rumor
      - rally_delta
      - affordance_memory

That is the exact Kubernetes mental model:

rally.incident = Deployment template / workload definition
rally.hijinx = higher-level controller spec
rally.hijinx.instance = live object/status
simulation.area = node/namespace/host surface
simulation.region = broader topology/environment provider

The analogy is not perfect, but it is useful.

The simulation.area is not just a location. It is a schedulable host. Its schema says areas have spatial envelope, environmental field conditions, affordances, and constraints.

The simulation.region gives broader topology: features, natives, surface, climate, hydrology, subdivisions, anchors. That makes it useful as a wider binding and context source for hijinx that depend on region-level conditions or features.

So the scheduling flow becomes:

Area declares affordances.
Region supplies broader context.
Hijinx template checks whether it can bind.
Pouch slots decide whether the party can carry it.
Hijinx fills narrative/environment context.
Hijinx deploys one or more incident specs.
Incident instance runs through generic lifecycle.
Hijinx resolves into residue.

For static area incidents, I think your instinct is exactly right. They become auto hijinx:

apiVersion: net.plantange/v1
kind: rally.hijinx

meta:
  title: Millstone Remembers
  name:
    common:
      en: millstone_remembers
  uuid: 02bd9100-a417-7f00-000000000400

spec:
  activation:
    mode: auto
    schedule:
      trigger: area_entered
      cooldown_steps: 256
      probability: 0.35

  binding:
    hosts:
      area:
        kind: simulation.area
        match:
          meta.name.common.en: millstone_bridge

  context_fill:
    forbidden_region:
      strategy: fixed
      value: bridge_sequence
    witness:
      strategy: random_enum
      values:
        - old_man
        - deputy
        - venue_owner
        - nobody_visible

  incidents:
    mode: sequence
    sequence:
      - ref: rally.incident/forbidden_bridge

  residue:
    on_resolved:
      - type: affordance_memory
        target: area
        tag: survived_millstone_bridge
    on_failed:
      - type: rumor
        key: millstone_bridge_played

That gives you one unifying model:

Player-started hijinx:
activation.mode = player_or_party

Static area incident:
activation.mode = auto

Both:
bind area + context + incident + residue

That is clean. And it lets you keep the whimsy without writing bespoke scripts for every goat-adjacent misunderstanding.

My suggested kind taxonomy:

simulation.area
A bounded host with environment, affordances, and constraints.

simulation.region
A larger context field containing areas/features/natives/climate/topology.

rally.incident
A declarative pressure unit with lifecycle, inputs, progress, clear, presentation, and immediate deltas.

rally.hijinx
A declarative scheme/context wrapper that binds incidents to area affordances, environmental frame, player/party intent, pouch slots, and residue.

rally.hijinx.instance
A live instantiation of a hijinx with filled context, reserved pouch slots, active incidents, and pending/resolved residue.

The main design warning: do not let rally.incident grow upward into hijinx. Keep it dumb and sharp.

An incident should know:

what pressure is active
what inputs count
how progress happens
when it clears
what immediate feedback to emit

A hijinx should know:

why this is happening here
who started it
what area affordances are being abused
who might remember
what slots are occupied
what residue remains

That keeps Stuck Note from caring whether it is a tutorial problem, a random Kurzz pressure event, or part of “Make A Scene at the funeral home.” It is just the stuck note. The hijinx makes it funny, local, and consequential.