@uchibeke/gallivant 0.0.0-stage → 0.2.0
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.
- package/CONTRIBUTING.md +26 -0
- package/LICENSE +204 -0
- package/README.md +201 -2
- package/SECURITY.md +19 -0
- package/bin/gallivant.mjs +257 -0
- package/bin/package-plugin.mjs +16 -0
- package/bin/release-live.mjs +29 -0
- package/bin/release-package.mjs +23 -0
- package/bin/session-runtime.mjs +13 -0
- package/bin/task-delivery-state.mjs +12 -0
- package/docs/README.md +70 -0
- package/docs/agent-setup.md +214 -0
- package/docs/app.md +111 -0
- package/docs/architecture.md +265 -0
- package/docs/chains.md +102 -0
- package/docs/codex-cloud.md +284 -0
- package/docs/commands.md +620 -0
- package/docs/config.md +430 -0
- package/docs/execution.md +796 -0
- package/docs/guardrails.md +311 -0
- package/docs/install.md +338 -0
- package/docs/launch.md +42 -0
- package/docs/learning.md +369 -0
- package/docs/network-recovery.md +101 -0
- package/docs/openai-agents.md +457 -0
- package/docs/prd.md +246 -0
- package/docs/preparation.md +110 -0
- package/docs/release-qualification.md +196 -0
- package/docs/releasing.md +140 -0
- package/docs/runtime-environment.md +113 -0
- package/docs/task-delivery.md +476 -0
- package/docs/tasks.md +739 -0
- package/lib/aport.mjs +95 -0
- package/lib/attribution.mjs +95 -0
- package/lib/block.mjs +89 -0
- package/lib/capabilities.mjs +3 -0
- package/lib/chains.mjs +222 -0
- package/lib/codex_cloud.mjs +160 -0
- package/lib/codex_cloud_config.mjs +20 -0
- package/lib/codex_cloud_input.mjs +49 -0
- package/lib/codex_cloud_result.mjs +74 -0
- package/lib/commands/doctor.mjs +457 -0
- package/lib/commands/init.mjs +34 -0
- package/lib/commands/learn.mjs +54 -0
- package/lib/commands/new.mjs +47 -0
- package/lib/commands/ready.mjs +45 -0
- package/lib/commands/register.mjs +195 -0
- package/lib/commands/resume.mjs +224 -0
- package/lib/commands/serve.mjs +349 -0
- package/lib/commands/start.mjs +247 -0
- package/lib/commands/start_services.mjs +22 -0
- package/lib/commands/status.mjs +150 -0
- package/lib/commands/task.mjs +73 -0
- package/lib/commands/up.mjs +228 -0
- package/lib/commands/why.mjs +143 -0
- package/lib/config.mjs +461 -0
- package/lib/context.mjs +100 -0
- package/lib/control.mjs +202 -0
- package/lib/control_contract.mjs +16 -0
- package/lib/coordinator.mjs +19 -0
- package/lib/credential.mjs +161 -0
- package/lib/daemon.mjs +156 -0
- package/lib/deliverable.mjs +357 -0
- package/lib/events.mjs +261 -0
- package/lib/executable.mjs +31 -0
- package/lib/execution.mjs +631 -0
- package/lib/execution_contract.mjs +15 -0
- package/lib/execution_input.mjs +4 -0
- package/lib/execution_io.mjs +94 -0
- package/lib/execution_journal.mjs +69 -0
- package/lib/execution_preparation.mjs +29 -0
- package/lib/execution_result.mjs +266 -0
- package/lib/fs.mjs +133 -0
- package/lib/gh.mjs +289 -0
- package/lib/git.mjs +434 -0
- package/lib/harness.mjs +521 -0
- package/lib/harness_log.mjs +432 -0
- package/lib/hash.mjs +16 -0
- package/lib/identity.mjs +98 -0
- package/lib/learning.mjs +654 -0
- package/lib/learning_contract.mjs +2 -0
- package/lib/learning_data.mjs +135 -0
- package/lib/learning_input.mjs +111 -0
- package/lib/learning_qualification.mjs +67 -0
- package/lib/learning_signer.mjs +151 -0
- package/lib/learning_store.mjs +88 -0
- package/lib/log.mjs +53 -0
- package/lib/network_diagnostics.mjs +64 -0
- package/lib/network_recovery.mjs +42 -0
- package/lib/notify.mjs +58 -0
- package/lib/openai_agents.mjs +373 -0
- package/lib/openai_agents_config.mjs +14 -0
- package/lib/openai_agents_http.mjs +87 -0
- package/lib/openai_agents_input.mjs +15 -0
- package/lib/openai_agents_sources.mjs +112 -0
- package/lib/owner_delivery.mjs +443 -0
- package/lib/plugin_package.mjs +340 -0
- package/lib/prd.mjs +635 -0
- package/lib/preparation_agents.mjs +33 -0
- package/lib/preparation_codex.mjs +212 -0
- package/lib/probe.mjs +126 -0
- package/lib/prompt.mjs +238 -0
- package/lib/provider_health.mjs +55 -0
- package/lib/public-format.mjs +31 -0
- package/lib/public_output.mjs +49 -0
- package/lib/release_package.mjs +154 -0
- package/lib/repository_files.mjs +86 -0
- package/lib/run.mjs +385 -0
- package/lib/run_log.mjs +94 -0
- package/lib/runtime-environment.mjs +87 -0
- package/lib/runtime_dependencies.mjs +15 -0
- package/lib/runtime_node.mjs +107 -0
- package/lib/runtime_state.mjs +174 -0
- package/lib/sources/index.mjs +27 -0
- package/lib/sources/prd.mjs +50 -0
- package/lib/start_contract.mjs +37 -0
- package/lib/state.mjs +246 -0
- package/lib/task_allowance.mjs +31 -0
- package/lib/task_completion.mjs +61 -0
- package/lib/task_contract.mjs +450 -0
- package/lib/task_delivery.mjs +219 -0
- package/lib/task_delivery_cancel.mjs +34 -0
- package/lib/task_delivery_contract.mjs +61 -0
- package/lib/task_delivery_mcp.mjs +40 -0
- package/lib/task_delivery_store.mjs +43 -0
- package/lib/task_delivery_view.mjs +88 -0
- package/lib/task_environment.mjs +281 -0
- package/lib/task_prepare.mjs +224 -0
- package/lib/task_store.mjs +38 -0
- package/lib/tasks.mjs +513 -0
- package/lib/tick.mjs +1635 -0
- package/lib/time.mjs +36 -0
- package/lib/tracker/assets.mjs +43 -0
- package/lib/tracker/pages.mjs +320 -0
- package/lib/tracker/runs.mjs +102 -0
- package/lib/tracker/stream.mjs +74 -0
- package/lib/tracker_process.mjs +107 -0
- package/lib/verification_sandbox.mjs +181 -0
- package/node_modules/smol-toml/LICENSE +24 -0
- package/node_modules/smol-toml/README.md +605 -0
- package/node_modules/smol-toml/dist/date.d.ts +41 -0
- package/node_modules/smol-toml/dist/date.js +156 -0
- package/node_modules/smol-toml/dist/error.d.ts +37 -0
- package/node_modules/smol-toml/dist/error.js +67 -0
- package/node_modules/smol-toml/dist/extract.js +289 -0
- package/node_modules/smol-toml/dist/index.cjs +770 -0
- package/node_modules/smol-toml/dist/index.d.ts +44 -0
- package/node_modules/smol-toml/dist/index.js +34 -0
- package/node_modules/smol-toml/dist/parse.d.ts +73 -0
- package/node_modules/smol-toml/dist/parse.js +164 -0
- package/node_modules/smol-toml/dist/primitive.js +151 -0
- package/node_modules/smol-toml/dist/stringify.d.ts +32 -0
- package/node_modules/smol-toml/dist/stringify.js +219 -0
- package/node_modules/smol-toml/dist/struct.js +156 -0
- package/node_modules/smol-toml/dist/util.d.ts +43 -0
- package/node_modules/smol-toml/dist/util.js +57 -0
- package/node_modules/smol-toml/package.json +68 -0
- package/npm-shrinkwrap.json +1823 -0
- package/package.json +105 -4
- package/templates/claude-settings.json +18 -0
- package/templates/passport.gallivant.json +83 -0
- package/templates/plugin/bundled-runtime.json +26 -0
- package/templates/plugin/schemas/mcp.schema.json +120 -0
- package/templates/plugin/schemas/plugin.schema.json +65 -0
- package/templates/plugin/task-delivery.SKILL.md +60 -0
- package/templates/prd.md +32 -0
- package/templates/tracker/tracker.css +301 -0
- package/templates/tracker/tracker.js +721 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Let an agent set up Gallivant
|
|
2
|
+
|
|
3
|
+
Give the prompt below to your coding agent. It can inspect prerequisites, install
|
|
4
|
+
the CLI, create a private config and validate a first task. You handle account
|
|
5
|
+
login and any required trust prompts. Setup does not need access to every private
|
|
6
|
+
repository, and installing the CLI does not start a paid session.
|
|
7
|
+
|
|
8
|
+
Start with **Codex or Claude and one repository**. Those are the harnesses supported
|
|
9
|
+
by `gallivant init`. [Hosted Agents API](openai-agents.md) uses an explicit config
|
|
10
|
+
instead; [other harnesses](../README.md#harness-support) have different setup and
|
|
11
|
+
capability boundaries. An installed binary alone does not establish working model
|
|
12
|
+
access or safe unattended operation.
|
|
13
|
+
|
|
14
|
+
## Prompt to give your agent
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
Set up Gallivant on this machine using its current docs:
|
|
18
|
+
https://github.com/uchibeke/gallivant/blob/main/docs/agent-setup.md
|
|
19
|
+
https://github.com/uchibeke/gallivant/blob/main/docs/install.md
|
|
20
|
+
|
|
21
|
+
Inspect the installed version, operating system, Node architecture, existing
|
|
22
|
+
Gallivant config and target repository before changing anything. Use the actual
|
|
23
|
+
CLI --help and the docs matching the installed version. Prefer an already working
|
|
24
|
+
Codex or Claude setup. Preserve existing credentials, settings, running jobs,
|
|
25
|
+
worktrees and uncommitted changes. Report proposed config changes without secrets.
|
|
26
|
+
|
|
27
|
+
Install/configure the CLI and run the setup checks. If Gallivant is not yet
|
|
28
|
+
published on npm, use the documented source installation. Do not overwrite an
|
|
29
|
+
existing config, weaken security settings, install global hooks, change repository
|
|
30
|
+
protections, sign into accounts or elevate privileges without the relevant user
|
|
31
|
+
authorization. Never ask me to paste API keys or tokens into this conversation.
|
|
32
|
+
Use the provider's login flow or a private credential file instead.
|
|
33
|
+
|
|
34
|
+
Ask only for missing decisions: target repository and checkout, approved base
|
|
35
|
+
branch, preferred available harness, first task scope, and allowed spending.
|
|
36
|
+
A setup request alone does not authorize inference, pushes or PR creation.
|
|
37
|
+
Prepare and validate a written brief without starting it unless I have authorized
|
|
38
|
+
the first run. Plain-language task preparation also uses inference.
|
|
39
|
+
|
|
40
|
+
Finish with a short report: installed version/path, config path, selected harness,
|
|
41
|
+
repository/base, doctor results, reviewer and completion evaluator settings,
|
|
42
|
+
remaining blockers, and the exact command to start the validated first task.
|
|
43
|
+
If I authorized that task, start it once, verify the saved run identity and status,
|
|
44
|
+
and give me its dashboard or SSH-tunnel instructions. Do not claim a running or
|
|
45
|
+
completed task unless observed. Do not merge, publish, deploy or alter accounts.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If the user already provided those decisions and permissions, reuse them. Do not
|
|
49
|
+
ask for the same approval again. This guide does not authorize any particular
|
|
50
|
+
provider, repository, spend or broader machine change by itself.
|
|
51
|
+
|
|
52
|
+
## 1. Inspect before installing
|
|
53
|
+
|
|
54
|
+
Use the user's shell and package manager; no path to a particular person's home
|
|
55
|
+
is required. The coordinator supports macOS and Linux with Node 22 or newer.
|
|
56
|
+
Linux needs `ps` from `procps`/`procps-ng`. Harnesses and hosted artifact verification
|
|
57
|
+
have their own sandbox prerequisites; see [installation](install.md).
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
node --version
|
|
61
|
+
node -p 'process.platform + " " + process.arch'
|
|
62
|
+
npm --version
|
|
63
|
+
git --version
|
|
64
|
+
gh --version
|
|
65
|
+
jq --version
|
|
66
|
+
command -v gallivant
|
|
67
|
+
gh auth status
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A missing command is a prerequisite to fix, not a reason to disable a guardrail.
|
|
71
|
+
Use the distribution's supported package instructions. Check the chosen harness
|
|
72
|
+
version/login using its CLI; `gallivant doctor` performs Gallivant's compatibility
|
|
73
|
+
and configuration checks after setup. Do not print credential files or tokens.
|
|
74
|
+
|
|
75
|
+
Inspect the target checkout separately from the Gallivant installation:
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
cd /path/to/target-repository
|
|
79
|
+
git status --short
|
|
80
|
+
git remote -v
|
|
81
|
+
git branch --show-current
|
|
82
|
+
git config --get user.name
|
|
83
|
+
git config --get user.email
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Replace the path with the actual authorized checkout. Do not reset, clean,
|
|
87
|
+
automatically commit, or switch a dirty checkout. Confirm which GitHub repository
|
|
88
|
+
and base branch the task should use; the current branch is not necessarily the
|
|
89
|
+
approved PR target. Account login and Git identity belong to the user.
|
|
90
|
+
|
|
91
|
+
## 2. Install and initialize
|
|
92
|
+
|
|
93
|
+
Use the [current installation instructions](install.md#1-the-command). Before
|
|
94
|
+
the first npm publication, install from source; `npm install -g @uchibeke/gallivant` is only
|
|
95
|
+
an option after the package is published. Avoid `sudo npm install`; use a
|
|
96
|
+
user-owned Node installation or prefix when global installation is not writable.
|
|
97
|
+
Do not silently replace a different installed version while its daemon is running.
|
|
98
|
+
|
|
99
|
+
After GitHub and the selected harness are signed in:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
gallivant --version
|
|
103
|
+
gallivant --help
|
|
104
|
+
gallivant init --harness codex
|
|
105
|
+
gallivant doctor
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Use `--harness claude` for Claude. `init` is noninteractive; it creates a config
|
|
109
|
+
only when none exists. It does not log in, install hooks or fix permissions.
|
|
110
|
+
If a config exists, skip `init` and preserve its unrelated keys. The normal path
|
|
111
|
+
is `~/.gallivant/config.json`; use `$GALLIVANT_HOME/config.json` if that variable is
|
|
112
|
+
already set. Keep it private (mode `600`). Choose a worktrees directory outside
|
|
113
|
+
the config home, using `init --worktrees /absolute/path` if needed.
|
|
114
|
+
|
|
115
|
+
Before a real run, finish the [harness guardrail setup](install.md#6-aport-on-claude-code)
|
|
116
|
+
for the selected harness. Some steps require human interaction: for example,
|
|
117
|
+
Codex's initial `/hooks` trust prompt cannot be automated. Report such a blocker
|
|
118
|
+
instead of changing the enforcement level or bypassing it.
|
|
119
|
+
|
|
120
|
+
Add the authorized repository to `repos` and its approved base where necessary,
|
|
121
|
+
following [configuration](config.md#the-base-branch). Review the config diff without
|
|
122
|
+
printing credentials. Confirm these decisions before dispatch:
|
|
123
|
+
|
|
124
|
+
| Decision | What must be established |
|
|
125
|
+
|---|---|
|
|
126
|
+
| Repository access | The run can read/push the intended repository; use a selected-repository token where appropriate. Public-only work does not require private-repository access. |
|
|
127
|
+
| Base protections | Required PR/ruleset protections exist. `doctor` suggests repairs; it does not authorize changing organization policy. |
|
|
128
|
+
| Review | The default Codex GitHub reviewer is installed for this repo, another supported reviewer is configured, or the user explicitly chose `reviewer: null` for CI and trusted human feedback. |
|
|
129
|
+
| Completion evaluator | Default APort hosted confirmation needs network access. A local evaluator is an explicit user choice, not an automatic workaround for a failure. |
|
|
130
|
+
| Cost | Confirm `max_iterations`, provider budget controls and allowed spend. `max_budget_usd` is a native per-run dollar cap for Claude; other local harnesses have no equivalent enforced dollar cap. |
|
|
131
|
+
|
|
132
|
+
Run `gallivant doctor` again and report every failure. Do not treat “signed in
|
|
133
|
+
(API unchecked)” as proof of inference access. `doctor` without extra flags checks
|
|
134
|
+
configuration, credentials and repository access without launching a model run.
|
|
135
|
+
`doctor --probe` launches a real Claude sandbox test when Claude is configured;
|
|
136
|
+
`doctor --send` sends a real notification. Use those only when authorized.
|
|
137
|
+
|
|
138
|
+
## 3. Validate a first task without inference
|
|
139
|
+
|
|
140
|
+
In the target checkout, use a new task name that does not already exist:
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
gallivant new first-docs-task
|
|
144
|
+
# Edit docs/prds/first-docs-task.md to describe the agreed task.
|
|
145
|
+
gallivant ready docs/prds/first-docs-task.md --check
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
With no `--harness` flag, `new` uses the first configured harness. If several are
|
|
149
|
+
configured, pass `--harness` with the harness agreed during setup.
|
|
150
|
+
|
|
151
|
+
Fill every required section with repository-specific facts. Pick a small useful
|
|
152
|
+
change, such as documenting existing test commands, with concrete acceptance
|
|
153
|
+
criteria and verification. The template deliberately contains placeholders;
|
|
154
|
+
validation must fail until they are replaced. [The PRD contract](prd.md) lists the
|
|
155
|
+
rules. Expected result: every rule says `ok` and exit status is `0`.
|
|
156
|
+
|
|
157
|
+
`ready --check` reads and validates locally without loading credentials, starting
|
|
158
|
+
a daemon or invoking a model. This is a useful stopping point when setup is
|
|
159
|
+
approved but implementation is not. Do not mark a failing brief ready manually.
|
|
160
|
+
|
|
161
|
+
If paid preparation is authorized, [natural-language task intake](tasks.md) can
|
|
162
|
+
write the brief instead. Use `task inspect` and returned revision/question IDs to
|
|
163
|
+
continue that same task; do not resubmit it simply because clarification is needed.
|
|
164
|
+
|
|
165
|
+
## 4. Start once, when authorized
|
|
166
|
+
|
|
167
|
+
Record readiness, then dispatch the validated brief against the approved branch:
|
|
168
|
+
|
|
169
|
+
```sh
|
|
170
|
+
gallivant ready docs/prds/first-docs-task.md
|
|
171
|
+
gallivant start docs/prds/first-docs-task.md --base YOUR_APPROVED_BASE
|
|
172
|
+
gallivant status --json
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Replace `YOUR_APPROVED_BASE` with the actual branch. `ready` can contact the
|
|
176
|
+
configured completion evaluator and updates the brief; `start` launches paid work
|
|
177
|
+
and permits repository changes, pushes and PR creation within that task.
|
|
178
|
+
It also starts/reuses the daemon and dashboard, and attempts to open the browser.
|
|
179
|
+
Do not launch a second `up` process.
|
|
180
|
+
|
|
181
|
+
For a headless machine, use `start ... --no-open`. Reach the loopback dashboard
|
|
182
|
+
through SSH instead of exposing it publicly:
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
# On your own computer; replace user@host with the coordinator's SSH destination.
|
|
186
|
+
ssh -N -L 8791:127.0.0.1:8791 user@host
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Then open `http://127.0.0.1:8791` locally. Substitute the reported port if configured
|
|
190
|
+
differently. `--no-server` skips the dashboard while keeping task monitoring.
|
|
191
|
+
Keep the coordinator awake and online; there is no automatic reboot/login service
|
|
192
|
+
installer. Startup is not a system service installation.
|
|
193
|
+
|
|
194
|
+
Save the reported task/run handle, worktree and status. If startup returns an error,
|
|
195
|
+
inspect `status` and `why HANDLE` before retrying: work may have started before a
|
|
196
|
+
service or browser failed. Diagnose network problems with
|
|
197
|
+
[the bounded network check](network-recovery.md). Send corrections with
|
|
198
|
+
`say HANDLE --note "..."`; use `resume HANDLE` for a stopped execution and
|
|
199
|
+
`recheck HANDLE` for an eligible PR review check. Neither is a blind retry for
|
|
200
|
+
unknown provider state.
|
|
201
|
+
|
|
202
|
+
`cancel HANDLE` fences further dispatch and requests provider cancellation;
|
|
203
|
+
confirm its status. `down` stops monitoring/dashboard services but **leaves active
|
|
204
|
+
agent processes running**. A ready-for-human result still requires the owner's
|
|
205
|
+
review and merge.
|
|
206
|
+
|
|
207
|
+
## What setup verification proves
|
|
208
|
+
|
|
209
|
+
The package's `npm run test:install` smoke test installs the tarball into a
|
|
210
|
+
throwaway directory, uses fixture CLI credentials, initializes a private config,
|
|
211
|
+
loads the packaged PRD template, and checks an offline brief. It does not use a
|
|
212
|
+
real account, invoke a model or certify a provider's current availability.
|
|
213
|
+
A first authorized end-to-end task is still needed to verify the user's actual
|
|
214
|
+
repository permissions, provider entitlement, sandbox, CI and review integration.
|
package/docs/app.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# The app and the orchestration boundary
|
|
2
|
+
|
|
3
|
+
Gallivant has a local browser dashboard, a CLI, and a shared task-delivery API.
|
|
4
|
+
They use the same task state and review coordinator. The dashboard shows what is
|
|
5
|
+
running, what needs attention and why; you can send a correction or resume an
|
|
6
|
+
existing run without finding its terminal session.
|
|
7
|
+
|
|
8
|
+
For a first run, use [installation](install.md) and [natural-language tasks](tasks.md).
|
|
9
|
+
The work can be code, documentation, research or another verifiable repository
|
|
10
|
+
artifact. GitHub pull requests remain the delivery surface; arbitrary browser
|
|
11
|
+
chores, email delivery and deployments are not separate supported task targets.
|
|
12
|
+
|
|
13
|
+
## Start the dashboard
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
gallivant start
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This ensures the daemon and dashboard are running, then opens the browser at
|
|
20
|
+
`http://127.0.0.1:8791`. `gallivant serve` uses the same startup path. Pass
|
|
21
|
+
`--no-open` to keep it in the background, `--no-server` to omit the dashboard,
|
|
22
|
+
or `--no-daemon` when you manage the coordinator separately. See
|
|
23
|
+
[commands](commands.md) for port selection, foreground mode and service ownership.
|
|
24
|
+
The dashboard is a local control surface, not a publicly exposed server.
|
|
25
|
+
|
|
26
|
+
## Work with an existing run
|
|
27
|
+
|
|
28
|
+
The dashboard and run sidebar share the CLI's status model. A run page shows
|
|
29
|
+
its current state, stop reason, PR link, session history and streamed logs.
|
|
30
|
+
History can be loaded without redispatching work. Provider capabilities, saved
|
|
31
|
+
execution state and artifact summaries are exposed through the same tracker;
|
|
32
|
+
reading a page does not poll a provider or incur inference.
|
|
33
|
+
|
|
34
|
+
Run actions have different effects:
|
|
35
|
+
|
|
36
|
+
| Action | Effect |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| Recheck PR | Ask the coordinator to refresh GitHub checks/review state and apply its normal readiness rules. |
|
|
39
|
+
| Resume agent | Continue the existing task on its current approved instructions. |
|
|
40
|
+
| Message agent | Send an operator correction to the existing session, including after a PR is ready for review. |
|
|
41
|
+
| Cancel task | Request cancellation through the shared provider-aware control path. |
|
|
42
|
+
|
|
43
|
+
The message composer is separate from the fixed actions. Normally a message waits
|
|
44
|
+
for active work to settle. Local providers with interrupt support offer an explicit
|
|
45
|
+
interrupt option; hosted providers expose only the controls they implement.
|
|
46
|
+
Requests are queued, and the daemon must be running to apply them. Messages keep
|
|
47
|
+
the brief, permissions and consumed budget; they do not authorize a new scope,
|
|
48
|
+
merge or deployment. Changing the delivery contract requires the explicit
|
|
49
|
+
resnapshot flow. Closed or merged PRs require a new task.
|
|
50
|
+
|
|
51
|
+
The current dashboard controls existing registered runs. Prepare new work through
|
|
52
|
+
`gallivant task` or `new` → `ready` → `start`. The exported
|
|
53
|
+
[task-delivery package](task-delivery.md) lets an authenticated host provide its own
|
|
54
|
+
intake UI/MCP surface without implementing another runner. That package is not
|
|
55
|
+
evidence that a hosted service or plugin integration is deployed.
|
|
56
|
+
|
|
57
|
+
## The browser boundary
|
|
58
|
+
|
|
59
|
+
GET routes read saved state. One `POST /api/run/control` route queues a bounded
|
|
60
|
+
operator request under the shared mutation lock; the daemon performs the action.
|
|
61
|
+
The POST requires a loopback peer, a loopback Host with the actual serving port,
|
|
62
|
+
an exact same-origin header and the server's CSRF token. Remote readers receive
|
|
63
|
+
no control token. There is no arbitrary shell command route.
|
|
64
|
+
|
|
65
|
+
These checks resist cross-origin requests and DNS rebinding; they are not
|
|
66
|
+
multi-user authentication or isolation from another process running as the same
|
|
67
|
+
OS user. Keep the standalone tracker on loopback. A server mounting the tracker
|
|
68
|
+
owns its authentication and bind address. Do not expose it through an unauthenticated
|
|
69
|
+
proxy. CSP, HTML escaping and text-only log insertion protect rendering of
|
|
70
|
+
repository, reviewer and agent-controlled text.
|
|
71
|
+
|
|
72
|
+
`mount(ctx, {base})` lets an existing dashboard, including Command Center, delegate
|
|
73
|
+
a path prefix. The tracker provides SSE logs with a bounded initial tail,
|
|
74
|
+
`Last-Event-ID` continuation and explicit history gaps. The CLI, standalone UI and
|
|
75
|
+
mounted UI retain one coordinator and one set of run identities.
|
|
76
|
+
|
|
77
|
+
## Local and hosted harnesses
|
|
78
|
+
|
|
79
|
+
Five local CLI adapters and OpenAI Agents API are implemented. Claude, Codex and
|
|
80
|
+
Agents API also have bounded natural-language preparation. Codex Cloud has a
|
|
81
|
+
transport but new dispatch is unavailable until the chosen environment can be
|
|
82
|
+
verified against the intended repository. [Configuration](config.md#execution-providers)
|
|
83
|
+
and the provider guides distinguish implemented capability, account access and
|
|
84
|
+
live qualification.
|
|
85
|
+
|
|
86
|
+
A hosted harness moves implementation compute off the device; it does not move
|
|
87
|
+
Gallivant's coordinator, GitHub publication or local verification. If this machine
|
|
88
|
+
sleeps, orchestration waits for it to return. Running the coordinator on a Node
|
|
89
|
+
host you operate requires its durable state and appropriate verification isolation.
|
|
90
|
+
Gallivant does not currently sell or provision that host for you.
|
|
91
|
+
|
|
92
|
+
Provider credentials and models belong to the selected harness/provider setup.
|
|
93
|
+
A subscription, API key or dashboard connection is not automatically usable by a
|
|
94
|
+
different transport. Costs depend on provider pricing, caching, task duration and
|
|
95
|
+
review rounds; Gallivant does not promise that API use is cheaper than a subscription.
|
|
96
|
+
See [execution](execution.md) for cost visibility and limits.
|
|
97
|
+
|
|
98
|
+
## Product decisions and future work
|
|
99
|
+
|
|
100
|
+
Gallivant owns the delivery contract, session ownership, retry bounds, review
|
|
101
|
+
feedback and completion evidence. Harnesses own tool execution and context
|
|
102
|
+
management. Adding a provider extends that boundary; it does not add a second
|
|
103
|
+
agent loop. Thin CLI adapters and durable hosted transports share the same review
|
|
104
|
+
state machine even though their execution details differ.
|
|
105
|
+
|
|
106
|
+
Earlier product exploration proposed a native desktop wrapper, managed hosting,
|
|
107
|
+
per-round harness routing and bundled provider plans. These remain possible
|
|
108
|
+
future directions, not capabilities enabled by this release. A desktop shell
|
|
109
|
+
would add conveniences such as an application icon; the browser dashboard already
|
|
110
|
+
provides run controls. Any future routing must preserve explicit provider consent,
|
|
111
|
+
authority, continuity and budget ownership rather than silently switching a run.
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
Gallivant separates task intent, execution and delivery. Natural-language intake
|
|
4
|
+
prepares a canonical brief, or an operator supplies one directly. One coordinator
|
|
5
|
+
starts implementation, watches GitHub reviews/checks, resumes corrections and
|
|
6
|
+
checks completion evidence. Code, documentation and research use the same model
|
|
7
|
+
when their outputs are reviewable repository artifacts. Integration remains a
|
|
8
|
+
human merge decision.
|
|
9
|
+
|
|
10
|
+
The capability registry includes five local CLI adapters and hosted OpenAI Agents
|
|
11
|
+
API. Codex Cloud is recognized but new dispatch is blocked on environment/repository
|
|
12
|
+
verification. [Execution](execution.md) describes durable transport ownership;
|
|
13
|
+
[preparation](preparation.md) describes the narrower drafting authority. Hosted
|
|
14
|
+
agent compute does not remove the local/Node coordinator or its durable state.
|
|
15
|
+
|
|
16
|
+
Repository review learning uses one coordinator-owned [learning service](learning.md)
|
|
17
|
+
for CLI and daemon observation, candidate dispositions, signing, transition recovery
|
|
18
|
+
and bounded shared inputs. Its state lives outside checkouts under the operator home;
|
|
19
|
+
coding-review cursors and budgets remain separate. Canonical JSON is authority,
|
|
20
|
+
escaped review text is a view, and only exact enrolled OpenSSH signatures authorize
|
|
21
|
+
transitions. Installed execution isolation remains UNQUALIFIED, so production
|
|
22
|
+
setup/extraction/application refuse activation. The [carried design](https://github.com/uchibeke/gallivant/blob/main/docs/plans/review-feedback-learning.md)
|
|
23
|
+
remains normative; the [qualification record](https://github.com/uchibeke/gallivant/blob/main/docs/audit/learning-qualification.md) lists gaps.
|
|
24
|
+
Shared inputs carry validated task/event paths for scope selection. Explicit fresh
|
|
25
|
+
learning continuity records its intent in the registry and execution boundary,
|
|
26
|
+
retaining task history and cumulative allowance while fencing the old session.
|
|
27
|
+
|
|
28
|
+
One actor, one file of rules, everything else is plumbing. Plain ESM; one bundled TOML parser handles isolated Codex configuration without inventing another config parser.
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
bin/gallivant.mjs CLI: parse args, build the context, call one command
|
|
32
|
+
lib/context.mjs wires the real dependencies once (config, state, gh, git, notify, log, runner)
|
|
33
|
+
|
|
34
|
+
lib/tick.mjs the state machine. Settle runs, fetch, terminal states, events, dispatch, ready, silence.
|
|
35
|
+
lib/chains.mjs approved linear plans; dispatch after confirmed merges through the same start path
|
|
36
|
+
lib/start_contract.mjs shared start validation, exact input fingerprints and carried-file preservation
|
|
37
|
+
lib/events.mjs pure rules: what counts as an event, trust, reviewer verdict, green checks, round counting
|
|
38
|
+
lib/prompt.mjs start, register and fix prompts; quoting and caps
|
|
39
|
+
lib/harness.mjs capability registry plus local argv, session, outcome and sign-in adapters
|
|
40
|
+
lib/tasks.mjs natural-language intake, revisions, validation, readiness and explicit execution consent
|
|
41
|
+
lib/task_delivery.mjs authenticated host operation boundary; delegates to the same task service
|
|
42
|
+
lib/execution.mjs durable local/remote dispatch, ownership, observations and recovery
|
|
43
|
+
lib/execution_result.mjs owned hosted artifact verification and coordinator publication
|
|
44
|
+
lib/control.mjs shared queued recheck, resume, message and cancellation controls
|
|
45
|
+
lib/block.mjs the PR body block: render, strict parse
|
|
46
|
+
lib/prd.mjs the contract: frontmatter, sections, rules, criteria, Verify commands, and the comparison of
|
|
47
|
+
the daemon's snapshot with the operator's own copy. Takes a Request, not a file
|
|
48
|
+
lib/hash.mjs one fingerprint spelling: twelve hex characters of a sha1, for the receipt and the contract
|
|
49
|
+
lib/sources/ direct brief-source adapters; PRD files today (intake generates the canonical brief)
|
|
50
|
+
lib/deliverable.mjs the completion gate: criteria from the PRD, the receipt, the policy's rules and deny codes
|
|
51
|
+
|
|
52
|
+
lib/public-format.mjs neutral GitHub names and legacy read aliases
|
|
53
|
+
lib/gh.mjs daemon GitHub access through gh; public write validation and status-label migration
|
|
54
|
+
lib/git.mjs per-PRD checkouts (partial clones), the base-branch merge, run environment
|
|
55
|
+
lib/run.mjs detached runs; liveness checks that the pid still runs the harness, adapted from command-center
|
|
56
|
+
lib/state.mjs the registry: atomic writes under a lock file, pure ids, handle resolution
|
|
57
|
+
lib/config.mjs defaults, validation, path expansion
|
|
58
|
+
lib/notify.mjs ntfy messages and the throttled error class
|
|
59
|
+
lib/probe.mjs health checks shared by doctor, start and the daemon
|
|
60
|
+
lib/aport.mjs what APort's installer leaves on disk: the mode file and the audit log, one parser each
|
|
61
|
+
lib/log.mjs, fs.mjs, time.mjs
|
|
62
|
+
|
|
63
|
+
lib/tracker/stream.mjs the SSE log tail: bounded first read, resume by last-event-id, follows the log file
|
|
64
|
+
lib/tracker/runs.mjs the index over ~/.gallivant/runs, and the id checks every route goes through
|
|
65
|
+
lib/tracker/pages.mjs the pages: escaping, the sidebar and its groups, the run detail pane, the CSP
|
|
66
|
+
lib/tracker/assets.mjs templates/tracker/{tracker.css,tracker.js}, read once, plus the ?theme value
|
|
67
|
+
|
|
68
|
+
lib/commands/ start, register, up (daemon + reconcile), status, why, doctor, serve
|
|
69
|
+
test/ node --test; helpers.mjs holds the fakes
|
|
70
|
+
templates/ passport, Claude sandbox settings, PRD, tracker/{tracker.css,tracker.js}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Design rules
|
|
74
|
+
|
|
75
|
+
New intake tasks targeting `delivery_ready` stop automatic observation and fixing
|
|
76
|
+
at the qualified saved handoff. Explicit queued recheck/resume/message controls
|
|
77
|
+
reopen the workflow. `integrated` targets and legacy written-PRD runs retain
|
|
78
|
+
monitoring. `completionStopsAutomation` and the exported `coordinatorEntryActive`
|
|
79
|
+
keep core and host schedulers aligned. The review transitions below apply while
|
|
80
|
+
an entry is monitored; malformed or incomplete delivery proof cannot mark it
|
|
81
|
+
finished or suppress work.
|
|
82
|
+
|
|
83
|
+
Harness implementation and demonstrated support are separate. The [live support matrix](https://github.com/uchibeke/gallivant/blob/main/docs/audit/harness-live-validation.md)
|
|
84
|
+
records installed versions, provider/model combinations, permission boundaries, budget controls and blocked phases.
|
|
85
|
+
Claude/Codex have existing production evidence; Gemini, OpenCode and Goose currently have blocked or failed coding attempts,
|
|
86
|
+
not successful start/resume proof. Provider subscription eligibility is independent of the harness adapter.
|
|
87
|
+
|
|
88
|
+
Repeat with `npm run test:live-harnesses -- --live` after the recorded operator remedies. The opt-in
|
|
89
|
+
`test/live-harnesses.mjs` is outside `test/*.test.mjs`; it uses the existing `buildRun`, `runEnv`, `makeRunner`,
|
|
90
|
+
version probe and outcome/session readers. Test-local native model overrides do not add per-task model selection
|
|
91
|
+
to Gallivant. `test/live-support.mjs` verifies model evidence, adapter identity, actual test tool execution, files,
|
|
92
|
+
behavior, commits, upstream and remote HEAD. Calls are sequential, at most 180 seconds per turn and 30 minutes
|
|
93
|
+
per suite, with one start/resume pair and at most one justified targeted retry per combination. Refusals stop
|
|
94
|
+
the affected phase; they are never converted into permission overrides. Ordinary command stderr such as
|
|
95
|
+
`Permission denied`, `Operation not permitted` or `PermissionDenied` is retained for investigation without
|
|
96
|
+
triggering an approval stop. Sandbox classification requires workspace/sandbox context; explicit permission
|
|
97
|
+
requests/rejections and guardrail decisions still stop the turn. Raw streams and exports stay private.
|
|
98
|
+
Recognized unsuccessful native outcomes (such as an aborted OpenCode step) are recorded as
|
|
99
|
+
`native-failure`, not adapter defects. The retry gate checks both saved outcome metadata and the
|
|
100
|
+
current parser's reading of the saved stream; a source change cannot qualify these failures for retry.
|
|
101
|
+
Goose's textual provider-error prefix must start an assistant message/content block. The adapter reads
|
|
102
|
+
across streamed chunks from those boundaries, preserving native failures after earlier output without
|
|
103
|
+
treating error examples embedded in prose or code fences as failed turns. Typed native errors remain failures.
|
|
104
|
+
Named Meta/xAI/Alibaba cases use Goose's native saved providers and OpenCode's separate Console BYOK
|
|
105
|
+
providers; they do not establish Zen/Go subscription access. Exact batch commands and blocked results are
|
|
106
|
+
in the audit. DeepSeek is excluded from new calls and Tencent remains deferred.
|
|
107
|
+
|
|
108
|
+
Every OpenCode turn pins `--model` to the case's recorded provider/model, including the existing-default
|
|
109
|
+
case. An unknown, incomplete or routing-alias default blocks before fixture creation or launch. Changing
|
|
110
|
+
native defaults afterward cannot change that pinned argument, and exported model evidence must match the
|
|
111
|
+
recorded selection on both turns. Named Console providers keep their exact selectors.
|
|
112
|
+
Goose likewise requires known provider and model selectors before fixture creation or turn reservation;
|
|
113
|
+
both are always passed explicitly. Missing or unparseable settings block without falling back to its
|
|
114
|
+
mutable default. Native environment/settings and the named Meta/xAI/Alibaba selectors remain supported.
|
|
115
|
+
|
|
116
|
+
Disposable turns clear inherited Git/GitHub/SSH, credential-manager and editor askpass variables, then
|
|
117
|
+
restore `runEnv`'s Git identity and isolated config with terminal credential prompts disabled. Git accepts only
|
|
118
|
+
file transport and its SSH command refuses execution, preventing default SSH identity fallback while keeping
|
|
119
|
+
provider `HOME` intact. Disposable environments override `GH_CONFIG_DIR` with `/dev/null` and disable gh
|
|
120
|
+
prompts: gh fails configuration loading before stored-account, keyring or API lookup. An empty gh config
|
|
121
|
+
directory alone does not prevent keyring fallback. Production `runEnv` keeps its separate gh configuration.
|
|
122
|
+
Child-environment controls do not replace native sandboxing. Short-lived callers use the
|
|
123
|
+
runner's `waitForKill` after requesting a stop so the process-group fallback runs before exit; the live suite
|
|
124
|
+
also waits before metadata export. Signal/watchdog callbacks defer checkpoints to the main async path;
|
|
125
|
+
failed ledger or runner metadata writes still reference and await the kill fallback before surfacing
|
|
126
|
+
the error. A pending kill owns its exit record, so an early leader exit cannot cause another metadata
|
|
127
|
+
write failure before descendants are stopped. An append can
|
|
128
|
+
reuse a version-blocked case after upgrade only if it has no turn, retry reservation or fixture, within the
|
|
129
|
+
original deadline and with the recorded provider/model unchanged.
|
|
130
|
+
|
|
131
|
+
Runner startup is awaited by initial dispatch, resume/fix dispatch, and the live suite. The runner
|
|
132
|
+
owns its freshly spawned PID until it returns a recorded job; a failed `job.json` or `latest-run`
|
|
133
|
+
write terminates that process group and awaits the existing kill fallback before rejecting. This
|
|
134
|
+
cleanup uses the in-memory job even if disk bookkeeping is missing or truncated. Startup-error
|
|
135
|
+
callbacks cannot repeat failed writes, and the parent's log descriptor closes on every startup path.
|
|
136
|
+
Live turn reservations remain spent when startup bookkeeping fails.
|
|
137
|
+
|
|
138
|
+
The shared runner pins child `PWD` to its requested working directory. OpenCode 1.18.31 prefers that
|
|
139
|
+
environment variable for its tool directory, so changing `spawn.cwd` alone is insufficient. The live suite
|
|
140
|
+
checks native session directory evidence and stops on a mismatch; start fixtures must have no prewritten
|
|
141
|
+
files or remote commits. See the audit's directory incident and recovery record before repeating a failed case.
|
|
142
|
+
An eligible start retry gets a distinct pristine checkout/remote while retaining the original fixture and
|
|
143
|
+
turn evidence; resume retries still require the last passed checkout and remote state. Retry and time limits
|
|
144
|
+
are unchanged, and no historical failure or exhausted retry is relabeled by these fixes.
|
|
145
|
+
|
|
146
|
+
Every turn reserves its case, fixture, source, invocation and running record in the atomic ledger before
|
|
147
|
+
launch, including first starts and resumes. The reservation points to the private runner directory even if
|
|
148
|
+
a crash prevents recording the job ID. A running reservation cannot be appended or retried as an unspent
|
|
149
|
+
case; inspect its private runner evidence and resolve any surviving process before scheduling further work.
|
|
150
|
+
The final deadline check includes reservation I/O and releases the reservation only when no launch occurred.
|
|
151
|
+
|
|
152
|
+
A passing disposable test would still not establish GitHub review/completion behavior. Full support requires
|
|
153
|
+
an authorized PRD through `ready`/`start`, PR creation, review/CI feedback, a same-session pushed fix and the
|
|
154
|
+
completion gate. Keep that lifecycle evidence separate from session fixtures and disposable local Git tests.
|
|
155
|
+
|
|
156
|
+
- **The daemon owns automatic transitions.** Operator commands record starts, controls and approved plans; the tick settles runs, handles reviews and advances chains. Local completion is observed using recorded process ownership and output; remote completion reconciles the exact saved provider job/session/turn. Durable identities allow a coordinator restart without launching duplicate work.
|
|
157
|
+
- **Rules are pure.** `events.mjs` takes the raw shapes `gh` returns and produces events. Every rule in the PRD has a unit test with no network.
|
|
158
|
+
- **One gate, and the brief is a Request.** `check()` takes a source-neutral Request, `{ id, repo, title, body, harness, status, source: { kind, url, author }, raw }`, plus the parsed fields the rules read. A source under `lib/sources/` reads whatever the operator typed and produces one; the direct source adapter accepts a PRD file. Natural-language intake uses the same canonical representation after bounded preparation and validation, rather than adding another completion gate. Two things follow. A rule that depends on where the brief lives rather than on what it says carries its own scope as data (`kinds: ["prd"]`: `path`, and `sources` because it resolves the files a brief carries against the repository root that path gives it), so a source without a path is not failed for a rule it cannot satisfy. And `repo` is a field the source fills in, not something a command derives: `start` reads `request.repo` and never calls `git.toplevel` itself. Adding a source is an adapter plus somewhere to record its verdict, never a second gate.
|
|
159
|
+
- **Dependencies are injected.** `makeTick` receives `gh`, `git`, `runnerFor`, `notify`, `clock`. The test suite builds fakes in `test/helpers.mjs`; the CLI builds real ones in `context.mjs`.
|
|
160
|
+
- **Per-harness knowledge lives with the adapter.** `harness.mjs` owns each CLI's argv, its session id shape (`isSession`), its sign-in check and the command that fixes a failed one (`signIn.fix`). `HARNESSES` in `config.mjs` derives from the shared execution registry, including remote capabilities. A rule about a harness written anywhere else goes stale the moment an adapter is added: a UUID regex in `block.mjs` was refusing goose's and opencode's real session ids, and `doctor` was telling three harnesses to run `codex login`. Everything that can put a session id into argv goes through `isSession`, the block and `register --session` alike, because `assertSafeArgv` matches flag spellings and cannot judge a value in a position it does not expect.
|
|
161
|
+
- **GitHub is the source of truth for identity; the registry for counters.** Both can be rebuilt: identity from the PR block, counters from the daemon's own marked comments (every round posts one, a reset posts one).
|
|
162
|
+
- **Four cursors, one meaning each, and none of them goes backwards.** Monotonicity is enforced in `advanceCursor`, not asked of each caller: three callers pass a boundary from a different source (the newest event dispatched, the head commit's committer date, a local clock reading) and a candidate before the cursor is refused outright, keys and all. `since`: events at or after it are new. `fixed_sha`: the head the loop last acted on, both the head when the last run started, so a push by a fix run is the head moving, and the head a ready entry was judged on, so a push after that reopens it. `checks_sha`: the head whose failing checks were already dispatched. `receipt_hash`: the completion receipt when the last run started. `since` is inclusive because GitHub stamps events to the second, so two that arrive in the same second share a timestamp; `dispatched` rides alongside it with the ids already handled at exactly that instant, and is cleared the moment the cursor moves to another second.
|
|
163
|
+
- **One run per entry is the lock.** A run root per entry means a second dispatch for the same PR is refused by the runner while the first is alive, and entries never block each other.
|
|
164
|
+
- **Every wait is bounded, and every stop has a reason.** Five steps in the tick decline rather than proceed when one GitHub write will not land, because proceeding would leave a worse state than waiting: the ready-label removal before a dispatch, the same removal before a moved head is judged, the identity block write a start run must land before it can leave `running`, the ready-label write that makes a judged PR say a human may merge it, and the round comment a settled round owes the PR. The fifth declines the rest of its tick rather than the round it announces, because the round is a push that already happened; the other four decline work that has not happened yet. All five are right to wait and none of them may wait forever, so `entry.retry` counts the CONSECUTIVE failures of one named write and the entry stops with `stuck` at five, naming the write and what GitHub said. The count resets the moment that write lands, so an entry whose GitHub came back is never stopped for the ticks it spent failing, and a different write failing starts its own count rather than adding to the old one. One mechanism for all five, and for any sixth: the recurring defect in this codebase is the same invariant enforced on one path and not its sibling, so a second counter would be the next finding. What makes a step belong here is not the write but the shape, so the list is expected to grow: the step declines, the entry stays in a state the next tick re-enters, and nothing else about the entry has changed. Two of the five were added a round after the first three, both found by enumerating the writes rather than by a failure report. Five is a constant with a comment in `tick.mjs` rather than a config key: the only question it answers is "transient or permanent", ten minutes at the default `poll_seconds` outlasts a rate-limit window and falls far short of a revoked token, and `max_iterations` is the wrong knob because it counts rounds that cost money where these cost one API call.
|
|
165
|
+
- **The snapshot is authoritative against the session and disputable by the operator, and those are two different rules.** `start` copies the brief into `~/.gallivant/prds/`, and the files it names in `sources` into `~/.gallivant/prds/<id>.sources/`, and the gate reads only those, which is what stops a session widening its own acceptance criteria. It does not stop the operator editing their own copy, and until the comparison existed nothing noticed: a run finished against criteria nobody was asking for, and the PR arrived satisfying a document that had changed. So the two files are compared, in the contract's own terms and no others (ordered criterion IDs/text, Verify commands, harness, context paths, carried-source paths, and any coordinator delivery ownership contract), and the verdict decides the action. A brief edited outside those fields is followed silently, because an entry stopped for a reworded Problem section is an entry whose operator learns to ignore this. A brief whose contract moved stops with `prd-changed` and the specific difference, because ADDED (valid but incomplete) and REWRITTEN (invalidated) are the same event in two files of text and guessing wrong is expensive in both directions. A brief that no longer passes the contract is `prd-illegal` and never reaches the snapshot, since one `start` would have refused is worse behind the gate than one merely out of date. A brief deleted is `prd-gone`. An entry with no recorded checkout, which is every entry `register` and `reconcile` build, compares to `unknown` and carries on, so recovery from GitHub is not made useless by a tree it was never told about.
|
|
166
|
+
|
|
167
|
+
**The verdict is a fingerprint's, and the diff only explains it.** `sameContract` compares `prdFingerprint` over the whole ordered contract; `contractDiff` produces the lines a person reads. Keeping those two jobs apart is the fix for a family of false negative, not tidiness. A diff has to group and set-subtract to be readable, and set arithmetic loses information: a second `- \`npm test\`` in the Verify section leaves every element of both lists present in the other, so the diff is empty while the commands the session must run green have changed. Reordering two criteria is the same shape, and it got its own targeted fix (the criteria are compared as `id: description` pairs, because `criteriaFromPrd` numbers them by position and the receipt on the open pull request attests those numbers, so a swap leaves both ids meaning the other thing and the receipt attesting each criterion with the other's evidence). A targeted fix per shape is a bug per shape nobody has thought of yet, so the verdict asks the one question a hash answers exactly, and a `changed` verdict whose diff came back empty says so in as many words rather than stopping an entry with nothing to show for it.
|
|
168
|
+
|
|
169
|
+
One reason this comparison had to fix `parseFrontmatter`'s line endings on the way past: the regex matched `\n` only, so a CRLF copy of a brief parsed as having no frontmatter at all and fell back to the default harness. That was survivable while `ready` was the only reader, with an operator in front of it. It stops being survivable when the brief is compared against a running entry, where a `.gitattributes` change reads as the harness changing and stops a healthy run blaming the wrong thing.
|
|
170
|
+
|
|
171
|
+
**Where it runs, and why the daemon reads the operator's tree at all.** Two halves, one implementation in `prd.mjs`. The tick owns automatic completion transitions, so a comparison that lived only in `status` and `why` would be a warning while the daemon finished the work anyway. But it does not run every tick on every entry: `entry.checkout` is a path on the operator's machine, and a stat and a parse per entry per poll for something that changes a handful of times per run buys nothing, while an unmounted volume would read as `gone` and stop healthy entries. So the daemon compares at the one point the contract is USED to certify something, immediately before the completion gate, and everything earlier (a fix round for a review, a round counted for a push) proceeds against the brief the session holds, because a dispatch is not a claim that the brief is satisfied. `status`, `why` and `resume` compare on demand, in the operator's own session where reading their checkout is unremarkable and where they are asking the question. **While a `prd-changed` stop is unresolved the gate judges neither brief**: the snapshot certifies one the operator abandoned, the edited copy certifies criteria the session never saw and never attested, and there is no third brief.
|
|
172
|
+
|
|
173
|
+
**A re-snapshot is an explicit act with an audit trail.** `ensurePrdSnapshot` is still the one writer and still refuses to overwrite; what changed is that the refusal is no longer absolute, because a brief the operator has deliberately edited can only become the brief the gate judges by replacing the snapshot. `resolvePrdSnapshot` is the only path to that, reached only through `gallivant resume --resnapshot`, and it warns with the file, who asked and every difference, in the same spirit as the existing line that says the session could have edited the copy it took. `retakePrdSnapshot` shares the writes for the silent case so a third caller cannot invent its own way to put a file in `prds/`. And the fingerprint is `shortHash` in `lib/hash.mjs`, which `receiptHash` now calls too: one spelling of "twelve hex characters of a sha1" rather than two that agree today.
|
|
174
|
+
- **Adapters own the flags.** Local config selects executable/settings paths; remote config selects explicit account, model, environment, network and allowance. Neither is an arbitrary argv escape hatch. CLI adapter changes still pass `assertSafeArgv`, which refuses `--dangerously-*` flags regardless of their source.
|
|
175
|
+
- **Reuse over build.** `gh` for the API, the harness's own resume and no-prompt flags, APort's own installers, ntfy's email header, command-center's runner layout so its log viewer can read a Gallivant run.
|
|
176
|
+
- **One value, resolved in one place.** The base branch is read by three things that must agree: the branch the checkout is cut from, the `gh pr create --base` the session is told to run, and the branch merged in before each round. `resolveBase` in `config.mjs` is the only thing that decides it, from the flag, then the repository's entry in `repos`, then the repository's default branch. Three callers working it out for themselves would agree by luck, and the failure when they stopped agreeing would be a pull request opened against a branch nobody chose.
|
|
177
|
+
- **Automate delivery; humans approve integration.** Chains advance only on a confirmed merge into their approved base. A successful process, `ready-for-human` or `done` is not a dependency signal. The implementation stays a linear queue inside the existing daemon, without automatic merges or stacked branches.
|
|
178
|
+
|
|
179
|
+
## Chains
|
|
180
|
+
|
|
181
|
+
`gallivant chain` records an approved linear plan in the same `state.json` as the run registry: ordered task IDs,
|
|
182
|
+
repository and base, input fingerprints, progress and dispatch ownership. The CLI approves the plan; the daemon
|
|
183
|
+
dispatches its milestones through the ordinary start path. Every milestone is a new task in one repository and on
|
|
184
|
+
one base, with the existing contract, credential, harness and completion gates.
|
|
185
|
+
|
|
186
|
+
A successor waits for fresh GitHub evidence that its predecessor's PR is `MERGED` into the planned base. The base
|
|
187
|
+
used by the new checkout must contain that merge commit before an agent starts. `CLOSED` stops the chain, while an
|
|
188
|
+
ordinary `needs-human` task stays the current milestone and uses existing run recovery. The final merge completes
|
|
189
|
+
the plan. Human review time is an intentional wait, not an iteration to spend or a timeout to work around.
|
|
190
|
+
|
|
191
|
+
Registration reserves future task IDs. A durable start claim is written under the registry's lock before clone or
|
|
192
|
+
spawn, preventing a CLI start from racing a queued dispatch. The claim is cleared atomically when the run entry is
|
|
193
|
+
recorded. No state lock is held across network calls. On recovery, a recorded run can be reconciled; a start whose
|
|
194
|
+
outcome is ambiguous stops visibly instead of risking duplicate work. `--retry` does not discard that claim.
|
|
195
|
+
|
|
196
|
+
Queued PRD and source fingerprints cover the bytes handed to the start path. Changed inputs require explicit
|
|
197
|
+
`chain --retry --resnapshot` approval for unstarted tasks; active tasks retain their normal `resume --resnapshot`
|
|
198
|
+
flow. Carried inputs cannot silently replace base files changed since plan approval. Cancellation prevents future
|
|
199
|
+
dispatch without removing or stopping an active task. See [chains](chains.md) for operator commands.
|
|
200
|
+
|
|
201
|
+
Queued plans cannot be reconstructed from PR metadata: GitHub has no representation of approved work that has not
|
|
202
|
+
started. Existing PR recovery remains unchanged, but preserving `state.json` is necessary to preserve a chain.
|
|
203
|
+
|
|
204
|
+
## The tick, step by step
|
|
205
|
+
|
|
206
|
+
For each registry entry that is not `done`:
|
|
207
|
+
|
|
208
|
+
1. **Settle.** Only the run this entry dispatched (`last.run`) is looked at, ownership first: a live run left behind by a reset or a recovery is ignored rather than waited on and then killed and blamed on the entry. If that run is alive and past `run_timeout_minutes`, kill it and stop with `timeout`. If a run ended and is not yet recorded: for a `running` entry, find the PR on the branch, write the block, enter `pr-open` with the event cursor covering reviews that landed during the start run; or stop with `no-pr`. The block write is a precondition of leaving `running`, not a best-effort extra: it is what recovery rebuilds the entry from once `state.json` is gone, so a failure keeps the entry in `running` with `last.exit` still unrecorded, which is the state the next tick re-enters and retries from. The session is saved on the way out so a retry never mints a second one, and the retry is counted: five consecutive failures of that write stop the entry with `stuck` rather than holding an open PR unwatched in silence, and the stop records the PR number so the message and `gallivant why` can link it. For a `pr-open` entry, the head moving or the receipt changing is a round: bump the iteration, record the run's exit, and note that the round comment is owed. A normal code push moves the feedback cursor to the push. A receipt-only or public-text correction preserves the feedback cursor and its dispatched event ids, so reviews arriving during that run remain eligible for the next dispatch. Its announcement uses the run's start time as a separate deduplication boundary, including the whole start second to match GitHub timestamp precision; markers from earlier seconds cannot discharge the new comment debt. Without a push, the reviewer clock also stays unchanged. Then post the comment, in step 4. A successful run with no head or receipt change returns to watching without charging another round; it does not stop merely for making no push. An unsuccessful run stops with `harness-error`, except for an eligible local transport failure: bounded recovery can resume the same session after a persisted backoff. It reconciles any pushed work first and respects iteration limits, cancellation and queued operator controls. See [network recovery](network-recovery.md) for eligibility, deadlines and retry limits.
|
|
209
|
+
|
|
210
|
+
The order there is the point. What happened is a push: a harness run spent money and moved the branch on GitHub, and nothing that fails afterwards can un-spend it. The comment is the announcement of that fact, and `max_iterations` bounds review rounds, so a counter that waits for the announcement is a bound that can be exceeded. The comment used to be the precondition of counting, which meant a refused comment left the push on GitHub, the fix in the branch and the iteration unchanged. So the local write goes first and the GitHub write goes last, which is the same rule the identity block write follows from the other side: order a pair so the irreversible half is last, and here the irreversible half already happened before the settle ran, which leaves the replayable half to a later tick.
|
|
211
|
+
2. **Fetch.** `gh pr view --json` for state, head, checks, issue comments, labels and body; REST for reviews and inline comments since the cursor, asked for from a second before it so the boundary second comes back whatever GitHub's filter does with it.
|
|
212
|
+
3. **Terminal and review state.** Merged or closed is `done`. A legacy `needs-human (draft)` stop returns to the watched state; other stops only retry their pending status labels. Structured recovery metadata migrates before further processing, preserving prose, evidence and task identity. A draft is converted to a regular PR, with a fresh reviewer window and remote confirmation on the next tick. Conversion failures use bounded retries without starting a paid correction. This does not satisfy any completion gate or clear an unrelated stop. Conflicting is `needs-human`. The base branch is then taken from the PR's own `baseRefName` rather than from anything remembered, because GitHub is the only authority on where a pull request is aimed: it is how an entry written before the field existed gets one, and how a PR someone retargeted from `main` to `staging` starts being merged against the branch it now targets.
|
|
213
|
+
4. **The round comment.** Owed by any round the settle counted and could not announce, posted here on the tick that counted it and again on every later tick until it lands. Idempotent against its marker, so a comment that reached GitHub under a failed response is not posted twice. It is a count on the same `entry.retry` as the four declining writes, for the same reason: a token that can push but cannot comment never recovers, and the markers on the PR are what a recovery rebuilds the iteration from, so an entry whose rounds are invisible to a recovery must not be left running in silence. What it declines is the rest of this tick rather than the round, because the comment is also the re-review request: an entry that owes one has asked the reviewer nothing, so its silence must not be judged and a second round must not be charged before the first is announced.
|
|
214
|
+
5. **Events.** The rules in `events.mjs`. Failing checks count against `checks_sha`, so red CI on a head the loop itself pushed is dispatched once; if it stays red on that head after the fix, the entry stops with `checks-red`.
|
|
215
|
+
6. **Dispatch.** Past the cap, stop with `iterations`. Otherwise recreate the checkout if it is gone, merge the base branch into the work branch, build the fix prompt, resume the session (or start one for a registered PR), move the cursor to the newest event and record the ids dispatched at that instant, record the run.
|
|
216
|
+
|
|
217
|
+
The merge is where a round of work starts from the current base rather than from wherever the base was when the branch was cut, and it is a merge rather than a rebase for three reasons that all live in this codebase. A rebase of a pushed branch can only move with a force-push, which the rules every session is given forbid. `reviewerVerdict` clears a head only when the commit the reviewer named is that head, so a rewrite makes every prior clearance describe a commit that no longer exists, and the reviewer's inline comments are anchored to those same commits. And step 7 reads a head the loop did not put there as a person's push, so a daemon force-push would look to the daemon itself like a stranger pushing. A merge moves the head forward, keeps every reviewed commit an ancestor, and needs no `--force`. An outstanding review therefore does not hold it up, which is the point: a rebase would have had to wait for the review, and that is a deadlock where the reviewer waits for a fix and the fix waits for the reviewer. The merge commit stays local until the session pushes, so nothing on GitHub moves, the reviewer clock is not reset and the round is still counted from the session's own push. It is recorded the moment it succeeds rather than by the save at the end of this step, because four things after it can return early and a merge nobody recorded is a change to a checkout with nothing to explain it. A conflict is aborted and the entry stops with `base-conflict` naming the base, because a half-merged checkout handed to a session as if it were clean is the worst thing this step could leave, and a conflict does not resolve on its own so retrying every tick would loop forever. Four things that would refuse identically on every poll stop the entry instead of looping: a checkout that cannot be recreated (`no-worktree`, with the cause as the event), a harness that refuses the configured `context_paths` (`context-paths`), no GitHub credential (`no-credential`) and no git identity on the machine (`no-identity`, whose event is the two `git config --global` commands that fix it). None of them resolves on its own, and the entry would otherwise redispatch the same feedback every poll with nothing notified. One invariant orders the rest of this step: **a spawned run must be recorded before anything else can fail.** Everything that can throw, the checkout and the ready-label removal included, happens before the spawn, and the registry write that owns the run comes immediately after it with no `await` in between. A process the registry does not know about is the worst state the dispatch can leave, because the settle step reads it as an unrelated leftover and the feedback that caused it is still new, so the same feedback spawns a second fixer when the first one exits. The label removal is the one write that can therefore hold a dispatch back, and holding it back is bounded: five consecutive failures stop the entry with `stuck`. `gh.removeLabel` throws by default and swallows only gh's own "label not found", which is what makes that bound reachable at all: the lenient behaviour was once the default, one of the two callers omitted the flag, and a GitHub that refused the write resolved as though the label had merely been absent.
|
|
218
|
+
7. **Ready.** First, a head nobody told the loop about. Both watching states go through the same check, in one place: `fixed_sha` follows the head, the reviewer clock resets to the push and the nudge becomes available again, because the reviewer has not seen this commit. A `ready` entry also leaves ready with the label off, since its verdict, its green checks and its accepted receipt all describe a commit that is not the head any more. Handling it for `ready` alone used to leave a `pr-open` entry judging the new head with the old head's age, so the same tick that noticed the push could stop it as `reviewer-silent` on a wait that expired before the commit existed, and the nudge already spent on the old head was never reissued. Then, for a `pr-open` entry: no events, no run, not a draft, every check green (none counts as green), reviewer clear or absent. Then the brief, compared with the operator's own copy: this is the only place the daemon reads their working tree, and it is here because this is where the contract is about to be used to certify something. A contract that moved stops the entry with `prd-changed` and the difference named, before the gate runs and without setting `gate_skipped`, which describes the other thing entirely (a PR that reached READY with the gate unrun, where this entry is not reaching ready at all). An edit outside the contract re-takes the snapshot, notifies nobody and carries on. Then the completion gate (`deliverable.mjs`): the PR body's receipt must attest every acceptance criterion of the PRD with evidence; a deny starts a receipt round. A receipt whose shape is wrong is one of those denies, not an exception. The output scan needs the whole diff, so a diff that cannot be read or reaches the 16 MiB scan cap is no decision at all. Large hosted payloads are sent in overlapping pieces within the API request limit, with full criteria and attestations on each; every piece must allow. Unreadable diffs and unavailable evaluators use the same bounded `entry.retry` mechanism as writes, with the cause visible in `status` and `why`; five consecutive failures stop as `needs-human`, without a fixer run or iteration. The label goes on before the registry says ready, so a label write that fails leaves the entry in `pr-open` to be retried rather than a ready PR carrying no label, and those retries are counted on the same `entry.retry` as every other declining write: a token that cannot write labels at all used to repeat the readiness judgement, the completion gate included, on every poll forever, while `status` said the entry was waiting on a reviewer who had already answered. Only then notify, once.
|
|
219
|
+
8. **Silence.** Reviewer configured and quiet since the last request: nudge once after `rereview_minutes`, stop with `reviewer-silent` after `stall_hours`. Draft conversion in step 3 starts a fresh reviewer window, so time spent as a draft does not count as reviewer silence.
|
|
220
|
+
|
|
221
|
+
The reviewer clock (`rereview.at`) is when the reviewer was last asked about the current head, and all three of those timers measure from it. Its best available value is the head commit's committer date, which is when the commit was made and not when it arrived on the branch, so it is floored at `rereview_minutes` ago: a commit made on Monday and pushed on Thursday must not stop the PR for a silence the reviewer was never given a chance to break. The rule is `reviewerClock` in `events.mjs`, pure and in one place because the tick is not its only writer: `reconcile` sets the same field on a recovered entry and goes through the same function, so a PR whose last round comment is older than `stall_hours` is not stopped `reviewer-silent` on the first tick after a crash recovery for silence that accrued while no daemon was running to ask.
|
|
222
|
+
|
|
223
|
+
## The tracker
|
|
224
|
+
|
|
225
|
+
`gallivant start` and `serve` share service startup: daemon, dashboard and browser,
|
|
226
|
+
with explicit opt-outs. Tracker GET routes read the same saved state as the CLI.
|
|
227
|
+
One protected POST queues Recheck PR, Resume agent, Message agent or Cancel task
|
|
228
|
+
through `requestControl`; only the daemon applies the queued action. The POST
|
|
229
|
+
requires loopback peer and Host/port, exact same-origin and a per-server CSRF
|
|
230
|
+
token, plus the shared mutation lock. Reads never drive provider polling. The
|
|
231
|
+
[app guide](app.md) explains the user controls. These rendering rules still hold:
|
|
232
|
+
|
|
233
|
+
- **One model, three renderings.** The dashboard, the sidebar and `/api/status` all come from `model(ctx)` in `status.mjs`, the same function the text view uses. A group that appears in one appears in all of them.
|
|
234
|
+
- **An id is validated against the registry, not by pattern.** Entry ids contain slashes, so a query string is accepted only when `state.get(id)` returns an entry; the path is then rebuilt from `idSlug`. Run ids go through `run.mjs`'s `[A-Za-z0-9_-]` check. Nothing from a URL is ever joined to a path directly.
|
|
235
|
+
- **The log is followed as a file.** The stream watches `job.log` with `fs.watch` and a one-second timer underneath, not the child's pipes, so a browser attached after a daemon restart still sees a run this process never spawned. It sends the last 400 lines on connect with a `skipped` count, carries an `id:` on every frame, and honours `last-event-id`, so a reconnect resumes rather than replaying. `res.setTimeout(0)` on the response, the request and the socket keeps Node's five-minute `requestTimeout` from cutting a long fix round. This is ported from command-center, where each of those was a bug first.
|
|
236
|
+
- **The log renderer is command-center's, ported.** `renderLine` and its parse come from that project's `public/logline.js` unchanged apart from an inlined escaper, because a Gallivant run log is harness output and that is the shape the file was written for: an indented `Tool: argument` line, or prose, with URLs inside either. Tool calls get their own column and colour, the closing line is coloured by outcome. Text is escaped before anything is built from it, so only markup the renderer itself wrote is ever inserted. A fix to those regexes belongs in both copies.
|
|
237
|
+
- **Everything interpolated is escaped, and the page runs no inline script.** PR titles, branch names, reasons, run names and log lines come from GitHub or an agent. The CSP is `default-src 'none'; script-src 'self'; style-src 'self'`, and the stylesheet and script are files under `templates/tracker/` served on their own routes, so there is no nonce to thread through a response and no hash to keep in step. Untrusted text is escaped before the bounded Markdown renderer builds markup; status messages and heartbeat text use `textContent`. Raw stderr and fenced code retain their literal characters.
|
|
238
|
+
- **Grouping is a link, not state.** The sidebar groups by entry state or by harness, chosen with `?by=`, so a grouped view is something you can bookmark or frame and there is nothing to keep in sync between the server and the page. Two options and a small switch, not a grouping engine. Collapsed state is the one thing the page keeps, in `localStorage` per view; a filter ignores it and shows every match expanded, which is the detail that makes a search feel right.
|
|
239
|
+
- **The palette can be forced from outside.** Every colour, font and spacing value is a `--gv-*` custom property with a default. A same-origin parent can override them; a cross-origin parent cannot touch this document at all, and CSS custom properties do not change that, so `?theme=dark|light` is the mechanism for that case. It stamps `data-theme` on the root element, because the stylesheet defaults to dark, while `?theme=light` explicitly selects its light palette; an embedding page does not automatically change it.
|
|
240
|
+
|
|
241
|
+
- **The router is separable from the server.** `handle(ctx, req, res, { base })` is the whole of the routing and `mount(ctx, { base })` wraps it as `{ owns, handle }` for a host server to delegate a prefix to, so the tracker can run standalone on its own port or inside a server somebody else starts. Every URL the pages emit is built from the mount path rather than from the root, and the script reads that path from `<body data-base>` since it is one static file that cannot be told at build time where it lives. A mounted tracker inherits the host's bind address and auth, which is the host's responsibility and not something this module can enforce.
|
|
242
|
+
|
|
243
|
+
Standalone it binds `127.0.0.1` with no account login. Loopback and the POST checks
|
|
244
|
+
protect the browser boundary; they do not authenticate different local OS users
|
|
245
|
+
or constrain another process running as the operator. The mounting host owns its
|
|
246
|
+
authentication and must not expose an unauthenticated tracker to the network.
|
|
247
|
+
|
|
248
|
+
## Adding a harness
|
|
249
|
+
|
|
250
|
+
Register one capability entry in `lib/harness.mjs`; `HARNESSES` derives from that
|
|
251
|
+
registry. A local adapter supplies start/resume argv, environment, session and
|
|
252
|
+
outcome parsing, sign-in/version probes, context support and attach behavior.
|
|
253
|
+
Declare and enforce its minimum version, native permissions and safe argv/env.
|
|
254
|
+
OpenCode is already implemented; unsafe auto-approval flags remain refused.
|
|
255
|
+
|
|
256
|
+
A remote provider implements the [execution transport contract](execution.md),
|
|
257
|
+
including durable acceptance/recovery, account ownership, bounded observations,
|
|
258
|
+
artifact verification and truthful control/cost capabilities. Preparation is an
|
|
259
|
+
optional separate authority boundary, never inferred from implementation support.
|
|
260
|
+
Add regression/installed fixtures and record actual live qualification separately;
|
|
261
|
+
a successful version probe alone cannot establish unattended support.
|
|
262
|
+
|
|
263
|
+
## Adding a reviewer
|
|
264
|
+
|
|
265
|
+
No code. Set `reviewer.login`, `reviewer.rereview_comment` and `reviewer.blocking_pattern` in config.
|