Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,32 @@ brings the owner back after the installation starts a reading of the suggestions
Adding by an address, which asks GitHub whether the App is installed there, stays for a repository that is not in the
list.

The state at the top of a project's card, its chip ("active · priority ×2" or "inactive"), is a button in a small form:
one click turns the project's bursts on or off (`POST /projects/<owner>/<name>/active`). The form says the state that it
wants and does not flip the one that it finds, so that a page that is out of date never turns the project the wrong way.
It changes only the project's active flag, but for one thing: a project with no priority (weight 0 or less, which only
`lastcall project add --weight 0` makes) is off whatever its flag says, so turning it on also gives it weight 1. The
owner comes back to the card (`#project-owner--name`) with no notice, since the chip is the answer; the Active box in
the card's Settings stays and works as before. The chip's accessible name says what a press does and keeps the text that
the chip shows ("Turn off bursts on owner/name (active · priority ×2)"), so it has no `aria-pressed`, which goes with a
name that never changes.

The trash button at the other end of the header removes a project from lastcall, and it only asks: it opens a popover
(`popovertarget`, as the help bubbles do) that says what removing means, with a Cancel button that closes it and has the
focus, and the Remove button of the form that posts `POST /projects/<owner>/<name>/remove`. Browsers without `popover`
show neither the question nor the trash button, since inline the question would sit in every card one click away from
removing it; `lastcall project remove` does it there. While a run is going on the project, the removal is refused and
the Projects page says to remove it when the run ends. Otherwise one transaction deletes the owner's orders for the
project that have not started (the service does not start one that it had already read) and the project's row, and the
owner is told that its history stays. Nothing else goes: runs, findings, ideas, labelled issues and pull requests are
keyed by the repository's name, not by the project, so adding the repository again, from the suggestions, where it is
back at once, or by its address, picks up where it left off, and the judge does not propose again what the owner
rejected. Only the project's own settings (priority, language, merge on approve and the rest) are forgotten. The choices
of containers and Loki sources in the settings stay too, and come back with the project: the Settings page shows them
apart, as chosen for a project that is not one now, and saving the page never erases them. While the project is out,
sync goes on following its open pull requests, so that what the owner does with them is still recorded, but lastcall
neither merges nor watches them. `lastcall project remove <owner/name>` follows the same rules from the command line.

