Independent Project Not affiliated with, sponsored by, or endorsed by the Watch Tower Bible and Tract Society or Jehovah's Witnesses.
jw-agent-toolkit
ES

Release F81 + F82 · operations + legal module

Operational scheduler · TJ vs State legal cases

Two application-layer modules. F81 closes the operational loop of the meeting-scheduler with organized-app importer + CRDT diff, a roster-editing CLI that honours last_updated so re-imports do not clobber manual edits, and strict Pydantic constraints YAML per congregation. F82 opens the legal module with a Territory catalog of 30 countries (RU/KP/ER/SG/TJ banned, CN/AZ/BY/VN/MM/TR/CU/KZ restricted, 17 free) that composes the existing LocaleContext without duplicating cultural fields, and the jw-legal plugin registering a legal-cases-tj BrainDomain via entry-point jw_agent_toolkit.brain_domains — 6 NodeTypeSpec + 8 EdgeTypeSpec, CONTRADICTS non-directional sensitive.

Sub-phases

5

New tests

+305

New packages

2

jw-meeting-scheduler · jw-legal

Total suite

3318

passing · 0 regressions

▌ Design decisions that wired the 5 sub-phases together

  • CRDT respect: upsert_person guards on last_updated, future re-imports will NOT clobber manual edits.
  • Opt-in at-rest encryption: PBKDF2 salt derived from congregation_id — same passphrase + two congregations = different keys.
  • Constraints YAML strict Pydantic: extra='forbid' + validator per field.
  • Territory composes LocaleContext: zero duplication of cultural fields, structural test enforces it.
  • CONTRADICTS non-directional + sensitive=True: the policy flags rather than merging incompatible laws.
  • End-to-end plugin discovery: new jw_agent_toolkit.brain_domains group in GROUPS without touching existing domains.
  • ban_history with inline sources (jw.org/legal URL, ECHR app no, SCJN ruling) — zero entries without a source.
  • Hand-rolled commented YAML because PyYAML doesn't preserve comments — the coordinator needs to see what row they're editing.

Phase 81.0

organized-app-importer

Backup JSON → local roster with CRDT diff + dry-run + at-rest encryption

✅ Shipped 🧪 38 tests T1 Operational / key
Technical guide →

New package jw-meeting-scheduler. Imports the JSON exported from organized-app web/desktop into a per-congregation local SQLite store. Mapper PersonType → flat PersonRecord: gender derived from separate male/female booleans, status derived from publisher_baptized/unbaptized.active, only active privileges, eligible_assignments flattened+deduped, last_updated = max(updatedAt) across Timestamped fields. Recursive walk of the nested SchedWeek picking up dict slots {value, type, updatedAt} → AssignmentHistoryEntry with aula auto-detected (suffix _B→aux_class_1, _C→aux_class_2). run_import pipeline with dry-run flag + compute_person_diff classifies added/updated/kept_local/unchanged. Opt-in at-rest encryption with FieldEncryptor + PBKDF2 salt derived from congregation_id.

