alissa-tools-github-revloop 0.23.0__tar.gz → 0.25.0__tar.gz

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 (32) hide show
  1. {alissa_tools_github_revloop-0.23.0/src/main/alissa_tools_github_revloop.egg-info → alissa_tools_github_revloop-0.25.0}/PKG-INFO +1 -1
  2. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/src/main/alissa/tools/github/revloop/__main__.py +12 -0
  3. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/src/main/alissa/tools/github/revloop/alissa.py +138 -10
  4. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/src/main/alissa/tools/github/revloop/config.py +75 -1
  5. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/src/main/alissa/tools/github/revloop/loop.py +11 -1
  6. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/src/main/alissa/tools/github/revloop/prreview.py +6 -1
  7. alissa_tools_github_revloop-0.25.0/src/main/alissa/tools/github/revloop/version +1 -0
  8. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0/src/main/alissa_tools_github_revloop.egg-info}/PKG-INFO +1 -1
  9. alissa_tools_github_revloop-0.23.0/src/main/alissa/tools/github/revloop/version +0 -1
  10. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/LICENSE +0 -0
  11. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/MANIFEST.in +0 -0
  12. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/NOTICE +0 -0
  13. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/README.md +0 -0
  14. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/requirements.txt +0 -0
  15. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/setup.cfg +0 -0
  16. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/setup.py +0 -0
  17. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
  18. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/src/main/alissa/tools/github/revloop/ghclient.py +0 -0
  19. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/src/main/alissa/tools/github/revloop/proc.py +0 -0
  20. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/src/main/alissa/tools/github/revloop/state.py +0 -0
  21. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/src/main/alissa/tools/github/revloop/version.py +0 -0
  22. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/src/main/alissa/tools/github/revloop/webui/__init__.py +0 -0
  23. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/src/main/alissa/tools/github/revloop/webui/__main__.py +0 -0
  24. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/src/main/alissa/tools/github/revloop/webui/auth.py +0 -0
  25. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/src/main/alissa/tools/github/revloop/webui/page.py +0 -0
  26. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/src/main/alissa/tools/github/revloop/webui/server.py +0 -0
  27. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/src/main/alissa/tools/github/revloop/webui/sources.py +0 -0
  28. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/src/main/alissa/tools/github/revloop/webui/sysinfo.py +0 -0
  29. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/src/main/alissa_tools_github_revloop.egg-info/SOURCES.txt +0 -0
  30. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/src/main/alissa_tools_github_revloop.egg-info/dependency_links.txt +0 -0
  31. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/src/main/alissa_tools_github_revloop.egg-info/entry_points.txt +0 -0
  32. {alissa_tools_github_revloop-0.23.0 → alissa_tools_github_revloop-0.25.0}/src/main/alissa_tools_github_revloop.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-revloop
3
- Version: 0.23.0
3
+ Version: 0.25.0
4
4
  Summary: ALISSA-TOOLS-GITHUB-REVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -14,6 +14,7 @@ from .config import (
14
14
  ON_MISSING_CREATE,
15
15
  ON_MISSING_SKIP,
16
16
  ON_MISSING_SPAWN,
17
+ TASK_LIST_BOW_ENV,
17
18
  Config,
18
19
  load_config_file,
19
20
  resolve_config_path,
@@ -181,6 +182,16 @@ def build_parser() -> argparse.ArgumentParser:
181
182
  help="list the sponsor-union corpus even if the config narrows it",
182
183
  )
183
184
 
185
+ over.add_argument(
186
+ "--task-list-bow",
187
+ dest="task_list_bow_id",
188
+ metavar="BOW_ID",
189
+ help="scope `alissa task list` to one body of work (--bow): the Convex "
190
+ "_id of the BOW review tasks are created into, NOT a repo's `autodev:` "
191
+ "feed BOW and not a mirrorInstanceId. A review task outside it is "
192
+ f"invisible to this daemon. Overridden by ${TASK_LIST_BOW_ENV}",
193
+ )
194
+
184
195
  dry = over.add_mutually_exclusive_group()
