nohalt 0.1.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.
- nohalt-0.1.0/.gitignore +18 -0
- nohalt-0.1.0/CHANGELOG.md +25 -0
- nohalt-0.1.0/LICENSE +21 -0
- nohalt-0.1.0/PKG-INFO +106 -0
- nohalt-0.1.0/README.md +75 -0
- nohalt-0.1.0/docs/ARCHITECTURE.md +100 -0
- nohalt-0.1.0/docs/CONFIGURATION.md +183 -0
- nohalt-0.1.0/docs/LESSONS.md +294 -0
- nohalt-0.1.0/docs/SAFETY.md +55 -0
- nohalt-0.1.0/docs/STACKS.md +108 -0
- nohalt-0.1.0/pyproject.toml +72 -0
- nohalt-0.1.0/src/nohalt/__init__.py +8 -0
- nohalt-0.1.0/src/nohalt/__main__.py +3 -0
- nohalt-0.1.0/src/nohalt/agents/__init__.py +81 -0
- nohalt-0.1.0/src/nohalt/agents/claude_code.py +85 -0
- nohalt-0.1.0/src/nohalt/agents/claude_stream.py +74 -0
- nohalt-0.1.0/src/nohalt/agents/command.py +47 -0
- nohalt-0.1.0/src/nohalt/agents/opencode.py +47 -0
- nohalt-0.1.0/src/nohalt/budget.py +82 -0
- nohalt-0.1.0/src/nohalt/cli.py +491 -0
- nohalt-0.1.0/src/nohalt/config.py +325 -0
- nohalt-0.1.0/src/nohalt/forge/__init__.py +48 -0
- nohalt-0.1.0/src/nohalt/forge/base.py +121 -0
- nohalt-0.1.0/src/nohalt/forge/github.py +229 -0
- nohalt-0.1.0/src/nohalt/forge/gitlab.py +328 -0
- nohalt-0.1.0/src/nohalt/forge/local.py +267 -0
- nohalt-0.1.0/src/nohalt/integrate/__init__.py +46 -0
- nohalt-0.1.0/src/nohalt/integrate/common.py +226 -0
- nohalt-0.1.0/src/nohalt/integrate/direct.py +72 -0
- nohalt-0.1.0/src/nohalt/integrate/mergers.py +274 -0
- nohalt-0.1.0/src/nohalt/integrate/train.py +374 -0
- nohalt-0.1.0/src/nohalt/models.py +145 -0
- nohalt-0.1.0/src/nohalt/prompt.py +115 -0
- nohalt-0.1.0/src/nohalt/prompts/__init__.py +1 -0
- nohalt-0.1.0/src/nohalt/prompts/core.md +57 -0
- nohalt-0.1.0/src/nohalt/prompts/frappe.md +7 -0
- nohalt-0.1.0/src/nohalt/queue.py +80 -0
- nohalt-0.1.0/src/nohalt/shims/__init__.py +35 -0
- nohalt-0.1.0/src/nohalt/shims/gh +83 -0
- nohalt-0.1.0/src/nohalt/shims/git +88 -0
- nohalt-0.1.0/src/nohalt/shims/glab +77 -0
- nohalt-0.1.0/src/nohalt/shims/pkill +59 -0
- nohalt-0.1.0/src/nohalt/stacks/__init__.py +93 -0
- nohalt-0.1.0/src/nohalt/stacks/frappe.py +113 -0
- nohalt-0.1.0/src/nohalt/state.py +198 -0
- nohalt-0.1.0/src/nohalt/supervisor.py +860 -0
- nohalt-0.1.0/src/nohalt/templates/frappe.toml +54 -0
- nohalt-0.1.0/src/nohalt/templates/generic.toml +70 -0
- nohalt-0.1.0/src/nohalt/util.py +185 -0
- nohalt-0.1.0/src/nohalt/verify.py +94 -0
- nohalt-0.1.0/src/nohalt/worktree.py +143 -0
- nohalt-0.1.0/tests/conftest.py +121 -0
- nohalt-0.1.0/tests/fake_agent.py +99 -0
- nohalt-0.1.0/tests/test_gitlab.py +288 -0
- nohalt-0.1.0/tests/test_integrate.py +226 -0
- nohalt-0.1.0/tests/test_mergers.py +112 -0
- nohalt-0.1.0/tests/test_shims.py +132 -0
- nohalt-0.1.0/tests/test_supervisor.py +317 -0
- nohalt-0.1.0/tests/test_units.py +374 -0
- nohalt-0.1.0/tests/test_verify.py +131 -0
nohalt-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-10-22)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* autonomous agent supervisor for GitHub and GitLab issues ([c401ee5](https://github.com/Seityl/nohalt/commit/c401ee527198bb896cdb92c604b245a6a781e36c))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* GitLab dogfood findings -- probe in launch mode, setup failures cost no attempt, drafts marked ready ([b67644d](https://github.com/Seityl/nohalt/commit/b67644d06a61561486ef470de3b0f352f88cf730))
|
|
14
|
+
* **prompt:** read GitHub issues over REST, not gh issue view ([ee0f8b4](https://github.com/Seityl/nohalt/commit/ee0f8b4e0b2c9155701750b461ade1f9e28f8262))
|
|
15
|
+
* **prompt:** verify command works when nohalt is not on PATH ([d462893](https://github.com/Seityl/nohalt/commit/d462893767c03cf7d6ff59045d7e458a7b33e871))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Documentation
|
|
19
|
+
|
|
20
|
+
* architecture, configuration, stacks, safety and lessons learnt ([51bdd1b](https://github.com/Seityl/nohalt/commit/51bdd1baaa92472b920e909d06b1c845b7641ae0))
|
|
21
|
+
|
|
22
|
+
## Changelog
|
|
23
|
+
|
|
24
|
+
All notable changes to this project are documented here. This file is maintained by
|
|
25
|
+
[release-please](https://github.com/googleapis/release-please) from conventional commits.
|
nohalt-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jeriel Francis trading as Seityl, and the nohalt contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
nohalt-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: nohalt
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Autonomous coding agents that keep going until the forge says done.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Seityl/nohalt
|
|
6
|
+
Project-URL: Issues, https://github.com/Seityl/nohalt/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/Seityl/nohalt/blob/main/CHANGELOG.md
|
|
8
|
+
Author: Jeriel Francis trading as Seityl
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agents,ai,autonomous,ci,claude-code,coding-agents,github,opencode,orchestration
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
21
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
25
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
29
|
+
Requires-Dist: twine>=5; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# nohalt
|
|
33
|
+
|
|
34
|
+
**Autonomous coding agents that keep going until the forge says done.**
|
|
35
|
+
|
|
36
|
+
[](https://github.com/Seityl/nohalt/actions/workflows/ci.yml)
|
|
37
|
+
[](LICENSE)
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
nohalt supervises a fleet of AI coding agents ([opencode](https://opencode.ai), [Claude Code](https://www.claude.com/product/claude-code), or any CLI) working through a queue of GitHub or GitLab issues: one agent per issue, each in its own git worktree, until every issue is **closed** -- not until the agents *say* they are done.
|
|
41
|
+
|
|
42
|
+
An endpoint that raised `NameError` on every call once shipped with green CI and a confident agent summary, and stayed broken for two months. Agents report success they have not earned. So nohalt believes exactly one thing: the issue's state on the forge, and an issue closes only when a reviewed, green pull request merges.
|
|
43
|
+
|
|
44
|
+
Everything else in nohalt exists because something went wrong without it, while agents ran unattended through the night on three production projects. Those lessons -- around a hundred of them -- are written up in [**docs/LESSONS.md**](docs/LESSONS.md), and the ones nohalt enforces are pinned by tests.
|
|
45
|
+
|
|
46
|
+
## What it does
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
queue.txt ──► scheduler ──► agent in its own worktree ──► PR ──► CI ──► integrator ──► merged ──► issue CLOSED
|
|
50
|
+
(deps) slots, memory, (opencode / claude-code) │ (direct or │
|
|
51
|
+
a model that │ │ release train) └─► pipeline watched;
|
|
52
|
+
answers ▼ ▼ red → fix-forward
|
|
53
|
+
verify lock: one test env red / conflicting → handed back issue, merging stops
|
|
54
|
+
per slot, at this commit to the agent with the failure
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- **Dependency-ordered queue.** `14: 13` means #14 waits until #13 is closed. Order is preference; dependencies are law. Edit it while nohalt runs.
|
|
58
|
+
- **GitHub or GitLab.** gitlab.com or self-hosted: merge requests, pipeline jobs as checks, `glab` for agents. Subgroups are fine.
|
|
59
|
+
- **Parallel agents, isolated.** One worktree and branch per issue; guard shims on each agent's `PATH` refuse merging, closing issues, pushing to the base branch, `git stash` (worktrees share one stash stack), and machine-wide `pkill`.
|
|
60
|
+
- **A verification lock.** Agents edit in parallel but verify one at a time per test slot (`nohalt verify <worktree> -- '<cmd>'`), with the environment reset to the commit under test.
|
|
61
|
+
- **Failures classified, not counted blindly.** A model-endpoint outage is not an attempt on the issue. A time limit continues in the same worktree. A hung agent (no output, no command running) is restarted. A red PR goes back to an agent with the failing checks. Only real failures spend attempts; then the issue is labelled for a human and the rest of the queue carries on.
|
|
62
|
+
- **One merger.** Agents never merge. `direct` mode merges green PRs one at a time and stops everything if the pipeline on the base branch goes red; `train` mode batches green PRs into a versioned release branch, resolving bookkeeping conflicts (package.json, migration lists, schema JSON) mechanically and handing real code conflicts back.
|
|
63
|
+
- **Survives itself.** Agents run in their own sessions. Kill or restart the supervisor and it adopts the running agents from their records. `nohalt keepalive` in cron brings it back.
|
|
64
|
+
- **Model routing and a spend guard.** Route by label or title, pin per issue, gate premium models behind approval, fall over to a model that answers, and cap paid-API spend per issue and in total.
|
|
65
|
+
- **Stacks.** A `generic` stack (your own `up`/`reset`/`env` hooks for the test environment) and a `frappe` stack (a pool of agent test sites on a shared bench, worktree-first `PYTHONPATH`, migrate before test, DocType/fixture/patch merge drivers).
|
|
66
|
+
|
|
67
|
+
## Quick start
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install nohalt # Python 3.11+, Linux; needs git, plus gh (GitHub) or glab (GitLab)
|
|
71
|
+
cd your-repo
|
|
72
|
+
nohalt init # writes nohalt.toml and queue.txt (--stack frappe for Frappe apps)
|
|
73
|
+
$EDITOR nohalt.toml queue.txt
|
|
74
|
+
nohalt doctor --probe # checks config, forge auth, branch protection, agent binaries, models
|
|
75
|
+
nohalt run --dry-run # what would launch now, what waits on what
|
|
76
|
+
nohalt run # supervise; Ctrl-C leaves agents running, `nohalt run` adopts them again
|
|
77
|
+
nohalt status --watch # one screen: agents, idle time, attempts, last line of each log
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Keep it running without a terminal:
|
|
81
|
+
|
|
82
|
+
```cron
|
|
83
|
+
*/3 * * * * cd /path/to/your-repo && nohalt keepalive
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Control while it runs: `nohalt stop` (between issues), `nohalt stop --now`, `nohalt pause`, `nohalt route <issue> <model>`, `nohalt unblock <issue>`, `nohalt queue --add 42 --after 41`, `nohalt prompt <issue>` (see exactly what an agent will be told).
|
|
87
|
+
|
|
88
|
+
## Before you run it
|
|
89
|
+
|
|
90
|
+
nohalt gives AI agents write access to your repository and, if you enable an integrator, lets nohalt merge and deploy. **Read [docs/SAFETY.md](docs/SAFETY.md).** In short: protect the base branch on the forge (the shims are speed bumps, protection is the wall), use a token scoped to the one repository, run agents as a user that cannot reach production, start with `integrate.mode = "none"`, and watch the first few issues end to end.
|
|
91
|
+
|
|
92
|
+
## Documentation
|
|
93
|
+
|
|
94
|
+
- [docs/CONFIGURATION.md](docs/CONFIGURATION.md) -- every key in `nohalt.toml`
|
|
95
|
+
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) -- the supervisor loop, issue lifecycle, state on disk
|
|
96
|
+
- [docs/STACKS.md](docs/STACKS.md) -- generic and Frappe test environments, writing your own
|
|
97
|
+
- [docs/SAFETY.md](docs/SAFETY.md) -- the threat model and the guardrails
|
|
98
|
+
- [docs/LESSONS.md](docs/LESSONS.md) -- what running agent fleets unattended taught us
|
|
99
|
+
|
|
100
|
+
## Status
|
|
101
|
+
|
|
102
|
+
Alpha. The mechanisms come from scripts that ran production agent fleets for weeks; this package is their consolidation into one tested, configurable tool, and it is younger than they are. Before the first release it was run end to end on both forges -- real agents (Claude Code on GitHub, opencode on a self-hosted GitLab), real CI, both `none` and `direct` integration, a supervisor `kill -9` mid-run -- and the bugs those runs found are fixed. Expect rough edges. Issues and pull requests are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
[MIT](LICENSE) © Jeriel Francis trading as Seityl, and the nohalt contributors.
|
nohalt-0.1.0/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# nohalt
|
|
2
|
+
|
|
3
|
+
**Autonomous coding agents that keep going until the forge says done.**
|
|
4
|
+
|
|
5
|
+
[](https://github.com/Seityl/nohalt/actions/workflows/ci.yml)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
nohalt supervises a fleet of AI coding agents ([opencode](https://opencode.ai), [Claude Code](https://www.claude.com/product/claude-code), or any CLI) working through a queue of GitHub or GitLab issues: one agent per issue, each in its own git worktree, until every issue is **closed** -- not until the agents *say* they are done.
|
|
10
|
+
|
|
11
|
+
An endpoint that raised `NameError` on every call once shipped with green CI and a confident agent summary, and stayed broken for two months. Agents report success they have not earned. So nohalt believes exactly one thing: the issue's state on the forge, and an issue closes only when a reviewed, green pull request merges.
|
|
12
|
+
|
|
13
|
+
Everything else in nohalt exists because something went wrong without it, while agents ran unattended through the night on three production projects. Those lessons -- around a hundred of them -- are written up in [**docs/LESSONS.md**](docs/LESSONS.md), and the ones nohalt enforces are pinned by tests.
|
|
14
|
+
|
|
15
|
+
## What it does
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
queue.txt ──► scheduler ──► agent in its own worktree ──► PR ──► CI ──► integrator ──► merged ──► issue CLOSED
|
|
19
|
+
(deps) slots, memory, (opencode / claude-code) │ (direct or │
|
|
20
|
+
a model that │ │ release train) └─► pipeline watched;
|
|
21
|
+
answers ▼ ▼ red → fix-forward
|
|
22
|
+
verify lock: one test env red / conflicting → handed back issue, merging stops
|
|
23
|
+
per slot, at this commit to the agent with the failure
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- **Dependency-ordered queue.** `14: 13` means #14 waits until #13 is closed. Order is preference; dependencies are law. Edit it while nohalt runs.
|
|
27
|
+
- **GitHub or GitLab.** gitlab.com or self-hosted: merge requests, pipeline jobs as checks, `glab` for agents. Subgroups are fine.
|
|
28
|
+
- **Parallel agents, isolated.** One worktree and branch per issue; guard shims on each agent's `PATH` refuse merging, closing issues, pushing to the base branch, `git stash` (worktrees share one stash stack), and machine-wide `pkill`.
|
|
29
|
+
- **A verification lock.** Agents edit in parallel but verify one at a time per test slot (`nohalt verify <worktree> -- '<cmd>'`), with the environment reset to the commit under test.
|
|
30
|
+
- **Failures classified, not counted blindly.** A model-endpoint outage is not an attempt on the issue. A time limit continues in the same worktree. A hung agent (no output, no command running) is restarted. A red PR goes back to an agent with the failing checks. Only real failures spend attempts; then the issue is labelled for a human and the rest of the queue carries on.
|
|
31
|
+
- **One merger.** Agents never merge. `direct` mode merges green PRs one at a time and stops everything if the pipeline on the base branch goes red; `train` mode batches green PRs into a versioned release branch, resolving bookkeeping conflicts (package.json, migration lists, schema JSON) mechanically and handing real code conflicts back.
|
|
32
|
+
- **Survives itself.** Agents run in their own sessions. Kill or restart the supervisor and it adopts the running agents from their records. `nohalt keepalive` in cron brings it back.
|
|
33
|
+
- **Model routing and a spend guard.** Route by label or title, pin per issue, gate premium models behind approval, fall over to a model that answers, and cap paid-API spend per issue and in total.
|
|
34
|
+
- **Stacks.** A `generic` stack (your own `up`/`reset`/`env` hooks for the test environment) and a `frappe` stack (a pool of agent test sites on a shared bench, worktree-first `PYTHONPATH`, migrate before test, DocType/fixture/patch merge drivers).
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install nohalt # Python 3.11+, Linux; needs git, plus gh (GitHub) or glab (GitLab)
|
|
40
|
+
cd your-repo
|
|
41
|
+
nohalt init # writes nohalt.toml and queue.txt (--stack frappe for Frappe apps)
|
|
42
|
+
$EDITOR nohalt.toml queue.txt
|
|
43
|
+
nohalt doctor --probe # checks config, forge auth, branch protection, agent binaries, models
|
|
44
|
+
nohalt run --dry-run # what would launch now, what waits on what
|
|
45
|
+
nohalt run # supervise; Ctrl-C leaves agents running, `nohalt run` adopts them again
|
|
46
|
+
nohalt status --watch # one screen: agents, idle time, attempts, last line of each log
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Keep it running without a terminal:
|
|
50
|
+
|
|
51
|
+
```cron
|
|
52
|
+
*/3 * * * * cd /path/to/your-repo && nohalt keepalive
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Control while it runs: `nohalt stop` (between issues), `nohalt stop --now`, `nohalt pause`, `nohalt route <issue> <model>`, `nohalt unblock <issue>`, `nohalt queue --add 42 --after 41`, `nohalt prompt <issue>` (see exactly what an agent will be told).
|
|
56
|
+
|
|
57
|
+
## Before you run it
|
|
58
|
+
|
|
59
|
+
nohalt gives AI agents write access to your repository and, if you enable an integrator, lets nohalt merge and deploy. **Read [docs/SAFETY.md](docs/SAFETY.md).** In short: protect the base branch on the forge (the shims are speed bumps, protection is the wall), use a token scoped to the one repository, run agents as a user that cannot reach production, start with `integrate.mode = "none"`, and watch the first few issues end to end.
|
|
60
|
+
|
|
61
|
+
## Documentation
|
|
62
|
+
|
|
63
|
+
- [docs/CONFIGURATION.md](docs/CONFIGURATION.md) -- every key in `nohalt.toml`
|
|
64
|
+
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) -- the supervisor loop, issue lifecycle, state on disk
|
|
65
|
+
- [docs/STACKS.md](docs/STACKS.md) -- generic and Frappe test environments, writing your own
|
|
66
|
+
- [docs/SAFETY.md](docs/SAFETY.md) -- the threat model and the guardrails
|
|
67
|
+
- [docs/LESSONS.md](docs/LESSONS.md) -- what running agent fleets unattended taught us
|
|
68
|
+
|
|
69
|
+
## Status
|
|
70
|
+
|
|
71
|
+
Alpha. The mechanisms come from scripts that ran production agent fleets for weeks; this package is their consolidation into one tested, configurable tool, and it is younger than they are. Before the first release it was run end to end on both forges -- real agents (Claude Code on GitHub, opencode on a self-hosted GitLab), real CI, both `none` and `direct` integration, a supervisor `kill -9` mid-run -- and the bugs those runs found are fixed. Expect rough edges. Issues and pull requests are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
[MIT](LICENSE) © Jeriel Francis trading as Seityl, and the nohalt contributors.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
nohalt is one supervisor loop and a set of small, replaceable parts:
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
src/nohalt/
|
|
7
|
+
supervisor.py the loop: reap → watch → integrate → launch → budget
|
|
8
|
+
queue.py queue.txt: "<issue>: <deps>"
|
|
9
|
+
state.py issue records and control files (JSON, written atomically)
|
|
10
|
+
worktree.py one git worktree per issue; rescue before removal
|
|
11
|
+
models.py routing, pins, premium gate, probes, fallbacks
|
|
12
|
+
budget.py paid-API spend estimate and caps
|
|
13
|
+
prompt.py core prompt + stack addendum + project files; strict placeholders
|
|
14
|
+
verify.py the verification lock (slot pool) and refused commands
|
|
15
|
+
forge/ the forge interface; GitHub (gh REST), GitLab (REST v4); a file-backed forge
|
|
16
|
+
agents/ opencode, claude-code, any command
|
|
17
|
+
stacks/ generic (shell hooks), frappe (site pool on a shared bench)
|
|
18
|
+
integrate/ none | direct | train, plus the bookkeeping merge drivers
|
|
19
|
+
shims/ gh, glab, git, pkill guards put first on every agent's PATH
|
|
20
|
+
cli.py the `nohalt` command
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## The loop
|
|
24
|
+
|
|
25
|
+
Every `tick` (default 60 s):
|
|
26
|
+
|
|
27
|
+
1. **Reap.** For every issue whose agent is running: past its time limit → stop it (`timeout`). No output for `hung_after` and no command of its own running → stop it (`hung`). Exited → *judge* it.
|
|
28
|
+
2. **Watch.** For every issue whose agent finished with a pull request: read the PR's checks on its **current head**. Red → hand back to an agent with the failing checks. Green but unmergeable → hand back to rebase. Green and mergeable → `ready`.
|
|
29
|
+
3. **Integrate.** `direct` or `train` merges `ready` PRs (see below). `none` does nothing: a human merges. Either way, an issue still open after its PR merged is closed by nohalt -- forges act on closing keywords only for the default branch.
|
|
30
|
+
4. **Launch.** Read the queue again (it can be edited live). Walk it in order. For each open issue that is not blocked, not held, has its dependencies closed, and has passed its backoff time, launch an agent if a slot in its model's pool is free, enough memory is available, and the model answers a probe (or a fallback does).
|
|
31
|
+
5. **Budget.** Every few minutes, estimate paid-API spend and move issues over their cap to a fallback model.
|
|
32
|
+
|
|
33
|
+
The supervisor holds an exclusive lock, so only one runs per state directory. A tick never blocks on long waits: watching a pipeline after a merge, or waiting for a usage limit to reset, is a state that later ticks re-examine.
|
|
34
|
+
|
|
35
|
+
## Issue lifecycle
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
┌──────────────── handed back (red CI, conflict, PR closed) ─────────────┐
|
|
39
|
+
▼ │
|
|
40
|
+
queued ──launch──► running ──exit──► judged ──PR open──► waiting_ci ──green──► ready ──merged──► done
|
|
41
|
+
▲ │ │ ▲
|
|
42
|
+
│ │ timeout / hung │ provider error / usage limit (not an attempt)
|
|
43
|
+
│ └─────────────────┴─► queued (resume, with backoff) │
|
|
44
|
+
│ │ │
|
|
45
|
+
│ └─ no PR, no close: attempt+1 ─► queued │
|
|
46
|
+
│ attempts used ─► blocked │
|
|
47
|
+
│ │
|
|
48
|
+
└──── a human removes the blocked label (counters reset) forge says CLOSED ┘ (at any point)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
How an exit is judged, in order:
|
|
52
|
+
|
|
53
|
+
| Evidence | Verdict | Costs an attempt? |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| Forge says CLOSED (or the issue no longer exists) | done: worktree rescued and removed, agent data deleted | -- |
|
|
56
|
+
| Forge unreachable | `exited`, judged again next tick | no |
|
|
57
|
+
| Stopped by the budget guard or by `stop --now` | requeued, resumes | no |
|
|
58
|
+
| The agent's output ends on a usage-limit event | requeued after 15 min, the model is not probed for 20 min | no |
|
|
59
|
+
| The agent's output ends on a model-endpoint error | requeued with backoff × n. Blocked after `provider_retries`, with a diagnosis that says it was not the issue's fault | no |
|
|
60
|
+
| Time limit, issue still open | continues in the same worktree, up to `timeout_continues` times | no |
|
|
61
|
+
| Hung | restarted, up to `revive_limit` times | no |
|
|
62
|
+
| A pull request exists | `waiting_ci` | no |
|
|
63
|
+
| None of the above | requeued after `revive_grace` (it may have stopped to ask). Blocked after `max_attempts` | **yes** |
|
|
64
|
+
|
|
65
|
+
A *resumed* launch continues the agent's previous session in the same worktree with a short "you were stopped, check git status and your PR first" prompt. A *fresh* launch (used after a rebase request, when the world has moved on) starts a new session from the full prompt.
|
|
66
|
+
|
|
67
|
+
## Integration
|
|
68
|
+
|
|
69
|
+
**direct.** Takes the first `ready` issue in queue order, checks again that its PR is green on its current head and mergeable, merges it, then watches the pipeline on the base branch's new head (`cd_workflow`, else `ci_workflow`, else the required checks). Nothing else merges until that pipeline is green. If it is red, or times out, nohalt opens a fix-forward issue at the front of the queue, and merging resumes when that issue closes. A `deploy` command, if set, runs after a green pipeline (unless a CD workflow already deploys).
|
|
70
|
+
|
|
71
|
+
**train.** With no release PR open, every `ready` issue's green PR (priority list first, then queue order, minus held issues and parked SHAs) is merged into `release/auto-<time>` off the base. Conflicts go to the merge drivers by glob (`[integrate.mergers]`). If any file stays in conflict, that PR is left out and handed back to rebase. The batch gets the next free version (from tags, or the version files), an optional regeneration command, and a PR closing every carried issue. On a later tick: green and mergeable → merge, tag, deploy, then the same pipeline gate as `direct` watches the base branch. Red → re-run the failed jobs once (more if the failure was the network). Still red → close the release, park the carried heads until their agents push, and hand each issue back.
|
|
72
|
+
|
|
73
|
+
## State on disk
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
<state_dir>/
|
|
77
|
+
supervisor.lock one supervisor at a time
|
|
78
|
+
issues/<n>.json IssueRecord: status, counters, model, pid, branch, worktree, PR, history
|
|
79
|
+
pids.json process start times (a recycled pid is not our agent)
|
|
80
|
+
pins.json approved.json model pins and premium approvals
|
|
81
|
+
probes.json limited.json model probe cache and usage-limit marks
|
|
82
|
+
spend.json budget high-water marks
|
|
83
|
+
integrate.json train.json integrator state that spans ticks
|
|
84
|
+
status.json last tick's summary (for `nohalt status`)
|
|
85
|
+
STOP PAUSE WAKE BATCH_PAUSE DEPLOY_BLOCKED BUDGET_EXHAUSTED control flags
|
|
86
|
+
priority hold one issue per line (train ordering / exclusion)
|
|
87
|
+
bin/ guard shims
|
|
88
|
+
locks/verify-<slot>.lock verification slots
|
|
89
|
+
agent-data/<n>/ per-agent session data (opencode XDG_DATA_HOME)
|
|
90
|
+
logs/ nohalt.log, events.jsonl, issue-<n>.log, guard.log, refused.log, deploy.log
|
|
91
|
+
work/<n>/ worktrees (unless [project] worktrees is set)
|
|
92
|
+
work/_release/ the train's integration tree
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Extending
|
|
96
|
+
|
|
97
|
+
- **A new agent CLI:** implement `launch`, `probe_launch`, `provider_failed` and `usage_limited` (see `agents/opencode.py`), and register it in `agents.get`. Before writing an adapter, try `command:` with `[agents.options.command] argv = [...]`.
|
|
98
|
+
- **A new stack:** subclass `stacks.Stack`. Implement `apply_defaults` (slots, refused commands, merge drivers) and `prepare` (bring the test environment to the worktree's commit inside the lock), and optionally a prompt addendum.
|
|
99
|
+
- **A new forge:** implement the `forge.base.Forge` protocol. Keep to its rule: a failed call raises `ForgeError`. It never returns something that looks like an answer.
|
|
100
|
+
- **A new merge driver:** a function `(repo, path) -> bool` in `integrate/mergers.py`, added to `DRIVERS`, with a test that runs it inside a real conflicted merge.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
One `nohalt.toml` per project. `nohalt init [--stack frappe]` writes a starter file, and `nohalt doctor` checks it. nohalt looks for `nohalt.toml` in the current directory and its parents, or takes `--config PATH`. Relative paths are relative to the file.
|
|
4
|
+
|
|
5
|
+
**Unknown keys are errors.** A typo that silently falls back to a default is how a guard quietly stops guarding.
|
|
6
|
+
|
|
7
|
+
Durations accept `90`, `"90s"`, `"30m"`, `"4h"` or `"2d"`. Models are always written `<agent>:<model>`: `opencode:anthropic/claude-sonnet-4-5`, `claude-code:sonnet`, `command:anything`.
|
|
8
|
+
|
|
9
|
+
## [project]
|
|
10
|
+
|
|
11
|
+
| Key | Default | Meaning |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `repo` | -- | `owner/name` on GitHub, `group[/subgroup]/name` on GitLab, or `local:/path/forge.json` for a file-backed forge (rehearsal, tests) |
|
|
14
|
+
| `forge` | `"github"` | `github` or `gitlab` |
|
|
15
|
+
| `forge_url` | `""` | GitLab only: the instance, e.g. `https://gitlab.example.com` (default `$GITLAB_HOST`, else `https://gitlab.com`). Also exported to agents as `GITLAB_HOST` |
|
|
16
|
+
| `token_env` | `"GITLAB_TOKEN"` | GitLab only: the variable holding a token with the `api` scope. If unset, nohalt uses the token `glab` is logged in with |
|
|
17
|
+
| `path` | `"."` | the main clone. Worktrees are cut from it |
|
|
18
|
+
| `base_branch` | `"main"` | branches start here. PRs target it. Agents may never push to it |
|
|
19
|
+
| `remote` | `"origin"` | |
|
|
20
|
+
| `state_dir` | `".nohalt"` | records, logs, locks, shims, agent data |
|
|
21
|
+
| `worktrees` | `<state_dir>/work` | where per-issue worktrees go (keep it out of a Frappe bench's `apps/`) |
|
|
22
|
+
| `shared_paths` | `[]` | untracked directories symlinked from the main clone into each worktree (e.g. `node_modules`) |
|
|
23
|
+
| `branch_prefixes` | `{ bug = "fix" }` | label → branch prefix |
|
|
24
|
+
| `default_prefix` | `"feature"` | branches are `<prefix>/<issue>-<slug>`. An existing branch matching `*/<issue>-*` is reused |
|
|
25
|
+
|
|
26
|
+
## [queue]
|
|
27
|
+
|
|
28
|
+
| Key | Default | Meaning |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `file` | `"queue.txt"` | `<issue>: <deps…>` per line, `#` comments. Re-read every tick |
|
|
31
|
+
| `blocked_label` | `"nohalt-blocked"` | put on issues that need a human. Remove it to retry, with counters reset |
|
|
32
|
+
| `held_labels` | `["needs-human"]` | issues carrying one of these are never worked |
|
|
33
|
+
| `tick` | `60` | seconds between passes |
|
|
34
|
+
| `poll_when_idle` | `600` | seconds between passes when nothing can move |
|
|
35
|
+
|
|
36
|
+
## [agents]
|
|
37
|
+
|
|
38
|
+
| Key | Default | Meaning |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `max_parallel` | `2` | agent slots shared by every model not in a `pools` entry. Measure: on a small machine, more is slower |
|
|
41
|
+
| `pools` | `{}` | model prefix → its own cap, e.g. `{ "claude-code:" = 2, "opencode:anthropic/" = 4 }`. Override live with `<state>/caps.json` |
|
|
42
|
+
| `min_free_mb` | `2000` | do not launch below this much available memory |
|
|
43
|
+
| `phase_timeout` | `"4h"` | one agent run |
|
|
44
|
+
| `long_timeout` | `"8h"` | for issues carrying one of `long_labels` |
|
|
45
|
+
| `long_labels` | `[]` | |
|
|
46
|
+
| `max_attempts` | `2` | real attempts (exits with no PR and no close) before blocking |
|
|
47
|
+
| `provider_retries` | `5` | model-endpoint failures per issue before blocking. Never counted as attempts |
|
|
48
|
+
| `provider_backoff` | `"2m"` | multiplied by the retry number |
|
|
49
|
+
| `timeout_continues` | `3` | time limits that continue in the same worktree before counting as an attempt |
|
|
50
|
+
| `hung_after` | `"30m"` | no output and no command running: stop and restart |
|
|
51
|
+
| `revive_limit` | `2` | hung restarts before counting as an attempt |
|
|
52
|
+
| `revive_grace` | `"15m"` | wait before relaunching an agent that stopped without a PR (it may have asked a question) |
|
|
53
|
+
| `ci_rounds` | `3` | red or conflicting PRs handed back before blocking |
|
|
54
|
+
| `log_max_mb` | `20` | per-issue agent logs rotate past this |
|
|
55
|
+
| `title_prefix` | `"nohalt"` | agent session titles: `<prefix> #<issue>` |
|
|
56
|
+
| `copy_files` | `[]` | files copied into each agent's private data dir (e.g. an opencode `auth.json`) |
|
|
57
|
+
| `options` | `{}` | per-adapter options, below |
|
|
58
|
+
|
|
59
|
+
```toml
|
|
60
|
+
[agents.options.opencode]
|
|
61
|
+
binary = "opencode"
|
|
62
|
+
agent = "build" # opencode --agent
|
|
63
|
+
extra_args = []
|
|
64
|
+
|
|
65
|
+
[agents.options.claude-code]
|
|
66
|
+
binary = "claude"
|
|
67
|
+
disallowed_tools = ["Task", "CronCreate", "..."] # default: sub-agents, schedulers, remote triggers, worktree tools
|
|
68
|
+
mcp = false # true keeps your MCP servers available to agents
|
|
69
|
+
|
|
70
|
+
[agents.options.command] # any CLI: placeholders {model} {prompt} {title} {data_dir}
|
|
71
|
+
argv = ["my-agent", "--model", "{model}", "{prompt}"]
|
|
72
|
+
resume_args = ["--continue"]
|
|
73
|
+
probe_argv = ["my-agent", "--model", "{model}", "--ping"]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## [models]
|
|
77
|
+
|
|
78
|
+
| Key | Default | Meaning |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `default` | `opencode:anthropic/claude-sonnet-4-5` | |
|
|
81
|
+
| `fallbacks` | `[]` | tried in order when an unpinned issue's model does not answer a probe |
|
|
82
|
+
| `premium` | `[]` | models that run only on issues approved with `nohalt route <n> <model> --approve` |
|
|
83
|
+
| `premium_fallback` | `""` | what a non-approved request for a premium model gets (default: `default`) |
|
|
84
|
+
| `probe` | `true` | ask the model for "OK" before launching. A dead endpoint otherwise kills every agent in its first second |
|
|
85
|
+
| `probe_ttl` | `"5m"` | probe results are cached this long |
|
|
86
|
+
| `[[models.route]]` | `[]` | `{ labels = [...], model = "..." }` or `{ title = "regex", model = "..." }`. The first match wins |
|
|
87
|
+
|
|
88
|
+
Pins (`nohalt route <issue> <model>`) beat rules and are never failed over: a pinned issue waits for its model.
|
|
89
|
+
|
|
90
|
+
## [budget]
|
|
91
|
+
|
|
92
|
+
| Key | Default | Meaning |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `enabled` | `false` | |
|
|
95
|
+
| `per_issue` | `15.0` | USD. Over it, the issue moves to `fallback_model` |
|
|
96
|
+
| `total` | `100.0` | USD across all paid issues. Once reached, every paid issue moves |
|
|
97
|
+
| `providers` | `["anthropic"]` | model id prefixes that are billed per token |
|
|
98
|
+
| `fallback_model` | `""` | where over-budget issues go. Empty: stop and block them |
|
|
99
|
+
| `rates` | Sonnet list prices | USD per million tokens: `input`, `output`, `cache_read`, `cache_write`. **Calibrate against your console**: estimates are routinely off by a factor until measured |
|
|
100
|
+
|
|
101
|
+
Spend is estimated from each opencode agent's own session database, keeping the high-water mark per issue (pruning session data never lowers the total).
|
|
102
|
+
|
|
103
|
+
## [verify]
|
|
104
|
+
|
|
105
|
+
| Key | Default | Meaning |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `slots` | `["default"]` | names, or `{ name, env = {...} }`. One verification per slot at a time |
|
|
108
|
+
| `placeholder` | `""` | a token in agents' commands replaced by the slot's `SITE` (the frappe stack sets it) |
|
|
109
|
+
| `refuse` | `[]` | substrings a verify command may not contain. Refusals are logged |
|
|
110
|
+
| `up` / `reset` / `env` | `""` | shell hooks (generic stack), see [STACKS.md](STACKS.md) |
|
|
111
|
+
| `pythonpath_worktree` | `false` | put the worktree first on `PYTHONPATH` (the frappe stack sets it) |
|
|
112
|
+
| `clean_pycache` | `true` | delete stale bytecode in the worktree before running |
|
|
113
|
+
|
|
114
|
+
## [integrate]
|
|
115
|
+
|
|
116
|
+
| Key | Default | Meaning |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| `mode` | `"none"` | `none` (a human merges), `direct`, `train`. Agents never merge in any mode |
|
|
119
|
+
| `required_checks` | `[]` | check-run names (GitHub) or job names in the newest pipeline (GitLab) that must pass. Empty: every check must pass, and at least one must exist. On GitLab, a job with `allow_failure` never makes a commit red, and a manual job counts as skipped |
|
|
120
|
+
| `merge_method` | `"squash"` | for `direct` (release PRs always merge with a merge commit). On GitLab, `squash` squashes; otherwise the project's merge method applies |
|
|
121
|
+
| `ci_workflow` / `cd_workflow` | `""` | runs watched on the base branch after a merge: GitHub workflow names, or on GitLab a pipeline's `workflow:name` (or `"pipeline"` for any pipeline on the commit) |
|
|
122
|
+
| `cd_timeout` | `"90m"` | a pipeline that does not finish in time counts as red |
|
|
123
|
+
| `base_pipeline_grace` | `"10m"` | if **no** pipeline appears on the base branch's new head within this time, there is nothing to watch, and merging continues. Many integration branches run no CI after a merge |
|
|
124
|
+
| `deploy` | `""` | shell command after a green pipeline (`direct`, when no CD workflow deploys) or a merged release (`train`). A failure writes `DEPLOY_BLOCKED` |
|
|
125
|
+
| `fix_forward` | `true` | a red pipeline opens an issue at the front of the queue and stops merging until it closes |
|
|
126
|
+
| `cancel_superseded` | `false` | cancel queued or running CI for commits no open PR or the base points at |
|
|
127
|
+
| `network_errors` | DNS/reset patterns | failed-log substrings that mark a red as the network's, not the code's |
|
|
128
|
+
| `mergers` | `{}` | glob → driver, see [STACKS.md](STACKS.md#merge-drivers-any-stack) |
|
|
129
|
+
|
|
130
|
+
```toml
|
|
131
|
+
[integrate.train]
|
|
132
|
+
branch_prefix = "release/"
|
|
133
|
+
version_files = [{ path = "pkg/__init__.py", pattern = '__version__ = "([^"]+)"' }] # group 1 is replaced
|
|
134
|
+
tag_prefix = "v"
|
|
135
|
+
after_version = "python scripts/regenerate_reference.py" # run in the release tree after stamping
|
|
136
|
+
red_retries = 1
|
|
137
|
+
batch_pause_file = "BATCH_PAUSE" # touch <state>/BATCH_PAUSE to hold new batches (expires after 1 h)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`<state>/priority` and `<state>/hold` (one issue per line) put issues first in, or keep them out of, the next batch.
|
|
141
|
+
|
|
142
|
+
## [stack]
|
|
143
|
+
|
|
144
|
+
`name = "generic"` or `"frappe"`. Other keys are the stack's options (frappe: `bench`, `app`, `sites`, `full_suite_guard`, `mergers`). See [STACKS.md](STACKS.md).
|
|
145
|
+
|
|
146
|
+
## [prompt]
|
|
147
|
+
|
|
148
|
+
| Key | Default | Meaning |
|
|
149
|
+
|---|---|---|
|
|
150
|
+
| `inline_issue` | `false` | put the issue's text and comments in the prompt, so agents don't need a working forge CLI to read it |
|
|
151
|
+
| `template` | `"builtin:core"` | the core prompt: `builtin:<name>` or a file |
|
|
152
|
+
| `extra` | `[]` | your project's own prompt files, appended in order |
|
|
153
|
+
| `vars` | `{}` | extra `{{PLACEHOLDERS}}`. **Never put secrets here**: prompts end up in logs |
|
|
154
|
+
|
|
155
|
+
Placeholders available: `{{ISSUE}} {{TITLE}} {{REPO}} {{BRANCH}} {{WORKTREE}} {{BASE}} {{VERIFY}}`, `{{SITE}}` when `verify.placeholder` is set, plus `vars`. An undefined placeholder is an error, not an empty string. Run `nohalt prompt <issue>` to see exactly what an agent will be told.
|
|
156
|
+
|
|
157
|
+
## Example: self-hosted GitLab
|
|
158
|
+
|
|
159
|
+
Work lands on an integration branch, and humans promote it to the default branch, which deploys. CI on merge requests is a short validation job, and the instance itself doesn't require pipelines to pass.
|
|
160
|
+
|
|
161
|
+
```toml
|
|
162
|
+
[project]
|
|
163
|
+
repo = "team/app" # group[/subgroup]/name
|
|
164
|
+
forge = "gitlab"
|
|
165
|
+
forge_url = "https://gitlab.example.com"
|
|
166
|
+
token_env = "NOHALT_GITLAB_TOKEN" # a project access token (Developer, api), not a person's token
|
|
167
|
+
base_branch = "develop" # NOT the default branch that deploys to production
|
|
168
|
+
|
|
169
|
+
[queue]
|
|
170
|
+
blocked_label = "blocked" # reuse the label humans already use
|
|
171
|
+
held_labels = ["in-progress", "Draft"] # matched case-insensitively
|
|
172
|
+
|
|
173
|
+
[integrate]
|
|
174
|
+
mode = "direct"
|
|
175
|
+
required_checks = ["validate"] # nohalt enforces this itself, even if GitLab doesn't
|
|
176
|
+
merge_method = "merge" # match the project's merge method
|
|
177
|
+
base_pipeline_grace = "5m" # develop runs no pipeline after a merge: don't wait for one
|
|
178
|
+
|
|
179
|
+
[prompt]
|
|
180
|
+
inline_issue = true # agents read the issue even if glab can't reach the host
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
If GitLab is configured to close issues only for merges into the default branch, nohalt closes them itself after a merge into `develop`.
|