@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/README.md CHANGED
@@ -19,10 +19,10 @@ Run this inside the Git checkout you want to work in:
19
19
  npx --yes @uchibeke/gallivant init --harness codex # or: --harness claude
20
20
  ```
21
21
 
22
- The setup flow below requires **0.2.1 or newer**. At verification on 2026-10-20,
23
- npm's `latest` was still 0.2.0, whose `init` only creates the config. A maintainer
24
- must [publish 0.2.1](docs/releasing.md) before this command provides repository
25
- registration, repeat safety and the diagnostic report described below.
22
+ The setup flow below requires **0.2.1 or newer**. `npx` runs npm's latest published
23
+ version. Changes merged on `main` become available through npm after a
24
+ [versioned release](docs/releasing.md); check the registry with
25
+ `npm view @uchibeke/gallivant version`.
26
26
 
27
27
  `init` writes a private config at `~/.gallivant/config.json`, records this
28
28
  repository, creates a separate directory for task checkouts, and ends with the
@@ -62,8 +62,8 @@ from you or an account with a third party.
62
62
 
63
63
  - **Reviewer.** The default loop waits for the Codex GitHub review integration
64
64
  (`chatgpt-codex-connector[bot]`). Enable it on the repository, configure
65
- another reviewer, or set `"reviewer": null` in the config to rely on CI and
66
- trusted human feedback. Without one of these, the first PR waits for a review
65
+ another reviewer, or opt into a [qualified first-party reviewer](docs/first-party-review.md).
66
+ Explicit legacy no-review needs both `"reviewer": null` and `review.legacy_no_review: true`. Without one of these, the first PR waits for a review
67
67
  that never arrives. See [the reviewer key](docs/config.md#the-reviewer-object).
68
68
  - **Branch protection.** `doctor` prints the `gh api` call that creates a
69
69
  ruleset blocking direct and force pushes to the default branch. Run it
@@ -80,14 +80,17 @@ from you or an account with a third party.
80
80
 
81
81
  ```sh
82
82
  gallivant task "Document how to run the existing tests in CONTRIBUTING.md. Use the commands already defined in the repository. Change documentation only." \
83
- --harness codex --verify 'git diff --check' --run
83
+ --harness codex --verify 'git diff --check'
84
+ # Review the draft, then substitute its returned task ID and revision:
85
+ gallivant task start TASK_ID --revision REVISION --run
84
86
  gallivant start