185
196
  dry.add_argument(
186
197
  "--dry-run",
@@ -221,6 +232,7 @@ def overrides_from(args: argparse.Namespace) -> dict:
221
232
  "checks_spawn_wait_seconds": args.checks_spawn_wait_seconds,
222
233
  "review_task_miss_ttl_polls": args.review_task_miss_ttl_polls,
223
234
  "task_list_self_scope": args.task_list_self_scope,
235
+ "task_list_bow_id": args.task_list_bow_id,
224
236
  "dry_run": args.dry_run,
225
237
  }
226
238
 
@@ -122,6 +122,48 @@ TASK_LIST_SELF_FLAG = "--self"
122
122
  TASK_LIST_VIEW_FLAG = "--view"
123
123
  TASK_LIST_DIGEST_VIEW = "digest"
124
124
 
125
+ # `--bow` narrows the CANDIDATE SET rather than filtering the answer, and that
126
+ # is the whole reason it is worth more than the flags above (issue #100).
127
+ #
128
+ # `--status` / `--self` / `--view` all start from the operator's involvement
129
+ # index -- every non-terminal task they are party to -- and cut it down. A BOW
130
+ # scope starts from that body of work's junction rows instead, which carry a
131
+ # denormalized status, so a row that does not match never costs a task-document
132
+ # read at all. The cache story is the same shape: a BOW-scoped query is
133
+ # invalidated only by writes INSIDE that BOW, so the operator's unrelated task
134
+ # churn stops re-billing this daemon's 30-second poll.
135
+ #
136
+ # It really is a SWAP and not an intersection, and that was measured against the
137
+ # installed CLI on 2026-08-28 rather than assumed (PR #101 round 1): across five
138
+ # real BOWs, not one answer was a subset of the plain 1335-row one, and the rows
139
+ # outside it were all non-terminal. Two consequences the composition story has to
140
+ # respect, from the same probe:
141
+ #
142
+ # * `--self` is IGNORED under `--bow`. `--bow X --self` returns exactly what
143
+ # `--bow X` returns, and that set is not a subset of the `--self` answer. So
144
+ # a BOW-scoped deployment gets the body of work's whole membership, actor-
145
+ # owned or not -- WIDER than `--self`, never narrower.
146
+ # * `--include-terminal` is ignored too (a BOW holding 52 tasks, 17 of them
147
+ # `validated`, answers 35 either way), so the default non-terminal filter is
148
+ # applied server-side but the flag that lifts it is not. `--status` could not
149
+ # be probed -- this CLI has none -- and on that evidence should not be
150
+ # assumed to compose either.
151
+ #
152
+ # None of that is a correctness risk, which is why the flags are still sent: a
153
+ # filter the server ignores makes the answer WIDER, `is_open` still runs
154
+ # client-side, and the argv is one the CLI accepts. It costs bytes, not reviews.
155
+ # Treat the list above as a dated snapshot, not a contract: this CLI has grown
156
+ # flags twice now without moving off version `0.1.0`.
157
+ #
158
+ # Opt-in per deployment for the same reason `--self` is, only sharper: a review
159
+ # task that is not attached to the configured BOW is INVISIBLE to the daemon --
160
+ # and on the default `on_missing_review_task` that is not even a missed round but
161
+ # a round spawned untethered from its task -- and review tasks are only born into
162
+ # a BOW once the sessions that create them say so. A configured BOW that nothing
163
+ # writes into lists empty; `list_tasks` retries plain and drops the BOW alone.
164
+ # That is a safety net, not a licence to guess the id.
165
+ TASK_LIST_BOW_FLAG = "--bow"
166
+
125
167
 
126
168
  @dataclass(frozen=True)
127
169
  class TaskListFlags:
@@ -135,6 +177,7 @@ class TaskListFlags:
135
177
  status: bool = False
136
178
  self_scope: bool = False
137
179
  digest: bool = False
180
+ bow: bool = False
138
181
 
139
182
 
