alissa-tools-github-devloop 0.8.27__tar.gz → 0.8.29__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 (38) hide show
  1. {alissa_tools_github_devloop-0.8.27/src/main/alissa_tools_github_devloop.egg-info → alissa_tools_github_devloop-0.8.29}/PKG-INFO +1 -1
  2. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa/tools/github/devloop/config.py +113 -3
  3. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa/tools/github/devloop/loop.py +64 -5
  4. alissa_tools_github_devloop-0.8.29/src/main/alissa/tools/github/devloop/version +1 -0
  5. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29/src/main/alissa_tools_github_devloop.egg-info}/PKG-INFO +1 -1
  6. alissa_tools_github_devloop-0.8.27/src/main/alissa/tools/github/devloop/version +0 -1
  7. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/LICENSE +0 -0
  8. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/MANIFEST.in +0 -0
  9. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/NOTICE +0 -0
  10. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/README.md +0 -0
  11. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/requirements.txt +0 -0
  12. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/setup.cfg +0 -0
  13. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/setup.py +0 -0
  14. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa/tools/github/devloop/__init__.py +0 -0
  15. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa/tools/github/devloop/__main__.py +0 -0
  16. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa/tools/github/devloop/alissa.py +0 -0
  17. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa/tools/github/devloop/alissa_client.py +0 -0
  18. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa/tools/github/devloop/bows.py +0 -0
  19. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa/tools/github/devloop/fleet_vitals.py +0 -0
  20. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa/tools/github/devloop/ghclient.py +0 -0
  21. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa/tools/github/devloop/hubs.py +0 -0
  22. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa/tools/github/devloop/janitor.py +0 -0
  23. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa/tools/github/devloop/loop_events.py +0 -0
  24. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa/tools/github/devloop/proc.py +0 -0
  25. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa/tools/github/devloop/prompts.py +0 -0
  26. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa/tools/github/devloop/state.py +0 -0
  27. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa/tools/github/devloop/version.py +0 -0
  28. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa/tools/github/devloop/webui/__init__.py +0 -0
  29. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa/tools/github/devloop/webui/__main__.py +0 -0
  30. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa/tools/github/devloop/webui/auth.py +0 -0
  31. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa/tools/github/devloop/webui/page.py +0 -0
  32. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa/tools/github/devloop/webui/server.py +0 -0
  33. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa/tools/github/devloop/webui/sources.py +0 -0
  34. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa/tools/github/devloop/webui/sysinfo.py +0 -0
  35. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa_tools_github_devloop.egg-info/SOURCES.txt +0 -0
  36. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa_tools_github_devloop.egg-info/dependency_links.txt +0 -0
  37. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa_tools_github_devloop.egg-info/entry_points.txt +0 -0
  38. {alissa_tools_github_devloop-0.8.27 → alissa_tools_github_devloop-0.8.29}/src/main/alissa_tools_github_devloop.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-devloop
3
- Version: 0.8.27
3
+ Version: 0.8.29
4
4
  Summary: ALISSA-TOOLS-GITHUB-DEVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -26,6 +26,10 @@ through to the config rather than fail the daemon's boot on a mode of `''`.
26
26
  `fleet_vitals_enabled` through `ALISSA_DEV_FLEET_VITALS_ENABLED` likewise.