85
87
  ```
86
88
 
87
- What happens: `task` uses your harness to prepare a brief, which is a paid
88
- inference call, and may ask you questions. `--run` authorizes implementation
89
- once the brief is ready; omit it to read the draft first and follow the printed
90
- `Next` action. The agent then works in a clone under `~/.gallivant-worktrees`,
89
+ `task` uses your harness to prepare a brief, which is a paid inference call,
90
+ and may ask you questions. Review the draft and answer any questions, then use
91
+ its printed `Next` action to approve that revision with `task start --run`.
92
+ Preparation always returns for review, even if its command includes `--run`.
93
+ The agent then works in a clone under `~/.gallivant-worktrees`,
91
94
  pushes a branch, opens a PR against the default branch, and the daemon watches CI
92
95
  and the reviewer, sending each round of feedback back to the same session.
93
96
  `gallivant start` opens the dashboard at **http://127.0.0.1:8791** and keeps
@@ -119,7 +122,7 @@ and the same review loop. [Writing briefs](docs/prd.md) · [Task commands](docs/
119
122
  2. Each tick (every two minutes by default) the daemon reads CI and reviewer
120
123
  state. A blocking review or a failing check starts a fix round in the same
121
124
  agent session, and the fix is pushed to the same PR.
122
- 3. Rounds stop at `max_iterations` (30 by default), when the reviewer stays
125
+ 3. Rounds stop at `max_iterations` (60 by default), when the reviewer stays
123
126
  silent for `stall_hours`, or when a run exceeds its timeout. Every stop is
124
127
  recorded with a reason you can read with `gallivant why`.
125
128
  4. **Ready** means the configured checks, reviewer and acceptance gate passed.
@@ -210,7 +213,7 @@ for the installed version's flags.
210
213
  daemon, including when execution is hosted. Gallivant is not a managed service.
211
214
  - **Your accounts pay for inference.** Subscriptions and API credits differ.
212
215
  There is no automatic provider fallback. Review [limits](docs/config.md) before
213
- unattended work; the default review allowance is 30 rounds.
216
+ unattended work; the default review allowance is 60 rounds.
214
217
  - **Your permissions apply.** Scope GitHub access to the required repositories.
215
218
  Harness permissions, OS sandboxing and optional APort checks are different
216
219
  controls. [Security boundaries](docs/guardrails.md).
@@ -223,9 +226,20 @@ for the installed version's flags.
223
226
 
224
227
  ## Documentation
225
228
 
226
- [Getting started](docs/install.md) · [Agent setup](docs/agent-setup.md) ·
227
- [Commands](docs/commands.md) · [Configuration](docs/config.md) ·
228
- [Dashboard](docs/app.md) · [All guides](docs/README.md)
229
+ | Need | Guide |
230
+ | --- | --- |
231
+ | Install and run your first task | [Getting started](docs/install.md) |
232
+ | Start from a request, answer questions, inspect results | [Natural-language tasks](docs/tasks.md) |
233
+ | Select the typed delivery profile and planner route for a request | [Typed delivery profiles](docs/task-profiles.md) |
234
+ | Write a brief or define noncoding acceptance checks | [PRD contract](docs/prd.md) |
235
+ | Use API-backed hosted execution | [OpenAI Agents](docs/openai-agents.md) |
236
+ | Dashboard messages, controls and recovery | [Dashboard](docs/app.md) · [Commands](docs/commands.md) |
237
+ | Models, credentials, limits and notifications | [Configuration](docs/config.md) |
238
+ | Plan sequential work | [Chains](docs/chains.md) |
239
+ | Embed task delivery in another application | [Task-delivery package](docs/task-delivery.md) |
240
+ | Understand internals or recorded qualification | [Documentation index](docs/README.md) |
241
+ | Set up from an agent | [Agent setup](docs/agent-setup.md) |
242
+ | Release, contribute or report a vulnerability | [Releasing](docs/releasing.md) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md) |
229
243
 
230
244
  **Asking a coding agent to set this up?** Give it the
231
245
  [setup-agent instructions](docs/agent-setup.md). They cover inspection, the
@@ -238,3 +252,11 @@ hosted service or publish a chat plugin.
238
252
 
239
253
  [Contributing](CONTRIBUTING.md) · [Security reports](SECURITY.md) ·
240
254
  [Releasing](docs/releasing.md) · [Apache-2.0 license](LICENSE)
255
+
256
+ ## Bitbucket Cloud
257
+
258
+ Bitbucket Cloud tasks support the same independent local Codex review service as GitHub. The adapter captures an exact same-repository PR comparison through Bitbucket's commit, diffstat, merge-base and source APIs, then the shared service divides large evidence into bounded jobs. Activation still requires a successful live route qualification; until that and the disposable private/public review matrix pass, keep an existing run's explicit legacy no-review policy. A prior public disposable run qualified PR publication and receipt acceptance only, not semantic review. See the [setup, activation limits and troubleshooting guide](docs/bitbucket-review.md). Bitbucket-only setup needs no GitHub sign-in and does not install a pipeline or alter repository settings.
259
+
260
+ ## First-party review (opt-in)
261
+
262
+ New tasks can pin `external-required`, `first-party-required`, or `both-required`. Repositories with the configured external reviewer retain their existing default. First-party review requires a separately qualified restricted route; it never claims Git-host bot approval. GitHub and Bitbucket Cloud are the only evidence adapters currently implemented. See [selection, qualification and migration](docs/first-party-review.md) and the [current live qualification gap](docs/audit/first-party-review.md). Existing no-review runs keep that requirement until the audited policy command changes them and disclose the absence of semantic review.
package/SECURITY.md CHANGED
@@ -17,3 +17,12 @@ they need. Automatic approval is not equivalent to an operating-system sandbox.
17
17
  The [OpenAI Agents API adapter](docs/openai-agents.md) keeps service credentials
18
18
  on the coordinator and publishes returned artifacts through that coordinator.
19
19
  Its hosted execution controls differ from local harness hooks and sandboxes.
20
+
21
+ ## Bitbucket reviewer isolation
22
+
23
+ Use a separate reviewer account and keep its publication credential outside the
24
+ coding process's filesystem and environment access. A private file readable by
25
+ the coding identity does not provide isolation. The review model receives the
26
+ prepared diff and a read-only checkout; an operator publishes its exact-head
27
+ verdict. See [reviewer setup](docs/bitbucket-review.md#independent-local-codex-reviewer).
28
+ Bitbucket dispatch remains disabled pending live qualification.
package/bin/gallivant.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // CLI entry. Parses arguments, builds the context, calls one command. No logic lives here.
3
+ import { reviewCommand } from "../lib/commands/review.mjs";
3
4
  import { acquireCliMutation } from "../lib/daemon.mjs";
4
5
  import { parseArgs } from "node:util";
5
6
  import { makeContext } from "../lib/context.mjs";
@@ -28,7 +29,7 @@ import { safeDisplay } from "../lib/learning_data.mjs";
28
29
 
29
30
  const HELP = `gallivant: give an agent a task, let the review loop run, go outside.
30
31
 
31
- gallivant init [--harness codex|claude] [--worktrees <directory>]
32
+ gallivant init [--harness codex|claude] [--worktrees <directory>] [--host-config <json>] [--base <branch>]
32
33
  set up the repository you are in: private config, this repo in repos,
33
34
  then doctor's report. Safe to repeat; never signs in or overwrites settings
34
35
  gallivant --version print the installed version
