@bridge_gpt/mcp-server 0.2.20 → 0.2.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONDUCTOR.md +86 -27
- package/README.md +80 -6
- package/build/base-ref.js +151 -0
- package/build/commands.generated.js +6 -4
- package/build/conductor/bridge-api-client.js +44 -3
- package/build/conductor/doctor.js +33 -22
- package/build/conductor/epic-runtime.js +101 -5
- package/build/conductor/pr-ci-producer.js +21 -2
- package/build/conductor/pr-discovery.js +12 -2
- package/build/conductor-bin.js +50 -20
- package/build/credential-store.js +564 -64
- package/build/executor/base-branch.js +50 -0
- package/build/executor/env.js +12 -1
- package/build/executor/job-errors.js +1 -0
- package/build/executor/job-runner.js +38 -7
- package/build/executor/test-clock.js +6 -1
- package/build/executor/worker-finalization.js +88 -1
- package/build/executor/worktree.js +21 -1
- package/build/index.js +1986 -428
- package/build/install-bridge.js +627 -69
- package/build/pipelines.generated.js +2 -2
- package/build/pr-base-contract.js +36 -0
- package/build/readme.generated.js +1 -1
- package/build/review-tickets.js +15 -5
- package/build/setup-epic.js +483 -0
- package/build/start-tickets.js +164 -75
- package/build/version.generated.js +1 -1
- package/build/worktree-core.js +62 -10
- package/package.json +3 -3
- package/public/js/main.min.js +9 -9
- package/public/js/main.min.js.map +1 -1
package/CONDUCTOR.md
CHANGED
|
@@ -3,14 +3,62 @@
|
|
|
3
3
|
Conductor is the **opt-in, off-by-default** coordination layer for running many
|
|
4
4
|
agent sessions together (epic supervision, inter-agent messaging, done-gate
|
|
5
5
|
evaluation, and conditional auto-merge). A normal `start-tickets` run does **not**
|
|
6
|
-
involve Conductor — you opt in per run with `--conductor
|
|
7
|
-
enables it internally.
|
|
6
|
+
involve Conductor — you opt in per run with `--conductor`.
|
|
8
7
|
|
|
9
|
-
This document is the reference for Conductor's
|
|
10
|
-
hooks, and the per-repo done-gate / auto-merge
|
|
11
|
-
`start-tickets` flags and cross-platform behavior, see
|
|
8
|
+
This document is the reference for Conductor's architecture, epic setup,
|
|
9
|
+
observability stream, local git hooks, and the per-repo done-gate / auto-merge
|
|
10
|
+
config. For the everyday `start-tickets` flags and cross-platform behavior, see
|
|
12
11
|
[README → CLI Subcommands](./README.md#cli-subcommands).
|
|
13
12
|
|
|
13
|
+
## Epic Conductor v2 — how an epic is actually driven
|
|
14
|
+
|
|
15
|
+
**The v1 `conductor epic-tick` command is frozen.** It throws
|
|
16
|
+
`EPIC_TICK_V1_FROZEN` on every invocation and advances nothing. There is nothing
|
|
17
|
+
to schedule locally. If you have an epic-tick schedule registered from an earlier
|
|
18
|
+
release, cancel it (`schedule-run cancel --id <id>`) — it is a dead timer.
|
|
19
|
+
`conductor doctor` flags one if it finds it.
|
|
20
|
+
|
|
21
|
+
v2 splits the old local tick into two halves:
|
|
22
|
+
|
|
23
|
+
| Half | Where it runs | What it does |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| **Reconciler** | **Server-side**, on the Bridge API worker dyno, every 30s | Selects every epic run whose status is `active`, evaluates gates, and enqueues executor jobs. Nothing to install or schedule. |
|
|
26
|
+
| **Executor** | **Locally**, on your machine | Polls for jobs, claims them, spawns worker agents, heartbeats. This is the only piece you run. |
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
npx -y @bridge_gpt/mcp-server executor --repo <name>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The reconciler drives a run purely off `epic_runs.status = 'active'` — there is no
|
|
33
|
+
local schedule, and no per-repo "enable the tick" flag. (An `epic_tick_enabled`
|
|
34
|
+
config field existed briefly and was inert; it has been removed.)
|
|
35
|
+
|
|
36
|
+
## Setting up an epic
|
|
37
|
+
|
|
38
|
+
One command creates the run, stores the plan, and approves it:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
npx -y @bridge_gpt/mcp-server setup-epic --epic-key BAPI-405 \
|
|
42
|
+
--plan-file docs/tmp/epic-plans/<slug>/epic-plan.dag.json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The plan sidecar (`epic-plan.dag.json`) is produced by the `decompose-epic`
|
|
46
|
+
pipeline step. `setup-epic` validates it locally (unique ticket keys, resolvable
|
|
47
|
+
`depends_on`/edge references, acyclicity) before sending anything, so a malformed
|
|
48
|
+
plan fails legibly instead of as a bare HTTP 400.
|
|
49
|
+
|
|
50
|
+
It is **idempotent**: re-running it on an epic that already has a live run reuses
|
|
51
|
+
that run rather than minting a second one. Use `--dry-run` to validate a plan and
|
|
52
|
+
preview the calls without mutating anything.
|
|
53
|
+
|
|
54
|
+
Once the plan is approved the run becomes `active`, the server-side reconciler
|
|
55
|
+
picks it up within ~30s, and your local `executor` starts claiming jobs.
|
|
56
|
+
|
|
57
|
+
> **A caveat worth knowing before you plan an epic.** Conductor has no
|
|
58
|
+
> merge-conflict handling, and plan-time file-overlap serialization is currently
|
|
59
|
+
> dark (the planner does not yet emit the `touched_files` metadata it needs). If
|
|
60
|
+
> two sibling tickets touch the same files, do not let them dispatch in parallel.
|
|
61
|
+
|
|
14
62
|
## Conductor observability (opt-in via `--conductor`, BAPI-394)
|
|
15
63
|
|
|
16
64
|
Conductor is **off by default** — without `--conductor` no `BAPI_CONDUCTOR_*` env,
|
|
@@ -28,7 +76,7 @@ in the env, hook command, or run metadata. Override the gate/supervisor labels w
|
|
|
28
76
|
`BAPI_CONDUCTOR_GATE_NAME` / `BAPI_CONDUCTOR_SUPERVISOR_MODE`. Inspect the stream
|
|
29
77
|
with `conductor doctor`. Observability is best-effort: a conductor failure never
|
|
30
78
|
blocks or aborts a spawn, and `--dry-run` performs no conductor side effects.
|
|
31
|
-
(Epic
|
|
79
|
+
(Epic dispatch always enables conductor internally, independent of this flag.)
|
|
32
80
|
|
|
33
81
|
When `--conductor` is set, the spawn boundary also injects
|
|
34
82
|
`BRIDGE_MCP_PROFILE=conductor` so each worker registers the 8 conductor/event/
|
|
@@ -61,10 +109,28 @@ hook presence and managed-snippet status **read-only** (a new `git hooks` sectio
|
|
|
61
109
|
MCP tool drives CI polling and gate evaluation regardless of whether hooks are
|
|
62
110
|
installed.
|
|
63
111
|
|
|
64
|
-
##
|
|
112
|
+
## Supervisor config — where the done gate and auto-merge live
|
|
113
|
+
|
|
114
|
+
> **These are not config fields.** They used to be `conductor_done_gate` and
|
|
115
|
+
> `conductor_auto_merge_enabled` on the generic config-field route; BAPI-438 moved
|
|
116
|
+
> them onto dedicated supervisor endpoints. Setting them via
|
|
117
|
+
> `PUT /jira/config-field/...` returns **HTTP 400 "Invalid config field"** — that
|
|
118
|
+
> rejection is correct, not a bug. Use the endpoints below.
|
|
119
|
+
|
|
120
|
+
Two scopes exist for each: a per-repo **project default**, and a per-epic override.
|
|
65
121
|
|
|
66
|
-
|
|
67
|
-
|
|
122
|
+
| Setting | Endpoint (project default) |
|
|
123
|
+
| --- | --- |
|
|
124
|
+
| `done_gate_config` | `PUT /jira/epic-runs/supervisor-setup/defaults/?repo_name=<repo>` |
|
|
125
|
+
| `auto_merge_enabled`, `merge_approval_required` | `PUT /jira/epic-runs/supervisor-config/defaults/?repo_name=<repo>` |
|
|
126
|
+
|
|
127
|
+
Swap `…/defaults/` for `…/runs/{epic_key}/…` to scope a setting to one epic. The
|
|
128
|
+
matching `GET` on each returns the effective value, with a `source` of `epic`,
|
|
129
|
+
`project_default`, or `none` (nothing configured — fails closed).
|
|
130
|
+
|
|
131
|
+
### `done_gate_config`
|
|
132
|
+
|
|
133
|
+
Defines the done gate. It supports exactly one condition,
|
|
68
134
|
`required_ci_checks_green`:
|
|
69
135
|
|
|
70
136
|
```json
|
|
@@ -81,30 +147,23 @@ config`) only when every listed required check is present, complete, and green f
|
|
|
81
147
|
the bound PR head SHA. The gate **fails closed**: an unset, disabled (`enabled` not
|
|
82
148
|
strictly `true`), malformed, empty, or unsupported config emits no `gate.met`.
|
|
83
149
|
|
|
84
|
-
|
|
150
|
+
### `auto_merge_enabled` (C6 conditional auto-merge)
|
|
85
151
|
|
|
86
152
|
When a worker's PR meets the done gate (`gate.met`), the supervisor can autonomously
|
|
87
|
-
merge it — but **only** when the repo has explicitly opted in.
|
|
88
|
-
`
|
|
89
|
-
as `conductor_done_gate`) is the opt-in switch:
|
|
90
|
-
|
|
91
|
-
```json
|
|
92
|
-
{ "enabled": true }
|
|
93
|
-
```
|
|
153
|
+
merge it — but **only** when the repo has explicitly opted in.
|
|
154
|
+
`auto_merge_enabled` on the supervisor-config endpoint is the opt-in switch.
|
|
94
155
|
|
|
95
|
-
|
|
96
|
-
Behavior:
|
|
156
|
+
**Auto-merge is disabled by default.** Behavior:
|
|
97
157
|
|
|
98
|
-
- **Disabled / unset /
|
|
99
|
-
`
|
|
100
|
-
|
|
101
|
-
|
|
158
|
+
- **Disabled / unset / unconfigured → dry-run.** Anything other than an explicit
|
|
159
|
+
`true` — including a repo with no supervisor config at all (`source: "none"`) —
|
|
160
|
+
fails **closed**: the supervisor records a `merge.dry_run` event and **no PR is
|
|
161
|
+
ever merged**.
|
|
102
162
|
- **Enabled → autonomous merge** when the gate is met and the deterministic guards
|
|
103
163
|
pass.
|
|
104
|
-
- **Kill-switch.** Set `
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
merge while the flag is off.
|
|
164
|
+
- **Kill-switch.** Set `auto_merge_enabled` to `false` to immediately stop
|
|
165
|
+
autonomous merges. The protected merge endpoint **independently re-enforces** the
|
|
166
|
+
flag, so even a conductor that calls it cannot merge while the flag is off.
|
|
108
167
|
|
|
109
168
|
Merge authority is **deterministic code, never an LLM**. The deterministic guards,
|
|
110
169
|
all bound to **PR number + expected head SHA (never a branch name)**:
|
package/README.md
CHANGED
|
@@ -37,6 +37,17 @@ store, and opens a fresh agent session to finish setup (`/install-bridge` then
|
|
|
37
37
|
API web UI **Security** page) and a **repo name** — everything else is derived. Add
|
|
38
38
|
`--dry-run` to preview every step without writing, pinging, or spawning anything.
|
|
39
39
|
|
|
40
|
+
**Were you sent a bootstrap invite?** Then you don't need an API key or the web UI
|
|
41
|
+
at all — run the command your operator gave you:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx -y @bridge_gpt/mcp-server@latest install-bridge --invite
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
That one-liner is deliberately **secret-free**: the CLI prompts for the bootstrap
|
|
48
|
+
invite token with **echo suppressed**, and sends it only in the request body. It
|
|
49
|
+
creates your project and mints your own admin API key in a single command.
|
|
50
|
+
|
|
40
51
|
To upgrade later, run:
|
|
41
52
|
|
|
42
53
|
```bash
|
|
@@ -76,19 +87,55 @@ derived). Resolution order:
|
|
|
76
87
|
- **API key:** `--api-key <key>` → `BAPI_API_KEY` env → an interactive (no-echo)
|
|
77
88
|
prompt. Generate one first on the Bridge API web UI **Security** page (see
|
|
78
89
|
[Generate an API Key](#2-generate-an-api-key)); the command consumes a key, it
|
|
79
|
-
never mints one
|
|
90
|
+
never mints one — **`--invite` is the one exception** (below). The key is
|
|
91
|
+
**never printed or logged**.
|
|
80
92
|
- **Repo name:** `--repo <name>` → `BAPI_REPO_NAME` env → an inferred default you
|
|
81
93
|
confirm interactively. It MUST match the server-side repository registration.
|
|
82
94
|
|
|
95
|
+
#### Bootstrap-invite onboarding (`--invite`)
|
|
96
|
+
|
|
97
|
+
With a **bootstrap invite** there is no pre-existing key and no web UI: this is the
|
|
98
|
+
one mode where `install-bridge` **creates** the project and its first admin key
|
|
99
|
+
instead of consuming one. Run `install-bridge --invite` and it:
|
|
100
|
+
|
|
101
|
+
1. **Prompts for the bootstrap invite token** with echo suppressed (the default —
|
|
102
|
+
see below).
|
|
103
|
+
2. **Generates your `key_secret`** (32 CSPRNG bytes) and **fsyncs it locally**
|
|
104
|
+
*before* contacting the server. If that write fails the run aborts and the
|
|
105
|
+
invite is **not** spent.
|
|
106
|
+
3. **Redeems the invite** — `POST /setup/bootstrap` with the token, repo name, and
|
|
107
|
+
`key_secret` in the **body** — which creates the project and mints your admin
|
|
108
|
+
key. (This replaces the connectivity ping: there is no key to ping with yet.)
|
|
109
|
+
4. **Verifies the newly-minted key**, then writes the per-host MCP config.
|
|
110
|
+
5. **Promotes the credential** to `bapi:<repo>` and **opens the agent session** —
|
|
111
|
+
the same Steps 3–5 as the normal flow.
|
|
112
|
+
|
|
113
|
+
Because the locally-saved `key_secret` is the only proof that can replay a
|
|
114
|
+
redemption, a re-run after a network failure is safe: it re-sends the *same* secret
|
|
115
|
+
and gets back the *same* project and key. If the repo name you chose is already
|
|
116
|
+
taken (names are globally unique) the server rolls back — your invite is untouched —
|
|
117
|
+
and the CLI asks for a different name and retries with the same token.
|
|
118
|
+
|
|
119
|
+
**The delivered one-liner is secret-free, by design.** "A copy/paste one-liner" and
|
|
120
|
+
"the token never touches shell history" are contradictory, so the token is *not* in
|
|
121
|
+
the command: the CLI asks for it, and it travels only in the request body.
|
|
122
|
+
`--invite <token>`, `--invite=<token>`, and `BAPI_INVITE` still work for
|
|
123
|
+
**scripting only** — and both forms **expose the token to your shell history and to
|
|
124
|
+
the process list**. Prefer the prompt.
|
|
125
|
+
|
|
83
126
|
Useful flags:
|
|
84
127
|
|
|
85
128
|
- `--dry-run` — preview every step (scaffold targets, config files and keys with
|
|
86
129
|
the key value **redacted**, the ping target, the credential store target, and
|
|
87
130
|
the exact agent spawn command) without writing, pinging, or spawning anything.
|
|
88
|
-
|
|
89
|
-
|
|
131
|
+
With `--invite` it also never calls the exchange endpoint and never generates or
|
|
132
|
+
stores a secret.
|
|
133
|
+
- `--force` — overwrite an existing real `BAPI_API_KEY` in a host config, or in the
|
|
134
|
+
credential store, without prompting (re-running is otherwise non-destructive).
|
|
90
135
|
- `--agent claude|cursor-agent` — which agent to launch for the agentic remainder
|
|
91
136
|
(default `claude`).
|
|
137
|
+
- `--invite [token]` — redeem a bootstrap invite (mutually exclusive with
|
|
138
|
+
`--api-key`). Omit the value to get the hidden prompt.
|
|
92
139
|
|
|
93
140
|
That's it — once `install-bridge` finishes you're connected. If you prefer to do
|
|
94
141
|
it by hand (or just want to understand each step), the manual flow below does the
|
|
@@ -276,7 +323,14 @@ These features are useful for most tickets.
|
|
|
276
323
|
- **What it does:** Creates one git worktree per ticket and spawns an agent session in each to implement them in parallel.
|
|
277
324
|
- **When it's useful:** (Implementation | Automation) When you're ready to start building one or more refined tickets concurrently.
|
|
278
325
|
- **How to use it:** `/start-tickets BAPI-248 BAPI-250` (see [CLI Subcommands](#cli-subcommands) for the full flag table and cross-platform behavior).
|
|
279
|
-
- **Flags:** `--auto` skip the approval gates · `--base-branch <branch>` branch off something other than the default
|
|
326
|
+
- **Flags:** `--auto` skip the approval gates · `--base-branch <branch>` branch off something other than the default · `--workflow implement|review-and-implement` selects which slash command each spawned worktree runs (default `implement`, byte-identical to today; `review-and-implement` runs `/review-ticket` then, after a per-ticket halt gate, `/implement-ticket` in the same session) · `--rounds=1|2` review-only, forwarded to the review phase, valid only with `--workflow review-and-implement`.
|
|
327
|
+
|
|
328
|
+
**2b. Review and Start**
|
|
329
|
+
- **What it does:** Spawns one worktree per ticket, each running review then (after a per-ticket human proceed/halt gate) implementation — the chained `review → gate → implement` composition.
|
|
330
|
+
- **When it's useful:** (Refinement | Implementation | Automation) The **recommended front door** for "review these tickets, then implement the ones that pass," starting from existing ticket keys (unlike `/full-automation`, which only accepts an idea).
|
|
331
|
+
- **How to use it:** `/review-and-start BAPI-248 BAPI-250` (single or multiple keys flow through the identical code path).
|
|
332
|
+
- **Flags:** `--auto` a single chain-level flag that auto-approves both the review and the implementation phase of every spawned session · `--rounds=1|2` forwarded to the review phase · `--agent`, `--base-branch`, `--max-parallel`, `--dry-run` mirror `/start-tickets`.
|
|
333
|
+
- Under the hood, this is a thin shim over `start-tickets --workflow review-and-implement <KEYS>` — the lower-level launcher seam documented above; the halt-gate decision logic lives in the spawned `/review-and-implement` session, never in this command or the CLI.
|
|
280
334
|
|
|
281
335
|
**3. Brainstorm**
|
|
282
336
|
- **What it does:** Fans your problem out to two different LLMs and returns their approaches directly. Runs in one of three modes, selected via `mode`: **`technical`** (default — implementation/architecture approaches), **`design`** (UI/UX and visual direction), or **`discovery`** (stakeholder discovery questions for early/vague tasks, grouped into `Technical Discovery Questions` and `Business / Stakeholder Discovery Questions` and tagged `[HUMAN]`/`[CODE]`/`[TICKET]`). Discovery needs no extra configuration. The legacy boolean `design=true` still works and maps to `mode: "design"`.
|
|
@@ -511,7 +565,7 @@ Beyond `--init` / `--upgrade`, the package ships operational subcommands of the
|
|
|
511
565
|
|
|
512
566
|
### `start-tickets`
|
|
513
567
|
|
|
514
|
-
Spawns one Worktrunk worktree + selected-agent session per Jira ticket and backs the `/start-tickets` slash command. The agent defaults to **Claude Code** (`claude`) and is configurable via `--agent`.
|
|
568
|
+
Spawns one Worktrunk worktree + selected-agent session per Jira ticket and backs the `/start-tickets` slash command. The agent defaults to **Claude Code** (`claude`) and is configurable via `--agent`. For existing ticket keys, `/review-and-start` (see [Usage Documentation → Review and Start](#tier-1--regularly-useful)) is the recommended enriched front door over this CLI's `--workflow review-and-implement` seam; using the CLI directly (below) remains the advanced/lower-level path.
|
|
515
569
|
|
|
516
570
|
```
|
|
517
571
|
npx -y @bridge_gpt/mcp-server start-tickets [flags] KEY [KEY ...]
|
|
@@ -520,6 +574,8 @@ npx -y @bridge_gpt/mcp-server start-tickets [flags] KEY [KEY ...]
|
|
|
520
574
|
| Flag | Default | Meaning |
|
|
521
575
|
|---|---|---|
|
|
522
576
|
| `--agent claude\|cursor-agent` | `claude` | Agent command to launch in each worktree |
|
|
577
|
+
| `--workflow implement\|review-and-implement` | `implement` | Slash command each spawned worktree runs. `implement` is byte-identical to today's `/implement-ticket <KEY> [--auto]`. `review-and-implement` spawns `/review-and-implement <KEY> [--auto] [--rounds=<n>]`, which runs `/review-ticket` then, after a per-ticket halt gate, `/implement-ticket` in the same session. `--auto` applies to the selected workflow as a whole. |
|
|
578
|
+
| `--rounds 1\|2` | unset | Review round count forwarded to the review phase. Review-only — valid only with `--workflow review-and-implement`. |
|
|
523
579
|
| `--terminal terminal\|iterm` | auto-detect via `$TERM_PROGRAM` | Override the macOS terminal app (honored on macOS only) |
|
|
524
580
|
| `--dry-run` | off | Print intended actions; create no worktrees, open no tabs (any OS) |
|
|
525
581
|
| `--branch KEY=BRANCH` | `feature/<KEY>` | Use a custom branch for that ticket (repeatable) |
|
|
@@ -557,9 +613,27 @@ npx -y @bridge_gpt/mcp-server doctor [--agent <name>]
|
|
|
557
613
|
|
|
558
614
|
It is **read-only**: it never installs anything, modifies your system, adds an npm `postinstall`, spawns a terminal, or starts the MCP server, and there is no `--fix`. For each prerequisite it prints found/missing and, when missing, the exact per-OS install command **as a manual instruction you run yourself**. The checked set is the `start-tickets` preflight prerequisites **plus `uv`** **plus the selected agent's command** (`claude` by default, or `cursor-agent` with `--agent cursor-agent`). The Worktrunk binary is probed via the resolved name (honoring `BAPI_WORKTRUNK_BIN`), not a hard-coded one. **Exit code:** `0` when all required prerequisites are present, non-zero when any is missing or the platform is unsupported. A failing `start-tickets` preflight now hints you to run `doctor` for an actionable diagnostics report.
|
|
559
615
|
|
|
616
|
+
### `setup-epic`
|
|
617
|
+
|
|
618
|
+
Bootstraps an Epic Conductor v2 run in one command — creates the epic run, stores the plan DAG, and approves it:
|
|
619
|
+
|
|
620
|
+
```
|
|
621
|
+
npx -y @bridge_gpt/mcp-server setup-epic --epic-key <KEY> --plan-file <path-to-epic-plan.dag.json>
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
The plan sidecar is produced by the `decompose-epic` pipeline step. `setup-epic` validates it locally (unique ticket keys, resolvable dependency references, acyclicity) before sending anything, so a malformed plan fails legibly instead of as a bare HTTP 400. It is **idempotent**: re-running it on an epic that already has a live run reuses that run rather than minting a second one. `--dry-run` validates and previews the calls without mutating anything; `--json` emits a single machine-readable result object.
|
|
625
|
+
|
|
626
|
+
Once the plan is approved, the **server-side reconciler** picks the run up within ~30s. To execute claimed jobs on your machine, run `executor`:
|
|
627
|
+
|
|
628
|
+
```
|
|
629
|
+
npx -y @bridge_gpt/mcp-server executor --repo <name>
|
|
630
|
+
```
|
|
631
|
+
|
|
560
632
|
### Conductor (epic & multi-agent orchestration)
|
|
561
633
|
|
|
562
|
-
Conductor is an **opt-in, off-by-default** layer for epic supervision, inter-agent messaging, done-gate evaluation, local git-hook event producers, and conditional auto-merge. Its full reference — `conductor install-git-hooks`, the `
|
|
634
|
+
Conductor is an **opt-in, off-by-default** layer for epic supervision, inter-agent messaging, done-gate evaluation, local git-hook event producers, and conditional auto-merge. Its full reference — the v2 architecture (server-side reconciler + local executor), `setup-epic`, `conductor install-git-hooks`, the supervisor `done_gate_config` / `auto_merge_enabled` settings, and the observability stream — lives in **[CONDUCTOR.md](./CONDUCTOR.md)**.
|
|
635
|
+
|
|
636
|
+
> The v1 `conductor epic-tick` command is **frozen** — it throws on every invocation. There is nothing to schedule locally, and `conductor doctor` flags any epic-tick schedule left over from an earlier release so you can cancel it.
|
|
563
637
|
|
|
564
638
|
## Custom Pipelines
|
|
565
639
|
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared remote-base resolution (BAPI-586).
|
|
3
|
+
*
|
|
4
|
+
* A focused, executor-agnostic helper that BOTH Conductor dispatch paths use to
|
|
5
|
+
* validate, fetch, and pin the configured remote base commit before creating a
|
|
6
|
+
* fresh worktree:
|
|
7
|
+
*
|
|
8
|
+
* - the interactive `start-tickets` non-mutating dispatch path
|
|
9
|
+
* (`orchestrateStartTickets`), and
|
|
10
|
+
* - the Epic Conductor v2 executor fresh worktree path
|
|
11
|
+
* (`executor/worktree.ts#ensureExecutorWorktree`).
|
|
12
|
+
*
|
|
13
|
+
* Extracted out of the large `start-tickets.ts` CLI module so executor code can
|
|
14
|
+
* depend on a small, focused module instead of pulling in the whole CLI. The
|
|
15
|
+
* operation is FETCH-ONLY: it never fast-forwards, resets, or force-moves any
|
|
16
|
+
* local branch ref — it fetches `origin/<base>` and resolves the fetched tip to
|
|
17
|
+
* an immutable commit SHA, which becomes Worktrunk's `-b <start-point>`.
|
|
18
|
+
*
|
|
19
|
+
* Import direction: this module imports the shared `RunCommand` type from
|
|
20
|
+
* `start-tickets.js` as a TYPE-ONLY import (erased at runtime) plus the
|
|
21
|
+
* `commandSucceeded` value from `start-tickets-prereqs.js`. `start-tickets.ts`
|
|
22
|
+
* imports the VALUES here and re-exports them, so the runtime graph stays
|
|
23
|
+
* acyclic (mirrors the existing `worktree-core.ts` / prereqs pattern).
|
|
24
|
+
*/
|
|
25
|
+
import path from "path";
|
|
26
|
+
import { commandSucceeded } from "./start-tickets-prereqs.js";
|
|
27
|
+
/**
|
|
28
|
+
* Returns an error string for an unsafe branch name, or null when valid.
|
|
29
|
+
*
|
|
30
|
+
* Rejects names that Git itself refuses (`git check-ref-format`) as well as
|
|
31
|
+
* injection-shaped inputs, WITHOUT invoking Git: an empty/whitespace-only name,
|
|
32
|
+
* a leading `-` (which git would parse as a flag, e.g. `--upload-pack=evil`),
|
|
33
|
+
* ASCII control characters, a `..` sequence, and a `.lock` suffix. Every failure
|
|
34
|
+
* is a short, secret-free reason string that names only the rule that failed.
|
|
35
|
+
*/
|
|
36
|
+
export function validateBranchName(branch) {
|
|
37
|
+
if (branch.trim().length === 0)
|
|
38
|
+
return "branch name must not be empty.";
|
|
39
|
+
if (branch.length > 255)
|
|
40
|
+
return "branch name must be 255 characters or fewer.";
|
|
41
|
+
if (branch.startsWith("-"))
|
|
42
|
+
return "branch name must not start with '-'.";
|
|
43
|
+
if (branch.includes(".."))
|
|
44
|
+
return "branch name must not contain '..'.";
|
|
45
|
+
if (branch.endsWith(".lock"))
|
|
46
|
+
return "branch name must not end with '.lock'.";
|
|
47
|
+
// Reject ASCII control characters (0x00-0x1F and 0x7F) without embedding
|
|
48
|
+
// raw control bytes in source.
|
|
49
|
+
for (let i = 0; i < branch.length; i++) {
|
|
50
|
+
const code = branch.charCodeAt(i);
|
|
51
|
+
if (code <= 0x1f || code === 0x7f) {
|
|
52
|
+
return "branch name must not contain control characters.";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Per-repository fetch serialization
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
/**
|
|
61
|
+
* A per-repository async mutex chain. The executor runner fills up to
|
|
62
|
+
* `maxConcurrent` slots against ONE shared clone, so once a `git fetch` is
|
|
63
|
+
* introduced on the fresh path, simultaneous fresh jobs would race on
|
|
64
|
+
* `FETCH_HEAD.lock` / `index.lock`. Serializing fetch-and-resolve per repository
|
|
65
|
+
* removes that collision while still letting independent repositories proceed
|
|
66
|
+
* concurrently (the map is keyed per normalized repo path, not one global lock).
|
|
67
|
+
*/
|
|
68
|
+
const repoFetchLocks = new Map();
|
|
69
|
+
/** Normalize a repo working directory into a stable, spelling-independent key. */
|
|
70
|
+
function normalizeRepoKey(cwd) {
|
|
71
|
+
// `path.resolve` collapses `.`/`..` segments and trailing separators so
|
|
72
|
+
// different spellings of the same directory share one lock entry.
|
|
73
|
+
return path.resolve(cwd);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Run `fn` while holding the per-repository lock. Concurrent calls for the same
|
|
77
|
+
* normalized repo run strictly one at a time; calls for different repos overlap.
|
|
78
|
+
* The lock is released and its map entry removed in `finally` — including on a
|
|
79
|
+
* thrown/failed operation — so a failed fetch never deadlocks later dispatches
|
|
80
|
+
* or leaks repository keys.
|
|
81
|
+
*/
|
|
82
|
+
async function withRepoFetchLock(repoKey, fn) {
|
|
83
|
+
const previous = repoFetchLocks.get(repoKey) ?? Promise.resolve();
|
|
84
|
+
let releaseCurrent;
|
|
85
|
+
const current = new Promise((resolve) => {
|
|
86
|
+
releaseCurrent = resolve;
|
|
87
|
+
});
|
|
88
|
+
// Later waiters chain on `previous` THEN on our gate, so they cannot start
|
|
89
|
+
// until we release. `current` only ever resolves, so this chain never rejects.
|
|
90
|
+
const chained = previous.then(() => current);
|
|
91
|
+
repoFetchLocks.set(repoKey, chained);
|
|
92
|
+
await previous.catch(() => { });
|
|
93
|
+
try {
|
|
94
|
+
return await fn();
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
releaseCurrent();
|
|
98
|
+
// Drop the entry only if no later waiter chained on after us (we are still
|
|
99
|
+
// the tail). Otherwise the newest waiter owns the key.
|
|
100
|
+
if (repoFetchLocks.get(repoKey) === chained) {
|
|
101
|
+
repoFetchLocks.delete(repoKey);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Test-only: clear all per-repository fetch locks between tests. */
|
|
106
|
+
export function __resetBaseRefFetchLocksForTests() {
|
|
107
|
+
repoFetchLocks.clear();
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Validate `baseBranch`, then (serialized per repository) fetch
|
|
111
|
+
* `origin/<baseBranch>` and resolve the fetched tip to an immutable commit SHA,
|
|
112
|
+
* WITHOUT ever fast-forwarding or force-moving any local branch ref (no
|
|
113
|
+
* `git merge --ff-only`, no `git branch --force`, no `git reset`). Pinning the
|
|
114
|
+
* SHA removes ambiguity and prevents Worktrunk or stale local branch state from
|
|
115
|
+
* selecting a sibling feature branch when the resulting SHA is passed as
|
|
116
|
+
* Worktrunk's `-b` start point.
|
|
117
|
+
*
|
|
118
|
+
* Validation runs BEFORE the lock is acquired so an injection-shaped ref name is
|
|
119
|
+
* rejected without any git invocation and without touching the mutex. Both the
|
|
120
|
+
* `git fetch` and the `git rev-parse` run inside the same per-repository lock so
|
|
121
|
+
* concurrent fresh jobs cannot collide on git lock files or resolve an unrelated
|
|
122
|
+
* fetch result. Failures name only the configured branch and the failed
|
|
123
|
+
* operation — never remote URLs, credentials, raw command arguments, or
|
|
124
|
+
* unbounded command output.
|
|
125
|
+
*/
|
|
126
|
+
export async function fetchAndResolveBaseSha(deps, baseBranch) {
|
|
127
|
+
const validationError = validateBranchName(baseBranch);
|
|
128
|
+
if (validationError) {
|
|
129
|
+
return { ok: false, error: `Invalid base branch '${baseBranch}': ${validationError}` };
|
|
130
|
+
}
|
|
131
|
+
const repoKey = normalizeRepoKey(deps.cwd);
|
|
132
|
+
return withRepoFetchLock(repoKey, async () => {
|
|
133
|
+
const fetch = await deps.runCommand("git", ["fetch", "origin", baseBranch], {
|
|
134
|
+
cwd: deps.cwd,
|
|
135
|
+
});
|
|
136
|
+
if (!commandSucceeded(fetch)) {
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
error: `git fetch origin ${baseBranch} failed. Check your network and 'git remote get-url origin', or pass --no-refresh-base to skip.`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const resolve = await deps.runCommand("git", ["rev-parse", "--verify", `origin/${baseBranch}^{commit}`], { cwd: deps.cwd });
|
|
143
|
+
if (!commandSucceeded(resolve)) {
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
error: `Failed to resolve origin/${baseBranch} to a commit SHA after fetch (git rev-parse --verify failed).`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return { ok: true, base_sha: resolve.stdout.trim() };
|
|
150
|
+
});
|
|
151
|
+
}
|