@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.
Files changed (168) hide show
  1. package/CONTRIBUTING.md +26 -0
  2. package/LICENSE +204 -0
  3. package/README.md +201 -2
  4. package/SECURITY.md +19 -0
  5. package/bin/gallivant.mjs +257 -0
  6. package/bin/package-plugin.mjs +16 -0
  7. package/bin/release-live.mjs +29 -0
  8. package/bin/release-package.mjs +23 -0
  9. package/bin/session-runtime.mjs +13 -0
  10. package/bin/task-delivery-state.mjs +12 -0
  11. package/docs/README.md +70 -0
  12. package/docs/agent-setup.md +214 -0
  13. package/docs/app.md +111 -0
  14. package/docs/architecture.md +265 -0
  15. package/docs/chains.md +102 -0
  16. package/docs/codex-cloud.md +284 -0
  17. package/docs/commands.md +620 -0
  18. package/docs/config.md +430 -0
  19. package/docs/execution.md +796 -0
  20. package/docs/guardrails.md +311 -0
  21. package/docs/install.md +338 -0
  22. package/docs/launch.md +42 -0
  23. package/docs/learning.md +369 -0
  24. package/docs/network-recovery.md +101 -0
  25. package/docs/openai-agents.md +457 -0
  26. package/docs/prd.md +246 -0
  27. package/docs/preparation.md +110 -0
  28. package/docs/release-qualification.md +196 -0
  29. package/docs/releasing.md +140 -0
  30. package/docs/runtime-environment.md +113 -0
  31. package/docs/task-delivery.md +476 -0
  32. package/docs/tasks.md +739 -0
  33. package/lib/aport.mjs +95 -0
  34. package/lib/attribution.mjs +95 -0
  35. package/lib/block.mjs +89 -0
  36. package/lib/capabilities.mjs +3 -0
  37. package/lib/chains.mjs +222 -0
  38. package/lib/codex_cloud.mjs +160 -0
  39. package/lib/codex_cloud_config.mjs +20 -0
  40. package/lib/codex_cloud_input.mjs +49 -0
  41. package/lib/codex_cloud_result.mjs +74 -0
  42. package/lib/commands/doctor.mjs +457 -0
  43. package/lib/commands/init.mjs +34 -0
  44. package/lib/commands/learn.mjs +54 -0
  45. package/lib/commands/new.mjs +47 -0
  46. package/lib/commands/ready.mjs +45 -0
  47. package/lib/commands/register.mjs +195 -0
  48. package/lib/commands/resume.mjs +224 -0
  49. package/lib/commands/serve.mjs +349 -0
  50. package/lib/commands/start.mjs +247 -0
  51. package/lib/commands/start_services.mjs +22 -0
  52. package/lib/commands/status.mjs +150 -0
  53. package/lib/commands/task.mjs +73 -0
  54. package/lib/commands/up.mjs +228 -0
  55. package/lib/commands/why.mjs +143 -0
  56. package/lib/config.mjs +461 -0
  57. package/lib/context.mjs +100 -0
  58. package/lib/control.mjs +202 -0
  59. package/lib/control_contract.mjs +16 -0
  60. package/lib/coordinator.mjs +19 -0
  61. package/lib/credential.mjs +161 -0
  62. package/lib/daemon.mjs +156 -0
  63. package/lib/deliverable.mjs +357 -0
  64. package/lib/events.mjs +261 -0
  65. package/lib/executable.mjs +31 -0
  66. package/lib/execution.mjs +631 -0
  67. package/lib/execution_contract.mjs +15 -0
  68. package/lib/execution_input.mjs +4 -0
  69. package/lib/execution_io.mjs +94 -0
  70. package/lib/execution_journal.mjs +69 -0
  71. package/lib/execution_preparation.mjs +29 -0
  72. package/lib/execution_result.mjs +266 -0
  73. package/lib/fs.mjs +133 -0
  74. package/lib/gh.mjs +289 -0
  75. package/lib/git.mjs +434 -0
  76. package/lib/harness.mjs +521 -0
  77. package/lib/harness_log.mjs +432 -0
  78. package/lib/hash.mjs +16 -0
  79. package/lib/identity.mjs +98 -0
  80. package/lib/learning.mjs +654 -0
  81. package/lib/learning_contract.mjs +2 -0
  82. package/lib/learning_data.mjs +135 -0
  83. package/lib/learning_input.mjs +111 -0
  84. package/lib/learning_qualification.mjs +67 -0
  85. package/lib/learning_signer.mjs +151 -0
  86. package/lib/learning_store.mjs +88 -0
  87. package/lib/log.mjs +53 -0
  88. package/lib/network_diagnostics.mjs +64 -0
  89. package/lib/network_recovery.mjs +42 -0
  90. package/lib/notify.mjs +58 -0
  91. package/lib/openai_agents.mjs +373 -0
  92. package/lib/openai_agents_config.mjs +14 -0
  93. package/lib/openai_agents_http.mjs +87 -0
  94. package/lib/openai_agents_input.mjs +15 -0
  95. package/lib/openai_agents_sources.mjs +112 -0
  96. package/lib/owner_delivery.mjs +443 -0
  97. package/lib/plugin_package.mjs +340 -0
  98. package/lib/prd.mjs +635 -0
  99. package/lib/preparation_agents.mjs +33 -0
  100. package/lib/preparation_codex.mjs +212 -0
  101. package/lib/probe.mjs +126 -0
  102. package/lib/prompt.mjs +238 -0
  103. package/lib/provider_health.mjs +55 -0
  104. package/lib/public-format.mjs +31 -0
  105. package/lib/public_output.mjs +49 -0
  106. package/lib/release_package.mjs +154 -0
  107. package/lib/repository_files.mjs +86 -0
  108. package/lib/run.mjs +385 -0
  109. package/lib/run_log.mjs +94 -0
  110. package/lib/runtime-environment.mjs +87 -0
  111. package/lib/runtime_dependencies.mjs +15 -0
  112. package/lib/runtime_node.mjs +107 -0
  113. package/lib/runtime_state.mjs +174 -0
  114. package/lib/sources/index.mjs +27 -0
  115. package/lib/sources/prd.mjs +50 -0
  116. package/lib/start_contract.mjs +37 -0
  117. package/lib/state.mjs +246 -0
  118. package/lib/task_allowance.mjs +31 -0
  119. package/lib/task_completion.mjs +61 -0
  120. package/lib/task_contract.mjs +450 -0
  121. package/lib/task_delivery.mjs +219 -0
  122. package/lib/task_delivery_cancel.mjs +34 -0
  123. package/lib/task_delivery_contract.mjs +61 -0
  124. package/lib/task_delivery_mcp.mjs +40 -0
  125. package/lib/task_delivery_store.mjs +43 -0
  126. package/lib/task_delivery_view.mjs +88 -0
  127. package/lib/task_environment.mjs +281 -0
  128. package/lib/task_prepare.mjs +224 -0
  129. package/lib/task_store.mjs +38 -0
  130. package/lib/tasks.mjs +513 -0
  131. package/lib/tick.mjs +1635 -0
  132. package/lib/time.mjs +36 -0
  133. package/lib/tracker/assets.mjs +43 -0
  134. package/lib/tracker/pages.mjs +320 -0
  135. package/lib/tracker/runs.mjs +102 -0
  136. package/lib/tracker/stream.mjs +74 -0
  137. package/lib/tracker_process.mjs +107 -0
  138. package/lib/verification_sandbox.mjs +181 -0
  139. package/node_modules/smol-toml/LICENSE +24 -0
  140. package/node_modules/smol-toml/README.md +605 -0
  141. package/node_modules/smol-toml/dist/date.d.ts +41 -0
  142. package/node_modules/smol-toml/dist/date.js +156 -0
  143. package/node_modules/smol-toml/dist/error.d.ts +37 -0
  144. package/node_modules/smol-toml/dist/error.js +67 -0
  145. package/node_modules/smol-toml/dist/extract.js +289 -0
  146. package/node_modules/smol-toml/dist/index.cjs +770 -0
  147. package/node_modules/smol-toml/dist/index.d.ts +44 -0
  148. package/node_modules/smol-toml/dist/index.js +34 -0
  149. package/node_modules/smol-toml/dist/parse.d.ts +73 -0
  150. package/node_modules/smol-toml/dist/parse.js +164 -0
  151. package/node_modules/smol-toml/dist/primitive.js +151 -0
  152. package/node_modules/smol-toml/dist/stringify.d.ts +32 -0
  153. package/node_modules/smol-toml/dist/stringify.js +219 -0
  154. package/node_modules/smol-toml/dist/struct.js +156 -0
  155. package/node_modules/smol-toml/dist/util.d.ts +43 -0
  156. package/node_modules/smol-toml/dist/util.js +57 -0
  157. package/node_modules/smol-toml/package.json +68 -0
  158. package/npm-shrinkwrap.json +1823 -0
  159. package/package.json +105 -4
  160. package/templates/claude-settings.json +18 -0
  161. package/templates/passport.gallivant.json +83 -0
  162. package/templates/plugin/bundled-runtime.json +26 -0
  163. package/templates/plugin/schemas/mcp.schema.json +120 -0
  164. package/templates/plugin/schemas/plugin.schema.json +65 -0
  165. package/templates/plugin/task-delivery.SKILL.md +60 -0
  166. package/templates/prd.md +32 -0
  167. package/templates/tracker/tracker.css +301 -0
  168. 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.