140
183
  # How deeply an OPTION may be indented in a commander help listing. Commander
@@ -370,7 +413,12 @@ class TaskDetail:
370
413
 
371
414
 
372
415
  class Alissa:
373
- def __init__(self, *, task_list_self_scope: bool = False) -> None:
416
+ def __init__(
417
+ self,
418
+ *,
419
+ task_list_self_scope: bool = False,
420
+ task_list_bow_id: "str | None" = None,
421
+ ) -> None:
374
422
  """`task_list_self_scope` opts the list call into `--self`.
375
423
 
376
424
  Default OFF, and that default is evidence, not caution -- see
@@ -378,8 +426,15 @@ class Alissa:
378
426
  of a DEPLOYMENT (who creates its review tasks), not of this code, and an
379
427
  operator who knows their review tasks are all actor-owned should be able
380
428
  to say so.
429
+
430
+ `task_list_bow_id` opts the list call into `--bow <id>` (issue #100).
431
+ Unset -- the default -- is today's call shape, unconditionally. It is
432
+ the id ALREADY RESOLVED through the config layers (file < CLI < env);
433
+ this class does not read the environment, so the one place that decides
434
+ what the id is stays `config.env_task_list_bow_id`.
381
435
  """
382
436
  self._task_list_self_scope = bool(task_list_self_scope)
437
+ self._task_list_bow_id = (task_list_bow_id or "").strip() or None
383
438
  # The probe's answer, memoized for the process; None = not probed yet.
384
439
  # A probe that FAILS is deliberately not memoized (see probe_task_list).
385
440
  self._task_list_flags: "TaskListFlags | None" = None
@@ -389,10 +444,27 @@ class Alissa:
389
444
  # wrongly is worse than a list that is large: `find_review_task` reads an
390
445
  # empty corpus as "this PR has no review task".
391
446
  self._task_list_narrowing_disabled = False
447
+ # Set when the BOW SPECIFICALLY has been disproved at runtime -- a
448
+ # BOW-scoped call that answered empty. Separate from the flag above
449
+ # because the evidence is different in kind: `--status`/`--self`/
450
+ # `--view` filter the same corpus, so an empty answer from them is
451
+ # anomalous, while `--bow` REPLACES the corpus and "this body of work
452
+ # holds nothing" is a legitimate answer (see `list_tasks`).
453
+ self._task_list_bow_disabled = False
392
454
  # One diagnostic per client for a status filter that could not be sent
393
455
  # (see `_warn_status_filter_dropped`); this path runs every poll pass.
394
456
  self._status_filter_warned = False
395
457
 
458
+ @property
459
+ def task_list_bow_id(self) -> "str | None":
460
+ """The BOW this client scopes its task list to, or None.
461
+
462
+ Read by the boot preflight, which must warn on the id this CLIENT holds
463
+ rather than on the one the config file happens to carry -- the env layer
464
+ wins over both file and CLI, so those two can disagree.
465
+ """
466
+ return self._task_list_bow_id
467
+
396
468
  # -- the `alissa task list` narrowing probe -----------------------------
397
469
 
398
470
  def probe_task_list(self) -> "TaskListFlags":
@@ -433,6 +505,7 @@ class Alissa:
433
505
  status=_advertises(helptext, TASK_LIST_STATUS_FLAG),
434
506
  self_scope=_advertises(helptext, TASK_LIST_SELF_FLAG),
435
507
  digest=_advertises(helptext, TASK_LIST_VIEW_FLAG),
508
+ bow=_advertises(helptext, TASK_LIST_BOW_FLAG),
436
509
  )
437
510
  self._task_list_flags = flags
438
511
  return flags
@@ -472,13 +545,26 @@ class Alissa:
472
545
  def task_list_argv(self, *, narrow_status: bool = True) -> list[str]:
