alissa-tools-github-orcloop 0.1.0__tar.gz → 0.3.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_orcloop-0.1.0/src/main/alissa_tools_github_orcloop.egg-info → alissa_tools_github_orcloop-0.3.0}/PKG-INFO +10 -7
- {alissa_tools_github_orcloop-0.1.0 → alissa_tools_github_orcloop-0.3.0}/README.md +10 -7
- {alissa_tools_github_orcloop-0.1.0 → alissa_tools_github_orcloop-0.3.0}/src/main/alissa/tools/github/orcloop/__init__.py +3 -3
- alissa_tools_github_orcloop-0.3.0/src/main/alissa/tools/github/orcloop/__main__.py +269 -0
- alissa_tools_github_orcloop-0.3.0/src/main/alissa/tools/github/orcloop/alissa_client.py +558 -0
- alissa_tools_github_orcloop-0.3.0/src/main/alissa/tools/github/orcloop/config.py +464 -0
- alissa_tools_github_orcloop-0.3.0/src/main/alissa/tools/github/orcloop/ghclient.py +372 -0
- alissa_tools_github_orcloop-0.3.0/src/main/alissa/tools/github/orcloop/loop.py +1605 -0
- alissa_tools_github_orcloop-0.3.0/src/main/alissa/tools/github/orcloop/markers.py +122 -0
- alissa_tools_github_orcloop-0.3.0/src/main/alissa/tools/github/orcloop/proc.py +62 -0
- alissa_tools_github_orcloop-0.3.0/src/main/alissa/tools/github/orcloop/state.py +200 -0
- alissa_tools_github_orcloop-0.3.0/src/main/alissa/tools/github/orcloop/version +1 -0
- {alissa_tools_github_orcloop-0.1.0 → alissa_tools_github_orcloop-0.3.0/src/main/alissa_tools_github_orcloop.egg-info}/PKG-INFO +10 -7
- {alissa_tools_github_orcloop-0.1.0 → alissa_tools_github_orcloop-0.3.0}/src/main/alissa_tools_github_orcloop.egg-info/SOURCES.txt +7 -0
- alissa_tools_github_orcloop-0.1.0/src/main/alissa/tools/github/orcloop/__main__.py +0 -45
- alissa_tools_github_orcloop-0.1.0/src/main/alissa/tools/github/orcloop/version +0 -1
- {alissa_tools_github_orcloop-0.1.0 → alissa_tools_github_orcloop-0.3.0}/MANIFEST.in +0 -0
- {alissa_tools_github_orcloop-0.1.0 → alissa_tools_github_orcloop-0.3.0}/requirements.txt +0 -0
- {alissa_tools_github_orcloop-0.1.0 → alissa_tools_github_orcloop-0.3.0}/setup.cfg +0 -0
- {alissa_tools_github_orcloop-0.1.0 → alissa_tools_github_orcloop-0.3.0}/setup.py +0 -0
- {alissa_tools_github_orcloop-0.1.0 → alissa_tools_github_orcloop-0.3.0}/src/main/alissa/tools/github/orcloop/version.py +0 -0
- {alissa_tools_github_orcloop-0.1.0 → alissa_tools_github_orcloop-0.3.0}/src/main/alissa_tools_github_orcloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_orcloop-0.1.0 → alissa_tools_github_orcloop-0.3.0}/src/main/alissa_tools_github_orcloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_orcloop-0.1.0 → alissa_tools_github_orcloop-0.3.0}/src/main/alissa_tools_github_orcloop.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alissa-tools-github-orcloop
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: ALISSA-TOOLS-GITHUB-ORCLOOP
|
|
5
5
|
Home-page: https://alissa.app
|
|
6
6
|
Author: Fahera
|
|
@@ -46,13 +46,16 @@ 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
|
-
## Status
|
|
49
|
+
## Status
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
The daemon is live: the config surface (O1), the Alissa/GitHub clients and the
|
|
52
|
+
release ledger (O2), and the decision loop — both the release and close edges
|
|
53
|
+
(O3) — are all in place, driven by the `alissa-orcloop` CLI. See the repo
|
|
54
|
+
[README](../README.md) for the end-to-end documentation: the pipeline role, the
|
|
55
|
+
`Autonomous-Dev` / `Autonomous-Scope` marker contract, both decision matrices,
|
|
56
|
+
the full `ALISSA_ORC_*` configuration surface, and the escalation catalog.
|
|
57
|
+
Packaging the daemon as a container image (O5) is the remaining lane. The
|
|
58
|
+
reserved runtime env prefix is `ALISSA_ORC_*`.
|
|
56
59
|
|
|
57
60
|
## Install
|
|
58
61
|
|
|
@@ -29,13 +29,16 @@ The rule: a distribution owns a leaf package and declares only that subtree
|
|
|
29
29
|
`__init__.py` at any namespace level would claim it for one distribution and
|
|
30
30
|
shadow the others.
|
|
31
31
|
|
|
32
|
-
## Status
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
## Status
|
|
33
|
+
|
|
34
|
+
The daemon is live: the config surface (O1), the Alissa/GitHub clients and the
|
|
35
|
+
release ledger (O2), and the decision loop — both the release and close edges
|
|
36
|
+
(O3) — are all in place, driven by the `alissa-orcloop` CLI. See the repo
|
|
37
|
+
[README](../README.md) for the end-to-end documentation: the pipeline role, the
|
|
38
|
+
`Autonomous-Dev` / `Autonomous-Scope` marker contract, both decision matrices,
|
|
39
|
+
the full `ALISSA_ORC_*` configuration surface, and the escalation catalog.
|
|
40
|
+
Packaging the daemon as a container image (O5) is the remaining lane. The
|
|
41
|
+
reserved runtime env prefix is `ALISSA_ORC_*`.
|
|
39
42
|
|
|
40
43
|
## Install
|
|
41
44
|
|
|
@@ -6,9 +6,9 @@ releases tasks marked ready as `alissa:develop` issues, and closes the loop when
|
|
|
6
6
|
the resulting PR merges. It is the mechanical driver that turns committed work in
|
|
7
7
|
Alissa into GitHub issues the develop/review daemons can pick up.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
clients, the decision loop, the Docker image) land across
|
|
9
|
+
O1 landed the config surface (`config.py`), the marker contract (`markers.py`),
|
|
10
|
+
and the real `alissa-orcloop` CLI. The remaining moving parts (Alissa/GitHub
|
|
11
|
+
clients, the decision loop, the Docker image) land across O2–O5; see the repo
|
|
12
12
|
README's lane map. The reserved runtime env prefix is `ALISSA_ORC_*`.
|
|
13
13
|
"""
|
|
14
14
|
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""CLI entry point: alissa-orcloop (or python -m alissa.tools.github.orcloop).
|
|
2
|
+
|
|
3
|
+
O1 wired the config surface and the argument parser; O3 wires the decision
|
|
4
|
+
loop behind it; O6 adds the release-feed selector (`--release-feed bow` and its
|
|
5
|
+
`--bow-prefix` / `--bow-owner` / `--scan-fallback-interval` knobs).
|
|
6
|
+
|
|
7
|
+
The CLI resolves and validates the full configuration
|
|
8
|
+
(four-layer precedence, fail-closed `repos` guard), reports the effective
|
|
9
|
+
settings, then runs the orchestrator: `--once` for a single sweep (pair it
|
|
10
|
+
with `--dry-run -v` to print every decision and its reasons without touching
|
|
11
|
+
GitHub or the task graph), otherwise `run_forever`. Config errors still surface
|
|
12
|
+
here, at startup, with a clean exit code.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import logging
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from .alissa_client import AlissaError
|
|
23
|
+
from .config import (
|
|
24
|
+
FEED_BOW,
|
|
25
|
+
RELEASE_FEEDS,
|
|
26
|
+
Config,
|
|
27
|
+
load_config_file,
|
|
28
|
+
resolve_config_path,
|
|
29
|
+
)
|
|
30
|
+
from .ghclient import RateLimited
|
|
31
|
+
from .loop import Orchestrator
|
|
32
|
+
from .proc import CommandError
|
|
33
|
+
from .version import version
|
|
34
|
+
|
|
35
|
+
log = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
39
|
+
p = argparse.ArgumentParser(
|
|
40
|
+
prog="alissa-orcloop",
|
|
41
|
+
description="Orchestrate the Alissa task graph into alissa:develop "
|
|
42
|
+
"issues and close the loop on merge — the orchestrator daemon of the "
|
|
43
|
+
"Alissa GitHub daemon family.",
|
|
44
|
+
epilog="Every setting below can also live in the config file; a CLI "
|
|
45
|
+
"flag wins over the file, and an ALISSA_ORC_* environment variable "
|
|
46
|
+
"wins over the flag. workspace_root is CLI-only, so one config can "
|
|
47
|
+
"drive several orchestrators over different workspaces.",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
p.add_argument(
|
|
51
|
+
"--version", action="version", version=f"%(prog)s {version.value}"
|
|
52
|
+
)
|
|
53
|
+
p.add_argument(
|
|
54
|
+
"--workspace-root",
|
|
55
|
+
type=Path,
|
|
56
|
+
default=None,
|
|
57
|
+
metavar="PATH",
|
|
58
|
+
help="the workspace this orchestrator runs against (default: current "
|
|
59
|
+
"directory); also where the ledger defaults",
|
|
60
|
+
)
|
|
61
|
+
p.add_argument(
|
|
62
|
+
"-c",
|
|
63
|
+
"--config-path",
|
|
64
|
+
"--config",
|
|
65
|
+
dest="config_path",
|
|
66
|
+
type=Path,
|
|
67
|
+
default=None,
|
|
68
|
+
metavar="PATH",
|
|
69
|
+
help="config file; without it, ./orcloop.config.json then "
|
|
70
|
+
"<workspace-root>/orcloop.config.json, else defaults only",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
mode = p.add_argument_group("mode")
|
|
74
|
+
mode.add_argument(
|
|
75
|
+
"--once", action="store_true", help="run a single pass and exit"
|
|
76
|
+
)
|
|
77
|
+
mode.add_argument("-v", "--verbose", action="store_true")
|
|
78
|
+
|
|
79
|
+
over = p.add_argument_group(
|
|
80
|
+
"config overrides (win over the config file; ALISSA_ORC_* wins over these)"
|
|
81
|
+
)
|
|
82
|
+
over.add_argument(
|
|
83
|
+
"--repo",
|
|
84
|
+
dest="repos",
|
|
85
|
+
action="append",
|
|
86
|
+
metavar="OWNER/REPO",
|
|
87
|
+
help="watch this repo; repeatable. When given, REPLACES the config "
|
|
88
|
+
"`repos` list. The allowlist is fail-closed: an empty allowlist is a "
|
|
89
|
+
"fatal startup error, not a no-op.",
|
|
90
|
+
)
|
|
91
|
+
over.add_argument("--label", metavar="LABEL", help="issue label released work carries")
|
|
92
|
+
over.add_argument("--poll-interval", type=int, metavar="SECONDS")
|
|
93
|
+
over.add_argument("--repo-parallelism", type=int, metavar="N", help="repos advanced per pass")
|
|
94
|
+
over.add_argument("--release-blocked-floor-days", type=int, metavar="DAYS")
|
|
95
|
+
over.add_argument("--marker-key", metavar="KEY", help="Autonomous-Dev trailer key")
|
|
96
|
+
over.add_argument("--scope-key", metavar="KEY", help="Autonomous-Scope trailer key")
|
|
97
|
+
over.add_argument("--alissa-endpoint", metavar="URL")
|
|
98
|
+
over.add_argument("--ledger-path", type=Path, metavar="PATH")
|
|
99
|
+
over.add_argument(
|
|
100
|
+
"--release-feed",
|
|
101
|
+
choices=RELEASE_FEEDS,
|
|
102
|
+
metavar="{" + ",".join(RELEASE_FEEDS) + "}",
|
|
103
|
+
help="where release candidates come from: 'scan' (the default) lists "
|
|
104
|
+
"every committed task and filters it on the release marker; 'bow' "
|
|
105
|
+
"reads the per-repo Bodies of Work named <bow-prefix><owner>/<repo>, "
|
|
106
|
+
"where attaching a task is the release trigger AND the repo declaration",
|
|
107
|
+
)
|
|
108
|
+
over.add_argument(
|
|
109
|
+
"--bow-prefix",
|
|
110
|
+
metavar="PREFIX",
|
|
111
|
+
help="title prefix that marks a Body of Work as a release feed "
|
|
112
|
+
"(default 'autodev: '); the rest of the title is the target owner/repo",
|
|
113
|
+
)
|
|
114
|
+
over.add_argument(
|
|
115
|
+
"--bow-owner",
|
|
116
|
+
dest="bow_owners",
|
|
117
|
+
action="append",
|
|
118
|
+
metavar="ACTOR_ID",
|
|
119
|
+
help="bow feed only: trust bodies of work OWNED by this Alissa actor "
|
|
120
|
+
"as release feeds; repeatable. When given, REPLACES the config "
|
|
121
|
+
"`bow_owners` list. Fail-closed like --repo: `--release-feed bow` with "
|
|
122
|
+
"an empty owner list is a fatal startup error, because being SHARED a "
|
|
123
|
+
"body of work is not a claim to drive this daemon. An actor id is the "
|
|
124
|
+
"`ownerActorId` on the container's row in GET /v1/bodies-of-work.",
|
|
125
|
+
)
|
|
126
|
+
over.add_argument(
|
|
127
|
+
"--scan-fallback-interval",
|
|
128
|
+
type=int,
|
|
129
|
+
metavar="SECONDS",
|
|
130
|
+
help="bow feed only: how often to ALSO run the full marker scan and "
|
|
131
|
+
"log every marked task the feed did not surface (default 3600; 0 "
|
|
132
|
+
"disables it). The sweep reports, it never releases.",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
close = over.add_mutually_exclusive_group()
|
|
136
|
+
close.add_argument(
|
|
137
|
+
"--close-loop",
|
|
138
|
+
dest="close_loop_enabled",
|
|
139
|
+
action="store_true",
|
|
140
|
+
default=None,
|
|
141
|
+
help="drive the origin task forward when a released PR merges (the default)",
|
|
142
|
+
)
|
|
143
|
+
close.add_argument(
|
|
144
|
+
"--no-close-loop",
|
|
145
|
+
dest="close_loop_enabled",
|
|
146
|
+
action="store_false",
|
|
147
|
+
help="release-only: never act on merges, even if the config enables it",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
dry = over.add_mutually_exclusive_group()
|
|
151
|
+
dry.add_argument(
|
|
152
|
+
"--dry-run",
|
|
153
|
+
dest="dry_run",
|
|
154
|
+
action="store_true",
|
|
155
|
+
default=None,
|
|
156
|
+
help="decide and log, but never release an issue or touch task/GitHub state",
|
|
157
|
+
)
|
|
158
|
+
dry.add_argument(
|
|
159
|
+
"--no-dry-run",
|
|
160
|
+
dest="dry_run",
|
|
161
|
+
action="store_false",
|
|
162
|
+
help="act for real even if the config sets dry_run",
|
|
163
|
+
)
|
|
164
|
+
return p
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def overrides_from(args: argparse.Namespace) -> dict:
|
|
168
|
+
"""CLI values, with None meaning 'not specified' so the config file shows
|
|
169
|
+
through. `repos` becomes a tuple so it matches the file form."""
|
|
170
|
+
return {
|
|
171
|
+
"repos": tuple(args.repos) if args.repos else None,
|
|
172
|
+
"label": args.label,
|
|
173
|
+
"poll_interval": args.poll_interval,
|
|
174
|
+
"repo_parallelism": args.repo_parallelism,
|
|
175
|
+
"release_blocked_floor_days": args.release_blocked_floor_days,
|
|
176
|
+
"marker_key": args.marker_key,
|
|
177
|
+
"scope_key": args.scope_key,
|
|
178
|
+
"alissa_endpoint": args.alissa_endpoint,
|
|
179
|
+
"ledger_path": args.ledger_path,
|
|
180
|
+
"close_loop_enabled": args.close_loop_enabled,
|
|
181
|
+
"dry_run": args.dry_run,
|
|
182
|
+
"release_feed": args.release_feed,
|
|
183
|
+
"bow_prefix": args.bow_prefix,
|
|
184
|
+
"bow_owners": tuple(args.bow_owners) if args.bow_owners else None,
|
|
185
|
+
"scan_fallback_interval": args.scan_fallback_interval,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def resolve_config(args: argparse.Namespace) -> Config:
|
|
190
|
+
workspace_root = args.workspace_root or Path.cwd()
|
|
191
|
+
path = resolve_config_path(args.config_path, workspace_root)
|
|
192
|
+
|
|
193
|
+
file_data = load_config_file(path) if path else {}
|
|
194
|
+
log.info("config: %s", path or "none found — defaults + CLI + env only")
|
|
195
|
+
|
|
196
|
+
return Config.build(workspace_root, file_data, overrides_from(args))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def log_effective_config(config: Config) -> None:
|
|
200
|
+
"""The resolved settings, defaults and all. INFO carries the decisions an
|
|
201
|
+
operator needs; -v (DEBUG) shows the full surface."""
|
|
202
|
+
log.info("watching %s for label %r", ", ".join(config.repos), config.label)
|
|
203
|
+
log.info(
|
|
204
|
+
"poll every %ss; repo_parallelism=%s; dry_run=%s; close_loop=%s",
|
|
205
|
+
config.poll_interval, config.repo_parallelism, config.dry_run,
|
|
206
|
+
config.close_loop_enabled,
|
|
207
|
+
)
|
|
208
|
+
if config.release_feed == FEED_BOW:
|
|
209
|
+
log.info(
|
|
210
|
+
"release feed: bow — candidates are the tasks attached to the "
|
|
211
|
+
"%r bodies of work OWNED by %s; scan fallback sweep %s",
|
|
212
|
+
config.bow_prefix,
|
|
213
|
+
", ".join(config.bow_owners),
|
|
214
|
+
f"every {config.scan_fallback_interval}s"
|
|
215
|
+
if config.scan_fallback_interval
|
|
216
|
+
else "DISABLED",
|
|
217
|
+
)
|
|
218
|
+
else:
|
|
219
|
+
log.info(
|
|
220
|
+
"release feed: scan — candidates are committed tasks carrying %r, "
|
|
221
|
+
"targeted by their %s trailer",
|
|
222
|
+
config.label, config.marker_key,
|
|
223
|
+
)
|
|
224
|
+
log.debug("marker_key: %s", config.marker_key)
|
|
225
|
+
log.debug("scope_key: %s", config.scope_key)
|
|
226
|
+
log.debug("release_blocked_floor_days: %s", config.release_blocked_floor_days)
|
|
227
|
+
log.debug("alissa_endpoint: %s", config.alissa_endpoint)
|
|
228
|
+
log.debug("ledger_db: %s", config.ledger_db)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def main(argv: "list[str] | None" = None) -> int:
|
|
232
|
+
args = build_parser().parse_args(argv)
|
|
233
|
+
|
|
234
|
+
logging.basicConfig(
|
|
235
|
+
level=logging.DEBUG if args.verbose else logging.INFO,
|
|
236
|
+
format="%(asctime)s %(levelname)-7s %(message)s",
|
|
237
|
+
datefmt="%H:%M:%S",
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
config = resolve_config(args)
|
|
242
|
+
log.info("workspace: %s", config.workspace_root)
|
|
243
|
+
log_effective_config(config)
|
|
244
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
245
|
+
print(f"config error: {exc}", file=sys.stderr)
|
|
246
|
+
return 2
|
|
247
|
+
except KeyboardInterrupt:
|
|
248
|
+
return 0
|
|
249
|
+
|
|
250
|
+
orchestrator = Orchestrator(config)
|
|
251
|
+
try:
|
|
252
|
+
if args.once:
|
|
253
|
+
orchestrator.poll_once()
|
|
254
|
+
else:
|
|
255
|
+
log.info("starting orchestrator; polling every %ss", config.poll_interval)
|
|
256
|
+
orchestrator.run_forever()
|
|
257
|
+
except KeyboardInterrupt:
|
|
258
|
+
return 0
|
|
259
|
+
except (AlissaError, CommandError, RateLimited) as exc:
|
|
260
|
+
# A single --once pass surfaces a transport/adapter failure as a non-zero
|
|
261
|
+
# exit for the operator; run_forever swallows and backs off instead of
|
|
262
|
+
# exiting, so this only fires under --once.
|
|
263
|
+
print(f"orchestrator error: {exc}", file=sys.stderr)
|
|
264
|
+
return 1
|
|
265
|
+
return 0
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
if __name__ == "__main__":
|
|
269
|
+
raise SystemExit(main())
|