agcoord 0.1.0__py3-none-any.whl
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.
- agcoord/__init__.py +19 -0
- agcoord/__main__.py +6 -0
- agcoord/cli.py +244 -0
- agcoord/frame.py +35 -0
- agcoord/github.py +847 -0
- agcoord/land.py +276 -0
- agcoord/merge.py +31 -0
- agcoord/py.typed +1 -0
- agcoord/queue.py +2740 -0
- agcoord/tui.py +636 -0
- agcoord-0.1.0.dist-info/METADATA +144 -0
- agcoord-0.1.0.dist-info/RECORD +16 -0
- agcoord-0.1.0.dist-info/WHEEL +5 -0
- agcoord-0.1.0.dist-info/entry_points.txt +2 -0
- agcoord-0.1.0.dist-info/licenses/LICENSE +21 -0
- agcoord-0.1.0.dist-info/top_level.txt +1 -0
agcoord/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Machine-local coordination for development agents and repository gates."""
|
|
2
|
+
|
|
3
|
+
from .queue import (
|
|
4
|
+
CoordinatorBroker,
|
|
5
|
+
CoordinatorClient,
|
|
6
|
+
CoordinatorError,
|
|
7
|
+
RepositoryIdentity,
|
|
8
|
+
discover_repository,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"CoordinatorBroker",
|
|
13
|
+
"CoordinatorClient",
|
|
14
|
+
"CoordinatorError",
|
|
15
|
+
"RepositoryIdentity",
|
|
16
|
+
"discover_repository",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
agcoord/__main__.py
ADDED
agcoord/cli.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""AGCoord: machine-local coordination for development agents and repositories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from typing import Iterable, TextIO
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .queue import (
|
|
14
|
+
CoordinatorClient,
|
|
15
|
+
CoordinatorError,
|
|
16
|
+
follow,
|
|
17
|
+
migrate_queue,
|
|
18
|
+
parse_resource_claims,
|
|
19
|
+
wait,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _resources(values: list[str]) -> dict[str, int]:
|
|
24
|
+
return parse_resource_claims(values)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _table(rows: list[dict]) -> str:
|
|
28
|
+
if not rows:
|
|
29
|
+
return "(nothing)"
|
|
30
|
+
display = [
|
|
31
|
+
{
|
|
32
|
+
"status": row["status"],
|
|
33
|
+
"kind": row["kind"],
|
|
34
|
+
"run": row["run_id"],
|
|
35
|
+
"repository": row["repository"],
|
|
36
|
+
"agent": row["agent"],
|
|
37
|
+
"label": row["label"],
|
|
38
|
+
"resources": ",".join(
|
|
39
|
+
f"{name}={units}" for name, units in row["resources"].items()
|
|
40
|
+
),
|
|
41
|
+
}
|
|
42
|
+
for row in rows
|
|
43
|
+
]
|
|
44
|
+
columns = ["status", "kind", "run", "repository", "agent", "label", "resources"]
|
|
45
|
+
widths = [max(len(name), *(len(str(row[name])) for row in display)) for name in columns]
|
|
46
|
+
lines = [
|
|
47
|
+
" ".join(name.ljust(width) for name, width in zip(columns, widths)),
|
|
48
|
+
" ".join("-" * width for width in widths),
|
|
49
|
+
]
|
|
50
|
+
lines.extend(
|
|
51
|
+
" ".join(str(row[name]).ljust(width) for name, width in zip(columns, widths))
|
|
52
|
+
for row in display
|
|
53
|
+
)
|
|
54
|
+
return "\n".join(lines)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
58
|
+
parser = argparse.ArgumentParser(prog="agcoord", description=__doc__)
|
|
59
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
60
|
+
parser.add_argument("--json", action="store_true", help="emit strict JSON")
|
|
61
|
+
parser.add_argument("--state-dir", help="override the user-scoped machine spool")
|
|
62
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
63
|
+
|
|
64
|
+
def state(command: argparse.ArgumentParser) -> argparse.ArgumentParser:
|
|
65
|
+
command.add_argument(
|
|
66
|
+
"--state-dir",
|
|
67
|
+
default=argparse.SUPPRESS,
|
|
68
|
+
help="override the user-scoped machine spool",
|
|
69
|
+
)
|
|
70
|
+
command.add_argument(
|
|
71
|
+
"--json",
|
|
72
|
+
action="store_true",
|
|
73
|
+
default=argparse.SUPPRESS,
|
|
74
|
+
help="emit strict JSON",
|
|
75
|
+
)
|
|
76
|
+
return command
|
|
77
|
+
|
|
78
|
+
state(commands.add_parser("list", help="show active, queued, and recent runs"))
|
|
79
|
+
show = state(commands.add_parser("show", help="show one run"))
|
|
80
|
+
show.add_argument("run_id")
|
|
81
|
+
log = state(commands.add_parser("log", help="print one run's log"))
|
|
82
|
+
log.add_argument("run_id")
|
|
83
|
+
log.add_argument("--follow", action="store_true", help="wait through terminal status")
|
|
84
|
+
cancel = state(commands.add_parser("cancel", help="cancel one live run"))
|
|
85
|
+
cancel.add_argument("run_id")
|
|
86
|
+
state(commands.add_parser("clear", help="clear terminal history and logs while idle"))
|
|
87
|
+
state(commands.add_parser("tui", help="open the machine queue terminal view"))
|
|
88
|
+
state(commands.add_parser("migrate", help="explicitly migrate an idle spool"))
|
|
89
|
+
|
|
90
|
+
def submission(name: str, help_text: str) -> argparse.ArgumentParser:
|
|
91
|
+
command = state(commands.add_parser(name, help=help_text))
|
|
92
|
+
command.add_argument("--label", default=name, help="short queue label")
|
|
93
|
+
command.add_argument("--checkout", default=".", help="command working tree")
|
|
94
|
+
command.add_argument("--repository", help="explicit stable repository identity")
|
|
95
|
+
command.add_argument("--agent", help="agent identity (default: AGCOORD_AGENT or PID)")
|
|
96
|
+
command.add_argument(
|
|
97
|
+
"--resource",
|
|
98
|
+
action="append",
|
|
99
|
+
default=[],
|
|
100
|
+
metavar="NAME=UNITS",
|
|
101
|
+
help="repeatable machine resource claim",
|
|
102
|
+
)
|
|
103
|
+
command.add_argument("worker_command", nargs=argparse.REMAINDER)
|
|
104
|
+
return command
|
|
105
|
+
|
|
106
|
+
submission("run", "submit a compatible check")
|
|
107
|
+
submission("full", "submit a clean exact-head repository barrier")
|
|
108
|
+
|
|
109
|
+
land = state(commands.add_parser(
|
|
110
|
+
"land",
|
|
111
|
+
help="gate and publish one fresh exact head without releasing its barrier",
|
|
112
|
+
))
|
|
113
|
+
land.add_argument("request", type=int, help="adapter request (GitHub PR number)")
|
|
114
|
+
land.add_argument("--adapter", default="github", help="publication adapter")
|
|
115
|
+
land.add_argument("--label", default="land", help="short queue label")
|
|
116
|
+
land.add_argument("--checkout", default=".", help="ticket worktree")
|
|
117
|
+
land.add_argument("--repository", help="explicit stable repository identity")
|
|
118
|
+
land.add_argument("--agent", help="agent identity")
|
|
119
|
+
land.add_argument("--resource", action="append", default=[], metavar="NAME=UNITS")
|
|
120
|
+
land.add_argument("worker_command", nargs="+")
|
|
121
|
+
return parser
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _client(args: argparse.Namespace, checkout: Path) -> CoordinatorClient:
|
|
125
|
+
return CoordinatorClient(
|
|
126
|
+
state_dir=getattr(args, "state_dir", None),
|
|
127
|
+
checkout=checkout,
|
|
128
|
+
autostart=True,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def run(args: argparse.Namespace, *, out: TextIO = sys.stdout) -> int:
|
|
133
|
+
checkout = Path(getattr(args, "checkout", ".")).expanduser().resolve()
|
|
134
|
+
emit = (
|
|
135
|
+
(lambda value: print(json.dumps(value, indent=2, sort_keys=True), file=out))
|
|
136
|
+
if args.json
|
|
137
|
+
else None
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if args.command == "migrate":
|
|
141
|
+
result = migrate_queue(state_dir=args.state_dir)
|
|
142
|
+
if emit:
|
|
143
|
+
emit(result)
|
|
144
|
+
elif result["changed"]:
|
|
145
|
+
print(
|
|
146
|
+
f"AGCoord: migrated protocol {result['from_protocol']} "
|
|
147
|
+
f"to {result['to_protocol']}",
|
|
148
|
+
file=out,
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
print(f"AGCoord: protocol {result['to_protocol']} already current", file=out)
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
def client_factory() -> CoordinatorClient:
|
|
155
|
+
return _client(args, checkout)
|
|
156
|
+
|
|
157
|
+
if args.command == "tui":
|
|
158
|
+
from .tui import run as run_tui
|
|
159
|
+
|
|
160
|
+
return run_tui(client_factory)
|
|
161
|
+
|
|
162
|
+
client = client_factory()
|
|
163
|
+
if args.command == "list":
|
|
164
|
+
snapshot = client.snapshot()
|
|
165
|
+
if emit:
|
|
166
|
+
emit(snapshot)
|
|
167
|
+
else:
|
|
168
|
+
print(_table([*snapshot["active"], *snapshot["queued"], *snapshot["recent"]]), file=out)
|
|
169
|
+
return 0
|
|
170
|
+
if args.command == "show":
|
|
171
|
+
row = client.status(args.run_id)
|
|
172
|
+
print(json.dumps(row, indent=2, sort_keys=True), file=out)
|
|
173
|
+
return 0
|
|
174
|
+
if args.command == "cancel":
|
|
175
|
+
row = client.cancel(args.run_id)
|
|
176
|
+
emit(row) if emit else print(
|
|
177
|
+
f"{row['run_id']}: {row['status']}"
|
|
178
|
+
+ (" · cancellation requested" if row["cancel_requested"] else ""),
|
|
179
|
+
file=out,
|
|
180
|
+
)
|
|
181
|
+
return 0
|
|
182
|
+
if args.command == "clear":
|
|
183
|
+
result = client.clear()
|
|
184
|
+
emit(result) if emit else print(f"AGCoord: cleared {result['cleared']} run(s)", file=out)
|
|
185
|
+
return 0
|
|
186
|
+
if args.command == "log":
|
|
187
|
+
offset = 0
|
|
188
|
+
while True:
|
|
189
|
+
page = client.log(args.run_id, offset=offset)
|
|
190
|
+
if emit:
|
|
191
|
+
emit(page)
|
|
192
|
+
return 0
|
|
193
|
+
print(page["text"], end="", file=out)
|
|
194
|
+
offset = page["next_offset"]
|
|
195
|
+
row = client.status(args.run_id)
|
|
196
|
+
if page["eof"] and (not args.follow or row["status"] not in {"queued", "running"}):
|
|
197
|
+
return 0
|
|
198
|
+
time.sleep(0.1)
|
|
199
|
+
|
|
200
|
+
if not checkout.is_dir():
|
|
201
|
+
raise CoordinatorError(f"checkout does not exist: {checkout}")
|
|
202
|
+
claims = _resources(args.resource)
|
|
203
|
+
command = list(args.worker_command)
|
|
204
|
+
if command and command[0] == "--":
|
|
205
|
+
command.pop(0)
|
|
206
|
+
if not command:
|
|
207
|
+
raise CoordinatorError(f"agcoord {args.command} needs a command after --")
|
|
208
|
+
if args.command == "land":
|
|
209
|
+
run_id = client.submit_land(
|
|
210
|
+
args.adapter,
|
|
211
|
+
args.request,
|
|
212
|
+
command,
|
|
213
|
+
checkout=str(checkout),
|
|
214
|
+
label=args.label,
|
|
215
|
+
resources=claims,
|
|
216
|
+
agent=args.agent,
|
|
217
|
+
repository=args.repository,
|
|
218
|
+
)
|
|
219
|
+
else:
|
|
220
|
+
run_id = client.submit(
|
|
221
|
+
command,
|
|
222
|
+
checkout=str(checkout),
|
|
223
|
+
kind="full" if args.command == "full" else "check",
|
|
224
|
+
label=args.label,
|
|
225
|
+
resources=claims,
|
|
226
|
+
agent=args.agent,
|
|
227
|
+
repository=args.repository,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if emit:
|
|
231
|
+
final = wait(client, run_id)
|
|
232
|
+
emit(final)
|
|
233
|
+
return int(final["exit_status"] if final["exit_status"] is not None else 70)
|
|
234
|
+
print(f"AGCoord: accepted {run_id}", file=out, flush=True)
|
|
235
|
+
return follow(client, run_id, out=out)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def main(argv: Iterable[str] | None = None) -> int:
|
|
239
|
+
args = build_parser().parse_args(argv)
|
|
240
|
+
try:
|
|
241
|
+
return run(args)
|
|
242
|
+
except CoordinatorError as exc:
|
|
243
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
244
|
+
return 2
|
agcoord/frame.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Coordinator-local terminal frame styling."""
|
|
2
|
+
|
|
3
|
+
# This static stylesheet is package-owned so the terminal UI has no application dependency.
|
|
4
|
+
CSS = """
|
|
5
|
+
Screen, .framed { background: #f7f7f3; color: #111111; }
|
|
6
|
+
Header { display: none; }
|
|
7
|
+
Footer { background: #e3e3df; color: #111111; height: 1; }
|
|
8
|
+
Footer > .footer-key--key { background: #e3e3df; color: #111111; text-style: bold; }
|
|
9
|
+
Footer > .footer-key--description { background: #e3e3df; color: #333333; }
|
|
10
|
+
DataTable { background: #ffffff; color: #111111; border: none; }
|
|
11
|
+
DataTable > .datatable--header { background: #ddddda; color: #111111; text-style: bold; }
|
|
12
|
+
DataTable > .datatable--cursor { background: #111111; color: #ffffff; text-style: bold; }
|
|
13
|
+
Button { background: #e0e0dd; color: #111111; border: none; height: 1; margin: 0 1 0 0; }
|
|
14
|
+
Button.-primary { background: #111111; color: #ffffff; }
|
|
15
|
+
Button:disabled { color: #777777; }
|
|
16
|
+
|
|
17
|
+
.subject { height: auto; padding: 0 1; background: #111111; color: #ffffff;
|
|
18
|
+
text-style: bold; }
|
|
19
|
+
.detail-rule { height: 1; padding: 0 1; background: #f7f7f3; color: #333333; }
|
|
20
|
+
.detail { height: auto; padding: 0 1; background: #ffffff; color: #111111; }
|
|
21
|
+
.status { height: 1; padding: 0 1; background: #e3e3df; color: #222222; }
|
|
22
|
+
|
|
23
|
+
#show, #confirm { width: 76%; max-width: 96; height: auto; max-height: 90%;
|
|
24
|
+
padding: 0 1; background: #ffffff; color: #111111;
|
|
25
|
+
border: double #111111; }
|
|
26
|
+
#show.wide { width: 96%; max-width: 150; max-height: 94%; }
|
|
27
|
+
#show-title, #confirm-title { height: auto; padding: 0 1; background: #ddddda;
|
|
28
|
+
color: #111111; text-style: bold; }
|
|
29
|
+
#show-scroll { height: auto; max-height: 100%; padding: 0 1; overflow-y: auto;
|
|
30
|
+
background: #ffffff; color: #111111; }
|
|
31
|
+
#show-body { height: auto; background: #ffffff; color: #111111; }
|
|
32
|
+
#show-keys { height: 1; }
|
|
33
|
+
#confirm-effects { height: auto; padding: 0 1; color: #222222; }
|
|
34
|
+
#confirm-buttons { height: auto; }
|
|
35
|
+
"""
|