473
546
  """The narrowest `alissa task list` this CLI actually supports.
474
547
 
475
- Every addition is probe-gated, so an older CLI -- today's, which offers
476
- none of them -- produces exactly the call the daemon has always made.
548
+ Every addition is probe-gated, so a CLI that offers none of them
549
+ produces exactly the call the daemon has always made. (The CLI installed
550
+ on 2026-08-28 offers `--self` and `--bow`, so that is no longer the
551
+ no-op it once was -- and it still reports version `0.1.0`, which is why
552
+ the gate reads the help rather than the version. See
553
+ TASK_LIST_BOW_FLAG.)
477
554
  """
478
555
  argv = ["alissa", "task", "list", "--json"]
479
556
  if self._task_list_narrowing_disabled:
480
557
  return argv
481
558
  flags = self.probe_task_list()
559
+ # The BOW goes FIRST because it is the only one of these that chooses a
560
+ # candidate set rather than filtering one, and reading the argv left to
561
+ # right is reading them in that order. The others are still sent: the
562
+ # projection (`--view digest`) genuinely applies within a BOW, and the
563
+ # ones the server ignores there cost bytes rather than reviews -- see
564
+ # TASK_LIST_BOW_FLAG for which is which, and for why that list is a
565
+ # dated snapshot rather than a contract.
566
+ if flags.bow and self._task_list_bow_id and not self._task_list_bow_disabled:
567
+ argv += [TASK_LIST_BOW_FLAG, self._task_list_bow_id]
482
568
  # An empty filter is not a filter: `_status_filter` answers `""` when
483
569
  # the open set carries a status the CLI would reject, and sending
484
570
  # `--status ""` would be that same rejected call with an extra step.
@@ -502,8 +588,9 @@ class Alissa:
502
588
  filtering. Newer CLIs offer more, so the call is now assembled from a
503
589
  boot-time probe of the installed CLI's help (`task_list_argv`): a status
504
590
  filter covering exactly the statuses a live review task can hold, a lean
505
- `--view digest`, and `--self` when the deployment says its review tasks
506
- are actor-owned. None of it is required; an absent flag is simply not
591
+ `--view digest`, `--self` when the deployment says its review tasks are
592
+ actor-owned, and `--bow <id>` when it has named the body of work they
593
+ are created into. None of it is required; an absent flag is simply not
507
594
  sent.
508
595
 
509
596
  Narrowing is still the SECOND line of defence, not the first. Even a
@@ -520,10 +607,17 @@ class Alissa:
520
607
  filter; every other narrowing still applies.
521
608
 
522
609
  A narrowed call that FAILS, or that answers with an empty corpus, is