@@ -51,15 +52,18 @@ const HELP = `gallivant: give an agent a task, let the review loop run, go outsi
51
52
  write a starting PRD into docs/prds/ of the repo you are in
52
53
  gallivant task [prepare] "<request>" [--stdin] [--dir <checkout>] [--repo <owner/repo>]
53
54
  [--base <branch>] [--harness <name>] [--context <file>] [--request-id <id>] [--run] [--json]
54
- [--completion-target delivery_ready|integrated] [--verify <command>] [--no-daemon]
55
- prepare an isolated brief; --run authorizes its exact ready revision
55
+ [--completion-target delivery_ready|integrated] [--delivery pull_request] [--route harness|model]
56
+ [--verify <command>] [--no-daemon]
57
+ prepare an isolated brief for review; approve later with task start --run
58
+ --delivery selects the typed delivery profile (the only delivery authority);
59
+ --route drafts through the connected harness or the operator's bounded model route
56
60
  Claude, Codex and OpenAI Agents support isolated preparation.
57
61
  Other local harnesses use written PRDs; Codex Cloud dispatch is unavailable
58
62
  gallivant task answer <task-id> --revision <n> --request-id <answer-id> --answer <question-id=text>
59
63
  resume saved questions; --answer, --context and --verify may repeat
60
64
  gallivant task inspect <task-id> [--json] | task list [--json]
61
65
  gallivant task resume|validate|start <task-id> --revision <n> [--run] [--no-daemon] [--json]
62
- shared versioned operations; start needs prior or --run authorization
66
+ shared versioned operations; start --run approves the reviewed revision
63
67
  gallivant ready <prd.md> [--check] check the PRD against the contract; flip it to ready when it passes
64
68
  gallivant start [prd.md] [--no-daemon] [--no-server] [--no-open] [--port 8791] [--base <branch>]
65
69
  start the daemon and dashboard, then open it; optionally start a ready PRD
@@ -67,6 +71,7 @@ const HELP = `gallivant: give an agent a task, let the review loop run, go outsi
67
71
  gallivant register <pr-url> [--harness <name>] [--session <id>] [--worktree <dir>]
68
72
  watch a hand-made PR; on an existing entry, reset it
69
73
  gallivant chain <name> <prd1.md> <prd2.md> ... [--base <branch>]
74
+ gallivant chain <name> --after <pr-url|handle> <next.md> ...
70
75
  queue ready PRDs; each starts after the previous PR is merged
71
76
  gallivant chain [<name>] [--cancel | --retry [--resnapshot]]
72
77
  inspect chains, stop future work, or retry after fixing a blocker
