@uchibeke/gallivant 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -16
- package/SECURITY.md +9 -0
- package/bin/gallivant.mjs +33 -10
- package/bin/promote-bitbucket.mjs +40 -0
- package/bin/task-pr.mjs +9 -0
- package/bin/task-review.mjs +15 -0
- package/docs/README.md +2 -0
- package/docs/architecture.md +11 -3
- package/docs/bitbucket-promotion.md +69 -0
- package/docs/bitbucket-review.md +330 -0
- package/docs/chains.md +30 -1
- package/docs/codex-cloud.md +7 -0
- package/docs/codex-review.md +50 -0
- package/docs/commands.md +65 -5
- package/docs/config.md +35 -10
- package/docs/first-party-review.md +266 -0
- package/docs/git-hosts.md +294 -0
- package/docs/guardrails.md +56 -0
- package/docs/install.md +18 -8
- package/docs/network-recovery.md +9 -0
- package/docs/openai-agents.md +2 -1
- package/docs/prd.md +23 -2
- package/docs/preparation.md +1 -1
- package/docs/release-qualification.md +1 -1
- package/docs/releasing.md +17 -2
- package/docs/task-delivery.md +4 -4
- package/docs/task-profiles.md +537 -0
- package/docs/tasks.md +59 -197
- package/lib/attribution.mjs +7 -3
- package/lib/block.mjs +10 -1
- package/lib/chains.mjs +101 -30
- package/lib/codex_cloud.mjs +2 -1
- package/lib/codex_cloud_input.mjs +15 -6
- package/lib/codex_cloud_result.mjs +1 -1
- package/lib/commands/doctor.mjs +22 -462
- package/lib/commands/init.mjs +72 -30
- package/lib/commands/register.mjs +25 -11
- package/lib/commands/resume.mjs +5 -1
- package/lib/commands/review.mjs +66 -0
- package/lib/commands/serve.mjs +8 -0
- package/lib/commands/start.mjs +30 -11
- package/lib/commands/status.mjs +18 -2
- package/lib/commands/task.mjs +9 -1
- package/lib/commands/up.mjs +19 -11
- package/lib/commands/why.mjs +7 -3
- package/lib/config.mjs +42 -9
- package/lib/context.mjs +23 -18
- package/lib/control.mjs +21 -6
- package/lib/credential.mjs +20 -14
- package/lib/deliverable.mjs +2 -0
- package/lib/events.mjs +96 -53
- package/lib/execution.mjs +48 -3
- package/lib/execution_preparation.mjs +16 -1
- package/lib/execution_result.mjs +26 -5
- package/lib/first_party_review.mjs +618 -0
- package/lib/gh.mjs +144 -56
- package/lib/git.mjs +133 -32
- package/lib/git_diff.mjs +121 -0
- package/lib/harness.mjs +14 -10
- package/lib/host_contract.mjs +71 -0
- package/lib/host_definitions.mjs +22 -0
- package/lib/hosts/bitbucket.mjs +553 -0
- package/lib/hosts/bitbucket_contract.mjs +57 -0
- package/lib/hosts/bitbucket_credential.mjs +62 -0
- package/lib/hosts/bitbucket_review.mjs +44 -0
- package/lib/hosts/bitbucket_review_producer.mjs +50 -0
- package/lib/hosts/github.mjs +57 -0
- package/lib/hosts/github_contract.mjs +27 -0
- package/lib/hosts/github_doctor.mjs +482 -0
- package/lib/hosts/github_review_summary.mjs +38 -0
- package/lib/hosts/transport_doctor.mjs +35 -0
- package/lib/hosts.mjs +130 -0
- package/lib/learning_qualification.mjs +1 -1
- package/lib/notify.mjs +1 -1
- package/lib/openai_agents_input.mjs +7 -2
- package/lib/openai_agents_sources.mjs +16 -8
- package/lib/plugin_package.mjs +13 -1
- package/lib/pr_operation.mjs +41 -0
- package/lib/prd.mjs +18 -3
- package/lib/preparation_codex.mjs +6 -1
- package/lib/probe.mjs +6 -4
- package/lib/process.mjs +24 -0
- package/lib/promotion.mjs +132 -0
- package/lib/promotion_git.mjs +63 -0
- package/lib/prompt.mjs +16 -13
- package/lib/provider_health.mjs +14 -3
- package/lib/repository.mjs +68 -0
- package/lib/repository_preflight.mjs +11 -0
- package/lib/review_evaluation.mjs +25 -0
- package/lib/review_evidence.mjs +500 -0
- package/lib/review_history.mjs +73 -0
- package/lib/review_image.mjs +145 -0
- package/lib/review_link.mjs +12 -0
- package/lib/review_policy.mjs +132 -0
- package/lib/review_policy_contract.mjs +18 -0
- package/lib/review_state.mjs +13 -0
- package/lib/review_visual.mjs +94 -0
- package/lib/runtime-environment.mjs +6 -0
- package/lib/runtime_node.mjs +3 -2
- package/lib/runtime_state.mjs +39 -5
- package/lib/sources/prd.mjs +6 -3
- package/lib/state.mjs +137 -24
- package/lib/task_completion.mjs +7 -3
- package/lib/task_contract.mjs +201 -27
- package/lib/task_delivery.mjs +9 -2
- package/lib/task_delivery_contract.mjs +13 -9
- package/lib/task_delivery_view.mjs +18 -2
- package/lib/task_environment.mjs +13 -1
- package/lib/task_limits.mjs +13 -0
- package/lib/task_model_config.mjs +99 -0
- package/lib/task_model_route.mjs +243 -0
- package/lib/task_prepare.mjs +21 -8
- package/lib/task_profile.mjs +1875 -0
- package/lib/task_repository_context.mjs +65 -0
- package/lib/tasks.mjs +252 -51
- package/lib/tick.mjs +353 -97
- package/lib/time.mjs +1 -0
- package/lib/tracker/pages.mjs +19 -5
- package/lib/tracker/review.mjs +21 -0
- package/lib/tracker_process.mjs +8 -1
- package/lib/verification_sandbox.mjs +2 -1
- package/lib/work_artifacts.mjs +79 -0
- package/npm-shrinkwrap.json +12 -3
- package/package.json +9 -3
- package/templates/plugin/task-delivery.SKILL.md +17 -10
- package/templates/plugin/task-intake-contract.json +18 -0
- package/templates/plugin/task-profile-requests.mjs +1448 -0
- package/templates/tracker/tracker.css +3 -0
- package/templates/tracker/tracker.js +38 -0
package/docs/tasks.md
CHANGED
|
@@ -13,11 +13,11 @@ gallivant task "Explain the existing backup and restore commands in docs/operati
|
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Inspect the returned draft or answer its questions, then use its printed `Next`
|
|
16
|
-
action to
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
run `gallivant start` with no PRD path.
|
|
16
|
+
action to approve that visible revision with `task start --revision <revision>
|
|
17
|
+
--run`. Preparation always returns a draft or questions, including when the
|
|
18
|
+
original command contains `--run`; it cannot pre-authorize an unseen draft.
|
|
19
|
+
Approval grants implementation under the selected delivery profile. To open the
|
|
20
|
+
dashboard after dispatch, run `gallivant start` with no PRD path.
|
|
21
21
|
|
|
22
22
|
## Supported preparation harnesses
|
|
23
23
|
|
|
@@ -40,183 +40,39 @@ open a pull request without execution authorization.
|
|
|
40
40
|
|
|
41
41
|
## Writing requests and publication boundaries
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
Explicit workflow merge or auto-merge requests, and prohibitions on creating
|
|
77
|
-
commits or branches, also return `TASK_DELIVERY_SCOPE` before preparation or a new
|
|
78
|
-
answer is saved. The review-PR workflow needs a branch and commits and grants no
|
|
79
|
-
merge authority. “Do not merge” remains a valid constraint.
|
|
80
|
-
This includes subject-led requests such as “You need to merge it” and passive
|
|
81
|
-
or nominal prohibitions such as “Commits are not allowed” and “Commit creation is
|
|
82
|
-
forbidden.” Compound feature objects such as commit messages and branch reports
|
|
83
|
-
remain implementation requirements.
|
|
84
|
-
|
|
85
|
-
Implementation subjects such as a PR status badge, pull-request template,
|
|
86
|
-
published articles, a publish button, a command to publish docs, local storage,
|
|
87
|
-
commit SHA, diff viewer or branch selection
|
|
88
|
-
remain ordinary requirements. Technical conditions also remain implementation
|
|
89
|
-
scope: `If name is empty, return friend. Open a PR for review.` and the answer
|
|
90
|
-
`Commit the database transaction after validation.` are supported. By contrast,
|
|
91
|
-
`Wait for my approval. Open a PR for review.` requires clarification before work.
|
|
92
|
-
An implementation may consist entirely of a prohibition, such as “Do not log API
|
|
93
|
-
keys in error responses.” That requirement remains in the acceptance criteria.
|
|
94
|
-
Application operations such as publishing events to a message bus, delivering
|
|
95
|
-
notifications, creating user records or merging duplicate contacts also remain
|
|
96
|
-
implementation scope. This distinction applies to initial text and later answers,
|
|
97
|
-
including application-user consent requirements. A technical operation cannot hide
|
|
98
|
-
a later workflow restriction: “Do not deliver notifications twice or create a
|
|
99
|
-
branch” still requires clarification. Quoted examples and merge-button/status
|
|
100
|
-
features do not authorize an actual merge.
|
|
101
|
-
Technical context can also come from a transport (“publish records to Kafka” or
|
|
102
|
-
“deliver reports to S3”),
|
|
103
|
-
repeated delivery (“do not deliver reports twice”) or “delivery of files.” Bare
|
|
104
|
-
commands, Git destinations and restrictions on the task's changes remain workflow
|
|
105
|
-
controls. An unknown noun alone, as in “do not publish the solution,” does not
|
|
106
|
-
override that boundary.
|
|
107
|
-
Destinations apply to their own action: a later report upload cannot qualify an
|
|
108
|
-
earlier restriction on publishing changes. Git metadata used by an application
|
|
109
|
-
operation is distinct from publishing to a Git repository.
|
|
110
|
-
Unrecognized coordinated wording ends destination scope. Shared destinations
|
|
111
|
-
require noun-phrase evidence: determiners, recognized modifiers/compounds or bare
|
|
112
|
-
plural/data nouns. This supports “invoices and tax reports,” “metrics plus traces”
|
|
113
|
-
and “widgets and the gizmo” without enumerating every domain noun. An unknown
|
|
114
|
-
phrase such as “forward reports” is not assumed to be a noun phrase merely because
|
|
115
|
-
its verb is absent from the action vocabulary. For ambiguous application wording,
|
|
116
|
-
state the destination on each action or use separate sentences; direct application
|
|
117
|
-
objects retain an open vocabulary.
|
|
118
|
-
Verb/adjective ambiguities such as “duplicate reports” cannot supply a shared
|
|
119
|
-
destination without a noun marker. Write “metrics and the duplicate reports to
|
|
120
|
-
S3,” or put the destination directly on each application operation.
|
|
121
|
-
The same applies to bare “email,” which can be an action: use “the email” when
|
|
122
|
-
sharing another object's destination, or state its application destination directly.
|
|
123
|
-
Likewise, “log entries” and “download links” can be commands or noun compounds;
|
|
124
|
-
use “the log entries” and “the download links” in shared-destination noun lists.
|
|
125
|
-
These conservative checks apply to new requests and answers. Saved ownership
|
|
126
|
-
contracts retain their approved source spans and interpretation.
|
|
127
|
-
Modifier recognition is intentionally bounded: word endings do not establish a
|
|
128
|
-
noun phrase (for example, “give” and “apply” are verbs). An unfamiliar modifier
|
|
129
|
-
requires a direct application destination instead of a guessed shared scope.
|
|
130
|
-
A later data object cannot qualify a workflow object such as “the solution.”
|
|
131
|
-
Coordinated transfer actions such as “upload reports” or “copy files” start their
|
|
132
|
-
own scope; their destinations cannot qualify an earlier publication prohibition.
|
|
133
|
-
This includes negative leads such as “never upload” and “avoid uploading.” Data
|
|
134
|
-
noun lists such as “metrics plus archived reports,” “the log entries” or “the download
|
|
135
|
-
links” can still share a destination; explicit action leads such as “please
|
|
136
|
-
download links” start a separate action.
|
|
137
|
-
Ordinary leading elisions such as `'90s` and `'tis` remain implementation text even
|
|
138
|
-
when a later possessive apostrophe follows. Use double quotes or backticks to quote
|
|
139
|
-
an entire example beginning with an elision. Saved ownership versions one through
|
|
140
|
-
three keep their original literal interpretation and exact source spans.
|
|
141
|
-
Negative coordination preserves each action's scope: “Do not commit the database
|
|
142
|
-
transaction nor push changes” still forbids delivery. Combining only technical
|
|
143
|
-
operations, such as a database transaction and inserting items into a queue,
|
|
144
|
-
remains implementation scope; `neither … nor …` follows the same distinction.
|
|
145
|
-
Modal forms such as “nor should you push” retain the prohibition too.
|
|
146
|
-
This includes “cannot,” contracted modals such as “can't” or “shouldn't,” and
|
|
147
|
-
permission wording such as “aren't allowed to push.” “In addition to,” “besides,”
|
|
148
|
-
and other supported coordination phrases keep the same action boundary.
|
|
149
|
-
PR quantifiers such as “any,” “more,” “multiple” and numeric counts also retain
|
|
150
|
-
their delivery meaning: “Don't submit any PRs” requires clarification, while
|
|
151
|
-
“Don't submit any PR descriptions” remains an implementation requirement. Only
|
|
152
|
-
the supported affirmative single-PR clause can create a publication obligation.
|
|
153
|
-
Qualifiers such as “GitHub,” “automated” or “experimental” cannot hide that PR
|
|
154
|
-
object. Reference phrases such as “generate a report about the PR” do not request
|
|
155
|
-
PR creation.
|
|
156
|
-
Prohibition forms such as “refrain from opening a PR” and “forbidden from creating
|
|
157
|
-
any PRs” also require clarification. Describing the procedure for opening a PR
|
|
158
|
-
does not itself request publication.
|
|
159
|
-
Active subjects also retain the restriction: “I forbid you from opening a PR” and
|
|
160
|
-
“prevent the agent from opening a PR” require clarification.
|
|
161
|
-
Passive equivalents such as “opening a PR is prohibited/disallowed/banned” do too.
|
|
162
|
-
Explicit merge delegation, including “I require you to merge,” “have the agent
|
|
163
|
-
merge” and “tell the agent to merge,” cannot grant merge authority.
|
|
164
|
-
An incomplete prefix such as “Do not” retains scope across line breaks and blank
|
|
165
|
-
lines. A complete implementation constraint such as “Do not change the API” can
|
|
166
|
-
still precede a separate affirmative PR instruction.
|
|
167
|
-
Multiword qualifiers such as “under any circumstances” or “for any reason” retain
|
|
168
|
-
the same restriction. Nominal prohibitions such as “PR creation is prohibited”
|
|
169
|
-
also require clarification. Application-data merges retain open object names,
|
|
170
|
-
including invoices, database rows, accounts and files; workflow PR/branch objects
|
|
171
|
-
and mixed data/workflow lists remain outside merge authority.
|
|
172
|
-
Bare branches and ref-shaped targets (`main`, `feature/foo`, `origin/main`, Git
|
|
173
|
-
revisions) are workflow merges. Application merges need named data evidence;
|
|
174
|
-
“merge invoices into the main table” retains that application context.
|
|
175
|
-
Explicit Git-qualified single-token operands remain ambiguous even when the token
|
|
176
|
-
is a data word: “merge invoices from tag metadata” requires clarification because
|
|
177
|
-
`metadata` can be a tag name. For application data, name its type explicitly, for
|
|
178
|
-
example “merge invoices with tag metadata records.” “Merge head counts into the
|
|
179
|
-
database” remains an application operation.
|
|
180
|
-
Quantity words do not resolve that ambiguity: “two extra tag names” and “three
|
|
181
|
-
further reference fields” still require clarification. Use an explicit data type,
|
|
182
|
-
such as “two extra tag name records” or “three further reference field records.”
|
|
183
|
-
The same prohibition leads apply to push, commit and branch creation, including
|
|
184
|
-
“refrain from pushing” and “I forbid you from committing.” A negative application
|
|
185
|
-
clause cannot hide a coordinated workflow restriction, as in “do not log keys or
|
|
186
|
-
commit changes”; independent statements and technical conditions retain scope.
|
|
187
|
-
Infinitive complements such as “I forbid the agent to push changes” follow the
|
|
188
|
-
same rule as “forbid the agent from pushing changes.”
|
|
189
|
-
Generating PR titles, bodies, labels and other feature metadata also remains
|
|
190
|
-
implementation scope; those objects do not authorize creating the PR itself.
|
|
191
|
-
Possessive forms such as “the PR's body” and “the pull requests’ titles” follow
|
|
192
|
-
the same rule and retain their exact original text in implementation requirements.
|
|
193
|
-
Hyphenated metadata compounds such as “PR-title” and “pull-request-body” do too.
|
|
194
|
-
PR fixtures and explicitly synthetic representations (such as a mock PR object or
|
|
195
|
-
a normalized PR record, fake PR object or dummy/stub payload) are implementation
|
|
196
|
-
data. Bare PR objects and actual Git commit/branch objects do not gain that exception.
|
|
197
|
-
Local conditions attached to publication are still refused; the parser does not
|
|
198
|
-
guess that a comma or semicolon ended a prohibition or approval requirement.
|
|
199
|
-
|
|
200
|
-
Keep every feature requirement and restriction, and state publication separately
|
|
201
|
-
when needed. Quoted literal labels/code remain implementation data, including
|
|
202
|
-
paired typographic single/double quotes; unmatched typographic quotes do not hide
|
|
203
|
-
instructions. If local-only
|
|
204
|
-
or patch-only output is required, do not authorize this review-PR workflow or
|
|
205
|
-
remove the restriction to bypass clarification.
|
|
206
|
-
Clarify the complete task with the owner and submit it under a new operation ID;
|
|
207
|
-
never delete a condition just to pass validation. Answers may refine implementation
|
|
208
|
-
but cannot silently replace publication ownership or add a publication prerequisite.
|
|
209
|
-
This applies to explicit publication clauses, the default review workflow and new
|
|
210
|
-
answers to older saved tasks without completion metadata. Exact previously
|
|
211
|
-
accepted answer retries still replay their saved receipt. There is no typed
|
|
212
|
-
delivery-amendment operation yet. The supported answer boundary
|
|
213
|
-
conservatively refuses recognized lifecycle/artifact language; it does not claim
|
|
214
|
-
to understand every possible paraphrase. Review the complete saved owner contract
|
|
215
|
-
before authorizing implementation, and clarify conflicting delivery intent instead
|
|
216
|
-
of treating a model's interpretation as permission.
|
|
217
|
-
Selecting `integrated` still grants no merge authority. Existing saved tasks,
|
|
218
|
-
approved criteria, false attestations and exact operation retries retain their
|
|
219
|
-
original semantics; this change does not repair historical results.
|
|
43
|
+
New tasks use an owner-selected [typed delivery profile](task-profiles.md).
|
|
44
|
+
The default implements the requested change on a task branch and opens one
|
|
45
|
+
pull request for review. The resolved repository and base are bound to that
|
|
46
|
+
profile. Selecting `integrated` waits for an externally observed merge; it
|
|
47
|
+
does not authorize Gallivant or the implementation agent to merge.
|
|
48
|
+
|
|
49
|
+
Describe the outcome, constraints and verification you want. Preparation uses
|
|
50
|
+
the selected harness or the operator-configured model route to organize that
|
|
51
|
+
request. Every original clause and accepted answer keeps its exact source span
|
|
52
|
+
and a visible mapping. Owner constraints remain authoritative even when the
|
|
53
|
+
planner places a clause in an assumption. Generated rationale and assumptions
|
|
54
|
+
cannot replace those constraints.
|
|
55
|
+
|
|
56
|
+
The delivery screen raises clarification questions for unsupported external
|
|
57
|
+
actions, conflicting targets or unresolved executable command arguments.
|
|
58
|
+
Request text, selected files and model output cannot widen the typed profile.
|
|
59
|
+
Help and read-only commands do not waive commands evaluated through shell
|
|
60
|
+
substitution before the tool starts. Literal examples, documented commands and
|
|
61
|
+
ordinary variable-valued read arguments retain their context. The screen is a
|
|
62
|
+
bounded clarification aid, not a claim to understand arbitrary shell programs
|
|
63
|
+
or every paraphrase; repository credentials and runtime enforcement still define
|
|
64
|
+
what an implementation can actually access.
|
|
65
|
+
|
|
66
|
+
Prepare the brief, inspect its requirements and spending allowance, answer any
|
|
67
|
+
material questions, then explicitly approve that revision to start. A prepared
|
|
68
|
+
brief alone never authorizes implementation. See [inspect, answer and
|
|
69
|
+
start](#inspect-answer-and-start) for the supported commands.
|
|
70
|
+
|
|
71
|
+
Historical tasks retain the free-text ownership contract and fragment version
|
|
72
|
+
with which they were created. Exact operation retries replay that saved identity;
|
|
73
|
+
new clause limits do not reinterpret a previously accepted line-based request.
|
|
74
|
+
This release keeps main's established legacy recognizer rather than adding a
|
|
75
|
+
second expanded publication-language parser. New tasks use the typed workflow.
|
|
220
76
|
|
|
221
77
|
## Repository context and preparation
|
|
222
78
|
|
|
@@ -267,13 +123,21 @@ a shorter corrected answer can reuse the refused operation identity. Raw whitesp
|
|
|
267
123
|
word counts alone are insufficient because JSON quoting escapes tabs. Recovery
|
|
268
124
|
settles any already-owned preparation turn and preserves usage before stopping
|
|
269
125
|
oversized saved state; it launches no further planning or repair turn.
|
|
270
|
-
An intake accepts at most 128 immutable requirement fragments across
|
|
271
|
-
request and answer
|
|
126
|
+
An intake accepts at most 128 immutable requirement fragments across material
|
|
127
|
+
request and answer clauses, matching its mapping capacity. Oversize requests stop
|
|
272
128
|
before repository lookup or preparation. An answer that exceeds the remaining
|
|
273
129
|
capacity is rejected before context reads or state changes; its operation ID and
|
|
274
130
|
revision remain available for a corrected reply. Prepare a smaller intake if
|
|
275
131
|
the remaining material questions cannot fit. Existing owned turns are reconciled
|
|
276
132
|
with usage retained before oversized legacy state stops without a new turn.
|
|
133
|
+
Each answer operation is also limited to 32 KiB of combined UTF-8 bytes across
|
|
134
|
+
question IDs and answer strings. A new over-limit operation returns `TASK_INPUT`
|
|
135
|
+
before signature hashing or delivery parsing, without changing the saved task.
|
|
136
|
+
Exact receipts created by an older version still replay up to the largest batch
|
|
137
|
+
the prior in-process service could accept: 128 answers of 32 KiB each with IDs up to 128
|
|
138
|
+
bytes (4,210,688 bytes total). A changed input under an existing operation ID
|
|
139
|
+
within that compatibility bound returns `TASK_CONFLICT`; larger input returns
|
|
140
|
+
`TASK_INPUT` before signature hashing.
|
|
277
141
|
The complete serialized preparation prompt is also limited to 120 KiB, including
|
|
278
142
|
JSON escaping, runtime instructions, saved answers, previous output and repair
|
|
279
143
|
feedback. This portable limit is checked before a harness probe or launch on every
|
|
@@ -488,20 +352,19 @@ changing their selected paths conflicts. Other CLI task operations reject
|
|
|
488
352
|
without discoverable test commands, answer the material question and supply the
|
|
489
353
|
exact repository command using `--verify 'python -m unittest'`.
|
|
490
354
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
`--no-daemon` leaves monitoring to an explicitly started `gallivant up`.
|
|
355
|
+
Preparation returns the draft and an explicit next action. Only a separate
|
|
356
|
+
`task start --revision <revision> --run` approves its review digest and starts
|
|
357
|
+
implementation through the normal daemon. Answers invalidate approval and return
|
|
358
|
+
the next revision for review. After explicit approval has been saved, a crash
|
|
359
|
+
retry reconciles that same approval and dispatch ownership without duplicating a
|
|
360
|
+
run. A saved typed preparation flag without a review approval requires explicit
|
|
361
|
+
start. Historical tasks created before typed profiles retain their saved consent.
|
|
362
|
+
`--no-daemon` on start leaves monitoring to an explicitly started `gallivant up`.
|
|
500
363
|
|
|
501
364
|
Readiness uses the existing PRD checker and readiness decision procedure. It proves
|
|
502
365
|
contract structure, not semantic correctness or passing implementation tests.
|
|
503
|
-
Review the assumptions and mapping.
|
|
504
|
-
requirement fragments; their exact text is attached to mapped criteria or shown as
|
|
366
|
+
Review the assumptions and mapping. Sentences, semicolon clauses and nonblank request lines outside quoted literals
|
|
367
|
+
and code are immutable requirement fragments; their exact text is attached to mapped criteria or shown as
|
|
505
368
|
unresolved. An explicit answer can clarify a previously questioned fragment; the
|
|
506
369
|
mapping records which answer resolved it. Saved question ownership determines the
|
|
507
370
|
clarification even when the planner omits `resolvedBy`. All applicable answers
|
|
@@ -531,8 +394,7 @@ new intake to establish its story ownership; no authorization is inferred from
|
|
|
531
394
|
untrusted context. Saved validation recomputes the same owner-derived body, so an
|
|
532
395
|
older draft containing planner-written executable scope cannot dispatch even if
|
|
533
396
|
its recorded hash and readiness digest match. Existing owned implementation runs
|
|
534
|
-
still reconcile without a duplicate launch.
|
|
535
|
-
already authorized `--run` work.
|
|
397
|
+
still reconcile without a duplicate launch. Existing revision-pinned approvals remain valid for exact crash retries.
|
|
536
398
|
Each question's requirement IDs must exactly match its unresolved mappings, with
|
|
537
399
|
no duplicate IDs. Saved ownership limits which fragments an answer can clarify.
|
|
538
400
|
Reopening an older unanswered question proves ownership from its saved mappings;
|
package/lib/attribution.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { workArtifactHook } from "./work_artifacts.mjs";
|
|
1
2
|
// Harness attribution in a commit message, and the `commit-msg` hook that refuses it.
|
|
2
3
|
//
|
|
3
4
|
// Commits use the operator's configured identity. Their message must also describe the change without runner
|
|
@@ -75,11 +76,12 @@ export const attributionError = line => [
|
|
|
75
76
|
*
|
|
76
77
|
* Exits 1 on a match, which is what makes `git commit` create no commit object at all.
|
|
77
78
|
*/
|
|
78
|
-
export const
|
|
79
|
-
// Written by Gallivant (lib/attribution.mjs). Refuses
|
|
79
|
+
export const commitMsgHook = (policy = {}) => `#!/usr/bin/env node
|
|
80
|
+
// Written by Gallivant (lib/attribution.mjs). Refuses task work artifacts and commit messages that credit an AI harness.
|
|
80
81
|
// Self-contained on purpose: this runs inside a sandboxed checkout that cannot read Gallivant's own source.
|
|
81
82
|
// Dynamic import works whether the checkout's package.json declares module or commonjs.
|
|
82
|
-
import("node:fs").then(({ readFileSync }) => {
|
|
83
|
+
import("node:fs").then(async ({ readFileSync }) => {
|
|
84
|
+
${workArtifactHook(policy)}
|
|
83
85
|
const ATTRIBUTION_PATTERNS = ${JSON.stringify(ATTRIBUTION_PATTERNS.map(re => [re.source, re.flags]))}.map(([source, flags]) => new RegExp(source, flags));
|
|
84
86
|
${attributionIn.toString()}
|
|
85
87
|
const attributionError = ${attributionError.toString()};
|
|
@@ -93,3 +95,5 @@ if (line) {
|
|
|
93
95
|
process.exit(0);
|
|
94
96
|
});
|
|
95
97
|
`;
|
|
98
|
+
|
|
99
|
+
export const COMMIT_MSG_HOOK = commitMsgHook();
|
package/lib/block.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { HARNESSES } from "./config.mjs";
|
|
4
4
|
import { isSessionFor } from "./harness.mjs";
|
|
5
5
|
import { BLOCK_OPEN, BLOCK_OPENS, BRANCH_PREFIXES } from "./public-format.mjs";
|
|
6
|
-
import { normalizeReceipt } from "./deliverable.mjs";
|
|
6
|
+
import { normalizeReceipt, withoutReceipt } from "./deliverable.mjs";
|
|
7
7
|
|
|
8
8
|
const CLOSE = "-->";
|
|
9
9
|
const FIELDS = ["prd", "harness", "session", "branch"];
|
|
@@ -21,6 +21,15 @@ function blocks(body) {
|
|
|
21
21
|
return [...body.matchAll(OPEN_RE)].map(match => ({ start: match.index, open: match[0].trimEnd(), end: body.indexOf(CLOSE, match.index + match[0].length) }));
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/** Preserve human prose; drop only recognized task-owned recovery/receipt records. */
|
|
25
|
+
export function withoutTaskMetadata(body) {
|
|
26
|
+
let text = String(body || "");
|
|
27
|
+
const found = blocks(text);
|
|
28
|
+
if (found.length > 1 || found.some(b => b.end < 0)) throw new Error("Ambiguous recovery metadata; repair the source PR body before promotion");
|
|
29
|
+
if (found.length) text = text.slice(0, found[0].start) + text.slice(found[0].end + CLOSE.length);
|
|
30
|
+
return withoutReceipt(text).trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
24
33
|
export function renderBlock({ prd, harness, session, branch }) {
|
|
25
34
|
const data = Buffer.from(JSON.stringify({ version: 1, prd, harness, session: session ?? null, branch }), "utf8").toString("base64url");
|
|
26
35
|
return `${BLOCK_OPEN}\n${data}\n${CLOSE}`;
|
package/lib/chains.mjs
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
|
+
import { terminalIdentityProblem } from "./host_contract.mjs";
|
|
2
|
+
import { hostApi } from "./hosts.mjs";
|
|
1
3
|
// Linear, operator-approved plans. The normal start/tick machinery still owns every individual run.
|
|
2
4
|
import { randomUUID } from "node:crypto";
|
|
3
5
|
import { requestFor } from "./sources/index.mjs";
|
|
4
6
|
import { resolveBase } from "./config.mjs";
|
|
5
|
-
import { entryId } from "./state.mjs";
|
|
7
|
+
import { entryId, resolveHandle, matchesChainAnchor } from "./state.mjs";
|
|
6
8
|
import { prdIdFromBranch } from "./git.mjs";
|
|
7
|
-
import {
|
|
9
|
+
import { hosts } from "./hosts.mjs";
|
|
8
10
|
import { start } from "./commands/start.mjs";
|
|
9
11
|
import { validateStartRequest, captureStartInput, contentDigest, checkBaseFiles } from "./start_contract.mjs";
|
|
10
12
|
|
|
11
13
|
const MAX_FAILURES = 5;
|
|
12
14
|
const flights = new Map();
|
|
13
15
|
const stamp = () => new Date().toISOString();
|
|
16
|
+
const rateLimitDelay = value => {
|
|
17
|
+
const seconds = Number(value);
|
|
18
|
+
const requested = value != null && Number.isFinite(seconds) && seconds >= 0 ? seconds * 1000
|
|
19
|
+
: typeof value === "string" && Number.isFinite(Date.parse(value)) ? Date.parse(value) - Date.now()
|
|
20
|
+
: 15 * 60_000;
|
|
21
|
+
return Math.min(2 * 60 * 60_000, Math.max(60_000, requested));
|
|
22
|
+
};
|
|
14
23
|
const validName = name => {
|
|
15
24
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name || "") || name.length > 80) throw new Error("chain name must be 1–80 lowercase letters, digits and single dashes");
|
|
16
25
|
};
|
|
@@ -30,29 +39,58 @@ async function planStep(ctx, path, askedBase) {
|
|
|
30
39
|
|
|
31
40
|
async function approveBaseFiles(ctx, plans) {
|
|
32
41
|
const { repo, base } = plans[0];
|
|
33
|
-
if (!await ctx.
|
|
34
|
-
const sha = await ctx.
|
|
42
|
+
if (!await hostApi(ctx).branchExists(repo, base)) throw new Error(`base ${repo}:${base} is missing or GitHub is unavailable`);
|
|
43
|
+
const sha = await hostApi(ctx).refSha(repo, base);
|
|
35
44
|
if (!/^[a-f0-9]{40,64}$/.test(sha || "")) throw new Error(`GitHub did not return a base commit for ${repo}:${base}`);
|
|
36
45
|
const files = new Map();
|
|
37
46
|
for (const plan of plans) {
|
|
38
47
|
for (const [path] of plan.input.files) {
|
|
39
|
-
if (!files.has(path)) files.set(path, contentDigest(await ctx.
|
|
48
|
+
if (!files.has(path)) files.set(path, contentDigest(await hostApi(ctx).fileAtStrict(repo, path, sha)));
|
|
40
49
|
plan.step.baseFiles[path] = files.get(path);
|
|
41
50
|
}
|
|
42
51
|
}
|
|
43
52
|
}
|
|
44
53
|
|
|
45
|
-
|
|
54
|
+
function prIdentityProblem(ctx, c, entry, pr) {
|
|
55
|
+
let parsed;
|
|
56
|
+
try { parsed = (ctx.hosts ?? hosts).fromUrl(pr.url); } catch { return "PR identity or intended base changed"; }
|
|
57
|
+
return parsed.repository.key !== c.repo || parsed.number !== entry.pr || pr.number !== entry.pr
|
|
58
|
+
|| pr.headRefName !== entry.branch || pr.isCrossRepository !== false || pr.baseRefName !== c.base
|
|
59
|
+
|| (entry.url && pr.url !== entry.url) ? "PR identity or intended base changed" : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function planAnchor(ctx, selector, plan) {
|
|
63
|
+
const entries = ctx.state.entries();
|
|
64
|
+
let id;
|
|
65
|
+
if (/^https?:\/\//i.test(selector)) {
|
|
66
|
+
const { repository, number } = (ctx.hosts ?? hosts).fromUrl(selector);
|
|
67
|
+
const matches = entries.filter(([, entry]) => entry.repo === repository.key && entry.pr === number);
|
|
68
|
+
if (matches.length !== 1) throw new Error("--after requires exactly one registered predecessor PR; register it first");
|
|
69
|
+
id = matches[0][0];
|
|
70
|
+
} else id = resolveHandle(entries, selector);
|
|
71
|
+
const entry = ctx.state.get(id);
|
|
72
|
+
if (!Number.isSafeInteger(entry?.pr) || entry.pr < 1 || !entry.url || !entry.branch
|
|
73
|
+
|| entry.repo !== plan.repo || entry.base !== plan.base) throw new Error("--after requires a registered PR in the same repository and approved base as the queued tasks");
|
|
74
|
+
const pr = await hostApi(ctx).prView(plan.repo, entry.pr);
|
|
75
|
+
const problem = prIdentityProblem(ctx, plan, entry, pr);
|
|
76
|
+
if (problem) throw new Error(`${id}: ${problem}`);
|
|
77
|
+
if (!["OPEN", "MERGED"].includes(pr.state)) throw new Error(`${id}: predecessor PR must be open or merged, not closed without merging`);
|
|
78
|
+
return { id, repo: entry.repo, base: entry.base, branch: entry.branch, pr: entry.pr, url: entry.url, merged: null };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function queueChain(ctx, { name, paths, base = null, after = null }) {
|
|
46
82
|
validName(name);
|
|
47
|
-
if (
|
|
83
|
+
if (after !== null && (typeof after !== "string" || !after.trim())) throw new Error("--after requires a registered PR URL or handle");
|
|
84
|
+
if (!Array.isArray(paths) || paths.length < (after ? 1 : 2)) throw new Error(after ? "a chain with --after needs at least one ready PRD path" : "a chain needs at least two ready PRD paths in execution order");
|
|
48
85
|
const plans = [];
|
|
49
86
|
for (const path of paths) plans.push(await planStep(ctx, path, base));
|
|
50
87
|
const first = plans[0];
|
|
51
88
|
if (plans.some(p => p.repo !== first.repo || p.base !== first.base || p.step.checkout !== first.step.checkout)) throw new Error("a chain must use one repository checkout and one base branch");
|
|
52
89
|
if (new Set(plans.map(p => p.step.id)).size !== plans.length) throw new Error("a chain cannot contain duplicate PRDs");
|
|
90
|
+
const anchor = after ? await planAnchor(ctx, after, first) : null;
|
|
53
91
|
await approveBaseFiles(ctx, plans);
|
|
54
92
|
return ctx.state.createChain({ name, repo: first.repo, base: first.base, status: "active", index: 0,
|
|
55
|
-
steps: plans.map(p => p.step), reason: null, failures: 0, created_at: stamp(), updated_at: stamp() });
|
|
93
|
+
...(anchor ? { after: anchor } : {}), steps: plans.map(p => p.step), reason: null, failures: 0, created_at: stamp(), updated_at: stamp() });
|
|
56
94
|
}
|
|
57
95
|
|
|
58
96
|
export function cancelChain(ctx, name) {
|
|
@@ -65,7 +103,7 @@ export function cancelChain(ctx, name) {
|
|
|
65
103
|
export async function retryChain(ctx, { name, resnapshot = false }) {
|
|
66
104
|
const c = ctx.state.getChain(name);
|
|
67
105
|
if (!c) throw new Error(`no chain ${name}`);
|
|
68
|
-
if (c.status !== "stopped") throw new Error(`chain ${name} is ${c.status}; --retry is for a stopped chain`);
|
|
106
|
+
if (c.status !== "stopped" && !(c.status === "active" && resnapshot)) throw new Error(`chain ${name} is ${c.status}; --retry is for a stopped chain, or --retry --resnapshot for undispatched steps in an active chain`);
|
|
69
107
|
for (const step of c.steps) {
|
|
70
108
|
if (ctx.state.startClaim(step.id) || (step.dispatch && !ctx.state.get(step.id))) throw new Error(uncertain(step.id));
|
|
71
109
|
}
|
|
@@ -109,7 +147,40 @@ function matchesEntry(c, step, entry) {
|
|
|
109
147
|
&& entry.start_token && (!step.entry || step.entry.token === entry.start_token);
|
|
110
148
|
}
|
|
111
149
|
|
|
150
|
+
// Owned milestones and registered predecessors use the same host evidence and merge gate.
|
|
151
|
+
async function observeMerge(ctx, c, id, entry) {
|
|
152
|
+
if (!entry.pr || (entry.last?.run && entry.last.exit == null) || ctx.runnerFor(id).current()?.running) return {};
|
|
153
|
+
const pr = await hostApi(ctx).prView(c.repo, entry.pr);
|
|
154
|
+
const identity = prIdentityProblem(ctx, c, entry, pr);
|
|
155
|
+
if (identity) return { problem: identity };
|
|
156
|
+
if (pr.state === "CLOSED") return { problem: `PR #${entry.pr} was closed without merging; reopen and register the PR before retrying, or cancel this chain` };
|
|
157
|
+
if (pr.state !== "MERGED") return {};
|
|
158
|
+
const problem = await terminalIdentityProblem(hostApi(ctx), entry, pr);
|
|
159
|
+
if (problem) return { problem };
|
|
160
|
+
const sha = pr.mergeCommit?.oid;
|
|
161
|
+
if (!/^[a-f0-9]{40,64}$/.test(sha || "") || !pr.mergedAt || !await hostApi(ctx).containsCommit(c.repo, c.base, sha)) {
|
|
162
|
+
return { problem: `merge commit is missing from the intended base ${c.base}` };
|
|
163
|
+
}
|
|
164
|
+
return { merged: { pr: entry.pr, sha, at: pr.mergedAt } };
|
|
165
|
+
}
|
|
166
|
+
|
|
112
167
|
async function tickChain(ctx, c) {
|
|
168
|
+
if (c.rateLimitUntil) {
|
|
169
|
+
if (Date.now() < Date.parse(c.rateLimitUntil)) return;
|
|
170
|
+
c = saveActive(ctx, c, { rateLimitUntil: null, reason: null });
|
|
171
|
+
if (c.status !== "active") return;
|
|
172
|
+
}
|
|
173
|
+
if (c.after) {
|
|
174
|
+
const entry = ctx.state.get(c.after.id);
|
|
175
|
+
if (!matchesChainAnchor(c.after, entry)) { await stop(ctx, c, `${c.after.id}: registered predecessor identity changed or is missing`); return; }
|
|
176
|
+
if (!c.after.merged) {
|
|
177
|
+
const observation = await observeMerge(ctx, c, c.after.id, entry);
|
|
178
|
+
if (observation.problem) await stop(ctx, c, `${c.after.id}: ${observation.problem}`);
|
|
179
|
+
else if (observation.merged) saveActive(ctx, c, { after: { ...c.after, merged: observation.merged }, failures: 0, reason: null });
|
|
180
|
+
else if (c.failures) saveActive(ctx, c, { failures: 0, reason: null });
|
|
181
|
+
return; // Observe the predecessor merge before a later tick can start any successor.
|
|
182
|
+
}
|
|
183
|
+
}
|
|
113
184
|
let step = c.steps[c.index];
|
|
114
185
|
if (step.dispatch) {
|
|
115
186
|
const entry = ctx.state.get(step.id);
|
|
@@ -122,22 +193,11 @@ async function tickChain(ctx, c) {
|
|
|
122
193
|
c = saveActive(ctx, c, { steps }); step = c.steps[c.index];
|
|
123
194
|
if (c.status !== "active") return;
|
|
124
195
|
}
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
const identity = repoFromUrl(pr.url);
|
|
129
|
-
if (identity?.repo !== c.repo || identity.number !== entry.pr || pr.number !== entry.pr
|
|
130
|
-
|| pr.headRefName !== entry.branch || pr.isCrossRepository !== false || pr.baseRefName !== c.base) {
|
|
131
|
-
await stop(ctx, c, `${step.id}: PR identity or intended base changed`); return;
|
|
132
|
-
}
|
|
133
|
-
if (pr.state === "CLOSED") { await stop(ctx, c, `${step.id}: PR #${entry.pr} was closed without merging; reopen and register the PR before retrying, or cancel this chain`); return; }
|
|
134
|
-
if (pr.state !== "MERGED") { if (c.failures) saveActive(ctx, c, { failures: 0, reason: null }); return; }
|
|
135
|
-
const sha = pr.mergeCommit?.oid;
|
|
136
|
-
if (!/^[a-f0-9]{40,64}$/.test(sha || "") || !pr.mergedAt || !await ctx.gh.containsCommit(c.repo, c.base, sha)) {
|
|
137
|
-
await stop(ctx, c, `${step.id}: merge commit is missing from the intended base ${c.base}`); return;
|
|
138
|
-
}
|
|
196
|
+
const observation = await observeMerge(ctx, c, step.id, entry);
|
|
197
|
+
if (observation.problem) { await stop(ctx, c, `${step.id}: ${observation.problem}`); return; }
|
|
198
|
+
if (!observation.merged) { if (c.failures) saveActive(ctx, c, { failures: 0, reason: null }); return; }
|
|
139
199
|
const steps = structuredClone(c.steps);
|
|
140
|
-
steps[c.index].merged =
|
|
200
|
+
steps[c.index].merged = observation.merged;
|
|
141
201
|
const index = c.index + 1;
|
|
142
202
|
saveActive(ctx, c, { steps, index, status: index === steps.length ? "done" : "active", reason: null, failures: 0 });
|
|
143
203
|
return; // At most one transition per chain per tick; another tick starts the successor.
|
|
@@ -146,13 +206,13 @@ async function tickChain(ctx, c) {
|
|
|
146
206
|
if (ctx.state.get(step.id) || ctx.state.startClaim(step.id)) { await stop(ctx, c, uncertain(step.id)); return; }
|
|
147
207
|
const p = await planStep(ctx, step.path, c.base);
|
|
148
208
|
if (p.input.digest !== step.digest) { await stop(ctx, c, `${step.id}: queued PRD or sources changed; review them, then gallivant chain ${c.name} --retry --resnapshot`); return; }
|
|
149
|
-
const after = c.steps.slice(0, c.index).map(s => s.merged?.sha);
|
|
209
|
+
const after = [...(c.after ? [c.after.merged?.sha] : []), ...c.steps.slice(0, c.index).map(s => s.merged?.sha)];
|
|
150
210
|
for (const sha of after) {
|
|
151
|
-
if (!sha || !await ctx.
|
|
211
|
+
if (!sha || !await hostApi(ctx).containsCommit(c.repo, c.base, sha)) { await stop(ctx, c, `predecessor merge is no longer in ${c.base}; inspect the base history`); return; }
|
|
152
212
|
}
|
|
153
213
|
// Fail before cloning for a normal source collision; start rechecks against the actual clone to close the race.
|
|
154
|
-
const baseSha = await ctx.
|
|
155
|
-
await checkBaseFiles(p.input, step.baseFiles, path => ctx.
|
|
214
|
+
const baseSha = await hostApi(ctx).refSha(c.repo, c.base);
|
|
215
|
+
await checkBaseFiles(p.input, step.baseFiles, path => hostApi(ctx).fileAtStrict(c.repo, path, baseSha));
|
|
156
216
|
const dispatch = randomUUID();
|
|
157
217
|
c = ctx.state.updateChain(c.name, current => {
|
|
158
218
|
if (current.status !== "active" || current.index !== c.index || current.steps[current.index].dispatch) return {};
|
|
@@ -163,6 +223,7 @@ async function tickChain(ctx, c) {
|
|
|
163
223
|
step = c.steps[c.index];
|
|
164
224
|
try {
|
|
165
225
|
await start(ctx, { path: step.path, base: c.base, chain: { name: c.name, index: c.index, dispatch, digest: step.digest, baseFiles: step.baseFiles, after } });
|
|
226
|
+
saveActive(ctx, c, { reason: null, failures: 0, rateLimitUntil: null });
|
|
166
227
|
} catch (error) {
|
|
167
228
|
const entry = ctx.state.get(step.id);
|
|
168
229
|
// A post-registration failure (for example config persistence) still owns a valid run. Reconcile next tick.
|
|
@@ -186,6 +247,14 @@ export async function tickChains(ctx) {
|
|
|
186
247
|
try { await tickChain(ctx, chain); results.push({ chain: chain.name, ok: true }); }
|
|
187
248
|
catch (error) {
|
|
188
249
|
const current = ctx.state.getChain(chain.name);
|
|
250
|
+
if (error.code === "BB_RATE_LIMIT") {
|
|
251
|
+
const rateLimitUntil = new Date(Date.now() + rateLimitDelay(error.retryAfter)).toISOString();
|
|
252
|
+
const reason = `Bitbucket API rate limited; next chain check after ${rateLimitUntil}`;
|
|
253
|
+
saveActive(ctx, current, { rateLimitUntil, reason, failures: 0 });
|
|
254
|
+
ctx.log.warn(`chain ${chain.name}: ${reason}`);
|
|
255
|
+
results.push({ chain: chain.name, ok: false, error: reason });
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
189
258
|
const failures = (current.failures || 0) + 1;
|
|
190
259
|
if (error.code === "CHAIN_INPUT_CHANGED") await stop(ctx, current, error.message);
|
|
191
260
|
else if (failures >= MAX_FAILURES) await stop(ctx, current, `${error.message} (${failures} failed checks; retry after fixing)`);
|
|
@@ -203,9 +272,10 @@ export async function tickChains(ctx) {
|
|
|
203
272
|
export function chainRows(ctx) {
|
|
204
273
|
return ctx.state.chains().map(([name, c]) => {
|
|
205
274
|
const step = c.steps[c.index], entry = step ? ctx.state.get(step.id) : null;
|
|
275
|
+
const anchor = c.after && !c.after.merged ? c.after : null;
|
|
206
276
|
return { name, status: c.status, repo: c.repo, base: c.base, completed: c.index, total: c.steps.length,
|
|
207
|
-
current: step?.id || null, reason: c.reason || (c.status === "done" ? "all milestones merged" : entry ? (entry.reason || (entry.pr ? `waiting for PR #${entry.pr} to merge` : "waiting for the current run")) : "queued"),
|
|
208
|
-
url: entry?.url || null };
|
|
277
|
+
current: anchor?.id || step?.id || null, reason: c.reason || (c.status === "done" ? "all milestones merged" : anchor ? `waiting for predecessor PR #${anchor.pr} to merge` : entry ? (entry.reason || (entry.pr ? `waiting for PR #${entry.pr} to merge` : "waiting for the current run")) : "queued"),
|
|
278
|
+
url: anchor?.url || entry?.url || null };
|
|
209
279
|
});
|
|
210
280
|
}
|
|
211
281
|
|
|
@@ -215,6 +285,7 @@ export function renderChains(ctx, name = null) {
|
|
|
215
285
|
const lines = rows.map(c => `${c.name}: ${c.status} — ${c.completed}/${c.total} merged -> ${c.repo}:${c.base}${["done", "cancelled"].includes(c.status) ? "" : ` — ${c.reason}`}`);
|
|
216
286
|
if (name) {
|
|
217
287
|
const c = ctx.state.getChain(name);
|
|
288
|
+
if (c.after) lines.push(` after ${c.after.id} — ${c.after.merged ? "merged" : "waiting"} #${c.after.pr}\n ${c.after.url}`);
|
|
218
289
|
c.steps.forEach((s, i) => lines.push(` ${i + 1}. ${s.id} — ${s.merged ? `merged #${s.merged.pr}` : s.dispatch ? "started" : "queued"}\n ${s.path}`));
|
|
219
290
|
if (c.status === "stopped") lines.push(`Inspect the reason, then: gallivant chain ${name} --retry`);
|
|
220
291
|
}
|
package/lib/codex_cloud.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Supported CLI only. There is no private Cloud HTTP client or review engine.
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { execFileAsync
|
|
3
|
+
import { execFileAsync } from "./process.mjs";
|
|
4
|
+
import { repositoryMatches } from "./repository.mjs";
|
|
4
5
|
import { ensureDir, readJson, writeJsonAtomic } from "./fs.mjs";
|
|
5
6
|
import { executionError, dispatchNotSentError, redact, accessProbeFailure } from "./execution_io.mjs";
|
|
6
7
|
import { validateCloudConfig, CLOUD_VERSION, CLOUD_DISPATCH_UNAVAILABLE } from "./codex_cloud_config.mjs";
|