523
- retried once unnarrowed and turns the narrowing off for the rest of the
524
- process. Both are how a CLI that advertises a flag its API does not
525
- serve would present, and either would otherwise read as "this actor has
526
- no review tasks" -- which is a skipped review, not a slower one.
610
+ retried once unnarrowed. Both are how a CLI that advertises a flag its
611
+ API does not serve would present, and either would otherwise read as
612
+ "this actor has no review tasks" -- which is a skipped review, not a
613
+ slower one.
614
+
615
+ What the retry then turns OFF differs by which signal it was, because
616
+ the two carry different evidence. A FAILURE turns off all narrowing for
617
+ the process. An EMPTY answer does too -- unless the call was BOW-scoped,
618
+ in which case only the BOW is dropped and the other flags stand: `--bow`
619
+ replaces the corpus instead of filtering it, so an empty answer from it
620
+ is a legitimate result and says nothing about the rest.
527
621
  """
528
622
  argv = self.task_list_argv(narrow_status=narrow_status)
529
623
  plain = ["alissa", "task", "list", "--json"]
@@ -532,6 +626,13 @@ class Alissa:
532
626
  except CommandError:
533
627
  if argv == plain:
534
628
  raise
629
+ # Deliberately blunter than the empty-answer path below, which
630
+ # attributes the disproof to `--bow` alone when it can. A hard
631
+ # failure cannot be attributed: it does not say which flag the
632
+ # server choked on, and a transient timeout or 5xx says nothing
633
+ # about the flags at all. So all narrowing goes -- wide but
634
+ # complete, which costs the optimization and never a review. The
635
+ # asymmetry is a decision, not a corner the BOW split missed.
535
636
  log.warning(
536
637
  "`%s` failed — retrying the plain task list and dropping the "
537
638
  "narrowing for this process", " ".join(argv),
@@ -558,7 +659,34 @@ class Alissa:
558
659
  "empty corpus from a filter this API does not serve", " ".join(argv),
559
660
  )
560
661
  tasks = self._tasks_from(run_json(plain, timeout=90) or [])
561
- if tasks:
662
+ if not tasks:
663
+ return tasks
664
+ if TASK_LIST_BOW_FLAG in argv:
665
+ # "The plain list found rows" proves nothing about a BOW-scoped
666
+ # call, because the two answers are not comparable: `--bow` swaps
667
+ # the candidate set rather than filtering the actor's, so its answer
668
+ # is not a subset of the plain one (measured 2026-08-28 -- see
669
+ # TASK_LIST_BOW_FLAG). Condemning the other three flags on this
670
+ # evidence would drop the whole optimization on the state the
671
+ # rollout STARTS in: a review BOW nothing has been created into yet.
672
+ #
673
+ # The BOW itself is still dropped for the process, and that is not a
674
+ # third option so much as the only tractable one: with a swap there
675
+ # is no way to tell a mistyped id from an empty-but-correct one, and
676
+ # holding the BOW would mean paying two list calls every pass, for
677
+ # ever, on a typo. Wide-but-complete, the direction everything on
678
+ # this path degrades in.
679
+ self._task_list_bow_disabled = True
680
+ log.warning(
681
+ "the BOW-scoped task list answered empty while the plain list "
682
+ "returned %d task(s) — that is not evidence the call is broken "
683
+ "(a BOW-scoped list REPLACES the actor's corpus), so only the "
684
+ "BOW is dropped for this process and the other narrowing "
685
+ "stands. Either the configured id is wrong, or nothing has been "
686
+ "created into that body of work yet; once it is populated, "
687
+ "restart the daemon to scope to it again", len(tasks),
688
+ )
689
+ else:
562
690
  self._task_list_narrowing_disabled = True
563
691
  log.warning(
564
692
  "the plain task list returned %d task(s) — the narrowed call is "
@@ -6,6 +6,13 @@ Settings come from three layers, later winning over earlier:
6
6
  2. a JSON config file (see `resolve_config_path`)
7
7
  3. CLI arguments
8
8
 
9
+ `task_list_bow_id` adds a fourth above them all — the environment
10
+ (`ALISSA_REVIEW_TASK_BOW`, see `env_task_list_bow_id`). It is the only key that
11
+ does, and the reason is that it is the only one a second entry point needs:
12
+ `alissa-pr-review` builds its own `Alissa` client with no config file and no
13
+ argv of the daemon's, so an id that lives only in the file or the flags reaches
14
+ the poll loop and silently misses that call site.
15
+
9
16
  `workspace_root` is deliberately **not** a config key — it is a property of the
10
17
  running process, not of the settings. That lets one config file drive several
11
18
  daemons over different workspaces on the same machine, each pointed with
@@ -15,6 +22,7 @@ daemons over different workspaces on the same machine, each pointed with
15
22
  from __future__ import annotations
16
23
 
17
24
  import json
25
+ import os
18
26
  import re
19
27
  from dataclasses import dataclass
20
28
  from pathlib import Path
@@ -135,9 +143,30 @@ CONFIG_KEYS = (
135
143
  "checks_spawn_wait_seconds",
136
144
  "review_task_miss_ttl_polls",
137
145
  "task_list_self_scope",
146
+ "task_list_bow_id",
138
147
  "dry_run",
139
148
  )
140
149
 
150
+ # The environment variable carrying the review BOW id (issue #100). It outranks
151
+ # both the config file and the CLI flag -- see the module docstring for why this
152
+ # one key gets a layer the others do not.
153
+ TASK_LIST_BOW_ENV = "ALISSA_REVIEW_TASK_BOW"
154
+
155
+
156
+ def env_task_list_bow_id(environ: "Mapping[str, str] | None" = None) -> "str | None":
157
+ """The review BOW id from the environment, or None when it is not set.
158
+
159
+ `environ` is passed in rather than read from `os.environ` at every call site
160
+ so the precedence is a pure function the tests can drive (the same shape
161
+ `webui.auth.require_passcode` uses). None and empty are the SAME answer: an
162
+ exported-but-empty variable is how a container renders "unset", and reading
163
+ it as an id would send `--bow ""` -- a call the CLI would take and answer
164
+ with nobody's tasks.
165
+ """
166
+ raw = (os.environ if environ is None else environ).get(TASK_LIST_BOW_ENV)
167
+ return (raw or "").strip() or None
168
+
169
+
141
170
  MIN_POLL_INTERVAL = 10 # the search API allows 30 req/min
142
171
 
143
172
  # A reviewer session that has not submitted after this long is presumed dead
@@ -343,6 +372,19 @@ class Config:
343
372
  # when the installed CLI does not advertise the flag.
344
373
  task_list_self_scope: bool = False
345
374
 
375
+ # The Convex `_id` of the body of work review tasks are created into. Set it
376
+ # and `alissa task list` is scoped to that BOW's junction rows (`--bow`)
377
+ # instead of the operator's whole involvement index; leave it None -- the
378
+ # default -- and the call shape is exactly what it has always been.
379
+ #
380
+ # Off by default because a review task OUTSIDE the configured BOW is
381
+ # invisible to the daemon, which is a missed round rather than a slower one,
382
+ # and nothing creates review tasks into a BOW until the operator's review
383
+ # protocol says to. Ignored when the installed CLI does not advertise the
384
+ # flag. See alissa.TASK_LIST_BOW_FLAG, and the README for the two ways to
385
+ # get the id wrong (a repo's `autodev:` feed BOW; a `mirrorInstanceId`).
386
+ task_list_bow_id: str | None = None
387
+
346
388
  dry_run: bool = False
347
389
 
348
390
  def __post_init__(self) -> None:
@@ -395,9 +437,15 @@ class Config:
395
437
  workspace_root: Path,
396
438
  file_data: Mapping[str, Any] | None = None,
397
439
  overrides: Mapping[str, Any] | None = None,
440
+ environ: Mapping[str, str] | None = None,
398
441
  ) -> "Config":
399
442
  """Merge the layers and validate. `overrides` entries that are None mean
