Ports retail's ParticleEmitterInfo / Particle::Init / Particle::Update
(0x005170d0..0x0051d400) and PhysicsScript runtime to a C# data-layer
plus a Silk.NET billboard renderer. Sky-PES path is debug-only behind
ACDREAM_ENABLE_SKY_PES because named-retail decomp confirms GameSky
copies SkyObject.pes_id but never reads it (CreateDeletePhysicsObjects
0x005073c0, MakeObject 0x00506ee0, UseTime 0x005075b0).
Post-review fixes folded into this commit:
H1: AttachLocal (is_parent_local=1) follows live parent each frame.
ParticleSystem.UpdateEmitterAnchor + ParticleHookSink.UpdateEntityAnchor
let the owning subsystem refresh AnchorPos every tick — matches
ParticleEmitter::UpdateParticles 0x0051d2d4 which re-reads the live
parent frame when is_parent_local != 0. Drops the renderer-side
cameraOffset hack that only worked when the parent was the camera.
H3: Strip the long stale comment in GfxObjMesh.cs that contradicted the
retail-faithful (1 - translucency) opacity formula. The code was
right; the comment was a leftover from an earlier hypothesis and
would have invited a wrong "fix".
M1: SkyRenderer tracks textures whose wrap mode it set to ClampToEdge
and restores them to Repeat at end-of-pass, so non-sky renderers
that share the GL handle can't silently inherit clamped wrap state.
M2: Post-scene Z-offset (-120m) only fires when the SkyObject is
weather-flagged AND bit 0x08 is clear, matching retail
GameSky::UpdatePosition 0x00506dd0. The old code applied it to
every post-scene object — a no-op today (every Dereth post-scene
entry happens to be weather-flagged) but a future post-scene-only
sun rim would have been pushed below the camera.
M4: ParticleSystem.EmitterDied event lets ParticleHookSink prune dead
handles from the per-entity tracking dictionaries, fixing a slow
leak where naturally-expired emitters' handles stayed in the
ConcurrentBag forever during long sessions.
M5: SkyPesEntityId moves the post-scene flag bit to 0x08000000 so it
can't ever overlap the object-index range. Synthetic IDs stay in
the reserved 0xFxxxxxxx space.
New tests (ParticleSystemTests + ParticleHookSinkTests):
- UpdateEmitterAnchor_AttachLocal_ParticlePositionFollowsLiveAnchor
- UpdateEmitterAnchor_AttachLocalCleared_ParticleFrozenAtSpawnOrigin
- EmitterDied_FiresOncePerHandle_AfterAllParticlesExpire
- Birthrate_PerSec_EmitsOnePerTickWhenIntervalElapsed (retail-faithful
single-emit-per-frame behavior)
- UpdateEntityAnchor_WithAttachLocal_MovesParticleToLiveAnchor
- EmitterDied_PrunesPerEntityHandleTracking
dotnet build green, dotnet test green: 695 / 393 / 243 = 1331 passed
(up from 1325).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
376 lines
17 KiB
Markdown
376 lines
17 KiB
Markdown
# Phase C.1 — PES particle rendering (sky aurora + portals + smoke)
|
||
|
||
**Status:** specced, not started.
|
||
**Filed:** 2026-04-27 (handoff from sky/weather session, branch merged at f7c9e88).
|
||
**Worktree:** to be created at `.worktrees/phase-c1-particles` on branch `feature/phase-c1-particles`.
|
||
|
||
**2026-04-28 correction:** named-retail decompile disproves the sky-PES
|
||
premise in this spec. `SkyDesc::GetSky` copies `default_pes_object` into
|
||
`CelestialPosition.pes_id`, but `GameSky::CreateDeletePhysicsObjects`
|
||
(`0x005073c0`), `GameSky::MakeObject` (`0x00506ee0`), and
|
||
`GameSky::UseTime` (`0x005075b0`) never read it. C.1 remains valid as the
|
||
generic PhysicsScript/particle renderer for real hooks, portals, smoke, etc.,
|
||
but per-SkyObject PES playback is debug-only and disabled by default.
|
||
|
||
---
|
||
|
||
## What you're building
|
||
|
||
A retail-faithful **PES (Particle Effect Schedule)** rendering system. PES is
|
||
retail's name for "scripted vertex-sprite emitter". The dat carries a PES file
|
||
per effect ID (e.g. `0x33000453`) describing how particles spawn, advect,
|
||
color-cycle, and expire. Retail attaches a PES to sky objects, motion-hook
|
||
events, and portal swirls. Without it, several visible-in-retail effects look
|
||
flat or absent in acdream:
|
||
|
||
- The dynamic "aurora-like light play" the user sees in retail's Rainy/Cloudy
|
||
sky is **not** a separate aurora system. It's PES particles attached to
|
||
SkyObjects via the `pes_id` field of `CelestialPosition` (verbatim retail
|
||
header at `acclient.h:35451`). See ISSUES.md #28 + #29.
|
||
- Portal swirls (rotating black disks today — see roadmap "Open visual
|
||
defects" → "Portals render as a rotating black disk").
|
||
- Chimney smoke + fireplace flames (`References/ACViewer/Physics/Particles/`
|
||
for the visual model).
|
||
- Spell cast effects (some are PES, some are setup-mesh-based).
|
||
|
||
Phase **E.3** already shipped the **data layer**: `ParticleSystem`,
|
||
`EmitterDescLoader`, `ParticleHookSink`, `PhysicsScriptRunner`, `VfxModel`
|
||
in `src/AcDream.Core/Vfx/`. **C.1** is the visual half — the GL renderer plus
|
||
SkyRenderer integration.
|
||
|
||
---
|
||
|
||
## Phase 0 — worktree setup
|
||
|
||
```bash
|
||
git -C C:/Users/erikn/source/repos/acdream worktree add .worktrees/phase-c1-particles -b feature/phase-c1-particles main
|
||
cd C:/Users/erikn/source/repos/acdream/.worktrees/phase-c1-particles
|
||
dotnet build && dotnet test
|
||
```
|
||
|
||
Expected: green baseline before touching code. If red, stop and investigate
|
||
(do not assume merge artifact).
|
||
|
||
---
|
||
|
||
## Step 0 — GREP NAMED FIRST
|
||
|
||
Per `CLAUDE.md`'s "Development workflow" rule, before any AC-specific
|
||
implementation step, grep `docs/research/named-retail/acclient_2013_pseudo_c.txt`
|
||
by `class::method` name. For PES specifically, search:
|
||
|
||
```bash
|
||
grep -nE "PhysicsScript::|PhysicsObj::PlayScript|PartArray::CreateSetup" \
|
||
docs/research/named-retail/acclient_2013_pseudo_c.txt | head -40
|
||
|
||
grep -nE "ParticleEmitter::|EmitterInfo::|EmitterDesc::|PESystem" \
|
||
docs/research/named-retail/acclient_2013_pseudo_c.txt | head -40
|
||
|
||
grep -nE "CreateParticleEmitter|DestroyParticleEmitter|RemoveParticleEmitter" \
|
||
docs/research/named-retail/acclient_2013_pseudo_c.txt | head -20
|
||
```
|
||
|
||
Then check `docs/research/named-retail/acclient.h` for verbatim retail structs:
|
||
- `EmitterInfo` (per-emitter description)
|
||
- `EmitterDesc` (per-particle description: lifetime, color curve, motion)
|
||
- `ParticleEmitter` (runtime instance)
|
||
- `PhysicsScript` (parsed `0x34xxxxxx` PES file: list of "spawn this emitter at this time" entries)
|
||
|
||
Once you have the named functions/structs, write a brief
|
||
`docs/research/2026-04-XX-pes-pseudocode.md` translating the C decomp into
|
||
clean pseudocode before porting (per CLAUDE.md "WRITE PSEUDOCODE" step). The
|
||
sky/weather session repeatedly proved this catches misinterpretations before
|
||
they become bugs.
|
||
|
||
---
|
||
|
||
## What we already have (Phase E.3 data-layer)
|
||
|
||
Read these files first — they encode the dat schema and runtime model:
|
||
|
||
- `src/AcDream.Core/Vfx/EmitterDescLoader.cs` — loads `EmitterInfo` /
|
||
`EmitterDesc` from the dat. Confirm field set matches retail's
|
||
`acclient.h` struct definitions.
|
||
- `src/AcDream.Core/Vfx/ParticleSystem.cs` — runtime stepper. **Verify**
|
||
the 13 motion-type integrators match retail's
|
||
`PhysicsScript::MotionType` enum exactly. The roadmap claims they do.
|
||
- `src/AcDream.Core/Vfx/PhysicsScriptRunner.cs` — schedules emitters per
|
||
the parsed PES. This is the timeline driver.
|
||
- `src/AcDream.Core/Vfx/ParticleHookSink.cs` — receives `CreateParticle`
|
||
hook calls from MotionInterpreter / AnimationSequencer (Phase E.1).
|
||
- `src/AcDream.Core/Vfx/VfxModel.cs` — the per-entity particle state.
|
||
|
||
Tests for these are in `tests/AcDream.Core.Tests/Vfx/`. Run them first to
|
||
confirm the data-layer baseline still passes after any refactor.
|
||
|
||
**What is NOT yet there** (and is C.1's job):
|
||
|
||
- A GL renderer that consumes `ParticleSystem.LiveParticles` and draws
|
||
billboarded quads.
|
||
- SkyRenderer integration so `CelestialPosition.pes_id` actually spawns a
|
||
PES — currently `SkyDescLoader` drops the field on the floor (see
|
||
`SkyObjectData` at `src/AcDream.Core/World/SkyDescLoader.cs:28-54`).
|
||
- Per-entity emitter spawn for non-sky cases (chimneys, portals, spell
|
||
effects). Probably a wiring exercise once the renderer exists.
|
||
|
||
---
|
||
|
||
## References (priority order)
|
||
|
||
Per `CLAUDE.md`'s "Reference hierarchy by domain" — particle systems are
|
||
client-side visual, so the priority is:
|
||
|
||
1. **`docs/research/named-retail/acclient_2013_pseudo_c.txt`** — the actual
|
||
retail client, fully named. Beats every other reference.
|
||
2. **`docs/research/named-retail/acclient.h`** — verbatim retail struct
|
||
definitions for `EmitterInfo`, `EmitterDesc`, `PhysicsScript`,
|
||
`ParticleEmitter`, `CelestialPosition`.
|
||
3. **`references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/ParticleBatcher.cs`**
|
||
— exact Silk.NET stack match, has the GL-side billboarded-quad batcher
|
||
we want to port. Check `ParticleEmitter` and rendering glue around it.
|
||
4. **`references/ACViewer/ACViewer/Physics/Particles/`** — MonoGame, has
|
||
the visual algorithms. Use as cross-check on color cycle + motion math.
|
||
5. **`references/ACE/Source/`** — server-side, mostly irrelevant for client
|
||
particles, but ACE's `EmitterDesc` structs can confirm field types.
|
||
6. **`references/AC2D/`** — older C++ AC client. Smaller scope, useful
|
||
cross-check.
|
||
7. **`references/Chorizite.ACProtocol/`** — clean-room protocol library;
|
||
PES is dat-side not protocol-side, so probably not relevant.
|
||
|
||
When two references disagree: decomp wins. Always.
|
||
|
||
---
|
||
|
||
## What we already learned (data points the next session shouldn't re-derive)
|
||
|
||
### CelestialPosition struct (verbatim retail header `acclient.h:35451`)
|
||
|
||
```c
|
||
struct CelestialPosition {
|
||
IDClass<_tagDataID,32,0> gfx_id;
|
||
IDClass<_tagDataID,32,0> pes_id; // ← particle scheduler ID
|
||
float heading;
|
||
float rotation;
|
||
AC1Legacy::Vector3 tex_velocity;
|
||
float transparent;
|
||
float luminosity;
|
||
float max_bright;
|
||
unsigned int properties;
|
||
};
|
||
```
|
||
|
||
### Dispatch path (named retail decomp)
|
||
|
||
- `GameSky::CreateDeletePhysicsObjects` at offset `0x005073c0` (decomp
|
||
~269036) iterates `sky_obj_pos` and calls `MakeObject(gfx_id, ...)`. The
|
||
current acdream port mirrors this for `gfx_id`. **The `pes_id` field is
|
||
silently dropped.** Wire it.
|
||
- `CPhysicsObj::InitPartArrayObject` at decomp ~280484 dispatches by type
|
||
prefix — type 6 → direct GfxObj, type 7 → `CPartArray::CreateSetup`.
|
||
This is the existing path C.1 needs to extend with PES-emitter spawn.
|
||
- `CPartArray::CreateSetup` at decomp ~287490 → `SetSetupID` →
|
||
`InitDefaults` (loads animations + scripts + physics-script-table).
|
||
Note: the **physics-script-table** mentioned here is the PES dispatcher.
|
||
|
||
### Properties bit semantics (decomp 268704+ = `GameSky::Draw`)
|
||
|
||
- bit `0x01` — post-scene placement (`after_sky_cell`). Acdream now
|
||
honours this (commit `034a684`). Foreground rain & particle emitters
|
||
with bit 0x01 set should render in the post-scene weather pass.
|
||
- bit `0x02` — hidden when fog override is active.
|
||
- bit `0x04` — only render if `LScape::weather_enabled`. Most PES-bearing
|
||
rows have this set.
|
||
- bit `0x08` — purpose unknown; seen on every PES-bearing entry. Worth
|
||
decoding in C.1 for completeness. Likely "this object owns a PES" or
|
||
"this is a transient effect".
|
||
|
||
### Specific PES IDs in Dereth (probe-confirmed)
|
||
|
||
`tools/StarsProbe/Program.cs` already enumerates these. For Rainy DG3:
|
||
|
||
| OI | Active window | Gfx | **PES** | Notes |
|
||
|----|--------------|-----|---------|-------|
|
||
| 5 | always | 0x02000714 | **0x330007DB** | low-rate background |
|
||
| 7 | 0.03–0.19 | 0x02000BA6 | **0x33000453** | early morning rain |
|
||
| 8 | 0.91–0.98 | 0x02000BA6 | **0x33000453** | late evening rain |
|
||
| 11 | 0.025–0.030 | 0x02000588 | **0x33000428** | dawn flash |
|
||
| 12 | 0.190–0.200 | 0x02000588 | **0x33000428** | morning end |
|
||
| 13 | 0.030–0.190 | 0x02000589 | **0x3300042C** | morning |
|
||
| 14 | 0.905–0.910 | 0x02000588 | **0x33000428** | dusk start |
|
||
| 15 | 0.980–0.990 | 0x02000588 | **0x33000428** | dusk end |
|
||
| 16 | 0.910–0.980 | 0x02000589 | **0x3300042C** | evening |
|
||
| **17** | **0.270–0.910** | 0x02000589 | **0x3300042C** | **most of daytime — pick this for first visual test** |
|
||
| 18 | 0.400–0.500 | 0x02000BA6 | 0x33000453 | midday burst |
|
||
|
||
Use **`0x3300042C`** as the canonical "render this and confirm it shows up"
|
||
target. It's active during normal daytime in any Rainy DayGroup; the user
|
||
can compare side-by-side with retail at the same in-game time.
|
||
|
||
### Already-working sky pass
|
||
|
||
The sky/weather session (commits `97fc1b5`..`e4bc6de`, all merged at
|
||
`f7c9e88`) shipped:
|
||
- retail-faithful `SunColor` / `AmbientColor` magnitude (`|sunVec|` formula)
|
||
- bit-0x01 post-scene partition
|
||
- Translucent-flag override on `FromSurfaceType` (cloud blend mode)
|
||
- Setup-backed (`0x020xxxxx`) sky object loading via `SetupMesh.Flatten`
|
||
- Sky fog + additive-fog-skip
|
||
|
||
So when you launch with `ACDREAM_DAT_DIR=...`, the static sky meshes already
|
||
render correctly. PES is the missing dynamic layer — adding it should be
|
||
strictly additive on top of the existing visuals.
|
||
|
||
---
|
||
|
||
## Implementation outline
|
||
|
||
Skeleton; the next session should fill in details after Step 0 grep + reading
|
||
the existing E.3 code.
|
||
|
||
### C.1.0 — Decomp pseudocode + verify E.3 matches
|
||
|
||
- Grep + read `PhysicsScript`, `EmitterDesc`, `EmitterInfo`,
|
||
`ParticleEmitter` in the named decomp.
|
||
- Translate to pseudocode in a `docs/research/2026-04-XX-pes-pseudocode.md`
|
||
file. Cite line numbers.
|
||
- Diff E.3's `EmitterDescLoader` field-by-field against the decomp. If
|
||
E.3 is wrong, add a regression test before fixing.
|
||
|
||
### C.1.1 — `PesObjectId` capture in `SkyDescLoader`
|
||
|
||
- `src/AcDream.Core/World/SkyDescLoader.cs:28-54` — extend `SkyObjectData`
|
||
with `uint PesObjectId` (currently dropped). Set it from
|
||
`CelestialPosition.pes_id` in the dat.
|
||
- Tests: golden-value test that `0x3300042C` flows from a fixture region
|
||
through to `SkyObjectData.PesObjectId`.
|
||
|
||
### C.1.2 — PES file decode (if E.3 doesn't already do it)
|
||
|
||
- DatReaderWriter probably has a generated `PhysicsScript` reader. Confirm.
|
||
If so, no work here. If not, implement decode + tests.
|
||
|
||
### C.1.3 — Emitter spawn in SkyRenderer
|
||
|
||
- `src/AcDream.App/Rendering/Sky/SkyRenderer.cs` — when a SkyObject has
|
||
`PesObjectId != 0`, request the `ParticleSystem` to spawn that PES
|
||
attached to the SkyObject's celestial position.
|
||
- Schedule per the SkyObject's `BeginTime` / `EndTime` window — only emit
|
||
while the day fraction is inside the window. Mirrors retail's
|
||
`GameSky::CreateDeletePhysicsObjects` activation logic.
|
||
|
||
### C.1.4 — Particle GL renderer
|
||
|
||
This is the bulk of C.1. Follow `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/ParticleBatcher.cs`
|
||
as the structural template — same Silk.NET stack, mature pattern. Key
|
||
elements:
|
||
|
||
- **Vertex sprite (billboard)**: 4 verts per particle, oriented to face
|
||
the camera. Quad UVs from the particle's texture id + animation phase.
|
||
- **Batched draw**: dynamic VBO; rebuild each frame from
|
||
`ParticleSystem.LiveParticles`. Sort by Z when blend mode is alpha-blend.
|
||
- **Color cycle**: per particle, interpolate the `EmitterDesc.ColorCurve`
|
||
(RGBA over normalized lifetime) and modulate. Verify the curve evaluator
|
||
matches retail's `EmitterDesc::GetColor(t)` — likely linear-interp between
|
||
curve nodes.
|
||
- **Blend modes**: PES particles use the same `TranslucencyKind` resolution
|
||
as static meshes. `Additive` is by far the most common for sky particles.
|
||
- **Depth handling**: depth test on, depth write **off** (translucent sprites
|
||
shouldn't occlude each other or solid geometry).
|
||
- **Per-pass split**: PES on sky objects with bit 0x01 set render in the
|
||
post-scene pass; bit 0x01 clear render in the pre-scene pass with the dome.
|
||
|
||
Reference shader pair: probably a single VS+FS for billboarded textured
|
||
quads. Look at WorldBuilder's `Shaders/particle.vert/.frag` for prior art.
|
||
|
||
### C.1.5 — Wire other PES sources
|
||
|
||
After sky particles work, do a sweep:
|
||
- Portals — `WorldEntity` weenies of class `WeenieClassId.Portal` should
|
||
spawn a portal-swirl PES. Replace the placeholder rotating-black-disk
|
||
rendering.
|
||
- Chimneys / fireplaces — `EnvCell.StaticObjects` for inn/cottage cells
|
||
reference PES IDs. Confirm via probe.
|
||
- Animation-hooked particles — `MotionInterpreter` already invokes
|
||
`CreateParticle` hooks via `ParticleHookSink` (Phase E.1). Verify the
|
||
hook reaches the new renderer.
|
||
|
||
### C.1.6 — Visual verification
|
||
|
||
Per `CLAUDE.md`'s "Visual verification workflow":
|
||
1. Build green, tests green.
|
||
2. Launch live client (`ACDREAM_DAT_DIR=...`, `ACDREAM_LIVE=1`, etc.).
|
||
3. Stand at Holtburg outdoors during a Rainy DayGroup at midday-ish (DayGroup
|
||
selection is LCG-deterministic from year+day_of_year — control by
|
||
forcing a specific date if needed).
|
||
4. User confirms aurora-like light play matches retail in dual-client
|
||
side-by-side comparison.
|
||
|
||
Acceptance: ISSUES.md #28 closes; #29 likely closes too (the residual cloud
|
||
density gap is hypothesised to roll into #28).
|
||
|
||
---
|
||
|
||
## Pitfalls to avoid (lessons from prior sessions)
|
||
|
||
1. **Do NOT guess the PES file format.** It has a header, a list of frames,
|
||
each with an emitter-desc-id and start time. The exact byte layout is in
|
||
the decomp — read it before writing the decode.
|
||
2. **Do NOT integrate via subagent without context.** The animation sequencer
|
||
integration cost a 4-fix marathon when a subagent rewrote the transform
|
||
pipeline. PES → renderer is similarly central; the subagent must read
|
||
the existing Vfx/ + SkyRenderer/ before editing.
|
||
3. **Do NOT re-derive things probes already answered.** `tools/StarsProbe`
|
||
has the full SkyObject + PES enumeration; just read its log.
|
||
4. **Do NOT skip the pseudocode step.** Write a pseudocode doc per C.1.0
|
||
before porting.
|
||
5. **Decomp wins all ties.** WorldBuilder's `ParticleBatcher` is a fine
|
||
template for Silk.NET idioms but its PES interpretation is a port —
|
||
if it disagrees with the decomp, the decomp is right.
|
||
6. **Translucent ≠ Additive ≠ AlphaBlend at the surface level.** The
|
||
sky/weather session learned this the hard way. PES particles will hit
|
||
the same `TranslucencyKindExtensions.FromSurfaceType` resolution; the
|
||
fixes already merged (`375065b`) handle the `Translucent + ClipMap`
|
||
override. Don't undo that.
|
||
7. **Bit `0x08` in SkyObject.Properties** is undecoded as of the merge.
|
||
Worth grepping for `& 0x08` or `& 8` in the named decomp during C.1.0
|
||
to determine if it gates PES specifically. If yes, only spawn a PES
|
||
when the bit is set.
|
||
|
||
---
|
||
|
||
## Critical files
|
||
|
||
- `docs/research/named-retail/acclient_2013_pseudo_c.txt` — primary oracle.
|
||
- `docs/research/named-retail/acclient.h` — verbatim retail structs.
|
||
- `docs/research/named-retail/symbols.json` — symbol → address lookup.
|
||
- `docs/ISSUES.md` — issues #28 (aurora root cause) and #29 (residual gap).
|
||
- `docs/plans/2026-04-11-roadmap.md` — phase identifier (C.1).
|
||
- `src/AcDream.Core/Vfx/` — Phase E.3 data-layer scaffolding.
|
||
- `src/AcDream.Core/World/SkyDescLoader.cs:28-54` — `SkyObjectData`,
|
||
needs `PesObjectId` capture.
|
||
- `src/AcDream.App/Rendering/Sky/SkyRenderer.cs` — sky pass; needs
|
||
particle emit + draw call.
|
||
- `tools/StarsProbe/Program.cs` — probe with PES IDs already enumerated.
|
||
- `tools/RainMeshProbe/Program.cs` — sky surface flag dump.
|
||
- `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/ParticleBatcher.cs`
|
||
— Silk.NET particle batcher template.
|
||
- `references/ACViewer/ACViewer/Physics/Particles/` — visual algorithm
|
||
cross-check.
|
||
|
||
---
|
||
|
||
## Rules of engagement (per CLAUDE.md)
|
||
|
||
- **Step 0 grep named first** before any AC-specific implementation.
|
||
- **One hypothesis at a time.** No bundled fixes. If a fix fails, return to
|
||
Phase 1 of `superpowers:systematic-debugging`, not Fix #2.
|
||
- **Three failed fixes ⇒ stop and question the architecture**, not Fix #4.
|
||
- **Visual verification is the only acceptance test that requires user
|
||
input.** Everything else proceeds without confirmation.
|
||
- **Drive autonomously** through full phases and across commit boundaries.
|
||
Don't stop mid-phase for routine progress check-ins.
|
||
- **Subagent policy**: default Sonnet for implementation chunks; Opus only
|
||
for load-bearing quality review at phase boundaries. Provide each subagent
|
||
with the full file paths it needs to read and the acceptance criteria.
|
||
- **Commits go to the feature branch** until the phase ships, then merge to
|
||
main with a `--no-ff` merge commit summarising the full phase.
|