@uchibeke/gallivant 0.0.0-stage → 0.2.1

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 +239 -2
  4. package/SECURITY.md +19 -0
  5. package/bin/gallivant.mjs +261 -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 +72 -0
  12. package/docs/agent-setup.md +228 -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 +649 -0
  18. package/docs/config.md +437 -0
  19. package/docs/execution.md +796 -0
  20. package/docs/guardrails.md +311 -0
  21. package/docs/install.md +398 -0
  22. package/docs/launch.md +45 -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 +149 -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 +466 -0
  43. package/lib/commands/init.mjs +245 -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 +249 -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 +102 -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 +707 -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 +140 -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 +1654 -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,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.
package/docs/chains.md ADDED
@@ -0,0 +1,102 @@
1
+ # PRD chains
2
+
3
+ **Gallivant automates delivery; humans approve integration.** A chain starts its next PRD only after GitHub confirms
4
+ that the preceding PR was merged into the planned base branch. Gallivant never merges PRs. Passing tests, an accepted
5
+ receipt, `ready-for-human`, and a finished agent session are progress toward that decision; none starts the next task.
6
+
7
+ This lets you approve a multi-hour or multi-day plan once, then review meaningful milestones as they arrive. It does
8
+ not promise uninterrupted execution while you are unavailable: a dependent chain waits for your merge. Put cohesive
9
+ implementation steps inside one PRD to keep the number of integration decisions manageable.
10
+ Research and documentation milestones work the same way when their outputs live
11
+ in the repository and have concrete verification. Natural-language task intake
12
+ can prepare individual briefs, but `chain` itself takes ready PRD paths; it does
13
+ not infer dependencies from chat or queue unfinished intake sessions. Hosted and
14
+ local implementations retain the same human merge gate, and the coordinator must
15
+ remain running to observe it.
16
+
17
+ ## Queue a plan
18
+
19
+ Create each PRD in the repository it describes and mark it ready:
20
+
21
+ ```sh
22
+ gallivant ready docs/prds/foundation.md
23
+ gallivant ready docs/prds/integration.md
24
+ gallivant ready docs/prds/launch.md
25
+ gallivant chain launch docs/prds/foundation.md docs/prds/integration.md docs/prds/launch.md
26
+ ```
27
+
28
+ A chain requires at least two ready PRDs, all in one checkout of the same repository and targeting the same base
29
+ branch. Configure that repository's [base](config.md#the-base-branch) before queueing, or pass `--base <branch>` when
30
+ creating the chain. Every task must be new to Gallivant; existing
31
+ registered runs and IDs already reserved by another chain are refused. Do not start queued PRDs separately.
32
+
33
+ Queueing records the approved order, base, and fingerprints of the PRDs and their carried `sources` files. It brings
34
+ up the existing daemon if needed. The daemon starts the first task, uses the ordinary implementation and review loop,
35
+ then waits for the merge. Each successor starts in its own checkout from the updated base. A final merge completes
36
+ the chain; closing any milestone's PR without merging stops it.
37
+
38
+ The scope is deliberately linear: one repository, one base, one active milestone per chain. There is no dependency
39
+ graph, stacked-PR mode or automatic merge. Unrelated tasks and other chains can run independently.
40
+
41
+ ## Inspect progress
42
+
43
+ ```sh
44
+ gallivant chain # list chains
45
+ gallivant chain launch # milestones and the current wait or stop
46
+ gallivant status # run status and chain progress
47
+ gallivant why your-repo#123 # details for a started milestone
48
+ ```
49
+
50
+ Merge the current PR when you accept it. There is no separate close or next-step command. The daemon checks the
51
+ actual PR state and intended base, then verifies that the successor's base contains the merge commit. A local
52
+ `done` entry alone is insufficient because it can also describe a closed, unmerged PR.
53
+
54
+ If the current task needs a correction, use the existing `say`, `recheck` or `resume` commands for that run. Its
55
+ session, checkout and review budget belong to the normal run lifecycle. Recovering that task does not require a
56
+ second chain retry; the chain continues watching its current milestone.
57
+
58
+ ## Change the remaining plan
59
+
60
+ Changing a queued PRD or a carried source pauses dispatch rather than silently accepting new instructions. Review
61
+ the changes, run `ready` for any updated brief, then explicitly approve the unstarted portion again:
62
+
63
+ ```sh
64
+ gallivant chain launch --retry --resnapshot
65
+ ```
66
+
67
+ This only reapproves briefs and sources for tasks that have not started. To update an active task, use
68
+ `gallivant resume <handle> --resnapshot` instead. The chain's order, task identities and base stay fixed; cancel and
69
+ queue a new plan if those need to change.
70
+
71
+ Keep `sources` for planning inputs that the successor should carry unchanged. Read predecessor implementations and
72
+ generated artifacts from the newly merged base. Gallivant refuses to overwrite a base file changed since queueing
73
+ with an older carried copy; reconcile the planning input before retrying. Keep the checkout containing the approved
74
+ briefs available until they have started.
75
+
76
+ ## Recover or cancel
77
+
78
+ After fixing a chain-level problem, retry its checks:
79
+
80
+ ```sh
81
+ gallivant chain launch --retry
82
+ ```
83
+
84
+ Retry does not approve edited briefs, reset iteration counts, or turn an unmerged PR into a satisfied dependency.
85
+ It applies only to a stopped chain. If you reopen a closed PR and want Gallivant to continue its review loop, register
86
+ that PR again before retrying the chain; a locally `done` run is not resumed merely by reopening its GitHub PR.
87
+ If a daemon stopped during dispatch and cannot prove whether a session started, the chain stops for inspection.
88
+ Check the chain detail, `gallivant why <handle>` when an entry exists, the run logs and `~/.gallivant/daemon.log`.
89
+ Retry never clears an ambiguous pending-start claim. Do not delete claims or worktrees merely to force another
90
+ start: an earlier process may still be working there.
91
+
92
+ To stop future dispatch:
93
+
94
+ ```sh
95
+ gallivant chain launch --cancel
96
+ ```
97
+
98
+ Cancellation preserves already started sessions, branches, PRs and checkouts. An active milestone remains under
99
+ the ordinary review loop. It does not cancel or undo work that has already started.
100
+
101
+ Chain approval and unstarted milestones live in local `state.json`; unlike an open PR's identity, they cannot be
102
+ reconstructed from GitHub. Preserve your Gallivant home when moving the daemon to another machine.