400
- "not specified on the CLI" and fall through to the file / defaults."""
443
+ "not specified on the CLI" and fall through to the file / defaults.
444
+
445
+ `environ` is the fourth layer and applies to `task_list_bow_id` alone;
446
+ it wins over both of the others (see the module docstring). Defaults to
447
+ the real environment, so callers that do not care pass nothing.
448
+ """
401
449
  raw: dict[str, Any] = dict(file_data or {})
402
450
 
403
451
  if "workspace_root" in raw:
@@ -419,6 +467,31 @@ class Config:
419
467
  if value is not None:
420
468
  raw[key] = value
421
469
 
470
+ # Applied AFTER the CLI overrides, which is the whole of "file < CLI <
471
+ # env": the variable is how a container hands the daemon an id that no
472
+ # file on its volume and no argv in its unit carries, so it has to be
473
+ # able to say something the other two do not.
474
+ env_bow = env_task_list_bow_id(environ)
475
+ if env_bow is not None:
476
+ raw["task_list_bow_id"] = env_bow
477
+ bow_id = raw.get("task_list_bow_id")
478
+ if bow_id is not None and not isinstance(bow_id, str):
479
+ # Deliberately NOT the `int(...)`/`bool(...)` coercion the numeric
480
+ # keys around here use. Those coerce into a value the validation
481
+ # below them then checks; there is nothing downstream to check an
482
+ # id against, so `0` would coerce to a well-formed `--bow 0` that
483
+ # can never resolve -- landing the deployment in the empty-answer
484
+ # path instead of telling it anything. A misspelled KEY already
485
+ # fails loudly at load; a mistyped VALUE should too.
486
+ raise ValueError(
487
+ f"task_list_bow_id must be the review BOW's Convex _id as a "
488
+ f"string (or null for unset), got a {type(bow_id).__name__}"
489
+ )
490
+ # Normalised the same way the env layer is, so `""` from a config file
491
+ # or a flag means "unset" too rather than `--bow ""` -- a call the CLI
492
+ # takes and answers with nobody's tasks.
493
+ bow_id = (bow_id or "").strip() or None
494
+
422
495
  mode = raw.get("on_missing_review_task", ON_MISSING_SPAWN)
