@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.
Files changed (129) hide show
  1. package/README.md +38 -16
  2. package/SECURITY.md +9 -0
  3. package/bin/gallivant.mjs +33 -10
  4. package/bin/promote-bitbucket.mjs +40 -0
  5. package/bin/task-pr.mjs +9 -0
  6. package/bin/task-review.mjs +15 -0
  7. package/docs/README.md +2 -0
  8. package/docs/architecture.md +11 -3
  9. package/docs/bitbucket-promotion.md +69 -0
  10. package/docs/bitbucket-review.md +330 -0
  11. package/docs/chains.md +30 -1
  12. package/docs/codex-cloud.md +7 -0
  13. package/docs/codex-review.md +50 -0
  14. package/docs/commands.md +65 -5
  15. package/docs/config.md +35 -10
  16. package/docs/first-party-review.md +266 -0
  17. package/docs/git-hosts.md +294 -0
  18. package/docs/guardrails.md +56 -0
  19. package/docs/install.md +18 -8
  20. package/docs/network-recovery.md +9 -0
  21. package/docs/openai-agents.md +2 -1
  22. package/docs/prd.md +23 -2
  23. package/docs/preparation.md +1 -1
  24. package/docs/release-qualification.md +1 -1
  25. package/docs/releasing.md +17 -2
  26. package/docs/task-delivery.md +4 -4
  27. package/docs/task-profiles.md +537 -0
  28. package/docs/tasks.md +59 -197
  29. package/lib/attribution.mjs +7 -3
  30. package/lib/block.mjs +10 -1
  31. package/lib/chains.mjs +101 -30
  32. package/lib/codex_cloud.mjs +2 -1
  33. package/lib/codex_cloud_input.mjs +15 -6
  34. package/lib/codex_cloud_result.mjs +1 -1
  35. package/lib/commands/doctor.mjs +22 -462
  36. package/lib/commands/init.mjs +72 -30
  37. package/lib/commands/register.mjs +25 -11
  38. package/lib/commands/resume.mjs +5 -1
  39. package/lib/commands/review.mjs +66 -0
  40. package/lib/commands/serve.mjs +8 -0
  41. package/lib/commands/start.mjs +30 -11
  42. package/lib/commands/status.mjs +18 -2
  43. package/lib/commands/task.mjs +9 -1
  44. package/lib/commands/up.mjs +19 -11
  45. package/lib/commands/why.mjs +7 -3
  46. package/lib/config.mjs +42 -9
  47. package/lib/context.mjs +23 -18
  48. package/lib/control.mjs +21 -6
  49. package/lib/credential.mjs +20 -14
  50. package/lib/deliverable.mjs +2 -0
  51. package/lib/events.mjs +96 -53
  52. package/lib/execution.mjs +48 -3
  53. package/lib/execution_preparation.mjs +16 -1
  54. package/lib/execution_result.mjs +26 -5
  55. package/lib/first_party_review.mjs +618 -0
  56. package/lib/gh.mjs +144 -56
  57. package/lib/git.mjs +133 -32
  58. package/lib/git_diff.mjs +121 -0
  59. package/lib/harness.mjs +14 -10
  60. package/lib/host_contract.mjs +71 -0
  61. package/lib/host_definitions.mjs +22 -0
  62. package/lib/hosts/bitbucket.mjs +553 -0
  63. package/lib/hosts/bitbucket_contract.mjs +57 -0
  64. package/lib/hosts/bitbucket_credential.mjs +62 -0
  65. package/lib/hosts/bitbucket_review.mjs +44 -0
  66. package/lib/hosts/bitbucket_review_producer.mjs +50 -0
  67. package/lib/hosts/github.mjs +57 -0
  68. package/lib/hosts/github_contract.mjs +27 -0
  69. package/lib/hosts/github_doctor.mjs +482 -0
  70. package/lib/hosts/github_review_summary.mjs +38 -0
  71. package/lib/hosts/transport_doctor.mjs +35 -0
  72. package/lib/hosts.mjs +130 -0
  73. package/lib/learning_qualification.mjs +1 -1
  74. package/lib/notify.mjs +1 -1
  75. package/lib/openai_agents_input.mjs +7 -2
  76. package/lib/openai_agents_sources.mjs +16 -8
  77. package/lib/plugin_package.mjs +13 -1
  78. package/lib/pr_operation.mjs +41 -0
  79. package/lib/prd.mjs +18 -3
  80. package/lib/preparation_codex.mjs +6 -1
  81. package/lib/probe.mjs +6 -4
  82. package/lib/process.mjs +24 -0
  83. package/lib/promotion.mjs +132 -0
  84. package/lib/promotion_git.mjs +63 -0
  85. package/lib/prompt.mjs +16 -13
  86. package/lib/provider_health.mjs +14 -3
  87. package/lib/repository.mjs +68 -0
  88. package/lib/repository_preflight.mjs +11 -0
  89. package/lib/review_evaluation.mjs +25 -0
  90. package/lib/review_evidence.mjs +500 -0
  91. package/lib/review_history.mjs +73 -0
  92. package/lib/review_image.mjs +145 -0
  93. package/lib/review_link.mjs +12 -0
  94. package/lib/review_policy.mjs +132 -0
  95. package/lib/review_policy_contract.mjs +18 -0
  96. package/lib/review_state.mjs +13 -0
  97. package/lib/review_visual.mjs +94 -0
  98. package/lib/runtime-environment.mjs +6 -0
  99. package/lib/runtime_node.mjs +3 -2
  100. package/lib/runtime_state.mjs +39 -5
  101. package/lib/sources/prd.mjs +6 -3
  102. package/lib/state.mjs +137 -24
  103. package/lib/task_completion.mjs +7 -3
  104. package/lib/task_contract.mjs +201 -27
  105. package/lib/task_delivery.mjs +9 -2
  106. package/lib/task_delivery_contract.mjs +13 -9
  107. package/lib/task_delivery_view.mjs +18 -2
  108. package/lib/task_environment.mjs +13 -1
  109. package/lib/task_limits.mjs +13 -0
  110. package/lib/task_model_config.mjs +99 -0
  111. package/lib/task_model_route.mjs +243 -0
  112. package/lib/task_prepare.mjs +21 -8
  113. package/lib/task_profile.mjs +1875 -0
  114. package/lib/task_repository_context.mjs +65 -0
  115. package/lib/tasks.mjs +252 -51
  116. package/lib/tick.mjs +353 -97
  117. package/lib/time.mjs +1 -0
  118. package/lib/tracker/pages.mjs +19 -5
  119. package/lib/tracker/review.mjs +21 -0
  120. package/lib/tracker_process.mjs +8 -1
  121. package/lib/verification_sandbox.mjs +2 -1
  122. package/lib/work_artifacts.mjs +79 -0
  123. package/npm-shrinkwrap.json +12 -3
  124. package/package.json +9 -3
  125. package/templates/plugin/task-delivery.SKILL.md +17 -10
  126. package/templates/plugin/task-intake-contract.json +18 -0
  127. package/templates/plugin/task-profile-requests.mjs +1448 -0
  128. package/templates/tracker/tracker.css +3 -0
  129. 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 authorize implementation. Add `--run` to the original request when you
17
- want implementation to start automatically once the brief is ready. This is
18
- execution authorization, including after clarification answers. It is not a
19
- merge, deployment or release approval. To open the dashboard after dispatch,
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
- This section is a detailed reference for requests that need clarification. For
44
- a first run, use the example above; for saved drafts, go to
45
- [inspect, answer and start](#inspect-answer-and-start).
46
-
47
- For a new request such as “Implement CSV and JSON export, test both, and open one
48
- pull request for review,” Gallivant preserves the original text and records exact
49
- source spans assigning only the affirmative publication clause to the coordinator.
50
- All remaining implementation requirements and prohibitions stay in the provider's
51
- acceptance criteria. The generated brief's **Coordinator delivery contract** is
52
- part of both initial approval and ongoing change detection. Its publication
53
- obligation is fulfilled only by matching canonical repository, PR, base, head and
54
- completion-gate evidence; a provider statement cannot fulfill it.
55
-
56
- New ownership contracts also include a same-statement connector such as `, and`,
57
- `and then` or `then` in the publication span, so it cannot become a dangling
58
- implementation requirement. Connectors split across lines or containing quoted
59
- implementation text need clarification. Previously approved contracts keep their
60
- original spans, criteria and fingerprints on retry; new requests use version five.
61
- Version five keeps feature metadata and synthetic-object qualifiers within one
62
- line, so a following line beginning with “title” or “body” cannot hide an earlier
63
- PR instruction. Saved versions one through four retain their approved feature
64
- interpretation and exact source spans.
65
-
66
- This first recognizer supports one unqualified `open/create/raise/submit` review-PR
67
- clause outside quoted text/code. It is not a general natural-language permission
68
- parser. Conditional or negated publication clauses, owner approval prerequisites,
69
- destination qualifiers, multiple PRs and delivery-only requests return
70
- `TASK_DELIVERY_SCOPE` before inference. Unsupported publication actions also
71
- require clarification; they never become impossible provider obligations.
72
- Initial requests and later answers share the same bounded directive checks.
73
- Local-only, no-push, task-hold or patch-only directions cannot silently fall back
74
- to the default review-PR workflow. Preference wording such as “I don't want a PR”
75
- or “We don't need a pull request” also requires clarification.
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 nonblank
271
- request and answer lines, matching its mapping capacity. Oversize requests stop
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
- `--run` on preparation persists execution authorization through questions and
492
- answers. Once the exact generated brief and source snapshot pass readiness, the
493
- service starts it once and brings up the normal daemon. No second confirmation is
494
- needed. If the process exits after saving readiness but before dispatch, retrying
495
- the same preparation request or answer identity resumes that saved authorization
496
- through the exact approval and dispatch-ownership checks. A preparation-only
497
- retry still returns the draft without implementation.
498
- Preparation without `--run` returns the draft and an explicit next action.
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. All nonblank request lines are immutable
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. No extra confirmation is added to
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;
@@ -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 COMMIT_MSG_HOOK = `#!/usr/bin/env node
79
- // Written by Gallivant (lib/attribution.mjs). Refuses a commit message that credits an AI harness.
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 { repoFromUrl } from "./gh.mjs";
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.gh.branchExists(repo, base)) throw new Error(`base ${repo}:${base} is missing or GitHub is unavailable`);
34
- const sha = await ctx.gh.refSha(repo, base);
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.gh.fileAtStrict(repo, path, sha)));
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
- export async function queueChain(ctx, { name, paths, base = null }) {
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 (!Array.isArray(paths) || paths.length < 2) throw new Error("a chain needs at least two ready PRD paths in execution order");
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
- // The normal run remains the recovery surface. Never have two milestone agents mutating dependent work.
126
- if (!entry.pr || (entry.last?.run && entry.last.exit == null) || ctx.runnerFor(step.id).current()?.running) return;
127
- const pr = await ctx.gh.prView(c.repo, entry.pr);
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 = { pr: entry.pr, sha, at: pr.mergedAt };
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.gh.containsCommit(c.repo, c.base, sha)) { await stop(ctx, c, `predecessor merge is no longer in ${c.base}; inspect the base history`); return; }
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.gh.refSha(c.repo, c.base);
155
- await checkBaseFiles(p.input, step.baseFiles, path => ctx.gh.fileAtStrict(c.repo, path, baseSha));
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
  }
@@ -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, repositoryMatches } from "./gh.mjs";
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";