@delorenj/pjangler 1.4.5 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +41 -520
  2. package/dist/assets/project-notebook-skill/SHA256SUMS +5 -5
  3. package/dist/assets/project-notebook-skill/export-manifest.json +5 -5
  4. package/dist/assets/project-notebook-skill/hooks/session-end.sh +20 -1
  5. package/dist/assets/project-notebook-skill/hooks/session-start.sh +20 -1
  6. package/dist/assets/project-notebook-skill/references/configuration.md +2 -0
  7. package/dist/assets/project-notebook-skill/scripts/project-hooks.py +33 -3
  8. package/dist/assets/project-notebook-skill/tests/test_project_hooks.py +37 -0
  9. package/dist/index.js +5747 -13483
  10. package/dist/mcp-server.js +6853 -14059
  11. package/dist/notebook/journal-service-main.js +5682 -0
  12. package/dist/project-registry-service.js +748 -0
  13. package/dist/prompt.js +2 -1
  14. package/package.json +15 -10
  15. package/scripts/install-project-registry.mjs +34 -0
  16. package/scripts/migrate-project-registry.mjs +117 -0
  17. package/src/project/registryClient.ts +62 -0
  18. package/templates/commonproject/copier.yml +16 -19
  19. package/templates/commonproject/template/.agents/hooks/README.md +10 -4
  20. package/templates/commonproject/template/.agents/hooks/hermes/hindsight-hook.sh +12 -2
  21. package/templates/commonproject/template/.agents/hooks/hooks.master.json +10 -10
  22. package/templates/commonproject/template/.agents/hooks/lib/hook-guard.sh +13 -0
  23. package/templates/commonproject/template/.agents/hooks/sync.py +66 -17
  24. package/templates/commonproject/template/.agents/skills.json +0 -1
  25. package/templates/commonproject/template/.github/workflows/code-review.yml.jinja +7 -1
  26. package/templates/commonproject/template/.project.json.jinja +1 -1
  27. package/templates/commonproject/template/mise.toml.jinja +11 -21
  28. package/contracts/fleet-contract.yaml +0 -513
  29. package/templates/commonproject/template/.mise/scripts/provision-packs.py +0 -587
  30. package/templates/commonproject/template/.mise/scripts/sync-skills.py +0 -2183
  31. package/templates/hermes-agent/copier.yml +0 -186
  32. package/templates/hermes-agent/template/.gitignore.jinja +0 -11
  33. package/templates/hermes-agent/template/.runtime-scaffold/.gitattributes +0 -10
  34. package/templates/hermes-agent/template/.runtime-scaffold/.gitignore.jinja +0 -44
  35. package/templates/hermes-agent/template/.runtime-scaffold/README.md +0 -56
  36. package/templates/hermes-agent/template/.runtime-scaffold/decisions/.gitkeep +0 -2
  37. package/templates/hermes-agent/template/.runtime-scaffold/logs/.gitkeep +0 -1
  38. package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +0 -50
  39. package/templates/hermes-agent/template/.runtime-scaffold/memories/USER.md +0 -51
  40. package/templates/hermes-agent/template/.scripts/00-banner.sh +0 -21
  41. package/templates/hermes-agent/template/.scripts/01-config.sh +0 -60
  42. package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -42
  43. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +0 -222
  44. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +0 -199
  45. package/templates/hermes-agent/template/.scripts/30-telegram.sh +0 -363
  46. package/templates/hermes-agent/template/.scripts/31-slack.sh +0 -457
  47. package/templates/hermes-agent/template/.scripts/40-plane.sh +0 -60
  48. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +0 -373
  49. package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +0 -18
  50. package/templates/hermes-agent/template/.scripts/70-systemd.sh +0 -354
  51. package/templates/hermes-agent/template/.scripts/80-registry.sh +0 -185
  52. package/templates/hermes-agent/template/.scripts/99-summary.sh +0 -87
  53. package/templates/hermes-agent/template/.scripts/_lib.sh +0 -1138
  54. package/templates/hermes-agent/template/.scripts/channel-transaction.py +0 -2340
  55. package/templates/hermes-agent/template/.scripts/checkpoint.sh +0 -82
  56. package/templates/hermes-agent/template/.scripts/config.example.toml +0 -70
  57. package/templates/hermes-agent/template/.scripts/credential-launch.sh +0 -85
  58. package/templates/hermes-agent/template/.scripts/heartbeat.sh +0 -283
  59. package/templates/hermes-agent/template/.scripts/lib/fleet-env.sh +0 -202
  60. package/templates/hermes-agent/template/.scripts/lib/parse-fleet-env.py +0 -761
  61. package/templates/hermes-agent/template/.scripts/lib/profile-config-lock.py +0 -182
  62. package/templates/hermes-agent/template/.scripts/lib/profile-config-seed.py +0 -108
  63. package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +0 -199
  64. package/templates/hermes-agent/template/.scripts/lib/voice-config.py +0 -546
  65. package/templates/hermes-agent/template/.scripts/lifecycle.sh +0 -126
  66. package/templates/hermes-agent/template/.scripts/momo-wip-lock.py +0 -137
  67. package/templates/hermes-agent/template/.scripts/providers/linear.sh +0 -329
  68. package/templates/hermes-agent/template/.scripts/providers/plane.sh +0 -671
  69. package/templates/hermes-agent/template/.scripts/providers/trello.sh +0 -241
  70. package/templates/hermes-agent/template/.scripts/reporter-watchdog.py +0 -242
  71. package/templates/hermes-agent/template/.scripts/secret-scan.py +0 -134
  72. package/templates/hermes-agent/template/.scripts/sentinel/bin/emit-event.py +0 -106
  73. package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-autonomous-review.sh +0 -339
  74. package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-close-gate.sh +0 -179
  75. package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +0 -162
  76. package/templates/hermes-agent/template/.scripts/sentinel/docs/bloodbank-events.md +0 -42
  77. package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +0 -77
  78. package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +0 -116
  79. package/templates/hermes-agent/template/.scripts/store-onepassword-secret.py +0 -260
  80. package/templates/hermes-agent/template/SOUL.md.jinja +0 -197
  81. package/templates/hermes-agent/template/hermes.jinja +0 -109
  82. package/templates/hermes-agent/template/momo.jinja +0 -177
  83. package/templates/hermes-agent/template/role.yaml.jinja +0 -112
@@ -65,6 +65,22 @@ def _now_iso() -> str:
65
65
  return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
66
66
 
67
67
 
68
+ def hub_owns_hook(hook_id: str, agent: str) -> bool:
69
+ """An activated central concern must not gain another native owner.
70
+
71
+ The manifest is written only after native cutover. Standalone CommonProject
72
+ consumers without Bloodbank retain the same project hook fallback.
73
+ """
74
+ path = Path(os.environ.get("BB_HOOK_OWNERSHIP", Path.home() / ".config/33god/hook-hub/ownership.json"))
75
+ concern = {"skill-check-reminder": "skill-reminder", "merge-forward-session-rebalance": "merge-forward"}.get(hook_id, hook_id)
76
+ try:
77
+ manifest = json.loads(path.read_text())
78
+ return (manifest.get("version") == 1 and agent in manifest.get("clis", [])
79
+ and concern in manifest.get("handler_ids", []) and Path(manifest["registry"]).is_file())
80
+ except (OSError, ValueError, KeyError, TypeError):
81
+ return False
82
+
83
+
68
84
  def load_local() -> dict:
69
85
  """Per-dev overrides from .agents/local.json (gitignored). Fails open.
70
86
 
@@ -111,6 +127,8 @@ def build_event_groups(master: dict, agent_key: str) -> dict:
111
127
  order: list[tuple[str, str | None]] = []
112
128
 
113
129
  for hook in master["hooks"]:
130
+ if hub_owns_hook(hook["id"], agent_key):
131
+ continue
114
132
  lifecycle = hook["lifecycle"]
115
133
  event = ev_map.get(lifecycle)
116
134
  if not event: # this agent doesn't map this lifecycle (e.g. hermes stub)
@@ -140,7 +158,7 @@ def build_event_groups(master: dict, agent_key: str) -> dict:
140
158
 
141
159
 
142
160
  # --------------------------------------------------------------------------- #
143
- # Claude dialect: own the `hooks` key of the committed .claude/settings.json.
161
+ # Claude dialect: own only commands under this project's canonical hook path.
144
162
  # --------------------------------------------------------------------------- #
145
163
  def render_claude(master: dict) -> dict:
146
164
  target = REPO_ROOT / master["agents"]["claude"]["config_target"]
@@ -151,7 +169,14 @@ def render_claude(master: dict) -> dict:
151
169
  except json.JSONDecodeError:
152
170
  warn(f"{target} is not valid JSON; refusing to overwrite")
153
171
  return {"target": target, "changed": False, "error": True}
154
- settings["hooks"] = build_event_groups(master, "claude")
172
+ hooks = settings.setdefault("hooks", {})
173
+ if not isinstance(hooks, dict):
174
+ warn(f"{target} has a non-object hooks key; refusing to overwrite")
175
+ return {"target": target, "changed": False, "error": True}
176
+ _strip_caf_hooks(hooks, "$CLAUDE_PROJECT_DIR/.agents/hooks/")
177
+ _strip_caf_hooks(hooks, f"{REPO_ROOT}/.agents/hooks/")
178
+ for event, groups in build_event_groups(master, "claude").items():
179
+ hooks.setdefault(event, []).extend(groups)
155
180
  return {"target": target, "settings": settings, "changed": True}
156
181
 
157
182
 
@@ -192,8 +217,13 @@ def _strip_caf_hooks(hooks_obj: dict, marker: str) -> bool:
192
217
  """Remove our hooks from every event; drop emptied groups. Returns changed."""
193
218
  changed = False
194
219
  for event, groups in list(hooks_obj.items()):
220
+ if not isinstance(groups, list):
221
+ continue
195
222
  new_groups = []
196
223
  for group in groups:
224
+ if not isinstance(group, dict) or not isinstance(group.get("hooks", []), list):
225
+ new_groups.append(group)
226
+ continue
197
227
  kept = [h for h in group.get("hooks", []) if marker not in h.get("command", "")]
198
228
  if len(kept) != len(group.get("hooks", [])):
199
229
  changed = True
@@ -246,10 +276,6 @@ def install_codex(master: dict) -> None:
246
276
  if target.exists() and target.read_text() == serialized:
247
277
  log(f"codex: up to date ({target})")
248
278
  return
249
- backup = target.with_suffix(target.suffix + ".caf-bak")
250
- if target.exists() and not backup.exists():
251
- backup.write_text(target.read_text())
252
- log(f"codex: backed up -> {backup}")
253
279
  target.write_text(serialized)
254
280
  log(f"codex: injected project hooks into {target}")
255
281
 
@@ -301,6 +327,8 @@ def kimi_block(master: dict) -> str:
301
327
  begin, end = _kimi_markers(master)
302
328
  lines = [begin]
303
329
  for hook in master["hooks"]:
330
+ if hub_owns_hook(hook["id"], "kimi"):
331
+ continue
304
332
  event = ev.get(hook["lifecycle"])
305
333
  if not event:
306
334
  continue
@@ -314,6 +342,8 @@ def kimi_block(master: dict) -> str:
314
342
  lines.append(f'command = "{cmd}"') # paths have no quotes/backslashes -> safe TOML string
315
343
  lines.append(f"timeout = {timeout}")
316
344
  lines.append("")
345
+ if len(lines) == 1:
346
+ return ""
317
347
  lines.append(end)
318
348
  return "\n".join(lines) + "\n"
319
349
 
@@ -334,6 +364,12 @@ def install_kimi(master: dict) -> None:
334
364
  return
335
365
  text = target.read_text() if target.exists() else ""
336
366
  body, _ = _strip_kimi_block(text, master) # work against config minus our block
367
+ block = kimi_block(master)
368
+ if not block:
369
+ if body != text:
370
+ target.write_text(body)
371
+ log("kimi: retired project hooks owned by the central hub")
372
+ return
337
373
  # Refuse if a foreign hooks definition exists (would collide with [[hooks]]).
338
374
  if re.search(r"(?m)^\s*hooks\s*=\s*\[[^\]]", body) or re.search(r"(?m)^\s*\[\[hooks\]\]", body):
339
375
  warn("kimi: existing non-CAF hooks found in config.toml; skipping to avoid a TOML conflict")
@@ -343,14 +379,10 @@ def install_kimi(master: dict) -> None:
343
379
  body += "\n"
344
380
  # Append "\n" + block (block ends with "\n"); the lone "\n" is what strip removes,
345
381
  # so the body above is preserved verbatim on uninstall.
346
- new = body + "\n" + kimi_block(master)
382
+ new = body + "\n" + block
347
383
  if text == new:
348
384
  log(f"kimi: up to date ({target})")
349
385
  return
350
- backup = target.with_suffix(target.suffix + ".caf-bak")
351
- if target.exists() and not backup.exists():
352
- backup.write_text(text)
353
- log(f"kimi: backed up -> {backup}")
354
386
  target.write_text(new)
355
387
  log(f"kimi: injected project hooks into {target}")
356
388
 
@@ -381,14 +413,13 @@ def hermes_commands(master: dict) -> list[tuple[str, str, int]]:
381
413
  return [
382
414
  (event, f"{runner} {event}", timeout)
383
415
  for _lifecycle, event in h["lifecycle_events"].items()
416
+ if any(hook["lifecycle"] == _lifecycle and not hub_owns_hook(hook["id"], "hermes") for hook in master["hooks"])
384
417
  ]
385
418
 
386
419
 
387
420
  def _backup_once(path: Path) -> None:
388
- backup = path.with_suffix(path.suffix + ".caf-bak")
389
- if path.exists() and not backup.exists():
390
- backup.write_text(path.read_text())
391
- log(f"hermes: backed up -> {backup.name}")
421
+ # Never copy potentially credential-bearing native settings beside source.
422
+ pass
392
423
 
393
424
 
394
425
  def _load_yaml(path: Path):
@@ -410,6 +441,9 @@ def install_hermes(master: dict) -> None:
410
441
  return
411
442
  marker = codex_marker()
412
443
  cmds = hermes_commands(master)
444
+ if not cmds:
445
+ uninstall_hermes(master)
446
+ return
413
447
 
414
448
  # --- config.yaml hooks block ---
415
449
  try:
@@ -419,13 +453,22 @@ def install_hermes(master: dict) -> None:
419
453
  return
420
454
  hooks = data.setdefault("hooks", {})
421
455
  cfg_changed = False
456
+ desired_pairs = {(event, command) for event, command, _ in cmds}
457
+ for event, entries in list(hooks.items()):
458
+ if not isinstance(entries, list):
459
+ continue
460
+ kept = [entry for entry in entries if marker not in entry.get("command", "")
461
+ or (event, entry.get("command")) in desired_pairs]
462
+ if kept != entries:
463
+ hooks[event] = kept
464
+ cfg_changed = True
422
465
  for event, command, timeout in cmds:
423
466
  entries = hooks.setdefault(event, [])
424
467
  ours = [e for e in entries if marker in e.get("command", "")]
425
468
  if not ours:
426
469
  entries.append({"command": command, "timeout": timeout})
427
470
  cfg_changed = True
428
- elif ours[0].get("command") != command: # drifted -> refresh
471
+ elif ours[0].get("command") != command or ours[0].get("timeout") != timeout:
429
472
  ours[0]["command"] = command
430
473
  ours[0]["timeout"] = timeout
431
474
  cfg_changed = True
@@ -439,6 +482,11 @@ def install_hermes(master: dict) -> None:
439
482
  allow_data = {"approvals": []}
440
483
  approvals = allow_data.setdefault("approvals", [])
441
484
  allow_changed = False
485
+ kept_approvals = [approval for approval in approvals if marker not in approval.get("command", "")
486
+ or (approval.get("event"), approval.get("command")) in desired_pairs]
487
+ if kept_approvals != approvals:
488
+ approvals = allow_data["approvals"] = kept_approvals
489
+ allow_changed = True
442
490
  for event, command, _timeout in cmds:
443
491
  if not any(a.get("command") == command and a.get("event") == event for a in approvals):
444
492
  approvals.append({
@@ -554,9 +602,10 @@ def cmd_check(master: dict) -> int:
554
602
  f"{base}/lib/hook-guard.sh {h['id']} {base}/{h['script']}"
555
603
  for h in master["hooks"]
556
604
  if master["agents"]["kimi"]["lifecycle_events"].get(h["lifecycle"])
605
+ and not hub_owns_hook(h["id"], "kimi")
557
606
  ]
558
607
  missing = [c for c in want_cmds if c not in present]
559
- if begin not in present or missing:
608
+ if (want_cmds and (begin not in present or missing)) or (not want_cmds and begin in present):
560
609
  warn(f"DRIFT: kimi config.toml missing the project hooks block "
561
610
  f"(run `mise run hooks:sync`)")
562
611
  rc = 1
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/delorenj/skillex/main/skills.schema.json",
3
3
  "inherit_global": true,
4
- "registry": "https://github.com/delorenj/skillex.git",
5
4
  "packs": [],
6
5
  "skills": []
7
6
  }
@@ -8,6 +8,12 @@
8
8
  # Until it is set the job runs and fails at the action step — see the "Code
9
9
  # Review" section of the CommonProject README.
10
10
  #
11
+ # Runner: self-hosted on big-chungus ONLY. GitHub-hosted minutes are billed and
12
+ # the budget is $0, so a hosted label (`ubuntu-latest`, `macos-*`) just sits
13
+ # unstarted. Org repos get the AutomaticAI-io runners; a personal-account repo
14
+ # needs its own repo-level runner (procedure: delonet-conventions skill,
15
+ # references/ci-runners.md).
16
+ #
11
17
  # Trigger: `pull_request`, deliberately NOT `pull_request_target`. The latter
12
18
  # would hand repo secrets to code from forked PRs; nothing here needs it.
13
19
  #
@@ -23,7 +29,7 @@ on:
23
29
 
24
30
  jobs:
25
31
  claude-review:
26
- runs-on: ubuntu-latest
32
+ runs-on: [self-hosted, Linux, delonet]
27
33
  permissions:
28
34
  contents: read
29
35
  pull-requests: read
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "project_name": {{ project_name | tojson }},
8
8
  "project_description": {{ project_description | tojson }},
9
- "project_slug": {{ project_slug | tojson }},
9
+ "project_id": {{ project_slug | lower | tojson }},
10
10
  "repo_path": "",
11
11
  "ticket_provider": {
12
12
  "type": {{ ticket_provider | tojson }},
@@ -20,35 +20,29 @@ _.file = [".env"]
20
20
  #
21
21
  # NOTE: hooks are an array of [[hooks.enter]] tables (NOT enter = [ ... ]) and
22
22
  # every {{config_root}} path is single-quoted so it survives a space in the
23
- # project path — mise runs each `script` via `sh -c`.
23
+ # project path — mise runs each `run` via `sh -c`. (`run`, not `script`: mise
24
+ # deprecated `script` for spawned hook commands and removes it in 2027.3.0.)
24
25
  [[hooks.enter]]
25
- script = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-agentfiles.sh' '{% raw %}{{config_root}}{% endraw %}'"
26
+ run = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-agentfiles.sh' '{% raw %}{{config_root}}{% endraw %}'"
26
27
  [[hooks.enter]]
27
28
  # Atomic (PJAN-24/PJAN-57): shell complexity lives in the managed script so
28
29
  # mise never interpolates temporary variables. The script uses mktemp, traps
29
30
  # cleanup, and replaces .env only after op inject succeeds.
30
- script = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/materialize-env.sh'"
31
- [[hooks.enter]]
32
- script = "python3 '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/provision-packs.py' --root '{% raw %}{{config_root}}{% endraw %}'"
33
- [[hooks.enter]]
34
- script = "python3 '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/sync-skills.py' --scope project --root '{% raw %}{{config_root}}{% endraw %}'"
31
+ run = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/materialize-env.sh'"
35
32
  [[hooks.enter]]
36
- script = "[ -f '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/codegraph.sh' ] && '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/codegraph.sh' || true"
33
+ run = "[ -f '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/codegraph.sh' ] && '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/codegraph.sh' || true"
37
34
  {%- if agent_hooks_layer %}
38
35
  [[hooks.enter]]
39
- script = "'{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py' --install --quiet"
36
+ run = "'{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py' --install --quiet"
40
37
  {%- endif %}
41
38
  {%- if agent_hooks_layer %}
42
39
  [[hooks.leave]]
43
- script = "'{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py' --uninstall --quiet"
40
+ run = "'{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py' --uninstall --quiet"
44
41
  {%- endif %}
45
42
 
46
43
  [[watch_files]]
47
44
  patterns = ["AGENTS.md"]
48
45
  task = "link:agentfiles"
49
- [[watch_files]]
50
- patterns = [".agents/skills.json"]
51
- task = "skills:sync"
52
46
  {% if agent_hooks_layer %}
53
47
  # Re-fan-out the agent hooks whenever the single source of truth changes.
54
48
  [[watch_files]]
@@ -60,15 +54,11 @@ description = "Symlink all agent files to AGENTS.md"
60
54
  run = "'{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-agentfiles.sh' '{% raw %}{{config_root}}{% endraw %}'"
61
55
 
62
56
  [tasks."skills:sync"]
63
- description = "Sync skills from manifest to local CLI dirs"
64
- depends = ["skills:provision:packs"]
65
- run = "python3 '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/sync-skills.py' --scope project --root '{% raw %}{{config_root}}{% endraw %}'"
66
-
67
- [tasks."skills:provision:packs"]
68
- description = "Provision every Skillex pack declared in .agents/skills.json"
69
- run = "python3 '{% raw %}{{config_root}}{% endraw %}/.mise/scripts/provision-packs.py' --root '{% raw %}{{config_root}}{% endraw %}'"
57
+ description = "Reconcile this project's selected skills"
58
+ tools = { "npm:@delorenj/skillex" = { version = "0.1.2", allow_low_downloads = true }, node = "24" }
59
+ run = "skillex sync --scope project --project '{% raw %}{{config_root}}{% endraw %}'"
70
60
  {% if agent_hooks_layer %}
71
- # --- Project-scoped agent hooks + skill fan-out (see .agents/hooks/README.md) ---
61
+ # --- Project-scoped agent hooks (see .agents/hooks/README.md) ---
72
62
 
73
63
  [tasks."hooks:sync"]
74
64
  description = "Fan out .agents/hooks/hooks.master.json to each agent CLI (claude committed; codex/kimi injected; hermes adapter)"