423
496
  if mode not in _MISSING_MODES:
424
497
  raise ValueError(
@@ -580,6 +653,7 @@ class Config:
580
653
  checks_spawn_wait_seconds=spawn_wait,
581
654
  review_task_miss_ttl_polls=miss_ttl,
582
655
  task_list_self_scope=bool(raw.get("task_list_self_scope", False)),
656
+ task_list_bow_id=bow_id,
583
657
  dry_run=bool(raw.get("dry_run", False)),
584
658
  )
585
659
 
@@ -1425,7 +1425,8 @@ class ReviewWatcher:
1425
1425
  config.reviewer_login, token_env=config.reviewer_token_env
1426
1426
  )
1427
1427
  self.alissa = alissa or Alissa(
1428
- task_list_self_scope=config.task_list_self_scope
1428
+ task_list_self_scope=config.task_list_self_scope,
1429
+ task_list_bow_id=config.task_list_bow_id,
1429
1430
  )
1430
1431
  self.state = state or State(config.state_db)
1431
1432
  # (repo, number, comment id) of every re-entry directive already
@@ -4063,6 +4064,15 @@ class ReviewWatcher:
4063
4064
  "does not advertise `task list --self` — the task list is not "
4064
4065
  "actor-scoped; upgrade the CLI or drop the key"
4065
4066
  )
4067
+ # Read off the CLIENT, not the config: the env layer outranks both the
4068
+ # file and the flag, so an operator who set only ALISSA_REVIEW_TASK_BOW
4069
+ # would otherwise get no warning about the CLI that cannot serve it.
4070
+ if self.alissa.task_list_bow_id and not self.alissa.probe_task_list().bow:
4071
+ warnings.append(
4072
+ "a review task BOW is configured but the installed `alissa` "
4073
+ "CLI does not advertise `task list --bow` — the task list is "
4074
+ "not BOW-scoped; upgrade the CLI or drop the key"
4075
+ )
4066
4076
 
4067
4077
  if not self.config.dry_run and not self.alissa.worker_running():
4068
4078
  warnings.append(
@@ -35,6 +35,7 @@ import sys
35
35
  import time
36
36
 
37
37
  from .alissa import Alissa, VERDICT_APPROVE, VERDICT_REQUEST_CHANGES
38
+ from .config import env_task_list_bow_id
38
39
  from .ghclient import GitHub
39
40
  from .proc import CommandError, run, run_json
40
41
 
@@ -145,7 +146,11 @@ def main(argv: list[str] | None = None) -> int:
145
146
  )
146
147
 
147
148
  gh = GitHub()
148
- alissa = Alissa()
149
+ # The same BOW narrowing the daemon's poll uses (issue #100), from the one
150
+ # channel this command has: it runs in a task worktree, so there is no
151
+ # revloop config file under its cwd and no daemon argv to inherit. Unset ->
152
+ # the call shape this command has always made.
153
+ alissa = Alissa(task_list_bow_id=env_task_list_bow_id())
149
154
 
150
155
  try:
151
156
  # Identity guard: GitHub forbids requesting review from the PR author, so
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-revloop
3
- Version: 0.23.0
3
+ Version: 0.25.0
4
4
  Summary: ALISSA-TOOLS-GITHUB-REVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera