@bridge_gpt/mcp-server 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -15
- package/build/agent-config-credential-migration.js +272 -0
- package/build/agents.generated.js +1 -1
- package/build/chain-orchestrator.js +16 -1
- package/build/commands.generated.js +9 -7
- package/build/conductor/bridge-api-client.js +625 -0
- package/build/conductor/claude-hook.js +251 -0
- package/build/conductor/cli.js +1048 -0
- package/build/conductor/data-normalization.js +114 -0
- package/build/conductor/doctor.js +164 -0
- package/build/conductor/done-gate.js +325 -0
- package/build/conductor/epic-reconcile.js +139 -0
- package/build/conductor/epic-runtime.js +611 -0
- package/build/conductor/epic-state.js +125 -0
- package/build/conductor/errors.js +85 -0
- package/build/conductor/git-ci-types.js +129 -0
- package/build/conductor/git-hooks.js +218 -0
- package/build/conductor/git-inspection.js +185 -0
- package/build/conductor/git-producer.js +137 -0
- package/build/conductor/merge-ledger.js +198 -0
- package/build/conductor/paths.js +224 -0
- package/build/conductor/plan.js +77 -0
- package/build/conductor/pr-ci-producer.js +427 -0
- package/build/conductor/pr-discovery.js +135 -0
- package/build/conductor/producer-ledger.js +125 -0
- package/build/conductor/redaction.js +112 -0
- package/build/conductor/store.js +1156 -0
- package/build/conductor/supervisor-config.js +150 -0
- package/build/conductor/supervisor-escalation.js +244 -0
- package/build/conductor/supervisor-judgment-python.js +141 -0
- package/build/conductor/supervisor-judgment.js +215 -0
- package/build/conductor/supervisor-ledger.js +119 -0
- package/build/conductor/supervisor-merge.js +127 -0
- package/build/conductor/supervisor-message-relay.js +61 -0
- package/build/conductor/supervisor-notification.js +39 -0
- package/build/conductor/supervisor-runtime.js +351 -0
- package/build/conductor/supervisor-state.js +572 -0
- package/build/conductor/supervisor-types.js +16 -0
- package/build/conductor/taxonomy.js +58 -0
- package/build/conductor/tools.js +367 -0
- package/build/conductor/types.js +9 -0
- package/build/conductor-bin.js +21 -0
- package/build/conductor-claude-hook-bin.js +21 -0
- package/build/credential-store.js +175 -4
- package/build/credentials-cli.js +223 -0
- package/build/decision-page-schema.js +60 -0
- package/build/decision-page-template.js +262 -10
- package/build/doctor.js +5 -1
- package/build/index.js +554 -66
- package/build/pipeline-orchestrator.js +5 -1
- package/build/pipeline-utils.js +45 -5
- package/build/pipelines.generated.js +37 -9
- package/build/readme.generated.js +1 -1
- package/build/review-tickets.js +596 -0
- package/build/scheduled-prompt.js +16 -10
- package/build/start-tickets-conductor.js +496 -0
- package/build/start-tickets-prereqs.js +32 -23
- package/build/start-tickets-repo.js +49 -0
- package/build/start-tickets.js +682 -81
- package/build/version.generated.js +1 -1
- package/design-assets/favicon/android-chrome-192x192.png +0 -0
- package/design-assets/favicon/android-chrome-512x512.png +0 -0
- package/design-assets/favicon/apple-touch-icon.png +0 -0
- package/design-assets/favicon/favicon-16x16.png +0 -0
- package/design-assets/favicon/favicon-32x32.png +0 -0
- package/design-assets/favicon/favicon.ico +0 -0
- package/design-assets/favicon/site.webmanifest +1 -0
- package/design-assets/just-logo-rough-draft.png +0 -0
- package/package.json +17 -5
- package/pipelines/idea-to-ticket.json +5 -0
- package/pipelines/plan-epic.json +16 -1
- package/pipelines/review-ticket.json +2 -1
- package/public/css/main.min.css +2 -0
- package/public/css/main.min.css.map +1 -0
- package/public/fonts/OFL.txt +93 -0
- package/public/fonts/SourceSansPro-Black.ttf +0 -0
- package/public/fonts/SourceSansPro-BlackItalic.ttf +0 -0
- package/public/fonts/SourceSansPro-Bold.ttf +0 -0
- package/public/fonts/SourceSansPro-BoldItalic.ttf +0 -0
- package/public/fonts/SourceSansPro-ExtraLight.ttf +0 -0
- package/public/fonts/SourceSansPro-ExtraLightItalic.ttf +0 -0
- package/public/fonts/SourceSansPro-Italic.ttf +0 -0
- package/public/fonts/SourceSansPro-Light.ttf +0 -0
- package/public/fonts/SourceSansPro-LightItalic.ttf +0 -0
- package/public/fonts/SourceSansPro-Regular.ttf +0 -0
- package/public/fonts/SourceSansPro-SemiBold.ttf +0 -0
- package/public/fonts/SourceSansPro-SemiBoldItalic.ttf +0 -0
- package/public/img/bridge-logo-160x51.webp +0 -0
- package/public/img/bridge-logo-300x92.webp +0 -0
- package/public/img/favicon/android-chrome-192x192.png +0 -0
- package/public/img/favicon/android-chrome-512x512.png +0 -0
- package/public/img/favicon/apple-touch-icon.png +0 -0
- package/public/img/favicon/favicon-16x16.png +0 -0
- package/public/img/favicon/favicon-32x32.png +0 -0
- package/public/img/favicon/favicon.ico +0 -0
- package/public/img/favicon/site.webmanifest +1 -0
- package/public/img/installation/bitbucket/app-password-1.png +0 -0
- package/public/img/installation/bitbucket/app-password-2.png +0 -0
- package/public/img/installation/bitbucket/create-token-1.png +0 -0
- package/public/img/installation/bitbucket/create-token-2.png +0 -0
- package/public/img/installation/bitbucket/webhook-1.png +0 -0
- package/public/img/installation/github/github-review-webhook.png +0 -0
- package/public/img/installation/jira/credentials/api-key.png +0 -0
- package/public/img/installation/jira/webhook/create-rule.png +0 -0
- package/public/img/installation/jira/webhook/project-settings.png +0 -0
- package/public/img/installation/jira/webhook/rule-create-1.png +0 -0
- package/public/img/installation/jira/webhook/rule-create-2.png +0 -0
- package/public/img/installation/jira/webhook/rule-create-3.png +0 -0
- package/public/img/installation/pinecone/pinecone-api-key.png +0 -0
- package/public/img/installation/pinecone/pinecone-index.png +0 -0
- package/public/js/main.min.js +2 -0
- package/public/js/main.min.js.map +1 -0
- package/smoke-test/SMOKE-TEST.md +17 -9
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* review-tickets — packaged CLI subcommand of the bridge-api-mcp-server bin.
|
|
3
|
+
*
|
|
4
|
+
* Reuses the `start-tickets` terminal spawn layer directly, running reviews in
|
|
5
|
+
* the current `deps.cwd`. Intentionally does NOT create Worktrunk worktrees,
|
|
6
|
+
* provision MCP registrations, run venv hooks, or perform difficulty/model-tier
|
|
7
|
+
* routing. One terminal tab per ticket is spawned with `/review-ticket <KEY>
|
|
8
|
+
* [--auto] --rounds=<1|2>` as the agent prompt.
|
|
9
|
+
*
|
|
10
|
+
* Optionally supports a supervisor-driven epic-dispatch path: when an
|
|
11
|
+
* `EpicDispatchIdentity` is threaded through `ReviewTicketsOptions.epic`, each
|
|
12
|
+
* spawned tab receives an epic-identity env block (BAPI_CONDUCTOR_EPIC_KEY /
|
|
13
|
+
* BAPI_CONDUCTOR_EPIC_RUN_ID / BAPI_CONDUCTOR_PLAN_VERSION / BAPI_CONDUCTOR_RUN_ID)
|
|
14
|
+
* and dispatch is made idempotent via an injected `claimEpicDispatch` callback.
|
|
15
|
+
* This path is worktree-free and base-branch-free; the human CLI path leaves
|
|
16
|
+
* `epic` unset and retains byte-for-byte identical behavior.
|
|
17
|
+
*/
|
|
18
|
+
import { createDefaultStartTicketsDeps, detectTerminal, runWithConcurrency, buildAgentInvocation, shSquoteInner, powershellSquote, isCommandOnPath, resolveFirstCommandOnPath, tmuxSessionNameForTicket, TICKET_KEY_PATTERN, DEFAULT_MAX_PARALLEL, } from "./start-tickets.js";
|
|
19
|
+
import { WINDOWS_TERMINAL_COMMAND, WINDOWS_POWERSHELL_CANDIDATES, TMUX_COMMAND, isSupportedStartTicketsPlatform, unsupportedPlatformMessage, } from "./start-tickets-prereqs.js";
|
|
20
|
+
import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentNames, isValidModelAlias, } from "./agent-registry.js";
|
|
21
|
+
import { injectConductorEnvIntoShellCommand, buildEpicIdentityEnv, mintStartTicketsRunId, } from "./start-tickets-conductor.js";
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Usage
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
export function getReviewTicketsUsage() {
|
|
26
|
+
return [
|
|
27
|
+
"Usage:",
|
|
28
|
+
" npx -y @bridge_gpt/mcp-server review-tickets [flags] KEY [KEY ...]",
|
|
29
|
+
"",
|
|
30
|
+
"Flags:",
|
|
31
|
+
" --auto Auto-approve all review gates (global, applies to all tickets)",
|
|
32
|
+
" --rounds=1|2 Review depth: 1=single-pass, 2=full two-pass (default: 2)",
|
|
33
|
+
" --review KEY=auto,rounds=1 Per-ticket review mode override (repeatable)",
|
|
34
|
+
" --agent claude|cursor-agent Agent command to launch in each tab (default: claude)",
|
|
35
|
+
" --model VALUE Model alias to pass as --model to the agent (optional passthrough)",
|
|
36
|
+
" --max-parallel N Max tabs to spawn concurrently (default: 3)",
|
|
37
|
+
" --dry-run Print intended commands; creates no tabs and opens no sessions",
|
|
38
|
+
" -h, --help Show this help",
|
|
39
|
+
"",
|
|
40
|
+
"Examples:",
|
|
41
|
+
" npx -y @bridge_gpt/mcp-server review-tickets BAPI-1 BAPI-2",
|
|
42
|
+
" npx -y @bridge_gpt/mcp-server review-tickets --auto --rounds=1 BAPI-1 BAPI-2",
|
|
43
|
+
" npx -y @bridge_gpt/mcp-server review-tickets --review BAPI-1=auto,rounds=1 --review BAPI-2=rounds=2 BAPI-1 BAPI-2",
|
|
44
|
+
" npx -y @bridge_gpt/mcp-server review-tickets --dry-run BAPI-1 BAPI-2",
|
|
45
|
+
"",
|
|
46
|
+
"Each KEY must match [A-Z]+-[0-9]+ (e.g., BAPI-248).",
|
|
47
|
+
"",
|
|
48
|
+
"Prerequisites (terminal launcher only — no wt, git-wt, git, or Worktrunk required):",
|
|
49
|
+
" macOS osascript",
|
|
50
|
+
" Windows wt.exe or PowerShell",
|
|
51
|
+
" Linux tmux",
|
|
52
|
+
"",
|
|
53
|
+
"review-tickets runs all tabs in the current repository cwd and creates no Worktrunk worktrees.",
|
|
54
|
+
"--dry-run works on any platform, including unsupported ones.",
|
|
55
|
+
].join("\n");
|
|
56
|
+
}
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Argument parsing
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
export function parseReviewOverrideEntry(entry, requestedKeys) {
|
|
61
|
+
const eqIdx = entry.indexOf("=");
|
|
62
|
+
if (eqIdx <= 0) {
|
|
63
|
+
return { ok: false, error: `Invalid --review value: '${entry}' (expected KEY=auto,rounds=1 or KEY=rounds=2).` };
|
|
64
|
+
}
|
|
65
|
+
const key = entry.slice(0, eqIdx);
|
|
66
|
+
const rest = entry.slice(eqIdx + 1);
|
|
67
|
+
if (!TICKET_KEY_PATTERN.test(key)) {
|
|
68
|
+
return { ok: false, error: `Invalid --review override key: '${key}' (keys must match [A-Z]+-[0-9]+).` };
|
|
69
|
+
}
|
|
70
|
+
if (!requestedKeys.has(key)) {
|
|
71
|
+
return { ok: false, error: `--review override key '${key}' is not one of the requested tickets.` };
|
|
72
|
+
}
|
|
73
|
+
const subtokens = rest.split(",");
|
|
74
|
+
const override = {};
|
|
75
|
+
for (const token of subtokens) {
|
|
76
|
+
if (token.trim() === "") {
|
|
77
|
+
return { ok: false, error: `Invalid --review value for '${key}': empty subtoken in '${rest}'.` };
|
|
78
|
+
}
|
|
79
|
+
if (token === "auto") {
|
|
80
|
+
override.auto = true;
|
|
81
|
+
}
|
|
82
|
+
else if (token.startsWith("rounds=")) {
|
|
83
|
+
const val = token.slice("rounds=".length);
|
|
84
|
+
if (val !== "1" && val !== "2") {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
error: `Invalid --review rounds for '${key}': '${val}' (must be 1 or 2).`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
override.rounds = Number(val);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
return { ok: false, error: `Invalid --review subtoken for '${key}': '${token}' (supported: auto, rounds=1, rounds=2).` };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { ok: true, key, override };
|
|
97
|
+
}
|
|
98
|
+
export function parseReviewTicketsArgs(argv) {
|
|
99
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
100
|
+
return { status: "help", usage: getReviewTicketsUsage() };
|
|
101
|
+
}
|
|
102
|
+
let dryRun = false;
|
|
103
|
+
let globalAuto = false;
|
|
104
|
+
let globalRounds;
|
|
105
|
+
let maxParallelRaw;
|
|
106
|
+
let agentName = DEFAULT_AGENT_NAME;
|
|
107
|
+
let modelAlias;
|
|
108
|
+
const keys = [];
|
|
109
|
+
const rawReviewEntries = [];
|
|
110
|
+
for (let i = 0; i < argv.length; i++) {
|
|
111
|
+
const arg = argv[i];
|
|
112
|
+
const takeValue = () => {
|
|
113
|
+
if (i + 1 >= argv.length)
|
|
114
|
+
return undefined;
|
|
115
|
+
i += 1;
|
|
116
|
+
return argv[i];
|
|
117
|
+
};
|
|
118
|
+
if (arg === "--dry-run") {
|
|
119
|
+
dryRun = true;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (arg === "--auto") {
|
|
123
|
+
globalAuto = true;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (arg === "--rounds" || arg.startsWith("--rounds=")) {
|
|
127
|
+
let value;
|
|
128
|
+
if (arg.startsWith("--rounds=")) {
|
|
129
|
+
value = arg.slice("--rounds=".length);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
value = takeValue();
|
|
133
|
+
if (value === undefined) {
|
|
134
|
+
return { status: "error", message: "--rounds requires a value (1 or 2)." };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (value !== "1" && value !== "2") {
|
|
138
|
+
return {
|
|
139
|
+
status: "error",
|
|
140
|
+
message: `Invalid --rounds value: '${value}' (must be 1 or 2).`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
globalRounds = Number(value);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (arg === "--max-parallel" || arg.startsWith("--max-parallel=")) {
|
|
147
|
+
if (arg.startsWith("--max-parallel=")) {
|
|
148
|
+
maxParallelRaw = arg.slice("--max-parallel=".length);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
const value = takeValue();
|
|
152
|
+
if (value === undefined) {
|
|
153
|
+
return { status: "error", message: "--max-parallel requires a positive integer value." };
|
|
154
|
+
}
|
|
155
|
+
maxParallelRaw = value;
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (arg === "--agent" || arg.startsWith("--agent=")) {
|
|
160
|
+
let value;
|
|
161
|
+
if (arg.startsWith("--agent=")) {
|
|
162
|
+
value = arg.slice("--agent=".length);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
value = takeValue();
|
|
166
|
+
if (value === undefined) {
|
|
167
|
+
return { status: "error", message: "--agent requires a value (an agent name)." };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!isAgentName(value)) {
|
|
171
|
+
return {
|
|
172
|
+
status: "error",
|
|
173
|
+
message: `Invalid --agent value: '${value}' (allowed agents: ${formatValidAgentNames()}).`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
agentName = value;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (arg === "--model" || arg.startsWith("--model=")) {
|
|
180
|
+
let value;
|
|
181
|
+
if (arg.startsWith("--model=")) {
|
|
182
|
+
value = arg.slice("--model=".length);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
value = takeValue();
|
|
186
|
+
if (value === undefined) {
|
|
187
|
+
return { status: "error", message: "--model requires a value (a model alias)." };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (!value || !isValidModelAlias(value)) {
|
|
191
|
+
return {
|
|
192
|
+
status: "error",
|
|
193
|
+
message: `Invalid --model value: '${value ?? ""}' (must match [A-Za-z0-9._:-]+).`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
modelAlias = value;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (arg === "--review" || arg.startsWith("--review=")) {
|
|
200
|
+
let value;
|
|
201
|
+
if (arg.startsWith("--review=")) {
|
|
202
|
+
value = arg.slice("--review=".length);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
value = takeValue();
|
|
206
|
+
if (value === undefined) {
|
|
207
|
+
return { status: "error", message: "--review requires a value (KEY=auto,rounds=1)." };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
rawReviewEntries.push(value);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (arg.startsWith("-")) {
|
|
214
|
+
return { status: "error", message: `Unknown flag: ${arg}` };
|
|
215
|
+
}
|
|
216
|
+
keys.push(arg);
|
|
217
|
+
}
|
|
218
|
+
// --- key validation ---
|
|
219
|
+
if (keys.length === 0) {
|
|
220
|
+
return {
|
|
221
|
+
status: "error",
|
|
222
|
+
message: "At least one ticket key is required (e.g., BAPI-248).",
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
const seen = new Set();
|
|
226
|
+
for (const key of keys) {
|
|
227
|
+
if (!TICKET_KEY_PATTERN.test(key)) {
|
|
228
|
+
return {
|
|
229
|
+
status: "error",
|
|
230
|
+
message: `Invalid ticket key: '${key}' (keys must match [A-Z]+-[0-9]+, e.g., BAPI-248).`,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (seen.has(key)) {
|
|
234
|
+
return { status: "error", message: `Duplicate ticket key: '${key}'.` };
|
|
235
|
+
}
|
|
236
|
+
seen.add(key);
|
|
237
|
+
}
|
|
238
|
+
// --- max-parallel validation ---
|
|
239
|
+
let maxParallel = DEFAULT_MAX_PARALLEL;
|
|
240
|
+
if (maxParallelRaw !== undefined) {
|
|
241
|
+
if (!/^[0-9]+$/.test(maxParallelRaw) || Number(maxParallelRaw) < 1) {
|
|
242
|
+
return {
|
|
243
|
+
status: "error",
|
|
244
|
+
message: `Invalid --max-parallel value: '${maxParallelRaw}' (must be a positive integer).`,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
maxParallel = Number(maxParallelRaw);
|
|
248
|
+
}
|
|
249
|
+
// --- review override validation (needs full key set) ---
|
|
250
|
+
const reviewOverrides = {};
|
|
251
|
+
for (const entry of rawReviewEntries) {
|
|
252
|
+
const result = parseReviewOverrideEntry(entry, seen);
|
|
253
|
+
if (!result.ok) {
|
|
254
|
+
return { status: "error", message: result.error };
|
|
255
|
+
}
|
|
256
|
+
// Merge: layer later parsed fields over earlier parsed fields
|
|
257
|
+
reviewOverrides[result.key] = { ...(reviewOverrides[result.key] ?? {}), ...result.override };
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
status: "ok",
|
|
261
|
+
options: {
|
|
262
|
+
keys,
|
|
263
|
+
dryRun,
|
|
264
|
+
maxParallel,
|
|
265
|
+
agentName,
|
|
266
|
+
modelAlias,
|
|
267
|
+
auto: globalAuto,
|
|
268
|
+
rounds: globalRounds,
|
|
269
|
+
reviewOverrides,
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// Mode resolution
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
export function resolveEffectiveReviewMode(key, options) {
|
|
277
|
+
const override = options.reviewOverrides[key];
|
|
278
|
+
const auto = override?.auto !== undefined ? override.auto : options.auto;
|
|
279
|
+
const rounds = override?.rounds !== undefined
|
|
280
|
+
? override.rounds
|
|
281
|
+
: options.rounds !== undefined
|
|
282
|
+
? options.rounds
|
|
283
|
+
: 2;
|
|
284
|
+
return { auto, rounds };
|
|
285
|
+
}
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// Command builders
|
|
288
|
+
// The existing start-tickets builders include a worktree `cd` prefix and are
|
|
289
|
+
// hard-wired to `/implement-ticket`. Review-aware builders are used instead so
|
|
290
|
+
// the prompt uses `/review-ticket` and no directory change is prepended.
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
export function buildReviewTicketPrompt(key, mode) {
|
|
293
|
+
const autoFlag = mode.auto ? " --auto" : "";
|
|
294
|
+
return `/review-ticket ${key}${autoFlag} --rounds=${mode.rounds}`;
|
|
295
|
+
}
|
|
296
|
+
export function buildPosixReviewAgentShellCommand(agent, key, mode, cwd, modelAlias) {
|
|
297
|
+
const prompt = buildReviewTicketPrompt(key, mode);
|
|
298
|
+
const invocation = buildAgentInvocation(agent, prompt, (p) => `'${shSquoteInner(p)}'`, modelAlias);
|
|
299
|
+
return `cd '${shSquoteInner(cwd)}' && ${invocation}`;
|
|
300
|
+
}
|
|
301
|
+
export function buildPowerShellReviewAgentShellCommand(agent, key, mode, modelAlias) {
|
|
302
|
+
const prompt = buildReviewTicketPrompt(key, mode);
|
|
303
|
+
return buildAgentInvocation(agent, prompt, powershellSquote, modelAlias);
|
|
304
|
+
}
|
|
305
|
+
export function buildReviewAgentShellCommand(agent, key, mode, platform, cwd, modelAlias) {
|
|
306
|
+
if (platform === "win32") {
|
|
307
|
+
return buildPowerShellReviewAgentShellCommand(agent, key, mode, modelAlias);
|
|
308
|
+
}
|
|
309
|
+
return buildPosixReviewAgentShellCommand(agent, key, mode, cwd, modelAlias);
|
|
310
|
+
}
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
// Unsupported platform message for review-tickets
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
export function unsupportedReviewTicketsPlatformMessage(platform) {
|
|
315
|
+
const base = unsupportedPlatformMessage(platform);
|
|
316
|
+
// Replace "start-tickets" wording with "review-tickets"
|
|
317
|
+
return base.replace(/start-tickets/g, "review-tickets");
|
|
318
|
+
}
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
// Preflight (launcher-only prerequisites — no Worktrunk/git/agent probes)
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
export async function runReviewTicketsPreflight(deps, options) {
|
|
323
|
+
if (options.dryRun)
|
|
324
|
+
return { ok: true };
|
|
325
|
+
if (!isSupportedStartTicketsPlatform(deps.platform)) {
|
|
326
|
+
return { ok: false, error: unsupportedReviewTicketsPlatformMessage(deps.platform) };
|
|
327
|
+
}
|
|
328
|
+
if (deps.platform === "darwin") {
|
|
329
|
+
if (await isCommandOnPath(deps, "osascript"))
|
|
330
|
+
return { ok: true };
|
|
331
|
+
return {
|
|
332
|
+
ok: false,
|
|
333
|
+
error: "osascript is required to open a macOS terminal tab for review-tickets but was not found on PATH.",
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
if (deps.platform === "win32") {
|
|
337
|
+
if (await isCommandOnPath(deps, WINDOWS_TERMINAL_COMMAND))
|
|
338
|
+
return { ok: true };
|
|
339
|
+
const powershell = await resolveFirstCommandOnPath(deps, WINDOWS_POWERSHELL_CANDIDATES);
|
|
340
|
+
if (powershell)
|
|
341
|
+
return { ok: true };
|
|
342
|
+
return {
|
|
343
|
+
ok: false,
|
|
344
|
+
error: "Windows Terminal (wt.exe) or PowerShell is required to open a review-tickets tab, but neither was found on PATH.",
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
// linux
|
|
348
|
+
if (await isCommandOnPath(deps, TMUX_COMMAND))
|
|
349
|
+
return { ok: true };
|
|
350
|
+
return {
|
|
351
|
+
ok: false,
|
|
352
|
+
error: "tmux is required to spawn Linux review-tickets sessions but was not found on PATH. Install tmux and retry.",
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
// Plan rows + orchestration
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
export function buildReviewPlanRows(deps, options, agent, status, overrides = {}) {
|
|
359
|
+
return options.keys.map((key) => {
|
|
360
|
+
const mode = resolveEffectiveReviewMode(key, options);
|
|
361
|
+
const baseCommand = buildReviewAgentShellCommand(agent, key, mode, deps.platform, deps.cwd, options.modelAlias);
|
|
362
|
+
let command = baseCommand;
|
|
363
|
+
let runId;
|
|
364
|
+
if (options.epic) {
|
|
365
|
+
const mint = overrides.mintRunId ?? mintStartTicketsRunId;
|
|
366
|
+
runId = mint([key]);
|
|
367
|
+
const env = { ...buildEpicIdentityEnv(options.epic), BAPI_CONDUCTOR_RUN_ID: runId };
|
|
368
|
+
command = injectConductorEnvIntoShellCommand(deps.platform, baseCommand, env);
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
key,
|
|
372
|
+
auto: mode.auto,
|
|
373
|
+
rounds: mode.rounds,
|
|
374
|
+
agentName: options.agentName,
|
|
375
|
+
modelAlias: options.modelAlias,
|
|
376
|
+
status,
|
|
377
|
+
command,
|
|
378
|
+
runId,
|
|
379
|
+
};
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
export async function orchestrateReviewTickets(deps, options, overrides = {}) {
|
|
383
|
+
const agent = resolveAgentSpec(options.agentName);
|
|
384
|
+
if (!agent) {
|
|
385
|
+
return {
|
|
386
|
+
ok: false,
|
|
387
|
+
error: `Unknown agent: '${options.agentName}'. Valid agents: ${formatValidAgentNames()}.`,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
if (options.dryRun) {
|
|
391
|
+
if (options.epic?.dispatch_key && options.keys.length > 1) {
|
|
392
|
+
return {
|
|
393
|
+
ok: false,
|
|
394
|
+
error: "epic.dispatch_key cannot be used with multiple keys: dispatch_key is a single-ticket claim " +
|
|
395
|
+
"and would be re-claimed for each key in the batch. Call orchestrateReviewTickets once per ticket instead.",
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
const rows = buildReviewPlanRows(deps, options, agent, "dry-run", overrides);
|
|
399
|
+
return { ok: true, rows };
|
|
400
|
+
}
|
|
401
|
+
const preflight = await runReviewTicketsPreflight(deps, options);
|
|
402
|
+
if (!preflight.ok)
|
|
403
|
+
return { ok: false, error: preflight.error };
|
|
404
|
+
// A dispatch_key embeds a single ticket (dispatch:{epic}:{ticket}:{plan_version}).
|
|
405
|
+
// Claiming the same key for every iteration of a multi-key batch would silently
|
|
406
|
+
// drop all but the first ticket; reject early with a clear error instead.
|
|
407
|
+
if (options.epic?.dispatch_key && options.keys.length > 1) {
|
|
408
|
+
return {
|
|
409
|
+
ok: false,
|
|
410
|
+
error: "epic.dispatch_key cannot be used with multiple keys: dispatch_key is a single-ticket claim " +
|
|
411
|
+
"and would be re-claimed for each key in the batch. Call orchestrateReviewTickets once per ticket instead.",
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
const terminal = detectTerminal(undefined, deps.env);
|
|
415
|
+
const rows = await runWithConcurrency(options.keys, options.maxParallel, async (key) => {
|
|
416
|
+
const mode = resolveEffectiveReviewMode(key, options);
|
|
417
|
+
const baseShellCommand = buildReviewAgentShellCommand(agent, key, mode, deps.platform, deps.cwd, options.modelAlias);
|
|
418
|
+
let shellCommand = baseShellCommand;
|
|
419
|
+
let runId;
|
|
420
|
+
if (options.epic) {
|
|
421
|
+
const mint = overrides.mintRunId ?? mintStartTicketsRunId;
|
|
422
|
+
runId = mint([key]);
|
|
423
|
+
const env = { ...buildEpicIdentityEnv(options.epic), BAPI_CONDUCTOR_RUN_ID: runId };
|
|
424
|
+
shellCommand = injectConductorEnvIntoShellCommand(deps.platform, baseShellCommand, env);
|
|
425
|
+
if (options.epic.dispatch_key) {
|
|
426
|
+
if (!overrides.claimEpicDispatch) {
|
|
427
|
+
return {
|
|
428
|
+
key,
|
|
429
|
+
auto: mode.auto,
|
|
430
|
+
rounds: mode.rounds,
|
|
431
|
+
agentName: options.agentName,
|
|
432
|
+
modelAlias: options.modelAlias,
|
|
433
|
+
status: "spawn-failed",
|
|
434
|
+
command: shellCommand,
|
|
435
|
+
runId,
|
|
436
|
+
error: "claimEpicDispatch is required when dispatch_key is present but was not provided",
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
await overrides.claimEpicDispatch(options.epic.dispatch_key, runId, deps);
|
|
441
|
+
}
|
|
442
|
+
catch (err) {
|
|
443
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
444
|
+
return {
|
|
445
|
+
key,
|
|
446
|
+
auto: mode.auto,
|
|
447
|
+
rounds: mode.rounds,
|
|
448
|
+
agentName: options.agentName,
|
|
449
|
+
modelAlias: options.modelAlias,
|
|
450
|
+
status: "already-dispatched",
|
|
451
|
+
command: shellCommand,
|
|
452
|
+
runId,
|
|
453
|
+
error: message,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// worktreePath is named per the spawner API contract but review-tickets passes
|
|
459
|
+
// the current repo cwd — no Worktrunk worktree is created.
|
|
460
|
+
const context = { key, worktreePath: deps.cwd };
|
|
461
|
+
try {
|
|
462
|
+
const result = await deps.spawnTerminalTab(deps, terminal, shellCommand, context);
|
|
463
|
+
if (result.ok) {
|
|
464
|
+
return {
|
|
465
|
+
key,
|
|
466
|
+
auto: mode.auto,
|
|
467
|
+
rounds: mode.rounds,
|
|
468
|
+
agentName: options.agentName,
|
|
469
|
+
modelAlias: options.modelAlias,
|
|
470
|
+
status: "spawned",
|
|
471
|
+
command: shellCommand,
|
|
472
|
+
runId,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
key,
|
|
477
|
+
auto: mode.auto,
|
|
478
|
+
rounds: mode.rounds,
|
|
479
|
+
agentName: options.agentName,
|
|
480
|
+
modelAlias: options.modelAlias,
|
|
481
|
+
status: "spawn-failed",
|
|
482
|
+
command: shellCommand,
|
|
483
|
+
runId,
|
|
484
|
+
error: result.error,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
catch (err) {
|
|
488
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
489
|
+
return {
|
|
490
|
+
key,
|
|
491
|
+
auto: mode.auto,
|
|
492
|
+
rounds: mode.rounds,
|
|
493
|
+
agentName: options.agentName,
|
|
494
|
+
modelAlias: options.modelAlias,
|
|
495
|
+
status: "spawn-failed",
|
|
496
|
+
command: shellCommand,
|
|
497
|
+
runId,
|
|
498
|
+
error: message,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
return { ok: true, rows };
|
|
503
|
+
}
|
|
504
|
+
// ---------------------------------------------------------------------------
|
|
505
|
+
// Summary formatting
|
|
506
|
+
// ---------------------------------------------------------------------------
|
|
507
|
+
export function formatReviewTicketsSummaryReport(rows) {
|
|
508
|
+
const lines = ["Summary:"];
|
|
509
|
+
for (const row of rows) {
|
|
510
|
+
const modelPart = row.modelAlias ? row.modelAlias : "default";
|
|
511
|
+
const runIdPart = row.runId ? ` run_id=${row.runId}` : "";
|
|
512
|
+
lines.push(`${row.key} auto=${row.auto} rounds=${row.rounds} agent=${row.agentName} model=${modelPart} status=${row.status}${runIdPart}`);
|
|
513
|
+
}
|
|
514
|
+
const failedRows = rows.filter((r) => r.status === "spawn-failed" || r.status === "already-dispatched");
|
|
515
|
+
if (failedRows.length > 0) {
|
|
516
|
+
lines.push("");
|
|
517
|
+
lines.push("Warnings:");
|
|
518
|
+
for (const row of failedRows) {
|
|
519
|
+
lines.push(` ${row.key}: ${row.error ?? row.status}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return lines.join("\n");
|
|
523
|
+
}
|
|
524
|
+
// ---------------------------------------------------------------------------
|
|
525
|
+
// Platform-specific spawn-failure hint
|
|
526
|
+
// ---------------------------------------------------------------------------
|
|
527
|
+
function reviewSpawnFailureHintForPlatform(platform) {
|
|
528
|
+
if (platform === "darwin") {
|
|
529
|
+
return ("One or more review tabs failed to spawn. If osascript reported a permission error, grant the " +
|
|
530
|
+
"required macOS permissions under System Settings -> Privacy & Security, then re-run: " +
|
|
531
|
+
"Automation (to let the terminal app be controlled) and Accessibility for the new-tab step.");
|
|
532
|
+
}
|
|
533
|
+
if (platform === "win32") {
|
|
534
|
+
return ("One or more review tabs failed to spawn. Ensure Windows Terminal (wt.exe) or PowerShell is " +
|
|
535
|
+
"installed and on PATH; see the per-ticket warnings above for details.");
|
|
536
|
+
}
|
|
537
|
+
if (platform === "linux") {
|
|
538
|
+
return ("One or more review sessions failed to spawn. Ensure tmux is installed and on PATH; see the " +
|
|
539
|
+
"per-ticket warnings above for details.");
|
|
540
|
+
}
|
|
541
|
+
return "One or more review tabs failed to spawn; see the per-ticket warnings above for details.";
|
|
542
|
+
}
|
|
543
|
+
// ---------------------------------------------------------------------------
|
|
544
|
+
// CLI wrapper
|
|
545
|
+
// ---------------------------------------------------------------------------
|
|
546
|
+
export async function runReviewTicketsCli(argv, overrides = {}) {
|
|
547
|
+
const log = (m) => console.log(m);
|
|
548
|
+
const errorLog = (m) => console.error(m);
|
|
549
|
+
const parsed = parseReviewTicketsArgs(argv);
|
|
550
|
+
if (parsed.status === "help") {
|
|
551
|
+
log(parsed.usage);
|
|
552
|
+
return 0;
|
|
553
|
+
}
|
|
554
|
+
if (parsed.status === "error") {
|
|
555
|
+
errorLog(`Error: ${parsed.message}`);
|
|
556
|
+
errorLog("");
|
|
557
|
+
errorLog(getReviewTicketsUsage());
|
|
558
|
+
return 1;
|
|
559
|
+
}
|
|
560
|
+
const options = parsed.options;
|
|
561
|
+
const deps = overrides.deps ?? createDefaultStartTicketsDeps();
|
|
562
|
+
if (options.dryRun) {
|
|
563
|
+
const agent = resolveAgentSpec(options.agentName);
|
|
564
|
+
if (!agent) {
|
|
565
|
+
errorLog(`Error: Unknown agent: '${options.agentName}'. Valid agents: ${formatValidAgentNames()}.`);
|
|
566
|
+
return 1;
|
|
567
|
+
}
|
|
568
|
+
const rows = buildReviewPlanRows(deps, options, agent, "dry-run", overrides.orchestrateOverrides);
|
|
569
|
+
for (const row of rows) {
|
|
570
|
+
log(`DRY-RUN: ${row.key} -> ${row.command}`);
|
|
571
|
+
}
|
|
572
|
+
log("");
|
|
573
|
+
log(formatReviewTicketsSummaryReport(rows));
|
|
574
|
+
return 0;
|
|
575
|
+
}
|
|
576
|
+
const result = await orchestrateReviewTickets(deps, options, overrides.orchestrateOverrides);
|
|
577
|
+
if (!result.ok) {
|
|
578
|
+
errorLog(`Error: ${result.error}`);
|
|
579
|
+
return 1;
|
|
580
|
+
}
|
|
581
|
+
log(formatReviewTicketsSummaryReport(result.rows));
|
|
582
|
+
const spawned = result.rows.filter((r) => r.status === "spawned");
|
|
583
|
+
if (deps.platform === "linux" && spawned.length > 0) {
|
|
584
|
+
log("");
|
|
585
|
+
log("Linux: each ticket runs in a detached tmux session. Attach with:");
|
|
586
|
+
for (const row of spawned) {
|
|
587
|
+
log(` tmux attach -t ${tmuxSessionNameForTicket(deps, row.key)}`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const spawnFailures = result.rows.filter((r) => r.status === "spawn-failed");
|
|
591
|
+
if (spawnFailures.length > 0) {
|
|
592
|
+
errorLog("");
|
|
593
|
+
errorLog(reviewSpawnFailureHintForPlatform(deps.platform));
|
|
594
|
+
}
|
|
595
|
+
return 0;
|
|
596
|
+
}
|
|
@@ -23,16 +23,16 @@ export function quotePromptToken(token) {
|
|
|
23
23
|
* the normalized `input.args`, plus (only where the command can parse them)
|
|
24
24
|
* `--scheduled-at <ISO>` and `--auto`.
|
|
25
25
|
*
|
|
26
|
-
* `--scheduled-at` is appended
|
|
27
|
-
*
|
|
28
|
-
* rejects unrecognized flags and halts (e.g. `start-tickets` stops
|
|
29
|
-
* unsupported flag), and the shared late-fire gate already embeds the
|
|
30
|
-
* time — so injecting `--scheduled-at` into their argv would break an
|
|
31
|
-
* `schedulable: true` command for no benefit.
|
|
26
|
+
* `--scheduled-at` is appended for `full-automation` (legacy) and `epic-tick`
|
|
27
|
+
* (BAPI-418), both of whose parsers accept it as a first-class argument. Every
|
|
28
|
+
* other command rejects unrecognized flags and halts (e.g. `start-tickets` stops
|
|
29
|
+
* on any unsupported flag), and the shared late-fire gate already embeds the
|
|
30
|
+
* scheduled time — so injecting `--scheduled-at` into their argv would break an
|
|
31
|
+
* otherwise `schedulable: true` command for no benefit.
|
|
32
32
|
*
|
|
33
33
|
* `--auto` is appended when auto-approve is set AND the command supports it (its
|
|
34
|
-
* schema declares a boolean `--auto` flag, or it is `full-automation`).
|
|
35
|
-
* never duplicated if already present in `input.args`.
|
|
34
|
+
* schema declares a boolean `--auto` flag, or it is `full-automation` / `epic-tick`).
|
|
35
|
+
* It is never duplicated if already present in `input.args`.
|
|
36
36
|
*
|
|
37
37
|
* This is the SINGLE source of the delegated argv: both the rendered target
|
|
38
38
|
* command line and the `$ARGUMENTS` body substitution derive from it, so the body
|
|
@@ -41,10 +41,16 @@ export function quotePromptToken(token) {
|
|
|
41
41
|
*/
|
|
42
42
|
export function buildAugmentedArgs(input) {
|
|
43
43
|
const args = [...input.args];
|
|
44
|
-
|
|
44
|
+
// Commands whose parsers accept --scheduled-at as a first-class argument.
|
|
45
|
+
// epic-tick already accepts --scheduled-at (parseEpicTickArgs, cli.ts) so it
|
|
46
|
+
// receives the scheduled time as a structured arg for the late-fire decision,
|
|
47
|
+
// not just via the embedded gate text.
|
|
48
|
+
if (input.commandName === "full-automation" || input.commandName === "epic-tick") {
|
|
45
49
|
args.push("--scheduled-at", input.scheduledAt);
|
|
46
50
|
}
|
|
47
|
-
const supportsAuto = schemaSupportsAutoFlag(input.schema) ||
|
|
51
|
+
const supportsAuto = schemaSupportsAutoFlag(input.schema) ||
|
|
52
|
+
input.commandName === "full-automation" ||
|
|
53
|
+
input.commandName === "epic-tick";
|
|
48
54
|
const alreadyHasAuto = input.args.includes("--auto");
|
|
49
55
|
if (input.autoApprove && supportsAuto && !alreadyHasAuto) {
|
|
50
56
|
args.push("--auto");
|