27
27
  `revert_label` rides `ALISSA_REVERT_LABEL` the same way (issue #131): a
28
28
  non-blank value wins over the CLI and the file, a blank one falls through.
29
+
30
+ `regenerate` (issue #138) is env-only: `ALISSA_DEV_REGENERATE` maps
31
+ repo-scoped globs to the command that regenerates them, and the daemon only
32
+ ever RENDERS that map into a worker's directive -- it never runs the command.
29
33
  """
30
34
 
31
35
  from __future__ import annotations
@@ -116,6 +120,94 @@ ENV_FLEET_VITALS = "ALISSA_DEV_FLEET_VITALS_ENABLED"
116
120
  # it (no `_DEV_` infix), because that is the name the operator runbook gives.
117
121
  ENV_REVERT_LABEL = "ALISSA_REVERT_LABEL"
118
122
 
123
+ # The regenerable-paths map (issue #138): `owner/repo:<glob>=<command>`
124
+ # entries, `|`-separated (the daemon-family list separator -- a command may
125
+ # itself carry `,`, `;` or spaces, so `|` is the ONLY separator here), default
126
+ # empty. Env-only, read straight from the environment like ENV_REVERT_LABEL:
127
+ # not a config key, not a build ARG, not written by the config generator. Each
128
+ # entry is validated at load and a malformed one is refused by NAME -- the
129
+ # variable's and the entry's -- because the failure mode of a silently
130
+ # dropped entry is a worker hand-merging a generated file a poll later. The
131
+ # command is rendered into the worker's directive and runs INSIDE the
132
+ # worker's worktree as part of the session; the daemon never executes it.
133
+ ENV_REGENERATE = "ALISSA_DEV_REGENERATE"
134
+
135
+ # `owner/repo` for a regenerate entry: exactly one `/`, no whitespace, and no
136
+ # `:` (the separator that ends it). Shape only, like the actor-id check.
137
+ _REGENERATE_REPO_RE = re.compile(r"^[^\s/:]+/[^\s/:]+\Z")
138
+
139
+
140
+ @dataclass(frozen=True)
141
+ class RegenerateRule:
142
+ """One `ALISSA_DEV_REGENERATE` entry: files under `glob` in `repo` are a
143
+ pure function of the code, regenerated by `command` (run in the worker's
144
+ worktree, never by the daemon). `repo` keeps the operator's casing;
145
+ matching casefolds, as `Config.watches` does."""
146
+
147
+ repo: str
148
+ glob: str
149
+ command: str
150
+
151
+
152
+ def parse_regenerate(raw: str) -> "tuple[RegenerateRule, ...]":
153
+ """Parse the ALISSA_DEV_REGENERATE value into rules, refusing malformed
154
+ entries by name.
155
+
156
+ The grammar is `owner/repo:<glob>=<command>` per entry, split on `|`
157
+ ONLY: the repo ends at the first `:`, the glob at the first `=`, and the
158
+ command is everything after it (so a command may carry `=` and `:`, and a
159
+ glob may carry `:` but not `=`). Whitespace around an entry and its three
160
+ parts is stripped; empty entries (a trailing `|`) are dropped, so a blank
161
+ or unset variable is the empty map -- today's behaviour exactly. An
162
+ entry with a missing part, a repo that is not `owner/repo`, or the same
163
+ repo+glob given twice with different commands is a ValueError naming
164
+ ENV_REGENERATE and the entry: a dropped entry would surface a poll later
165
+ as a hand-merged generated file, which is the very thing the map exists
166
+ to prevent. Exact duplicates collapse.
167
+ """
168
+ rules: "list[RegenerateRule]" = []
169
+ seen: "dict[tuple[str, str], RegenerateRule]" = {}
170
+ for part in (raw or "").split("|"):
171
+ entry = part.strip()
172
+ if not entry:
173
+ continue
174
+
175
+ def refuse(problem: str) -> ValueError:
176
+ return ValueError(
177
+ f"{ENV_REGENERATE} entry {entry!r} is malformed: {problem} "
178
+ f"(expected owner/repo:<glob>=<command>, entries separated "
179
+ f"by '|')"
180
+ )
181
+
182
+ repo, colon, rest = entry.partition(":")
183
+ if not colon:
184
+ raise refuse("missing ':' between the repo and the glob")
185
+ repo = repo.strip()
186
+ if not _REGENERATE_REPO_RE.match(repo):
187
+ raise refuse(f"repo {repo!r} is not of the form owner/repo")
188
+ glob, equals, command = rest.partition("=")
189
+ if not equals:
190
+ raise refuse("missing '=' between the glob and the command")
191
+ glob = glob.strip()
192
+ command = command.strip()
193
+ if not glob:
194
+ raise refuse("the glob is empty")
195
+ if not command:
196
+ raise refuse("the command is empty")
197
+ rule = RegenerateRule(repo=repo, glob=glob, command=command)
198
+ key = (repo.casefold(), glob)
199
+ prior = seen.get(key)
200
+ if prior is not None:
201
+ if prior.command != command:
202
+ raise refuse(
203
+ f"{repo}:{glob} is already mapped to {prior.command!r}"
204
+ )
205
+ continue
206
+ seen[key] = rule
207
+ rules.append(rule)
208
+ return tuple(rules)
209
+
210
+
119
211
  # The prompt responder's master switch (issue #127): `on` answers, `observe`
120
212
  # classifies and narrates but sends nothing, `off` never captures a pane.
121
213
  PROMPT_ON = "on"
@@ -744,6 +836,12 @@ class Config:
744
836
  # to act (at the incident's growth rate, ~2 days). 0 never warns.
745
837
  disk_warn_percent: int = 80
746
838
 
839
+ # The regenerable-paths map (issue #138), from ALISSA_DEV_REGENERATE only
840
+ # (no config key, no CLI flag). Empty by default, and empty means every
841
+ # worker directive renders byte-identical to a build without the map.
842
+ # Read through `regenerate_for(owner, repo)`, never executed here.
843
+ regenerate: tuple[RegenerateRule, ...] = ()
844
+
747
845
  dry_run: bool = False
748
846
 
749
847
  # Derived at build time: `repos` casefolded for matching. GitHub names are
@@ -875,6 +973,13 @@ class Config:
875
973
  and gets an answer that includes the enrolled repos."""
876
974
  return full_name.casefold() in self._repo_match
877
975
 
976
+ def regenerate_for(self, owner: str, repo: str) -> "tuple[RegenerateRule, ...]":
977
+ """The regenerate rules scoped to `owner/repo`, in the order the
978
+ operator wrote them; empty for a repo with no entry. Casefolded like
979
+ `watches`: GitHub names are case-insensitive."""
980
+ wanted = f"{owner}/{repo}".casefold()
981
+ return tuple(r for r in self.regenerate if r.repo.casefold() == wanted)
982
+
878
983
  def trusts_feed_owner(self, actor_id: "str | None") -> bool:
879
984
  """May a Body of Work owned by this actor act as a repo feed?
880
985
 
@@ -900,9 +1005,10 @@ class Config:
900
1005
  """Merge the layers and validate. `overrides` entries that are None mean
901
1006
  "not specified on the CLI" and fall through to the file / defaults.
902
1007
 
903
- `environ` is only read for `ALISSA_DEV_REPOS_SOURCE`, which rides one
904
- rail above the CLI (see the module docstring); it defaults to the real
905
- environment and is a parameter so a test never has to monkeypatch it."""
1008
+ `environ` is only read for the env rails (`ALISSA_DEV_REPOS_SOURCE`
1009
+ and its siblings, see the module docstring, plus the env-only
1010
+ `ALISSA_DEV_REGENERATE` map); it defaults to the real environment and
1011
+ is a parameter so a test never has to monkeypatch it."""
906
1012
  raw: dict[str, Any] = dict(file_data or {})
907
1013
 
908
1014
  if "workspace_root" in raw:
@@ -994,6 +1100,9 @@ class Config:
994
1100
  revert_label = env_revert_label
995
1101
  if not revert_label.strip():
996
1102
  raise ValueError("revert_label must not be blank")
1103
+ # The regenerable-paths map (issue #138): env-only, validated here so
1104
+ # a malformed entry fails the boot by name instead of a directive.
1105
+ regenerate = parse_regenerate(env_map.get(ENV_REGENERATE, ""))
997
1106
 
998
1107
  bow_owners = normalize_bow_owners(raw.get("bow_owners", cls.bow_owners))
999
1108
 
@@ -1284,6 +1393,7 @@ class Config:
1284
1393
  raw.get("cache_paths", cls.cache_paths), "cache_paths"
1285
1394
  ),
1286
1395
  disk_warn_percent=warn_percent,
1396
+ regenerate=regenerate,
1287
1397
  dry_run=bool(raw.get("dry_run", cls.dry_run)),
1288
1398
  )
