Pjangler Parity Rules
Location: src/parity/rules.ts in the pjangler repo; concrete recipes own and
register the checks they consume.
Architecture
Each parity rule is an object with:
id— dotted name (e.g.systemd.sentinel,hermes.registry-parity)title— human-readable descriptionaudit(ctx)— returns{ status, summary, details, fixable }migrate(ctx, finding)— attempts to fix what audit caught; returns{ status, summary, changedFiles, details }
ctx contains: repoRoot, pjanglerRoot, homeDir, dryRun.
Checks are returned by the owning recipe factories (for example,
createBmadChecks()) and registered through the recipe catalog. The legacy
facade must read that same registry rather than maintaining a second rule list.
Gitignore boundary
Structural parity may add a small repo-owned rule or remove exact obsolete
lines previously generated by pjangler. It must preserve every unknown line and
must never mutate Git's index. Resolve the effective global ignore dynamically;
do not copy it into the repo or hard-code its path. Route any already-tracked
ignored-file reconciliation, commit, and push to gitignore-maintenance, where
the operator reviews provenance and proves local files survive before untracking.
Key functions
discoverRoles(repoRoot)— walksagents/hermes/*/role.yaml, returnsRoleMeta[]withagentId,roleDir,roleYamlPath, etc.ownedRegistryEntries(registry, repoRoot)— filters registry entries whoserole_dirresolves to the same project root asrepoRoot. UsesrealOrSelf(dirname(dirname(dirname(roleDir))))comparison.realOrSelf(path)—realpathSyncwith fallback to the input string.safeReadText(path)— reads a file, returnsnullif it doesn't exist.
Build and test
npm run typecheck && npm run build && npm test
node dist/index.js audit # run all rules
node dist/index.js migrate --all # fix all fixable rules
Pitfalls
Profile-based / report-only rules
Some rules wrap a dedicated audit profile (e.g. the momo-lifecycle-plane profile that checks whether a repo is ready for the Momo PM orchestrator lifecycle). These rules cannot be fully auto-repaired by pj migrate and must be guarded so pj project init does not endlessly select them on legacy repos. See the pjangler-parity skill in the 33GOD PM runtime for the full recipe, including the guard pattern and the momo-lifecycle-plane regression-test fixture.
ownedRegistryEntries scoping after a repo move
ownedRegistryEntries filters by realOrSelf(repoRoot). After a repo moves (e.g. code/pjangler to code/33GOD/pjangler), registry entries with stale role_dir paths resolve to the old location, not the new repoRoot. The migrate loop over ownedRegistryEntries never sees them, so repoint logic doesn't fire even though audit catches the mismatch.
Fix pattern: Walk roles from discoverRoles() and look up each agent's entry in the registry by agentId directly. This mirrors what the audit does (it also walks roles and looks up by agentId).
Systemd unit staleness
The systemd.sentinel migrate checks whether unit files exist and enables them. But after a repo move, unit files still exist with stale ExecStart/WorkingDirectory paths. The consumer crashes on start.
Fix pattern: Read each unit file and check whether it contains /agents/hermes/ but not the current role.roleDir. If stale (or missing), re-run the provisioning script (70-systemd.sh) with FORCE_SYSTEMD=1 to regenerate units, instead of just enabling them.
Migrate must fix what audit catches
The audit and migrate functions can have different scoping. Audit walks roles and looks up registry entries by agentId; migrate walks ownedRegistryEntries which scopes by repo root. When these differ (repo move, stale paths), migrate silently no-ops while audit fails. Always ensure migrate covers the same entries audit checks.