A project with merge on approve or the watch on shows a notice on its card while GitHub says that the App's installation
cannot read checks and commit statuses (an App made before v0.7.2 cannot): the two steps, which are to set them to
Read-only on the App's permissions page and then to accept them on the installation, and the link to that page. The
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ Projects, and Settings holds a fourth that is optional:
docker compose --profile local run --rm ollama-pull qwen3-coder # a model, about 19 GB: a graphics card helps
```
- 📁 **Projects.** lastcall suggests your repositories and says why. Tick the ones you want, or add the suggested ones
or all of them at once, with a filter when you have many. Any other repository goes in by its address.
or all of them at once, with a filter when you have many. Any other repository goes in by its address. A click on
a project's state turns its bursts on or off, and its trash button removes it from lastcall but keeps its history.
- 🔔 **Settings.** Where lastcall should notify you. Choose the service and fill in what it asks for: Telegram (paste
your bot's token and lastcall finds your chat), ntfy, Discord, Slack or email, or any of the
[services that Apprise reaches](https://github.com/caronc/apprise#supported-notifications). lastcall saves a
Expand Down Expand Up @@ -196,6 +197,7 @@ docker compose run --rm cli quota claude-max
docker compose run --rm cli account tier claude-max deep --effort max # the judge and fixes, at max
docker compose run --rm cli github repos
docker compose run --rm cli project add <owner/name> --signals <its project in Sentry or GlitchTip>
docker compose run --rm cli project remove <owner/name> # its history stays
docker compose run --rm cli signals connect --url https://sentry.io --org <slug> # asks for the token
docker compose run --rm cli signals show <owner/name> # its errors, as audits read them
docker compose run --rm cli logs list # containers, with suggestions
Expand Down
24 changes: 24 additions & 0 deletions src/lastcall/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
lastcall account add <account> --engine opencode --provider openrouter --model <model> --budget 20 # an API key
lastcall account tier <account> deep --effort max # a tier's model and effort; - for lastcall's default
lastcall project add <owner/name> [--weight 1] [--language Spanish] [--inactive] [--signals <tracker project>]
lastcall project remove <owner/name> # stop working on it; its history stays
lastcall signals connect --url https://sentry.io --org <slug> # the error tracker, Sentry or GlitchTip
lastcall signals show <owner/name> # the errors in production that the audits read
lastcall logs list # this host's containers, with their projects or suggestions
Expand Down Expand Up @@ -149,6 +150,10 @@ def main(argv: list[str] | None = None) -> int:
proj_add.add_argument("--inactive", action="store_true", help="keep it, but do not audit it")
proj_add.add_argument("--signals", help="its project in the error tracker (Sentry or GlitchTip), by slug")
proj.add_parser("list", help="the projects")
proj_remove = proj.add_parser(
"remove", help="remove a project; its history stays, so adding it again picks up where it left off"
)
proj_remove.add_argument("repo", help="owner/name")

sig = commands.add_parser("signals", help="errors in production, from Sentry or GlitchTip").add_subparsers(
dest="action", required=True
Expand Down Expand Up @@ -324,6 +329,8 @@ def dispatch(args: argparse.Namespace) -> int:
state = "active" if p.active and p.weight > 0 else "inactive"
print(f"{p.repo:<40} {state:<8} weight {p.weight:g} {p.language or ''}")
return 0
case "project", "remove":
return remove_project(args.repo)
case "quota", _:
return quota(account(args.account))
case "github", "setup":
Expand Down Expand Up @@ -413,6 +420,23 @@ def add_account(args: argparse.Namespace) -> int:
return 0


def remove_project(repo: str) -> int:
"""The same as the card's trash button: refused while a run is going on the project; otherwise its orders that
have not started go with it, and its history stays."""
st = store()
project = st.project(repo)
if project is None:
raise ValueError(f"no project {repo!r}: `lastcall project list` shows them")
if not st.remove_project(project.repo):
print(f"A run is going on {project.repo}: remove it when the run ends.", file=sys.stderr)
return 1
print(
f"{project.repo} removed. Its history stays: `lastcall project add {project.repo}` picks up where it "
"left off, with the default settings."
)
return 0


def connect_signals(url: str, org: str, token_file: str | None) -> int:
url = url.strip().rstrip("/")
if not signals.address(url):
Expand Down
18 changes: 16 additions & 2 deletions src/lastcall/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,7 @@
"web.help.dashboard.state": "State: only active projects get bursts. The priority multiplies a project's points when lastcall picks what to work on next; you change both on the Projects page.",
"web.help.dashboard.trigger": "Trigger: burst, when lastcall started it on its own with leftover quota; manual, when you started it, from the Projects page or the command line. After the dot comes the kind of work, when it is not an audit: improvement, labelled issues, tests or dependencies.",
"web.help.dashboard.status": "Status: stopped early means that before a step the account had reached your reserve or its monthly budget, had paid usage on, or had no quota reading, so the run ended there. Interrupted means lastcall itself stopped while the run was going, for example at a restart. Failed means an error stopped it.",
"web.help.projects.active": "Active projects take part in bursts. Even then, a burst skips a project while it has 2 lastcall pull requests waiting for you, or when the same account audited it in the last 7 days, unless you ticked ideas or labelled issues for it. The run buttons work even when the project is inactive.",
"web.help.projects.active": "Active projects take part in bursts. Even then, a burst skips a project while it has 2 lastcall pull requests waiting for you, or when the same account audited it in the last 7 days, unless you ticked ideas or labelled issues for it. The run buttons work even when the project is inactive. Click the state at the top of a card to turn bursts on or off; the “Active” box in Settings does the same.",
"web.help.projects.weight": "When an account has quota to spend, lastcall gives each active project points (never audited by that account, days and new commits since its last audit, pending findings, errors in production, what you asked for) and starts with the highest. Priority multiplies those points: High (×2) doubles them, Low (×0.5) halves them. It does not make a run longer.",
"web.help.projects.language": "The language of this project's pull requests, issues and reports. With “The account's”, lastcall writes in the language of whichever account does the run, set on the Accounts page. It does not change these pages or your notifications.",
"web.help.projects.max_prs": "The most pull requests one run opens on this project, whatever it does: an audit, tests, dependency upgrades, the ideas of a plan or labelled issues. What does not fit waits for a later run.",
Expand Down Expand Up @@ -812,5 +812,19 @@
"web.setup.step1_text": "lastcall writes a setup code in its log. Show the log with:",
"web.setup.step2": "Choose a password",
"web.error.title_origin": "Form not accepted",
"web.error.title_csrf": "Form expired"
"web.error.title_csrf": "Form expired",
"web.project.turn_off": "Turn off bursts on {repo} ({state})",
"web.project.turn_on": "Turn on bursts on {repo} ({state})",
"web.project.remove": "Remove {repo} from lastcall",
"web.project.remove_title": "Remove {repo} from lastcall?",
"web.project.remove_stops": "lastcall stops working on this repository.",
"web.project.remove_history": "Its history and what lastcall learned stay. Add it again and it picks up where it left off, without proposing again what you rejected.",
"web.project.remove_settings": "Its settings (priority, language and the rest) are forgotten.",
"web.project.remove_github": "Nothing changes on GitHub: the repository and its pull requests stay.",
"web.project.remove_back": "It goes back to the list of repositories to add.",
"web.project.remove_confirm": "Remove",
"web.project.remove_cancel": "Cancel",
"web.project.remove_busy": "A run is going on {repo} right now. Remove it when the run ends.",
"web.done.removed": "{repo} removed. Its history stays: add it again from the list below at any time.",
"web.logs.gone": "It was chosen for {project}, which is not a project now: add {project} again and it is read again."
}
18 changes: 16 additions & 2 deletions src/lastcall/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,7 @@
"web.help.dashboard.state": "Estado: solo los proyectos activos reciben ráfagas. La prioridad multiplica los puntos de un proyecto cuando lastcall elige en qué trabajar; las dos cosas se cambian en la página de proyectos.",
"web.help.dashboard.trigger": "Origen: ráfaga, cuando lastcall la empezó por su cuenta con cuota sobrante; manual, cuando la empezaste tú, desde la página de proyectos o la línea de comandos. Tras el punto va el tipo de trabajo, cuando no es una auditoría: mejora, issues etiquetados, tests o dependencias.",
"web.help.dashboard.status": "Estado: parada antes de tiempo significa que, antes de un paso, la cuenta había llegado a tu reserva o a su presupuesto mensual, tenía activo el uso de pago o no tenía lectura de cuota, así que la ejecución terminó ahí. Interrumpida significa que lastcall se detuvo mientras la ejecución estaba en marcha, por ejemplo al reiniciarse. Fallida significa que un error la detuvo.",
"web.help.projects.active": "Los proyectos activos entran en las ráfagas. Aun así, una ráfaga se salta un proyecto mientras tenga 2 pull requests de lastcall esperándote, o si la misma cuenta lo auditó en los últimos 7 días, salvo que hayas marcado ideas o etiquetado issues para él. Los botones para lanzar una ejecución funcionan aunque el proyecto esté inactivo.",
"web.help.projects.active": "Los proyectos activos entran en las ráfagas. Aun así, una ráfaga se salta un proyecto mientras tenga 2 pull requests de lastcall esperándote, o si la misma cuenta lo auditó en los últimos 7 días, salvo que hayas marcado ideas o etiquetado issues para él. Los botones para lanzar una ejecución funcionan aunque el proyecto esté inactivo. Haz clic en el estado, arriba en la tarjeta, para activar o desactivar las ráfagas; la casilla «Activo» de Ajustes hace lo mismo.",
"web.help.projects.weight": "Cuando una cuenta tiene cuota que gastar, lastcall da puntos a cada proyecto activo (nunca auditado por esa cuenta, días y commits nuevos desde su última auditoría, hallazgos pendientes, errores en producción, lo que hayas pedido) y empieza por el que más tiene. La prioridad multiplica esos puntos: Alta los duplica y Baja los reduce a la mitad. No hace la ejecución más larga.",
"web.help.projects.language": "El idioma de los pull requests, issues e informes de este proyecto. Con «El de la cuenta», lastcall escribe en el idioma de la cuenta que haga la ejecución, el que se elige en Cuentas. No cambia estas páginas ni tus notificaciones.",
"web.help.projects.max_prs": "El máximo de pull requests que abre una ejecución en este proyecto, haga lo que haga: una auditoría, tests, actualizaciones de dependencias, las ideas de un plan o issues etiquetados. Lo que no cabe espera a una ejecución posterior.",
Expand Down Expand Up @@ -812,5 +812,19 @@
"web.setup.step1_text": "lastcall escribe un código de configuración en su log. Muestra el log con:",
"web.setup.step2": "Elige una contraseña",
"web.error.title_origin": "Formulario no aceptado",
"web.error.title_csrf": "Formulario caducado"
"web.error.title_csrf": "Formulario caducado",
"web.project.turn_off": "Desactivar las ráfagas en {repo} ({state})",
"web.project.turn_on": "Activar las ráfagas en {repo} ({state})",
"web.project.remove": "Quitar {repo} de lastcall",
"web.project.remove_title": "¿Quitar {repo} de lastcall?",
"web.project.remove_stops": "lastcall deja de trabajar en este repositorio.",
"web.project.remove_history": "Su historial y lo que lastcall aprendió se quedan. Si lo añades de nuevo, sigue donde lo dejó, sin volver a proponer lo que rechazaste.",
"web.project.remove_settings": "Sus ajustes (prioridad, idioma y el resto) se olvidan.",
"web.project.remove_github": "En GitHub no cambia nada: el repositorio y sus pull requests se quedan.",
"web.project.remove_back": "Vuelve a la lista de repositorios que puedes añadir.",
"web.project.remove_confirm": "Quitar",
"web.project.remove_cancel": "Cancelar",
"web.project.remove_busy": "Hay una ejecución en curso en {repo}. Quítalo cuando termine.",
"web.done.removed": "{repo} quitado. Su historial se queda: vuelve a añadirlo cuando quieras desde la lista de abajo.",
"web.logs.gone": "Se eligió para {project}, que ya no es un proyecto: añade {project} de nuevo y se vuelve a leer."
}
11 changes: 11 additions & 0 deletions src/lastcall/logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ def choose(store: Store, choices: Mapping[str, str], key: str = CHOSEN) -> None:
store.set_setting(key, json.dumps(dict(sorted(choices.items()))))


def shown(choice: str, repos: Iterable[str]) -> tuple[str, str]:
"""What the Settings page shows of the owner's choice for a container or a source: the project, as the projects
page writes it, while it is one; or, apart, the name that the choice keeps after the owner removed the project.
Settings keeps that choice, so that adding the project again brings it back, and the page shows none meanwhile:
a form that sent the removed name back as the one it showed would erase it."""
for repo in repos:
if repo.lower() == choice.lower():
return repo, ""
return "", choice


def project_of(container: Mapping[str, Any], choices: Mapping[str, str]) -> str:
"""The project of a container: its label, or else the owner's choice; "" for none."""
return str(container.get("label") or "") or choices.get(str(container.get("name") or ""), "")
Expand Down
3 changes: 2 additions & 1 deletion src/lastcall/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,8 @@ def take_orders(self, now: float) -> None:
account = max(free, key=self.leftover)
recipe = order["kind"] if order["kind"] in ("improve", "issues", "tests", "deps") else "audit"
crew = [a for a in free if a is not account] if order["account"] == "*" and recipe == "audit" else []
store.start_order(order["id"], now)
if not store.start_order(order["id"], now): # the owner removed its project while it waited
continue
self.log(f"{account.id}: {recipe} of {order['repo']} ordered by the owner")
self.launch(account, order["repo"], "manual", crew, recipe)

Expand Down
Loading
Loading