1289
1399
 
@@ -458,7 +458,7 @@ from collections import Counter
458
458
  from dataclasses import dataclass, replace
459
459
  from enum import Enum
460
460
  from pathlib import Path
461
- from typing import Callable
461
+ from typing import Callable, Sequence
462
462
 
463
463
  from . import janitor
464
464
  from . import fleet_vitals as fleet_vitals_mod
@@ -486,6 +486,7 @@ from .config import (
486
486
  PROMPT_OFF,
487
487
  REPOS_BOWS,
488
488
  Config,
489
+ RegenerateRule,
489
490
  )
490
491
  from .ghclient import (
491
492
  FIRST_PARENT_MAX_PAGES,
@@ -771,6 +772,49 @@ _SHELL_RULE = (
771
772
  "change the command; do not retry it. "
772
773
  )
773
774
 
775
+ # The regenerate-on-conflict rule (issue #138), rendered into every worker
776
+ # directive that can end up merging main into its branch -- develop, resume,
777
+ # fix, fix-resume and maintain -- and ONLY for a repo with entries in
778
+ # ALISSA_DEV_REGENERATE: `render_regenerate_rule` returns "" otherwise, so a
779
+ # repo without entries (and a deployment without the variable) renders every
780
+ # directive byte-identical to before the rule existed. A generated file is a
781
+ # pure function of the code; a hand-merge of it usually works and is still
782
+ # the wrong operation (the repo's generated-drift CI job fails any hand-merge
783
+ # that does not match, at the cost of a review round), so the rule is: take
784
+ # either side, finish the merge, rerun the generator, commit. The command
785
+ # runs in the worker's worktree as part of the session -- the daemon never
786
+ # executes it, it only spells it here.
787
+ _REGENERATE_RULE_ONE = (
788
+ "If merging main conflicts inside `{glob}`, do not hand-merge it: "
789
+ "`git checkout --theirs -- <paths>` (or `--ours` — either side), finish "
790
+ "the merge, run `{command}`, and commit the regenerated files in the "
791
+ "same push; the repo's CI verifies them. "
792
+ )
793
+ _REGENERATE_RULE_MANY = (
794
+ "If merging main conflicts inside {paths}, do not hand-merge it: "
795
+ "`git checkout --theirs -- <paths>` (or `--ours` — either side), finish "
796
+ "the merge, run that path's regenerate command, and commit the "
797
+ "regenerated files in the same push; the repo's CI verifies them. "
798
+ )
799
+
800
+
801
+ def render_regenerate_rule(rules: "Sequence[RegenerateRule]") -> str:
802
+ """The rule for one repo's ALISSA_DEV_REGENERATE entries, or "" when it
803
+ has none. One entry renders the issue's own sentence with the glob and the
804
+ command substituted; several render one sentence naming each glob with
805
+ its command. Always ends in a space so it splices between two sentences
806
+ of a directive, and its placeholder renders to nothing when empty."""
807
+ if not rules:
808
+ return ""
809
+ if len(rules) == 1:
810
+ return _REGENERATE_RULE_ONE.format(
811
+ glob=rules[0].glob, command=rules[0].command
812
+ )
813
+ named = [f"`{r.glob}` (regenerate with `{r.command}`)" for r in rules]
814
+ paths = ", ".join(named[:-1]) + " or " + named[-1]
815
+ return _REGENERATE_RULE_MANY.format(paths=paths)
816
+
817
+
774
818
  DEV_DIRECTIVE = (
775
819
  "You are an IMPLEMENTER, not a reviewer. Implement GitHub issue "
776
820
  "{issue_url} (attempt {attempt} of {cap}) — the issue is your "
@@ -786,7 +830,8 @@ DEV_DIRECTIVE = (
786
830
  + _BRANCH_RULE
787
831
  + _SHELL_RULE
788
832
  + "Implement to the issue's acceptance criteria and VERIFY locally (run the "
789
- "repo's test/style/type scripts) before publishing. Open a DRAFT PR whose "
833
+ "repo's test/style/type scripts) before publishing. {regenerate_rule}"
834
+ "Open a DRAFT PR whose "
790
835
  "body contains `Closes #{number}` and BOTH task refs — the origin task "
791
836
  "AND your implementation task: the PR body is the canonical cross-actor "
792
837
  "record, and each actor's tasks live in its own partition — and attach "
@@ -845,7 +890,8 @@ RESUME_DIRECTIVE = (
845
890
  "are unchanged and still authoritative — and VERIFY locally (run the "
846
891
  "repo's test/style/type scripts) before publishing. NEVER force-push: "
847
892
  "history stays append-only, or the PR's review threads lose their "
848
- "anchors. Keep the PR body carrying `Closes #{number}` and BOTH task refs "
893
+ "anchors. {regenerate_rule}"
894
+ "Keep the PR body carrying `Closes #{number}` and BOTH task refs "
849
895
  "(the origin task AND your implementation task) — restoring them if the "
850
896
  "dead session did not. "
851
897
  + _SHELL_RULE
@@ -906,7 +952,7 @@ FIX_DIRECTIVE = (
906
952
  + "FIRST verify the triage of every prior review finding per CR8: exactly "
907
953
  "one reasoned `[triage:*]` reply on each finding's thread — post any that "
908
954
  "are missing. Then fix the pursued findings on the same branch and VERIFY "
909
- "locally (run the repo's test/style/type scripts). "
955
+ "locally (run the repo's test/style/type scripts). {regenerate_rule}"
910
956
  + _NEWER_ROUNDS
911
957
  + _FIX_DELIVERY_NOTE
912
958
  + " "
@@ -964,7 +1010,7 @@ FIX_RESUME_DIRECTIVE = (
964
1010
  "thread it already answered. Then fix the pursued findings, completing "
965
1011
  "the ORIGINAL acceptance criteria the issue set, and VERIFY locally (run "
966
1012
  "the repo's test/style/type scripts). NEVER force-push: history stays "
967
- "append-only, or the review threads lose their anchors. "
1013
+ "append-only, or the review threads lose their anchors. {regenerate_rule}"
968
1014
  + _NEWER_ROUNDS
969
1015
  + _FIX_DELIVERY_NOTE
970
1016
  + " "
@@ -1015,6 +1061,7 @@ MAINTAIN_DIRECTIVE = (
1015
1061
  "while history is append-only — resolving any conflicts so BOTH sides' "
1016
1062
  "intent is preserved, then run the repo's four scripts (unit tests, "
1017
1063
  "coverage, style, types) and get ALL of them green before publishing. "
1064
+ "{regenerate_rule}"
1018
1065
  + _REVIEW_SEAT
1019
1066
  + "The terminal sequence's ORDER is load-bearing: push the merge commit, "
1020
1067
  "THEN re-request review from {reviewers} — the push stales any approve, "
@@ -3680,6 +3727,7 @@ class DevWatcher:
3680
3727
  "none resolved — follow the alissa-pr-review skill's reviewer "
3681
3728
  "resolution"
3682
3729
  )
3730
+ regenerate_rule = self._regenerate_rule(issue.owner, issue.repo)
3683
3731
  if resume_pr is not None:
3684
3732
  directive = RESUME_DIRECTIVE.format(
3685
3733
  issue_url=issue.url,
@@ -3693,6 +3741,7 @@ class DevWatcher:
3693
3741
  head_ref=resume_pr.head_ref,
3694
3742
  worktree_dir=worktree_dir(resume_pr.head_ref),
3695
3743
  dead_session=dead_session,
3744
+ regenerate_rule=regenerate_rule,
3696
3745
  )
3697
3746
  else:
3698
3747
  directive = DEV_DIRECTIVE.format(
@@ -3703,6 +3752,7 @@ class DevWatcher:
3703
3752
  task_instruction=task_instruction,
3704
3753
  reviewers=reviewers,
3705
3754
  session=name,
3755
+ regenerate_rule=regenerate_rule,
3706
3756
  )
3707
3757
 
3708
3758
  # assign_self FIRST: the assignment is the server-side in-flight
@@ -6802,6 +6852,7 @@ class DevWatcher:
6802
6852
  reviewer=review.author,
6803
6853
  session=name,
6804
6854
  dead_session=dead_session,
6855
+ regenerate_rule=self._regenerate_rule(pr.owner, pr.repo),
6805
6856
  )
6806
6857
 
6807
6858
  # BEFORE the enqueue (issue #53), for the reason _spawn spells out:
@@ -7065,6 +7116,7 @@ class DevWatcher:
7065
7116
  worktree_dir=worktree_dir(pr.head_ref),
7066
7117
  reviewers=reviewers,
7067
7118
  session=name,
7119
+ regenerate_rule=self._regenerate_rule(pr.owner, pr.repo),
7068
7120
  )
7069
7121
 
7070
7122
  # BEFORE the enqueue (issue #53), for the reason _spawn spells out:
@@ -7685,6 +7737,13 @@ class DevWatcher:
7685
7737
  if self._append_activity(owner, repo, number, line, display_slug):
7686
7738
  self.state.record_activity(slug_key, number, kind)
7687
7739
 
7740
+ def _regenerate_rule(self, owner: str, repo: str) -> str:
7741
+ """The regenerate-on-conflict clause for this repo's directives
7742
+ (issue #138): "" -- and so a byte-identical directive -- unless
7743
+ ALISSA_DEV_REGENERATE names `owner/repo`. Shared by all five worker
7744
+ directives. Rendered only: the command is the worker's to run."""
7745
+ return render_regenerate_rule(self.config.regenerate_for(owner, repo))
7746
+
7688
7747
  def _ensure_hub(self, owner: str, repo: str) -> tuple[Path, str | None]:
7689
7748
  """Resolve a spawn's cwd (the hub ROOT), hub-ifying the repo first if
7690
7749
  configured. Shared by all three edges (issue, fix, and maintenance
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-devloop
3
- Version: 0.8.27
3
+ Version: 0.8.29
4
4
  Summary: ALISSA-TOOLS-GITHUB-DEVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera