5450521ed2407fded754d96f7f42e772eef287c8
16
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f847cb2171 |
The sheet header said 9 while the body said 4 (R-256)
Verifying R-255 on a live sheet found the rule working everywhere it was built and contradicted in the first place a GM looks. The header's ARMOUR field is the hide the creature HAS; on a grounded beast the hit-location table under it read 4 and the header still read 9. Nothing computes off that field, so nothing in the code was wrong. It was wrong at the table. The field stays editable and stays 9 — that is the creature's hide and what a GM types into. Under it, when system.grounded is set, the sheet now says what is in play: DOWN: 4 · CHEST 9, derived from exposedHideFor rather than from a second opinion, with the reason on the tooltip. Verified live: no badge in the air, and grounded the badge and the location table agree at 4 with the forequarters at 9. check-rules forbids redefining a rule by NAME, which misses how this goes wrong in practice — nobody redefines exposedHideFor, they write Math.floor(hide / 2) in the sheet because it is cheaper than an import. I wrote that version first and check-rules passed it. It now fails any file outside rules.mjs that halves a hide inline, naming file and line; check files are exempt because stating the expected value independently is what a test is for. That guard also turned up something I have not fixed, recorded in R-256: armourAgainst lives in ringbrp.mjs rather than rules.mjs, and tools/simulate.mjs has its own inline copy of it. They agree today so no published number is wrong, but every figure in the bestiary and the lethality baseline comes out of that simulator. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ddc4f99ff4 |
Hit locations reach ordinary combat, and creatures get a body
THE DEFECT. locationFor had ONE caller in the engine, hitLocationRoll, and hitLocationRoll was called from fall, burn, detonate and burstAttack. Falling, fire, explosions and automatic fire. Nothing else. rollWeaponDamage rolled dice, printed a number and returned it, so the chat Damage button, the character sheet's damage button and the item sheet's all stopped there. A grenade knew which arm it took off. A sword did not. Every individual piece was already correct - the tables, per-location armour and coverage, disable and destroy thresholds, Major Wound, dying, death, the figure on the sheet. They simply were not joined up, which is why this reads as a wiring change rather than a new subsystem. - rollWeaponDamage now carries through to hitLocationRoll: it rolls the location, applies that location's armour, applies the wound, and handles the rest through the machinery that already owned it. Nothing here re-implements any of that - locationModeFor picks the melee or ranged column from the weapon's class. The two columns have always differed and nothing was choosing between them - with no target it does exactly what it did before and SAYS SO on a card, rather than failing quietly - burstAttack opts out with locate:false - it rolls its own location per round, and letting both fire would have applied every round of a burst twice BODY PLANS. Two new tables: quadruped (four legs, fore/hindquarters, neck, a head that is hard to reach) and winged (that, plus wings). Forequarters are the vital. DISABLE EITHER WING AND IT CANNOT FLY, which is the fight a party can win. Barghest, Kelpie and Church grim were all species "baseline" - a black dog, a horse and a grave-hound, each hit-located with two arms and a chest. Fixed. Added "The supporter", a winged heraldic beast, so the new table is exercised by real content instead of existing unused. Body plan is a NEW FIELD, not a species. A bear is not a playable people with characteristic dice and talents, so creature specs take bodyPlan and the playable species list is untouched. THE NPC SHEET had no hit locations at all - 102 lines against the character sheet's 627 - while every NPC in the game had them derived and wired to consequences. It now carries the body-plan selector, the figure, the per-location table, the condition and a GROUNDED flag. The sheet class already extended the character sheet, so it already HAD the context; the template never used it. FOUND IN PASSING, by the new guard: kind "body" - the abdomen, the trunk, the hindquarters - did nothing whatever. Its printed effect has always promised "-30% to all Physical actions and bleeding 1 hit point per round until First Aid" and the code delivered none of it, on every body plan including the humanoid default. It now bleeds 1, or 2 destroyed, exactly as bodyX says. NEW GUARD: check-anatomy. Every body plan must cover 1-20 exactly once in BOTH modes, every location must be drawn and every drawn shape must be a location, and every kind must be one locationEffectsFor acts on, has effect text for, and has a destruction outcome. Verified it fails on a d20 gap, a shapeless limb and an unhandled kind before wiring it in. Ninth guard. VERIFIED IN A RUNNING FOUNDRY, not just unit-tested: melee bar reads the Melee column and the rifle reads Ranged on the same creature; forequarters graded as a vital hit; a maxed wing set flightLost and the sheet said GROUNDED; the real chat Damage button rolled 1D20 15, found the right foreleg, applied 12 through armour, disabled the leg and took the dog from 16 to 4; and with nothing targeted nothing was touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
24b1aaa682 |
Development: skills improve from what beat you, not what worked
Reversed experience. A skill is marked when it FAILS - failure or fumble - and never when it works, and the improvement roll is the same reversal: roll OVER your rating and the skill goes up 1d6. Nobody learns anything from the lock that opened first time. The two halves pull against each other on purpose, so ratings converge instead of running away. Simulated over 2000 campaigns: a 10% and an 85% skill start 75 points apart and are 11 apart after 60 cases. - rules.mjs owns it: DEVELOPMENT, developmentMarksOn, developmentImproves, developmentCeiling, developmentGainFor - trainingCap has been on the skill schema and the item sheet since it was written and nothing has ever read it. Development reads it now: a skill stops at its own cap, and everything else stops at DEVELOPMENT.ceiling, because a d6 gain could otherwise push a 98% skill past 100 - GM opens the phase from a GM-only toggle in the token controls; it posts a card naming every agent and their marked count, and puts a Development button on each sheet. Players press their own - the toggle re-reads the setting on change, since getSceneControlButtons only fires at load and the button would otherwise lie if the phase were opened by the API or a second GM - rules journal and player primer rewritten from the tables; the old text described the pre-reversal rule on both halves - 7 spot-checks, 4 behaviour tests; verified end to end in a running Foundry Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
37b4043746 |
Play a full case, then fix what it found
Ran THE HOLLOW ROAD with five agents — approach, crossing, contact, the thing on
the road, extraction, debrief — with panic and Coherence both live. It ran clean
and it found one thing that mattered.
ANOMALY LORE 0. Marta Holloway, Field Lead, Senior Field Officer, could not roll
the department's signature skill. Nor could Okonkwo. Induction was gated to
TRADES on the reasoning that a career posting is itself the agency training —
but 17 of the 25 postings do not list anomaly lore, its printed base is 0 on
purpose, and nothing else supplies it. So the people who joined the department
straight out of school were the only ones who never received its basic training,
which is exactly backwards.
Induction is a FLOOR for everybody now, posting or trade, in the live generator
and the pack builder both. It never demotes: Bhattacharya keeps Registry's 63,
Holloway goes 0 → 43, Okonkwo 0 → 33. All twenty built agents verified non-zero.
Two GM tools the case showed were missing, both found by feeling the friction
rather than by reading anything:
- clearPanic(actors). Panic clears when the scene does — one line in the book
and, until now, five sheets edited by hand while the table waited. It leaves
Coherence alone, which is the whole point of having two tracks.
- partyState(). Everybody's hit points, panic and Coherence in one card. With
five agents the alternative is asking "what are you on?" five times a scene,
which is friction a table absorbs silently and then blames on the game.
And the player-facing gap: the primer never mentioned panic. Players got a
one-page aide-memoire for a track they now meet in the first scene. It has a
page of its own and a row on the cheat sheet, both generated from the same
tables as the rules.
One thing I reported as a bug and withdraw: Okonkwo's "Firearm (Pistol) 5%" was
me rolling the wrong weapon. He carries a marksman rifle at 53%. check-kits was
right.
Version 1.2.0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
5aaae186e0 |
A fumbled relief attempt puts a point on
Trying to steady somebody and making it worse is the most familiar thing on that list, and until now it was the only outcome that cost nothing. Done as a NEGATIVE relief rather than a second code path: panicReliefFor returns -1 on a fumble, and easePanic already subtracts what it is given, so subtracting a negative raises the track. One number, one direction, no branch that could drift from the other. The clamp still holds — fumbling at 5 stays at 5. The chat card had to be told, because otherwise it prints the word "Psychology" over a number that has just gone UP and the table reads that as a bug rather than as the rule. It now leads with a red FUMBLED tag. Guarded three ways: a spot-check in check-rules for the -1 itself, and two behaviour tests — one that the sign survives easePanic's subtraction and actually raises the track, one that it cannot push past the top. A test on the constant alone would have passed even if easePanic had healed on a fumble. Verified live: 2 → fumble → 3 → success → 2 → special → 0, and a fumble at 5 staying at 5, with the cards reading correctly. Version 1.1.2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
cbad87ba98 |
Add Occult, Religion and Psychology, and the Panic track
From the first game's list. The three skills are not decoration — they are the
levers on the new track, which is why they arrived together.
PANIC, 0 to 5, up. It is the other half of Coherence and the rules page says so
in the first line, because a table that confuses them will make the wrong call
at the worst moment:
Coherence what knowing has cost you weeks 10-0, down
Panic what is happening to you NOW minutes 0-5, up
A trigger gives a point with NO ROLL. Asking for a die to find out whether you
are frightened slows the scene and tells the player how to feel; the die is for
getting rid of it, which is the interesting direction.
0 steady · 1 rattled · 2 shaken -5% · 3 losing it -10%
4 overwhelmed -20% · 5 broken
The first point is free on purpose, so the opening shock of a scene costs the
table nothing. At 5 you break — freeze, flee or fixate, choose fast — and come
back at 3, not at 0, because breaking is not a rest.
Relief is where the skills come in. A success takes one point, a special takes
two. Psychology is the only thing that steadies somebody who is NOT you.
Religion steadies you alone and does not care whether you are right. Occult is
the odd one: naming a thing correctly, out loud, helps everybody who can hear —
the department's actual work, and the only relief that reaches a room. Plus
POW×5 to steady yourself, and your anchor object once per case, which is what
that briefing scene was for.
The three skills are placed in six registers rather than left floating: Occult
to the professor and to Registry, Psychology to the doctor and to Interview,
Religion to Antiquities and Pathology.
Guards, because a track nothing reads is this project's oldest failure: the five
new rules carry spot-checks in check-rules, and check-behaviour gains nine tests
— the free first point, monotonic penalties, only 5 breaking, clamping at both
ends, breaking not clearing the track, special taking two, every band/trigger/
relief having a label, and every relief naming a skill the catalogue actually
has.
Verified in a live game: penalty reaches the roll (58 → 58 → 53 → 48 → 38 for
panic 0-4, criticals and specials recomputed with it), the full arc runs 0 → 2 →
4 → eased to 2 → pushed to 5 → breaks → 3 with no errors, the sheet shows the
band, and the rules journal has a Panic page generated from the same tables.
Version 1.1.0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
51bd8e618a |
Audit every remaining sheet and dialog; add the Culland Hall scene
Opened all of it this time, rather than driving the system through its API.
Two more defects of the same family as the NPC sheet, and in the same place —
the types added last:
- Talent and Art had no TYPES.Item.* label, so their sheets printed the raw
key "TYPES.Item.talent" under the name.
- Neither had a branch in item-sheet.hbs or an isTalent/isPower flag, so the
Detail tab rendered EMPTY over a document holding category, rule, uses per
case, Coherence cost, push cost and origin. Eleven of thirteen types were
handled, which is exactly the shape an eye skips.
Both now have a header statline and a full Detail section, and a guard walks
template.json: every declared type must have a label, a flag set in ringbrp.mjs,
and an {{#if isX}} section in the sheet.
One hardening fix: loseCoherence applies the cost and THEN announces it, so
anything that throws in between leaves the actor quietly poorer with no message.
localize() throws on a non-string, which an unrecognised reason key is.
Everything else came back clean, and it is worth recording what was checked so
the next audit knows what "checked" meant: 13 item types opened, all render; the
expedition sheet (4 tabs); all 7 chat cards driven through their real entry
points — roll, coherence, reaction via Combat.rollAll, hit location, weapon
damage, major wound, and contact through a case file that actually has a group;
all 3 dialogs opened by clicking the controls a player clicks. No errors, no raw
localisation keys anywhere.
Also: Culland Hall as a playable Scene. Drawn as SVG rather than generated,
because a room count, a door position and a label are all load-bearing here and
a generator cannot be trusted with any of them — three label-collision passes in
the browser before it was clean, which is the documented cost of doing it this
way. 2800x2000 at 100px to the square so the Foundry grid lands on the grid
drawn into the image; 5ft squares; no walls, because half-done walls are worse
than none and a GM who wants vision can add them in ten minutes.
Verified in Foundry: the scene imports, activates, and draws.
Version 1.0.2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
02ec47a351 |
Fix every NPC sheet, which has never opened
npc-sheet.hbs line 43 read
{{#if (lookup ../skillsByCategory key).length}}
and Handlebars cannot read a property off a subexpression. That is a PARSE
error, so the template never compiled, so every NPC sheet in the system threw
on click — Natives & Threats, the starter's enemy roster, and the whole LAST
ADMISSION cast. #if already treats an empty array as falsy, so the .length was
never doing anything in the first place.
It was invisible to all four existing guards, and the reason is worth writing
down: check-rules reads .mjs, check-behaviour says in its own header that it
cannot reach sheets, and every play-through in this project has driven the
system through its API rather than by clicking. Nobody had ever opened an NPC
sheet, including me, in any of the sessions that "played through" scenarios
full of NPCs.
So: check-templates.mjs, a fifth guard, compiling every .hbs in about a tenth
of a second, wired into the build ahead of check-behaviour. It also names the
specific shape — a property read off a subexpression — because the raw parse
error says only where. Verified it bites by reintroducing the bug. Handlebars
is a devDependency now.
Opening the sheet then showed a second thing nobody had seen: the NPC skill
rows carry four children and were using the character sheet's seven-column
grid, so the name landed in a 13px column and the whole list overlapped
itself. The NPC row now has its own four-column template.
Verified by opening all six LAST ADMISSION cast sheets with an error hook
attached: six rendered, no errors, and Old Marrow reads HP 20/20, major wound
10, armour 2, Brawl 60%, Melee Weapon (Polearm) 60%.
Version 1.0.1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
4101221eb1 |
Fill the empty space in the actor sheet header
The header was one flex row — identity, then vitals pinned to the top right. The identity column is tall (name, rank, marksmanship, the two style hints, two age fields) and the vitals row is short, so everything under the vitals was empty blue: roughly a third of the header. Split it into two columns and stack the status column: vitals, then the derived numbers, then the readied weapons. Both of those already existed as full-width bands directly below, so nothing new is on screen — it is the same content arranged to use the space, and it puts damage modifier and readied weapons next to hit points, which is where they are read during a fight. Two sizing traps, both found by measuring rather than looking: The status column is an auto track, so its width came from max-content — the readied-weapons line, whose length is whatever the weapon names happen to be. That crushed identity into a 190px gutter with the name truncated to "Marta Hollow". width:0 + min-width:100% keeps those panels out of the measurement so the column is sized by the vitals and they stretch to match. The vitals row is wide enough on its own to do the same thing, so the columns are proportional (1fr / 1.6fr) and the vitals now wrap. Measured at 1020px: header 367px tall before, 266px after, columns 241 and 235 so they end level. At 1400px: 214px tall, columns 189 and 158, vitals unwrapped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c61700566c |
Shields: armour you raise, and the only armour you can defend with
A shield is the one piece of armour you hold, and it protects you exactly as much as you are currently holding it up. Raised, its points apply to what it covers and it can parry on a new Shield skill. Lowered, it is weight. The price of keeping it up is that it occupies a hand and you are shooting around it: -20% to your own ranged attacks, printed on the card like everything else. Only one shield goes up at a time. Seven of them, 2-point buckler to 6-point ballistic shield; the pavise and barrier plate cover legs too because they are walls you carry. `degradation` finally does something — it has been on every armour item since the packs were first built and was read by nothing. A blow costs the shield a point of wear for every step it stood above an ordinary success, so an ordinary hit costs nothing, a special 1 and a critical 2. At its limit it comes apart, drops, and will not be raised again. Two defects found by driving the real functions rather than reading the diff, both introduced by this change: - Every shield defence quietly rolled DODGE. "shield" passed through defenceTypeAllowed unchanged, so neither parry branch matched and it fell through. Invisible in the card; it showed up as eight rolls at a supposed 85% returning six failures. Taking a blow on the shield IS a parry, so it is now a flag on a parry rather than a defence type of its own. - The wear rule was unreachable: it fired only on a clean stop, but under graded defences only a critical fully stops a critical, so a shield would never have taken a scratch. Wear is for what the shield ABSORBED — dragging a critical down to a special is exactly when shields splinter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6ed9ded362 |
Armour covers what it covers, and Coherence moves to the vitals
Asking for the type of armour per location turned out not to be a display request. Every equipped piece added its points to EVERY location, so a Brodie helmet protected your shins and a cuirass protected your skull — with a catalogue containing sallets, lobster-tail helmets and three cuirasses, that could not be printed without the sheet saying something visibly false. Armour now declares coverage as a named set rather than a list of locations, because a piece has to fit whatever body wears it: all, torso, torsoArms, torsoLimbs, head. The sets expand to location KINDS, so a cuirass covers a baseline torso and a vesh trunk and ridge without either knowing the other exists. Unstated coverage is `all`, so anything that does not declare behaves as everything did before. Eighteen of the twenty-five catalogue pieces are not whole-body and now say so. The trap was double counting: hitLocationRoll subtracted the global worn total PLUS the body's own armour at that location, which was right while armour was global and becomes a double count the moment a location knows its own pieces. It reads the location's total and nothing else, verified by firing fourteen hits of 10 damage across every location and asserting damage applied == 10 - that location's armour. A single ARMOUR number no longer exists, so the header shows the torso with "0-8 by location" beside it when the rest of you is not that. This is a real difficulty change: limbs are frequently bare and limbs are 12 of 20 on the melee table. Coherence moves out of the derived strip into the vitals at hit-point size, with its band under it and the box reddening as it falls. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3421ed4a2e |
Reorder the combat tab, and make weapons readyable
There was no way to ready a weapon. The strip under the characteristics reads `equipped`, and the only toggle-equipped control on the sheet was on the ARMOUR list — a weapon could only be readied by opening its item sheet and finding the tickbox there. The weapons table now carries the same toggle, the row is tinted when readied, and a line above the table says what the control does. The tab runs in the order it is used: weapons, armour, defend, reaction, body last. Weapons lead with 40px art and a display-face name, having previously sat below two screens of anatomy and reaction arithmetic. The figure is a partial, drawn in three places — header thumbnail, combat tab, anatomy tab. Three inline copies would have been the template-layer version of the defect check-rules exists to prevent: a location present in one drawing and missing from another. The header carries a 34px figure and every location with its damage and the armour over it, so "how much armour is on the part that just got hit" is answered on the main page without changing tab. Also fixed: `.ringbrp-sheet a` outranks a bare `.rb-equip`, so an unequipped toggle was painted white and read as lit — true of the armour toggle too, since it was added. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4bfa7110d0 |
A lamp lights the people standing in it
The goods yard fight made the case: three agents inside the same pool of light shot at -50% while the fourth, four metres away, shot at -10%. A lamp now has a reach, per item rather than global, because a weapon lamp and a vehicle searchlight are not the same offer — 4 m for cold light strips, 6 m for a weapon lamp, 10 m for the issue field lamp, 30 m for a roof searchlight. Anyone inside it is in the light and the card names whose lamp it is, so both halves of the rule travel together: the pool is one position and the whole group is standing in it. Measured from the tokens. The shot that started this — no lamp of her own, four metres from somebody else's, in pitch dark — goes from 20% to 60%, while an agent twenty-five metres out still eats the full -50%. An UNKNOWN distance is explicitly not lit: Number(null) is 0, so an unmeasured distance would otherwise have read as standing on top of the lamp and lit any scene with no tokens placed. The dialog carries a tick instead, pre-set when the canvas could measure it and the GM's answer when it could not. Note: template.json is read when Foundry loads the system, so a deployed server needs a restart and not just a pull before the migration can stamp the new field. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2e10ecced9 |
Fire modes, fitted sights and lamps, and a tab rail
Selectable fire, with the modifiers on the choice Every weapon has carried a fireMode since the packs were first built and nothing read it: a musket could rip a ten-round burst and "rapid" bought a weapon nothing. FIRE_MODE now gates the burst rules and carries its own attack modifier — slow +10% and then you work for the next one, fast a second shot at -20% to both, rapid bursts, beam +5% and it can be held on the target for damage instead of hits. The gate lives in burstAttack too, so no macro can route round it. `fast` was on five catalogue weapons and was not in the system's list of modes at all, so re-saving one of them silently blanked it; the list is derived from the rule now. burstAttack shipped complete and was reachable from nothing. It is the resolution path the selector chooses, and takes the dialog's whole stack rather than charging the player twice for the same burst. Sights and lamps that apply themselves A sight is graded by range band, so a telescopic sight is a -10% liability inside ten metres and worth +20% at extreme, and it only counts if it fits. The band is already known when the modifier is wanted, so nothing is selected and nothing is forgotten. The dark costs something now; a carried lamp buys it back to dim, never to daylight, and the card says it shows them exactly where you are. A migration fits gear that predates the fields, matched on the stamped key rather than by name. The damage card The last card still wearing Foundry's stock dice styling — it posted a bare Roll and let the core template dress it. It renders through the module's own card now, with the Roll still on the message so dice stay inspectable. A rail instead of a strip Eight tabs across an 820px sheet compressed to 11px type. They run down the side now, one line each, with counts as badges; the sheet is 152px wider to pay for it. Also fixed: any weapon with a Coherence cost could not be attacked with AT ALL — the handler re-read the weapon after an await, when currentTarget is already null, and the attack died on a TypeError with nothing shown. And the duplicate-rule guard failed an honest table because FIRE_MODE and speedBands share two key names; it wants a majority now, and prints partial overlaps rather than swallowing them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
89ce557355 |
Death at zero, and a case brief that answers the useful questions
R-223: nothing happened at 0 hit points. An agent was marked down and lay there forever; the only mention of death in the system was flavour text on a destroyed head. Zero now starts a clock whose length is the agent's own Major Wound threshold, ticked from the same hook that charges blood loss. First Aid or Medicine stabilises and stops the count; healing above zero clears it; heal() refuses the dead. On death, Coherence decides what the record says — from a proper death in service down to not being recorded as dead at all. Case generator: four phases instead of five (the arc drew two interchangeable middles that read as padding). The brief now leads with Where / When / Classification / Phases, states the opposition in full, and has a "How you get back" section. place and duration were generated and never printed. The same facts are on the case file itself. 300 seeds checked for object leaks; one found and fixed (group taboo/desire are objects with .text). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d0144f744e |
The Custodians v0.6.1 — full system
A Foundry VTT game system: Basic Roleplaying d100 in a drab British agency that deals with crossings, where reality keeps a record of what belongs where and can be made to get it wrong. Contents: - rules.mjs as the single authority for every rule formula, with 40 rules and 132 spot-checked values. Nothing may restate a formula it defines. - 25 postings, each with a key characteristic, trained skills, talents and its own kit; a 175-item catalogue across four eras; 231 generated icons. - Reaction Lanes initiative, re-rolled every round including the first. - Coherence, and the Quiet Arts, which only ever arrive from a crossing gone wrong. - Graded defences: only a critical stops a critical. - Borrowed Authority — access as a record rather than a hacking roll. - A case generator, and a rules journal generated from rules.mjs so the book cannot drift from the code. Built compendia are committed so a remote Foundry needs only the files. node_modules is not; `npm install` restores it for rebuilding content. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |