alissa-tools-github-revloop 0.22.0__tar.gz → 0.24.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.
- {alissa_tools_github_revloop-0.22.0/src/main/alissa_tools_github_revloop.egg-info → alissa_tools_github_revloop-0.24.0}/PKG-INFO +1 -1
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/src/main/alissa/tools/github/revloop/__main__.py +12 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/src/main/alissa/tools/github/revloop/alissa.py +171 -17
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/src/main/alissa/tools/github/revloop/config.py +75 -1
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/src/main/alissa/tools/github/revloop/loop.py +11 -1
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/src/main/alissa/tools/github/revloop/prreview.py +9 -4
- alissa_tools_github_revloop-0.24.0/src/main/alissa/tools/github/revloop/version +1 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0/src/main/alissa_tools_github_revloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_revloop-0.22.0/src/main/alissa/tools/github/revloop/version +0 -1
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/LICENSE +0 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/MANIFEST.in +0 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/NOTICE +0 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/README.md +0 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/requirements.txt +0 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/setup.cfg +0 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/setup.py +0 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/src/main/alissa/tools/github/revloop/ghclient.py +0 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/src/main/alissa/tools/github/revloop/proc.py +0 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/src/main/alissa/tools/github/revloop/state.py +0 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/src/main/alissa/tools/github/revloop/version.py +0 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/src/main/alissa/tools/github/revloop/webui/__init__.py +0 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/src/main/alissa/tools/github/revloop/webui/__main__.py +0 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/src/main/alissa/tools/github/revloop/webui/auth.py +0 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/src/main/alissa/tools/github/revloop/webui/page.py +0 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/src/main/alissa/tools/github/revloop/webui/server.py +0 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/src/main/alissa/tools/github/revloop/webui/sources.py +0 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/src/main/alissa/tools/github/revloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/src/main/alissa_tools_github_revloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/src/main/alissa_tools_github_revloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/src/main/alissa_tools_github_revloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_revloop-0.22.0 → alissa_tools_github_revloop-0.24.0}/src/main/alissa_tools_github_revloop.egg-info/top_level.txt +0 -0
|
@@ -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
|
|
|
@@ -22,6 +22,12 @@ log = logging.getLogger(__name__)
|
|
|
22
22
|
# 0.2.0 validates `--status` against exactly these seven client-side and exits 1
|
|
23
23
|
# on anything else BEFORE issuing any request, so a single non-canonical value
|
|
24
24
|
# does not narrow the call slightly less -- it costs the whole narrowed call.
|
|
25
|
+
#
|
|
26
|
+
# A status Alissa adds lands HERE first: a name added to `OPEN_STATUSES` alone
|
|
27
|
+
# reads as non-canonical and drops the narrowing for the whole fleet. That is
|
|
28
|
+
# the safe direction -- wide, never a wrong task resolved -- and the canonical
|
|
29
|
+
# test fails on it, but it fails naming `OPEN_STATUSES`, which is the constant
|
|
30
|
+
# that is right; this is the one that is stale (PR #98 round 1).
|
|
25
31
|
CANONICAL_TASK_STATUSES = frozenset(
|
|
26
32
|
{
|
|
27
33
|
"draft",
|
|
@@ -79,24 +85,24 @@ def _status_filter(statuses: "set[str] | frozenset[str]") -> str:
|
|
|
79
85
|
and `--self` narrowing down with it and pinning this process to the
|
|
80
86
|
whole-corpus list (`list_tasks`). One wide call beats every call being wide.
|
|
81
87
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
Pure on purpose. The condition is a code defect and deserves saying out
|
|
89
|
+
loud, but not from here: this runs at IMPORT, which is the one moment the
|
|
90
|
+
daemon's logging is not configured yet (see
|
|
91
|
+
`Alissa._warn_status_filter_dropped`). The tests are what pin the condition;
|
|
92
|
+
the runtime warning is the second line, and it is emitted where it can both
|
|
93
|
+
be formatted and be true.
|
|
85
94
|
"""
|
|
86
|
-
|
|
87
|
-
if unknown:
|
|
88
|
-
log.warning(
|
|
89
|
-
"not narrowing `alissa task list` by status: %s not canonical "
|
|
90
|
-
"Alissa status(es) -- the CLI rejects the whole call over one "
|
|
91
|
-
"unknown value, so this daemon lists the corpus unnarrowed",
|
|
92
|
-
", ".join(unknown),
|
|
93
|
-
)
|
|
95
|
+
if set(statuses) - CANONICAL_TASK_STATUSES:
|
|
94
96
|
return ""
|
|
95
97
|
return ",".join(sorted(statuses))
|
|
96
98
|
|
|
97
99
|
|
|
98
100
|
TASK_LIST_STATUS_FILTER = _status_filter(OPEN_STATUSES)
|
|
99
101
|
|
|
102
|
+
# Why the filter above is empty, when it is -- kept for the diagnostic, which
|
|
103
|
+
# cannot be emitted from module scope. Empty in the healthy case.
|
|
104
|
+
NON_CANONICAL_OPEN_STATUSES = tuple(sorted(OPEN_STATUSES - CANONICAL_TASK_STATUSES))
|
|
105
|
+
|
|
100
106
|
# `--self` drops the SPONSOR's corpus and keeps only the calling actor's rows.
|
|
101
107
|
#
|
|
102
108
|
# NOT enabled by default, and the reason is measured rather than cautious. On
|
|
@@ -116,6 +122,48 @@ TASK_LIST_SELF_FLAG = "--self"
|
|
|
116
122
|
TASK_LIST_VIEW_FLAG = "--view"
|
|
117
123
|
TASK_LIST_DIGEST_VIEW = "digest"
|
|
118
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
|
+
|
|
119
167
|
|
|
120
168
|
@dataclass(frozen=True)
|
|
121
169
|
class TaskListFlags:
|
|
@@ -129,6 +177,7 @@ class TaskListFlags:
|
|
|
129
177
|
status: bool = False
|
|
130
178
|
self_scope: bool = False
|
|
131
179
|
digest: bool = False
|
|
180
|
+
bow: bool = False
|
|
132
181
|
|
|
133
182
|
|
|
134
183
|
# How deeply an OPTION may be indented in a commander help listing. Commander
|
|
@@ -364,7 +413,12 @@ class TaskDetail:
|
|
|
364
413
|
|
|
365
414
|
|
|
366
415
|
class Alissa:
|
|
367
|
-
def __init__(
|
|
416
|
+
def __init__(
|
|
417
|
+
self,
|
|
418
|
+
*,
|
|
419
|
+
task_list_self_scope: bool = False,
|
|
420
|
+
task_list_bow_id: "str | None" = None,
|
|
421
|
+
) -> None:
|
|
368
422
|
"""`task_list_self_scope` opts the list call into `--self`.
|
|
369
423
|
|
|
370
424
|
Default OFF, and that default is evidence, not caution -- see
|
|
@@ -372,8 +426,15 @@ class Alissa:
|
|
|
372
426
|
of a DEPLOYMENT (who creates its review tasks), not of this code, and an
|
|
373
427
|
operator who knows their review tasks are all actor-owned should be able
|
|
374
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`.
|
|
375
435
|
"""
|
|
376
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
|
|
377
438
|
# The probe's answer, memoized for the process; None = not probed yet.
|
|
378
439
|
# A probe that FAILS is deliberately not memoized (see probe_task_list).
|
|
379
440
|
self._task_list_flags: "TaskListFlags | None" = None
|
|
@@ -383,6 +444,26 @@ class Alissa:
|
|
|
383
444
|
# wrongly is worse than a list that is large: `find_review_task` reads an
|
|
384
445
|
# empty corpus as "this PR has no review task".
|
|
385
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
|
|
454
|
+
# One diagnostic per client for a status filter that could not be sent
|
|
455
|
+
# (see `_warn_status_filter_dropped`); this path runs every poll pass.
|
|
456
|
+
self._status_filter_warned = False
|
|
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
|
|
386
467
|
|
|
387
468
|
# -- the `alissa task list` narrowing probe -----------------------------
|
|
388
469
|
|
|
@@ -424,10 +505,43 @@ class Alissa:
|
|
|
424
505
|
status=_advertises(helptext, TASK_LIST_STATUS_FLAG),
|
|
425
506
|
self_scope=_advertises(helptext, TASK_LIST_SELF_FLAG),
|
|
426
507
|
digest=_advertises(helptext, TASK_LIST_VIEW_FLAG),
|
|
508
|
+
bow=_advertises(helptext, TASK_LIST_BOW_FLAG),
|
|
427
509
|
)
|
|
428
510
|
self._task_list_flags = flags
|
|
429
511
|
return flags
|
|
430
512
|
|
|
513
|
+
def _warn_status_filter_dropped(self) -> None:
|
|
514
|
+
"""Say, once, that a status narrowing this CLI offers is going unused.
|
|
515
|
+
|
|
516
|
+
Not emitted where the filter is COMPUTED, which is import time, and that
|
|
517
|
+
is the whole point of it living here (PR #98 round 1): `__main__` pulls
|
|
518
|
+
this module in through `.loop` long before it calls
|
|
519
|
+
`logging.basicConfig`, so a warning at module scope falls through to
|
|
520
|
+
`logging.lastResort` -- bare on stderr, no timestamp or logger name, and
|
|
521
|
+
outside whatever handler the deployment configured.
|
|
522
|
+
|
|
523
|
+
It would also be premature there. At import nothing yet knows whether
|
|
524
|
+
this CLI serves `--status` at all, and on a CLI that does not, the
|
|
525
|
+
filter costs nothing and there is nothing to warn about. Here the
|
|
526
|
+
message is only emitted when the flag was on offer and could not be
|
|
527
|
+
used, which is the only case where the defect actually costs reads.
|
|
528
|
+
"""
|
|
529
|
+
if self._status_filter_warned:
|
|
530
|
+
return
|
|
531
|
+
self._status_filter_warned = True
|
|
532
|
+
reason = (
|
|
533
|
+
"non-canonical status(es) in OPEN_STATUSES: "
|
|
534
|
+
+ ", ".join(NON_CANONICAL_OPEN_STATUSES)
|
|
535
|
+
if NON_CANONICAL_OPEN_STATUSES
|
|
536
|
+
else "OPEN_STATUSES is empty"
|
|
537
|
+
)
|
|
538
|
+
log.warning(
|
|
539
|
+
"`alissa task list` offers --status and this daemon is not using "
|
|
540
|
+
"it (%s) -- the CLI rejects the whole call over one unknown value, "
|
|
541
|
+
"so the corpus is listed unnarrowed",
|
|
542
|
+
reason,
|
|
543
|
+
)
|
|
544
|
+
|
|
431
545
|
def task_list_argv(self, *, narrow_status: bool = True) -> list[str]:
|
|
432
546
|
"""The narrowest `alissa task list` this CLI actually supports.
|
|
433
547
|
|
|
@@ -438,11 +552,23 @@ class Alissa:
|
|
|
438
552
|
if self._task_list_narrowing_disabled:
|
|
439
553
|
return argv
|
|
440
554
|
flags = self.probe_task_list()
|
|
555
|
+
# The BOW goes FIRST because it is the only one of these that chooses a
|
|
556
|
+
# candidate set rather than filtering one, and reading the argv left to
|
|
557
|
+
# right is reading them in that order. The others are still sent: the
|
|
558
|
+
# projection (`--view digest`) genuinely applies within a BOW, and the
|
|
559
|
+
# ones the server ignores there cost bytes rather than reviews -- see
|
|
560
|
+
# TASK_LIST_BOW_FLAG for which is which, and for why that list is a
|
|
561
|
+
# dated snapshot rather than a contract.
|
|
562
|
+
if flags.bow and self._task_list_bow_id and not self._task_list_bow_disabled:
|
|
563
|
+
argv += [TASK_LIST_BOW_FLAG, self._task_list_bow_id]
|
|
441
564
|
# An empty filter is not a filter: `_status_filter` answers `""` when
|
|
442
565
|
# the open set carries a status the CLI would reject, and sending
|
|
443
566
|
# `--status ""` would be that same rejected call with an extra step.
|
|
444
|
-
if flags.status and narrow_status
|
|
445
|
-
|
|
567
|
+
if flags.status and narrow_status:
|
|
568
|
+
if TASK_LIST_STATUS_FILTER:
|
|
569
|
+
argv += [TASK_LIST_STATUS_FLAG, TASK_LIST_STATUS_FILTER]
|
|
570
|
+
else:
|
|
571
|
+
self._warn_status_filter_dropped()
|
|
446
572
|
if flags.self_scope and self._task_list_self_scope:
|
|
447
573
|
argv.append(TASK_LIST_SELF_FLAG)
|
|
448
574
|
if flags.digest:
|
|
@@ -458,8 +584,9 @@ class Alissa:
|
|
|
458
584
|
filtering. Newer CLIs offer more, so the call is now assembled from a
|
|
459
585
|
boot-time probe of the installed CLI's help (`task_list_argv`): a status
|
|
460
586
|
filter covering exactly the statuses a live review task can hold, a lean
|
|
461
|
-
`--view digest`,
|
|
462
|
-
|
|
587
|
+
`--view digest`, `--self` when the deployment says its review tasks are
|
|
588
|
+
actor-owned, and `--bow <id>` when it has named the body of work they
|
|
589
|
+
are created into. None of it is required; an absent flag is simply not
|
|
463
590
|
sent.
|
|
464
591
|
|
|
465
592
|
Narrowing is still the SECOND line of defence, not the first. Even a
|
|
@@ -514,7 +641,34 @@ class Alissa:
|
|
|
514
641
|
"empty corpus from a filter this API does not serve", " ".join(argv),
|
|
515
642
|
)
|
|
516
643
|
tasks = self._tasks_from(run_json(plain, timeout=90) or [])
|
|
517
|
-
if tasks:
|
|
644
|
+
if not tasks:
|
|
645
|
+
return tasks
|
|
646
|
+
if TASK_LIST_BOW_FLAG in argv:
|
|
647
|
+
# "The plain list found rows" proves nothing about a BOW-scoped
|
|
648
|
+
# call, because the two answers are not comparable: `--bow` swaps
|
|
649
|
+
# the candidate set rather than filtering the actor's, so its answer
|
|
650
|
+
# is not a subset of the plain one (measured 2026-08-28 -- see
|
|
651
|
+
# TASK_LIST_BOW_FLAG). Condemning the other three flags on this
|
|
652
|
+
# evidence would drop the whole optimization on the state the
|
|
653
|
+
# rollout STARTS in: a review BOW nothing has been created into yet.
|
|
654
|
+
#
|
|
655
|
+
# The BOW itself is still dropped for the process, and that is not a
|
|
656
|
+
# third option so much as the only tractable one: with a swap there
|
|
657
|
+
# is no way to tell a mistyped id from an empty-but-correct one, and
|
|
658
|
+
# holding the BOW would mean paying two list calls every pass, for
|
|
659
|
+
# ever, on a typo. Wide-but-complete, the direction everything on
|
|
660
|
+
# this path degrades in.
|
|
661
|
+
self._task_list_bow_disabled = True
|
|
662
|
+
log.warning(
|
|
663
|
+
"the BOW-scoped task list answered empty while the plain list "
|
|
664
|
+
"returned %d task(s) — that is not evidence the call is broken "
|
|
665
|
+
"(a BOW-scoped list REPLACES the actor's corpus), so only the "
|
|
666
|
+
"BOW is dropped for this process and the other narrowing "
|
|
667
|
+
"stands. Either the configured id is wrong, or nothing has been "
|
|
668
|
+
"created into that body of work yet; once it is populated, "
|
|
669
|
+
"restart the daemon to scope to it again", len(tasks),
|
|
670
|
+
)
|
|
671
|
+
else:
|
|
518
672
|
self._task_list_narrowing_disabled = True
|
|
519
673
|
log.warning(
|
|
520
674
|
"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
|
|
|
@@ -75,9 +76,9 @@ def _review_task_ref(alissa: Alissa, owner: str, repo: str, number: int) -> str
|
|
|
75
76
|
Deliberately not `Alissa.find_review_task` (which filters to open tasks): we
|
|
76
77
|
still want the ref just after the task is validated, to read its verdict.
|
|
77
78
|
`narrow_status=False` says the same thing to the LIST call for the same
|
|
78
|
-
reason -- the daemon's server-side status filter is exactly its
|
|
79
|
-
set
|
|
80
|
-
exists to avoid.
|
|
79
|
+
reason -- the daemon's server-side status filter is either exactly its
|
|
80
|
+
`is_open` set or absent altogether, and in the first case narrowing by it
|
|
81
|
+
here would re-impose the filter this function exists to avoid.
|
|
81
82
|
"""
|
|
82
83
|
pattern = re.compile(
|
|
83
84
|
rf"^Review PR\s+{re.escape(owner)}/{re.escape(repo)}#{number}\b", re.IGNORECASE
|
|
@@ -145,7 +146,11 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
145
146
|
)
|
|
146
147
|
|
|
147
148
|
gh = GitHub()
|
|
148
|
-
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.24.0
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.22.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|