@@ -81,6 +86,15 @@ const HELP = `gallivant: give an agent a task, let the review loop run, go outsi
81
86
  --resnapshot makes an edited brief the one the gate judges
82
87
  --fresh-session replaces retired learned memory after settlement
83
88
  gallivant recheck <handle> queue a PR recheck without resetting its budget
89
+ gallivant review retry <handle> --reason <text>
90
+ retry confirmed incomplete or coordinator-cancelled review; retain pieces and limits
91
+ gallivant review qualify --repo <owner/repo> [--capability text|image] [--run [--wait] [--retry --reason <text>]]
92
+ inspect qualification; --run launches a bounded probe; --wait records its exit before returning
93
+ gallivant review status <handle> show pinned policy, owned verdict and unresolved review evidence
94
+ gallivant review policy <handle> --mode external-required|first-party-required|both-required --reason "..."
95
+ explicitly replace policy and re-evaluate the current head
96
+ gallivant review dispute <handle> --finding <id> --reason "..."
97
+ record a dispute for a person; never accepts the author's own dispute
84
98
  gallivant cancel <handle> stop further dispatch; request provider cancellation
85
99
  gallivant say <handle> --note "..." [--interrupt]
86
100
  send an update in the same session; --interrupt stops the active run first
@@ -94,15 +108,17 @@ const OPTIONS = {
94
108
  send: { type: "boolean" }, probe: { type: "boolean" }, help: { type: "boolean", short: "h" }, version: { type: "boolean", short: "v" },
95
109
  "no-server": { type: "boolean" }, "no-open": { type: "boolean" }, foreground: { type: "boolean" }, worktrees: { type: "string" },
96
110
  "no-daemon": { type: "boolean" }, note: { type: "string" }, resnapshot: { type: "boolean" }, "fresh-session": { type: "boolean" }, interrupt: { type: "boolean" },
97
- base: { type: "string" }, harness: { type: "string" }, session: { type: "string" }, worktree: { type: "string" }, dir: { type: "string" }, port: { type: "string" },
98
- cancel: { type: "boolean" }, retry: { type: "boolean" },
111
+ base: { type: "string" }, after: { type: "string" }, harness: { type: "string" }, session: { type: "string" }, worktree: { type: "string" }, dir: { type: "string" }, port: { type: "string" },
112
+ cancel: { type: "boolean" }, retry: { type: "boolean" }, wait: { type: "boolean" },
99
113
  run: { type: "boolean" }, stdin: { type: "boolean" }, repo: { type: "string" }, revision: { type: "string" },
100
114
  "request-id": { type: "string" }, context: { type: "string", multiple: true },
101
- "completion-target": { type: "string" },
115
+ "completion-target": { type: "string" }, delivery: { type: "string" }, route: { type: "string" },
102
116
  answer: { type: "string", multiple: true }, verify: { type: "string", multiple: true },
103
117
  since: { type: "string" }, until: { type: "string" }, "dry-run": { type: "boolean" },
104
118
  mode: { type: "string" }, status: { type: "string" }, from: { type: "string" }, reason: { type: "string" },
119
+ "host-config": { type: "string" },
105
120
  "key-path": { type: "string" }, "ssh-keygen": { type: "string" }, "learning-budget": { type: "string" },
121
+ finding: { type: "string" }, capability: { type: "string" },
106
122
  };
107
123
 
108
124
  async function dispatch(argv) {
@@ -111,9 +127,10 @@ async function dispatch(argv) {
111
127
  const [cmd, arg] = positionals;
112
128
  if (values.version) { console.log(VERSION); return 0; }
113
129
  if (values.help || !cmd || cmd === "help") { console.log(HELP); return 0; }
130
+ if (values.after !== undefined && (cmd !== "chain" || !values.after.trim())) throw new Error("--after requires a registered PR URL or handle when queuing a new chain");
114
131
  if (cmd === "init") {
115
132
  if (arg) throw new Error("init takes no positional argument; run it from inside the target checkout");
116
- const r = await init({ harness: values.harness, worktrees: values.worktrees });
133
+ const r = await init({ harness: values.harness, worktrees: values.worktrees, hostConfig: values["host-config"], base: values.base });
117
134
  const report = renderDoctor(r.doctor);
118
135
  console.log(renderInit(r, report));
119
136
  // Doctor's exit code, so a script or an agent reading only the status knows whether setup still has open items.
@@ -165,7 +182,7 @@ async function dispatch(argv) {
165
182
  }
166
183
  }
167
184
  const ctx = makeContext();
168
- const mutating = ["start", "resume", "register", "remove", "recheck", "say", "cancel"].includes(cmd) || cmd === "chain" && (positionals.length > 2 || values.cancel || values.retry);
185
+ const mutating = ["start", "resume", "register", "remove", "recheck", "say", "cancel"].includes(cmd) || cmd === "review" && arg !== "status" || cmd === "chain" && (positionals.length > 2 || values.cancel || values.retry);
169
186
  const owner = mutating ? acquireCliMutation(ctx.home) : null;
170
187
  try {
171
188
  if (cmd === "recheck" || cmd === "say" || cmd === "cancel") {
@@ -180,6 +197,11 @@ async function dispatch(argv) {
180
197
  return 0;
181
198
  }
182
199
  switch (cmd) {
200
+ case "review": {
201
+ const result = await reviewCommand(ctx, { action: arg, handle: positionals[2], repo: values.repo, mode: values.mode, reason: values.reason, finding: values.finding, run: values.run, capability: values.capability, retry: values.retry, wait: values.wait });
202
+ console.log(JSON.stringify(result, null, 2));
203
+ return result.qualified === false ? 1 : 0;
204
+ }
183
205
  case "chain": {
184
206
  const paths = positionals.slice(2);
185
207
  const operation = paths.length > 0 || values.retry;
@@ -188,7 +210,8 @@ async function dispatch(argv) {
188
210
  if (values.retry && paths.length) throw new Error("--retry uses the existing plan; do not pass PRD paths");
189
211
  if ((values.cancel || values.retry) && !arg) throw new Error("a chain name is required");
190
212
  if (values.base && !paths.length) throw new Error("--base is set when queuing a new chain");
191
- if (paths.length) await queueChain(ctx, { name: arg, paths, base: values.base });
213
+ if (values.after && (!paths.length || values.retry || values.cancel)) throw new Error("--after is set only when queuing a new chain with ready PRD paths");
214
+ if (paths.length) await queueChain(ctx, { name: arg, paths, base: values.base, after: values.after });
192
215
  else if (values.cancel) cancelChain(ctx, arg);
193
216
  else if (values.retry) await retryChain(ctx, { name: arg, resnapshot: values.resnapshot });
194
217
  console.log(renderChains(ctx, arg));
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { readFile, writeFile } from "node:fs/promises";
4
+ import { makeContext } from "../lib/context.mjs";
5
+ import { promote } from "../lib/promotion.mjs";
6
+ import { makePromotionTransfer } from "../lib/promotion_git.mjs";
7
+
8
+ const help = `Usage: npm run promote:bitbucket -- --github-pr URL --bitbucket-repo bitbucket.org/workspace/repo [--bitbucket-pr N] [--base main] [--branch name] [--out plan.json]
9
+ Default: read-only dry run. Inspect the plan, then add --apply --expected-plan DIGEST.
10
+ Merged sources use retained exact acceptance. If the reported base changed, --accepted-plan plan.json supplies an optional archived-base fallback.
11
+ No force push, merge or deployment. See docs/bitbucket-promotion.md.`;
12
+ let transfer;
13
+ try {
14
+ const { values } = parseArgs({ options: {
15
+ "github-pr": { type: "string" }, "bitbucket-repo": { type: "string" }, "bitbucket-pr": { type: "string" },
16
+ base: { type: "string" }, branch: { type: "string" }, out: { type: "string" }, "accepted-plan": { type: "string" },
17
+ apply: { type: "boolean", default: false }, "expected-plan": { type: "string" }, help: { type: "boolean", short: "h" },
18
+ } });
19
+ if (values.help) console.log(help);
20
+ else {
21
+ if (!values["github-pr"] || !values["bitbucket-repo"]) throw new Error(help);
22
+ if (values.apply && values.out) throw new Error("--out saves a dry-run plan only; omit it when applying");
23
+ let acceptedPlan;
24
+ if (values["accepted-plan"]) {
25
+ const text = await readFile(values["accepted-plan"], "utf8");
26
+ if (Buffer.byteLength(text) > 1_048_576) throw new Error("Accepted plan is oversized");
27
+ acceptedPlan = JSON.parse(text);
28
+ }
29
+ const ctx = makeContext({ readOnly: true });
30
+ transfer = await makePromotionTransfer(ctx);
31
+ const result = await promote(ctx, {
32
+ githubPr: values["github-pr"], bitbucketRepo: values["bitbucket-repo"],
33
+ ...(values["bitbucket-pr"] !== undefined ? { bitbucketPr: Number(values["bitbucket-pr"]) } : {}),
34
+ branch: values.branch, base: values.base, acceptedPlan, apply: values.apply, expectedPlan: values["expected-plan"],
35
+ }, transfer);
36
+ if (values.out) await writeFile(values.out, `${JSON.stringify(result.plan, null, 2)}\n`, { flag: "wx", mode: 0o600 });
37
+ console.log(JSON.stringify(result, null, 2));
38
+ }
39
+ } catch (error) { console.error(error.message); process.exitCode = 1; }
40
+ finally { await transfer?.close(); }
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import { prOperation } from "../lib/pr_operation.mjs";
3
+ try {
4
+ const result = await prOperation(process.argv.slice(2));
5
+ console.log(typeof result === "string" ? result : JSON.stringify(result, null, 2));
6
+ } catch (error) {
7
+ console.error(error.message);
8
+ process.exitCode = 1;
9
+ }
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { readFileSync } from "node:fs";
4
+ import { makeBitbucketReviewProducer } from "../lib/hosts/bitbucket_review_producer.mjs";
5
+ try {
6
+ const { values, positionals } = parseArgs({ allowPositionals: true, options: Object.fromEntries(["config", "url", "input", "result"].map(key => [key, { type: "string" }])) });
7
+ if (positionals.length !== 1 || !["prepare", "publish"].includes(positionals[0]) || !values.config || !values.url) throw new Error("Use task-review prepare|publish --config <private-reviewer-config> --url <exact-PR-URL> [--input <prepared-json> --result <verdict-json>]");
8
+ const json = path => JSON.parse(readFileSync(path, "utf8"));
9
+ const producer = makeBitbucketReviewProducer({ config: json(values.config) });
10
+ const result = positionals[0] === "prepare" ? await producer.prepare(values.url) : await producer.publish(values.url, json(values.input), json(values.result));
11
+ console.log(JSON.stringify(result, null, 2));
12
+ } catch (error) {
13
+ console.error(error.message);
14
+ process.exitCode = 1;
15
+ }
package/docs/README.md CHANGED
@@ -17,9 +17,11 @@ requirements and checks.
17
17
  | Install, configure access and complete a first task | [Getting started](install.md) |
18
18
  | Ask an agent to inspect, install and verify setup | [Agent setup](agent-setup.md) |
19
19
  | Describe work, answer questions, inspect a draft and authorize execution | [Natural-language tasks](tasks.md) |
20
+ | Choose the typed delivery profile, planner route and review a prepared task | [Typed delivery profiles](task-profiles.md) |
20
21
  | Write a brief with acceptance criteria, source files and verification commands | [PRD contract](prd.md) |
21
22
  | Find commands, monitor progress, message or resume a run | [Commands](commands.md) |
22
23
  | Configure repositories, credentials, harnesses and execution limits | [Configuration](config.md) |
24
+ | Request and verify Codex reviews against the current commit | [Codex review procedure](codex-review.md) |
23
25
  | Use the local dashboard and understand product boundaries | [Application](app.md) |
24
26
  | Run coding work on an API-backed hosted environment | [OpenAI Agents API](openai-agents.md) |
25
27
  | Understand the unavailable Codex Cloud dispatch and its required qualification | [Codex Cloud](codex-cloud.md) |
@@ -38,6 +38,10 @@ lib/events.mjs pure rules: what counts as an event, trust, reviewer ve
38
38
  lib/prompt.mjs start, register and fix prompts; quoting and caps
39
39
  lib/harness.mjs capability registry plus local argv, session, outcome and sign-in adapters
40
40
  lib/tasks.mjs natural-language intake, revisions, validation, readiness and explicit execution consent
41
+ lib/task_profile.mjs portable typed delivery profile, deterministic delivery screen and planner tag coverage (vendored by Agent Passport)
42
+ lib/task_model_route.mjs bounded text-only model route for drafting; pre-call allowance checks, usage-derived cost
43
+ lib/task_model_config.mjs operator configuration and absolute ceilings for that route
44
+ lib/task_repository_context.mjs read-only, bounded repository snapshot pinned to the prepared revision
41
45
  lib/task_delivery.mjs authenticated host operation boundary; delegates to the same task service
42
46
  lib/execution.mjs durable local/remote dispatch, ownership, observations and recovery
43
47
  lib/execution_result.mjs owned hosted artifact verification and coordinator publication
@@ -72,10 +76,10 @@ templates/ passport, Claude sandbox settings, PRD, tracker/{tracke
72
76
 
73
77
  ## Design rules
74
78
 
75
- New intake tasks targeting `delivery_ready` stop automatic observation and fixing
79
+ Legacy review policies on intake tasks targeting `delivery_ready` stop automatic observation and fixing
76
80
  at the qualified saved handoff. Explicit queued recheck/resume/message controls
77
81
  reopen the workflow. `integrated` targets and legacy written-PRD runs retain
78
- monitoring. `completionStopsAutomation` and the exported `coordinatorEntryActive`
82
+ monitoring. Version-1 review policies keep ready deliveries monitored so late trusted feedback and changed input can revoke readiness. `completionStopsAutomation` and the exported `coordinatorEntryActive`
79
83
  keep core and host schedulers aligned. The review transitions below apply while
80
84
  an entry is monitored; malformed or incomplete delivery proof cannot mark it
81
85
  finished or suppress work.
@@ -215,7 +219,7 @@ For each registry entry that is not `done`:
215
219
  6. **Dispatch.** Past the cap, stop with `iterations`. Otherwise recreate the checkout if it is gone, merge the base branch into the work branch, build the fix prompt, resume the session (or start one for a registered PR), move the cursor to the newest event and record the ids dispatched at that instant, record the run.
216
220
 
217
221
  The merge is where a round of work starts from the current base rather than from wherever the base was when the branch was cut, and it is a merge rather than a rebase for three reasons that all live in this codebase. A rebase of a pushed branch can only move with a force-push, which the rules every session is given forbid. `reviewerVerdict` clears a head only when the commit the reviewer named is that head, so a rewrite makes every prior clearance describe a commit that no longer exists, and the reviewer's inline comments are anchored to those same commits. And step 7 reads a head the loop did not put there as a person's push, so a daemon force-push would look to the daemon itself like a stranger pushing. A merge moves the head forward, keeps every reviewed commit an ancestor, and needs no `--force`. An outstanding review therefore does not hold it up, which is the point: a rebase would have had to wait for the review, and that is a deadlock where the reviewer waits for a fix and the fix waits for the reviewer. The merge commit stays local until the session pushes, so nothing on GitHub moves, the reviewer clock is not reset and the round is still counted from the session's own push. It is recorded the moment it succeeds rather than by the save at the end of this step, because four things after it can return early and a merge nobody recorded is a change to a checkout with nothing to explain it. A conflict is aborted and the entry stops with `base-conflict` naming the base, because a half-merged checkout handed to a session as if it were clean is the worst thing this step could leave, and a conflict does not resolve on its own so retrying every tick would loop forever. Four things that would refuse identically on every poll stop the entry instead of looping: a checkout that cannot be recreated (`no-worktree`, with the cause as the event), a harness that refuses the configured `context_paths` (`context-paths`), no GitHub credential (`no-credential`) and no git identity on the machine (`no-identity`, whose event is the two `git config --global` commands that fix it). None of them resolves on its own, and the entry would otherwise redispatch the same feedback every poll with nothing notified. One invariant orders the rest of this step: **a spawned run must be recorded before anything else can fail.** Everything that can throw, the checkout and the ready-label removal included, happens before the spawn, and the registry write that owns the run comes immediately after it with no `await` in between. A process the registry does not know about is the worst state the dispatch can leave, because the settle step reads it as an unrelated leftover and the feedback that caused it is still new, so the same feedback spawns a second fixer when the first one exits. The label removal is the one write that can therefore hold a dispatch back, and holding it back is bounded: five consecutive failures stop the entry with `stuck`. `gh.removeLabel` throws by default and swallows only gh's own "label not found", which is what makes that bound reachable at all: the lenient behaviour was once the default, one of the two callers omitted the flag, and a GitHub that refused the write resolved as though the label had merely been absent.
218
- 7. **Ready.** First, a head nobody told the loop about. Both watching states go through the same check, in one place: `fixed_sha` follows the head, the reviewer clock resets to the push and the nudge becomes available again, because the reviewer has not seen this commit. A `ready` entry also leaves ready with the label off, since its verdict, its green checks and its accepted receipt all describe a commit that is not the head any more. Handling it for `ready` alone used to leave a `pr-open` entry judging the new head with the old head's age, so the same tick that noticed the push could stop it as `reviewer-silent` on a wait that expired before the commit existed, and the nudge already spent on the old head was never reissued. Then, for a `pr-open` entry: no events, no run, not a draft, every check green (none counts as green), reviewer clear or absent. Then the brief, compared with the operator's own copy: this is the only place the daemon reads their working tree, and it is here because this is where the contract is about to be used to certify something. A contract that moved stops the entry with `prd-changed` and the difference named, before the gate runs and without setting `gate_skipped`, which describes the other thing entirely (a PR that reached READY with the gate unrun, where this entry is not reaching ready at all). An edit outside the contract re-takes the snapshot, notifies nobody and carries on. Then the completion gate (`deliverable.mjs`): the PR body's receipt must attest every acceptance criterion of the PRD with evidence; a deny starts a receipt round. A receipt whose shape is wrong is one of those denies, not an exception. The output scan needs the whole diff, so a diff that cannot be read or reaches the 16 MiB scan cap is no decision at all. Large hosted payloads are sent in overlapping pieces within the API request limit, with full criteria and attestations on each; every piece must allow. Unreadable diffs and unavailable evaluators use the same bounded `entry.retry` mechanism as writes, with the cause visible in `status` and `why`; five consecutive failures stop as `needs-human`, without a fixer run or iteration. The label goes on before the registry says ready, so a label write that fails leaves the entry in `pr-open` to be retried rather than a ready PR carrying no label, and those retries are counted on the same `entry.retry` as every other declining write: a token that cannot write labels at all used to repeat the readiness judgement, the completion gate included, on every poll forever, while `status` said the entry was waiting on a reviewer who had already answered. Only then notify, once.
222
+ 7. **Ready.** First, a head nobody told the loop about. Both watching states go through the same check, in one place: `fixed_sha` follows the head, the reviewer clock resets to the push and the nudge becomes available again, because the reviewer has not seen this commit. A `ready` entry also leaves ready with the label off, since its verdict, its green checks and its accepted receipt all describe a commit that is not the head any more. Handling it for `ready` alone used to leave a `pr-open` entry judging the new head with the old head's age, so the same tick that noticed the push could stop it as `reviewer-silent` on a wait that expired before the commit existed, and the nudge already spent on the old head was never reissued. Then, for a `pr-open` entry: no events, no run, not a draft, checks settled and the pinned review policy satisfied. Version-1 policies require explicit zero-check CI authorization for an empty check response; legacy no-review is disclosed as absence of review. Then the brief, compared with the operator's own copy: this is the only place the daemon reads their working tree, and it is here because this is where the contract is about to be used to certify something. A contract that moved stops the entry with `prd-changed` and the difference named, before the gate runs and without setting `gate_skipped`, which describes the other thing entirely (a PR that reached READY with the gate unrun, where this entry is not reaching ready at all). An edit outside the contract re-takes the snapshot, notifies nobody and carries on. Then the completion gate (`deliverable.mjs`): the PR body's receipt must attest every acceptance criterion of the PRD with evidence; a deny starts a receipt round. A receipt whose shape is wrong is one of those denies, not an exception. The output scan needs the whole diff, so a diff that cannot be read or reaches the 16 MiB scan cap is no decision at all. Large hosted payloads are sent in overlapping pieces within the API request limit, with full criteria and attestations on each; every piece must allow. Unreadable diffs and unavailable evaluators use the same bounded `entry.retry` mechanism as writes, with the cause visible in `status` and `why`; five consecutive failures stop as `needs-human`, without a fixer run or iteration. The label goes on before the registry says ready, so a label write that fails leaves the entry in `pr-open` to be retried rather than a ready PR carrying no label, and those retries are counted on the same `entry.retry` as every other declining write: a token that cannot write labels at all used to repeat the readiness judgement, the completion gate included, on every poll forever, while `status` said the entry was waiting on a reviewer who had already answered. Only then notify, once.
219
223
  8. **Silence.** Reviewer configured and quiet since the last request: nudge once after `rereview_minutes`, stop with `reviewer-silent` after `stall_hours`. Draft conversion in step 3 starts a fresh reviewer window, so time spent as a draft does not count as reviewer silence.
220
224
 
221
225
  The reviewer clock (`rereview.at`) is when the reviewer was last asked about the current head, and all three of those timers measure from it. Its best available value is the head commit's committer date, which is when the commit was made and not when it arrived on the branch, so it is floored at `rereview_minutes` ago: a commit made on Monday and pushed on Thursday must not stop the PR for a silence the reviewer was never given a chance to break. The rule is `reviewerClock` in `events.mjs`, pure and in one place because the tick is not its only writer: `reconcile` sets the same field on a recovered entry and goes through the same function, so a PR whose last round comment is older than `stall_hours` is not stopped `reviewer-silent` on the first tick after a crash recovery for silence that accrued while no daemon was running to ask.
@@ -263,3 +267,7 @@ a successful version probe alone cannot establish unattended support.
263
267
  ## Adding a reviewer
264
268
 
265
269
  No code. Set `reviewer.login`, `reviewer.rereview_comment` and `reviewer.blocking_pattern` in config.
270
+
271
+ ## Coordinator-owned first-party review
272
+
273
+ `review_policy.mjs` owns versioned task policy and input bindings; `review_evidence.mjs` captures complete snapshots and validates structured judgement. `first_party_review.mjs` advances keyed review jobs through the existing execution registry, in a separate session and allowance. The shared tick handles readiness, trusted external feedback and fix rounds. Review jobs do not create a second scheduler or publish independent approvals. Host adapters provide the same normalized evidence contract; only the existing GitHub boundary is implemented here. See [policy and recovery behavior](first-party-review.md), including native qualification and measured rollout limitations.
@@ -0,0 +1,69 @@
1
+ # Review on GitHub, deliver through Bitbucket
2
+
3
+ `npm run promote:bitbucket` transfers an accepted GitHub PR's **original commits** to Bitbucket Cloud and creates or updates its Bitbucket PR. It uses Git transport, the existing host adapters and the same review/check policy as the coordinator. It does not copy files, create commits, force-push, merge, enable CI or deploy.
4
+
5
+ This is an operator command from a Gallivant source checkout. Install dependencies with `npm ci`. Configure both hosts using the [Git host guide](git-hosts.md); Git push access and Bitbucket API repository/PR access are both required. The command uses the existing scoped credentials, including a configured Bitbucket SSH key. Never put a token in a repository URL or a plan file.
6
+
7
+ ## Before promotion
8
+
9
+ - Register and finish the GitHub run. It must retain an exact accepted completion and review binding under an explicit review policy. A label, green CI or the PR author's statement alone is insufficient.
10
+ - Stopped dispatch and queued/applying operator controls block promotion. Cancellation, messages, resume and recheck requests must be reconciled before accepted work can be delivered. These controls are checked again after plan validation and after a push; a cancellation during an already-running push can leave the branch updated, but prevents subsequent PR publication when observed.
11
+ - Only the exact remotely reviewed commit is transferred. Local checkout edits, untracked carried research inputs, environment files and session logs are neither read nor included. The transfer uses its own disposable bare Git repository, so an operator checkout may remain dirty or may already have been removed after merge.
12
+ - Specify the destination repository explicitly. No organization or repository mapping is inferred.
13
+ - For an existing Bitbucket PR, supply its number. This updates that PR rather than creating a duplicate.
14
+ - The Bitbucket base must have the same Git tree as the reviewed GitHub base. Its commit hash may differ because the two hosts merged earlier changes separately. Promote dependent work in order, after the preceding Bitbucket change has merged. Reconcile differing trees on GitHub and obtain review again.
15
+ - An existing destination branch must be an ancestor of the reviewed source head. Divergence is not resolved automatically.
16
+
17
+ GitHub Actions may remain disabled for a review-only mirror. Zero CI checks are accepted **only** when the source run's operator-approved review policy explicitly allows zero checks. Configured required checks still must exist and pass. Local test results and production release requirements are not replaced by that setting. Missing required external or first-party review blocks promotion.
18
+
19
+ ## Preview, approve and apply
20
+
21
+ For an existing mapped PR:
22
+
23
+ ```sh
24
+ npm run promote:bitbucket -- \
25
+ --github-pr https://github.com/example/app/pull/1 \
26
+ --bitbucket-repo bitbucket.org/example/app \
27
+ --bitbucket-pr 93 \
28
+ --out accepted-promotion.json
29
+ ```
30
+
31
+ The default is a remote read-only dry run. It fetches into a disposable temporary Git repository and prints the source and destination heads, bases, exact title/body, review policy, check count and a SHA-256 `digest`. `--out` saves that plan locally with mode 600 and refuses to overwrite an existing file. Keep private plans outside your repository; they contain private PR prose and the previous destination PR text, which may include its task metadata. Transport credentials are never added to the plan.
32
+
33
+ Inspect the complete plan, then rerun the same mapping with its digest:
34
+
35
+ ```sh
36
+ npm run promote:bitbucket -- \
37
+ --github-pr https://github.com/example/app/pull/1 \
38
+ --bitbucket-repo bitbucket.org/example/app \
39
+ --bitbucket-pr 93 \
40
+ --apply --expected-plan DIGEST_FROM_THE_DRY_RUN
41
+ ```
42
+
43
+ The digest is an exact human approval pin, not a substitute for fresh review evidence. Changed source code, review, checks, public text, destination branch, base or existing PR text requires another dry run. The command reads accepted state and operator controls from the run registry; it never writes that registry or starts another agent.
44
+
45
+ For a new PR, omit `--bitbucket-pr`. The default destination branch is `work/github-OWNER-REPO-pr-N`; `--branch` and `--base` select explicit alternatives. Subsequent runs reuse an existing PR only when its repository, branch/base and source-review provenance match. Otherwise supply an explicit PR mapping. Closed, cross-repository or duplicate targets are rejected.
46
+
47
+ The Bitbucket title and human-readable body come from the GitHub PR, with a source-review link and original commit SHA appended. Source task receipts and recovery/session metadata are removed because they do not authorize a new host/task. The source GitHub PR retains the complete evidence.
48
+
49
+ ## After merging on GitHub
50
+
51
+ Run the same dry-run command after merge. When GitHub still supplies the original reviewed base, the coordinator's retained acceptance hash is checked directly against the original head, base, body, checks and policy. No pre-merge plan file is required. The accepted local record and fresh clear reviewer/check evidence remain mandatory; merging a PR before it was accepted does not manufacture that authority.
52
+
53
+ Optionally save `--out accepted-promotion.json` while the PR is accepted and still open. If GitHub later reports a different base, that archived plan can reconstruct the original base using:
54
+
55
+ ```sh
56
+ --accepted-plan /absolute/path/to/accepted-promotion.json
57
+ ```
58
+
59
+ Use the new printed digest for `--apply --expected-plan ...`, keeping `--accepted-plan` on that command too when using the fallback. The reconstructed binding must exactly match the coordinator's retained acceptance hash. The plan cannot replace a missing local acceptance record or authorize changed code, body, checks or policy.
60
+
61
+ The transferred head is the original reviewed PR head even when GitHub used squash or merge commits. The GitHub merge/squash commit is never described as reviewed or silently substituted. Squash merges can make subsequent branch histories diverge between hosts; prefer history-preserving merges for this bridge. A future branch that is not a fast-forward of its mapped Bitbucket branch must be reconciled and reviewed, not force-pushed.
62
+
63
+ ## Races and retries
64
+
65
+ Apply rechecks the plan before pushing. A native pre-push guard checks the server-advertised old branch SHA; Git's ordinary fast-forward update then compares that SHA on the server. No force or force-with-lease option is used.
66
+
67
+ There is no atomic transaction across GitHub and Bitbucket. The command rechecks evidence and destination after the push, before publishing the PR, and verifies the returned PR head/base. The publication adapter's final reconciliation read also enforces the approved PR absence or exact existing number and previous title/body. An observed concurrent creation or text edit stops publication. This adapter has no atomic conditional-update contract with Bitbucket: an edit between that final read and the write can still be overwritten without detection, so avoid concurrent PR editing during apply. If an operation fails, including a lost push response, the branch or PR may already have been updated; the error states that possibility. Inspect the target and rerun a dry run. Never respond with a force push. Bitbucket merge and deployment remain separate operator decisions and retain their normal release controls.
68
+
69
+ This bridge supports GitHub to Bitbucket Cloud only. It does not automatically schedule promotion, accept unregistered PRs, establish live production readiness, or migrate CI/secrets/deployment configuration.