alissa-tools-github-revloop 0.31.13__tar.gz → 0.31.17__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.31.13/src/main/alissa_tools_github_revloop.egg-info → alissa_tools_github_revloop-0.31.17}/PKG-INFO +13 -1
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/README.md +12 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/__main__.py +55 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/config.py +128 -1
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/fleet_vitals.py +11 -1
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/ghclient.py +58 -0
- alissa_tools_github_revloop-0.31.17/src/main/alissa/tools/github/revloop/janitor.py +373 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/loop.py +272 -1
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/loop_events.py +79 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/proc.py +20 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/state.py +24 -5
- alissa_tools_github_revloop-0.31.17/src/main/alissa/tools/github/revloop/version +1 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17/src/main/alissa_tools_github_revloop.egg-info}/PKG-INFO +13 -1
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa_tools_github_revloop.egg-info/SOURCES.txt +1 -0
- alissa_tools_github_revloop-0.31.13/src/main/alissa/tools/github/revloop/version +0 -1
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/LICENSE +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/MANIFEST.in +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/NOTICE +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/requirements.txt +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/setup.cfg +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/setup.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/alissa.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/alissa_client.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/bows.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/parking.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/prompts.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/prreview.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/trust.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/version.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/webui/__init__.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/webui/__main__.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/webui/auth.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/webui/page.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/webui/server.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/webui/sources.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa/tools/github/revloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa_tools_github_revloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/src/main/alissa_tools_github_revloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_revloop-0.31.13 → alissa_tools_github_revloop-0.31.17}/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.31.
|
|
3
|
+
Version: 0.31.17
|
|
4
4
|
Summary: ALISSA-TOOLS-GITHUB-REVLOOP
|
|
5
5
|
Home-page: https://alissa.app
|
|
6
6
|
Author: Fahera
|
|
@@ -46,6 +46,18 @@ The rule: a distribution owns a leaf package and declares only that subtree
|
|
|
46
46
|
`__init__.py` at any namespace level would claim it for one distribution and
|
|
47
47
|
shadow the others.
|
|
48
48
|
|
|
49
|
+
## Discovery PRs are not reviewed
|
|
50
|
+
|
|
51
|
+
A PR in a plans repository (`plan_repos`, `ALISSA_REVIEW_PLAN_REPOS`) whose
|
|
52
|
+
body carries `<!-- alissa-discovery v1 -->` on a line of its own and whose
|
|
53
|
+
every changed path is under `knowledge-base/` gets **no review round** — no
|
|
54
|
+
reviewer session, no review task — and one activity-comment line per head
|
|
55
|
+
saying why. A marked PR that touches anything else (or whose file list cannot
|
|
56
|
+
be read in full) is reviewed under the code directive, unchanged. The fleet
|
|
57
|
+
flag `discovery_review` (`ALISSA_REVIEW_DISCOVERY_REVIEW`, default off) turns
|
|
58
|
+
the skip off: every discovery PR is then reviewed like any other. See
|
|
59
|
+
*Discovery PRs* in the repository README.
|
|
60
|
+
|
|
49
61
|
## Install
|
|
50
62
|
|
|
51
63
|
```sh
|
|
@@ -24,6 +24,18 @@ The rule: a distribution owns a leaf package and declares only that subtree
|
|
|
24
24
|
`__init__.py` at any namespace level would claim it for one distribution and
|
|
25
25
|
shadow the others.
|
|
26
26
|
|
|
27
|
+
## Discovery PRs are not reviewed
|
|
28
|
+
|
|
29
|
+
A PR in a plans repository (`plan_repos`, `ALISSA_REVIEW_PLAN_REPOS`) whose
|
|
30
|
+
body carries `<!-- alissa-discovery v1 -->` on a line of its own and whose
|
|
31
|
+
every changed path is under `knowledge-base/` gets **no review round** — no
|
|
32
|
+
reviewer session, no review task — and one activity-comment line per head
|
|
33
|
+
saying why. A marked PR that touches anything else (or whose file list cannot
|
|
34
|
+
be read in full) is reviewed under the code directive, unchanged. The fleet
|
|
35
|
+
flag `discovery_review` (`ALISSA_REVIEW_DISCOVERY_REVIEW`, default off) turns
|
|
36
|
+
the skip off: every discovery PR is then reviewed like any other. See
|
|
37
|
+
*Discovery PRs* in the repository README.
|
|
38
|
+
|
|
27
39
|
## Install
|
|
28
40
|
|
|
29
41
|
```sh
|
|
@@ -16,6 +16,10 @@ from .config import (
|
|
|
16
16
|
FLEET_VITALS_ENV,
|
|
17
17
|
HUB_ADD,
|
|
18
18
|
HUB_SKIP,
|
|
19
|
+
JANITOR_ENABLED_ENV,
|
|
20
|
+
JANITOR_GRACE_MINUTES_ENV,
|
|
21
|
+
JANITOR_INTERVAL_MINUTES_ENV,
|
|
22
|
+
JANITOR_WARN_PCT_ENV,
|
|
19
23
|
LOOP_EVENTS_ENV,
|
|
20
24
|
ON_MISSING_CREATE,
|
|
21
25
|
ON_MISSING_SKIP,
|
|
@@ -280,6 +284,45 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
280
284
|
action="store_false",
|
|
281
285
|
help="do not push fleet vitals even if the config enables it",
|
|
282
286
|
)
|
|
287
|
+
janitor = over.add_mutually_exclusive_group()
|
|
288
|
+
janitor.add_argument(
|
|
289
|
+
"--janitor",
|
|
290
|
+
dest="janitor_enabled",
|
|
291
|
+
action="store_true",
|
|
292
|
+
default=None,
|
|
293
|
+
help="remove each hub's stale main/.review-scratch-* and REVIEW-* "
|
|
294
|
+
"folders that no live session or open round holds, prune the "
|
|
295
|
+
"worktree records, and warn on a full volume (the default). "
|
|
296
|
+
f"Overridden by ${JANITOR_ENABLED_ENV}",
|
|
297
|
+
)
|
|
298
|
+
janitor.add_argument(
|
|
299
|
+
"--no-janitor",
|
|
300
|
+
dest="janitor_enabled",
|
|
301
|
+
action="store_false",
|
|
302
|
+
help="never sweep the workspace, even if the config enables it",
|
|
303
|
+
)
|
|
304
|
+
over.add_argument(
|
|
305
|
+
"--janitor-grace-minutes",
|
|
306
|
+
type=int,
|
|
307
|
+
metavar="MINUTES",
|
|
308
|
+
help="how old (by mtime) a scratch folder or REVIEW- checkout must be "
|
|
309
|
+
"before the janitor may remove it (default: 1440). "
|
|
310
|
+
f"Overridden by ${JANITOR_GRACE_MINUTES_ENV}",
|
|
311
|
+
)
|
|
312
|
+
over.add_argument(
|
|
313
|
+
"--janitor-interval-minutes",
|
|
314
|
+
type=int,
|
|
315
|
+
metavar="MINUTES",
|
|
316
|
+
help="how often the janitor sweeps, at the end of a pass; 0 = every "
|
|
317
|
+
f"pass (default: 10). Overridden by ${JANITOR_INTERVAL_MINUTES_ENV}",
|
|
318
|
+
)
|
|
319
|
+
over.add_argument(
|
|
320
|
+
"--janitor-warn-pct",
|
|
321
|
+
type=int,
|
|
322
|
+
metavar="PERCENT",
|
|
323
|
+
help="workspace volume usage at or above which a sweep logs a "
|
|
324
|
+
f"WARNING; 0 = never (default: 80). Overridden by ${JANITOR_WARN_PCT_ENV}",
|
|
325
|
+
)
|
|
283
326
|
over.add_argument(
|
|
284
327
|
"--alissa-endpoint",
|
|
285
328
|
dest="alissa_endpoint",
|
|
@@ -336,6 +379,10 @@ def overrides_from(args: argparse.Namespace) -> dict:
|
|
|
336
379
|
"task_list_bow_id": args.task_list_bow_id,
|
|
337
380
|
"loop_events_enabled": args.loop_events_enabled,
|
|
338
381
|
"fleet_vitals_enabled": args.fleet_vitals_enabled,
|
|
382
|
+
"janitor_enabled": args.janitor_enabled,
|
|
383
|
+
"janitor_grace_minutes": args.janitor_grace_minutes,
|
|
384
|
+
"janitor_interval_minutes": args.janitor_interval_minutes,
|
|
385
|
+
"janitor_warn_pct": args.janitor_warn_pct,
|
|
339
386
|
"alissa_endpoint": args.alissa_endpoint,
|
|
340
387
|
"dry_run": args.dry_run,
|
|
341
388
|
}
|
|
@@ -422,6 +469,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
422
469
|
config = resolve_config(args)
|
|
423
470
|
log.info("workspace: %s", config.workspace_root)
|
|
424
471
|
log.info("repos_source: %s", config.repos_source)
|
|
472
|
+
log.info(
|
|
473
|
+
"janitor: %s (grace %d min, every %s, warn at %s)",
|
|
474
|
+
"on" if config.janitor_enabled else "off",
|
|
475
|
+
config.janitor_grace_minutes,
|
|
476
|
+
f"{config.janitor_interval_minutes} min"
|
|
477
|
+
if config.janitor_interval_minutes else "pass",
|
|
478
|
+
f"{config.janitor_warn_pct}%" if config.janitor_warn_pct else "never",
|
|
479
|
+
)
|
|
425
480
|
config = resolve_feed_authority(config)
|
|
426
481
|
|
|
427
482
|
watcher = ReviewWatcher(config)
|
|
@@ -183,6 +183,7 @@ CONFIG_KEYS = (
|
|
|
183
183
|
"authors",
|
|
184
184
|
"plan_repos",
|
|
185
185
|
"plan_authors",
|
|
186
|
+
"discovery_review",
|
|
186
187
|
"operators",
|
|
187
188
|
"agent_profile",
|
|
188
189
|
"reviewer_login",
|
|
@@ -206,6 +207,10 @@ CONFIG_KEYS = (
|
|
|
206
207
|
"prompt_kill_minutes",
|
|
207
208
|
"prompt_max_answers",
|
|
208
209
|
"waiting_dir",
|
|
210
|
+
"janitor_enabled",
|
|
211
|
+
"janitor_grace_minutes",
|
|
212
|
+
"janitor_interval_minutes",
|
|
213
|
+
"janitor_warn_pct",
|
|
209
214
|
"alissa_endpoint",
|
|
210
215
|
"dry_run",
|
|
211
216
|
)
|
|
@@ -233,6 +238,14 @@ REPOS_SOURCE_ENV = "ALISSA_REVIEW_REPOS_SOURCE"
|
|
|
233
238
|
BOWS_REFRESH_POLLS_ENV = "ALISSA_REVIEW_BOWS_REFRESH_POLLS"
|
|
234
239
|
BOW_OWNERS_ENV = "ALISSA_REVIEW_BOW_OWNERS"
|
|
235
240
|
|
|
241
|
+
# The workspace janitor's four rails (issue #167). Same precedence and the
|
|
242
|
+
# same blank-falls-through rule as the bows rails, so a container can tune
|
|
243
|
+
# (or switch off) the janitor without a file edit on its volume.
|
|
244
|
+
JANITOR_ENABLED_ENV = "ALISSA_REVIEW_JANITOR_ENABLED"
|
|
245
|
+
JANITOR_GRACE_MINUTES_ENV = "ALISSA_REVIEW_JANITOR_GRACE_MINUTES"
|
|
246
|
+
JANITOR_INTERVAL_MINUTES_ENV = "ALISSA_REVIEW_JANITOR_INTERVAL_MINUTES"
|
|
247
|
+
JANITOR_WARN_PCT_ENV = "ALISSA_REVIEW_JANITOR_WARN_PCT"
|
|
248
|
+
|
|
236
249
|
# The plan directive's two allowlists (issue #148; genloop design §6.1, D12).
|
|
237
250
|
# A PR gets the PLAN directive only when all four signals hold, and two of
|
|
238
251
|
# them are fleet configuration: the repository is one of `plan_repos` and the
|
|
@@ -256,6 +269,15 @@ BOW_OWNERS_ENV = "ALISSA_REVIEW_BOW_OWNERS"
|
|
|
256
269
|
PLAN_REPOS_ENV = "ALISSA_REVIEW_PLAN_REPOS"
|
|
257
270
|
PLAN_AUTHORS_ENV = "ALISSA_REVIEW_PLAN_AUTHORS"
|
|
258
271
|
PLAN_REPO_OWNER_PLACEHOLDER = "<owner>"
|
|
272
|
+
|
|
273
|
+
# The discovery fleet flag (issue #171; build-discovery design §3.3, DS-7).
|
|
274
|
+
# OFF by default: a DISCOVERY PR -- a plans repository, the discovery marker,
|
|
275
|
+
# every changed path under `knowledge-base/` (loop.discovery_skip_reason) --
|
|
276
|
+
# gets no review round at all, because its defence is the knowledge-base lint
|
|
277
|
+
# of record, staleness, the plan refuter and the user's acceptance. ON, every
|
|
278
|
+
# discovery PR is reviewed under the code directive like any other PR. Same
|
|
279
|
+
# env > file > default precedence and boolean spellings as the janitor's rail.
|
|
280
|
+
DISCOVERY_REVIEW_ENV = "ALISSA_REVIEW_DISCOVERY_REVIEW"
|
|
259
281
|
DEFAULT_PLAN_REPO_NAME = "alissa-dark-factory-plans"
|
|
260
282
|
DEFAULT_PLAN_REPOS: "tuple[str, ...]" = (
|
|
261
283
|
f"{PLAN_REPO_OWNER_PLACEHOLDER}/{DEFAULT_PLAN_REPO_NAME}",
|
|
@@ -408,6 +430,36 @@ def env_bows_refresh_polls(
|
|
|
408
430
|
) from None
|
|
409
431
|
|
|
410
432
|
|
|
433
|
+
def _env_int(name: str, what: str, environ: "Mapping[str, str] | None") -> "int | None":
|
|
434
|
+
"""One integer env rail, or None when unset/blank. A non-integer raises,
|
|
435
|
+
naming the variable -- the startup phase turns that into `config error`."""
|
|
436
|
+
raw = _env_text(name, environ)
|
|
437
|
+
if raw is None:
|
|
438
|
+
return None
|
|
439
|
+
try:
|
|
440
|
+
return int(raw)
|
|
441
|
+
except ValueError:
|
|
442
|
+
raise ValueError(f"{name} must be an integer {what}, got {raw!r}") from None
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def env_janitor(environ: "Mapping[str, str] | None" = None) -> "dict[str, Any]":
|
|
446
|
+
"""The janitor keys the environment sets (issue #167), as config-key ->
|
|
447
|
+
value; a key whose variable is unset or blank is absent."""
|
|
448
|
+
out: "dict[str, Any]" = {}
|
|
449
|
+
enabled = _env_bool(JANITOR_ENABLED_ENV, environ)
|
|
450
|
+
if enabled is not None:
|
|
451
|
+
out["janitor_enabled"] = enabled
|
|
452
|
+
for key, name, what in (
|
|
453
|
+
("janitor_grace_minutes", JANITOR_GRACE_MINUTES_ENV, "number of minutes"),
|
|
454
|
+
("janitor_interval_minutes", JANITOR_INTERVAL_MINUTES_ENV, "number of minutes"),
|
|
455
|
+
("janitor_warn_pct", JANITOR_WARN_PCT_ENV, "percentage"),
|
|
456
|
+
):
|
|
457
|
+
value = _env_int(name, what, environ)
|
|
458
|
+
if value is not None:
|
|
459
|
+
out[key] = value
|
|
460
|
+
return out
|
|
461
|
+
|
|
462
|
+
|
|
411
463
|
def env_bow_owners(environ: "Mapping[str, str] | None" = None) -> "str | None":
|
|
412
464
|
"""`ALISSA_REVIEW_BOW_OWNERS` verbatim, or None when unset/blank. The
|
|
413
465
|
`|`/`,` splitting and the actor-id check happen in `normalize_bow_owners`,
|
|
@@ -663,6 +715,23 @@ DEFAULT_VERDICT_COOLDOWN_SECONDS = 120
|
|
|
663
715
|
DEFAULT_REVIEW_TASK_MISS_TTL_POLLS = 10
|
|
664
716
|
|
|
665
717
|
|
|
718
|
+
# -- the workspace janitor (issue #167) ---------------------------------------
|
|
719
|
+
#
|
|
720
|
+
# How old a `.review-scratch-*` or `REVIEW-*` folder must be (by mtime) before
|
|
721
|
+
# the janitor may remove it. A day: far past any round's life (the stale-round
|
|
722
|
+
# window is 90 minutes), so the janitor only ever takes what every round that
|
|
723
|
+
# could want it has long finished with -- and the live-session / open-round
|
|
724
|
+
# guard holds anything a slow round is still using regardless.
|
|
725
|
+
DEFAULT_JANITOR_GRACE_MINUTES = 24 * 60
|
|
726
|
+
|
|
727
|
+
# How often the janitor sweeps, at the end of a pass. 0 = every pass.
|
|
728
|
+
DEFAULT_JANITOR_INTERVAL_MINUTES = 10
|
|
729
|
+
|
|
730
|
+
# The workspace volume's used percentage at or above which a sweep logs one
|
|
731
|
+
# WARNING. Devloop's threshold; 0 = never warn.
|
|
732
|
+
DEFAULT_JANITOR_WARN_PCT = 80
|
|
733
|
+
|
|
734
|
+
|
|
666
735
|
# -- the product-stability guard (issue #105) ---------------------------------
|
|
667
736
|
#
|
|
668
737
|
# How many consecutive `request_changes` rounds must move NOTHING outside
|
|
@@ -837,6 +906,10 @@ class Config:
|
|
|
837
906
|
plan_repos: tuple[str, ...] = DEFAULT_PLAN_REPOS
|
|
838
907
|
plan_authors: tuple[str, ...] = ()
|
|
839
908
|
|
|
909
|
+
# Review discovery PRs after all (issue #171); see DISCOVERY_REVIEW_ENV.
|
|
910
|
+
# False -- the default -- skips them.
|
|
911
|
+
discovery_review: bool = False
|
|
912
|
+
|
|
840
913
|
# GitHub logins whose re-entry ack may raise a capped PR's effective cap
|
|
841
914
|
# (loop.parse_reentry_ack). Empty -- the default -- means NO ack is ever
|
|
842
915
|
# honoured: the lever fails closed, because anyone who can comment on a PR
|
|
@@ -978,6 +1051,18 @@ class Config:
|
|
|
978
1051
|
# writes the same value here so the daemon looks where the hooks wrote.
|
|
979
1052
|
waiting_dir: str = DEFAULT_WAITING_DIR
|
|
980
1053
|
|
|
1054
|
+
# The workspace janitor (issue #167; janitor.py): every
|
|
1055
|
+
# `janitor_interval_minutes` (0 = every pass), at the end of a pass, remove
|
|
1056
|
+
# each hub's `main/.review-scratch-*` and `REVIEW-*` folders older than
|
|
1057
|
+
# `janitor_grace_minutes` that no live session or open ledger round holds,
|
|
1058
|
+
# prune `.source`'s worktree records, and WARN once when the workspace
|
|
1059
|
+
# volume is at or above `janitor_warn_pct` (0 = never). ON by default: the
|
|
1060
|
+
# disk it reclaims fills a shared volume within weeks without it.
|
|
1061
|
+
janitor_enabled: bool = True
|
|
1062
|
+
janitor_grace_minutes: int = DEFAULT_JANITOR_GRACE_MINUTES
|
|
1063
|
+
janitor_interval_minutes: int = DEFAULT_JANITOR_INTERVAL_MINUTES
|
|
1064
|
+
janitor_warn_pct: int = DEFAULT_JANITOR_WARN_PCT
|
|
1065
|
+
|
|
981
1066
|
dry_run: bool = False
|
|
982
1067
|
|
|
983
1068
|
def __post_init__(self) -> None:
|
|
@@ -1112,7 +1197,7 @@ class Config:
|
|
|
1112
1197
|
|
|
1113
1198
|
`environ` is the fourth layer and applies to the env-backed keys
|
|
1114
1199
|
(`task_list_bow_id`, `loop_events_enabled`, `fleet_vitals_enabled`,
|
|
1115
|
-
`repos_source`, `bows_refresh_polls`, `bow_owners`); it wins over both of the others
|
|
1200
|
+
`repos_source`, `bows_refresh_polls`, `bow_owners`, the `janitor_*` keys); it wins over both of the others
|
|
1116
1201
|
(see the module docstring). Defaults to the real environment, so
|
|
1117
1202
|
callers that do not care pass nothing.
|
|
1118
1203
|
"""
|
|
@@ -1187,6 +1272,8 @@ class Config:
|
|
|
1187
1272
|
env_owners = env_bow_owners(environ)
|
|
1188
1273
|
if env_owners is not None:
|
|
1189
1274
|
raw["bow_owners"] = env_owners
|
|
1275
|
+
# The janitor's rails (issue #167), same layer, same reason.
|
|
1276
|
+
raw.update(env_janitor(environ))
|
|
1190
1277
|
# The plan allowlists (issue #148), same rail, same reason.
|
|
1191
1278
|
env_plan_repos = env_plan_list(PLAN_REPOS_ENV, environ)
|
|
1192
1279
|
if env_plan_repos is not None:
|
|
@@ -1194,6 +1281,16 @@ class Config:
|
|
|
1194
1281
|
env_plan_authors = env_plan_list(PLAN_AUTHORS_ENV, environ)
|
|
1195
1282
|
if env_plan_authors is not None:
|
|
1196
1283
|
raw["plan_authors"] = env_plan_authors
|
|
1284
|
+
# The discovery flag (issue #171), same layer, same reason.
|
|
1285
|
+
env_discovery = _env_bool(DISCOVERY_REVIEW_ENV, environ)
|
|
1286
|
+
if env_discovery is not None:
|
|
1287
|
+
raw["discovery_review"] = env_discovery
|
|
1288
|
+
discovery_review = raw.get("discovery_review", cls.discovery_review)
|
|
1289
|
+
if not isinstance(discovery_review, bool):
|
|
1290
|
+
# A JSON string "false" is truthy; refused rather than read as on.
|
|
1291
|
+
raise ValueError(
|
|
1292
|
+
f"discovery_review must be true or false, got {discovery_review!r}"
|
|
1293
|
+
)
|
|
1197
1294
|
|
|
1198
1295
|
source = raw.get("repos_source", cls.repos_source)
|
|
1199
1296
|
if source not in _REPOS_SOURCES:
|
|
@@ -1439,6 +1536,31 @@ class Config:
|
|
|
1439
1536
|
if not waiting_dir:
|
|
1440
1537
|
raise ValueError("waiting_dir must be a non-empty path")
|
|
1441
1538
|
|
|
1539
|
+
janitor_grace = int(
|
|
1540
|
+
raw.get("janitor_grace_minutes", cls.janitor_grace_minutes)
|
|
1541
|
+
)
|
|
1542
|
+
if janitor_grace < 1:
|
|
1543
|
+
# No 0-means-off here: `janitor_enabled` is the off switch, and a
|
|
1544
|
+
# zero grace would make a scratch folder removable the minute a
|
|
1545
|
+
# guard misses it.
|
|
1546
|
+
raise ValueError(
|
|
1547
|
+
f"janitor_grace_minutes must be >= 1, got {janitor_grace}"
|
|
1548
|
+
)
|
|
1549
|
+
janitor_interval = int(
|
|
1550
|
+
raw.get("janitor_interval_minutes", cls.janitor_interval_minutes)
|
|
1551
|
+
)
|
|
1552
|
+
if janitor_interval < 0:
|
|
1553
|
+
raise ValueError(
|
|
1554
|
+
f"janitor_interval_minutes must be >= 0 (0 = every pass), "
|
|
1555
|
+
f"got {janitor_interval}"
|
|
1556
|
+
)
|
|
1557
|
+
janitor_warn = int(raw.get("janitor_warn_pct", cls.janitor_warn_pct))
|
|
1558
|
+
if not 0 <= janitor_warn <= 100:
|
|
1559
|
+
raise ValueError(
|
|
1560
|
+
f"janitor_warn_pct must be between 0 and 100 (0 = never warn), "
|
|
1561
|
+
f"got {janitor_warn}"
|
|
1562
|
+
)
|
|
1563
|
+
|
|
1442
1564
|
state_path = raw.get("state_path")
|
|
1443
1565
|
return cls(
|
|
1444
1566
|
workspace_root=Path(workspace_root),
|
|
@@ -1454,6 +1576,7 @@ class Config:
|
|
|
1454
1576
|
authors=authors,
|
|
1455
1577
|
plan_repos=plan_repos,
|
|
1456
1578
|
plan_authors=plan_authors,
|
|
1579
|
+
discovery_review=discovery_review,
|
|
1457
1580
|
operators=operators,
|
|
1458
1581
|
agent_profile=raw.get("agent_profile", "claude"),
|
|
1459
1582
|
reviewer_login=raw.get("reviewer_login"),
|
|
@@ -1477,6 +1600,10 @@ class Config:
|
|
|
1477
1600
|
prompt_kill_minutes=prompt_kill,
|
|
1478
1601
|
prompt_max_answers=prompt_answers,
|
|
1479
1602
|
waiting_dir=waiting_dir,
|
|
1603
|
+
janitor_enabled=bool(raw.get("janitor_enabled", cls.janitor_enabled)),
|
|
1604
|
+
janitor_grace_minutes=janitor_grace,
|
|
1605
|
+
janitor_interval_minutes=janitor_interval,
|
|
1606
|
+
janitor_warn_pct=janitor_warn,
|
|
1480
1607
|
alissa_endpoint=endpoint,
|
|
1481
1608
|
dry_run=bool(raw.get("dry_run", False)),
|
|
1482
1609
|
)
|
|
@@ -86,6 +86,7 @@ from .alissa_client import (
|
|
|
86
86
|
AlissaError,
|
|
87
87
|
)
|
|
88
88
|
from .config import REPOS_BOWS, Config
|
|
89
|
+
from .janitor import disk_used_pct as volume_used_pct
|
|
89
90
|
from .proc import CommandError, run as proc_run
|
|
90
91
|
|
|
91
92
|
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
@@ -117,7 +118,10 @@ VITALS_FAILED = "failed"
|
|
|
117
118
|
# The blocks Studio's schema gained after the base contract, each in its own
|
|
118
119
|
# lane: `idle` (seat parking L3) and `config` (provisioner L5). A strict 400
|
|
119
120
|
# naming one of them is an old Studio, not a bad snapshot.
|
|
120
|
-
|
|
121
|
+
#
|
|
122
|
+
# `diskUsedPct` (issue #167) joins them: the workspace volume's used percent,
|
|
123
|
+
# which Studio's schema may not carry yet.
|
|
124
|
+
OPTIONAL_BLOCKS = ("idle", "config", "diskUsedPct")
|
|
121
125
|
|
|
122
126
|
# How long a snapshot leaves an optional block out after Studio refused it as
|
|
123
127
|
# an unknown key, before it offers the block again. An hour: one extra POST
|
|
@@ -296,6 +300,7 @@ def build_snapshot(
|
|
|
296
300
|
repos: "tuple[str, ...]" = (),
|
|
297
301
|
now: "int | float | None" = None,
|
|
298
302
|
config: "Mapping | None" = None,
|
|
303
|
+
disk_used_pct: "float | None" = None,
|
|
299
304
|
) -> dict:
|
|
300
305
|
"""The contract body for this pass, off the console's builders.
|
|
301
306
|
|
|
@@ -314,6 +319,9 @@ def build_snapshot(
|
|
|
314
319
|
|
|
315
320
|
`config` is the seat's `config_block`, or None to leave the key out (the
|
|
316
321
|
console's push, which has no derived answer to give).
|
|
322
|
+
|
|
323
|
+
`diskUsedPct` (issue #167) is the workspace volume's used percent, null
|
|
324
|
+
when it could not be read.
|
|
317
325
|
"""
|
|
318
326
|
stamp = time.time() if now is None else now
|
|
319
327
|
snaps = sources.snapshots(DURATION_POINTS)
|
|
@@ -365,6 +373,7 @@ def build_snapshot(
|
|
|
365
373
|
"queueDepth": _int_or_none(queue_depth),
|
|
366
374
|
"kpis": None,
|
|
367
375
|
"inbox": inbox,
|
|
376
|
+
"diskUsedPct": disk_used_pct,
|
|
368
377
|
"idle": idle,
|
|
369
378
|
}
|
|
370
379
|
if config is not None:
|
|
@@ -519,6 +528,7 @@ class FleetVitalsPusher:
|
|
|
519
528
|
repos=self.config.repos,
|
|
520
529
|
now=self._clock(),
|
|
521
530
|
config=self._config(),
|
|
531
|
+
disk_used_pct=volume_used_pct(self.config.workspace_root),
|
|
522
532
|
))
|
|
523
533
|
|
|
524
534
|
def _config(self) -> "Mapping | None":
|
|
@@ -93,6 +93,12 @@ COMPARE_COMMITS_PER_PAGE = 1
|
|
|
93
93
|
# gone?" probe would answer yes on every large PR and abandon real verdicts.
|
|
94
94
|
PR_COMMIT_CAP = 250
|
|
95
95
|
|
|
96
|
+
# GitHub's OWN cap on the pull-request files endpoint: it "lists a maximum of
|
|
97
|
+
# 3000 files" (30 pages of 100). Past it the listing is silently short, so a
|
|
98
|
+
# PR at the cap cannot prove that EVERY changed path is under some prefix --
|
|
99
|
+
# which is the one question `pull_request_files` is asked (issue #171).
|
|
100
|
+
PR_FILES_CAP = 3000
|
|
101
|
+
|
|
96
102
|
# Environment variables `gh` reads a token from. Both are cleared before an
|
|
97
103
|
# explicitly-routed call, so an inherited implementer credential cannot win by
|
|
98
104
|
# being the one variable we did not think to overwrite.
|
|
@@ -783,6 +789,58 @@ class GitHub:
|
|
|
783
789
|
return out
|
|
784
790
|
page += 1
|
|
785
791
|
|
|
792
|
+
def pull_request_files(self, owner: str, repo: str, number: int) -> list[str]:
|
|
793
|
+
"""Every path the PR changes, paged to the end.
|
|
794
|
+
|
|
795
|
+
Read on ONE path: the discovery skip (issue #171), which asks whether
|
|
796
|
+
EVERY changed path is under `knowledge-base/`. That is an absence
|
|
797
|
+
question again -- "nothing outside the prefix" -- so a listing at
|
|
798
|
+
GitHub's PR_FILES_CAP is REFUSED (`TruncatedListing`) rather than
|
|
799
|
+
answered short, and the caller reviews the PR instead of skipping it.
|
|
800
|
+
|
|
801
|
+
A RENAME contributes BOTH names, for the reason `compare_files` gives:
|
|
802
|
+
`src/x.py` moved to `knowledge-base/x.py` changed a path outside the
|
|
803
|
+
prefix, and reading only the destination would make it look confined.
|
|
804
|
+
"""
|
|
805
|
+
out: list[str] = []
|
|
806
|
+
seen: set[str] = set()
|
|
807
|
+
listed = 0
|
|
808
|
+
page = 1
|
|
809
|
+
while True:
|
|
810
|
+
data = (
|
|
811
|
+
self._api(
|
|
812
|
+
"-X",
|
|
813
|
+
"GET",
|
|
814
|
+
f"repos/{owner}/{repo}/pulls/{number}/files",
|
|
815
|
+
"-f",
|
|
816
|
+
f"per_page={PER_PAGE}",
|
|
817
|
+
"-f",
|
|
818
|
+
f"page={page}",
|
|
819
|
+
)
|
|
820
|
+
or []
|
|
821
|
+
)
|
|
822
|
+
listed += len(data)
|
|
823
|
+
for entry in data:
|
|
824
|
+
for key in ("filename", "previous_filename"):
|
|
825
|
+
name = str(entry.get(key) or "")
|
|
826
|
+
if name and name not in seen:
|
|
827
|
+
seen.add(name)
|
|
828
|
+
out.append(name)
|
|
829
|
+
if listed >= PR_FILES_CAP:
|
|
830
|
+
log.warning(
|
|
831
|
+
"%s/%s#%d reached GitHub's %d-file listing cap — the file "
|
|
832
|
+
"list is not complete, so 'nothing outside the prefix' "
|
|
833
|
+
"cannot be read from it",
|
|
834
|
+
owner, repo, number, PR_FILES_CAP,
|
|
835
|
+
)
|
|
836
|
+
raise TruncatedListing(
|
|
837
|
+
f"{owner}/{repo}#{number} reached GitHub's {PR_FILES_CAP}-file "
|
|
838
|
+
f"listing cap; absence cannot be proven from it"
|
|
839
|
+
)
|
|
840
|
+
if len(data) < PER_PAGE:
|
|
841
|
+
return out
|
|
842
|
+
page += 1
|
|
843
|
+
|
|
786
844
|
# -- CI check rollup ---------------------------------------------------
|
|
787
845
|
|
|
788
846
|
def check_rollup(self, owner: str, repo: str, sha: str) -> CheckRollup:
|