This document records the decided answers for the ACKS module family so no
future session re-derives them differently. When something here changes, change
it here first, then propagate with bin/sync-toolchain.mjs.
acks-<feature>/
module.json Foundry manifest — the real version number lives here
scripts/ ESM runtime code; entry point scripts/module.mjs
templates/ Handlebars templates (may be nested)
styles/ CSS shipped to Foundry
lang/en.json Flat i18n keys, prefixed "<MODULE-ID-UPPERCASED>."
packs/_source/<pack>/ JSON pack sources — committed
packs/<pack>/ Compiled LevelDB packs — committed AND shipped
ruledata/ (optional) runtime-fetched JSON rules — ships in zip
tools/ Dev harness — NOT shipped
build-packs.mjs SYNCED harness (needs tools/pack-data.mjs)
pack-data.mjs Module-owned document content; exports `packs` map
validate.mjs SYNCED validator
test-logic.mjs (optional) module-owned pure-logic tests
docs/MODEL.md Design doc (reuse → extend → enhance → invent)
(rules extracts are NOT in the repo — see "Rules extracts" below)
.github/workflows/release.yml SYNCED release workflow
.gitignore .gitattributes SYNCED
LICENSE SYNCED proprietary license + disclaimer (see docs/LICENSING.md)
CLAUDE.md RENDERED from skeleton per repo
.claude/settings.json SYNCED permission allowlist
package.json MERGED (scripts/devDeps/engines enforced)
package-lock.json Committed (CI uses `npm ci`)
- Default branch:
main. (acks-formationis a legacymaster; renaming requires a remote default-branch change on GitHub — do it deliberately.) - Tags:
v<semver>and the tag MUST equalmodule.jsonversion(CI enforces this). - Compiled LevelDB packs are committed. They ship in the zip and make git-clone dev installs work without a build step.
.gitattributesis mandatory.core.autocrlf=trueis common on Windows; without thebinarymarkers git rewrites line endings in LevelDB files on checkout.CURRENTis plain text ("MANIFEST-00000N\n") that LevelDB parses strictly — a CRLF checkout breaks the pack on a fresh clone. Never remove those lines.- Never ignore
packs/**/*.log— LevelDB stores live data in.logfiles. The canonical.gitignorecarries a comment to that effect. - Lockfiles are committed for reproducible CI installs.
- After a local
npm run build:packs, ifpacks/_sourceshows no diff, the LevelDB churn is timestamp noise:git restore packs/ && git clean -fd packs/. Commit rebuilt packs only when sources actually changed.
id== repo name == npm package name;title=ACKS II — <Feature>.version: plain semverX.Y.Z. Bump before tagging.compatibility:minimum: "14",verified: "14.364"(raiseverifiedas tested; existing modules keep their historical minimums until retested).relationships.systems:acks,minimum: "14".- Every
relationships.requiresentry carries a humanreasonand acompatibility.minimum. Standard deps when needed:lib-wrapper(>=1.12.0) for safe method wrapping,socketlib(>=1.1.0) for GM-routed writes. manifest/download:https://github.com/NocTempre/<id>/releases/latest/download/module.json|zip.- Declare a pack in
packsonly once it has content (the build harness skips empty packs; CI fails on declared-but-missing packs). languages: at leasten→lang/en.json.
The release procedure lives once, in this repo:
.github/workflows/release-module.yml (a reusable workflow_call workflow).
Each module carries only a thin synced release.yml caller that references it
@main — so procedure changes here propagate to every module automatically,
with no sync step. (Pin the reference to a tag/SHA only if stability ever
matters more than propagation.)
Trigger: push tag v*. Steps: tag/version match check → npm ci →
build:packs → validate → test (each --if-present) → declared-pack
existence check → ship-by-default zip → publish module.json + module.zip
to the GitHub release. A manual "Run workflow" on a module performs a
dry run: full build + validation, no tag check, no publish.
A second synced workflow, toolchain-check.yml, runs on every module push/PR:
it checks out this template and fails CI if any canonical file drifted from
canon — hand-edits can't survive unnoticed.
The zip includes everything except an explicit dev-only denylist (.git*,
.github, .claude, CLAUDE.md, node_modules, tools, packs/_source,
package*.json). Rationale: the old allowlist zip silently dropped new
runtime dirs (henchmen's ruledata/ had to be patched in by hand). New
runtime content now ships automatically; add to the denylist only for new
dev-only paths.
Release procedure (also encoded in the acks-release skill):
- Bump
module.jsonversion (+ changelog if present). npm run build:packs— commit pack changes only if_sourcechanged.npm run validate(andnpm testwhere present).- Commit,
git tag v<version>, push branch + tag. - Confirm publication with BOUNDED checks (never
gh run watch— it hangs through GitHub API outages, which 2026-07-16 stranded several agents): pollgh release view v<version> --json assets~30s apart, cap ~5 min, in a background/Monitor wait rather than foreground. On API 5xx, stop and report — the pushed tag completes on its own. - Verify:
curl -sm 15 -L https://github.com/NocTempre/<id>/releases/latest/download/module.jsonreports the new version (needs the repo public; while private usegh release view— unauthenticated manifest fetches 404).
- Node >= 20 (CI pins 20). devDependencies exactly:
@foundryvtt/foundryvtt-cli ^1.0.0,classic-level ^3.0.0,handlebars ^4.7.9. (The core system uses foundryvtt-cli v3; upgrading the modules is a deliberate, together-only change.) tools/pack-data.mjsis the module-owned content file. Contract:export const packs = { "<pack-name>": () => [docs] }(arrays also accepted). Large data may live in sibling files (e.g. monsters'bestiary-data.mjs) re-exported through the map. Documents carry 16-char alphanumeric_idand a_keyending in that_id.tools/validate.mjs(synced) checks: JS syntax ofscripts/**+tools/**, Handlebars compilation oftemplates/**, JSON validity ofmodule.json/package.json/lang/*.json/ruledata/**/packs/_source/**, pack_id/_keyinvariants,module.jsoninvariants (semver, paths exist, manifest URL shape), and that every i18n key referenced in code exists inlang/en.json(dynamic-suffix tolerant).- Optional
tools/test-logic.mjs(module-owned): pure-logic regression tests that mock minimal Foundry globals and import the real scripts (see acks-equipment). Wire as"test"in package.json; CI runs it--if-present. - Optional
tools/validate-extra.mjs(module-owned): an extra check that must run as PART of validation (not justnpm test). The canonicalvalidate.mjsauto-runs it last if present and fails on its non-zero exit, soscripts.validatestays exactlynode tools/validate.mjs(canon) while the module still gets its own gate. acks-content uses it to run its IP-safety register lint (tools/lint-register.mjs) — chaining the lint intoscripts.validatedirectly would drift from canon and fail toolchain-check. - Foundry dev install: junction, not copy:
New-Item -ItemType Junction -Path "$env:LOCALAPPDATA\FoundryVTT\Data\modules\<id>" -Target "C:\Proj\<id>" - Local live testing: check the local rules reference directory for
C:\Proj\acks-rules\TEST_ENVIRONMENT.md. If it exists, it defines this machine's Foundry test server (URL, world, users, drive-it-by-API notes) — use it for live verification. If it does not exist, skip live testing and rely onvalidate+build:packs(+testwhere present). The file is machine-specific and LOCAL-ONLY: every developer defines their own (or none); never commit ports, world ids, user names, or passwords to any repo.
Anything a module puts into a shared registry carries its key, so nothing collides and any identifier in a bug report greps straight back to its owner. One form per registry — no legacy alternatives (the 2026-07-15 migration renamed every pre-existing deviation rather than grandfathering it):
- JS registries —
globalThisexposures, custom hook names (Hooks.call/callAll), Handlebars helpers — start with the camelCase namespace: the module id camelCased (acks-influence→acksInfluence, hookacksInfluenceRollComplete, helperacksMonstersHas). Derived from the id, never declared. Hyphens can't appear in JS identifiers, hence camelCase here and kebab elsewhere. Firing another acks-* module's hook is a warning, not a failure — deliberate cross-module calls are legitimate. - CSS classes (top-level selectors) start with the module id as-is
(kebab):
.acks-formation-skill-tab. - lang keys start with
<ID-UPPERCASED>.; Foundry-owned roots (TYPES.*) are allowlisted in the validator — extend that allowlist in the template only. - Pack document
_ids (top-level) start with the short key declared inmodule.json→flags.<id>.idPrefix— mandatory for any module with packs. It exists only because ids are exactly 16 chars (the camel namespace wouldn't leave room). Keep it short; scaffolding derives a default (acks+ word initials) overridable with--key. - Settings, document flags, and socketlib channels are already scoped by the Foundry APIs — no check needed.
Declared short keys (must stay unique across the family): equipment acksEq,
formation acksfm, henchmen acksHm, influence acksInfl, monsters acksm.
Consumers of another module's hooks use the literal hook name (grep-ability is
the point); the firing module also publishes its hook names on its API object
(e.g. globalThis.acksInfluence.hooks).
Every module is an ACKS II App under the ACKS II App License. Two rules
are non-negotiable and enforced by canon: (1) the code is proprietary — no
open-source/Open-Game/compatibility license may ever be applied to it (nothing
that lets others take the registered code); (2) the mandatory disclaimers
(unofficial, registration number, required books, free, trademark) are stated
clearly in each README. The canonical LICENSE (a COPY file, CI-enforced)
carries the code terms; the per-module registration number and required
publications live in the README "## License" section. Full rules and rationale:
docs/LICENSING.md.
Reuse → extend → enhance → invent (from acks-monsters, adopted family-wide):
reuse core system documents and fields; extend only with genuinely new data in
flags["<module-id>"]; enhance with alternate sheets/wrappers (libWrapper)
rather than replacements; invent nothing the system already provides.
docs/MODEL.md in each module records how that module applies this.
Rules extracts are LOCAL-ONLY. The canonical extract of the relevant
ACKS II rules for each module lives at C:\Proj\acks-rules\<module-id>\
(never in a repo — licensed book text; the public repos got indexed online,
so on 2026-07-16 the extracts were purged from all five modules' git history
and the repos went private). Sessions cite the local extract instead of
re-reading PDFs. The canonical .gitignore carries armor patterns against
accidental re-adds; anything shipped in-repo (ruledata/, compendium text)
must stay paraphrase-with-citation, never verbatim book text.
- Each module carries a rendered
CLAUDE.md(layout, commands, conventions, pointer here) and a synced.claude/settings.jsonallowlist covering the routine dev loop (npm, node harness, git incl. commit/tag/push, gh run, GitHub API reads).settings.local.jsonstays gitignored for personal grants. - Shared skills live here in
.claude/skills/and are installed to~/.claude/skills/bysync-toolchain.mjs --install-skills, so they work from any working directory (sessions usually run fromfoundryvtt-acks-corewith the modules as additional dirs — note that in that setup only the core repo's settings govern permissions; the per-module settings.json pays off in standalone sessions). - The core repo keeps its Claude context in an untracked
CLAUDE.md(via.git/info/exclude) to avoid polluting the AutarchLLC fork.
All five modules are on canon (sync --check reports zero drift). Remaining
deliberate deviations:
| Repo | Deviation | Path back to canon |
|---|---|---|
| acks-formation | master branch; pack data inline in a custom 21 KB build-packs.mjs |
rename branch on GitHub; extract data to pack-data.mjs, then sync harness |
| acks-influence | pack data inline in custom build-packs.mjs; compatibility.minimum still 13 |
extract data; raise minimum when retested on 14 |
Pack-data _stats guidance learned during rollout: henchmen and equipment use
fixed timestamps (rebuilds are byte-identical); formation/influence/monsters
still stamp Date.now() at import, so every rebuild churns packs/_source.
Prefer fixed timestamps in new pack data — pin the stamp to the value already
committed and the rebuild is a no-op, so a dirty packs/_source then genuinely
means the content changed. With Date.now() data, compare diffs excluding
createdTime/modifiedTime before deciding whether to commit a rebuild.
Three layers keep the family consistent, by mechanism rather than discipline:
- Reusable release workflow (
release-module.yml@main) — CI logic propagates to all modules automatically; nothing to sync. - CI drift check (
toolchain-check.yml, synced) — canonical files (validators, dotfiles, Claude infra) are enforced: a module push with hand-edited or stale canon fails CI untilsync-toolchain --applyruns. - Template CI (
ci.ymlhere) — every template change scaffolds a module from the skeleton and runs the canonical build + validate, so canon itself can't break silently.
Possible later: publish the harness as a git-dependency npm package
(acks-tools) with bin entries, replacing the vendored tools/*.mjs.