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.
Files changed (24) hide show
  1. {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
  2. {alissa_tools_github_orcloop-0.1.0 → alissa_tools_github_orcloop-0.3.0}/README.md +10 -7
  3. {alissa_tools_github_orcloop-0.1.0 → alissa_tools_github_orcloop-0.3.0}/src/main/alissa/tools/github/orcloop/__init__.py +3 -3
  4. alissa_tools_github_orcloop-0.3.0/src/main/alissa/tools/github/orcloop/__main__.py +269 -0
  5. alissa_tools_github_orcloop-0.3.0/src/main/alissa/tools/github/orcloop/alissa_client.py +558 -0
  6. alissa_tools_github_orcloop-0.3.0/src/main/alissa/tools/github/orcloop/config.py +464 -0
  7. alissa_tools_github_orcloop-0.3.0/src/main/alissa/tools/github/orcloop/ghclient.py +372 -0
  8. alissa_tools_github_orcloop-0.3.0/src/main/alissa/tools/github/orcloop/loop.py +1605 -0
  9. alissa_tools_github_orcloop-0.3.0/src/main/alissa/tools/github/orcloop/markers.py +122 -0
  10. alissa_tools_github_orcloop-0.3.0/src/main/alissa/tools/github/orcloop/proc.py +62 -0
  11. alissa_tools_github_orcloop-0.3.0/src/main/alissa/tools/github/orcloop/state.py +200 -0
  12. alissa_tools_github_orcloop-0.3.0/src/main/alissa/tools/github/orcloop/version +1 -0
  13. {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
  14. {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
  15. alissa_tools_github_orcloop-0.1.0/src/main/alissa/tools/github/orcloop/__main__.py +0 -45
  16. alissa_tools_github_orcloop-0.1.0/src/main/alissa/tools/github/orcloop/version +0 -1
  17. {alissa_tools_github_orcloop-0.1.0 → alissa_tools_github_orcloop-0.3.0}/MANIFEST.in +0 -0
  18. {alissa_tools_github_orcloop-0.1.0 → alissa_tools_github_orcloop-0.3.0}/requirements.txt +0 -0
  19. {alissa_tools_github_orcloop-0.1.0 → alissa_tools_github_orcloop-0.3.0}/setup.cfg +0 -0
  20. {alissa_tools_github_orcloop-0.1.0 → alissa_tools_github_orcloop-0.3.0}/setup.py +0 -0
  21. {alissa_tools_github_orcloop-0.1.0 → alissa_tools_github_orcloop-0.3.0}/src/main/alissa/tools/github/orcloop/version.py +0 -0
  22. {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
  23. {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
  24. {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.1.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 — O0 scaffold
49
+ ## Status
50
50
 
51
- This is the **O0 scaffold** only: the distribution skeleton, repo tooling, and
52
- a CLI stub (`alissa-orcloop --version`). No daemon logic yetconfig surface,
53
- Alissa/GitHub clients, the decision loop, and the Docker image land across
54
- O1–O5 (see the repo [README](../README.md) lane map). The reserved runtime env
55
- prefix is `ALISSA_ORC_*`.
51
+ The daemon is live: the config surface (O1), the Alissa/GitHub clients and the
52
+ release ledger (O2), and the decision loopboth 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 — O0 scaffold
33
-
34
- This is the **O0 scaffold** only: the distribution skeleton, repo tooling, and
35
- a CLI stub (`alissa-orcloop --version`). No daemon logic yetconfig surface,
36
- Alissa/GitHub clients, the decision loop, and the Docker image land across
37
- O1–O5 (see the repo [README](../README.md) lane map). The reserved runtime env
38
- prefix is `ALISSA_ORC_*`.
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 loopboth 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
- This is the **O0 scaffold**: the distribution skeleton, repo tooling, and CLI
10
- stub only no daemon logic. The moving parts (config surface, Alissa/GitHub
11
- clients, the decision loop, the Docker image) land across O1–O5; see the repo
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())