What was delivered

  • load_backup with Pydantic OrganizedAppBackup envelope + F51 PersonType schema list.
  • map_person: gender derived from separate male/female booleans, status from publisher_baptized/unbaptized.active + statusHistory, only active privileges (end_date empty or future), eligible_assignments flattened+deduped sorted by code value.
  • slugify_person_id with NFD diacritic strip + intra-word decoration strip (Juan Pérez → juan-perez, O'Connor → oconnor).
  • map_schedule_week recursive walk of the dict tree picking up {value, type, updatedAt} slots, aula auto-detected from field suffix.
  • SchedulerStore SQLite with compound index (person_id, assignment_code, meeting_date DESC) for fast rotation queries.
  • CRDT guard in upsert_person: if existing.last_updated ≥ rec.last_updated, no write (local newer wins).
  • record_history with INSERT OR IGNORE by entry_id (idempotent).
  • FieldEncryptor wrapper with salt b'jw-meeting-scheduler/v1:' + congregation_id → same passphrase + different congregation = different keys (verified in tests with Fernet InvalidToken).
  • run_import pipeline with dry-run flag + ImportDiff (added/updated/kept_local/unchanged) rendered in Rich table.
  • jw scheduler import --backup --congregation [--dry-run] [--passphrase] CLI complete.
  • 26 scheduler tests (5 models + 3 crypto + 4 loader + 5 person_mapper + 4 schedule_mapper + 7 store + 4 diff + 4 pipeline) + 2 CLI tests.
  • Guide docs/guias/meeting-scheduler-import.md with quickstart + encryption recipes.

Phase 81.1

roster-cli-crdt-preserving

people list · person edit · history — a re-import won't clobber your edits

✅ Shipped 🧪 8 tests T2 CLI / UX
Technical guide →

Manual roster-editing CLI on top of the F81.0 store. The critical piece: every edit bumps last_updated with datetime.now(UTC) → a future re-import from organized-app with an earlier last_updated will NOT clobber the manual edit (CRDT guarantee). Lets you add/remove privileges (ms, elder), add/remove eligible assignments (accepts the name MM_BibleReading or the numeric code 100), change status (active/irregular/inactive/disfellowshipped). Plus a history command listing meeting_date DESC with field, code (enum name), aula, confirmed, cancelled to see a person's history.

What was delivered

  • jw scheduler people list (Rich table of the roster with privileges, eligible count, last_updated).
  • jw scheduler person edit <slug> with --add-privilege ms/elder, --remove-privilege.
  • --add-eligible / --remove-eligible accept the AssignmentCode name (MM_BibleReading) or the numeric value (100).
  • --set-status to switch active/irregular/inactive/disfellowshipped/deceased.
  • Every edit bumps last_updated via datetime.now(UTC).strftime('%Y-%m-%dT%H:%M:%S') — CRDT-preserving: a future re-import respects the edit.
  • jw scheduler history --person <slug> --congregation <id> listing meeting_date DESC with field, code, aula, confirmed, cancelled.
  • Clear errors: unknown person → exit code 2 with a Rich red message, invalid assignment code → Pydantic ValueError.
  • 8 CLI tests (2 people list + 4 person edit + 2 history).

Phase 81.2

constraints-yaml-per-congregation

Strict Pydantic + hand-rolled commented YAML + lint/show

✅ Shipped 🧪 23 tests T2 CLI / UX
Technical guide →

AssignmentConstraints Pydantic v2 strict (extra='forbid') with 9 fields: congregation_id regex pattern, gap_minimum_days dict[AssignmentCode, int] with 18 default codes (60d bible_reading, 90d speaker+TGWTalk, 45d student parts), max_assignments_per_month [1,10], pair_experienced_with_novice, require_brother_for_reading, languages_active (≥1), aulas_active (subset of main_hall/aux_class_1/aux_class_2), weights dict[str, float] non-negative. Loader/writer in constraints_io.py: jw scheduler constraints init writes the template with hand-rolled comments (PyYAML doesn't preserve comments — hand-rolled solution renders each AssignmentCode.value with its .name as a comment so the coordinator knows which row they're editing).

What was delivered

  • AssignmentConstraints Pydantic v2 with extra='forbid' — unknown fields rejected at parse time.
  • DEFAULT_GAP_MINIMUM_DAYS: 18 codes with sensible values (60d MM_BibleReading, 90d MM_TGWTalk + WM_Speaker + WM_WTStudyConductor, 45d student parts and starting_conversation).
  • field_validator rejects negative gap_minimum_days, negative weights, empty languages_active, aulas_active outside {main_hall, aux_class_1, aux_class_2}.
  • congregation_id regex pattern ^[a-z0-9_-]{3,64}$ — same as PersonRecord.person_id.
  • constraints_io.load_constraints with typed ConstraintsLoadError for malformed YAML vs schema mismatch vs missing file.
  • write_default_constraints with --overwrite guard: raises by default if the file exists.
  • Hand-rolled commented YAML dump renders ' 100: 60 # MM_BibleReading' for each AssignmentCode (PyYAML doesn't preserve comments).
  • jw scheduler constraints init [--force] · jw scheduler constraints lint · jw scheduler constraints show with 3 Rich tables (key fields + gap_minimum_days + weights).
  • pyyaml>=6 added as a jw-meeting-scheduler dep.
  • 23 tests passing (8 model + 9 IO roundtrip + 6 CLI).

Phase 82.0

territory-catalog-composing-locale-context

30 curated countries that compose LocaleContext without duplicating cultural fields

✅ Shipped 🧪 200 tests T1 Operational / key
Technical guide →

Territory catalog in jw_core/territories.py as a dataclass that composes jw_core.data.locale_context.LocaleContext via the self.locale property keyed by iso_3166. Zero duplication: cultural fields (name, languages, dominant_religions, sensitive_topics, cultural_anchors, holidays_to_acknowledge, notes) live ONLY in LocaleContext; legal fields (jw_branch_region, legal_status_summary, ban_history) live ONLY in Territory. get_territory_full(iso) merges both. 30 curated countries with ban_history carrying inline sources (jw.org/legal URL, ECHR application number such as Krupko v Russia 26587/07, SCJN/SCOTUS rulings such as Barnette 1943): 5 banned (RU, KP, ER, SG, TJ), 8 restricted (CN, AZ, BY, VN, MM, TR, CU, KZ), 17 free.

What was delivered

  • Territory frozen dataclass with iso_3166 + jw_branch_region + legal_status_summary + ban_history; self.locale property composes LocaleContext.
  • LOCALE_CONTEXTS extended with 15 new countries required by the legal catalog (KP, ER, SG, TJ, CU, VN, MM, GR, AM, AZ, TR, GE, MD, BY, KZ).
  • Banned block (5): RU with 2017 Supreme Court ruling + Taganrog ECHR, KP, ER 1994 decree, SG 1972 Societies Act, TJ 2007 Ministerial ban.
  • Free with resolved history (12): ES 1970 Religious Liberty, MX 1992 art. 24 reform, US Cantwell + Barnette + Watchtower v Stratton, AR decreto 1867 → 1029, KR 2018 alternative service, DE 2000 Bundesverwaltungsgericht, FR 2011 ECHR Témoins de Jéhovah, GR Kokkinakis + Manoussakis, AM Bayatyan v Armenia.
  • Additional context (10): VN, MM, TR Tarhan ECHR, GE 97 Members of Gldani, MD, CO, PE, PH Ebralinag, CU, KZ 2011 Religion Law.
  • Helpers get_territory_full (merges Territory + LocaleContext into a dict), territories_by_status (banned/restricted/free/unknown), territories_by_branch (substring match).
  • pycountry>=24 added as a jw-core dep for ISO 3166-1 validation.
  • 91 parametrized invariant tests: iso_is_valid_alpha2 (pycountry), every_territory_has_locale_context, jw_branch_region_non_empty, ≥30 territories.
  • Structural test test_territory_no_field_duplicates_locale_context: assertion on Territory.__dataclass_fields__ vs forbidden_overlap {name, languages, dominant_religions, sensitive_topics, cultural_anchors, holidays_to_acknowledge, notes}.
  • Guide docs/guias/territories.md with quickstart + table of the 30 countries + how to add a new one.

Phase 82.1

jw-legal-brain-domain-plugin

End-to-end BrainDomain plugin discovery with CONTRADICTS sensitive

✅ Shipped 🧪 36 tests T1 Operational / key
Technical guide →

New jw-legal package registers the legal-cases-tj BrainDomain via entry-point jw_agent_toolkit.brain_domains (the sixth group in the F41 plugin SDK, added to jw_core.plugins.registry.GROUPS next to agents/parsers/embedders/vlm_providers/gen_providers). 6 NodeTypeSpec (LegalCase, Law, Territory referencing F82.0 by iso, CourtPrecedent, LegalArgument, PersecutionEvent) + 8 EdgeTypeSpec with CONTRADICTS non-directional sensitive=True → the second-brain's conflict policy flags rather than silently merging. discover_domains() returns {'tj', 'legal-cases-tj'} with no conflict. Structurally conforms to the runtime_checkable BrainDomain Protocol.

What was delivered

  • New package packages/jw-legal/ with entry-point [project.entry-points.'jw_agent_toolkit.brain_domains'] legal-cases-tj = 'jw_legal.brain:LegalCasesTJBrainDomain'.
  • Group jw_agent_toolkit.brain_domains added to jw_core.plugins.registry.GROUPS + REQUIRED_BY_GROUP = ('name', 'nodes', 'edges').
  • 6 NodeTypeSpec with canonical_id_pattern + properties + wiki_page_template + obsidian_subdir + confidence_threshold per type.
  • LegalCase confidence 0.95 (primary rulings are expensive to get wrong), LegalArgument + PersecutionEvent 0.70 (more interpretive).
  • Territory references the F82.0 catalog by iso_3166_1_alpha2 — the plugin does NOT duplicate LocaleContext's cultural data.
  • 8 EdgeTypeSpec: CITES_LAW, APPLIES_IN_TERRITORY, APPEALS_AGAINST self-link, SUPPORTED_BY_PRECEDENT, CONTRADICTS non-directional + sensitive (flag instead of merging), GROUNDS_ARGUMENT, OCCURRED_IN, JUDGED_BY.
  • Structural test every_source_and_target_references_known_node_type: zero references to unknown types.
  • LegalCasesTJBrainDomain conforms to the runtime_checkable BrainDomain Protocol — isinstance(d, BrainDomain) passes.
  • discover_domains() returns {'tj', 'legal-cases-tj'} with no conflict — verified end-to-end with clear_plugin_cache().
  • 36 tests passing (1 scaffold + 12 nodes + 10 edges + 6 BrainDomain class + 7 discovery + 1 plugin verify enumeration update).
  • Guide docs/guias/jw-legal-brain-domain.md with node table + edge table + discovery examples.

▌ End-to-end flow: from organized-app backup to legal plugin

The 5 sub-phases compose two parallel flows that share infra (FieldEncryptor, models_organized, BrainDomain SDK):

# ─── F81: congregational operations ────────────────────────────────

# 1. Import the roster from organized-app
uv run jw scheduler import \
  --backup ~/Downloads/organized-backup.json \
  --congregation kingdom-hall-central \
  --passphrase "correct-horse-battery-staple"

# 2. Manual edits (CRDT-preserving — a re-import won't clobber)
uv run jw scheduler person edit juan-perez \
  --congregation kingdom-hall-central \
  --add-privilege ms \
  --add-eligible MM_TGWTalk

# 3. Declare local rules in strict YAML
uv run jw scheduler constraints init --congregation kingdom-hall-central
# Edit ~/.jw-agent-toolkit/congregations/kingdom-hall-central/constraints.yaml
uv run jw scheduler constraints lint --congregation kingdom-hall-central
uv run jw scheduler constraints show --congregation kingdom-hall-central


# ─── F82: legal module ─────────────────────────────────────────────

# 4. F82.0 Territory catalog: 30 countries with sourced ban_history
python -c "
from jw_core.territories import get_territory_full, territories_by_status

full = get_territory_full('RU')
print(full['jw_branch_region'])        # 'Russia (closed since 2017)'
print(full['ban_history'][0])
# "2017-04-20: Supreme Court ruling designates JWs as 'extremist'"
print(full['name']['en'])              # 'Russia' (from LocaleContext)

# Filter by status
banned = territories_by_status('banned')
print([t.iso_3166 for t in banned])    # ['RU', 'KP', 'ER', 'SG', 'TJ']
"

# 5. F82.1 BrainDomain plugin discovery
python -c "
from jw_brain.domain.registry import discover_domains
from jw_core.plugins.factory import clear_plugin_cache

clear_plugin_cache()
domains = discover_domains()
print(sorted(domains.keys()))          # ['legal-cases-tj', 'tj']

legal = domains['legal-cases-tj']
print([n.name for n in legal.nodes])
# ['LegalCase', 'Law', 'Territory', 'CourtPrecedent',
#  'LegalArgument', 'PersecutionEvent']
print([e.name for e in legal.edges])
# ['CITES_LAW', 'APPLIES_IN_TERRITORY', 'APPEALS_AGAINST',
#  'SUPPORTED_BY_PRECEDENT', 'CONTRADICTS', 'GROUNDS_ARGUMENT',
#  'OCCURRED_IN', 'JUDGED_BY']
"

▌ Coming next — F81.3+ and F82.2+

F81 — meeting-scheduler

  • F81.3 — CP-SAT solver with OR-Tools, hard + soft constraints, ProposedSchedWeek + structured infeasibility_reason per slot.
  • F81.4assignment_generator agent with @fidelity_wrap.
  • F81.5 — REST endpoints /api/v1/scheduler/{suggest,confirm}.
  • F81.6 — Tauri UI with per-slot override.

F82 — legal-cases-tj

  • F82.2HUDOCSource ECHR ingestion (Krupko, Bayatyan, Religionsgemeinschaft).
  • F82.3legal_case_researcher agent with @fidelity_wrap(PF020 no-hallucinated-rulings, hard).
  • F82.4LegalReasoningStep extends F67 ReasoningTree with legal_kind.
  • F82.5hermeneutics_analyzer agent (10 E2E goldens).
  • F82.6precedent_synthesizer with Meta-orchestrator F65 cross-country DAG.
  • F82.7 — YAML principles PF020-PF024 in jw-eval.